diff --git a/res/css/_components.scss b/res/css/_components.scss index 5145133127..60725ff5d4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -72,6 +72,7 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_PrototypeCreateGroupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @@ -106,6 +107,7 @@ @import "./views/elements/_FormButton.scss"; @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; +@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_PowerSelector.scss"; diff --git a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss new file mode 100644 index 0000000000..a0f505b1ff --- /dev/null +++ b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss @@ -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; + } + } + } + } +} diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss new file mode 100644 index 0000000000..5858a60629 --- /dev/null +++ b/res/css/views/elements/_InfoTooltip.scss @@ -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'); +} diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg new file mode 100644 index 0000000000..bde5253bea --- /dev/null +++ b/res/img/element-icons/add-photo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg new file mode 100644 index 0000000000..b5769074ab --- /dev/null +++ b/res/img/element-icons/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a10af429b9..2b764d00c9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -77,6 +77,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { leaveRoomBehaviour } from "../../utils/membership"; +import PrototypeCreateGroupDialog from "../views/dialogs/PrototypeCreateGroupDialog"; /** constants for MatrixChat.state.view */ export enum Views { @@ -620,7 +621,10 @@ export default class MatrixChat extends React.PureComponent { this.createRoom(payload.public); break; 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); break; } diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts new file mode 100644 index 0000000000..1027ca7607 --- /dev/null +++ b/src/components/views/dialogs/IDialogProps.ts @@ -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; +} diff --git a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx new file mode 100644 index 0000000000..7427b2737c --- /dev/null +++ b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx @@ -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 { + private avatarUploadRef: React.RefObject = React.createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + name: "", + localpart: "", + error: null, + busy: false, + avatarFile: null, + avatarPreview: null, + }; + } + + private onNameChange = (ev: ChangeEvent) => { + 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) => { + 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) => { + 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 = ( + + {_t("Community ID: +:%(domain)s", { + domain: MatrixClientPeg.getHomeserverName(), + }, { + localpart: () => {this.state.localpart}, + })} + + + ); + } + + let helpText = ( + + {_t("You can change this later if needed.")} + + ); + if (this.state.error) { + helpText = ( + + {this.state.error} + + ); + } + + let preview = ; + if (!this.state.avatarPreview) { + preview =
+ } + + return ( + +
+
+
+ + {helpText} + + {/*nbsp is to reserve the height of this element when there's nothing*/} +  {communityId} + + + {_t("Create")} + +
+
+ + + {preview} + +
+ {_t("PRO TIP")} + + {_t("An image will help people identify your community.")} + +
+
+
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx index f6767dcb8d..81f628343b 100644 --- a/src/components/views/dialogs/ServerOfflineDialog.tsx +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IDialogProps } from "./IDialogProps"; -interface IProps { - onFinished: (bool) => void; +interface IProps extends IDialogProps { } export default class ServerOfflineDialog extends React.PureComponent { diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index dc2a987f13..22f83d391c 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -31,6 +31,7 @@ import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext, selectText} from "../../../utils/strings"; import StyledCheckbox from '../elements/StyledCheckbox'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { IDialogProps } from "./IDialogProps"; const socials = [ { @@ -60,8 +61,7 @@ const socials = [ }, ]; -interface IProps { - onFinished: () => void; +interface IProps extends IDialogProps { target: Room | User | Group | RoomMember | MatrixEvent; permalinkCreator: RoomPermalinkCreator; } diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx new file mode 100644 index 0000000000..645951aab9 --- /dev/null +++ b/src/components/views/elements/InfoTooltip.tsx @@ -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 { + 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 ? :
; + return ( +
+ + {children} + {tip} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d052ba5a51..9a246a421d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1484,6 +1484,7 @@ "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", + "Information": "Information", "Language Dropdown": "Language Dropdown", "Manage Integrations": "Manage Integrations", "%(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:", "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", "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: +:%(domain)s": "Community ID: +:%(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.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback",