Add new create group dialog

This commit is contained in:
Travis Ralston 2020-08-25 13:58:15 -06:00
parent 8feda74156
commit 7c1a9993e3
12 changed files with 474 additions and 5 deletions

View file

@ -72,6 +72,7 @@
@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_PrototypeCreateGroupDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss";
@ -106,6 +107,7 @@
@import "./views/elements/_FormButton.scss"; @import "./views/elements/_FormButton.scss";
@import "./views/elements/_IconButton.scss"; @import "./views/elements/_IconButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InfoTooltip.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_PowerSelector.scss";

View file

@ -0,0 +1,102 @@
/*
Copyright 2020 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_PrototypeCreateGroupDialog {
.mx_Dialog_content {
display: flex;
flex-direction: row;
margin-bottom: 12px;
.mx_PrototypeCreateGroupDialog_colName {
flex-basis: 66.66%;
padding-right: 100px;
.mx_Field input {
font-size: $font-16px;
line-height: $font-20px;
}
.mx_PrototypeCreateGroupDialog_subtext {
display: block;
color: $muted-fg-color;
margin-bottom: 16px;
&:last-child {
margin-top: 16px;
}
&.mx_PrototypeCreateGroupDialog_subtext_error {
color: $warning-color;
}
}
.mx_PrototypeCreateGroupDialog_communityId {
position: relative;
.mx_InfoTooltip {
float: right;
}
}
.mx_AccessibleButton {
display: block;
height: 32px;
font-size: $font-16px;
line-height: 32px;
}
}
.mx_PrototypeCreateGroupDialog_colAvatar {
flex-basis: 33.33%;
.mx_PrototypeCreateGroupDialog_avatarContainer {
margin-top: 12px;
margin-bottom: 20px;
.mx_PrototypeCreateGroupDialog_avatar,
.mx_PrototypeCreateGroupDialog_placeholderAvatar {
width: 96px;
height: 96px;
border-radius: 96px;
}
.mx_PrototypeCreateGroupDialog_placeholderAvatar {
background-color: #368BD6; // hardcoded for both themes
&::before {
display: inline-block;
background-color: #fff; // hardcoded because the background is
mask-repeat: no-repeat;
mask-size: 96px;
width: 96px;
height: 96px;
mask-position: center;
content: '';
vertical-align: middle;
mask-image: url('$(res)/img/element-icons/add-photo.svg');
}
}
}
.mx_PrototypeCreateGroupDialog_tip {
&>b, &>span {
display: block;
color: $muted-fg-color;
}
}
}
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2020 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_InfoTooltip_icon {
width: 16px;
height: 16px;
display: inline-block;
}
.mx_InfoTooltip_icon::before {
display: inline-block;
background-color: $muted-fg-color;
mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
mask-position: center;
content: '';
vertical-align: middle;
mask-image: url('$(res)/img/element-icons/info.svg');
}

View file

@ -0,0 +1,5 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7988 34.9062C37.077 33.5217 38.2978 32.5 39.7396 32.5H44.2604C45.7022 32.5 46.923 33.5217 47.2012 34.9062C47.2429 35.1137 47.3232 35.3141 47.4627 35.4731L48.0649 36.1595C48.2548 36.3759 48.5287 36.5 48.8166 36.5H52C53.1046 36.5 54 37.3954 54 38.5V49.5C54 50.6046 53.1046 51.5 52 51.5H32C30.8954 51.5 30 50.6046 30 49.5V38.5C30 37.3954 30.8954 36.5 32 36.5H35.1834C35.4713 36.5 35.7452 36.3759 35.9351 36.1595L36.5373 35.4731C36.6768 35.3141 36.7571 35.1137 36.7988 34.9062ZM42 47.5C44.2091 47.5 46 45.7091 46 43.5C46 41.2909 44.2091 39.5 42 39.5C39.7909 39.5 38 41.2909 38 43.5C38 45.7091 39.7909 47.5 42 47.5Z" fill="white"/>
<rect x="32" y="35" width="3" height="1" rx="0.5" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.75 27C59.75 26.5858 59.4142 26.25 59 26.25C58.5858 26.25 58.25 26.5858 58.25 27V31.25L54 31.25C53.5858 31.25 53.25 31.5858 53.25 32C53.25 32.4142 53.5858 32.75 54 32.75L58.25 32.75V37C58.25 37.4142 58.5858 37.75 59 37.75C59.4142 37.75 59.75 37.4142 59.75 37V32.75L64 32.75C64.4142 32.75 64.75 32.4142 64.75 32C64.75 31.5858 64.4142 31.25 64 31.25L59.75 31.25V27Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="9.5" stroke="#787878"/>
<path d="M9.79248 14H11.2065V8H9.79248V14ZM10.5034 7.14844C10.9526 7.14844 11.3198 6.80469 11.3198 6.38281C11.3198 5.95703 10.9526 5.61328 10.5034 5.61328C10.0503 5.61328 9.68311 5.95703 9.68311 6.38281C9.68311 6.80469 10.0503 7.14844 10.5034 7.14844Z" fill="#787878"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

View file

@ -77,6 +77,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel"; import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership"; import { leaveRoomBehaviour } from "../../utils/membership";
import PrototypeCreateGroupDialog from "../views/dialogs/PrototypeCreateGroupDialog";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -620,7 +621,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.createRoom(payload.public); this.createRoom(payload.public);
break; break;
case 'view_create_group': { case 'view_create_group': {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = PrototypeCreateGroupDialog;
}
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
break; break;
} }

View file

@ -0,0 +1,19 @@
/*
Copyright 2020 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.
*/
export interface IDialogProps {
onFinished: (bool) => void;
}

