Add UI button registration API (registerButton, unregisterButton, getButtonsForLocation)

This commit is contained in:
2026-04-22 00:51:57 +10:00
parent 6f506c59a4
commit 6c389381df
20 changed files with 438 additions and 21 deletions

141
dist/PluginRegistry.js vendored
View File

@@ -11,6 +11,7 @@ export class PluginRegistry {
this.beforeSendInterceptors = [];
this.receiveInterceptors = [];
this.renderers = new Map();
this.buttons = new Map();
this.settingsSchemas = new Map();
this.pluginSettings = new Map();
this.matrixHandlers = new Map();
@@ -57,6 +58,10 @@ export class PluginRegistry {
if (renderer.pluginId === id)
this.renderers.delete(type);
}
for (const [buttonId, btn] of this.buttons.entries()) {
if (btn.pluginId === id)
this.buttons.delete(buttonId);
}
for (const [themeId, theme] of this.themes.entries()) {
if (theme.pluginId === id) {
this.onThemeUnregistered?.(themeId);
@@ -93,11 +98,14 @@ export class PluginRegistry {
this.commands.delete(name);
}
/** Execute a command by name, passing raw argument string. */
async executeCommand(name, rawArgs) {
async executeCommand(name, rawArgs, roomId) {
const cmd = this.commands.get(name);
if (!cmd)
throw new Error(`Command /${name} not found`);
const args = this.parseCommandArgs(cmd.command, rawArgs);
if (roomId) {
args.roomId = roomId;
}
const entry = this.plugins.get(cmd.pluginId);
if (!entry)
throw new Error(`Plugin ${cmd.pluginId} not loaded`);
@@ -106,11 +114,25 @@ export class PluginRegistry {
parseCommandArgs(command, rawArgs) {
if (!command.args || command.args.length === 0)
return {};
const parts = rawArgs.trim().split(/\s+/);
const trimmedArgs = rawArgs.trim();
if (!trimmedArgs)
return {};
const argNames = command.args.map(arg => typeof arg === 'string' ? arg : arg.name);
// If only one arg (that's not roomId), give it all the text
const nonRoomIdArgs = argNames.filter(name => name !== 'roomId');
if (nonRoomIdArgs.length === 1) {
const result = {};
result[nonRoomIdArgs[0]] = trimmedArgs;
return result;
}
// Otherwise, split by whitespace
const parts = trimmedArgs.split(/\s+/);
const result = {};
command.args.forEach((arg, index) => {
const argName = typeof arg === 'string' ? arg : arg.name;
result[argName] = parts[index];
if (argName !== 'roomId') {
result[argName] = parts[index];
}
});
return result;
}
@@ -171,6 +193,119 @@ export class PluginRegistry {
return this.renderers.get(type)?.renderer;
}
// ---------------------------------------------------------------------------
// UI Buttons
// ---------------------------------------------------------------------------
/** Register a UI button contributed by a plugin. */
registerButton(pluginId, button) {
const fullId = `${pluginId}:${button.id}`;
if (this.buttons.has(fullId)) {
console.warn(`[PluginRegistry] Button ${fullId} already registered`);
return;
}
this.buttons.set(fullId, { pluginId, button: { ...button, id: fullId } });
}
/** Remove a registered button by ID. */
unregisterButton(pluginId, buttonId) {
const fullId = `${pluginId}:${buttonId}`;
this.buttons.delete(fullId);
}
/**
* Get all buttons for a specific UI location, sorted by position configuration.
* Returns buttons in the order they should be rendered.
*/
getButtonsForLocation(location) {
const buttons = Array.from(this.buttons.values())
.filter(({ button }) => button.location === location)
.map(({ button }) => button);
return this.sortButtonsByPosition(buttons);
}
/**
* Sort buttons according to their position configuration.
* - Buttons with position.before/after are positioned relative to existing buttons
* - Buttons in groups are kept together
* - Within groups, buttons are sorted by order property
*/
sortButtonsByPosition(buttons) {
if (buttons.length === 0)
return [];
const grouped = new Map();
const ungrouped = [];
const positionedButtons = new Map();
for (const button of buttons) {
positionedButtons.set(button.id, button);
if (button.position?.group) {
if (!grouped.has(button.position.group)) {
grouped.set(button.position.group, []);
}
grouped.get(button.position.group).push(button);
}
else {
ungrouped.push(button);
}
}
for (const [_group, groupButtons] of grouped) {
groupButtons.sort((a, b) => {
const orderA = a.position?.order ?? 0;
const orderB = b.position?.order ?? 0;
return orderA - orderB;
});
}
const result = [];
const inserted = new Set();
const insertButton = (button) => {
if (inserted.has(button.id))
return;
const group = button.position?.group ? grouped.get(button.position.group) : [button];
if (!group)
return;
for (const btn of group) {
if (!inserted.has(btn.id)) {
result.push(btn);
inserted.add(btn.id);
}
}
};
for (const button of [...ungrouped, ...Array.from(grouped.values()).flat()]) {
if (inserted.has(button.id))
continue;
if (button.position?.before) {
const beforeButton = positionedButtons.get(button.position.before);
if (beforeButton) {
const idx = result.findIndex(b => b.id === beforeButton.id);
if (idx !== -1) {
const group = button.position.group ? grouped.get(button.position.group) : [button];
if (group) {
for (let i = group.length - 1; i >= 0; i--) {
if (!inserted.has(group[i].id)) {
result.splice(idx, 0, group[i]);
inserted.add(group[i].id);
}
}
}
continue;
}
}
}
if (button.position?.after) {
const afterButton = positionedButtons.get(button.position.after);
if (afterButton) {
const idx = result.findIndex(b => b.id === afterButton.id);
if (idx !== -1) {
insertButton(button);
continue;
}
}
}
insertButton(button);
}
for (const button of buttons) {
if (!inserted.has(button.id)) {
result.push(button);
}
}
return result;
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
/** Define the settings schema for a plugin and load persisted values. */