Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Message editing: render avatars for pills in the editor #2997

Merged
merged 18 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions res/css/views/elements/_MessageEditor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ limitations under the License.
// padding around and in the editor.
// Actual values from fiddling around in inspector
margin: -7px -10px -5px -10px;
overflow: visible !important; // override mx_EventTile_content

.mx_MessageEditor_editor {
border-radius: 4px;
Expand All @@ -33,20 +34,28 @@ limitations under the License.
max-height: 200px;
overflow-x: auto;

span {
display: inline-block;
padding: 0 5px;
border-radius: 4px;
color: white;
}
span.mx_UserPill, span.mx_RoomPill {
padding-left: 21px;
position: relative;

span.user-pill, span.room-pill {
border-radius: 16px;
display: inline-block;
color: $primary-fg-color;
background-color: $other-user-pill-bg-color;
padding-left: 5px;
padding-right: 5px;
// avatar psuedo element
&::before {
position: absolute;
left: 2px;
top: 2px;
content: var(--avatar-letter);
width: 16px;
height: 16px;
background: var(--avatar-background), $avatar-bg-color;
color: $avatar-initial-color;
background-repeat: no-repeat;
background-size: 16px;
border-radius: 8px;
text-align: center;
font-weight: normal;
line-height: 16px;
font-size: 10.4px;
}
}
}

Expand All @@ -61,7 +70,7 @@ limitations under the License.
z-index: 100;
right: 0;
margin: 0 -110px 0 0;
padding-right: 104px;
padding-right: 147px;

.mx_AccessibleButton {
margin-left: 5px;
Expand Down
5 changes: 5 additions & 0 deletions res/css/views/rooms/_EventTile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ limitations under the License.

.mx_EventTile_continuation {
padding-top: 0px ! important;

&.mx_EventTile_isEditing {
padding-top: 5px ! important;
bwindels marked this conversation as resolved.
Show resolved Hide resolved
margin-top: -5px;
}
}

.mx_EventTile_isEditing {
Expand Down
68 changes: 68 additions & 0 deletions src/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import {ContentRepo} from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';

module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
Expand Down Expand Up @@ -58,4 +59,71 @@ module.exports = {
}
return require('../res/img/' + images[total % images.length] + '.png');
},

/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
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();
},

avatarUrlForRoom(room, width, height, resizeMethod) {
const explicitRoomAvatar = room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
resizeMethod,
false,
);
if (explicitRoomAvatar) {
return explicitRoomAvatar;
}

let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
resizeMethod,
false,
);
}
return null;
},
};
34 changes: 1 addition & 33 deletions src/components/views/avatars/BaseAvatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,38 +133,6 @@ module.exports = React.createClass({
}
},

/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
*/
_getInitialLetter: function(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();
},

render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
Expand All @@ -176,7 +144,7 @@ module.exports = React.createClass({
} = this.props;

if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
Expand Down
34 changes: 3 additions & 31 deletions src/components/views/avatars/RoomAvatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import sdk from "../../../index";
import DMRoomMap from '../../../utils/DMRoomMap';
import Avatar from '../../../Avatar';

module.exports = React.createClass({
displayName: 'RoomAvatar',
Expand Down Expand Up @@ -89,7 +89,6 @@ module.exports = React.createClass({
props.resizeMethod,
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), // lowest priority
].filter(function(url) {
return (url != null && url != "");
});
Expand All @@ -98,41 +97,14 @@ module.exports = React.createClass({
getRoomAvatarUrl: function(props) {
if (!props.room) return null;

return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
return Avatar.avatarUrlForRoom(
props.room,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
},

getOneToOneAvatar: function(props) {
const room = props.room;
if (!room) {
return null;
}
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
}
return null;
},

onRoomAvatarClick: function() {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Expand Down
8 changes: 5 additions & 3 deletions src/components/views/elements/MessageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames';

export default class MessageEditor extends React.Component {
static propTypes = {
Expand All @@ -40,16 +41,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,
Expand Down Expand Up @@ -176,7 +178,7 @@ export default class MessageEditor extends React.Component {
</div>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div className="mx_MessageEditor">
return <div className={classNames("mx_MessageEditor", this.props.className)}>
{ autoComplete }
<div
className="mx_MessageEditor_editor"
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/TextualBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ module.exports = React.createClass({
render: function() {
if (this.props.isEditing) {
const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} />;
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
const mxEvent = this.props.mxEvent;
Expand Down
6 changes: 4 additions & 2 deletions src/editor/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -83,7 +84,8 @@ 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;
Expand Down
10 changes: 5 additions & 5 deletions src/editor/deserialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can room ever be null here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's the current room. If you can write events (to be able to edit them later), the current room should always be an instance of Room AFAIK.

case "#": return new RoomPillPart(resourceId);
default: return new PlainPart(n.textContent);
}
}
Expand All @@ -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");
Expand Down
Loading