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
This commit is contained in:
parent
c7e5c1fce8
commit
2957a45c4b
13 changed files with 162 additions and 71 deletions
|
@ -66,3 +66,7 @@ export const EditorToolbarBase = style({
|
||||||
export const EditorToolbar = style({
|
export const EditorToolbar = style({
|
||||||
padding: config.space.S100,
|
padding: config.space.S100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MarkdownBtnBox = style({
|
||||||
|
paddingRight: config.space.S100,
|
||||||
|
});
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import React, { ReactNode, useState } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
import { ReactEditor, useSlate } from 'slate-react';
|
import { ReactEditor, useSlate } from 'slate-react';
|
||||||
import {
|
import {
|
||||||
|
headingLevel,
|
||||||
isAnyMarkActive,
|
isAnyMarkActive,
|
||||||
isBlockActive,
|
isBlockActive,
|
||||||
isMarkActive,
|
isMarkActive,
|
||||||
|
@ -31,6 +32,8 @@ import { BlockType, MarkType } from './types';
|
||||||
import { HeadingLevel } from './slate';
|
import { HeadingLevel } from './slate';
|
||||||
import { isMacOS } from '../../utils/user-agent';
|
import { isMacOS } from '../../utils/user-agent';
|
||||||
import { KeySymbol } from '../../utils/key-symbol';
|
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 }) {
|
function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
|
||||||
return (
|
return (
|
||||||
|
@ -115,13 +118,13 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
||||||
|
|
||||||
export function HeadingBlockButton() {
|
export function HeadingBlockButton() {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const [level, setLevel] = useState<HeadingLevel>(1);
|
const level = headingLevel(editor);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const isActive = isBlockActive(editor, BlockType.Heading);
|
const isActive = isBlockActive(editor, BlockType.Heading);
|
||||||
|
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||||
|
|
||||||
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setLevel(selectedLevel);
|
|
||||||
toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
|
toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
|
||||||
ReactEditor.focus(editor);
|
ReactEditor.focus(editor);
|
||||||
};
|
};
|
||||||
|
@ -130,7 +133,6 @@ export function HeadingBlockButton() {
|
||||||
<PopOut
|
<PopOut
|
||||||
open={open}
|
open={open}
|
||||||
offset={5}
|
offset={5}
|
||||||
align="Start"
|
|
||||||
position="Top"
|
position="Top"
|
||||||
content={
|
content={
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
|
@ -145,15 +147,51 @@ export function HeadingBlockButton() {
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
<Box gap="100">
|
<Box gap="100">
|
||||||
<IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
|
<TooltipProvider
|
||||||
<Icon size="200" src={Icons.Heading1} />
|
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + Shift + 1`} />}
|
||||||
</IconButton>
|
delay={500}
|
||||||
<IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
|
>
|
||||||
<Icon size="200" src={Icons.Heading2} />
|
{(triggerRef) => (
|
||||||
</IconButton>
|
<IconButton
|
||||||
<IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
|
ref={triggerRef}
|
||||||
<Icon size="200" src={Icons.Heading3} />
|
onClick={() => handleMenuSelect(1)}
|
||||||
</IconButton>
|
size="400"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon size="200" src={Icons.Heading1} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider
|
||||||
|
tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + Shift + 2`} />}
|
||||||
|
delay={500}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={() => handleMenuSelect(2)}
|
||||||
|
size="400"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon size="200" src={Icons.Heading2} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider
|
||||||
|
tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + Shift + 3`} />}
|
||||||
|
delay={500}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={() => handleMenuSelect(3)}
|
||||||
|
size="400"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon size="200" src={Icons.Heading3} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</Menu>
|
</Menu>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
@ -169,7 +207,7 @@ export function HeadingBlockButton() {
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Icon size="200" src={Icons[`Heading${level}`]} />
|
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
|
||||||
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
@ -210,8 +248,10 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
|
||||||
export function Toolbar() {
|
export function Toolbar() {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||||
|
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
||||||
|
|
||||||
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
|
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
|
||||||
|
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={css.EditorToolbarBase}>
|
<Box className={css.EditorToolbarBase}>
|
||||||
|
@ -271,12 +311,12 @@ export function Toolbar() {
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.OrderedList}
|
format={BlockType.OrderedList}
|
||||||
icon={Icons.OrderList}
|
icon={Icons.OrderList}
|
||||||
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
|
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + Shift + 7`} />}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.UnorderedList}
|
format={BlockType.UnorderedList}
|
||||||
icon={Icons.UnorderList}
|
icon={Icons.UnorderList}
|
||||||
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
|
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + Shift + 8`} />}
|
||||||
/>
|
/>
|
||||||
<HeadingBlockButton />
|
<HeadingBlockButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -292,6 +332,28 @@ export function Toolbar() {
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Box className={css.MarkdownBtnBox} shrink="No" grow="Yes" justifyContent="End">
|
||||||
|
<TooltipProvider
|
||||||
|
align="End"
|
||||||
|
tooltip={<BtnTooltip text="Inline Markdown" />}
|
||||||
|
delay={500}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={triggerRef}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
onClick={() => setIsMarkdown(!isMarkdown)}
|
||||||
|
aria-pressed={isMarkdown}
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
disabled={disableInline || !!isAnyMarkActive(editor)}
|
||||||
|
>
|
||||||
|
<Icon size="200" src={Icons.Markdown} filled={isMarkdown} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
<span />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -15,12 +15,15 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
|
||||||
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
|
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
|
||||||
|
|
||||||
export const BLOCK_HOTKEYS: Record<string, BlockType> = {
|
export const BLOCK_HOTKEYS: Record<string, BlockType> = {
|
||||||
'mod+7': BlockType.OrderedList,
|
'mod+shift+7': BlockType.OrderedList,
|
||||||
'mod+8': BlockType.UnorderedList,
|
'mod+shift+8': BlockType.UnorderedList,
|
||||||
"mod+'": BlockType.BlockQuote,
|
"mod+'": BlockType.BlockQuote,
|
||||||
'mod+;': BlockType.CodeBlock,
|
'mod+;': BlockType.CodeBlock,
|
||||||
};
|
};
|
||||||
const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS);
|
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.
|
* @return boolean true if shortcut is toggled.
|
||||||
|
@ -86,6 +89,18 @@ export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Elem
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (blockToggled) return true;
|
if (blockToggled) return true;
|
||||||
|
if (isHeading1(event)) {
|
||||||
|
toggleBlock(editor, BlockType.Heading, { level: 1 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isHeading2(event)) {
|
||||||
|
toggleBlock(editor, BlockType.Heading, { level: 2 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isHeading3(event)) {
|
||||||
|
toggleBlock(editor, BlockType.Heading, { level: 3 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const inlineToggled = isBlockActive(editor, BlockType.CodeBlock)
|
const inlineToggled = isBlockActive(editor, BlockType.CodeBlock)
|
||||||
? false
|
? false
|
||||||
|
|
|
@ -52,6 +52,16 @@ export const isBlockActive = (editor: Editor, format: BlockType) => {
|
||||||
return !!match;
|
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 };
|
type BlockOption = { level: HeadingLevel };
|
||||||
const NESTED_BLOCK = [
|
const NESTED_BLOCK = [
|
||||||
BlockType.OrderedList,
|
BlockType.OrderedList,
|
||||||
|
|
|
@ -68,6 +68,7 @@ export type EmojiItemInfo = {
|
||||||
type: EmojiType;
|
type: EmojiType;
|
||||||
data: string;
|
data: string;
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
|
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 getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
|
||||||
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
|
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
|
||||||
const data = element.getAttribute('data-emoji-data');
|
const data = element.getAttribute('data-emoji-data');
|
||||||
|
const label = element.getAttribute('title');
|
||||||
const shortcode = element.getAttribute('data-emoji-shortcode');
|
const shortcode = element.getAttribute('data-emoji-shortcode');
|
||||||
|
|
||||||
if (type && data && shortcode)
|
if (type && data && shortcode && label)
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
shortcode,
|
shortcode,
|
||||||
|
label,
|
||||||
};
|
};
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -633,7 +636,7 @@ export function EmojiBoard({
|
||||||
returnFocusOnDeactivate?: boolean;
|
returnFocusOnDeactivate?: boolean;
|
||||||
onEmojiSelect?: (unicode: string, shortcode: string) => void;
|
onEmojiSelect?: (unicode: string, shortcode: string) => void;
|
||||||
onCustomEmojiSelect?: (mxc: 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;
|
allowTextCustomEmoji?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const emojiTab = tab === EmojiBoardTab.Emoji;
|
const emojiTab = tab === EmojiBoardTab.Emoji;
|
||||||
|
@ -712,7 +715,7 @@ export function EmojiBoard({
|
||||||
if (!evt.altKey && !evt.shiftKey) requestClose();
|
if (!evt.altKey && !evt.shiftKey) requestClose();
|
||||||
}
|
}
|
||||||
if (emojiInfo.type === EmojiType.Sticker) {
|
if (emojiInfo.type === EmojiType.Sticker) {
|
||||||
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode);
|
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
|
||||||
if (!evt.altKey && !evt.shiftKey) requestClose();
|
if (!evt.altKey && !evt.shiftKey) requestClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -783,7 +786,7 @@ export function EmojiBoard({
|
||||||
data-emoji-board-search
|
data-emoji-board-search
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
size="400"
|
size="400"
|
||||||
placeholder="Search"
|
placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
after={
|
after={
|
||||||
allowTextCustomEmoji && result?.query ? (
|
allowTextCustomEmoji && result?.query ? (
|
||||||
|
@ -791,6 +794,7 @@ export function EmojiBoard({
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
after={<Icon src={Icons.ArrowRight} size="50" />}
|
after={<Icon src={Icons.ArrowRight} size="50" />}
|
||||||
|
outlined
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const searchInput = document.querySelector<HTMLInputElement>(
|
const searchInput = document.querySelector<HTMLInputElement>(
|
||||||
'[data-emoji-board-search="true"]'
|
'[data-emoji-board-search="true"]'
|
||||||
|
|
|
@ -29,7 +29,6 @@ import {
|
||||||
config,
|
config,
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import to from 'await-to-js';
|
|
||||||
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import {
|
import {
|
||||||
|
@ -216,30 +215,24 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendUpload = async (uploads: UploadSuccess[]) => {
|
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);
|
const fileItem = selectedFiles.find((f) => f.file === upload.file);
|
||||||
if (fileItem && fileItem.file.type.startsWith('image')) {
|
if (!fileItem) throw new Error('Broken upload');
|
||||||
const [imgError, imgContent] = await to(getImageMsgContent(mx, fileItem, upload.mxc));
|
|
||||||
if (imgError) console.warn(imgError);
|
if (fileItem.file.type.startsWith('image')) {
|
||||||
if (imgContent) mx.sendMessage(roomId, imgContent);
|
return getImageMsgContent(mx, fileItem, upload.mxc);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (fileItem && fileItem.file.type.startsWith('video')) {
|
if (fileItem.file.type.startsWith('video')) {
|
||||||
const [videoError, videoContent] = await to(getVideoMsgContent(mx, fileItem, upload.mxc));
|
return getVideoMsgContent(mx, fileItem, upload.mxc);
|
||||||
if (videoError) console.warn(videoError);
|
|
||||||
if (videoContent) mx.sendMessage(roomId, videoContent);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (fileItem && fileItem.file.type.startsWith('audio')) {
|
if (fileItem.file.type.startsWith('audio')) {
|
||||||
mx.sendMessage(roomId, getAudioMsgContent(fileItem, upload.mxc));
|
return getAudioMsgContent(fileItem, upload.mxc);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fileItem) {
|
|
||||||
mx.sendMessage(roomId, getFileMsgContent(fileItem, upload.mxc));
|
|
||||||
}
|
}
|
||||||
|
return getFileMsgContent(fileItem, upload.mxc);
|
||||||
});
|
});
|
||||||
handleCancelUpload(uploads);
|
handleCancelUpload(uploads);
|
||||||
await Promise.allSettled(sendPromises);
|
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
|
||||||
|
contents.forEach((content) => mx.sendMessage(roomId, content));
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = useCallback(() => {
|
const submit = useCallback(() => {
|
||||||
|
@ -319,7 +312,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) {
|
if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
|
@ -359,7 +352,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
moveCursor(editor);
|
moveCursor(editor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStickerSelect = async (mxc: string, shortcode: string) => {
|
const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => {
|
||||||
const stickerUrl = mx.mxcUrlToHttp(mxc);
|
const stickerUrl = mx.mxcUrlToHttp(mxc);
|
||||||
if (!stickerUrl) return;
|
if (!stickerUrl) return;
|
||||||
|
|
||||||
|
@ -369,7 +362,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
mx.sendEvent(roomId, EventType.Sticker, {
|
mx.sendEvent(roomId, EventType.Sticker, {
|
||||||
body: shortcode,
|
body: label,
|
||||||
url: mxc,
|
url: mxc,
|
||||||
info,
|
info,
|
||||||
});
|
});
|
||||||
|
|
|
@ -392,7 +392,7 @@ export const MessageDeleteItem = as<
|
||||||
variant="Critical"
|
variant="Critical"
|
||||||
before={
|
before={
|
||||||
deleteState.status === AsyncStatus.Loading ? (
|
deleteState.status === AsyncStatus.Loading ? (
|
||||||
<Spinner fill="Soft" variant="Critical" size="200" />
|
<Spinner fill="Solid" variant="Critical" size="200" />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
aria-disabled={deleteState.status === AsyncStatus.Loading}
|
aria-disabled={deleteState.status === AsyncStatus.Loading}
|
||||||
|
@ -522,7 +522,7 @@ export const MessageReportItem = as<
|
||||||
variant="Critical"
|
variant="Critical"
|
||||||
before={
|
before={
|
||||||
reportState.status === AsyncStatus.Loading ? (
|
reportState.status === AsyncStatus.Loading ? (
|
||||||
<Spinner fill="Soft" variant="Critical" size="200" />
|
<Spinner fill="Solid" variant="Critical" size="200" />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
aria-disabled={
|
aria-disabled={
|
||||||
|
@ -702,7 +702,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
||||||
if (evt.altKey || !window.getSelection()?.isCollapsed) return;
|
if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return;
|
||||||
const tag = (evt.target as any).tagName;
|
const tag = (evt.target as any).tagName;
|
||||||
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
|
@ -129,7 +129,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) {
|
if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
handleSave();
|
handleSave();
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||||
position="Top"
|
position="Top"
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip style={{ maxWidth: toRem(200) }}>
|
<Tooltip style={{ maxWidth: toRem(200) }}>
|
||||||
<Text size="T300">
|
<Text className={css.ReactionsTooltipText} size="T300">
|
||||||
<ReactionTooltipMsg room={room} reaction={key} events={rEvents} />
|
<ReactionTooltipMsg room={room} reaction={key} events={rEvents} />
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -79,3 +79,7 @@ export const ReactionsContainer = style({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ReactionsTooltipText = style({
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
});
|
||||||
|
|
|
@ -54,23 +54,10 @@ export const getImageMsgContent = async (
|
||||||
};
|
};
|
||||||
if (imgEl) {
|
if (imgEl) {
|
||||||
const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height));
|
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 = {
|
content.info = {
|
||||||
...getImageInfo(imgEl, file),
|
...getImageInfo(imgEl, file),
|
||||||
[MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash,
|
[MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash,
|
||||||
...thumbContent,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (encInfo) {
|
if (encInfo) {
|
||||||
|
|
|
@ -45,6 +45,8 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg';
|
||||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
import { isMacOS } from '../../utils/user-agent';
|
||||||
|
import { KeySymbol } from '../../utils/key-symbol';
|
||||||
|
|
||||||
function AppearanceSection() {
|
function AppearanceSection() {
|
||||||
const [, updateState] = useState({});
|
const [, updateState] = useState({});
|
||||||
|
@ -147,7 +149,7 @@ function AppearanceSection() {
|
||||||
onToggle={() => setEnterForNewline(!enterForNewline) }
|
onToggle={() => setEnterForNewline(!enterForNewline) }
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
content={<Text variant="b3">Use SHIFT + ENTER to send message and ENTER for newline.</Text>}
|
content={<Text variant="b3">{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}</Text>}
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Inline Markdown formatting"
|
title="Inline Markdown formatting"
|
||||||
|
|
|
@ -23,24 +23,28 @@ export type RulesRunner = (
|
||||||
) => string | undefined;
|
) => string | undefined;
|
||||||
|
|
||||||
const MIN_ANY = '(.+?)';
|
const MIN_ANY = '(.+?)';
|
||||||
|
const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
|
||||||
|
|
||||||
const BOLD_MD_1 = '**';
|
const BOLD_MD_1 = '**';
|
||||||
const BOLD_PREFIX_1 = '\\*{2}';
|
const BOLD_PREFIX_1 = '\\*{2}';
|
||||||
const BOLD_NEG_LA_1 = '(?!\\*)';
|
const BOLD_NEG_LA_1 = '(?!\\*)';
|
||||||
const BOLD_REG_1 = new RegExp(`${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`);
|
const BOLD_REG_1 = new RegExp(
|
||||||
|
`${URL_NEG_LB}${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`
|
||||||
|
);
|
||||||
const BoldRule: MDRule = {
|
const BoldRule: MDRule = {
|
||||||
match: (text) => text.match(BOLD_REG_1),
|
match: (text) => text.match(BOLD_REG_1),
|
||||||
html: (parse, match) => {
|
html: (parse, match) => {
|
||||||
const [, g1] = match;
|
const [, g1] = match;
|
||||||
const child = parse(g1);
|
return `<strong data-md="${BOLD_MD_1}">${parse(g1)}</strong>`;
|
||||||
return `<strong data-md="${BOLD_MD_1}">${child}</strong>`;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ITALIC_MD_1 = '*';
|
const ITALIC_MD_1 = '*';
|
||||||
const ITALIC_PREFIX_1 = '\\*';
|
const ITALIC_PREFIX_1 = '\\*';
|
||||||
const ITALIC_NEG_LA_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 = {
|
const ItalicRule1: MDRule = {
|
||||||
match: (text) => text.match(ITALIC_REG_1),
|
match: (text) => text.match(ITALIC_REG_1),
|
||||||
html: (parse, match) => {
|
html: (parse, match) => {
|
||||||
|
@ -52,7 +56,9 @@ const ItalicRule1: MDRule = {
|
||||||
const ITALIC_MD_2 = '_';
|
const ITALIC_MD_2 = '_';
|
||||||
const ITALIC_PREFIX_2 = '_';
|
const ITALIC_PREFIX_2 = '_';
|
||||||
const ITALIC_NEG_LA_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 = {
|
const ItalicRule2: MDRule = {
|
||||||
match: (text) => text.match(ITALIC_REG_2),
|
match: (text) => text.match(ITALIC_REG_2),
|
||||||
html: (parse, match) => {
|
html: (parse, match) => {
|
||||||
|
@ -65,7 +71,7 @@ const UNDERLINE_MD_1 = '__';
|
||||||
const UNDERLINE_PREFIX_1 = '_{2}';
|
const UNDERLINE_PREFIX_1 = '_{2}';
|
||||||
const UNDERLINE_NEG_LA_1 = '(?!_)';
|
const UNDERLINE_NEG_LA_1 = '(?!_)';
|
||||||
const UNDERLINE_REG_1 = new RegExp(
|
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 = {
|
const UnderlineRule: MDRule = {
|
||||||
match: (text) => text.match(UNDERLINE_REG_1),
|
match: (text) => text.match(UNDERLINE_REG_1),
|
||||||
|
@ -78,7 +84,9 @@ const UnderlineRule: MDRule = {
|
||||||
const STRIKE_MD_1 = '~~';
|
const STRIKE_MD_1 = '~~';
|
||||||
const STRIKE_PREFIX_1 = '~{2}';
|
const STRIKE_PREFIX_1 = '~{2}';
|
||||||
const STRIKE_NEG_LA_1 = '(?!~)';
|
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 = {
|
const StrikeRule: MDRule = {
|
||||||
match: (text) => text.match(STRIKE_REG_1),
|
match: (text) => text.match(STRIKE_REG_1),
|
||||||
html: (parse, match) => {
|
html: (parse, match) => {
|
||||||
|
@ -90,7 +98,9 @@ const StrikeRule: MDRule = {
|
||||||
const CODE_MD_1 = '`';
|
const CODE_MD_1 = '`';
|
||||||
const CODE_PREFIX_1 = '`';
|
const CODE_PREFIX_1 = '`';
|
||||||
const CODE_NEG_LA_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 = {
|
const CodeRule: MDRule = {
|
||||||
match: (text) => text.match(CODE_REG_1),
|
match: (text) => text.match(CODE_REG_1),
|
||||||
html: (parse, match) => {
|
html: (parse, match) => {
|
||||||
|
@ -103,7 +113,7 @@ const SPOILER_MD_1 = '||';
|
||||||
const SPOILER_PREFIX_1 = '\\|{2}';
|
const SPOILER_PREFIX_1 = '\\|{2}';
|
||||||
const SPOILER_NEG_LA_1 = '(?!\\|)';
|
const SPOILER_NEG_LA_1 = '(?!\\|)';
|
||||||
const SPOILER_REG_1 = new RegExp(
|
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 = {
|
const SpoilerRule: MDRule = {
|
||||||
match: (text) => text.match(SPOILER_REG_1),
|
match: (text) => text.match(SPOILER_REG_1),
|
||||||
|
|
Loading…
Reference in a new issue