feat: implement command handling and custom rendering in editor and message components
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
PopOut,
|
||||
Scroll,
|
||||
Text,
|
||||
color,
|
||||
config,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
getBeginCommand,
|
||||
trimCommand,
|
||||
getMentions,
|
||||
setCommandValidator,
|
||||
} from '../../components/editor';
|
||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
@@ -186,9 +188,22 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [autocompleteQuery, setAutocompleteQuery] =
|
||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||
const [commandHint, setCommandHint] = useState<{
|
||||
command: string;
|
||||
description: string;
|
||||
args?: string[];
|
||||
}>();
|
||||
|
||||
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(
|
||||
async (files: File[]) => {
|
||||
setUploadBoard(true);
|
||||
@@ -312,7 +327,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const submit = useCallback(async () => {
|
||||
uploadBoardHandlers.current?.handleSend();
|
||||
|
||||
const commandName = getBeginCommand(editor);
|
||||
let commandName = getBeginCommand(editor);
|
||||
let plainText = toPlainText(editor.children, isMarkdown).trim();
|
||||
let customHtml = trimCustomHtml(
|
||||
toMatrixCustomHTML(editor.children, {
|
||||
@@ -323,6 +338,21 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
);
|
||||
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) {
|
||||
plainText = trimCommand(commandName, plainText);
|
||||
customHtml = trimCommand(commandName, customHtml);
|
||||
@@ -347,13 +377,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
commandContent.exe(plainText);
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
setCommandHint(undefined);
|
||||
sendTypingStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check plugin commands
|
||||
try {
|
||||
const result = await pluginRegistry.executeCommand(commandName, plainText);
|
||||
const result = await pluginRegistry.executeCommand(commandName, plainText, roomId);
|
||||
if (result) {
|
||||
// If command returns text, send it as a message
|
||||
const content: IContent = {
|
||||
@@ -368,6 +399,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
}
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
setCommandHint(undefined);
|
||||
sendTypingStatus(false);
|
||||
return;
|
||||
}
|
||||
@@ -445,6 +477,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
setReplyDraft(undefined);
|
||||
setCommandHint(undefined);
|
||||
sendTypingStatus(false);
|
||||
}, [mx, roomId, threadRootId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons]);
|
||||
|
||||
@@ -463,10 +496,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
setAutocompleteQuery(undefined);
|
||||
return;
|
||||
}
|
||||
if (commandHint) {
|
||||
setCommandHint(undefined);
|
||||
return;
|
||||
}
|
||||
setReplyDraft(undefined);
|
||||
}
|
||||
},
|
||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing]
|
||||
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing, commandHint]
|
||||
);
|
||||
|
||||
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||
@@ -485,8 +522,35 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
? getAutocompleteQuery<AutocompletePrefix>(editor, prevWordRange, AUTOCOMPLETE_PREFIXES)
|
||||
: undefined;
|
||||
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(() => {
|
||||
@@ -597,7 +661,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
requestClose={handleCloseAutocomplete}
|
||||
/>
|
||||
)}
|
||||
{autocompleteQuery?.prefix === AutocompletePrefix.Command && (
|
||||
{autocompleteQuery?.prefix === AutocompletePrefix.Command && !commandHint && (
|
||||
<CommandAutocomplete
|
||||
room={room}
|
||||
editor={editor}
|
||||
@@ -614,43 +678,72 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
onKeyUp={handleKeyUp}
|
||||
onPaste={handlePaste}
|
||||
top={
|
||||
replyDraft && (
|
||||
<div>
|
||||
<>
|
||||
{commandHint && (
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="300"
|
||||
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
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
|
||||
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>
|
||||
<Text size="T200" priority="400">
|
||||
<b>/{commandHint.command}</b>
|
||||
{commandHint.args && commandHint.args.length > 0 && (
|
||||
<span style={{ opacity: 0.6 }}> {commandHint.args.map(arg => `[${arg}]`).join(' ')}</span>
|
||||
)}
|
||||
</Text>
|
||||
{commandHint.description && (
|
||||
<Text size="T200" priority="300" style={{ opacity: 0.7 }}>
|
||||
{commandHint.description}
|
||||
</Text>
|
||||
)}
|
||||
</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={
|
||||
<IconButton
|
||||
|
||||
@@ -14,10 +14,14 @@ interface PluginLoaderProps {
|
||||
* Should be placed high in the component tree after MatrixClient is available.
|
||||
*/
|
||||
export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
||||
console.log('[PluginLoader] Component rendering, matrixClient:', !!matrixClient);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPlugins = async () => {
|
||||
try {
|
||||
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) {
|
||||
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();
|
||||
|
||||
console.log('[PluginLoader] Plugins to load:', pluginsToLoad.length);
|
||||
console.log('[PluginLoader] Plugin list:', pluginsToLoad.map(p => p.id));
|
||||
|
||||
for (const installedPlugin of pluginsToLoad) {
|
||||
try {
|
||||
@@ -65,6 +70,7 @@ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
||||
);
|
||||
|
||||
const compatContext = Object.assign(context as Record<string, unknown>, {
|
||||
matrixClient,
|
||||
matrix: {
|
||||
on: (eventType: string, handler: (...args: unknown[]) => void) =>
|
||||
matrixClient.on(eventType as any, handler as any),
|
||||
@@ -74,6 +80,10 @@ export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
|
||||
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);
|
||||
await plugin.onLoad(compatContext as any);
|
||||
console.log(`[PluginLoader] Successfully loaded plugin: ${installedPlugin.id}`);
|
||||
|
||||
Reference in New Issue
Block a user