feat: implement command handling and custom rendering in editor and message components
This commit is contained in:
@@ -33,6 +33,7 @@ import { TextViewer } from './text-viewer';
|
|||||||
import { testMatrixTo } from '../plugins/matrix-to';
|
import { testMatrixTo } from '../plugins/matrix-to';
|
||||||
import { IImageContent } from '../../types/matrix/common';
|
import { IImageContent } from '../../types/matrix/common';
|
||||||
import { filterDisabledUrls } from '../hooks/useRoomEmbedFilters';
|
import { filterDisabledUrls } from '../hooks/useRoomEmbedFilters';
|
||||||
|
import { pluginRegistry } from '../features/settings/plugins/PluginAPI';
|
||||||
|
|
||||||
type RenderMessageContentProps = {
|
type RenderMessageContentProps = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -309,5 +310,36 @@ export function RenderMessageContent({
|
|||||||
return <MBadEncrypted />;
|
return <MBadEncrypted />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for custom renderer from plugins FIRST
|
||||||
|
const customRenderer = pluginRegistry.getRenderer('message');
|
||||||
|
console.log('[RenderMessageContent] customRenderer function:', typeof customRenderer);
|
||||||
|
if (customRenderer) {
|
||||||
|
try {
|
||||||
|
const messageData = {
|
||||||
|
msgtype: msgType,
|
||||||
|
...getContent(),
|
||||||
|
};
|
||||||
|
console.log('[RenderMessageContent] Calling customRenderer with msgtype:', msgType);
|
||||||
|
console.log('[RenderMessageContent] messageData:', messageData);
|
||||||
|
const customContent = customRenderer(
|
||||||
|
messageData,
|
||||||
|
() => {
|
||||||
|
console.log('[RenderMessageContent] defaultRenderer called');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('[RenderMessageContent] customRenderer returned:', customContent);
|
||||||
|
console.log('[RenderMessageContent] customContent type:', typeof customContent);
|
||||||
|
if (customContent) {
|
||||||
|
console.log('[RenderMessageContent] Returning custom content');
|
||||||
|
return customContent as React.ReactElement;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RenderMessageContent] Custom renderer error:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[RenderMessageContent] No custom renderer registered');
|
||||||
|
}
|
||||||
|
|
||||||
return <UnsupportedContent />;
|
return <UnsupportedContent />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Box, Scroll, Text } from 'folds';
|
import { Box, Scroll, Text } from 'folds';
|
||||||
import { Descendant, Editor, createEditor } from 'slate';
|
import { Descendant, Editor, createEditor, Transforms, Range, Element as SlateElement, Text as SlateText, Point } from 'slate';
|
||||||
import {
|
import {
|
||||||
Slate,
|
Slate,
|
||||||
Editable,
|
Editable,
|
||||||
@@ -23,6 +23,7 @@ import { RenderElement, RenderLeaf } from './Elements';
|
|||||||
import { CustomElement } from './slate';
|
import { CustomElement } from './slate';
|
||||||
import * as css from './Editor.css';
|
import * as css from './Editor.css';
|
||||||
import { toggleKeyboardShortcut } from './keyboard';
|
import { toggleKeyboardShortcut } from './keyboard';
|
||||||
|
import { createCommandElement } from './utils';
|
||||||
|
|
||||||
const initialValue: CustomElement[] = [
|
const initialValue: CustomElement[] = [
|
||||||
{
|
{
|
||||||
@@ -52,8 +53,25 @@ const withVoid = (editor: Editor): Editor => {
|
|||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CommandValidatorFn = (commandName: string) => boolean;
|
||||||
|
|
||||||
|
let commandValidator: CommandValidatorFn | null = null;
|
||||||
|
|
||||||
|
export const setCommandValidator = (validator: CommandValidatorFn) => {
|
||||||
|
commandValidator = validator;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withCommandAutoConvert = (editor: Editor): Editor => {
|
||||||
|
// Removed auto-conversion on space to prevent focus issues
|
||||||
|
// Commands are now highlighted visually via decorations
|
||||||
|
// and executed when Enter is pressed
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
export const useEditor = (): Editor => {
|
export const useEditor = (): Editor => {
|
||||||
const [editor] = useState(() => withInline(withVoid(withReact(withHistory(createEditor())))));
|
const [editor] = useState(() =>
|
||||||
|
withCommandAutoConvert(withInline(withVoid(withReact(withHistory(createEditor())))))
|
||||||
|
);
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,6 +115,49 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||||||
|
|
||||||
const renderLeaf = useCallback((props: RenderLeafProps) => <RenderLeaf {...props} />, []);
|
const renderLeaf = useCallback((props: RenderLeafProps) => <RenderLeaf {...props} />, []);
|
||||||
|
|
||||||
|
const decorate = useCallback(([node, path]: [any, number[]]) => {
|
||||||
|
const ranges: any[] = [];
|
||||||
|
|
||||||
|
// Only decorate text nodes in the first paragraph
|
||||||
|
if (
|
||||||
|
path.length === 2 &&
|
||||||
|
path[0] === 0 &&
|
||||||
|
path[1] === 0 &&
|
||||||
|
SlateText.isText(node)
|
||||||
|
) {
|
||||||
|
const firstChild = editor.children[0];
|
||||||
|
|
||||||
|
if (SlateElement.isElement(firstChild) && firstChild.type === BlockType.Paragraph) {
|
||||||
|
const [firstInline, secondInline] = firstChild.children;
|
||||||
|
|
||||||
|
// Don't decorate if we already have a CommandElement
|
||||||
|
if (SlateElement.isElement(secondInline) && secondInline.type === BlockType.Command) {
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the text matches /command pattern
|
||||||
|
const text = node.text;
|
||||||
|
const match = text.match(/^(\s*\/\w+)/);
|
||||||
|
|
||||||
|
if (match && commandValidator) {
|
||||||
|
const commandText = match[1].trim();
|
||||||
|
const commandName = commandText.substring(1);
|
||||||
|
|
||||||
|
if (commandValidator(commandName)) {
|
||||||
|
// Create decoration for the command text
|
||||||
|
ranges.push({
|
||||||
|
anchor: { path, offset: 0 },
|
||||||
|
focus: { path, offset: match[1].length },
|
||||||
|
pendingCommand: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
const handleKeydown: KeyboardEventHandler = useCallback(
|
const handleKeydown: KeyboardEventHandler = useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
onKeyDown?.(evt);
|
onKeyDown?.(evt);
|
||||||
@@ -143,6 +204,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||||||
renderPlaceholder={renderPlaceholder}
|
renderPlaceholder={renderPlaceholder}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
|
decorate={decorate}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
|
|||||||
@@ -269,6 +269,13 @@ export function RenderLeaf({ attributes, leaf, children }: RenderLeafProps) {
|
|||||||
{child}
|
{child}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
if (leaf.pendingCommand)
|
||||||
|
child = (
|
||||||
|
<span className={css.Command({ active: true })} {...attributes}>
|
||||||
|
<InlineChromiumBugfix />
|
||||||
|
{child}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
if (child !== children) return child;
|
if (child !== children) return child;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './autocomplete';
|
export * from './autocomplete';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './Editor';
|
export { CustomEditor, useEditor, setCommandValidator } from './Editor';
|
||||||
export * from './Elements';
|
export * from './Elements';
|
||||||
export * from './keyboard';
|
export * from './keyboard';
|
||||||
export * from './output';
|
export * from './output';
|
||||||
|
|||||||
1
src/app/components/editor/slate.d.ts
vendored
1
src/app/components/editor/slate.d.ts
vendored
@@ -18,6 +18,7 @@ export type FormattedText = Text & {
|
|||||||
strikeThrough?: boolean;
|
strikeThrough?: boolean;
|
||||||
code?: boolean;
|
code?: boolean;
|
||||||
spoiler?: boolean;
|
spoiler?: boolean;
|
||||||
|
pendingCommand?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LinkElement = {
|
export type LinkElement = {
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { DefaultReset } from 'folds';
|
import { DefaultReset, toRem } from 'folds';
|
||||||
|
|
||||||
export const Image = style([
|
export const Image = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
objectFit: 'cover',
|
display: 'block',
|
||||||
width: '100%',
|
maxWidth: '100%',
|
||||||
height: '100%',
|
maxHeight: '100%',
|
||||||
|
imageRendering: 'auto',
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
transform: 'translateZ(0)',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Video = style([
|
export const Video = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
objectFit: 'contain',
|
display: 'block',
|
||||||
width: '100%',
|
maxWidth: '100%',
|
||||||
height: '100%',
|
maxHeight: '100%',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -195,15 +195,10 @@ export function MImage({ content, renderImageContent, outlined }: MImageProps) {
|
|||||||
if (typeof mxcUrl !== 'string') {
|
if (typeof mxcUrl !== 'string') {
|
||||||
return <BrokenContent />;
|
return <BrokenContent />;
|
||||||
}
|
}
|
||||||
const height = scaleYDimension(imgInfo?.w || 400, 400, imgInfo?.h || 400);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Attachment outlined={outlined}>
|
<Attachment outlined={outlined}>
|
||||||
<AttachmentBox
|
<AttachmentBox>
|
||||||
style={{
|
|
||||||
height: toRem(height < 48 ? 48 : height),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderImageContent({
|
{renderImageContent({
|
||||||
body: content.body || 'Image',
|
body: content.body || 'Image',
|
||||||
info: imgInfo,
|
info: imgInfo,
|
||||||
@@ -245,8 +240,6 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
|
|||||||
return <BrokenContent />;
|
return <BrokenContent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = scaleYDimension(videoInfo.w || 400, 400, videoInfo.h || 400);
|
|
||||||
|
|
||||||
const filename = content.filename ?? content.body ?? 'Video';
|
const filename = content.filename ?? content.body ?? 'Video';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -265,11 +258,7 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</AttachmentHeader>
|
</AttachmentHeader>
|
||||||
<AttachmentBox
|
<AttachmentBox>
|
||||||
style={{
|
|
||||||
height: toRem(height < 48 ? 48 : height),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderVideoContent({
|
{renderVideoContent({
|
||||||
body: content.body || 'Video',
|
body: content.body || 'Video',
|
||||||
info: videoInfo,
|
info: videoInfo,
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ export const Attachment = recipe({
|
|||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
borderRadius: config.radii.R400,
|
borderRadius: config.radii.R400,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maxWidth: '100%',
|
maxWidth: toRem(400),
|
||||||
width: toRem(400),
|
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
outlined: {
|
outlined: {
|
||||||
@@ -29,9 +28,8 @@ export const AttachmentHeader = style({
|
|||||||
export const AttachmentBox = style([
|
export const AttachmentBox = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
maxWidth: '100%',
|
maxWidth: toRem(400),
|
||||||
maxHeight: toRem(600),
|
maxHeight: toRem(400),
|
||||||
width: toRem(400),
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ export const RelativeBase = style([
|
|||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
display: 'flex',
|
||||||
height: '100%',
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const AbsoluteContainer = style([
|
export const AbsoluteContainer = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
display: 'flex',
|
||||||
top: 0,
|
alignItems: 'center',
|
||||||
left: 0,
|
justifyContent: 'center',
|
||||||
width: '100%',
|
maxWidth: '100%',
|
||||||
height: '100%',
|
maxHeight: '100%',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
PopOut,
|
PopOut,
|
||||||
Scroll,
|
Scroll,
|
||||||
Text,
|
Text,
|
||||||
|
color,
|
||||||
config,
|
config,
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
@@ -53,6 +54,7 @@ import {
|
|||||||
getBeginCommand,
|
getBeginCommand,
|
||||||
trimCommand,
|
trimCommand,
|
||||||
getMentions,
|
getMentions,
|
||||||
|
setCommandValidator,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
@@ -186,9 +188,22 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||||
const [autocompleteQuery, setAutocompleteQuery] =
|
const [autocompleteQuery, setAutocompleteQuery] =
|
||||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||||
|
const [commandHint, setCommandHint] = useState<{
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
args?: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
const sendTypingStatus = useTypingStatusUpdater(mx, roomId);
|
const sendTypingStatus = useTypingStatusUpdater(mx, roomId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCommandValidator((commandName: string) => {
|
||||||
|
const isBuiltInCommand = commands[commandName as Command];
|
||||||
|
const isPluginCommand = pluginRegistry.getCommands().some(cmd => cmd.name === commandName);
|
||||||
|
return !!(isBuiltInCommand || isPluginCommand);
|
||||||
|
});
|
||||||
|
}, [commands]);
|
||||||
|
|
||||||
const handleFiles = useCallback(
|
const handleFiles = useCallback(
|
||||||
async (files: File[]) => {
|
async (files: File[]) => {
|
||||||
setUploadBoard(true);
|
setUploadBoard(true);
|
||||||
@@ -312,7 +327,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
const submit = useCallback(async () => {
|
const submit = useCallback(async () => {
|
||||||
uploadBoardHandlers.current?.handleSend();
|
uploadBoardHandlers.current?.handleSend();
|
||||||
|
|
||||||
const commandName = getBeginCommand(editor);
|
let commandName = getBeginCommand(editor);
|
||||||
let plainText = toPlainText(editor.children, isMarkdown).trim();
|
let plainText = toPlainText(editor.children, isMarkdown).trim();
|
||||||
let customHtml = trimCustomHtml(
|
let customHtml = trimCustomHtml(
|
||||||
toMatrixCustomHTML(editor.children, {
|
toMatrixCustomHTML(editor.children, {
|
||||||
@@ -323,6 +338,21 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
);
|
);
|
||||||
let msgType = MsgType.Text;
|
let msgType = MsgType.Text;
|
||||||
|
|
||||||
|
// If no CommandElement, check if text starts with a slash command
|
||||||
|
if (!commandName && plainText.startsWith('/')) {
|
||||||
|
const match = plainText.match(/^\/(\w+)(\s|$)/);
|
||||||
|
if (match) {
|
||||||
|
const potentialCommand = match[1];
|
||||||
|
// Check if it's a valid command (built-in or plugin)
|
||||||
|
const isBuiltInCommand = commands[potentialCommand as Command];
|
||||||
|
const isPluginCommand = pluginRegistry.getCommands().some(cmd => cmd.name === potentialCommand);
|
||||||
|
|
||||||
|
if (isBuiltInCommand || isPluginCommand) {
|
||||||
|
commandName = potentialCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (commandName) {
|
if (commandName) {
|
||||||
plainText = trimCommand(commandName, plainText);
|
plainText = trimCommand(commandName, plainText);
|
||||||
customHtml = trimCommand(commandName, customHtml);
|
customHtml = trimCommand(commandName, customHtml);
|
||||||
@@ -347,13 +377,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
commandContent.exe(plainText);
|
commandContent.exe(plainText);
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
resetEditorHistory(editor);
|
resetEditorHistory(editor);
|
||||||
|
setCommandHint(undefined);
|
||||||
sendTypingStatus(false);
|
sendTypingStatus(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check plugin commands
|
// Check plugin commands
|
||||||
try {
|
try {
|
||||||
const result = await pluginRegistry.executeCommand(commandName, plainText);
|
const result = await pluginRegistry.executeCommand(commandName, plainText, roomId);
|
||||||
if (result) {
|
if (result) {
|
||||||
// If command returns text, send it as a message
|
// If command returns text, send it as a message
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
@@ -368,6 +399,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
}
|
}
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
resetEditorHistory(editor);
|
resetEditorHistory(editor);
|
||||||
|
setCommandHint(undefined);
|
||||||
sendTypingStatus(false);
|
sendTypingStatus(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -445,6 +477,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
resetEditorHistory(editor);
|
resetEditorHistory(editor);
|
||||||
setReplyDraft(undefined);
|
setReplyDraft(undefined);
|
||||||
|
setCommandHint(undefined);
|
||||||
sendTypingStatus(false);
|
sendTypingStatus(false);
|
||||||
}, [mx, roomId, threadRootId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons]);
|
}, [mx, roomId, threadRootId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons]);
|
||||||
|
|
||||||
@@ -463,10 +496,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
setAutocompleteQuery(undefined);
|
setAutocompleteQuery(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (commandHint) {
|
||||||
|
setCommandHint(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setReplyDraft(undefined);
|
setReplyDraft(undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing]
|
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing, commandHint]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
@@ -485,8 +522,35 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
? getAutocompleteQuery<AutocompletePrefix>(editor, prevWordRange, AUTOCOMPLETE_PREFIXES)
|
? getAutocompleteQuery<AutocompletePrefix>(editor, prevWordRange, AUTOCOMPLETE_PREFIXES)
|
||||||
: undefined;
|
: undefined;
|
||||||
setAutocompleteQuery(query);
|
setAutocompleteQuery(query);
|
||||||
|
|
||||||
|
// Check if user is typing a command and show hint
|
||||||
|
const plainText = toPlainText(editor.children, false).trim();
|
||||||
|
const commandMatch = plainText.match(/^\/(\w+)(\s|$)/);
|
||||||
|
|
||||||
|
if (commandMatch) {
|
||||||
|
const commandName = commandMatch[1];
|
||||||
|
const builtInCmd = commands[commandName as Command];
|
||||||
|
const pluginCmd = pluginRegistry.getCommands().find(cmd => cmd.name === commandName);
|
||||||
|
|
||||||
|
if (builtInCmd) {
|
||||||
|
setCommandHint({
|
||||||
|
command: commandName,
|
||||||
|
description: builtInCmd.description,
|
||||||
|
});
|
||||||
|
} else if (pluginCmd) {
|
||||||
|
setCommandHint({
|
||||||
|
command: commandName,
|
||||||
|
description: pluginCmd.command.description || '',
|
||||||
|
args: pluginCmd.command.args,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCommandHint(undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCommandHint(undefined);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[editor, sendTypingStatus, hideActivity]
|
[editor, sendTypingStatus, hideActivity, commands]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCloseAutocomplete = useCallback(() => {
|
const handleCloseAutocomplete = useCallback(() => {
|
||||||
@@ -597,7 +661,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
requestClose={handleCloseAutocomplete}
|
requestClose={handleCloseAutocomplete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{autocompleteQuery?.prefix === AutocompletePrefix.Command && (
|
{autocompleteQuery?.prefix === AutocompletePrefix.Command && !commandHint && (
|
||||||
<CommandAutocomplete
|
<CommandAutocomplete
|
||||||
room={room}
|
room={room}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
@@ -614,43 +678,72 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
top={
|
top={
|
||||||
replyDraft && (
|
<>
|
||||||
<div>
|
{commandHint && (
|
||||||
<Box
|
<Box
|
||||||
alignItems="Center"
|
style={{
|
||||||
gap="300"
|
padding: `${config.space.S200} ${config.space.S300}`,
|
||||||
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
|
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||||
|
borderBottom: 'none',
|
||||||
|
borderTopLeftRadius: config.radii.R400,
|
||||||
|
borderTopRightRadius: config.radii.R400,
|
||||||
|
marginBottom: config.space.S100,
|
||||||
|
}}
|
||||||
|
direction="Column"
|
||||||
|
gap="100"
|
||||||
>
|
>
|
||||||
<IconButton
|
<Text size="T200" priority="400">
|
||||||
onClick={() => setReplyDraft(undefined)}
|
<b>/{commandHint.command}</b>
|
||||||
variant="SurfaceVariant"
|
{commandHint.args && commandHint.args.length > 0 && (
|
||||||
size="300"
|
<span style={{ opacity: 0.6 }}> {commandHint.args.map(arg => `[${arg}]`).join(' ')}</span>
|
||||||
radii="300"
|
)}
|
||||||
>
|
</Text>
|
||||||
<Icon src={Icons.Cross} size="50" />
|
{commandHint.description && (
|
||||||
</IconButton>
|
<Text size="T200" priority="300" style={{ opacity: 0.7 }}>
|
||||||
<Box direction="Row" gap="200" alignItems="Center">
|
{commandHint.description}
|
||||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
</Text>
|
||||||
<ReplyLayout
|
)}
|
||||||
userColor={replyUsernameColor}
|
|
||||||
username={
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
<b>
|
|
||||||
{getMemberDisplayName(room, replyDraft.userId) ??
|
|
||||||
getMxIdLocalPart(replyDraft.userId) ??
|
|
||||||
replyDraft.userId}
|
|
||||||
</b>
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
{trimReplyFromBody(replyDraft.body)}
|
|
||||||
</Text>
|
|
||||||
</ReplyLayout>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
)}
|
||||||
)
|
{replyDraft && (
|
||||||
|
<div>
|
||||||
|
<Box
|
||||||
|
alignItems="Center"
|
||||||
|
gap="300"
|
||||||
|
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setReplyDraft(undefined)}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.Cross} size="50" />
|
||||||
|
</IconButton>
|
||||||
|
<Box direction="Row" gap="200" alignItems="Center">
|
||||||
|
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||||
|
<ReplyLayout
|
||||||
|
userColor={replyUsernameColor}
|
||||||
|
username={
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>
|
||||||
|
{getMemberDisplayName(room, replyDraft.userId) ??
|
||||||
|
getMxIdLocalPart(replyDraft.userId) ??
|
||||||
|
replyDraft.userId}
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{trimReplyFromBody(replyDraft.body)}
|
||||||
|
</Text>
|
||||||
|
</ReplyLayout>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
before={
|
before={
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ interface PluginLoaderProps {
|
|||||||
* Should be placed high in the component tree after MatrixClient is available.
|
* Should be placed high in the component tree after MatrixClient is available.
|
||||||
*/
|
*/
|
||||||
export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
||||||
|
console.log('[PluginLoader] Component rendering, matrixClient:', !!matrixClient);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPlugins = async () => {
|
const loadPlugins = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('[PluginLoader] Loading plugins...');
|
console.log('[PluginLoader] Loading plugins...');
|
||||||
|
console.log('[PluginLoader] window.electron:', !!window.electron);
|
||||||
|
console.log('[PluginLoader] window.electron.plugins:', !!window.electron?.plugins);
|
||||||
|
|
||||||
if (!window.electron?.plugins) {
|
if (!window.electron?.plugins) {
|
||||||
console.log('[PluginLoader] Running in web mode, plugins not supported');
|
console.log('[PluginLoader] Running in web mode, plugins not supported');
|
||||||
@@ -27,6 +31,7 @@ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
|||||||
const pluginsToLoad = await pluginMarketplaceManager.listEnabledPlugins();
|
const pluginsToLoad = await pluginMarketplaceManager.listEnabledPlugins();
|
||||||
|
|
||||||
console.log('[PluginLoader] Plugins to load:', pluginsToLoad.length);
|
console.log('[PluginLoader] Plugins to load:', pluginsToLoad.length);
|
||||||
|
console.log('[PluginLoader] Plugin list:', pluginsToLoad.map(p => p.id));
|
||||||
|
|
||||||
for (const installedPlugin of pluginsToLoad) {
|
for (const installedPlugin of pluginsToLoad) {
|
||||||
try {
|
try {
|
||||||
@@ -65,6 +70,7 @@ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const compatContext = Object.assign(context as Record<string, unknown>, {
|
const compatContext = Object.assign(context as Record<string, unknown>, {
|
||||||
|
matrixClient,
|
||||||
matrix: {
|
matrix: {
|
||||||
on: (eventType: string, handler: (...args: unknown[]) => void) =>
|
on: (eventType: string, handler: (...args: unknown[]) => void) =>
|
||||||
matrixClient.on(eventType as any, handler as any),
|
matrixClient.on(eventType as any, handler as any),
|
||||||
@@ -74,6 +80,10 @@ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
|||||||
React,
|
React,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[PluginLoader] compatContext.React:', !!compatContext.React);
|
||||||
|
console.log('[PluginLoader] compatContext.ui:', !!compatContext.ui);
|
||||||
|
console.log('[PluginLoader] About to register plugin and call onLoad...');
|
||||||
|
|
||||||
pluginRegistry.registerPlugin(installedPlugin.id, plugin, context);
|
pluginRegistry.registerPlugin(installedPlugin.id, plugin, context);
|
||||||
await plugin.onLoad(compatContext as any);
|
await plugin.onLoad(compatContext as any);
|
||||||
console.log(`[PluginLoader] Successfully loaded plugin: ${installedPlugin.id}`);
|
console.log(`[PluginLoader] Successfully loaded plugin: ${installedPlugin.id}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user