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

Commit e03eac1

Browse files
Add room and user avatars to rte (#10497)
Co-authored-by: Michael Telatynski <[email protected]>
1 parent 5c0e5eb commit e03eac1

File tree

6 files changed

+406
-82
lines changed

6 files changed

+406
-82
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"dependencies": {
6262
"@babel/runtime": "^7.12.5",
6363
"@matrix-org/analytics-events": "^0.5.0",
64-
"@matrix-org/matrix-wysiwyg": "^1.4.1",
64+
"@matrix-org/matrix-wysiwyg": "^2.0.0",
6565
"@matrix-org/react-sdk-module-api": "^0.0.4",
6666
"@sentry/browser": "^7.0.0",
6767
"@sentry/tracing": "^7.0.0",

res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,32 +106,52 @@ limitations under the License.
106106
in the current composer, there don't appear to be any styles associated with those classes
107107
in this repo */
108108
a[data-mention-type] {
109-
/* these entries duplicate mx_Pill from _Pill.pcss */
109+
/* combine mx_Pill from _Pill.pcss */
110110
padding: $font-1px 0.4em;
111111
line-height: $font-17px;
112112
border-radius: $font-16px;
113-
vertical-align: text-top;
114-
/* TODO turning this on hides the cursor from the composer for some
115-
reason, so comment out for now and assess if it's needed when we add
116-
the Avatars
117-
display: inline-flex;
118-
align-items: center; not required with the above turned off
119-
120-
Potential fix is using display: inline, width: fit-content
121-
*/
113+
display: inline;
122114
box-sizing: border-box;
123115
max-width: 100%;
124116
overflow: hidden;
125117

126118
color: $accent-fg-color;
127119
background-color: $pill-bg-color;
128120

129-
/* combining the overrides from _BasicMessageComposer.pcss */
121+
/* ...with the overrides from _BasicMessageComposer.pcss */
130122
user-select: all;
131123
position: relative;
132124
cursor: unset; /* We don't want indicate clickability */
133125
text-overflow: ellipsis;
134126
white-space: nowrap;
127+
128+
/* avatar pseudo element */
129+
&::before {
130+
/* After consolidation, all of the styling from _Pill.scss was being overridden,
131+
so take what is in _BasicMessageComposer.pcss as the starting point */
132+
display: inline-block;
133+
content: var(--avatar-letter);
134+
background: var(--avatar-background), $background;
135+
136+
width: $font-16px;
137+
min-width: $font-16px; /* ensure the avatar is not compressed */
138+
height: $font-16px;
139+
line-height: $font-16px;
140+
text-align: center;
141+
142+
/* Get the positioning of the avatar just right for consistency with timeline */
143+
margin-inline-start: -0.4rem;
144+
margin-inline-end: 0.24rem;
145+
vertical-align: 0.12rem;
146+
147+
background-repeat: no-repeat;
148+
background-size: $font-16px;
149+
border-radius: $font-16px;
150+
151+
color: $avatar-initial-color;
152+
font-weight: normal;
153+
font-size: $font-10-4px;
154+
}
135155
}
136156
}
137157

src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx

Lines changed: 9 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ limitations under the License.
1515
*/
1616

1717
import React, { ForwardedRef, forwardRef } from "react";
18-
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
1918
import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
2019

2120
import { useRoomContext } from "../../../../../contexts/RoomContext";
2221
import Autocomplete from "../../Autocomplete";
2322
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
2423
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
24+
import { getMentionDisplayText, getMentionAttributes, buildQuery } from "../utils/autocomplete";
2525

2626
interface WysiwygAutocompleteProps {
2727
/**
@@ -37,55 +37,6 @@ interface WysiwygAutocompleteProps {
3737
handleMention: FormattingFunctions["mention"];
3838
}
3939

40-
/**
41-
* Builds the query for the `<Autocomplete />` component from the rust suggestion. This
42-
* will change as we implement handling / commands.
43-
*
44-
* @param suggestion - represents if the rust model is tracking a potential mention
45-
* @returns an empty string if we can not generate a query, otherwise a query beginning
46-
* with @ for a user query, # for a room or space query
47-
*/
48-
function buildQuery(suggestion: MappedSuggestion | null): string {
49-
if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
50-
// if we have an empty key character, we do not build a query
51-
// TODO implement the command functionality
52-
return "";
53-
}
54-
55-
return `${suggestion.keyChar}${suggestion.text}`;
56-
}
57-
58-
/**
59-
* Given a room type mention, determine the text that should be displayed in the mention
60-
* TODO expand this function to more generally handle outputting the display text from a
61-
* given completion
62-
*
63-
* @param completion - the item selected from the autocomplete, currently treated as a room completion
64-
* @param client - the MatrixClient is required for us to look up the correct room mention text
65-
* @returns the text to display in the mention
66-
*/
67-
function getRoomMentionText(completion: ICompletion, client: MatrixClient): string {
68-
const roomId = completion.completionId;
69-
const alias = completion.completion;
70-
71-
let roomForAutocomplete: Room | null | undefined;
72-
73-
// Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias
74-
// that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now
75-
if (roomId) {
76-
roomForAutocomplete = client.getRoom(roomId);
77-
} else if (!alias.startsWith("#")) {
78-
roomForAutocomplete = client.getRoom(alias);
79-
} else {
80-
roomForAutocomplete = client.getRooms().find((r) => {
81-
return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias);
82-
});
83-
}
84-
85-
// if we haven't managed to find the room, use the alias as a fallback
86-
return roomForAutocomplete?.name || alias;
87-
}
88-
8940
/**
9041
* Given the current suggestion from the rust model and a handler function, this component
9142
* will display the legacy `<Autocomplete />` component (as used in the BasicMessageComposer)
@@ -99,22 +50,14 @@ const WysiwygAutocomplete = forwardRef(
9950
const client = useMatrixClientContext();
10051

10152
function handleConfirm(completion: ICompletion): void {
102-
if (!completion.href || !client) return;
103-
104-
switch (completion.type) {
105-
case "user":
106-
handleMention(completion.href, completion.completion);
107-
break;
108-
case "room": {
109-
handleMention(completion.href, getRoomMentionText(completion, client));
110-
break;
111-
}
112-
// TODO implement the command functionality
113-
// case "command":
114-
// console.log("/command functionality not yet in place");
115-
// break;
116-
default:
117-
break;
53+
// TODO handle all of the completion types
54+
// Using this to pick out the ones we can handle during implementation
55+
if (client && room && completion.href && (completion.type === "room" || completion.type === "user")) {
56+
handleMention(
57+
completion.href,
58+
getMentionDisplayText(completion, client),
59+
getMentionAttributes(completion, client, room),
60+
);
11861
}
11962
}
12063

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Attributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
18+
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
19+
20+
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
21+
import * as Avatar from "../../../../../Avatar";
22+
23+
/**
24+
* Builds the query for the `<Autocomplete />` component from the rust suggestion. This
25+
* will change as we implement handling / commands.
26+
*
27+
* @param suggestion - represents if the rust model is tracking a potential mention
28+
* @returns an empty string if we can not generate a query, otherwise a query beginning
29+
* with @ for a user query, # for a room or space query
30+
*/
31+
export function buildQuery(suggestion: MappedSuggestion | null): string {
32+
if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
33+
// if we have an empty key character, we do not build a query
34+
// TODO implement the command functionality
35+
return "";
36+
}
37+
38+
return `${suggestion.keyChar}${suggestion.text}`;
39+
}
40+
41+
/**
42+
* Find the room from the completion by looking it up using the client from the context
43+
* we are currently in
44+
*
45+
* @param completion - the completion from the autocomplete
46+
* @param client - the current client we are using
47+
* @returns a Room if one is found, null otherwise
48+
*/
49+
export function getRoomFromCompletion(completion: ICompletion, client: MatrixClient): Room | null {
50+
const roomId = completion.completionId;
51+
const aliasFromCompletion = completion.completion;
52+
53+
let roomToReturn: Room | null | undefined;
54+
55+
// Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias
56+
// that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now
57+
if (roomId) {
58+
roomToReturn = client.getRoom(roomId);
59+
} else if (!aliasFromCompletion.startsWith("#")) {
60+
roomToReturn = client.getRoom(aliasFromCompletion);
61+
} else {
62+
roomToReturn = client.getRooms().find((r) => {
63+
return r.getCanonicalAlias() === aliasFromCompletion || r.getAltAliases().includes(aliasFromCompletion);
64+
});
65+
}
66+
67+
return roomToReturn ?? null;
68+
}
69+
70+
/**
71+
* Given an autocomplete suggestion, determine the text to display in the pill
72+
*
73+
* @param completion - the item selected from the autocomplete
74+
* @param client - the MatrixClient is required for us to look up the correct room mention text
75+
* @returns the text to display in the mention
76+
*/
77+
export function getMentionDisplayText(completion: ICompletion, client: MatrixClient): string {
78+
if (completion.type === "user") {
79+
return completion.completion;
80+
} else if (completion.type === "room") {
81+
// try and get the room and use it's name, if not available, fall back to
82+
// completion.completion
83+
return getRoomFromCompletion(completion, client)?.name || completion.completion;
84+
}
85+
return "";
86+
}
87+
88+
/**
89+
* For a given completion, the attributes will change depending on the completion type
90+
*
91+
* @param completion - the item selected from the autocomplete
92+
* @param client - the MatrixClient is required for us to look up the correct room mention text
93+
* @returns an object of attributes containing HTMLAnchor attributes or data-* attri
94+
*/
95+
export function getMentionAttributes(completion: ICompletion, client: MatrixClient, room: Room): Attributes {
96+
// to ensure that we always have something set in the --avatar-letter CSS variable
97+
// as otherwise alignment varies depending on whether the content is empty or not
98+
const defaultLetterContent = "-";
99+
100+
if (completion.type === "user") {
101+
// logic as used in UserPillPart.setAvatar in parts.ts
102+
const mentionedMember = room.getMember(completion.completionId || "");
103+
104+
if (!mentionedMember) return {};
105+
106+
const name = mentionedMember.name || mentionedMember.userId;
107+
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(mentionedMember.userId);
108+
const avatarUrl = Avatar.avatarUrlForMember(mentionedMember, 16, 16, "crop");
109+
let initialLetter = defaultLetterContent;
110+
if (avatarUrl === defaultAvatarUrl) {
111+
initialLetter = Avatar.getInitialLetter(name) ?? defaultLetterContent;
112+
}
113+
114+
return {
115+
"data-mention-type": completion.type,
116+
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
117+
};
118+
} else if (completion.type === "room") {
119+
// logic as used in RoomPillPart.setAvatar in parts.ts
120+
const mentionedRoom = getRoomFromCompletion(completion, client);
121+
const aliasFromCompletion = completion.completion;
122+
123+
let initialLetter = defaultLetterContent;
124+
let avatarUrl = Avatar.avatarUrlForRoom(mentionedRoom ?? null, 16, 16, "crop");
125+
if (!avatarUrl) {
126+
initialLetter = Avatar.getInitialLetter(mentionedRoom?.name || aliasFromCompletion) ?? defaultLetterContent;
127+
avatarUrl = Avatar.defaultAvatarUrlForString(mentionedRoom?.roomId ?? aliasFromCompletion);
128+
}
129+
130+
return {
131+
"data-mention-type": completion.type,
132+
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
133+
};
134+
}
135+
136+
return {};
137+
}

0 commit comments

Comments
 (0)