Add an invite users to community step to dialog flow
This commit is contained in:
parent
7c1a9993e3
commit
56c7f86983
7 changed files with 371 additions and 11 deletions
|
@ -72,6 +72,7 @@
|
|||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||
@import "./views/dialogs/_PrototypeCommunityInviteDialog.scss";
|
||||
@import "./views/dialogs/_PrototypeCreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||
|
|
88
res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss
Normal file
88
res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss
Normal 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_PrototypeCommunityInviteDialog {
|
||||
&.mx_Dialog_fixedWidth {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 0;
|
||||
|
||||
.mx_PrototypeCommunityInviteDialog_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_PrototypeCommunityInviteDialog_morePeople {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mx_PrototypeCommunityInviteDialog_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_PrototypeCommunityInviteDialog_personIdentifiers {
|
||||
display: inline-block;
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_PrototypeCommunityInviteDialog_personName {
|
||||
font-weight: 600;
|
||||
font-size: $font-14px;
|
||||
color: $primary-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_PrototypeCommunityInviteDialog_personId {
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_PrototypeCommunityInviteDialog_primaryButton {
|
||||
display: block;
|
||||
font-size: $font-13px;
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 PrototypeCommunityInviteDialog from "./components/views/dialogs/PrototypeCommunityInviteDialog";
|
||||
|
||||
/**
|
||||
* 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', '', PrototypeCommunityInviteDialog, {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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
252
src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx
Normal file
252
src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx
Normal 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 PrototypeCommunityInviteDialog 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_PrototypeCommunityInviteDialog_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_PrototypeCommunityInviteDialog_personIdentifiers">
|
||||
<span className="mx_PrototypeCommunityInviteDialog_personName">{person.user.name}</span>
|
||||
<span className="mx_PrototypeCommunityInviteDialog_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_PrototypeCommunityInviteDialog_morePeople"
|
||||
>{_t("Show more")}</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.state.people.length > 0) {
|
||||
peopleIntro = (
|
||||
<div className="mx_PrototypeCommunityInviteDialog_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_PrototypeCommunityInviteDialog"
|
||||
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_PrototypeCommunityInviteDialog_primaryButton"
|
||||
>{buttonText}</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ 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 {
|
||||
}
|
||||
|
@ -67,7 +68,7 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent<IPro
|
|||
// the background for the user to look at while they invite people.
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
let avatarUrl = null;
|
||||
let avatarUrl = ''; // must be a string for synapse to accept it
|
||||
if (this.state.avatarFile) {
|
||||
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
|
||||
}
|
||||
|
@ -87,11 +88,15 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent<IPro
|
|||
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',
|
||||
|
@ -99,8 +104,6 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent<IPro
|
|||
group_is_new: true,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Show invite dialog
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
|
|
|
@ -1729,6 +1729,15 @@
|
|||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -1783,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",
|
||||
|
@ -1898,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",
|
||||
|
|
Loading…
Reference in a new issue