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
|
@ -65,6 +65,7 @@ export function PlainTextComposer({
|
|||
onSelect,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
} = usePlainTextListeners(initialContent, onChange, onSend, eventRelation);
|
||||
|
||||
const composerFunctions = useComposerFunctions(editorRef, setContent);
|
||||
|
@ -90,6 +91,7 @@ export function PlainTextComposer({
|
|||
suggestion={suggestion}
|
||||
handleMention={handleMention}
|
||||
handleCommand={handleCommand}
|
||||
handleAtRoomMention={handleAtRoomMention}
|
||||
/>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
|
|
|
@ -41,6 +41,11 @@ interface WysiwygAutocompleteProps {
|
|||
* a command in the autocomplete list or pressing enter on a selected item
|
||||
*/
|
||||
handleCommand: FormattingFunctions["command"];
|
||||
|
||||
/**
|
||||
* Handler purely for the at-room mentions special case
|
||||
*/
|
||||
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +57,7 @@ interface WysiwygAutocompleteProps {
|
|||
*/
|
||||
const WysiwygAutocomplete = forwardRef(
|
||||
(
|
||||
{ suggestion, handleMention, handleCommand }: WysiwygAutocompleteProps,
|
||||
{ suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps,
|
||||
ref: ForwardedRef<Autocomplete>,
|
||||
): JSX.Element | null => {
|
||||
const { room } = useRoomContext();
|
||||
|
@ -72,15 +77,7 @@ const WysiwygAutocomplete = forwardRef(
|
|||
return;
|
||||
}
|
||||
case "at-room": {
|
||||
// TODO improve handling of at-room to either become a span or use a placeholder href
|
||||
// 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),
|
||||
);
|
||||
handleAtRoomMention(getMentionAttributes(completion, client, room));
|
||||
return;
|
||||
}
|
||||
case "room":
|
||||
|
|
|
@ -30,10 +30,11 @@ import { useRoomContext } from "../../../../../contexts/RoomContext";
|
|||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
||||
import { isNotNull } from "../../../../../Typeguards";
|
||||
|
||||
interface WysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (content: string) => void;
|
||||
onChange: (content: string) => void;
|
||||
onSend: () => void;
|
||||
placeholder?: string;
|
||||
initialContent?: string;
|
||||
|
@ -60,10 +61,11 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
const autocompleteRef = useRef<Autocomplete | null>(null);
|
||||
|
||||
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,
|
||||
inputEventProcessor,
|
||||
});
|
||||
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
|
||||
const isReady = isWysiwygReady && !disabled;
|
||||
|
@ -72,10 +74,10 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
useSetCursorPosition(!isReady, ref);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && content !== null) {
|
||||
onChange?.(content);
|
||||
if (!disabled && isNotNull(messageContent)) {
|
||||
onChange(messageContent);
|
||||
}
|
||||
}, [onChange, content, disabled]);
|
||||
}, [onChange, messageContent, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: Event): void {
|
||||
|
@ -115,6 +117,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
ref={autocompleteRef}
|
||||
suggestion={suggestion}
|
||||
handleMention={wysiwyg.mention}
|
||||
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
||||
handleCommand={wysiwyg.command}
|
||||
/>
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { useSettingValue } from "../../../../../hooks/useSettings";
|
||||
|
@ -72,7 +72,8 @@ export function usePlainTextListeners(
|
|||
onPaste(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>): void;
|
||||
onKeyDown(event: KeyboardEvent<HTMLDivElement>): 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;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
|
@ -97,10 +98,11 @@ export function usePlainTextListeners(
|
|||
setContent(text);
|
||||
onChange?.(text);
|
||||
} 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;
|
||||
setContent(currentRefContent);
|
||||
onChange?.(currentRefContent);
|
||||
const amendedContent = amendInnerHtml(currentRefContent);
|
||||
setContent(amendedContent);
|
||||
onChange?.(amendedContent);
|
||||
}
|
||||
},
|
||||
[onChange, ref],
|
||||
|
@ -109,7 +111,7 @@ export function usePlainTextListeners(
|
|||
// 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
|
||||
// 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 onInput = useCallback(
|
||||
|
@ -188,5 +190,6 @@ export function usePlainTextListeners(
|
|||
onSelect,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
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 { isNotNull, isNotUndefined } from "../../../../../Typeguards";
|
||||
import { isNotNull } from "../../../../../Typeguards";
|
||||
|
||||
/**
|
||||
* Information about the current state of the `useSuggestion` hook.
|
||||
|
@ -53,7 +53,8 @@ export function useSuggestion(
|
|||
editorRef: React.RefObject<HTMLDivElement>,
|
||||
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;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
|
@ -64,9 +65,12 @@ export function useSuggestion(
|
|||
// we can not depend on input events only
|
||||
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);
|
||||
|
||||
const handleAtRoomMention = (attributes: AllowedMentionAttributes): void =>
|
||||
processMention("#", "@room", attributes, suggestionData, setSuggestionData, setText);
|
||||
|
||||
const handleCommand = (replacementText: string): void =>
|
||||
processCommand(replacementText, suggestionData, setSuggestionData, setText);
|
||||
|
||||
|
@ -74,6 +78,7 @@ export function useSuggestion(
|
|||
suggestion: suggestionData?.mappedSuggestion ?? null,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
|
@ -143,7 +148,7 @@ export function processSelectionChange(
|
|||
export function processMention(
|
||||
href: 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,
|
||||
setSuggestionData: React.Dispatch<React.SetStateAction<SuggestionState>>,
|
||||
setText: (text?: string) => void,
|
||||
|
@ -160,9 +165,11 @@ export function processMention(
|
|||
const linkTextNode = document.createTextNode(displayName);
|
||||
linkElement.setAttribute("href", href);
|
||||
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);
|
||||
|
||||
// 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.
|
||||
*/
|
||||
|
||||
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 { 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
|
||||
* @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
|
||||
// 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.
|
||||
const defaultLetterContent = "\u200b";
|
||||
const attributes: AllowedMentionAttributes = new Map();
|
||||
|
||||
if (completion.type === "user") {
|
||||
// logic as used in UserPillPart.setAvatar in parts.ts
|
||||
const mentionedMember = room.getMember(completion.completionId || "");
|
||||
|
||||
if (!mentionedMember) return {};
|
||||
if (!mentionedMember) return attributes;
|
||||
|
||||
const name = mentionedMember.name || mentionedMember.userId;
|
||||
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(mentionedMember.userId);
|
||||
|
@ -112,10 +116,8 @@ export function getMentionAttributes(completion: ICompletion, client: MatrixClie
|
|||
initialLetter = Avatar.getInitialLetter(name) ?? defaultLetterContent;
|
||||
}
|
||||
|
||||
return {
|
||||
"data-mention-type": completion.type,
|
||||
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
|
||||
};
|
||||
attributes.set("data-mention-type", completion.type);
|
||||
attributes.set("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);
|
||||
|
@ -128,12 +130,12 @@ export function getMentionAttributes(completion: ICompletion, client: MatrixClie
|
|||
avatarUrl = Avatar.defaultAvatarUrlForString(mentionedRoom?.roomId ?? aliasFromCompletion);
|
||||
}
|
||||
|
||||
return {
|
||||
"data-mention-type": completion.type,
|
||||
"style": `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`,
|
||||
};
|
||||
attributes.set("data-mention-type", completion.type);
|
||||
attributes.set("style", `--avatar-background: url(${avatarUrl}); --avatar-letter: '${initialLetter}'`);
|
||||
} 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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue