commit 29dfd9926f8a71dac9d10b0764062fd426a75c61
parent b0f7d12a96be6651243b35a5aa9729f51308a034
Author: MTRNord <mtrnord1@gmail.com>
Date: Wed, 7 Jun 2023 23:31:20 +0200
Add basic support for mention pills
Diffstat:
13 files changed, 292 insertions(+), 131 deletions(-)
diff --git a/changelog.d/mentions.feature b/changelog.d/mentions.feature
@@ -0,0 +1 @@
+Add basic support for Mention Pills
+\ No newline at end of file
diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts
@@ -102,6 +102,10 @@ export class MatrixClient extends EventEmitter {
return this.user.access_token;
}
+ public get hostname(): string | undefined {
+ return this.user.hostname;
+ }
+
public get isLoggedIn(): boolean {
return this.user.access_token !== undefined;
}
diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts
@@ -1,9 +1,11 @@
import EventEmitter from "events";
import {
IRoomEvent,
+ IRoomMemberEvent,
IRoomStateEvent,
isRoomAvatarEvent,
isRoomCreateEvent,
+ isRoomMemberEvent,
isRoomTopicEvent,
isSpaceChildEvent,
isSpaceParentEvent
@@ -41,6 +43,7 @@ export class Room extends EventEmitter {
private joined_count: number = 0;
private invited_count: number = 0;
private is_dm: boolean = false;
+ private has_all_users: boolean = false;
public windowPos: {
[list: string]: number
@@ -248,11 +251,49 @@ export class Room extends EventEmitter {
return isBot;
}
+ public async joinedMembers(): Promise<IRoomMemberEvent[]> {
+ if (!this.has_all_users) {
+ if (!this.client.hostname) {
+ throw Error("Hostname must be set first");
+ }
+ if (!this.client.accessToken) {
+ throw Error("Access token must be set first");
+ }
+ // We dont have all members so we need to fetch them
+ const resp = await fetch(`${this.client.hostname}/_matrix/client/v3/rooms/${this.roomID}/members`, {
+ headers: {
+ "Authorization": `Bearer ${this.client.accessToken}`
+ }
+ });
+ if (!resp.ok) {
+ if (resp.status === 404 || resp.status === 403) {
+ return this.stateEvents.filter((event) => {
+ if (isRoomMemberEvent(event) && event.content.membership == "join") {
+ return event;
+ }
+ });
+ }
+ console.error(resp);
+ throw Error("Error fetching profile info. See console for error.");
+ }
+ const json = await resp.json() as { chunk: IRoomMemberEvent[] };
+ this.stateEvents = [...this.stateEvents, ...json.chunk];
+ this.has_all_users = true;
+ }
+ return this.stateEvents.filter((event) => {
+ if (isRoomMemberEvent(event) && event.content.membership == "join") {
+ return event;
+ }
+ });
+ }
+
public isEncrypted(): boolean {
let isEncrypted: boolean = false;
this.stateEvents.forEach((event) => {
if (event.type === "m.room.encryption" && event.content.algorithm === "m.megolm.v1.aes-sha2" && event.state_key === "") {
isEncrypted = true;
+ // This gets called at room opening so this works for now
+ this.has_all_users = true;
}
});
return isEncrypted;
diff --git a/src/components/events/messageEvent.scss b/src/components/events/messageEvent.scss
@@ -0,0 +1,32 @@
+#event {
+ #text-event {
+ h1 {
+ @apply text-2xl text-black font-bold;
+ margin: 0;
+ margin-bottom: 12px;
+ padding: 0;
+ }
+
+ h2 {
+ @apply text-xl text-black font-bold;
+ margin: 0;
+ margin-top: 10px;
+ padding: 0;
+ }
+
+ a[href^="https://matrix.to"],
+ a[href^="matrix:"] {
+ @apply rounded-lg bg-slate-300 p-1 select-none;
+ color: #000 !important;
+ display: inline-flex;
+ gap: 0.25rem;
+ line-height: normal;
+ }
+
+ a[href^="https://matrix.to"]::before,
+ a[href^="matrix:"]::before {
+ content: "@";
+ line-height: 101%;
+ }
+ }
+}
+\ No newline at end of file
diff --git a/src/components/events/messageEvent.tsx b/src/components/events/messageEvent.tsx
@@ -13,6 +13,7 @@ import 'highlight.js/styles/base16/solarized-dark.css';
import { OnlineState } from "../../app/sdk/api/otherEnums";
import { Room } from "../../app/sdk/room";
import { MessageWrapper } from "./wrapper";
+import './messageEvent.scss';
type MessageEventProps = {
/**
@@ -269,6 +270,9 @@ const TextMessage: FC<TextMessage> = memo(({ event, room, hasPreviousEvent, mess
}
if (event.content.format === "org.matrix.custom.html") {
+ if (event.content.formatted_body.includes("<a")) {
+ console.log(event)
+ }
let sanitized = DOMPurify.sanitize(event.content.formatted_body!, {
ADD_TAGS: [
"font",
@@ -309,7 +313,8 @@ const TextMessage: FC<TextMessage> = memo(({ event, room, hasPreviousEvent, mess
"img",
"details",
"summary"
- ]
+ ],
+ ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
})
// Extract code and language from the html
const codeRegex = /<pre><code (?:class="language-(?<language>.*?)")?.*?>(?<code>[\s\S]*?)<\/code><\/pre>/;
@@ -339,7 +344,7 @@ const TextMessage: FC<TextMessage> = memo(({ event, room, hasPreviousEvent, mess
hasPreviousEvent={hasPreviousEvent}
>
{/* TODO: Fixme */}
- <p className={`${text_color} text-base font-normal`} dangerouslySetInnerHTML={{ __html: linkified }}></p>
+ <p id="text-event" className={`${text_color} text-base font-normal`} dangerouslySetInnerHTML={{ __html: linkified }}></p>
</MessageWrapper>
)
} else {
@@ -352,7 +357,7 @@ const TextMessage: FC<TextMessage> = memo(({ event, room, hasPreviousEvent, mess
dm={room?.isDM() || false}
hasPreviousEvent={hasPreviousEvent}
>
- <Linkify options={linkifyOptions} as='p' className={`${text_color} text-base font-normal`}>{event.content.body}</Linkify>
+ <Linkify options={linkifyOptions} as='p' id="text-event" className={`${text_color} text-base font-normal`}>{event.content.body}</Linkify>
</MessageWrapper>
)
}
diff --git a/src/components/events/wrapper.tsx b/src/components/events/wrapper.tsx
@@ -45,7 +45,7 @@ export const MessageWrapper: FC<MessageWrapperProps> = memo(({ hasPreviousEvent,
dm={dm}
isBot={isBot}
/>}
- <div className={!hasPreviousEvent ? "flex flex-col gap-1" : "ml-12"}>
+ <div className={!hasPreviousEvent ? "flex flex-col gap-1" : "ml-12"} id="event">
{!hasPreviousEvent && <h2 className="flex flex-row items-center gap-2 text-base font-medium text-red-500 whitespace-pre-wrap">{isBot ? <Bot size={16} /> : <></>}{displayname}</h2>}
{children}
</div>
diff --git a/src/components/input/chat/input.scss b/src/components/input/chat/input.scss
@@ -342,22 +342,17 @@
}
.editor-heading-h1 {
- font-size: 24px;
- color: rgb(5, 5, 5);
- font-weight: 400;
+ @apply text-2xl text-black font-bold;
margin: 0;
margin-bottom: 12px;
padding: 0;
}
.editor-heading-h2 {
- font-size: 15px;
- color: rgb(101, 103, 107);
- font-weight: 700;
+ @apply text-xl text-black font-bold;
margin: 0;
margin-top: 10px;
padding: 0;
- text-transform: uppercase;
}
.editor-listitem {
@@ -485,4 +480,117 @@
.editor-tokenFunction {
color: #dd4a68;
+}
+
+// Mentions
+.typeahead-popover {
+ background: #fff;
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ margin-top: 25px;
+}
+
+.typeahead-popover ul {
+ padding: 0;
+ list-style: none;
+ margin: 0;
+ border-radius: 8px;
+ max-height: 200px;
+ overflow-y: scroll;
+}
+
+.typeahead-popover ul::-webkit-scrollbar {
+ display: none;
+}
+
+.typeahead-popover ul {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.typeahead-popover ul li {
+ margin: 0;
+ min-width: 180px;
+ font-size: 14px;
+ outline: none;
+ cursor: pointer;
+ border-radius: 8px;
+}
+
+.typeahead-popover ul li.selected {
+ background: #eee;
+}
+
+.typeahead-popover li {
+ margin: 0 8px 0 8px;
+ padding: 8px;
+ color: #050505;
+ cursor: pointer;
+ line-height: 16px;
+ font-size: 15px;
+ display: flex;
+ align-content: center;
+ flex-direction: row;
+ flex-shrink: 0;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 0;
+}
+
+.typeahead-popover li.active {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ background-size: contain;
+}
+
+.typeahead-popover li:first-child {
+ border-radius: 8px 8px 0px 0px;
+}
+
+.typeahead-popover li:last-child {
+ border-radius: 0px 0px 8px 8px;
+}
+
+.typeahead-popover li:hover {
+ background-color: #eee;
+}
+
+.typeahead-popover li .text {
+ display: flex;
+ line-height: 20px;
+ flex-grow: 1;
+ min-width: 150px;
+}
+
+.typeahead-popover li .icon {
+ display: flex;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+ margin-right: 8px;
+ line-height: 16px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.mentions-menu {
+ width: 250px;
+}
+
+.mention,
+span[data-lexical-mention] {
+ @apply rounded-md bg-slate-300 p-1 select-none;
+}
+
+
+.mention::before,
+span[data-lexical-mention]::before {
+ content: "@";
+}
+
+.mention:focus {
+ box-shadow: rgb(180 213 255) 0px 0px 0px 2px;
+ outline: none;
}
\ No newline at end of file
diff --git a/src/components/input/chat/input.tsx b/src/components/input/chat/input.tsx
@@ -29,6 +29,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { $getSelection, $isRangeSelection, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_CRITICAL, INSERT_PARAGRAPH_COMMAND, KEY_ENTER_COMMAND, ParagraphNode } from 'lexical';
import { useLocation, } from 'react-router-dom';
import { Room } from '../../../app/sdk/room';
+import { MentionsPlugin } from './plugins/mentions/MentionsPlugin';
+import { MentionNode } from './plugins/mentions/MentionNode';
export const CAN_USE_DOM: boolean =
typeof window !== 'undefined' &&
@@ -116,6 +118,7 @@ const SendButton: FC<SendButtonProps> = ({ onStartSending, onStopSending, room }
editor.getEditorState().read(() => {
let htmlMessage = $generateHtmlFromNodes(editor);
+ console.log("HTML Message", htmlMessage)
// TODO: Make sure that we strip any non matrix stuff
const codeRegex = /(?<all><code .* (?:data-highlight-language="(?<language>.*?)")(?: .*?)?>(?<code>[\s\S]*?)<\/code>)/;
let matched = codeRegex.exec(htmlMessage);
@@ -146,11 +149,36 @@ const SendButton: FC<SendButtonProps> = ({ onStartSending, onStopSending, room }
}
matched = codeRegex.exec(htmlMessage)
}
+ const paragraphRegex = /(?<paragraph> (?:class=".*?"|data-lexical-.*?=".*?")).*?/;
+ let paragraphMatched = paragraphRegex.exec(htmlMessage);
+ while (paragraphMatched !== null) {
+ if (paragraphMatched) {
+ const { groups } = paragraphMatched;
+ if (groups) {
+ const { paragraph } = groups;
+ htmlMessage = htmlMessage.replace(paragraph, "")
+ }
+ }
+ paragraphMatched = paragraphRegex.exec(htmlMessage)
+ }
+ const spanRegex = /(?<span><span.*?>(?<content>.*?)<\/span>).*?/;
+ let spanMatched = spanRegex.exec(htmlMessage);
+ while (spanMatched !== null) {
+ if (spanMatched) {
+ const { groups } = spanMatched;
+ if (groups) {
+ const { span, content } = groups;
+ htmlMessage = htmlMessage.replace(span, content)
+ }
+ }
+ spanMatched = spanRegex.exec(htmlMessage)
+ }
+
const plainMessage = $convertToMarkdownString(TRANSFORMERS);
console.log(htmlMessage)
// TODO: local echo
- if ((htmlMessage === "" && plainMessage === "") || htmlMessage === '<p class="editor-paragraph"><br></p>') {
+ if ((htmlMessage === "" && plainMessage === "") || htmlMessage === '<p><br></p>') {
return;
}
onStartSending();
@@ -286,6 +314,7 @@ const ChatInput: FC<ChatInputProps> = memo(({ namespace, room, id }: ChatInputPr
TableRowNode,
AutoLinkNode,
LinkNode,
+ MentionNode,
CustomParagraphNode,
{
replace: ParagraphNode,
@@ -313,6 +342,7 @@ const ChatInput: FC<ChatInputProps> = memo(({ namespace, room, id }: ChatInputPr
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<ClearEditorPlugin />
<RoomChangePlugin room={room} />
+ <MentionsPlugin room={room} />
{/*<TreeViewPlugin />*/}
</div>
</div>
diff --git a/src/components/input/chat/plugins/ToolbarPlugin.tsx b/src/components/input/chat/plugins/ToolbarPlugin.tsx
@@ -564,7 +564,7 @@ const ToolbarPlugin = memo(() => {
const [portalContainer, setPortalContainer] = useState<HTMLDivElement | null>(null);
useEffect(() => {
- const container = document.getElementById("room-wrapper");
+ const container = document.getElementsByClassName("room-wrapper")[0];
const portalContainer = document.createElement('div');
container?.prepend(portalContainer)
setPortalContainer(portalContainer)
diff --git a/src/components/input/chat/plugins/mentions/MentionNode.ts b/src/components/input/chat/plugins/mentions/MentionNode.ts
@@ -10,10 +10,11 @@ import {
SerializedTextNode,
TextNode
} from "lexical";
+import { IRoomMemberEvent } from "../../../../../app/sdk/api/events";
export type SerializedMentionNode = Spread<
{
- mentionName: string;
+ mentionEvent: IRoomMemberEvent;
type: "mention";
version: 1;
},
@@ -24,9 +25,15 @@ function convertMentionElement(
domNode: HTMLElement
): DOMConversionOutput | null {
const textContent = domNode.textContent;
+ const mxid = domNode.getAttribute("data-lexical-mention-mxid");
- if (textContent !== null) {
- const node = $createMentionNode(textContent);
+ if (textContent && mxid) {
+ const node = $createMentionNode({
+ state_key: mxid,
+ content: {
+ displayname: textContent,
+ }
+ } as IRoomMemberEvent);
return {
node
};
@@ -35,9 +42,8 @@ function convertMentionElement(
return null;
}
-const mentionStyle = "background-color: rgba(24, 119, 232, 0.2)";
export class MentionNode extends TextNode {
- __mention: string;
+ __mention: IRoomMemberEvent;
static getType(): string {
return "mention";
@@ -47,7 +53,7 @@ export class MentionNode extends TextNode {
return new MentionNode(node.__mention, node.__text, node.__key);
}
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
- const node = $createMentionNode(serializedNode.mentionName);
+ const node = $createMentionNode(serializedNode.mentionEvent);
node.setTextContent(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
@@ -56,15 +62,15 @@ export class MentionNode extends TextNode {
return node;
}
- constructor(mentionName: string, text?: string, key?: NodeKey) {
- super(text ?? mentionName, key);
- this.__mention = mentionName;
+ constructor(memberEvent: IRoomMemberEvent, text?: string, key?: NodeKey) {
+ super(text ?? (memberEvent.content.displayname || memberEvent.state_key), key);
+ this.__mention = memberEvent;
}
exportJSON(): SerializedMentionNode {
return {
...super.exportJSON(),
- mentionName: this.__mention,
+ mentionEvent: this.__mention,
type: "mention",
version: 1
};
@@ -72,14 +78,15 @@ export class MentionNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
- dom.style.cssText = mentionStyle;
dom.className = "mention";
return dom;
}
exportDOM(): DOMExportOutput {
- const element = document.createElement("span");
+ const element = document.createElement("a");
+ element.setAttribute("href", `matrix:u/${this.__mention.state_key.replace("@", "")}`);
element.setAttribute("data-lexical-mention", "true");
+ element.setAttribute("data-lexical-mention-mxid", this.__mention.state_key);
element.textContent = this.__text;
return { element };
}
@@ -90,8 +97,8 @@ export class MentionNode extends TextNode {
static importDOM(): DOMConversionMap | null {
return {
- span: (domNode: HTMLElement) => {
- if (!domNode.hasAttribute("data-lexical-mention")) {
+ a: (domNode: HTMLElement) => {
+ if (!domNode.hasAttribute("data-lexical-mention") && !domNode.hasAttribute("data-lexical-mention-mxid")) {
return null;
}
return {
@@ -111,8 +118,8 @@ export class MentionNode extends TextNode {
}
}
-export function $createMentionNode(mentionName: string): MentionNode {
- const mentionNode = new MentionNode(mentionName);
+export function $createMentionNode(memberEvent: IRoomMemberEvent,): MentionNode {
+ const mentionNode = new MentionNode(memberEvent);
mentionNode.setMode("segmented").toggleDirectionless();
return mentionNode;
}
diff --git a/src/components/input/chat/plugins/mentions/MentionsPlugin.tsx b/src/components/input/chat/plugins/mentions/MentionsPlugin.tsx
@@ -6,10 +6,12 @@ import {
useBasicTypeaheadTriggerMatch
} from "@lexical/react/LexicalTypeaheadMenuPlugin";
import { TextNode } from "lexical";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { FC, useCallback, useEffect, useMemo, useState } from "react";
import * as ReactDOM from "react-dom";
import { $createMentionNode } from "./MentionNode";
+import { Room } from "../../../../../app/sdk/room";
+import { IRoomMemberEvent } from "../../../../../app/sdk/api/events";
const PUNCTUATION =
"\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%'\"~=<>_:;";
@@ -80,98 +82,23 @@ const SUGGESTION_LIST_LENGTH_LIMIT = 5;
const mentionsCache = new Map();
-const dummyMentionsData = [
- "Aayla Secura",
- "Admiral Dodd Rancit",
- "Aurra Sing",
- "BB-8",
- "Bo-Katan Kryze",
- "Breha Antilles-Organa",
- "C-3PO",
- "Captain Quarsh Panaka",
- "Chewbacca",
- "Darth Tyranus",
- "Daultay Dofine",
- "Dexter Jettster",
- "Ebe E. Endocott",
- "Eli Vanto",
- "Ezra Bridger",
- "Faro Argyus",
- "Finis Valorum",
- "FN-2003",
- 'Garazeb "Zeb" Orrelios',
- "Grand Inquisitor",
- "Greeata Jendowanian",
- "Hammerhead",
- "Han Solo",
- "Hevy",
- "Hondo Ohnaka",
- "Ima-Gun Di",
- "Inquisitors",
- "Inspector Thanoth",
- "Jabba",
- "Janus Greejatus",
- "Jaxxon",
- "K-2SO",
- "Kanan Jarrus",
- "Kylo Ren",
- "L3-37",
- "Lieutenant Kaydel Ko Connix",
- "Luke Skywalker",
- "Mace Windu",
- "Maximilian Veers",
- "Mother Talzin",
- "Nahdar Vebb",
- "Nahdonnis Praji",
- "Nien Nunb",
- "Obi-Wan Kenobi",
- "Odd Ball",
- "Orrimarko",
- "Petty Officer Thanisson",
- "Pooja Naberrie",
- "PZ-4CO",
- "Quarrie",
- "Quiggold",
- "Quinlan Vos",
- "R2-D2",
- "Raymus Antilles",
- "Ree-Yees",
- "Sana Starros",
- "Shmi Skywalker",
- "Shu Mai",
- "Tallissan Lintra",
- "Tarfful",
- "Thane Kyrell",
- "U9-C4",
- "Unkar Plutt",
- "Val Beckett",
- "Vice Admiral Amilyn Holdo",
- "Vober Dand",
- "WAC-47",
- "Wedge Antilles",
- "Wicket W. Warrick",
- "Xamuel Lennox",
- "Yaddle",
- "Yarael Poof",
- "Yoda",
- "Zam Wesell",
- "Ziro the Hutt",
- "Zuckuss"
-];
-
const dummyLookupService = {
- search(string: string, callback: (results: Array<string>) => void): void {
- setTimeout(() => {
- const results = dummyMentionsData.filter((mention) =>
- mention.toLowerCase().includes(string.toLowerCase())
- );
+ search(string: string, room: Room, callback: (results: IRoomMemberEvent[]) => void): void {
+ setTimeout(async () => {
+ const results = (await room.joinedMembers()).filter((member) => {
+ if (member.content.displayname) {
+ return member.content.displayname.toLowerCase().includes(string.toLowerCase()) || member.state_key.toLowerCase().includes(string.toLowerCase());
+ } else {
+ return member.state_key.toLowerCase().includes(string.toLowerCase());
+ }
+ });
callback(results);
}, 500);
}
};
-function useMentionLookupService(mentionString: string | null) {
- const [results, setResults] = useState<Array<string>>([]);
+function useMentionLookupService(mentionString: string | null, room: Room) {
+ const [results, setResults] = useState<IRoomMemberEvent[]>([]);
useEffect(() => {
const cachedResults = mentionsCache.get(mentionString);
@@ -189,7 +116,7 @@ function useMentionLookupService(mentionString: string | null) {
}
mentionsCache.set(mentionString, null);
- dummyLookupService.search(mentionString, (newResults) => {
+ dummyLookupService.search(mentionString, room, (newResults) => {
mentionsCache.set(mentionString, newResults);
setResults(newResults);
});
@@ -252,12 +179,12 @@ function getPossibleQueryMatch(text: string): QueryMatch | null {
}
class MentionTypeaheadOption extends TypeaheadOption {
- name: string;
+ event: IRoomMemberEvent;
picture: JSX.Element;
- constructor(name: string, picture: JSX.Element) {
- super(name);
- this.name = name;
+ constructor(event: IRoomMemberEvent, picture: JSX.Element) {
+ super(event.content.displayname || event.state_key);
+ this.event = event;
this.picture = picture;
}
}
@@ -292,17 +219,21 @@ function MentionsTypeaheadMenuItem({
onClick={onClick}
>
{option.picture}
- <span className="text">{option.name}</span>
+ <span className="text">{option.event.content.displayname || option.event.state_key}</span>
</li>
);
}
-export default function MentionsPlugin(): JSX.Element | null {
+export type MentionsPluginOptions = {
+ room: Room;
+}
+
+export const MentionsPlugin: FC<MentionsPluginOptions> = ({ room }): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
const [queryString, setQueryString] = useState<string | null>(null);
- const results = useMentionLookupService(queryString);
+ const results = useMentionLookupService(queryString, room);
const checkForSlashTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
minLength: 0
@@ -323,7 +254,7 @@ export default function MentionsPlugin(): JSX.Element | null {
closeMenu: () => void
) => {
editor.update(() => {
- const mentionNode = $createMentionNode(selectedOption.name);
+ const mentionNode = $createMentionNode(selectedOption.event);
if (nodeToReplace) {
nodeToReplace.replace(mentionNode);
}
@@ -345,6 +276,7 @@ export default function MentionsPlugin(): JSX.Element | null {
return (
<LexicalTypeaheadMenuPlugin<MentionTypeaheadOption>
+ anchorClassName="room-wrapper"
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForMentionMatch}
@@ -352,8 +284,7 @@ export default function MentionsPlugin(): JSX.Element | null {
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
- ) =>
- anchorElementRef && results.length
+ ) => anchorElementRef && results.length
? ReactDOM.createPortal(
<div className="typeahead-popover mentions-menu">
<ul>
@@ -375,9 +306,9 @@ export default function MentionsPlugin(): JSX.Element | null {
</ul>
</div>,
// @ts-ignore TODO: fix this
- anchorElementRef
+ anchorElementRef.current
)
- : null
+ : <></>
}
/>
);
diff --git a/src/pages/MainPage.scss b/src/pages/MainPage.scss
@@ -45,7 +45,7 @@
}
}
- #room-wrapper {
+ .room-wrapper {
grid-area: header / header / editor-end / editor-end;
}
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
@@ -485,7 +485,7 @@ const MainPage = memo(() => {
</div>
{
room ? <>
- <div id="room-wrapper"></div>
+ <div className="room-wrapper"></div>
<div id='room-info'>
<Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.presence} isBot={false} />
<div className='flex mr-2'>