From 710338c01f9ac0d85045111266e46c15afef4526 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 17 May 2019 19:48:05 +0100 Subject: [PATCH 01/18] pass member and room to editor pills to get avatar url --- src/components/views/elements/MessageEditor.js | 5 +++-- src/editor/autocomplete.js | 8 +++++--- src/editor/deserialize.js | 10 +++++----- src/editor/parts.js | 17 ++++++++++++++--- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 0c249d067b..4f1e1675a5 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -40,16 +40,17 @@ export default class MessageEditor extends React.Component { constructor(props, context) { super(props, context); + const room = this.context.matrixClient.getRoom(this.props.event.getRoomId()); const partCreator = new PartCreator( () => this._autocompleteRef, query => this.setState({query}), + room, ); this.model = new EditorModel( - parseEvent(this.props.event), + parseEvent(this.props.event, room), partCreator, this._updateEditorState, ); - const room = this.context.matrixClient.getRoom(this.props.event.getRoomId()); this.state = { autoComplete: null, room, diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index d2f73b1dff..82d4086b45 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -17,11 +17,12 @@ limitations under the License. import {UserPillPart, RoomPillPart, PlainPart} from "./parts"; export default class AutocompleteWrapperModel { - constructor(updateCallback, getAutocompleterComponent, updateQuery) { + constructor(updateCallback, getAutocompleterComponent, updateQuery, room) { this._updateCallback = updateCallback; this._getAutocompleterComponent = getAutocompleterComponent; this._updateQuery = updateQuery; this._query = null; + this._room = room; } onEscape(e) { @@ -83,11 +84,12 @@ export default class AutocompleteWrapperModel { case "@": { const displayName = completion.completion; const userId = completion.completionId; - return new UserPillPart(userId, displayName); + const member = this._room.getMember(userId); + return new UserPillPart(userId, displayName, member); } case "#": { const displayAlias = completion.completionId; - return new RoomPillPart(displayAlias); + return new RoomPillPart(displayAlias, this._room); } // also used for emoji completion default: diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index a7f28badb1..0c9d090ea5 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -17,7 +17,7 @@ limitations under the License. import { MATRIXTO_URL_PATTERN } from '../linkify-matrix'; import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts"; -function parseHtmlMessage(html) { +function parseHtmlMessage(html, room) { const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. @@ -37,8 +37,8 @@ function parseHtmlMessage(html) { const resourceId = pillMatch[1]; // The room/user ID const prefix = pillMatch[2]; // The first character of prefix switch (prefix) { - case "@": return new UserPillPart(resourceId, n.textContent); - case "#": return new RoomPillPart(resourceId, n.textContent); + case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId)); + case "#": return new RoomPillPart(resourceId, n.textContent, room); default: return new PlainPart(n.textContent); } } @@ -54,10 +54,10 @@ function parseHtmlMessage(html) { return parts; } -export function parseEvent(event) { +export function parseEvent(event, room) { const content = event.getContent(); if (content.format === "org.matrix.custom.html") { - return parseHtmlMessage(content.formatted_body || ""); + return parseHtmlMessage(content.formatted_body || "", room); } else { const body = content.body || ""; const lines = body.split("\n"); diff --git a/src/editor/parts.js b/src/editor/parts.js index bf792b1ab9..53d596ae2d 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -216,8 +216,9 @@ export class NewlinePart extends BasePart { } export class RoomPillPart extends PillPart { - constructor(displayAlias) { + constructor(displayAlias, room) { super(displayAlias, displayAlias); + this._room = room; } get type() { @@ -226,6 +227,11 @@ export class RoomPillPart extends PillPart { } export class UserPillPart extends PillPart { + constructor(userId, displayName, member) { + super(userId, displayName); + this._member = member; + } + get type() { return "user-pill"; } @@ -256,9 +262,14 @@ export class PillCandidatePart extends PlainPart { } export class PartCreator { - constructor(getAutocompleterComponent, updateQuery) { + constructor(getAutocompleterComponent, updateQuery, room) { this._autoCompleteCreator = (updateCallback) => { - return new AutocompleteWrapperModel(updateCallback, getAutocompleterComponent, updateQuery); + return new AutocompleteWrapperModel( + updateCallback, + getAutocompleterComponent, + updateQuery, + room, + ); }; } From 3b598a1782d6ad720622773cc6b59459b2c9de81 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 17 May 2019 19:49:46 +0100 Subject: [PATCH 02/18] set pill avatar through css variables to set on psuedo-element this way it won't interfere with editor selection/caret --- res/css/views/elements/_MessageEditor.scss | 17 ++++++++++++++++- src/editor/parts.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index cc5649a224..49bd48b6bd 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -45,8 +45,23 @@ limitations under the License. display: inline-block; color: $primary-fg-color; background-color: $other-user-pill-bg-color; - padding-left: 5px; + padding-left: 21px; padding-right: 5px; + position: relative; + + &::before { + position: absolute; + left: 2px; + top: 2px; + content: var(--avatar-letter); + width: 16px; + height: 16px; + background: var(--avatar-background); //set on parent by JS + color: var(--avatar-color); + background-repeat: no-repeat; + background-size: 16px; + border-radius: 8px; + } } } diff --git a/src/editor/parts.js b/src/editor/parts.js index 53d596ae2d..f1f64f4b05 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -15,6 +15,7 @@ limitations under the License. */ import AutocompleteWrapperModel from "./autocomplete"; +import Avatar from "../Avatar"; class BasePart { constructor(text = "") { @@ -232,6 +233,20 @@ export class UserPillPart extends PillPart { this._member = member; } + toDOMNode() { + const pill = super.toDOMNode(); + const avatarUrl = Avatar.avatarUrlForMember(this._member, 16, 16); + if (avatarUrl) { + pill.style.setProperty("--avatar-background", `url('${avatarUrl}')`); + pill.style.setProperty("--avatar-letter", "''"); + } else { + pill.style.setProperty("--avatar-background", `green`); + pill.style.setProperty("--avatar-color", `white`); + pill.style.setProperty("--avatar-letter", `'${this.text[0].toUpperCase()}'`); + } + return pill; + } + get type() { return "user-pill"; } From a47e722fa1b3f909ffa07492fdec3e8235043f9b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 17 May 2019 19:51:03 +0100 Subject: [PATCH 03/18] same height as normal pill --- res/css/views/elements/_MessageEditor.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index 49bd48b6bd..4cc44ab4eb 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -41,6 +41,8 @@ limitations under the License. } span.user-pill, span.room-pill { + height: 20px; + line-height: 20px; border-radius: 16px; display: inline-block; color: $primary-fg-color; From e58d844e5bd237725daa9b929753341d613d80db Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 May 2019 14:20:36 +0200 Subject: [PATCH 04/18] move getInitialLetter to Avatar so we can reuse it for editor pills --- src/Avatar.js | 32 ++++++++++++++++++++++ src/components/views/avatars/BaseAvatar.js | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Avatar.js b/src/Avatar.js index 99b558fa93..67608da53d 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -58,4 +58,36 @@ module.exports = { } return require('../res/img/' + images[total % images.length] + '.png'); }, + + /** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + */ + getInitialLetter(name) { + if (name.length < 1) { + return undefined; + } + + let idx = 0; + const initial = name[0]; + if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + let chars = 1; + const first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + const second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; + } + } + + const firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); + }, }; diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47de7c9dc4..10fb4f824a 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -176,7 +176,7 @@ module.exports = React.createClass({ } = this.props; if (imageUrl === this.state.defaultImageUrl) { - const initialLetter = this._getInitialLetter(name); + const initialLetter = AvatarLogic.getInitialLetter(name); const textNode = (