Merge pull request #5144 from matrix-org/travis/communities/proto/create-flow

Communities v2 prototype: Create community flow
pull/21833/head
Travis Ralston 2020-08-26 09:07:17 -06:00 committed by GitHub
commit 82949c038a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 870 additions and 19 deletions

View File

@ -61,7 +61,9 @@
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@ -106,6 +108,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";

View File

@ -0,0 +1,88 @@
/*
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_CommunityPrototypeInviteDialog {
&.mx_Dialog_fixedWidth {
width: 360px;
}
.mx_Dialog_content {
margin-bottom: 0;
.mx_CommunityPrototypeInviteDialog_people {
position: relative;
margin-bottom: 4px;
.mx_AccessibleButton {
display: inline-block;
background-color: $focus-bg-color; // XXX: Abuse of variables
border-radius: 4px;
padding: 3px 5px;
font-size: $font-12px;
float: right;
}
}
.mx_CommunityPrototypeInviteDialog_morePeople {
margin-top: 8px;
}
.mx_CommunityPrototypeInviteDialog_person {
position: relative;
margin-top: 4px;
& > * {
vertical-align: middle;
}
.mx_Checkbox {
position: absolute;
right: 0;
top: calc(50% - 8px); // checkbox is 16px high
width: 16px; // to force a square
}
.mx_CommunityPrototypeInviteDialog_personIdentifiers {
display: inline-block;
& > * {
display: block;
}
.mx_CommunityPrototypeInviteDialog_personName {
font-weight: 600;
font-size: $font-14px;
color: $primary-fg-color;
margin-left: 7px;
}
.mx_CommunityPrototypeInviteDialog_personId {
font-size: $font-12px;
color: $muted-fg-color;
margin-left: 7px;
}
}
}
.mx_CommunityPrototypeInviteDialog_primaryButton {
display: block;
font-size: $font-13px;
line-height: 20px;
height: 20px;
margin-top: 24px;
}
}
}

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_CreateCommunityPrototypeDialog {
.mx_Dialog_content {
display: flex;
flex-direction: row;
margin-bottom: 12px;
.mx_CreateCommunityPrototypeDialog_colName {
flex-basis: 66.66%;
padding-right: 100px;
.mx_Field input {
font-size: $font-16px;
line-height: $font-20px;
}
.mx_CreateCommunityPrototypeDialog_subtext {
display: block;
color: $muted-fg-color;
margin-bottom: 16px;
&:last-child {
margin-top: 16px;
}
&.mx_CreateCommunityPrototypeDialog_subtext_error {
color: $warning-color;
}
}
.mx_CreateCommunityPrototypeDialog_communityId {
position: relative;
.mx_InfoTooltip {
float: right;
}
}
.mx_AccessibleButton {
display: block;
height: 32px;
font-size: $font-16px;
line-height: 32px;
}
}
.mx_CreateCommunityPrototypeDialog_colAvatar {
flex-basis: 33.33%;
.mx_CreateCommunityPrototypeDialog_avatarContainer {
margin-top: 12px;
margin-bottom: 20px;
.mx_CreateCommunityPrototypeDialog_avatar,
.mx_CreateCommunityPrototypeDialog_placeholderAvatar {
width: 96px;
height: 96px;
border-radius: 96px;
}
.mx_CreateCommunityPrototypeDialog_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_CreateCommunityPrototypeDialog_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

@ -23,6 +23,7 @@ import Modal from './Modal';
import * as sdk from './';
import { _t } from './languageHandler';
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
/**
* Invites multiple addresses to a room
@ -56,6 +57,13 @@ export function showRoomInviteDialog(roomId) {
);
}
export function showCommunityRoomInviteDialog(roomId, communityName) {
Modal.createTrackedDialog(
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
}
/**
* Checks if the given MatrixEvent is a valid 3rd party user invite.
* @param {MatrixEvent} event The event to check
@ -77,7 +85,7 @@ export function isValid3pidInvite(event) {
export function inviteUsersToRoom(roomId, userIds) {
return inviteMultipleToRoom(roomId, userIds).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
return _showAnyInviteErrors(result.states, room, result.inviter);
showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -88,7 +96,7 @@ export function inviteUsersToRoom(roomId, userIds) {
});
}
function _showAnyInviteErrors(addrs, room, inviter) {
export function showAnyInviteErrors(addrs, room, inviter) {
// Show user any errors
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
if (failedUsers.length === 1 && inviter.fatal) {
@ -100,6 +108,7 @@ function _showAnyInviteErrors(addrs, room, inviter) {
title: _t("Failed to invite users to the room:", {roomName: room.name}),
description: inviter.getErrorText(failedUsers[0]),
});
return false;
} else {
const errorList = [];
for (const addr of failedUsers) {
@ -118,8 +127,9 @@ function _showAnyInviteErrors(addrs, room, inviter) {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description,
});
return false;
}
}
return addrs;
return true;
}

View File

@ -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 CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
/** constants for MatrixChat.state.view */
export enum Views {
@ -620,7 +621,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
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 = CreateCommunityPrototypeDialog;
}
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
break;
}

