Add room and user avatars to rte (#10497)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
5c0e5eb0fb
commit
e03eac12c3
6 changed files with 406 additions and 82 deletions
|
@ -61,7 +61,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.5.0",
|
"@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",
|
"@matrix-org/react-sdk-module-api": "^0.0.4",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
|
|
@ -106,19 +106,11 @@ limitations under the License.
|
||||||
in the current composer, there don't appear to be any styles associated with those classes
|
in the current composer, there don't appear to be any styles associated with those classes
|
||||||
in this repo */
|
in this repo */
|
||||||
a[data-mention-type] {
|
a[data-mention-type] {
|
||||||
/* these entries duplicate mx_Pill from _Pill.pcss */
|
/* combine mx_Pill from _Pill.pcss */
|
||||||
padding: $font-1px 0.4em;
|
padding: $font-1px 0.4em;
|
||||||
line-height: $font-17px;
|
line-height: $font-17px;
|
||||||
border-radius: $font-16px;
|
border-radius: $font-16px;
|
||||||
vertical-align: text-top;
|
display: inline;
|
||||||
/* 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
|
|
||||||
*/
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -126,12 +118,40 @@ limitations under the License.
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
background-color: $pill-bg-color;
|
background-color: $pill-bg-color;
|
||||||
|
|
||||||
/* combining the overrides from _BasicMessageComposer.pcss */
|
/* ...with the overrides from _BasicMessageComposer.pcss */
|
||||||
user-select: all;
|
user-select: all;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: unset; /* We don't want indicate clickability */
|
cursor: unset; /* We don't want indicate clickability */
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ForwardedRef, forwardRef } from "react";
|
import React, { ForwardedRef, forwardRef } from "react";
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
import Autocomplete from "../../Autocomplete";
|
import Autocomplete from "../../Autocomplete";
|
||||||
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
|
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
|
||||||
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||||
|
import { getMentionDisplayText, getMentionAttributes, buildQuery } from "../utils/autocomplete";
|
||||||
|
|
||||||
interface WysiwygAutocompleteProps {
|
interface WysiwygAutocompleteProps {
|
||||||
/**
|
/**
|
||||||
|
@ -37,55 +37,6 @@ interface WysiwygAutocompleteProps {
|
||||||
handleMention: FormattingFunctions["mention"];
|
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
|
* 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)
|
* will display the legacy `<Autocomplete />` component (as used in the BasicMessageComposer)
|
||||||
|
@ -99,22 +50,14 @@ const WysiwygAutocomplete = forwardRef(
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
function handleConfirm(completion: ICompletion): void {
|
function handleConfirm(completion: ICompletion): void {
|
||||||
if (!completion.href || !client) return;
|
// TODO handle all of the completion types
|
||||||
|
// Using this to pick out the ones we can handle during implementation
|
||||||
switch (completion.type) {
|
if (client && room && completion.href && (completion.type === "room" || completion.type === "user")) {
|
||||||
case "user":
|
handleMention(
|
||||||
handleMention(completion.href, completion.completion);
|
completion.href,
|
||||||
break;
|
getMentionDisplayText(completion, client),
|
||||||
case "room": {
|
getMentionAttributes(completion, client, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {};
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
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 { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { ICompletion } from "../../../../../../src/autocomplete/Autocompleter";
|
||||||
|
import {
|
||||||
|
buildQuery,
|
||||||
|
getRoomFromCompletion,
|
||||||
|
getMentionDisplayText,
|
||||||
|
getMentionAttributes,
|
||||||
|
} from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/autocomplete";
|
||||||
|
import { createTestClient, mkRoom } from "../../../../../test-utils";
|
||||||
|
import * as _mockAvatar from "../../../../../../src/Avatar";
|
||||||
|
|
||||||
|
const mockClient = createTestClient();
|
||||||
|
const mockRoomId = "mockRoomId";
|
||||||
|
const mockRoom = mkRoom(mockClient, mockRoomId);
|
||||||
|
|
||||||
|
const createMockCompletion = (props: Partial<ICompletion>): ICompletion => {
|
||||||
|
return {
|
||||||
|
completion: "mock",
|
||||||
|
range: { beginning: true, start: 0, end: 0 },
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("../../../../../../src/Avatar");
|
||||||
|
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
afterAll(() => jest.restoreAllMocks());
|
||||||
|
|
||||||
|
describe("buildQuery", () => {
|
||||||
|
it("returns an empty string for a falsy argument", () => {
|
||||||
|
expect(buildQuery(null)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string when keyChar is falsy", () => {
|
||||||
|
const noKeyCharSuggestion = { keyChar: "" as const, text: "test", type: "unknown" as const };
|
||||||
|
expect(buildQuery(noKeyCharSuggestion)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string when suggestion is a command", () => {
|
||||||
|
// TODO alter this test when commands are implemented
|
||||||
|
const commandSuggestion = { keyChar: "/" as const, text: "slash", type: "command" as const };
|
||||||
|
expect(buildQuery(commandSuggestion)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines the keyChar and text of the suggestion in the query", () => {
|
||||||
|
const handledSuggestion = { keyChar: "@" as const, text: "alice", type: "mention" as const };
|
||||||
|
expect(buildQuery(handledSuggestion)).toBe("@alice");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRoomFromCompletion", () => {
|
||||||
|
const createMockRoomCompletion = (props: Partial<ICompletion>): ICompletion => {
|
||||||
|
return createMockCompletion({ ...props, type: "room" });
|
||||||
|
};
|
||||||
|
|
||||||
|
it("calls getRoom with completionId if present in the completion", () => {
|
||||||
|
const testId = "arbitraryId";
|
||||||
|
const completionWithId = createMockRoomCompletion({ completionId: testId });
|
||||||
|
|
||||||
|
getRoomFromCompletion(completionWithId, mockClient);
|
||||||
|
|
||||||
|
expect(mockClient.getRoom).toHaveBeenCalledWith(testId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getRoom with completion if present and correct format", () => {
|
||||||
|
const testCompletion = "arbitraryCompletion";
|
||||||
|
const completionWithId = createMockRoomCompletion({ completionId: testCompletion });
|
||||||
|
|
||||||
|
getRoomFromCompletion(completionWithId, mockClient);
|
||||||
|
|
||||||
|
expect(mockClient.getRoom).toHaveBeenCalledWith(testCompletion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getRooms if no completionId is present and completion starts with #", () => {
|
||||||
|
const completionWithId = createMockRoomCompletion({ completion: "#hash" });
|
||||||
|
|
||||||
|
const result = getRoomFromCompletion(completionWithId, mockClient);
|
||||||
|
|
||||||
|
expect(mockClient.getRoom).not.toHaveBeenCalled();
|
||||||
|
expect(mockClient.getRooms).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// in this case, because the mock client returns an empty array of rooms
|
||||||
|
// from the call to get rooms, we'd expect the result to be null
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getMentionDisplayText", () => {
|
||||||
|
it("returns an empty string if we are not handling a user or a room type", () => {
|
||||||
|
const nonHandledCompletionTypes = ["at-room", "community", "command"] as const;
|
||||||
|
const nonHandledCompletions = nonHandledCompletionTypes.map((type) => createMockCompletion({ type }));
|
||||||
|
|
||||||
|
nonHandledCompletions.forEach((completion) => {
|
||||||
|
expect(getMentionDisplayText(completion, mockClient)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the completion if we are handling a user", () => {
|
||||||
|
const testCompletion = "display this";
|
||||||
|
const userCompletion = createMockCompletion({ type: "user", completion: testCompletion });
|
||||||
|
|
||||||
|
expect(getMentionDisplayText(userCompletion, mockClient)).toBe(testCompletion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the room name when the room has a valid completionId", () => {
|
||||||
|
const testCompletionId = "testId";
|
||||||
|
const userCompletion = createMockCompletion({ type: "room", completionId: testCompletionId });
|
||||||
|
|
||||||
|
// as this uses the mockClient, the name will be the mock room name returned from there
|
||||||
|
expect(getMentionDisplayText(userCompletion, mockClient)).toBe(mockClient.getRoom("")?.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the completion for a room if completion starts with #", () => {
|
||||||
|
const testCompletion = "#hash";
|
||||||
|
const userCompletion = createMockCompletion({ type: "room", completion: testCompletion });
|
||||||
|
|
||||||
|
// as this uses the mockClient, the name will be the mock room name returned from there
|
||||||
|
expect(getMentionDisplayText(userCompletion, mockClient)).toBe(testCompletion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getMentionAttributes", () => {
|
||||||
|
// TODO handle all completion types
|
||||||
|
it("returns an empty object for completion types other than room or user", () => {
|
||||||
|
const nonHandledCompletionTypes = ["at-room", "community", "command"] as const;
|
||||||
|
const nonHandledCompletions = nonHandledCompletionTypes.map((type) => createMockCompletion({ type }));
|
||||||
|
|
||||||
|
nonHandledCompletions.forEach((completion) => {
|
||||||
|
expect(getMentionAttributes(completion, mockClient, mockRoom)).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const testAvatarUrlForString = "www.stringUrl.com";
|
||||||
|
const testAvatarUrlForMember = "www.memberUrl.com";
|
||||||
|
const testAvatarUrlForRoom = "www.roomUrl.com";
|
||||||
|
const testInitialLetter = "z";
|
||||||
|
|
||||||
|
const mockAvatar = mocked(_mockAvatar);
|
||||||
|
mockAvatar.defaultAvatarUrlForString.mockReturnValue(testAvatarUrlForString);
|
||||||
|
mockAvatar.avatarUrlForMember.mockReturnValue(testAvatarUrlForMember);
|
||||||
|
mockAvatar.avatarUrlForRoom.mockReturnValue(testAvatarUrlForRoom);
|
||||||
|
mockAvatar.getInitialLetter.mockReturnValue(testInitialLetter);
|
||||||
|
|
||||||
|
describe("user mentions", () => {
|
||||||
|
it("returns an empty object when no member can be found", () => {
|
||||||
|
const userCompletion = createMockCompletion({ type: "user" });
|
||||||
|
|
||||||
|
// mock not being able to find a member
|
||||||
|
mockRoom.getMember.mockImplementationOnce(() => null);
|
||||||
|
|
||||||
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns expected attributes when avatar url is not default", () => {
|
||||||
|
const userCompletion = createMockCompletion({ type: "user" });
|
||||||
|
|
||||||
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
"data-mention-type": "user",
|
||||||
|
"style": `--avatar-background: url(${testAvatarUrlForMember}); --avatar-letter: '-'`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns expected style attributes when avatar url matches default", () => {
|
||||||
|
const userCompletion = createMockCompletion({ type: "user" });
|
||||||
|
|
||||||
|
// mock a single implementation of avatarUrlForMember to make it match the default
|
||||||
|
mockAvatar.avatarUrlForMember.mockReturnValueOnce(testAvatarUrlForString);
|
||||||
|
|
||||||
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
"data-mention-type": "user",
|
||||||
|
"style": `--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("room mentions", () => {
|
||||||
|
it("returns expected attributes when avatar url for room is truthy", () => {
|
||||||
|
const userCompletion = createMockCompletion({ type: "room" });
|
||||||
|
|
||||||
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
"data-mention-type": "room",
|
||||||
|
"style": `--avatar-background: url(${testAvatarUrlForRoom}); --avatar-letter: '-'`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns expected style attributes when avatar url for room is falsy", () => {
|
||||||
|
const userCompletion = createMockCompletion({ type: "room" });
|
||||||
|
|
||||||
|
// mock a single implementation of avatarUrlForRoom to make it falsy
|
||||||
|
mockAvatar.avatarUrlForRoom.mockReturnValueOnce(null);
|
||||||
|
|
||||||
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
"data-mention-type": "room",
|
||||||
|
"style": `--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1704,10 +1704,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658"
|
||||||
integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw==
|
integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw==
|
||||||
|
|
||||||
"@matrix-org/matrix-wysiwyg@^1.4.1":
|
"@matrix-org/matrix-wysiwyg@^2.0.0":
|
||||||
version "1.4.1"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-1.4.1.tgz#80c392f036dc4d6ef08a91c4964a68682e977079"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.0.0.tgz#913eb5faa5d43c7a4ee9bda68de1aa1dcc49a11d"
|
||||||
integrity sha512-B8sxY3pE2XyRyQ1g7cx0YjGaDZ1A0Uh5XxS/lNdxQ/0ctRJj6IBy7KtiUjxDRdA15ioZnf6aoJBRkBSr02qhaw==
|
integrity sha512-xRYFwsf6Jx7KTCpwU91mVhPA8q/c+oOVyK98NnexyK/IcQS7BMFAns5GZX9b6ZEy38u30KoxeN6mxvxi+ysQgg==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||||
version "3.2.14"
|
version "3.2.14"
|
||||||
|
|
Loading…
Reference in a new issue