From 2957a45c4ba1088787611ad7b9982cdfaccee642 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:50:38 +1100 Subject: [PATCH] Room input improvements (#1502) * prevent context menu when editing message * send sticker body (#1479) * update emojiboard search text reaction input label * stop generating upload image thumbnail (#1475) * maintain upload order * Fix message options spinner variant * add markdown toggle in editor toolbar * fix heading toggle icon update with cursor move * add hotkeys for heading * change editor markdown btn style * use Ctrl + Enter to send message (#1470) * fix reaction tooltip word-break * add shift in editor hokeys with number * stop parsing markdown in link --- src/app/components/editor/Editor.css.ts | 4 + src/app/components/editor/Toolbar.tsx | 92 ++++++++++++++++--- src/app/components/editor/keyboard.ts | 19 +++- src/app/components/editor/utils.ts | 10 ++ src/app/components/emoji-board/EmojiBoard.tsx | 12 ++- src/app/organisms/room/RoomInput.tsx | 37 +++----- src/app/organisms/room/message/Message.tsx | 6 +- .../organisms/room/message/MessageEditor.tsx | 2 +- src/app/organisms/room/message/Reactions.tsx | 2 +- src/app/organisms/room/message/styles.css.ts | 4 + src/app/organisms/room/msgContent.ts | 13 --- src/app/organisms/settings/Settings.jsx | 4 +- src/app/utils/markdown.ts | 28 ++++-- 13 files changed, 162 insertions(+), 71 deletions(-) diff --git a/src/app/components/editor/Editor.css.ts b/src/app/components/editor/Editor.css.ts index edce743..09a444e 100644 --- a/src/app/components/editor/Editor.css.ts +++ b/src/app/components/editor/Editor.css.ts @@ -66,3 +66,7 @@ export const EditorToolbarBase = style({ export const EditorToolbar = style({ padding: config.space.S100, }); + +export const MarkdownBtnBox = style({ + paddingRight: config.space.S100, +}); diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx index 766a1d8..6feae00 100644 --- a/src/app/components/editor/Toolbar.tsx +++ b/src/app/components/editor/Toolbar.tsx @@ -19,6 +19,7 @@ import { import React, { ReactNode, useState } from 'react'; import { ReactEditor, useSlate } from 'slate-react'; import { + headingLevel, isAnyMarkActive, isBlockActive, isMarkActive, @@ -31,6 +32,8 @@ import { BlockType, MarkType } from './types'; import { HeadingLevel } from './slate'; import { isMacOS } from '../../utils/user-agent'; import { KeySymbol } from '../../utils/key-symbol'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) { return ( @@ -115,13 +118,13 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) { export function HeadingBlockButton() { const editor = useSlate(); - const [level, setLevel] = useState(1); + const level = headingLevel(editor); const [open, setOpen] = useState(false); const isActive = isBlockActive(editor, BlockType.Heading); + const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; const handleMenuSelect = (selectedLevel: HeadingLevel) => { setOpen(false); - setLevel(selectedLevel); toggleBlock(editor, BlockType.Heading, { level: selectedLevel }); ReactEditor.focus(editor); }; @@ -130,7 +133,6 @@ export function HeadingBlockButton() { - handleMenuSelect(1)} size="400" radii="300"> - - - handleMenuSelect(2)} size="400" radii="300"> - - - handleMenuSelect(3)} size="400" radii="300"> - - + } + delay={500} + > + {(triggerRef) => ( + handleMenuSelect(1)} + size="400" + radii="300" + > + + + )} + + } + delay={500} + > + {(triggerRef) => ( + handleMenuSelect(2)} + size="400" + radii="300" + > + + + )} + + } + delay={500} + > + {(triggerRef) => ( + handleMenuSelect(3)} + size="400" + radii="300" + > + + + )} + @@ -169,7 +207,7 @@ export function HeadingBlockButton() { size="400" radii="300" > - + )} @@ -210,8 +248,10 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) { export function Toolbar() { const editor = useSlate(); const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; + const disableInline = isBlockActive(editor, BlockType.CodeBlock); const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph); + const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); return ( @@ -271,12 +311,12 @@ export function Toolbar() { } + tooltip={} /> } + tooltip={} /> @@ -292,6 +332,28 @@ export function Toolbar() { )} + + } + delay={500} + > + {(triggerRef) => ( + setIsMarkdown(!isMarkdown)} + aria-pressed={isMarkdown} + size="300" + radii="300" + disabled={disableInline || !!isAnyMarkActive(editor)} + > + + + )} + + + diff --git a/src/app/components/editor/keyboard.ts b/src/app/components/editor/keyboard.ts index 2ea2dbe..370f3e8 100644 --- a/src/app/components/editor/keyboard.ts +++ b/src/app/components/editor/keyboard.ts @@ -15,12 +15,15 @@ export const INLINE_HOTKEYS: Record = { const INLINE_KEYS = Object.keys(INLINE_HOTKEYS); export const BLOCK_HOTKEYS: Record = { - 'mod+7': BlockType.OrderedList, - 'mod+8': BlockType.UnorderedList, + 'mod+shift+7': BlockType.OrderedList, + 'mod+shift+8': BlockType.UnorderedList, "mod+'": BlockType.BlockQuote, 'mod+;': BlockType.CodeBlock, }; const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS); +const isHeading1 = isKeyHotkey('mod+shift+1'); +const isHeading2 = isKeyHotkey('mod+shift+2'); +const isHeading3 = isKeyHotkey('mod+shift+3'); /** * @return boolean true if shortcut is toggled. @@ -86,6 +89,18 @@ export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent { return !!match; }; +export const headingLevel = (editor: Editor): HeadingLevel | undefined => { + const [nodeEntry] = Editor.nodes(editor, { + match: (node) => Element.isElement(node) && node.type === BlockType.Heading, + }); + const [node] = nodeEntry ?? []; + if (!node) return undefined; + if ('level' in node) return node.level; + return undefined; +}; + type BlockOption = { level: HeadingLevel }; const NESTED_BLOCK = [ BlockType.OrderedList, diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 52df925..5452722 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -68,6 +68,7 @@ export type EmojiItemInfo = { type: EmojiType; data: string; shortcode: string; + label: string; }; const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; @@ -75,13 +76,15 @@ const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; const data = element.getAttribute('data-emoji-data'); + const label = element.getAttribute('title'); const shortcode = element.getAttribute('data-emoji-shortcode'); - if (type && data && shortcode) + if (type && data && shortcode && label) return { type, data, shortcode, + label, }; return undefined; }; @@ -633,7 +636,7 @@ export function EmojiBoard({ returnFocusOnDeactivate?: boolean; onEmojiSelect?: (unicode: string, shortcode: string) => void; onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; - onStickerSelect?: (mxc: string, shortcode: string) => void; + onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; allowTextCustomEmoji?: boolean; }) { const emojiTab = tab === EmojiBoardTab.Emoji; @@ -712,7 +715,7 @@ export function EmojiBoard({ if (!evt.altKey && !evt.shiftKey) requestClose(); } if (emojiInfo.type === EmojiType.Sticker) { - onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode); + onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label); if (!evt.altKey && !evt.shiftKey) requestClose(); } }; @@ -783,7 +786,7 @@ export function EmojiBoard({ data-emoji-board-search variant="SurfaceVariant" size="400" - placeholder="Search" + placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'} maxLength={50} after={ allowTextCustomEmoji && result?.query ? ( @@ -791,6 +794,7 @@ export function EmojiBoard({ variant="Primary" radii="Pill" after={} + outlined onClick={() => { const searchInput = document.querySelector( '[data-emoji-board-search="true"]' diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index f7b219b..8dc4e64 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -29,7 +29,6 @@ import { config, toRem, } from 'folds'; -import to from 'await-to-js'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { @@ -216,30 +215,24 @@ export const RoomInput = forwardRef( }; const handleSendUpload = async (uploads: UploadSuccess[]) => { - const sendPromises = uploads.map(async (upload) => { + const contentsPromises = uploads.map(async (upload) => { const fileItem = selectedFiles.find((f) => f.file === upload.file); - if (fileItem && fileItem.file.type.startsWith('image')) { - const [imgError, imgContent] = await to(getImageMsgContent(mx, fileItem, upload.mxc)); - if (imgError) console.warn(imgError); - if (imgContent) mx.sendMessage(roomId, imgContent); - return; + if (!fileItem) throw new Error('Broken upload'); + + if (fileItem.file.type.startsWith('image')) { + return getImageMsgContent(mx, fileItem, upload.mxc); } - if (fileItem && fileItem.file.type.startsWith('video')) { - const [videoError, videoContent] = await to(getVideoMsgContent(mx, fileItem, upload.mxc)); - if (videoError) console.warn(videoError); - if (videoContent) mx.sendMessage(roomId, videoContent); - return; + if (fileItem.file.type.startsWith('video')) { + return getVideoMsgContent(mx, fileItem, upload.mxc); } - if (fileItem && fileItem.file.type.startsWith('audio')) { - mx.sendMessage(roomId, getAudioMsgContent(fileItem, upload.mxc)); - return; - } - if (fileItem) { - mx.sendMessage(roomId, getFileMsgContent(fileItem, upload.mxc)); + if (fileItem.file.type.startsWith('audio')) { + return getAudioMsgContent(fileItem, upload.mxc); } + return getFileMsgContent(fileItem, upload.mxc); }); handleCancelUpload(uploads); - await Promise.allSettled(sendPromises); + const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); + contents.forEach((content) => mx.sendMessage(roomId, content)); }; const submit = useCallback(() => { @@ -319,7 +312,7 @@ export const RoomInput = forwardRef( const handleKeyDown: KeyboardEventHandler = useCallback( (evt) => { - if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) { + if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) { evt.preventDefault(); submit(); } @@ -359,7 +352,7 @@ export const RoomInput = forwardRef( moveCursor(editor); }; - const handleStickerSelect = async (mxc: string, shortcode: string) => { + const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => { const stickerUrl = mx.mxcUrlToHttp(mxc); if (!stickerUrl) return; @@ -369,7 +362,7 @@ export const RoomInput = forwardRef( ); mx.sendEvent(roomId, EventType.Sticker, { - body: shortcode, + body: label, url: mxc, info, }); diff --git a/src/app/organisms/room/message/Message.tsx b/src/app/organisms/room/message/Message.tsx index 14996a9..25d894f 100644 --- a/src/app/organisms/room/message/Message.tsx +++ b/src/app/organisms/room/message/Message.tsx @@ -392,7 +392,7 @@ export const MessageDeleteItem = as< variant="Critical" before={ deleteState.status === AsyncStatus.Loading ? ( - + ) : undefined } aria-disabled={deleteState.status === AsyncStatus.Loading} @@ -522,7 +522,7 @@ export const MessageReportItem = as< variant="Critical" before={ reportState.status === AsyncStatus.Loading ? ( - + ) : undefined } aria-disabled={ @@ -702,7 +702,7 @@ export const Message = as<'div', MessageProps>( ); const handleContextMenu: MouseEventHandler = (evt) => { - if (evt.altKey || !window.getSelection()?.isCollapsed) return; + if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return; const tag = (evt.target as any).tagName; if (typeof tag === 'string' && tag.toLowerCase() === 'a') return; evt.preventDefault(); diff --git a/src/app/organisms/room/message/MessageEditor.tsx b/src/app/organisms/room/message/MessageEditor.tsx index 776e1d4..0756c38 100644 --- a/src/app/organisms/room/message/MessageEditor.tsx +++ b/src/app/organisms/room/message/MessageEditor.tsx @@ -129,7 +129,7 @@ export const MessageEditor = as<'div', MessageEditorProps>( const handleKeyDown: KeyboardEventHandler = useCallback( (evt) => { - if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) { + if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) { evt.preventDefault(); handleSave(); } diff --git a/src/app/organisms/room/message/Reactions.tsx b/src/app/organisms/room/message/Reactions.tsx index bc32c1a..17b914e 100644 --- a/src/app/organisms/room/message/Reactions.tsx +++ b/src/app/organisms/room/message/Reactions.tsx @@ -68,7 +68,7 @@ export const Reactions = as<'div', ReactionsProps>( position="Top" tooltip={ - + diff --git a/src/app/organisms/room/message/styles.css.ts b/src/app/organisms/room/message/styles.css.ts index 9cb0f2e..a5f2f6b 100644 --- a/src/app/organisms/room/message/styles.css.ts +++ b/src/app/organisms/room/message/styles.css.ts @@ -79,3 +79,7 @@ export const ReactionsContainer = style({ }, }, }); + +export const ReactionsTooltipText = style({ + wordBreak: 'break-all', +}); diff --git a/src/app/organisms/room/msgContent.ts b/src/app/organisms/room/msgContent.ts index e4cf1cb..0760ec9 100644 --- a/src/app/organisms/room/msgContent.ts +++ b/src/app/organisms/room/msgContent.ts @@ -54,23 +54,10 @@ export const getImageMsgContent = async ( }; if (imgEl) { const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); - const [thumbError, thumbContent] = await to( - generateThumbnailContent( - mx, - imgEl, - getThumbnailDimensions(imgEl.width, imgEl.height), - !!encInfo - ) - ); - if (thumbContent && thumbContent.thumbnail_info) { - thumbContent.thumbnail_info[MATRIX_BLUR_HASH_PROPERTY_NAME] = blurHash; - } - if (thumbError) console.warn(thumbError); content.info = { ...getImageInfo(imgEl, file), [MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash, - ...thumbContent, }; } if (encInfo) { diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 962a80b..ae094ef 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -45,6 +45,8 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; +import { isMacOS } from '../../utils/user-agent'; +import { KeySymbol } from '../../utils/key-symbol'; function AppearanceSection() { const [, updateState] = useState({}); @@ -147,7 +149,7 @@ function AppearanceSection() { onToggle={() => setEnterForNewline(!enterForNewline) } /> )} - content={Use SHIFT + ENTER to send message and ENTER for newline.} + content={{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}} /> string | undefined; const MIN_ANY = '(.+?)'; +const URL_NEG_LB = '(? text.match(BOLD_REG_1), html: (parse, match) => { const [, g1] = match; - const child = parse(g1); - return `${child}`; + return `${parse(g1)}`; }, }; const ITALIC_MD_1 = '*'; const ITALIC_PREFIX_1 = '\\*'; const ITALIC_NEG_LA_1 = '(?!\\*)'; -const ITALIC_REG_1 = new RegExp(`${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`); +const ITALIC_REG_1 = new RegExp( + `${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}` +); const ItalicRule1: MDRule = { match: (text) => text.match(ITALIC_REG_1), html: (parse, match) => { @@ -52,7 +56,9 @@ const ItalicRule1: MDRule = { const ITALIC_MD_2 = '_'; const ITALIC_PREFIX_2 = '_'; const ITALIC_NEG_LA_2 = '(?!_)'; -const ITALIC_REG_2 = new RegExp(`${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`); +const ITALIC_REG_2 = new RegExp( + `${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}` +); const ItalicRule2: MDRule = { match: (text) => text.match(ITALIC_REG_2), html: (parse, match) => { @@ -65,7 +71,7 @@ const UNDERLINE_MD_1 = '__'; const UNDERLINE_PREFIX_1 = '_{2}'; const UNDERLINE_NEG_LA_1 = '(?!_)'; const UNDERLINE_REG_1 = new RegExp( - `${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` + `${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` ); const UnderlineRule: MDRule = { match: (text) => text.match(UNDERLINE_REG_1), @@ -78,7 +84,9 @@ const UnderlineRule: MDRule = { const STRIKE_MD_1 = '~~'; const STRIKE_PREFIX_1 = '~{2}'; const STRIKE_NEG_LA_1 = '(?!~)'; -const STRIKE_REG_1 = new RegExp(`${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`); +const STRIKE_REG_1 = new RegExp( + `${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}` +); const StrikeRule: MDRule = { match: (text) => text.match(STRIKE_REG_1), html: (parse, match) => { @@ -90,7 +98,9 @@ const StrikeRule: MDRule = { const CODE_MD_1 = '`'; const CODE_PREFIX_1 = '`'; const CODE_NEG_LA_1 = '(?!`)'; -const CODE_REG_1 = new RegExp(`${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`); +const CODE_REG_1 = new RegExp( + `${URL_NEG_LB}${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}` +); const CodeRule: MDRule = { match: (text) => text.match(CODE_REG_1), html: (parse, match) => { @@ -103,7 +113,7 @@ const SPOILER_MD_1 = '||'; const SPOILER_PREFIX_1 = '\\|{2}'; const SPOILER_NEG_LA_1 = '(?!\\|)'; const SPOILER_REG_1 = new RegExp( - `${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` + `${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` ); const SpoilerRule: MDRule = { match: (text) => text.match(SPOILER_REG_1),