cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

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:
Achangelog.d/mentions.feature | 2++
Msrc/app/sdk/client.ts | 4++++
Msrc/app/sdk/room.ts | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/components/events/messageEvent.scss | 33+++++++++++++++++++++++++++++++++
Msrc/components/events/messageEvent.tsx | 11++++++++---
Msrc/components/events/wrapper.tsx | 2+-
Msrc/components/input/chat/input.scss | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/components/input/chat/input.tsx | 32+++++++++++++++++++++++++++++++-
Msrc/components/input/chat/plugins/ToolbarPlugin.tsx | 2+-
Msrc/components/input/chat/plugins/mentions/MentionNode.ts | 39+++++++++++++++++++++++----------------
Msrc/components/input/chat/plugins/mentions/MentionsPlugin.tsx | 131+++++++++++++++++++------------------------------------------------------------
Msrc/pages/MainPage.scss | 2+-
Msrc/pages/MainPage.tsx | 2+-
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'>