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

@@ -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.