View File

@ -146,6 +146,24 @@ const TagPanel = createReactClass({
mx_TagPanel_items_selected: itemsSelected,
});
let createButton = (
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
createButton = (
<ActionButton
tooltip
label={_t("Create community")}
action="view_create_group"
className="mx_TagTile mx_TagTile_plus" />
);
}
return <div className={classes}>
<div className="mx_TagPanel_clearButton_container">
{ clearButton }
@ -168,11 +186,7 @@ const TagPanel = createReactClass({
{ this.renderGlobalIcon() }
{ tags }
<div>
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
{createButton}
</div>
{ provided.placeholder }
</div>

View File

@ -0,0 +1,252 @@
/*
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, FormEvent } 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";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import { arrayFastClone } from "../../../utils/arrays";
import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import {humanizeTime} from "../../../utils/humanize";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
interface IProps extends IDialogProps {
roomId: string;
communityName: string;
}
interface IPerson {
userId: string;
user: RoomMember;
lastActive: number;
}
interface IState {
emailTargets: string[];
userTargets: string[];
showPeople: boolean;
people: IPerson[];
numPeople: number;
busy: boolean;
}
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
emailTargets: [],
userTargets: [],
showPeople: false,
people: this.buildSuggestions(),
numPeople: 5, // arbitrary default
busy: false,
};
}
private buildSuggestions(): IPerson[] {
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
if (this.props.roomId) {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
return InviteDialog.buildRecents(alreadyInvited);
}
private onSubmit = async (ev: FormEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const success = showAnyInviteErrors(result.states, room, result.inviter);
if (success) {
this.props.onFinished(true);
} else {
this.setState({busy: false});
}
} catch (e) {
this.setState({busy: false});
console.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
}
};
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) {
targets.push(ev.target.value);
} else {
targets[index] = ev.target.value;
}
this.setState({emailTargets: targets});
};
private onAddressBlur = (index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({emailTargets: targets});
}
};
private onShowPeopleClick = () => {
this.setState({showPeople: !this.state.showPeople});
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
const targets = arrayFastClone(this.state.userTargets);
if (selected && !targets.includes(person.userId)) {
targets.push(person.userId);
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({userTargets: targets});
};
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}
height={avatarSize}
/>
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
<span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
<span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
</div>
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
</div>
);
}
private onShowMorePeople = () => {
this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
};
public render() {
const emailAddresses = [];
this.state.emailTargets.forEach((address, i) => {
emailAddresses.push(
<Field
key={i}
value={address}
onChange={(e) => this.onAddressChange(e, i)}
label={_t("Email address")}
placeholder={_t("Email address")}
onBlur={() => this.onAddressBlur(i)}
/>
);
});
// Push a clean input
emailAddresses.push(
<Field
key={emailAddresses.length}
value={""}
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
/>
);
let peopleIntro = null;
let people = [];
if (this.state.showPeople) {
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
humansToPresent.forEach((person, i) => {
people.push(this.renderPerson(person, i));
});
if (humansToPresent.length < this.state.people.length) {
people.push(
<AccessibleButton
onClick={this.onShowMorePeople}
kind="link" key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople"
>{_t("Show more")}</AccessibleButton>
);
}
}
if (this.state.people.length > 0) {
peopleIntro = (
<div className="mx_CommunityPrototypeInviteDialog_people">
<span>{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})}</span>
<AccessibleButton onClick={this.onShowPeopleClick}>
{this.state.showPeople ? _t("Hide") : _t("Show")}
</AccessibleButton>
</div>
);
}
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", {count: targetCount});
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
{emailAddresses}
{peopleIntro}
{people}
<AccessibleButton
kind="primary" onClick={this.onSubmit}
disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton"
>{buttonText}</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View File

@ -0,0 +1,220 @@
/*
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";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
export default class CreateCommunityPrototypeDialog 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 = ''; // must be a string for synapse to accept it
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,
});
// Close our own dialog before moving much further
this.props.onFinished(true);
if (result.room_id) {
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
});
showCommunityRoomInviteDialog(result.room_id, this.state.name);
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
} 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_CreateCommunityPrototypeDialog_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_CreateCommunityPrototypeDialog_subtext">
{_t("You can change this later if needed.")}
</span>
);
if (this.state.error) {
helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error">
{this.state.error}
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
}
return (
<BaseDialog
className="mx_CreateCommunityPrototypeDialog"
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_CreateCommunityPrototypeDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{helpText}
<span className="mx_CreateCommunityPrototypeDialog_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_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton onClick={this.onChangeAvatar} className="mx_CreateCommunityPrototypeDialog_avatarContainer">
{preview}
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{_t("Add image (optional)")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View File

@ -88,6 +88,13 @@ export default createReactClass({
action: 'view_room',
room_id: result.room_id,
});
// 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,
});
} else {
dis.dispatch({
action: 'view_group',

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

@ -327,7 +327,7 @@ export default class InviteDialog extends React.PureComponent {
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: "",
recents: this._buildRecents(alreadyInvited),
recents: InviteDialog.buildRecents(alreadyInvited),
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this._buildSuggestions(alreadyInvited),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
@ -344,7 +344,7 @@ export default class InviteDialog extends React.PureComponent {
this._editorRef = createRef();
}
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the

View File

@ -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<IProps> {

View File

@ -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;
}

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 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",
@ -1597,6 +1598,15 @@
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
"Unavailable": "Unavailable",
"Changelog": "Changelog",
"Email address": "Email address",
"Add another email": "Add another email",
"People you know on %(brand)s": "People you know on %(brand)s",
"Hide": "Hide",
"Show": "Show",
"Skip": "Skip",
"Send %(count)s invites|other": "Send %(count)s invites",
"Send %(count)s invites|one": "Send %(count)s invite",
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
"You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Removing…": "Removing…",
"Destroy cross-signing keys?": "Destroy cross-signing keys?",
@ -1607,6 +1617,15 @@
"Clear all data in this session?": "Clear all data in this session?",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.",
"Clear all data": "Clear all data",
"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",
"Create": "Create",
"Add image (optional)": "Add image (optional)",
"An image will help people identify your community.": "An image will help people identify your community.",
"Community IDs cannot be empty.": "Community IDs cannot be empty.",
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
"Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
@ -1615,7 +1634,6 @@
"Example": "Example",
"Community ID": "Community ID",
"example": "example",
"Create": "Create",
"Please enter a name for the room": "Please enter a name for the room",
"Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.",
"This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.",
@ -1774,9 +1792,7 @@
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
"Verification Pending": "Verification Pending",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
"Email address": "Email address",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip",
"A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'",
"Username not available": "Username not available",
"Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
@ -1889,7 +1905,6 @@
"Set status": "Set status",
"Set a new status...": "Set a new status...",
"View Community": "View Community",
"Hide": "Hide",
"Reload": "Reload",
"Take picture": "Take picture",
"Remove for everyone": "Remove for everyone",
@ -2091,6 +2106,7 @@
"Click to mute video": "Click to mute video",
"Click to unmute audio": "Click to unmute audio",
"Click to mute audio": "Click to mute audio",
"Create community": "Create community",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",