export const EditorToolbar = style({
padding: config.space.S100,
});
+
+export const MarkdownBtnBox = style({
+ paddingRight: config.space.S100,
+});
import React, { ReactNode, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react';
import {
+ headingLevel,
isAnyMarkActive,
isBlockActive,
isMarkActive,
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 (
export function HeadingBlockButton() {
const editor = useSlate();
- const [level, setLevel] = useState<HeadingLevel>(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);
};
<PopOut
open={open}
offset={5}
- align="Start"
position="Top"
content={
<FocusTrap
>
<Menu style={{ padding: config.space.S100 }}>
<Box gap="100">
- <IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
- <Icon size="200" src={Icons.Heading1} />
- </IconButton>
- <IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
- <Icon size="200" src={Icons.Heading2} />
- </IconButton>
- <IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
- <Icon size="200" src={Icons.Heading3} />
- </IconButton>
+ <TooltipProvider
+ tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + Shift + 1`} />}
+ delay={500}
+ >
+ {(triggerRef) => (
+ <IconButton
+ ref={triggerRef}
+ onClick={() => handleMenuSelect(1)}
+ 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>
</Menu>
</FocusTrap>
size="400"
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} />
</IconButton>
)}
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 (
<Box className={css.EditorToolbarBase}>
<BlockButton
format={BlockType.OrderedList}
icon={Icons.OrderList}
- tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
+ tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + Shift + 7`} />}
/>
<BlockButton
format={BlockType.UnorderedList}
icon={Icons.UnorderList}
- tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
+ tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + Shift + 8`} />}
/>
<HeadingBlockButton />
</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>
</Scroll>
</Box>
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
export const BLOCK_HOTKEYS: Record<string, BlockType> = {
- '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.
return false;
});
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)
? false
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,
type: EmojiType;
data: string;
shortcode: string;
+ label: string;
};
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;
};
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;
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();
}
};
data-emoji-board-search
variant="SurfaceVariant"
size="400"
- placeholder="Search"
+ placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
maxLength={50}
after={
allowTextCustomEmoji && result?.query ? (
variant="Primary"
radii="Pill"
after={<Icon src={Icons.ArrowRight} size="50" />}
+ outlined
onClick={() => {
const searchInput = document.querySelector<HTMLInputElement>(
'[data-emoji-board-search="true"]'
config,
toRem,
} from 'folds';
-import to from 'await-to-js';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import {
};
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 && 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) throw new Error('Broken upload');
+
+ if (fileItem.file.type.startsWith('image')) {
+ return getImageMsgContent(mx, fileItem, upload.mxc);
}
- if (fileItem && fileItem.file.type.startsWith('audio')) {
- mx.sendMessage(roomId, getAudioMsgContent(fileItem, upload.mxc));
- return;
+ if (fileItem.file.type.startsWith('video')) {
+ return getVideoMsgContent(mx, fileItem, upload.mxc);
}
- 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(() => {
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();
}
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;
);
mx.sendEvent(roomId, EventType.Sticker, {
- body: shortcode,
+ body: label,
url: mxc,
info,
});
variant="Critical"
before={
deleteState.status === AsyncStatus.Loading ? (
- <Spinner fill="Soft" variant="Critical" size="200" />
+ <Spinner fill="Solid" variant="Critical" size="200" />
) : undefined
}
aria-disabled={deleteState.status === AsyncStatus.Loading}
variant="Critical"
before={
reportState.status === AsyncStatus.Loading ? (
- <Spinner fill="Soft" variant="Critical" size="200" />
+ <Spinner fill="Solid" variant="Critical" size="200" />
) : undefined
}
aria-disabled={
);
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;
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
evt.preventDefault();
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();
}
position="Top"
tooltip={
<Tooltip style={{ maxWidth: toRem(200) }}>
- <Text size="T300">
+ <Text className={css.ReactionsTooltipText} size="T300">
<ReactionTooltipMsg room={room} reaction={key} events={rEvents} />
</Text>
</Tooltip>
},
},
});
+
+export const ReactionsTooltipText = style({
+ wordBreak: 'break-all',
+});
};
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) {
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({});
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
title="Inline Markdown formatting"
) => string | undefined;
const MIN_ANY = '(.+?)';
+const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
const BOLD_MD_1 = '**';
const BOLD_PREFIX_1 = '\\*{2}';
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 = {
match: (text) => text.match(BOLD_REG_1),
html: (parse, match) => {
const [, g1] = match;
- const child = parse(g1);
- return `<strong data-md="${BOLD_MD_1}">${child}</strong>`;
+ return `<strong data-md="${BOLD_MD_1}">${parse(g1)}</strong>`;
},
};
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) => {
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) => {
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),
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) => {
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) => {
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),