fix: Improve plugin ui entry points, Fix chat scroll, fix notifications, fix user rights

This commit is contained in:
2026-05-17 16:09:16 +02:00
parent 8e3ccf4157
commit 8631290c01
35 changed files with 1560 additions and 619 deletions

View File

@@ -37,21 +37,23 @@ Example context shape:
## Action Context
Composer, toolbar, and profile actions receive context directly.
Composer, toolbar, and profile actions receive context directly. Toolbar actions are launched from the server side panel's View plugins menu and report `source: 'toolbarAction'`.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerToolbarAction('where-am-i', {
label: 'Where am I?',
run: (actionContext) => {
context.api.logger.info('Toolbar action context', {
source: actionContext.source,
serverId: actionContext.server?.id,
textChannelId: actionContext.textChannel?.id,
voiceChannelId: actionContext.voiceChannel?.id
});
}
}));
context.subscriptions.push(
context.api.ui.registerToolbarAction('where-am-i', {
label: 'Where am I?',
run: (actionContext) => {
context.api.logger.info('Toolbar action context', {
source: actionContext.source,
serverId: actionContext.server?.id,
textChannelId: actionContext.textChannel?.id,
voiceChannelId: actionContext.voiceChannel?.id
});
}
})
);
}
```
@@ -70,4 +72,4 @@ export function activate(context) {
}
```
Logs are visible in the Plugin Manager. Avoid logging passwords, bearer tokens, or private message contents.
Logs are visible in the Plugin Manager. Avoid logging passwords, bearer tokens, or private message contents.

View File

