feat(plugins): implement plugin loading and management system
- Added PluginLoader component to dynamically load and initialize plugins. - Created Plugins component for managing installed and marketplace plugins. - Introduced PluginAPI for plugin interaction and settings management. - Defined types for plugin metadata, installed plugins, and plugin index. - Implemented settings rendering for plugins based on their schema. - Integrated marketplace plugin fetching and installation logic. - Added support for enabling/disabling and uninstalling plugins.
This commit is contained in:
@@ -3,6 +3,7 @@ import { Editor } from 'slate';
|
||||
import { Box, config, MenuItem, Text } from 'folds';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Command, useCommands } from '../../hooks/useCommands';
|
||||
import { pluginRegistry } from '../settings/plugins/PluginAPI';
|
||||
import {
|
||||
AutocompleteMenu,
|
||||
AutocompleteQuery,
|
||||
@@ -38,7 +39,13 @@ export function CommandAutocomplete({
|
||||
}: CommandAutocompleteProps) {
|
||||
const mx = useMatrixClient();
|
||||
const commands = useCommands(mx, room);
|
||||
const commandNames = useMemo(() => Object.keys(commands) as Command[], [commands]);
|
||||
|
||||
// Merge built-in commands with plugin commands
|
||||
const commandNames = useMemo(() => {
|
||||
const builtInCommands = Object.keys(commands) as Command[];
|
||||
const pluginCommands = pluginRegistry.getCommands().map(cmd => cmd.name);
|
||||
return [...builtInCommands, ...pluginCommands];
|
||||
}, [commands]);
|
||||
|
||||
const [result, search, resetSearch] = useAsyncSearch(
|
||||
commandNames,
|
||||
@@ -79,33 +86,40 @@ export function CommandAutocomplete({
|
||||
}
|
||||
requestClose={requestClose}
|
||||
>
|
||||
{autoCompleteNames.map((commandName) => (
|
||||
<MenuItem
|
||||
key={commandName}
|
||||
as="button"
|
||||
radii="300"
|
||||
style={{ height: 'unset' }}
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
onTabPress(evt, () => handleAutocomplete(commandName))
|
||||
}
|
||||
onClick={() => handleAutocomplete(commandName)}
|
||||
>
|
||||
<Box
|
||||
style={{ padding: `${config.space.S300} 0` }}
|
||||
grow="Yes"
|
||||
direction="Column"
|
||||
gap="100"
|
||||
justifyContent="SpaceBetween"
|
||||
{autoCompleteNames.map((commandName) => {
|
||||
// Get description from built-in commands or plugin commands
|
||||
const builtInCmd = commands[commandName as Command];
|
||||
const pluginCmd = pluginRegistry.getCommands().find(cmd => cmd.name === commandName);
|
||||
const description = builtInCmd?.description || pluginCmd?.command.description || '';
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={commandName}
|
||||
as="button"
|
||||
radii="300"
|
||||
style={{ height: 'unset' }}
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
onTabPress(evt, () => handleAutocomplete(commandName))
|
||||
}
|
||||
onClick={() => handleAutocomplete(commandName)}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
<Text truncate priority="300" size="T200">
|
||||
{commands[commandName].description}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Box
|
||||
style={{ padding: `${config.space.S300} 0` }}
|
||||
grow="Yes"
|
||||
direction="Column"
|
||||
gap="100"
|
||||
justifyContent="SpaceBetween"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
<Text truncate priority="300" size="T200">
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</AutocompleteMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
import { getMemberDisplayName, getMentionContent, trimReplyFromBody } from '../../utils/room';
|
||||
import { CommandAutocomplete } from './CommandAutocomplete';
|
||||
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands';
|
||||
import { pluginRegistry } from '../settings/plugins/PluginAPI';
|
||||
import { mobileOrTablet } from '../../utils/user-agent';
|
||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||
import { ReplyLayout, ThreadIndicator } from '../../components/message';
|
||||
@@ -308,7 +309,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
contents.forEach((content) => mx.sendMessage(roomId, content as any));
|
||||
};
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const submit = useCallback(async () => {
|
||||
uploadBoardHandlers.current?.handleSend();
|
||||
|
||||
const commandName = getBeginCommand(editor);
|
||||
@@ -340,9 +341,30 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
plainText = `${UNFLIP} ${plainText}`;
|
||||
customHtml = `${UNFLIP} ${customHtml}`;
|
||||
} else if (commandName) {
|
||||
// Check built-in commands first
|
||||
const commandContent = commands[commandName as Command];
|
||||
if (commandContent) {
|
||||
commandContent.exe(plainText);
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
sendTypingStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check plugin commands
|
||||
try {
|
||||
const result = await pluginRegistry.executeCommand(commandName, plainText);
|
||||
if (result) {
|
||||
// If command returns text, send it as a message
|
||||
const content: IContent = {
|
||||
msgtype: MsgType.Text,
|
||||
body: result,
|
||||
};
|
||||
mx.sendMessage(roomId, content as any);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[RoomInput] Plugin command /${commandName} failed:`, err);
|
||||
// Show error to user (could integrate with notification system)
|
||||
}
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
@@ -398,6 +420,27 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
'm.in_reply_to': { event_id: threadRootId },
|
||||
};
|
||||
}
|
||||
|
||||
// Process message through plugin interceptors
|
||||
try {
|
||||
const messageContext = {
|
||||
content: content.body,
|
||||
roomId,
|
||||
eventType: 'm.room.message',
|
||||
formatted: content.formatted_body,
|
||||
metadata: {},
|
||||
};
|
||||
const processedContext = await pluginRegistry.processBeforeSend(messageContext);
|
||||
|
||||
// Update content with intercepted values
|
||||
content.body = processedContext.content;
|
||||
if (processedContext.formatted) {
|
||||
content.formatted_body = processedContext.formatted;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[RoomInput] Plugin interceptor error:', err);
|
||||
}
|
||||
|
||||
mx.sendMessage(roomId, content as any);
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
|
||||
Reference in New Issue
Block a user