diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss
index 364c796f16..eaad74d581 100644
--- a/res/css/views/dialogs/_DMInviteDialog.scss
+++ b/res/css/views/dialogs/_DMInviteDialog.scss
@@ -21,15 +21,51 @@ limitations under the License.
.mx_DMInviteDialog_editor {
flex: 1;
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_Field {
- margin: 0;
+ .mx_DMInviteDialog_userTile {
+ display: inline-block;
+ float: left;
+ position: relative;
+ top: 7px;
+ }
+
+ // Using a textarea for this element, to circumvent autofill
+ // 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 {
width: 48px;
margin-left: 10px;
+ height: 25px;
+ line-height: 25px;
}
}
@@ -57,6 +93,43 @@ limitations under the License.
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 {
font-weight: 600;
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;
+ }
+}
diff --git a/res/img/icon-email-pill-avatar.svg b/res/img/icon-email-pill-avatar.svg
new file mode 100644
index 0000000000..c107ccc480
--- /dev/null
+++ b/res/img/icon-email-pill-avatar.svg
@@ -0,0 +1,37 @@
+
+
\ No newline at end of file
diff --git a/res/img/icon-pill-remove.svg b/res/img/icon-pill-remove.svg
new file mode 100644
index 0000000000..adf6fd4771
--- /dev/null
+++ b/res/img/icon-pill-remove.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js
index c5e9c92131..b40865d075 100644
--- a/src/components/views/dialogs/DMInviteDialog.js
+++ b/src/components/views/dialogs/DMInviteDialog.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
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 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;
_displayName: string;
_avatarUrl: string;
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
+ super();
this._userId = userDirResult.user_id;
this._displayName = userDirResult.display_name;
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 {
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
+ ?
+ :