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:
2026-04-18 02:55:44 +10:00
parent 9eb5e4fa32
commit d1d3033c15
13 changed files with 2674 additions and 49 deletions

View File

@@ -0,0 +1,331 @@
import React, { useEffect, useState } from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import { Plugin, PluginContext, pluginRegistry } from './PluginAPI';
import { InstalledPlugin } from './types';
import { sendNotification } from '../../../utils/tauri';
interface PluginLoaderProps {
matrixClient: MatrixClient;
children: React.ReactNode;
}
/**
* Component that loads and initializes enabled plugins.
* Should be placed high in the component tree after MatrixClient is available.
*/
export function PluginLoader({ matrixClient, children }: PluginLoaderProps) {
const [loading, setLoading] = useState(true);
const [loadedPlugins, setLoadedPlugins] = useState<string[]>([]);
useEffect(() => {
let mounted = true;
const loadPlugins = async () => {
try {
console.log('[PluginLoader] Loading plugins...');
// Get list of installed plugins from Electron
if (!window.electron?.plugins) {
console.log('[PluginLoader] Running in web mode, plugins not supported');
setLoading(false);
return;
}
const response = await window.electron.plugins.list();
if (!response.success || !response.data) {
console.error('[PluginLoader] Failed to list plugins:', response.error);
setLoading(false);
return;
}
const installedPlugins = response.data;
console.log('[PluginLoader] Found installed plugins:', installedPlugins.length);
// Check which plugins are enabled in localStorage
const enabledPluginsStr = localStorage.getItem('enabledPlugins');
const enabledPlugins = enabledPluginsStr ? JSON.parse(enabledPluginsStr) : {};
const pluginsToLoad = installedPlugins.filter(
(plugin: any) => enabledPlugins[plugin.id] !== false
);
console.log('[PluginLoader] Plugins to load:', pluginsToLoad.length);
const loaded: string[] = [];
const React = await import('react');
// Load each enabled plugin
for (const installedPlugin of pluginsToLoad) {
try {
console.log(`[PluginLoader] Loading plugin: ${installedPlugin.id}`);
// Dynamically import the plugin
const pluginModule = await loadPluginModule(installedPlugin.id);
// Handle both CommonJS (module.exports = {...}) and ES6 (export default {...})
const plugin: Plugin = pluginModule?.default || pluginModule;
if (!plugin || typeof plugin !== 'object') {
console.error(
`[PluginLoader] Plugin ${installedPlugin.id} does not export a valid plugin object`
);
continue;
}
// Validate plugin structure
if (typeof plugin.onLoad !== 'function') {
console.error(
`[PluginLoader] Plugin ${installedPlugin.id} does not have an onLoad function`
);
continue;
}
// Create enhanced plugin context
const context = createPluginContext(installedPlugin.id, matrixClient, React);
// Register plugin
pluginRegistry.registerPlugin(installedPlugin.id, plugin, context);
// Call plugin's onLoad
await plugin.onLoad(context);
loaded.push(installedPlugin.id);
console.log(`[PluginLoader] Successfully loaded plugin: ${installedPlugin.id}`);
} catch (error) {
console.error(`[PluginLoader] Failed to load plugin ${installedPlugin.id}:`, error);
pluginRegistry.addLog(installedPlugin.id, 'error', ['Failed to load:', error]);
}
}
if (mounted) {
setLoadedPlugins(loaded);
setLoading(false);
}
} catch (error) {
console.error('[PluginLoader] Error loading plugins:', error);
if (mounted) {
setLoading(false);
}
}
};
loadPlugins();
return () => {
mounted = false;
// Cleanup: unload all plugins
console.log('[PluginLoader] Cleaning up plugins');
pluginRegistry.clear();
};
}, [matrixClient]);
// Show children immediately, plugins load in background
return <>{children}</>;
}
/**
* Create enhanced plugin context with all APIs
*/
function createPluginContext(
pluginId: string,
matrixClient: MatrixClient,
React: typeof import('react')
): PluginContext {
return {
pluginId,
matrixClient,
React,
// Commands API
commands: {
register: (command) => {
pluginRegistry.registerCommand(pluginId, command);
pluginRegistry.addLog(pluginId, 'log', [`Registered command: /${command.name}`]);
},
unregister: (name) => {
pluginRegistry.unregisterCommand(name);
},
execute: async (name, args) => {
return pluginRegistry.executeCommand(name, JSON.stringify(args));
},
},
// Messages API
messages: {
onBeforeSend: (interceptor) => {
pluginRegistry.addBeforeSendInterceptor(pluginId, interceptor);
pluginRegistry.addLog(pluginId, 'log', ['Registered beforeSend interceptor']);
},
onReceive: (interceptor) => {
pluginRegistry.addReceiveInterceptor(pluginId, interceptor);
pluginRegistry.addLog(pluginId, 'log', ['Registered receive interceptor']);
},
},
// UI API
ui: {
registerRenderer: (type, renderer) => {
pluginRegistry.registerRenderer(pluginId, type, renderer);
pluginRegistry.addLog(pluginId, 'log', [`Registered renderer for: ${type}`]);
},
unregisterRenderer: (type) => {
pluginRegistry.unregisterRenderer(type);
},
},
// Settings API
settings: {
define: (schema) => {
pluginRegistry.defineSettings(pluginId, schema);
pluginRegistry.addLog(pluginId, 'log', ['Defined settings schema']);
},
get: <T = any,>(key: string): T | undefined => {
return pluginRegistry.getPluginSetting<T>(pluginId, key);
},
set: (key, value) => {
pluginRegistry.setPluginSetting(pluginId, key, value);
},
},
// Themes API
themes: {
register: (theme) => {
pluginRegistry.registerTheme(pluginId, theme);
pluginRegistry.addLog(pluginId, 'log', [`Registered theme: ${theme.name}`]);
},
unregister: (themeId) => {
pluginRegistry.unregisterTheme(pluginId, themeId);
},
},
// Matrix events API
matrix: {
on: (eventType, handler) => {
pluginRegistry.addMatrixHandler(pluginId, eventType, handler);
matrixClient.on(eventType as any, handler as any);
pluginRegistry.addLog(pluginId, 'log', [`Listening to Matrix event: ${eventType}`]);
},
off: (eventType, handler) => {
pluginRegistry.removeMatrixHandler(pluginId, eventType, handler);
matrixClient.off(eventType as any, handler as any);
},
},
// Timers API
timers: {
setInterval: (callback, ms) => {
const id = window.setInterval(() => {
try {
callback();
} catch (err) {
pluginRegistry.addLog(pluginId, 'error', ['Interval error:', err]);
}
}, ms);
pluginRegistry.addTimer(pluginId, id);
return id;
},
setTimeout: (callback, ms) => {
const id = window.setTimeout(() => {
try {
callback();
} catch (err) {
pluginRegistry.addLog(pluginId, 'error', ['Timeout error:', err]);
}
pluginRegistry.removeTimer(pluginId, id);
}, ms);
pluginRegistry.addTimer(pluginId, id);
return id;
},
clearInterval: (id) => {
window.clearInterval(id);
pluginRegistry.removeTimer(pluginId, id);
},
clearTimeout: (id) => {
window.clearTimeout(id);
pluginRegistry.removeTimer(pluginId, id);
},
},
// Notifications API
notify: async (options) => {
const opts = typeof options === 'string' ? { title: 'Plugin', body: options } : options;
console.log(`[Plugin ${pluginId}] Notification:`, opts.title, opts.body);
pluginRegistry.addLog(pluginId, 'log', ['Notification:', opts]);
// Send actual notification
try {
await sendNotification({
title: opts.title || 'Plugin',
body: opts.body || '',
path: undefined, // Could be enhanced to support navigation
});
} catch (err) {
console.error(`[Plugin ${pluginId}] Notification failed:`, err);
}
},
// Logging API
log: (...args) => {
console.log(`[Plugin ${pluginId}]`, ...args);
pluginRegistry.addLog(pluginId, 'log', args);
},
warn: (...args) => {
console.warn(`[Plugin ${pluginId}]`, ...args);
pluginRegistry.addLog(pluginId, 'warn', args);
},
error: (...args) => {
console.error(`[Plugin ${pluginId}]`, ...args);
pluginRegistry.addLog(pluginId, 'error', args);
},
// Require API
require: (requiredPluginId) => {
const exports = pluginRegistry.getPluginExports(requiredPluginId);
if (!exports) {
throw new Error(`Plugin ${requiredPluginId} not found or has no exports`);
}
pluginRegistry.addLog(pluginId, 'log', [`Required plugin: ${requiredPluginId}`]);
return exports;
},
};
}
/**
* Load a plugin module from the filesystem.
* Handles both Electron and web environments.
*/
async function loadPluginModule(pluginId: string): Promise<any> {
// Check if we're in Electron
if (window.electron?.plugins) {
// In Electron, we need to read the plugin file and evaluate it
// This is necessary because dynamic imports don't work with file:// URLs in all cases
try {
// Request Electron to load the plugin code
const response = await window.electron.plugins.readPluginCode(pluginId);
if (!response.success || !response.data) {
throw new Error(response.error || 'Failed to read plugin code');
}
const pluginCode = response.data;
// Create a sandboxed environment for the plugin
const pluginExports: any = {};
const module = { exports: pluginExports };
// Evaluate the plugin code
// eslint-disable-next-line no-new-func
const pluginFunction = new Function('module', 'exports', pluginCode);
pluginFunction(module, pluginExports);
return module.exports;
} catch (error) {
console.error(`[PluginLoader] Error evaluating plugin ${pluginId}:`, error);
throw error;
}
} else {
// In web mode, plugins are not supported
throw new Error('Plugins are only supported in the desktop app');
}
}