Update rich text editor dependency and associated changes (#11098)
* fix logic error * update types * extract message content to variable * use the new messageContent property * sort out mention types to make them a map * update getMentionAttributes to use AllowedMentionAttributes * add plain text handling * change type and handling for attributes when creating a mention in plain text * tidy, add comment * revert TS config change * fix broken types in test * update tests * bump rte * fix import and ts errors * fix broken tests
This commit is contained in:
parent
97765613bc
commit
fa31ed55d2
12 changed files with 108 additions and 77 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": "^2.2.2",
|
"@matrix-org/matrix-wysiwyg": "^2.3.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.5",
|
"@matrix-org/react-sdk-module-api": "^0.0.5",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
|
|
@ -65,6 +65,7 @@ export function PlainTextComposer({
|
||||||
onSelect,
|
onSelect,
|
||||||
handleCommand,
|
handleCommand,
|
||||||
handleMention,
|
handleMention,
|
||||||
|
handleAtRoomMention,
|
||||||
} = usePlainTextListeners(initialContent, onChange, onSend, eventRelation);
|
} = usePlainTextListeners(initialContent, onChange, onSend, eventRelation);
|
||||||
|
|
||||||
const composerFunctions = useComposerFunctions(editorRef, setContent);
|
const composerFunctions = useComposerFunctions(editorRef, setContent);
|
||||||
|
@ -90,6 +91,7 @@ export function PlainTextComposer({
|
||||||
suggestion={suggestion}
|
suggestion={suggestion}
|
||||||
handleMention={handleMention}
|
handleMention={handleMention}
|
||||||
handleCommand={handleCommand}
|
handleCommand={handleCommand}
|
||||||
|
handleAtRoomMention={handleAtRoomMention}
|
||||||
/>
|
/>
|
||||||
<Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|
|
@ -41,6 +41,11 @@ interface WysiwygAutocompleteProps {
|
||||||
* a command in the autocomplete list or pressing enter on a selected item
|
* a command in the autocomplete list or pressing enter on a selected item
|
||||||
*/
|
*/
|
||||||
handleCommand: FormattingFunctions["command"];
|
handleCommand: FormattingFunctions["command"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler purely for the at-room mentions special case
|
||||||
|
*/
|
||||||
|
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,7 +57,7 @@ interface WysiwygAutocompleteProps {
|
||||||
*/
|
*/
|
||||||
const WysiwygAutocomplete = forwardRef(
|
const WysiwygAutocomplete = forwardRef(
|
||||||
(
|
(
|
||||||
{ suggestion, handleMention, handleCommand }: WysiwygAutocompleteProps,
|
{ suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps,
|
||||||
ref: ForwardedRef<Autocomplete>,
|
ref: ForwardedRef<Autocomplete>,
|
||||||
): JSX.Element | null => {
|
): JSX.Element | null => {
|
||||||
const { room } = useRoomContext();
|
const { room } = useRoomContext();
|
||||||
|
@ -72,15 +77,7 @@ const WysiwygAutocomplete = forwardRef(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "at-room": {
|
case "at-room": {
|
||||||
// TODO improve handling of at-room to either become a span or use a placeholder href
|
handleAtRoomMention(getMentionAttributes(completion, client, room));
|
||||||
// We have an issue in that we can't use a placeholder because the rust model is always
|
|
||||||
// applying a prefix to the href, so an href of "#" becomes https://# and also we can not
|
|
||||||
// represent a plain span in rust
|
|
||||||
handleMention(
|
|
||||||
window.location.href,
|
|
||||||
getMentionDisplayText(completion, client),
|
|
||||||
getMentionAttributes(completion, client, room),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case "room":
|
case "room":
|
||||||
|
|
|
@ -30,10 +30,11 @@ import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../dispatcher/actions";
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
||||||
|
import { isNotNull } from "../../../../../Typeguards";
|
||||||
|
|
||||||
interface WysiwygComposerProps {
|
interface WysiwygComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?: (content: string) => void;
|
onChange: (content: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
initialContent?: string;
|
initialContent?: string;
|
||||||
|
@ -60,10 +61,11 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||||
const autocompleteRef = useRef<Autocomplete | null>(null);
|
const autocompleteRef = useRef<Autocomplete | null>(null);
|
||||||
|
|
||||||
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);
|
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);
|
||||||
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion } = useWysiwyg({
|
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({
|
||||||
initialContent,
|
initialContent,
|
||||||
inputEventProcessor,
|
inputEventProcessor,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isFocused, onFocus } = useIsFocused();
|
const { isFocused, onFocus } = useIsFocused();
|
||||||
|
|
||||||
const isReady = isWysiwygReady && !disabled;
|
const isReady = isWysiwygReady && !disabled;
|
||||||
|
@ -72,10 +74,10 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||||
useSetCursorPosition(!isReady, ref);
|
useSetCursorPosition(!isReady, ref);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!disabled && content !== null) {
|
if (!disabled && isNotNull(messageContent)) {
|
||||||
onChange?.(content);
|
onChange(messageContent);
|
||||||
}
|
}
|
||||||
}, [onChange, content, disabled]);
|
}, [onChange, messageContent, disabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(e: Event): void {
|
function handleClick(e: Event): void {
|
||||||
|
@ -115,6 +117,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||||
ref={autocompleteRef}
|
ref={autocompleteRef}
|
||||||
suggestion={suggestion}
|
suggestion={suggestion}
|
||||||
handleMention={wysiwyg.mention}
|
handleMention={wysiwyg.mention}
|
||||||
|
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
||||||
handleCommand={wysiwyg.command}
|
handleCommand={wysiwyg.command}
|
||||||
/>
|
/>
|
||||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KeyboardEvent, RefObject, SyntheticEvent, useCallback, useRef, useState } from "react";
|
import { KeyboardEvent, RefObject, SyntheticEvent, useCallback, useRef, useState } from "react";
|
||||||
import { Attributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
||||||
import { IEventRelation } from "matrix-js-sdk/src/matrix";
|
import { IEventRelation } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
@ -72,7 +72,8 @@ export function usePlainTextListeners(
|
||||||
onPaste(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>): void;
|
onPaste(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>): void;
|
||||||
onKeyDown(event: KeyboardEvent<HTMLDivElement>): void;
|
onKeyDown(event: KeyboardEvent<HTMLDivElement>): void;
|
||||||
setContent(text?: string): void;
|
setContent(text?: string): void;
|
||||||
handleMention: (link: string, text: string, attributes: Attributes) => void;
|
handleMention: (link: string, text: string, attributes: AllowedMentionAttributes) => void;
|
||||||
|
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||||
handleCommand: (text: string) => void;
|
handleCommand: (text: string) => void;
|
||||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
suggestion: MappedSuggestion | null;
|
suggestion: MappedSuggestion | null;
|
||||||
|
@ -97,10 +98,11 @@ export function usePlainTextListeners(
|
||||||
setContent(text);
|
setContent(text);
|
||||||
onChange?.(text);
|
onChange?.(text);
|
||||||
} else if (isNotNull(ref) && isNotNull(ref.current)) {
|
} else if (isNotNull(ref) && isNotNull(ref.current)) {
|
||||||
// if called with no argument, read the current innerHTML from the ref
|
// if called with no argument, read the current innerHTML from the ref and amend it as per `onInput`
|
||||||
const currentRefContent = ref.current.innerHTML;
|
const currentRefContent = ref.current.innerHTML;
|
||||||
setContent(currentRefContent);
|
const amendedContent = amendInnerHtml(currentRefContent);
|
||||||
onChange?.(currentRefContent);
|
setContent(amendedContent);
|
||||||
|
onChange?.(amendedContent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, ref],
|
[onChange, ref],
|
||||||
|
@ -109,7 +111,7 @@ export function usePlainTextListeners(
|
||||||
// For separation of concerns, the suggestion handling is kept in a separate hook but is
|
// For separation of concerns, the suggestion handling is kept in a separate hook but is
|
||||||
// nested here because we do need to be able to update the `content` state in this hook
|
// nested here because we do need to be able to update the `content` state in this hook
|
||||||
// when a user selects a suggestion from the autocomplete menu
|
// when a user selects a suggestion from the autocomplete menu
|
||||||
const { suggestion, onSelect, handleCommand, handleMention } = useSuggestion(ref, setText);
|
const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention } = useSuggestion(ref, setText);
|
||||||
|
|
||||||
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
|
@ -188,5 +190,6 @@ export function usePlainTextListeners(
|
||||||
onSelect,
|
onSelect,
|
||||||
handleCommand,
|
handleCommand,
|
||||||
handleMention,
|
handleMention,
|
||||||
|
handleAtRoomMention,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Attributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
||||||
import { SyntheticEvent, useState } from "react";
|
import { SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
import { isNotNull, isNotUndefined } from "../../../../../Typeguards";
|
import { isNotNull } from "../../../../../Typeguards";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about the current state of the `useSuggestion` hook.
|
* Information about the current state of the `useSuggestion` hook.
|
||||||
|
@ -53,7 +53,8 @@ export function useSuggestion(
|
||||||
editorRef: React.RefObject<HTMLDivElement>,
|
editorRef: React.RefObject<HTMLDivElement>,
|
||||||
setText: (text?: string) => void,
|
setText: (text?: string) => void,
|
||||||
): {
|
): {
|
||||||
handleMention: (href: string, displayName: string, attributes: Attributes) => void;
|
handleMention: (href: string, displayName: string, attributes: AllowedMentionAttributes) => void;
|
||||||
|
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||||
handleCommand: (text: string) => void;
|
handleCommand: (text: string) => void;
|
||||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
suggestion: MappedSuggestion | null;
|
suggestion: MappedSuggestion | null;
|
||||||
|
@ -64,9 +65,12 @@ export function useSuggestion(
|
||||||
// we can not depend on input events only
|
// we can not depend on input events only
|
||||||
const onSelect = (): void => processSelectionChange(editorRef, setSuggestionData);
|
const onSelect = (): void => processSelectionChange(editorRef, setSuggestionData);
|
||||||
|
|
||||||
const handleMention = (href: string, displayName: string, attributes: Attributes): void =>
|
const handleMention = (href: string, displayName: string, attributes: AllowedMentionAttributes): void =>
|
||||||
processMention(href, displayName, attributes, suggestionData, setSuggestionData, setText);
|
processMention(href, displayName, attributes, suggestionData, setSuggestionData, setText);
|
||||||
|
|
||||||
|
const handleAtRoomMention = (attributes: AllowedMentionAttributes): void =>
|
||||||
|
processMention("#", "@room", attributes, suggestionData, setSuggestionData, setText);
|
||||||
|
|
||||||
const handleCommand = (replacementText: string): void =>
|
const handleCommand = (replacementText: string): void =>
|
||||||
processCommand(replacementText, suggestionData, setSuggestionData, setText);
|
processCommand(replacementText, suggestionData, setSuggestionData, setText);
|
||||||
|
|
||||||
|
@ -74,6 +78,7 @@ export function useSuggestion(
|
||||||
suggestion: suggestionData?.mappedSuggestion ?? null,
|
suggestion: suggestionData?.mappedSuggestion ?? null,
|
||||||
handleCommand,
|
handleCommand,
|
||||||
handleMention,
|
handleMention,
|
||||||
|
handleAtRoomMention,
|
||||||
onSelect,
|
onSelect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -143,7 +148,7 @@ export function processSelectionChange(
|
||||||
export function processMention(
|
export function processMention(
|
||||||
href: string,
|
href: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
attributes: Attributes, // these will be used when formatting the link as a pill
|
attributes: AllowedMentionAttributes, // these will be used when formatting the link as a pill
|
||||||
suggestionData: SuggestionState,
|
suggestionData: SuggestionState,
|
||||||
setSuggestionData: React.Dispatch<React.SetStateAction<SuggestionState>>,
|
setSuggestionData: React.Dispatch<React.SetStateAction<SuggestionState>>,
|
||||||
setText: (text?: string) => void,
|
setText: (text?: string) => void,
|
||||||
|
@ -160,9 +165,11 @@ export function processMention(
|
||||||
const linkTextNode = document.createTextNode(displayName);
|
const linkTextNode = document.createTextNode(displayName);
|
||||||
linkElement.setAttribute("href", href);
|
linkElement.setAttribute("href", href);
|
||||||
linkElement.setAttribute("contenteditable", "false");
|
linkElement.setAttribute("contenteditable", "false");
|
||||||
Object.entries(attributes).forEach(
|
|
||||||
([attr, value]) => isNotUndefined(value) && linkElement.setAttribute(attr, value),
|
for (const [attr, value] of attributes.entries()) {
|
||||||
);
|
linkElement.setAttribute(attr, value);
|
||||||
|
}
|
||||||
|
|
||||||
linkElement.appendChild(linkTextNode);
|
linkElement.appendChild(linkTextNode);
|
||||||
|
|
||||||
// create text nodes to go before and after the link
|
// create text nodes to go before and after the link
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Attributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
|
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
|
||||||
|
@ -91,18 +91,22 @@ export function getMentionDisplayText(completion: ICompletion, client: MatrixCli
|
||||||
* @param client - the MatrixClient is required for us to look up the correct room mention text
|
* @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-* attributes
|
* @returns an object of attributes containing HTMLAnchor attributes or data-* attributes
|
||||||
*/
|
*/
|
||||||
export function getMentionAttributes(completion: ICompletion, client: MatrixClient, room: Room): Attributes {
|
export function getMentionAttributes(
|
||||||
|
completion: ICompletion,
|
||||||
|
client: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
): AllowedMentionAttributes {
|
||||||
// To ensure that we always have something set in the --avatar-letter CSS variable
|
// 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.
|
// as otherwise alignment varies depending on whether the content is empty or not.
|
||||||
|
|
||||||
// Use a zero width space so that it counts as content, but does not display anything.
|
// Use a zero width space so that it counts as content, but does not display anything.
|
||||||
const defaultLetterContent = "\u200b";
|
const defaultLetterContent = "\u200b";
|
||||||
|
const attributes: AllowedMentionAttributes = new Map();
|
||||||
|
|
||||||
if (completion.type === "user") {
|
if (completion.type === "user") {
|
||||||
// logic as used in UserPillPart.setAvatar in parts.ts
|
// logic as used in UserPillPart.setAvatar in parts.ts
|
||||||
const mentionedMember = room.getMember(completion.completionId || "");
|
const mentionedMember = room.getMember(completion.completionId || "");
|
||||||
|
|
||||||
if (!mentionedMember) return {};
|
if (!mentionedMember) return attributes;
|
||||||
|
|
||||||
const name = mentionedMember.name || mentionedMember.userId;
|
const name = mentionedMember.name || mentionedMember.userId;
|
||||||
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(mentionedMember.userId);
|
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(mentionedMember.userId);
|
||||||
|
@ -112,10 +116,8 @@ export function getMentionAttributes(completion: ICompletion, client: MatrixClie
|
||||||
initialLetter = Avatar.getInitialLetter(name) ?? defaultLetterContent;
|
initialLetter = Avatar.getInitialLetter(name) ?? defaultLetterContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
attributes.set("data-mention-type", completion.type);
|
||||||
"data-mention-type": completion.type,
|
attributes.set("style", `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`);
|
||||||
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
|
|
||||||
};
|
|
||||||
} else if (completion.type === "room") {
|
} else if (completion.type === "room") {
|
||||||
// logic as used in RoomPillPart.setAvatar in parts.ts
|
// logic as used in RoomPillPart.setAvatar in parts.ts
|
||||||
const mentionedRoom = getRoomFromCompletion(completion, client);
|
const mentionedRoom = getRoomFromCompletion(completion, client);
|
||||||
|
@ -128,12 +130,12 @@ export function getMentionAttributes(completion: ICompletion, client: MatrixClie
|
||||||
avatarUrl = Avatar.defaultAvatarUrlForString(mentionedRoom?.roomId ?? aliasFromCompletion);
|
avatarUrl = Avatar.defaultAvatarUrlForString(mentionedRoom?.roomId ?? aliasFromCompletion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
attributes.set("data-mention-type", completion.type);
|
||||||
"data-mention-type": completion.type,
|
attributes.set("style", `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`);
|
||||||
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
|
|
||||||
};
|
|
||||||
} else if (completion.type === "at-room") {
|
} else if (completion.type === "at-room") {
|
||||||
return { "data-mention-type": completion.type };
|
// TODO add avatar logic for at-room
|
||||||
|
attributes.set("data-mention-type", completion.type);
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe("WysiwygAutocomplete", () => {
|
||||||
]);
|
]);
|
||||||
const mockHandleMention = jest.fn();
|
const mockHandleMention = jest.fn();
|
||||||
const mockHandleCommand = jest.fn();
|
const mockHandleCommand = jest.fn();
|
||||||
|
const mockHandleAtRoomMention = jest.fn();
|
||||||
|
|
||||||
const renderComponent = (props: Partial<React.ComponentProps<typeof WysiwygAutocomplete>> = {}) => {
|
const renderComponent = (props: Partial<React.ComponentProps<typeof WysiwygAutocomplete>> = {}) => {
|
||||||
const mockClient = stubClient();
|
const mockClient = stubClient();
|
||||||
|
@ -84,6 +85,7 @@ describe("WysiwygAutocomplete", () => {
|
||||||
suggestion={null}
|
suggestion={null}
|
||||||
handleMention={mockHandleMention}
|
handleMention={mockHandleMention}
|
||||||
handleCommand={mockHandleCommand}
|
handleCommand={mockHandleCommand}
|
||||||
|
handleAtRoomMention={mockHandleAtRoomMention}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
|
@ -98,6 +100,7 @@ describe("WysiwygAutocomplete", () => {
|
||||||
suggestion={null}
|
suggestion={null}
|
||||||
handleMention={mockHandleMention}
|
handleMention={mockHandleMention}
|
||||||
handleCommand={mockHandleCommand}
|
handleCommand={mockHandleCommand}
|
||||||
|
handleAtRoomMention={mockHandleAtRoomMention}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument();
|
||||||
|
|
|
@ -164,7 +164,7 @@ describe("WysiwygComposer", () => {
|
||||||
const mockCompletions: ICompletion[] = [
|
const mockCompletions: ICompletion[] = [
|
||||||
{
|
{
|
||||||
type: "user",
|
type: "user",
|
||||||
href: "www.user1.com",
|
href: "https://matrix.to/#/@user_1:element.io",
|
||||||
completion: "user_1",
|
completion: "user_1",
|
||||||
completionId: "@user_1:host.local",
|
completionId: "@user_1:host.local",
|
||||||
range: { start: 1, end: 1 },
|
range: { start: 1, end: 1 },
|
||||||
|
@ -172,7 +172,7 @@ describe("WysiwygComposer", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "user",
|
type: "user",
|
||||||
href: "www.user2.com",
|
href: "https://matrix.to/#/@user_2:element.io",
|
||||||
completion: "user_2",
|
completion: "user_2",
|
||||||
completionId: "@user_2:host.local",
|
completionId: "@user_2:host.local",
|
||||||
range: { start: 1, end: 1 },
|
range: { start: 1, end: 1 },
|
||||||
|
@ -189,7 +189,7 @@ describe("WysiwygComposer", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "room",
|
type: "room",
|
||||||
href: "www.room1.com",
|
href: "https://matrix.to/#/#room_1:element.io",
|
||||||
completion: "#room_with_completion_id",
|
completion: "#room_with_completion_id",
|
||||||
completionId: "@room_1:host.local",
|
completionId: "@room_1:host.local",
|
||||||
range: { start: 1, end: 1 },
|
range: { start: 1, end: 1 },
|
||||||
|
@ -197,7 +197,7 @@ describe("WysiwygComposer", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "room",
|
type: "room",
|
||||||
href: "www.room2.com",
|
href: "https://matrix.to/#/#room_2:element.io",
|
||||||
completion: "#room_without_completion_id",
|
completion: "#room_without_completion_id",
|
||||||
range: { start: 1, end: 1 },
|
range: { start: 1, end: 1 },
|
||||||
component: <div>room_without_completion_id</div>,
|
component: <div>room_without_completion_id</div>,
|
||||||
|
@ -285,9 +285,9 @@ describe("WysiwygComposer", () => {
|
||||||
|
|
||||||
it("pressing enter selects the mention and inserts it into the composer as a link", async () => {
|
it("pressing enter selects the mention and inserts it into the composer as a link", async () => {
|
||||||
await insertMentionInput();
|
await insertMentionInput();
|
||||||
|
|
||||||
// press enter
|
// press enter
|
||||||
await userEvent.keyboard("{Enter}");
|
await userEvent.keyboard("{Enter}");
|
||||||
|
screen.debug();
|
||||||
|
|
||||||
// check that it closes the autocomplete
|
// check that it closes the autocomplete
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
|
@ -72,7 +72,7 @@ describe("processMention", () => {
|
||||||
it("returns early when suggestion is null", () => {
|
it("returns early when suggestion is null", () => {
|
||||||
const mockSetSuggestion = jest.fn();
|
const mockSetSuggestion = jest.fn();
|
||||||
const mockSetText = jest.fn();
|
const mockSetText = jest.fn();
|
||||||
processMention("href", "displayName", {}, null, mockSetSuggestion, mockSetText);
|
processMention("href", "displayName", new Map(), null, mockSetSuggestion, mockSetText);
|
||||||
|
|
||||||
expect(mockSetSuggestion).not.toHaveBeenCalled();
|
expect(mockSetSuggestion).not.toHaveBeenCalled();
|
||||||
expect(mockSetText).not.toHaveBeenCalled();
|
expect(mockSetText).not.toHaveBeenCalled();
|
||||||
|
@ -95,7 +95,7 @@ describe("processMention", () => {
|
||||||
processMention(
|
processMention(
|
||||||
href,
|
href,
|
||||||
displayName,
|
displayName,
|
||||||
{ "data-test-attribute": "test" },
|
new Map([["style", "test"]]),
|
||||||
{ node: textNode, startOffset: 0, endOffset: 2 } as unknown as Suggestion,
|
{ node: textNode, startOffset: 0, endOffset: 2 } as unknown as Suggestion,
|
||||||
mockSetSuggestionData,
|
mockSetSuggestionData,
|
||||||
mockSetText,
|
mockSetText,
|
||||||
|
@ -109,7 +109,7 @@ describe("processMention", () => {
|
||||||
expect(linkElement).toBeInstanceOf(HTMLAnchorElement);
|
expect(linkElement).toBeInstanceOf(HTMLAnchorElement);
|
||||||
expect(linkElement).toHaveAttribute(href, href);
|
expect(linkElement).toHaveAttribute(href, href);
|
||||||
expect(linkElement).toHaveAttribute("contenteditable", "false");
|
expect(linkElement).toHaveAttribute("contenteditable", "false");
|
||||||
expect(linkElement).toHaveAttribute("data-test-attribute", "test");
|
expect(linkElement).toHaveAttribute("style", "test");
|
||||||
expect(linkElement.textContent).toBe(displayName);
|
expect(linkElement.textContent).toBe(displayName);
|
||||||
|
|
||||||
expect(mockSetText).toHaveBeenCalledWith();
|
expect(mockSetText).toHaveBeenCalledWith();
|
||||||
|
|
|
@ -143,12 +143,12 @@ describe("getMentionDisplayText", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getMentionAttributes", () => {
|
describe("getMentionAttributes", () => {
|
||||||
it("returns an empty object for completion types other than room, user or at-room", () => {
|
it("returns an empty map for completion types other than room, user or at-room", () => {
|
||||||
const nonHandledCompletionTypes = ["community", "command"] as const;
|
const nonHandledCompletionTypes = ["community", "command"] as const;
|
||||||
const nonHandledCompletions = nonHandledCompletionTypes.map((type) => createMockCompletion({ type }));
|
const nonHandledCompletions = nonHandledCompletionTypes.map((type) => createMockCompletion({ type }));
|
||||||
|
|
||||||
nonHandledCompletions.forEach((completion) => {
|
nonHandledCompletions.forEach((completion) => {
|
||||||
expect(getMentionAttributes(completion, mockClient, mockRoom)).toEqual({});
|
expect(getMentionAttributes(completion, mockClient, mockRoom)).toEqual(new Map());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -164,14 +164,14 @@ describe("getMentionAttributes", () => {
|
||||||
mockAvatar.getInitialLetter.mockReturnValue(testInitialLetter);
|
mockAvatar.getInitialLetter.mockReturnValue(testInitialLetter);
|
||||||
|
|
||||||
describe("user mentions", () => {
|
describe("user mentions", () => {
|
||||||
it("returns an empty object when no member can be found", () => {
|
it("returns an empty map when no member can be found", () => {
|
||||||
const userCompletion = createMockCompletion({ type: "user" });
|
const userCompletion = createMockCompletion({ type: "user" });
|
||||||
|
|
||||||
// mock not being able to find a member
|
// mock not being able to find a member
|
||||||
mockRoom.getMember.mockImplementationOnce(() => null);
|
mockRoom.getMember.mockImplementationOnce(() => null);
|
||||||
|
|
||||||
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual(new Map());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns expected attributes when avatar url is not default", () => {
|
it("returns expected attributes when avatar url is not default", () => {
|
||||||
|
@ -179,10 +179,12 @@ describe("getMentionAttributes", () => {
|
||||||
|
|
||||||
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual(
|
||||||
"data-mention-type": "user",
|
new Map([
|
||||||
"style": `--avatar-background: url(${testAvatarUrlForMember}); --avatar-letter: '\u200b'`,
|
["data-mention-type", "user"],
|
||||||
});
|
["style", `--avatar-background: url(${testAvatarUrlForMember}); --avatar-letter: '\u200b'`],
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns expected style attributes when avatar url matches default", () => {
|
it("returns expected style attributes when avatar url matches default", () => {
|
||||||
|
@ -193,10 +195,15 @@ describe("getMentionAttributes", () => {
|
||||||
|
|
||||||
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual(
|
||||||
"data-mention-type": "user",
|
new Map([
|
||||||
"style": `--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
["data-mention-type", "user"],
|
||||||
});
|
[
|
||||||
|
"style",
|
||||||
|
`--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,10 +213,12 @@ describe("getMentionAttributes", () => {
|
||||||
|
|
||||||
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual(
|
||||||
"data-mention-type": "room",
|
new Map([
|
||||||
"style": `--avatar-background: url(${testAvatarUrlForRoom}); --avatar-letter: '\u200b'`,
|
["data-mention-type", "room"],
|
||||||
});
|
["style", `--avatar-background: url(${testAvatarUrlForRoom}); --avatar-letter: '\u200b'`],
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns expected style attributes when avatar url for room is falsy", () => {
|
it("returns expected style attributes when avatar url for room is falsy", () => {
|
||||||
|
@ -220,10 +229,15 @@ describe("getMentionAttributes", () => {
|
||||||
|
|
||||||
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(userCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual(
|
||||||
"data-mention-type": "room",
|
new Map([
|
||||||
"style": `--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
["data-mention-type", "room"],
|
||||||
});
|
[
|
||||||
|
"style",
|
||||||
|
`--avatar-background: url(${testAvatarUrlForString}); --avatar-letter: '${testInitialLetter}'`,
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -233,7 +247,7 @@ describe("getMentionAttributes", () => {
|
||||||
|
|
||||||
const result = getMentionAttributes(atRoomCompletion, mockClient, mockRoom);
|
const result = getMentionAttributes(atRoomCompletion, mockClient, mockRoom);
|
||||||
|
|
||||||
expect(result).toEqual({ "data-mention-type": "at-room" });
|
expect(result).toEqual(new Map([["data-mention-type", "at-room"]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1598,10 +1598,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
||||||
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
||||||
|
|
||||||
"@matrix-org/matrix-wysiwyg@^2.2.2":
|
"@matrix-org/matrix-wysiwyg@^2.3.0":
|
||||||
version "2.2.2"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.2.2.tgz#911d0a9858a5a4b620f93777085daac8eff6a220"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.3.0.tgz#7a815fb90600342cc74c03a3cc7c9908a1d15dd1"
|
||||||
integrity sha512-FprkgKiqEHoFUfaamKwTGBENqDxbORFgoPjiE1b9yPS3hgRswobVKRl4qrXgVgFj4qQ7gWeTqogiyrHXkm1myw==
|
integrity sha512-VtA+Bti2IdqpnpCNaTFHMjbpKXe4xHR+OWWJl/gjuYgn4NJO9lfeeEIv34ftC6dBh7R280JEiMxQ9mDcH0J54g==
|
||||||
|
|
||||||
"@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