@@ -10,17 +10,17 @@ Prefer registered UI contributions over direct DOM mounting. Contribution APIs l
## Required Capabilities
| Method | Capability |
| --- | --- |
| `ui.registerAppPage(id, contribution)` | `ui.pages` |
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` |
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` |
| Method | Capability |
| --------------------------------------------- | -------------------- |
| `ui.registerAppPage(id, contribution)` | `ui.pages` |
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` |
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` |
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` |
| `ui.registerComposerAction(id, contribution)` | `ui.pages` |
| `ui.registerProfileAction(id, contribution)` | `ui.pages` |
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` |
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` |
| `ui.mountElement(id, request)` | `ui.dom` |
| `ui.registerComposerAction(id, contribution)` | `ui.pages` |
| `ui.registerProfileAction(id, contribution)` | `ui.pages` |
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` |
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` |
| `ui.mountElement(id, request)` | `ui.dom` |
Every registration returns a disposable. Push it into `context.subscriptions`.
@@ -28,15 +28,17 @@ Every registration returns a disposable. Push it into `context.subscriptions`.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerAppPage('dashboard', {
label: 'Raid Dashboard',
path: '/plugins/example.raid-helper/dashboard',
render: () => {
const root = document.createElement('section');
root.innerHTML = '<h1>Raid Dashboard</h1><p>Tonight: dungeon practice.</p>';
return root;
}
}));
context.subscriptions.push(
context.api.ui.registerAppPage('dashboard', {
label: 'Raid Dashboard',
path: '/plugins/example.raid-helper/dashboard',
render: () => {
const root = document.createElement('section');
root.innerHTML = '<h1>Raid Dashboard</h1><p>Tonight: dungeon practice.</p>';
return root;
}
})
);
}
```
@@ -46,22 +48,24 @@ The page is hosted by `/plugins/:pluginId/:pageId`.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerSettingsPage('preferences', {
label: 'Raid Helper',
settingsKey: 'raid-helper',
order: 20,
render: () => {
const wrapper = document.createElement('section');
const label = document.createElement('label');
const checkbox = document.createElement('input');
context.subscriptions.push(
context.api.ui.registerSettingsPage('preferences', {
label: 'Raid Helper',
settingsKey: 'raid-helper',
order: 20,
render: () => {
const wrapper = document.createElement('section');
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
label.append(checkbox, ' Enable ready-check reminders');
wrapper.append(label);
return wrapper;
}
}));
checkbox.type = 'checkbox';
checkbox.checked = true;
label.append(checkbox, ' Enable ready-check reminders');
wrapper.append(label);
return wrapper;
}
})
);
}
```
@@ -71,23 +75,26 @@ Use `ui.registerSidePanel` for content that belongs in the server sidebar plugin
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerSidePanel('soundboard', {
label: 'Soundboard',
order: 10,
render: () => {
const panel = document.createElement('div');
const button = document.createElement('button');
context.subscriptions.push(
context.api.ui.registerSidePanel('soundboard', {
label: 'Soundboard',
order: 10,
render: () => {
const panel = document.createElement('div');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Play chime';
button.onclick = () => context.api.media.playAudioClip({
url: 'https://cdn.example.com/chime.wav',
volume: 0.6
});
panel.append(button);
return panel;
}
}));
button.type = 'button';
button.textContent = 'Play chime';
button.onclick = () =>
context.api.media.playAudioClip({
url: 'https://cdn.example.com/chime.wav',
volume: 0.6
});
panel.append(button);
return panel;
}
})
);
}
```
@@ -97,11 +104,13 @@ Capabilities required: `ui.sidePanel` and `media.playAudio`.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerChannelSection('events', {
label: 'Event Rooms',
type: 'custom',
order: 50
}));
context.subscriptions.push(
context.api.ui.registerChannelSection('events', {
label: 'Event Rooms',
type: 'custom',
order: 50
})
);
}
```
@@ -109,16 +118,15 @@ export function activate(context) {
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerComposerAction('insert-standup', {
icon: 'ST',
label: 'Insert standup prompt',
run: (actionContext) => {
context.api.messages.send(
'Standup: yesterday I..., today I..., blocked by...',
actionContext.textChannel?.id
);
}
}));
context.subscriptions.push(
context.api.ui.registerComposerAction('insert-standup', {
icon: 'ST',
label: 'Insert standup prompt',
run: (actionContext) => {
context.api.messages.send('Standup: yesterday I..., today I..., blocked by...', actionContext.textChannel?.id);
}
})
);
}
```
@@ -128,45 +136,65 @@ Capabilities required: `ui.pages` and `messages.send`.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerProfileAction('wave', {
label: 'Wave',
run: (actionContext) => {
context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`);
}
}));
context.subscriptions.push(
context.api.ui.registerProfileAction('wave', {
label: 'Wave',
run: (actionContext) => {
context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`);
}
})
);
}
```
## Toolbar Action
Toolbar actions are command-style plugin entries shown in the server side panel's View plugins menu. Use them for small actions that should be easy to launch from a server, such as opening a plugin page, sending a status message, starting a timer, or toggling a plugin feature.
The View plugins link appears in `[data-testid="plugin-room-side-panel"]` when the plugin side-panel area is rendered. Opening it shows an overlay menu, positioned like profile-card overlays, with registered actions laid out as plugin icon tiles. The `icon` field can be short text such as `RH`, an emoji, or an image URL; when omitted, MetoYou falls back to initials from the plugin/action labels.
Toolbar action callbacks receive an action context with `source: 'toolbarAction'`, the current user, current server, active text channel, and current voice channel when available.
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerToolbarAction('open-dashboard', {
label: 'Raid Helper',
run: () => {
context.api.logger.info('Open the Raid Helper plugin page from /plugins/example.raid-helper/dashboard');
}
}));
context.subscriptions.push(
context.api.ui.registerToolbarAction('open-dashboard', {
icon: 'RH',
label: 'Raid Helper',
run: (actionContext) => {
context.api.logger.info('Raid Helper opened', {
channelId: actionContext.textChannel?.id,
serverId: actionContext.server?.id
});
}
})
);
}
```
Capabilities required: `ui.pages`. Add any capability your action uses, such as `messages.send` or `server.read`.
Use `registerSidePanel` instead when the plugin needs persistent sidebar content, and use `registerAppPage` when the plugin needs a full-page workflow.
## Embed Renderer
```js
export function activate(context) {
context.subscriptions.push(context.api.ui.registerEmbedRenderer('raid-card', {
embedType: 'raid.card',
render: (payload) => {
const card = document.createElement('article');
const title = document.createElement('h3');
const body = document.createElement('p');
context.subscriptions.push(
context.api.ui.registerEmbedRenderer('raid-card', {
embedType: 'raid.card',
render: (payload) => {
const card = document.createElement('article');
const title = document.createElement('h3');
const body = document.createElement('p');
title.textContent = payload?.title ?? 'Raid';
body.textContent = payload?.description ?? 'No description provided.';
card.append(title, body);
return card;
}
}));
title.textContent = payload?.title ?? 'Raid';
body.textContent = payload?.description ?? 'No description provided.';
card.append(title, body);
return card;
}
})
);
}
```
@@ -202,11 +230,13 @@ export function activate(context) {
badge.style.color = 'white';
badge.style.borderRadius = '6px';
context.subscriptions.push(context.api.ui.mountElement('active-badge', {
target: 'body',
position: 'beforeend',
element: badge
}));
context.subscriptions.push(
context.api.ui.mountElement('active-badge', {
target: 'body',
position: 'beforeend',
element: badge
})
);
}
```
@@ -224,12 +254,14 @@ export function activate(context) {
const banner = document.createElement('div');
banner.textContent = 'Raid helper active in this chat.';
context.subscriptions.push(context.api.ui.mountElement('chat-banner', {
target,
position: 'afterbegin',
element: banner
}));
context.subscriptions.push(
context.api.ui.mountElement('chat-banner', {
target,
position: 'afterbegin',
element: banner
})
);
}
```
The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible.
The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible.