mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge pull request #3815 from matrix-org/travis/ftue/user-lists/4-composer
Wire up the invite targets dialog to a real composer and show selections
This commit is contained in:
commit
ba73600cf0
@ -21,15 +21,51 @@ limitations under the License.
|
|||||||
.mx_DMInviteDialog_editor {
|
.mx_DMInviteDialog_editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%; // Needed to make the Field inside grow
|
width: 100%; // Needed to make the Field inside grow
|
||||||
|
background-color: $user-tile-hover-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 25px;
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile {
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field {
|
// Using a textarea for this element, to circumvent autofill
|
||||||
margin: 0;
|
// Mostly copied from AddressPickerDialog
|
||||||
|
textarea,
|
||||||
|
textarea:focus {
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: nowrap;
|
||||||
|
|
||||||
|
// Roughly fill about 2/5ths of the available space. This is to try and 'fill' the
|
||||||
|
// remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have
|
||||||
|
// support for "fill remaining width", but traditional tricks don't work with what
|
||||||
|
// we're pushing into this "field". Flexbox just makes things worse. The theory is
|
||||||
|
// that users won't need more than about 2/5ths of the input to find the person
|
||||||
|
// they're looking for.
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DMInviteDialog_goButton {
|
.mx_DMInviteDialog_goButton {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +93,43 @@ limitations under the License.
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_avatarStack {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_selected {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 36px;
|
||||||
|
background-color: $username-variant1-color;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||||
|
mask-size: 100%;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px; // 50%
|
||||||
|
left: 6px; // 50%
|
||||||
|
background-color: #ffffff; // this is fine without a var because it's for both themes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_DMInviteDialog_roomTile_name {
|
.mx_DMInviteDialog_roomTile_name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -83,3 +156,38 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog.
|
||||||
|
.mx_DMInviteDialog_userTile {
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_pill {
|
||||||
|
background-color: $username-variant1-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
color: #ffffff; // this is fine without a var because it's for both themes
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_avatar {
|
||||||
|
border-radius: 20px;
|
||||||
|
position: relative;
|
||||||
|
left: -5px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.mx_DMInviteDialog_userTile_avatar {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_name {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_remove {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
37
res/img/icon-email-pill-avatar.svg
Normal file
37
res/img/icon-email-pill-avatar.svg
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="69px" height="68px" viewBox="0 0 69 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 58 (84663) - https://sketch.com -->
|
||||||
|
<title>at-sign</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<filter x="-5.9%" y="-7.9%" width="111.8%" height="115.8%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="FTUE" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g id="FTUE---User-list-suggestions" transform="translate(-5161.000000, -1379.000000)" stroke="#368BD6">
|
||||||
|
<g id="Group-14-Copy-10" transform="translate(4748.000000, 1168.000000)">
|
||||||
|
<g id="Web-Copy-6" filter="url(#filter-1)">
|
||||||
|
<g id="modal-copy" transform="translate(253.000000, 118.000000)">
|
||||||
|
<g id="content" transform="translate(40.000000, 107.000000)">
|
||||||
|
<g id="Group-43">
|
||||||
|
<g id="Group-15-Copy-6" transform="translate(143.000000, 6.000000)">
|
||||||
|
<g id="at-sign" transform="translate(5.500000, 6.500000)">
|
||||||
|
<circle id="Oval" cx="6.28571429" cy="5.71428571" r="2.28571429"></circle>
|
||||||
|
<path d="M8.57142857,3.42857143 L8.57142857,6.28571429 C8.57142857,7.23248814 9.33894043,8 10.2857143,8 C11.2324881,8 12,7.23248814 12,6.28571429 L12,5.71428571 C11.9998328,3.05880261 10.1703625,0.75337961 7.58436487,0.149884297 C4.9983672,-0.453611015 2.33741804,0.803881013 1.16186053,3.18498476 C-0.0136969889,5.5660885 0.605971794,8.4432307 2.65750183,10.1292957 C4.70903186,11.8153606 7.65171364,11.8659623 9.76,10.2514286" id="Path"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
res/img/icon-pill-remove.svg
Normal file
1
res/img/icon-pill-remove.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="58" height="60" viewBox="26 25 6 6" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-5.9%" y="-7.9%" width="111.8%" height="115.8%" filterUnits="objectBoundingBox" id="a"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g filter="url(#a)" transform="translate(-406 -215)" stroke="#61708B"><path d="M438 240l-6 6M432 240l6 6"/></g></svg>
|
After Width: | Height: | Size: 693 B |
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
@ -31,18 +31,44 @@ import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo";
|
|||||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||||
|
|
||||||
class DirectoryMember {
|
// This is the interface that is expected by various components in this file. It is a bit
|
||||||
|
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
|
// for 3PIDs/email addresses.
|
||||||
|
//
|
||||||
|
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||||
|
class Member {
|
||||||
|
/**
|
||||||
|
* The display name of this Member. For users this should be their profile's display
|
||||||
|
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||||
|
*/
|
||||||
|
get name(): string { throw new Error("Member class not implemented"); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||||
|
* be the 3PID address (email).
|
||||||
|
*/
|
||||||
|
get userId(): string { throw new Error("Member class not implemented"); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||||
|
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||||
|
*/
|
||||||
|
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryMember extends Member {
|
||||||
_userId: string;
|
_userId: string;
|
||||||
_displayName: string;
|
_displayName: string;
|
||||||
_avatarUrl: string;
|
_avatarUrl: string;
|
||||||
|
|
||||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||||
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this._displayName = userDirResult.display_name;
|
this._displayName = userDirResult.display_name;
|
||||||
this._avatarUrl = userDirResult.avatar_url;
|
this._avatarUrl = userDirResult.avatar_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These next members are to implement the contract expected by DMRoomTile
|
// These next class members are for the Member interface
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this._displayName || this._userId;
|
return this._displayName || this._userId;
|
||||||
}
|
}
|
||||||
@ -56,13 +82,64 @@ class DirectoryMember {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DMUserTile extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||||
|
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||||
|
};
|
||||||
|
|
||||||
|
_onRemove = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onRemove(this.props.member);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
|
const avatarSize = 20;
|
||||||
|
const avatar = this.props.member.isEmail
|
||||||
|
? <img
|
||||||
|
className='mx_DMInviteDialog_userTile_avatar'
|
||||||
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
|
width={avatarSize} height={avatarSize} />
|
||||||
|
: <BaseAvatar
|
||||||
|
className='mx_DMInviteDialog_userTile_avatar'
|
||||||
|
url={getHttpUriForMxc(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||||
|
avatarSize, avatarSize, "crop")}
|
||||||
|
name={this.props.member.name}
|
||||||
|
idName={this.props.member.userId}
|
||||||
|
width={avatarSize}
|
||||||
|
height={avatarSize} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='mx_DMInviteDialog_userTile'>
|
||||||
|
<span className='mx_DMInviteDialog_userTile_pill'>
|
||||||
|
{avatar}
|
||||||
|
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className='mx_DMInviteDialog_userTile_remove'
|
||||||
|
onClick={this._onRemove}
|
||||||
|
>
|
||||||
|
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DMRoomTile extends React.PureComponent {
|
class DMRoomTile extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// Has properties to match RoomMember: userId (str), name (str), getMxcAvatarUrl(): string
|
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||||
member: PropTypes.object.isRequired,
|
|
||||||
lastActiveTs: PropTypes.number,
|
lastActiveTs: PropTypes.number,
|
||||||
onToggle: PropTypes.func.isRequired,
|
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
|
||||||
highlightWord: PropTypes.string,
|
highlightWord: PropTypes.string,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClick = (e) => {
|
_onClick = (e) => {
|
||||||
@ -70,7 +147,7 @@ class DMRoomTile extends React.PureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
this.props.onToggle(this.props.member.userId);
|
this.props.onToggle(this.props.member);
|
||||||
};
|
};
|
||||||
|
|
||||||
_highlightName(str: string) {
|
_highlightName(str: string) {
|
||||||
@ -121,19 +198,37 @@ class DMRoomTile extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const avatarSize = 36;
|
const avatarSize = 36;
|
||||||
const avatarUrl = getHttpUriForMxc(
|
const avatar = this.props.member.isEmail
|
||||||
|
? <img
|
||||||
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
|
width={avatarSize} height={avatarSize} />
|
||||||
|
: <BaseAvatar
|
||||||
|
url={getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||||
avatarSize, avatarSize, "crop");
|
avatarSize, avatarSize, "crop")}
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
|
||||||
<BaseAvatar
|
|
||||||
url={avatarUrl}
|
|
||||||
name={this.props.member.name}
|
name={this.props.member.name}
|
||||||
idName={this.props.member.userId}
|
idName={this.props.member.userId}
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
height={avatarSize}
|
height={avatarSize} />;
|
||||||
/>
|
|
||||||
|
let checkmark = null;
|
||||||
|
if (this.props.isSelected) {
|
||||||
|
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||||
|
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||||
|
// the browser from reloading the image source when the avatar remounts).
|
||||||
|
const stackedAvatar = (
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
|
||||||
|
{avatar}
|
||||||
|
{checkmark}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||||
|
{stackedAvatar}
|
||||||
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||||
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
@ -149,12 +244,13 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_debounceTimer: number = null;
|
_debounceTimer: number = null;
|
||||||
|
_editorRef: any = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
targets: [], // string[] of mxids/email addresses
|
targets: [], // array of Member objects (see interface above)
|
||||||
filterText: "",
|
filterText: "",
|
||||||
recents: this._buildRecents(),
|
recents: this._buildRecents(),
|
||||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
@ -162,6 +258,8 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._editorRef = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||||
@ -245,7 +343,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startDm = () => {
|
_startDm = () => {
|
||||||
this.props.onFinished(this.state.targets);
|
this.props.onFinished(this.state.targets.map(t => t.userId));
|
||||||
};
|
};
|
||||||
|
|
||||||
_cancel = () => {
|
_cancel = () => {
|
||||||
@ -292,14 +390,33 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
};
|
};
|
||||||
|
|
||||||
_toggleMember = (userId) => {
|
_toggleMember = (member: Member) => {
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
const idx = targets.indexOf(userId);
|
const idx = targets.indexOf(member);
|
||||||
if (idx >= 0) targets.splice(idx, 1);
|
if (idx >= 0) targets.splice(idx, 1);
|
||||||
else targets.push(userId);
|
else targets.push(member);
|
||||||
this.setState({targets});
|
this.setState({targets});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_removeMember = (member: Member) => {
|
||||||
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
const idx = targets.indexOf(member);
|
||||||
|
if (idx >= 0) {
|
||||||
|
targets.splice(idx, 1);
|
||||||
|
this.setState({targets});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onClickInputArea = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this._editorRef && this._editorRef.current) {
|
||||||
|
this._editorRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_renderSection(kind: "recents"|"suggestions") {
|
_renderSection(kind: "recents"|"suggestions") {
|
||||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||||
@ -360,6 +477,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
key={r.userId}
|
key={r.userId}
|
||||||
onToggle={this._toggleMember}
|
onToggle={this._toggleMember}
|
||||||
highlightWord={this.state.filterText}
|
highlightWord={this.state.filterText}
|
||||||
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
@ -371,23 +489,30 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
_renderEditor() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const targets = this.state.targets.map(t => (
|
||||||
const Field = sdk.getComponent("elements.Field");
|
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
));
|
||||||
|
const input = (
|
||||||
// Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197
|
<textarea
|
||||||
// For now, we just list who the targets are.
|
key={"input"}
|
||||||
const editor = (
|
rows={1}
|
||||||
<div className='mx_DMInviteDialog_editor'>
|
|
||||||
<Field
|
|
||||||
id="inviteTargets"
|
|
||||||
value={this.state.filterText}
|
|
||||||
onChange={this._updateFilter}
|
onChange={this._updateFilter}
|
||||||
|
defaultValue={this.state.filterText}
|
||||||
|
ref={this._editorRef}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
|
||||||
|
{targets}
|
||||||
|
{input}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
const targets = this.state.targets.map(t => <div key={t}>{t}</div>);
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
const userId = MatrixClientPeg.get().getUserId();
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
return (
|
return (
|
||||||
@ -406,9 +531,8 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{targets}
|
|
||||||
<div className='mx_DMInviteDialog_addressBar'>
|
<div className='mx_DMInviteDialog_addressBar'>
|
||||||
{editor}
|
{this._renderEditor()}
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
onClick={this._startDm}
|
onClick={this._startDm}
|
||||||
|
Loading…
Reference in New Issue
Block a user