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

Add room and user avatars to rte #10497

Merged
merged 29 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1bbe5d4
refactor to prep for avatar work
Mar 28, 2023
d1b5a31
make an avatar appear
Mar 28, 2023
852ad1a
align user avatar correctly
Mar 29, 2023
0a78b16
add room avatar logic
Mar 29, 2023
ae27f56
add another util for finding room from the client
Mar 29, 2023
bb9fbc7
use newly extracted util function to reduce repetition
Mar 29, 2023
c8d61d4
fix TS errors
Apr 3, 2023
7dd317e
extract helper functions to new file
Mar 31, 2023
c093a53
create test file, add buildQuery tests
Mar 31, 2023
9215be3
add getRoomFromCompletion tests
Mar 31, 2023
8b0559f
test getMentionDisplayText
Mar 31, 2023
de4f781
start off the getMentionAttributes tests
Mar 31, 2023
e6f8cb4
write user and room tests for getMentionAttributes
Apr 3, 2023
6b60a73
use mocked to satisfy TS
Apr 3, 2023
a6e4072
correct typo
Apr 3, 2023
b2ff75c
fix TS errors
Apr 3, 2023
bdb6d57
add comments
Apr 3, 2023
9c867fc
consolidate new CSS, use rem over px, remove comments
Apr 3, 2023
bd25c1a
change how missing href is handled
Apr 3, 2023
0039726
bump rte to 2.0.0
Apr 5, 2023
bf9d721
Merge remote-tracking branch 'origin/develop' into alunturner/avatars…
Apr 5, 2023
2846735
tweak vertical alignment
Apr 5, 2023
4a0b0fe
always ensure attributes have initial content
Apr 5, 2023
1b68099
always ensure we have content in CSS variable
Apr 5, 2023
c82c269
update tests
Apr 5, 2023
bab9f73
tweak vertical align again
Apr 5, 2023
4216c14
Merge branch 'develop' into alunturner/avatars-for-rte
artcodespace Apr 5, 2023
1a6a4ae
Merge branch 'develop' into alunturner/avatars-for-rte
t3chguy Apr 5, 2023
de33735
Merge branch 'develop' into alunturner/avatars-for-rte
t3chguy Apr 6, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.5.0",
"@matrix-org/matrix-wysiwyg": "^1.4.1",
"@matrix-org/matrix-wysiwyg": "^2.0.0",
"@matrix-org/react-sdk-module-api": "^0.0.4",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
Expand Down
42 changes: 31 additions & 11 deletions res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -106,32 +106,52 @@ limitations under the License.
in the current composer, there don't appear to be any styles associated with those classes
in this repo */
a[data-mention-type] {
/* these entries duplicate mx_Pill from _Pill.pcss */
/* combine mx_Pill from _Pill.pcss */
padding: $font-1px 0.4em;
line-height: $font-17px;
border-radius: $font-16px;
vertical-align: text-top;
/* TODO turning this on hides the cursor from the composer for some
reason, so comment out for now and assess if it's needed when we add
the Avatars
display: inline-flex;
align-items: center; not required with the above turned off

Potential fix is using display: inline, width: fit-content
*/
display: inline;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;

color: $accent-fg-color;
background-color: $pill-bg-color;

/* combining the overrides from _BasicMessageComposer.pcss */
/* ...with the overrides from _BasicMessageComposer.pcss */
user-select: all;
position: relative;
cursor: unset; /* We don't want indicate clickability */
text-overflow: ellipsis;
white-space: nowrap;

/* avatar pseudo element */
&::before {
/* After consolidation, all of the styling from _Pill.scss was being overridden,
so take what is in _BasicMessageComposer.pcss as the starting point */
display: inline-block;
content: var(--avatar-letter);
background: var(--avatar-background), $background;

width: $font-16px;
min-width: $font-16px; /* ensure the avatar is not compressed */
height: $font-16px;
line-height: $font-16px;
text-align: center;

/* Get the positioning of the avatar just right for consistency with timeline */
margin-inline-start: -0.4rem;
margin-inline-end: 0.24rem;
vertical-align: 0.12rem;

background-repeat: no-repeat;
background-size: $font-16px;
border-radius: $font-16px;

color: $avatar-initial-color;
font-weight: normal;
font-size: $font-10-4px;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ limitations under the License.
*/

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

import { useRoomContext } from "../../../../../contexts/RoomContext";
import Autocomplete from "../../Autocomplete";
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
import { getMentionDisplayText, getMentionAttributes, buildQuery } from "../utils/autocomplete";

interface WysiwygAutocompleteProps {
/**
Expand All @@ -37,55 +37,6 @@ interface WysiwygAutocompleteProps {
handleMention: FormattingFunctions["mention"];
}

/**
* Builds the query for the `<Autocomplete />` component from the rust suggestion. This
* will change as we implement handling / commands.
*
* @param suggestion - represents if the rust model is tracking a potential mention
* @returns an empty string if we can not generate a query, otherwise a query beginning
* with @ for a user query, # for a room or space query
*/
function buildQuery(suggestion: MappedSuggestion | null): string {
if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
// if we have an empty key character, we do not build a query
// TODO implement the command functionality
return "";
}

return `${suggestion.keyChar}${suggestion.text}`;
}

/**
* Given a room type mention, determine the text that should be displayed in the mention
* TODO expand this function to more generally handle outputting the display text from a
* given completion
*
* @param completion - the item selected from the autocomplete, currently treated as a room completion
* @param client - the MatrixClient is required for us to look up the correct room mention text
* @returns the text to display in the mention
*/
function getRoomMentionText(completion: ICompletion, client: MatrixClient): string {
const roomId = completion.completionId;
const alias = completion.completion;

let roomForAutocomplete: Room | null | undefined;

// Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias
// that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now
if (roomId) {
roomForAutocomplete = client.getRoom(roomId);
} else if (!alias.startsWith("#")) {
roomForAutocomplete = client.getRoom(alias);
} else {
roomForAutocomplete = client.getRooms().find((r) => {
return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias);
});
}

// if we haven't managed to find the room, use the alias as a fallback
return roomForAutocomplete?.name || alias;
}

/**
* Given the current suggestion from the rust model and a handler function, this component
* will display the legacy `<Autocomplete />` component (as used in the BasicMessageComposer)
Expand All @@ -99,22 +50,14 @@ const WysiwygAutocomplete = forwardRef(
const client = useMatrixClientContext();

function handleConfirm(completion: ICompletion): void {
if (!completion.href || !client) return;

switch (completion.type) {
case "user":
handleMention(completion.href, completion.completion);
break;
case "room": {
handleMention(completion.href, getRoomMentionText(completion, client));
break;
}
// TODO implement the command functionality
// case "command":
// console.log("/command functionality not yet in place");
// break;
default:
break;
// TODO handle all of the completion types
// Using this to pick out the ones we can handle during implementation
if (client && room && completion.href && (completion.type === "room" || completion.type === "user")) {
handleMention(
completion.href,
getMentionDisplayText(completion, client),
getMentionAttributes(completion, client, room),
);
}
}

Expand Down
137 changes: 137 additions & 0 deletions src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Attributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";

import { ICompletion } from "../../../../../autocomplete/Autocompleter";
import * as Avatar from "../../../../../Avatar";

/**
* Builds the query for the `<Autocomplete />` component from the rust suggestion. This
* will change as we implement handling / commands.
*
* @param suggestion - represents if the rust model is tracking a potential mention
* @returns an empty string if we can not generate a query, otherwise a query beginning
* with @ for a user query, # for a room or space query
*/
export function buildQuery(suggestion: MappedSuggestion | null): string {
if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
// if we have an empty key character, we do not build a query
// TODO implement the command functionality
return "";
}

return `${suggestion.keyChar}${suggestion.text}`;
}