View file

@ -0,0 +1,217 @@
/*
Copyright 2020 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, { ChangeEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
export default class PrototypeCreateGroupDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
this.state = {
name: "",
localpart: "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({name: ev.target.value, localpart});
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
try {
let avatarUrl = null;
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
const result = await MatrixClientPeg.get().createGroup({
localpart: this.state.localpart,
profile: {
name: this.state.name,
avatar_url: avatarUrl,
},
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({action: 'deselect_tags'}, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
});
if (result.room_id) {
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
});
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
// TODO: Show invite dialog
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t(
"There was an error creating your community. The name may be taken or the " +
"server is unable to process your request.",
),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
} else {
this.setState({busy: true});
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let communityId = null;
if (this.state.localpart) {
communityId = (
<span className="mx_PrototypeCreateGroupDialog_communityId">
{_t("Community ID: +<localpart />:%(domain)s", {
domain: MatrixClientPeg.getHomeserverName(),
}, {
localpart: () => <u>{this.state.localpart}</u>,
})}
<InfoTooltip
tooltip={_t(
"Use this when referencing your community to others. The community ID " +
"cannot be changed.",
)}
/>
</span>
);
}
let helpText = (
<span className="mx_PrototypeCreateGroupDialog_subtext">
{_t("You can change this later if needed.")}
</span>
);
if (this.state.error) {
helpText = (
<span className="mx_PrototypeCreateGroupDialog_subtext mx_PrototypeCreateGroupDialog_subtext_error">
{this.state.error}
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_PrototypeCreateGroupDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_PrototypeCreateGroupDialog_placeholderAvatar" />
}
return (
<BaseDialog
className="mx_PrototypeCreateGroupDialog"
onFinished={this.props.onFinished}
title={_t("What's the name of your community or team?")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_PrototypeCreateGroupDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{helpText}
<span className="mx_PrototypeCreateGroupDialog_subtext">
{/*nbsp is to reserve the height of this element when there's nothing*/}
&nbsp;{communityId}
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{_t("Create")}
</AccessibleButton>
</div>
<div className="mx_PrototypeCreateGroupDialog_colAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton onClick={this.onChangeAvatar} className="mx_PrototypeCreateGroupDialog_avatarContainer">
{preview}
</AccessibleButton>
<div className="mx_PrototypeCreateGroupDialog_tip">
<b>{_t("PRO TIP")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
interface IProps { interface IProps extends IDialogProps {
onFinished: (bool) => void;
} }
export default class ServerOfflineDialog extends React.PureComponent<IProps> { export default class ServerOfflineDialog extends React.PureComponent<IProps> {

View file

@ -31,6 +31,7 @@ import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings"; import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox'; import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
const socials = [ const socials = [
{ {
@ -60,8 +61,7 @@ const socials = [
}, },
]; ];
interface IProps { interface IProps extends IDialogProps {
onFinished: () => void;
target: Room | User | Group | RoomMember | MatrixEvent; target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
} }

View file

@ -0,0 +1,73 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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 from 'react';
import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import { _t } from "../../../languageHandler";
interface ITooltipProps {
tooltip?: React.ReactNode;
tooltipClassName?: string;
}
interface IState {
hover: boolean;
}
export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseLeave = () => {
this.setState({
hover: false,
});
};
render() {
const {tooltip, children, tooltipClassName} = this.props;
const title = _t("Information");
// Tooltip are forced on the right for a more natural feel to them on info icons
const tip = this.state.hover ? <Tooltip
className="mx_InfoTooltip_container"
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
label={tooltip || title}
forceOnRight={true}
/> : <div />;
return (
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
<span className="mx_InfoTooltip_icon" aria-label={title} />
{children}
{tip}
</div>
);
}
}

View file

@ -1484,6 +1484,7 @@
"Rotate Right": "Rotate Right", "Rotate Right": "Rotate Right",
"Rotate clockwise": "Rotate clockwise", "Rotate clockwise": "Rotate clockwise",
"Download this file": "Download this file", "Download this file": "Download this file",
"Information": "Information",
"Language Dropdown": "Language Dropdown", "Language Dropdown": "Language Dropdown",
"Manage Integrations": "Manage Integrations", "Manage Integrations": "Manage Integrations",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
@ -1728,6 +1729,14 @@
"Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
"If you didnt sign in to this session, your account may be compromised.": "If you didnt sign in to this session, your account may be compromised.", "If you didnt sign in to this session, your account may be compromised.": "If you didnt sign in to this session, your account may be compromised.",
"This wasn't me": "This wasn't me", "This wasn't me": "This wasn't me",
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.",
"Community ID: +<localpart />:%(domain)s": "Community ID: +<localpart />:%(domain)s",
"Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.",
"You can change this later if needed.": "You can change this later if needed.",
"What's the name of your community or team?": "What's the name of your community or team?",
"Enter name": "Enter name",
"PRO TIP": "PRO TIP",
"An image will help people identify your community.": "An image will help people identify your community.",
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.",
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.", "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback", "Report bugs & give feedback": "Report bugs & give feedback",