feat: Integrate forum layout support across various components and enhance room handling logic
This commit is contained in:
191
src/app/features/forum/ForumChatComposer.tsx
Normal file
191
src/app/features/forum/ForumChatComposer.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { Editor } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Icon, IconButton, Icons, Line, PopOut } from 'folds';
|
||||
import {
|
||||
CustomEditor,
|
||||
Toolbar,
|
||||
EmoticonAutocomplete,
|
||||
AutocompletePrefix,
|
||||
AutocompleteQuery,
|
||||
getAutocompleteQuery,
|
||||
createEmoticonElement,
|
||||
moveCursor,
|
||||
} from '../../components/editor';
|
||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { mobileOrTablet } from '../../utils/user-agent';
|
||||
import { PluginButtonSlot } from '../settings/plugins/PluginButtonSlot';
|
||||
|
||||
type ForumChatComposerProps = {
|
||||
editor: Editor;
|
||||
roomId: string;
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
onSend?: () => void;
|
||||
showAttachButton?: boolean;
|
||||
maxHeight?: string;
|
||||
};
|
||||
|
||||
/** Same chrome as {@link RoomInput} (attach, emoji, toolbar, send). */
|
||||
export function ForumChatComposer({
|
||||
editor,
|
||||
roomId,
|
||||
room,
|
||||
placeholder = 'Send a message…',
|
||||
onSend,
|
||||
showAttachButton = false,
|
||||
maxHeight,
|
||||
}: ForumChatComposerProps) {
|
||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const imagePackRooms = useImagePackRooms(roomId, roomToParents);
|
||||
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const [autocompleteQuery, setAutocompleteQuery] =
|
||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(evt) => {
|
||||
if (
|
||||
onSend &&
|
||||
(isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt)))
|
||||
) {
|
||||
evt.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
},
|
||||
[enterForNewline, onSend]
|
||||
);
|
||||
|
||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||
(evt) => {
|
||||
const prevWordRange = getAutocompleteQuery(editor, evt);
|
||||
if (prevWordRange?.prefix === AutocompletePrefix.Emoticon) {
|
||||
setAutocompleteQuery({ prefix: AutocompletePrefix.Emoticon, range: prevWordRange.range });
|
||||
return;
|
||||
}
|
||||
setAutocompleteQuery(undefined);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const handleEmoticonSelect = (key: string, shortcode: string) => {
|
||||
editor.insertNode(createEmoticonElement(key, shortcode));
|
||||
moveCursor(editor);
|
||||
};
|
||||
|
||||
const handleCloseAutocomplete = useCallback(() => {
|
||||
setAutocompleteQuery(undefined);
|
||||
ReactEditor.focus(editor);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{autocompleteQuery?.prefix === AutocompletePrefix.Emoticon && (
|
||||
<EmoticonAutocomplete
|
||||
imagePackRooms={imagePackRooms}
|
||||
editor={editor}
|
||||
query={autocompleteQuery}
|
||||
requestClose={handleCloseAutocomplete}
|
||||
/>
|
||||
)}
|
||||
<CustomEditor
|
||||
editor={editor}
|
||||
placeholder={placeholder}
|
||||
maxHeight={maxHeight}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
before={showAttachButton ? <PluginButtonSlot location="composer-actions" /> : undefined}
|
||||
after={
|
||||
<>
|
||||
<IconButton
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
type="button"
|
||||
onClick={() => setToolbar(!toolbar)}
|
||||
aria-label="Formatting"
|
||||
>
|
||||
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
|
||||
</IconButton>
|
||||
<UseStateProvider initial={undefined}>
|
||||
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (
|
||||
<PopOut
|
||||
offset={16}
|
||||
alignOffset={-44}
|
||||
position="Top"
|
||||
align="End"
|
||||
anchor={
|
||||
emojiBoardTab === undefined
|
||||
? undefined
|
||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||
}
|
||||
content={
|
||||
<EmojiBoard
|
||||
tab={emojiBoardTab}
|
||||
onTabChange={setEmojiBoardTab}
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return undefined;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
ref={emojiBtnRef}
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Emoji}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
type="button"
|
||||
aria-label="Emoji"
|
||||
>
|
||||
<Icon src={Icons.Smile} filled={emojiBoardTab === EmojiBoardTab.Emoji} />
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
<PluginButtonSlot location="text-composer-toolbar" />
|
||||
{onSend && (
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={onSend}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label="Send"
|
||||
>
|
||||
<Icon src={Icons.Send} />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
bottom={
|
||||
toolbar ? (
|
||||
<div>
|
||||
<Line variant="SurfaceVariant" size="300" />
|
||||
<Toolbar />
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user