/**
* Find the room from the completion by looking it up using the client from the context
* we are currently in
*
* @param completion - the completion from the autocomplete
* @param client - the current client we are using
* @returns a Room if one is found, null otherwise
*/
export function getRoomFromCompletion(completion: ICompletion, client: MatrixClient): Room | null {
const roomId = completion.completionId;
const aliasFromCompletion = completion.completion;

let roomToReturn: Room | null | undefined;

// Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias
// that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now
if (roomId) {
roomToReturn = client.getRoom(roomId);
} else if (!aliasFromCompletion.startsWith("#")) {
roomToReturn = client.getRoom(aliasFromCompletion);
} else {
roomToReturn = client.getRooms().find((r) => {
return r.getCanonicalAlias() === aliasFromCompletion || r.getAltAliases().includes(aliasFromCompletion);
});
}

return roomToReturn ?? null;
}

/**
* Given an autocomplete suggestion, determine the text to display in the pill
*
* @param completion - the item selected from the autocomplete
* @param client - the MatrixClient is required for us to look up the correct room mention text
* @returns the text to display in the mention
*/
export function getMentionDisplayText(completion: ICompletion, client: MatrixClient): string {
if (completion.type === "user") {
return completion.completion;
} else if (completion.type === "room") {
// try and get the room and use it's name, if not available, fall back to
// completion.completion
return getRoomFromCompletion(completion, client)?.name || completion.completion;
}
return "";
}

/**
* For a given completion, the attributes will change depending on the completion type
*
* @param completion - the item selected from the autocomplete
* @param client - the MatrixClient is required for us to look up the correct room mention text
* @returns an object of attributes containing HTMLAnchor attributes or data-* attri
*/
export function getMentionAttributes(completion: ICompletion, client: MatrixClient, room: Room): Attributes {
// to ensure that we always have something set in the --avatar-letter CSS variable
// as otherwise alignment varies depending on whether the content is empty or not
const defaultLetterContent = "-";

if (completion.type === "user") {
// logic as used in UserPillPart.setAvatar in parts.ts
const mentionedMember = room.getMember(completion.completionId || "");

if (!mentionedMember) return {};

const name = mentionedMember.name || mentionedMember.userId;
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(mentionedMember.userId);
const avatarUrl = Avatar.avatarUrlForMember(mentionedMember, 16, 16, "crop");
let initialLetter = defaultLetterContent;
if (avatarUrl === defaultAvatarUrl) {
initialLetter = Avatar.getInitialLetter(name) ?? defaultLetterContent;
}

return {
"data-mention-type": completion.type,
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
};
} else if (completion.type === "room") {
// logic as used in RoomPillPart.setAvatar in parts.ts
const mentionedRoom = getRoomFromCompletion(completion, client);
const aliasFromCompletion = completion.completion;

let initialLetter = defaultLetterContent;
let avatarUrl = Avatar.avatarUrlForRoom(mentionedRoom ?? null, 16, 16, "crop");
if (!avatarUrl) {
initialLetter = Avatar.getInitialLetter(mentionedRoom?.name || aliasFromCompletion) ?? defaultLetterContent;
avatarUrl = Avatar.defaultAvatarUrlForString(mentionedRoom?.roomId ?? aliasFromCompletion);
}

return {
"data-mention-type": completion.type,
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
};
}

return {};
}
Loading