fix: Improve plugin ui entry points, Fix chat scroll, fix notifications, fix user rights
This commit is contained in:
@@ -8,18 +8,18 @@ This page maps the app routes and important DOM areas. It is useful for plugin a
|
||||
|
||||
## Angular Routes
|
||||
|
||||
| Route | Component | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/` | Redirect | Redirects to `/search`. |
|
||||
| `/login` | `LoginComponent` | User login. |
|
||||
| `/register` | `RegisterComponent` | User registration. |
|
||||
| `/invite/:inviteId` | `InviteComponent` | Resolve and accept invite links. |
|
||||
| `/search` | `ServerSearchComponent` | Search and join servers. |
|
||||
| `/room/:roomId` | `ChatRoomComponent` | Main server page with text, voice, members, and plugin panels. |
|
||||
| `/dm` | `DmWorkspaceComponent` | Direct-message workspace. |
|
||||
| `/dm/:conversationId` | `DmWorkspaceComponent` | A selected direct-message conversation. |
|
||||
| `/settings` | `SettingsComponent` | App, voice, server, plugin, desktop, theme, local API settings. |
|
||||
| `/plugin-store` | `PluginStoreComponent` | Browse plugin sources and install/update plugins. |
|
||||
| Route | Component | Purpose |
|
||||
| ---------------------------- | ------------------------- | --------------------------------------------------------------------- |
|
||||
| `/` | Redirect | Redirects to `/search`. |
|
||||
| `/login` | `LoginComponent` | User login. |
|
||||
| `/register` | `RegisterComponent` | User registration. |
|
||||
| `/invite/:inviteId` | `InviteComponent` | Resolve and accept invite links. |
|
||||
| `/search` | `ServerSearchComponent` | Search and join servers. |
|
||||
| `/room/:roomId` | `ChatRoomComponent` | Main server page with text, voice, members, and plugin panels. |
|
||||
| `/dm` | `DmWorkspaceComponent` | Direct-message workspace. |
|
||||
| `/dm/:conversationId` | `DmWorkspaceComponent` | A selected direct-message conversation. |
|
||||
| `/settings` | `SettingsComponent` | App, voice, server, plugin, desktop, theme, local API settings. |
|
||||
| `/plugin-store` | `PluginStoreComponent` | Browse plugin sources and install/update plugins. |
|
||||
| `/plugins/:pluginId/:pageId` | `PluginPageHostComponent` | Host for plugin app pages registered with `api.ui.registerAppPage()`. |
|
||||
|
||||
## Page Shell
|
||||
@@ -46,6 +46,7 @@ The server page is the most important page for plugins.
|
||||
<section>Text Channels</section>
|
||||
<section>Voice Channels</section>
|
||||
<section data-testid="plugin-room-side-panel">
|
||||
<button>View plugins</button>
|
||||
<app-plugin-render-host></app-plugin-render-host>
|
||||
</section>
|
||||
<section>Members</section>
|
||||
@@ -135,11 +136,11 @@ Prefer plugin APIs over DOM selectors. When direct DOM mounting is necessary, us
|
||||
|
||||
Common targets:
|
||||
|
||||
| Selector | Area |
|
||||
| --- | --- |
|
||||
| `body` | Global overlays or modals. |
|
||||
| `app-chat-messages` | Main text channel surface. |
|
||||
| `app-rooms-side-panel` | Server side panel. |
|
||||
| `[data-testid="plugin-room-side-panel"]` | Plugin side-panel area in the server sidebar. |
|
||||
| Selector | Area |
|
||||
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `body` | Global overlays or modals. |
|
||||
| `app-chat-messages` | Main text channel surface. |
|
||||
| `app-rooms-side-panel` | Server side panel. |
|
||||
| `[data-testid="plugin-room-side-panel"]` | Plugin side-panel area in the server sidebar, including the View plugins trigger for `registerToolbarAction()` tiles. |
|
||||
|
||||
Avoid depending on Tailwind utility classes; they are layout details and may change.
|
||||
Avoid depending on Tailwind utility classes; they are layout details and may change.
|
||||
|
||||
@@ -71,18 +71,18 @@ Plugins run only in the renderer. They do not run in Electron main and do not ru
|
||||
|
||||
Choose communication APIs like this:
|
||||
|
||||
| Need | Use | Notes |
|
||||
| --- | --- | --- |
|
||||
| Visible normal chat message | `api.messages.send` | Persists locally, updates chat UI, broadcasts peer chat event. |
|
||||
| Visible bot-style message | `api.server.registerPluginUser` plus `api.messages.sendAsPluginUser` | Requires `users.manage` and `messages.send`. |
|
||||
| Plugin state sync between connected clients | `api.messageBus.publish` and `api.messageBus.subscribe` | P2P data-channel envelope, not a visible chat message. |
|
||||
| Plugin state sync plus recent chat snapshot | `api.messageBus.publish` with `includeLatestMessages` | Also needs `messages.read`. |
|
||||
| Metadata through signaling server | `api.events.publishServer` and `api.events.subscribeServer` | Event must be declared in manifest. |
|
||||
| Low-level peer data | `api.p2p.broadcastData` or `api.p2p.sendData` | Prefer message bus for structured topics/subscriptions. |
|
||||
| Local user preferences | `api.clientData` | User-scoped local storage/database. |
|
||||
| Local per-server plugin data | `api.serverData` | User-scoped and current-server-scoped local storage/database. |
|
||||
| App UI extension | `api.ui.*` | Prefer registered contributions over DOM mounting. |
|
||||
| Audio/video/voice effects | `api.media.*` | Browser media APIs and voice facade. |
|
||||
| Need | Use | Notes |
|
||||
| ------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| Visible normal chat message | `api.messages.send` | Persists locally, updates chat UI, broadcasts peer chat event. |
|
||||
| Visible bot-style message | `api.server.registerPluginUser` plus `api.messages.sendAsPluginUser` | Requires `users.manage` and `messages.send`. |
|
||||
| Plugin state sync between connected clients | `api.messageBus.publish` and `api.messageBus.subscribe` | P2P data-channel envelope, not a visible chat message. |
|
||||
| Plugin state sync plus recent chat snapshot | `api.messageBus.publish` with `includeLatestMessages` | Also needs `messages.read`. |
|
||||
| Metadata through signaling server | `api.events.publishServer` and `api.events.subscribeServer` | Event must be declared in manifest. |
|
||||
| Low-level peer data | `api.p2p.broadcastData` or `api.p2p.sendData` | Prefer message bus for structured topics/subscriptions. |
|
||||
| Local user preferences | `api.clientData` | User-scoped local storage/database. |
|
||||
| Local per-server plugin data | `api.serverData` | User-scoped and current-server-scoped local storage/database. |
|
||||
| App UI extension | `api.ui.*` | Prefer registered contributions over DOM mounting. |
|
||||
| Audio/video/voice effects | `api.media.*` | Browser media APIs and voice facade. |
|
||||
|
||||
## How The App Looks
|
||||
|
||||
@@ -122,47 +122,51 @@ Main server page shape:
|
||||
|
||||
Important routes:
|
||||
|
||||
| Route | Purpose |
|
||||
| --- | --- |
|
||||
| `/search` | Search and join servers. |
|
||||
| `/room/:roomId` | Main server workspace with text, voice, members, and plugin panels. |
|
||||
| `/dm` and `/dm/:conversationId` | Direct-message workspace. |
|
||||
| `/settings` | App, voice, server, plugin, desktop, theme, and local API settings. |
|
||||
| `/plugin-store` | Browse and install plugins. |
|
||||
| `/plugins/:pluginId/:pageId` | Host for pages registered with `api.ui.registerAppPage`. |
|
||||
| Route | Purpose |
|
||||
| ------------------------------- | ------------------------------------------------------------------- |
|
||||
| `/search` | Search and join servers. |
|
||||
| `/room/:roomId` | Main server workspace with text, voice, members, and plugin panels. |
|
||||
| `/dm` and `/dm/:conversationId` | Direct-message workspace. |
|
||||
| `/settings` | App, voice, server, plugin, desktop, theme, and local API settings. |
|
||||
| `/plugin-store` | Browse and install plugins. |
|
||||
| `/plugins/:pluginId/:pageId` | Host for pages registered with `api.ui.registerAppPage`. |
|
||||
|
||||
Direct DOM mounting is a last resort. Route-specific targets may not exist when `activate` runs. If `api.ui.mountElement` cannot find the target, it throws `Plugin mount target not found: <selector>` and plugin activation fails.
|
||||
|
||||
Stable direct-mount targets when necessary:
|
||||
|
||||
| Selector | Area |
|
||||
| --- | --- |
|
||||
| `body` | Safest global target for overlays, badges, and modals. It exists during activation. |
|
||||
| `app-chat-messages` | Main text channel surface. Use only after checking the element exists. |
|
||||
| Selector | Area |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `body` | Safest global target for overlays, badges, and modals. It exists during activation. |
|
||||
| `app-chat-messages` | Main text channel surface. Use only after checking the element exists. |
|
||||
| `app-rooms-side-panel` | Server side panel. Use only after checking the element exists. Prefer `registerSidePanel` for plugin sidebar content. |
|
||||
|
||||
Do not mount directly into `[data-testid="plugin-room-side-panel"]`. That area is owned by the plugin side-panel registry and is rendered only on the server page. For server sidebar UI, use:
|
||||
|
||||
```js
|
||||
context.subscriptions.push(api.ui.registerSidePanel('control-panel', {
|
||||
label: 'Control Panel',
|
||||
order: 20,
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const button = document.createElement('button');
|
||||
context.subscriptions.push(
|
||||
api.ui.registerSidePanel('control-panel', {
|
||||
label: 'Control Panel',
|
||||
order: 20,
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.type = 'button';
|
||||
button.textContent = 'Run Action';
|
||||
button.addEventListener('click', () => {
|
||||
api.logger.info('Side-panel action clicked');
|
||||
});
|
||||
button.type = 'button';
|
||||
button.textContent = 'Run Action';
|
||||
button.addEventListener('click', () => {
|
||||
api.logger.info('Side-panel action clicked');
|
||||
});
|
||||
|
||||
root.append(button);
|
||||
return root;
|
||||
}
|
||||
}));
|
||||
root.append(button);
|
||||
return root;
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
For small command-style plugin entries, use `api.ui.registerToolbarAction()`. Those actions appear as icon tiles in the server side panel's View plugins menu and receive `source: 'toolbarAction'` in their action context.
|
||||
|
||||
Do not depend on Tailwind classes or internal styling classes.
|
||||
|
||||
## Manifest
|
||||
@@ -300,10 +304,10 @@ Validation rules:
|
||||
|
||||
Scope meanings:
|
||||
|
||||
| Scope | Meaning |
|
||||
| --- | --- |
|
||||
| `client` or omitted | Installed globally for this local user/client. |
|
||||
| `server` | Installed for a specific chat server as local client plugin plus server requirement metadata. |
|
||||
| Scope | Meaning |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `client` or omitted | Installed globally for this local user/client. |
|
||||
| `server` | Installed for a specific chat server as local client plugin plus server requirement metadata. |
|
||||
|
||||
Most generated plugins should use `kind: "client"`. Use `kind: "library"` only for dependency metadata with no executable entrypoint.
|
||||
|
||||
@@ -326,10 +330,7 @@ interface TojuClientPluginModule {
|
||||
ready?: (context: TojuPluginActivationContext) => Promise<void> | void;
|
||||
deactivate?: (context: TojuPluginActivationContext) => Promise<void> | void;
|
||||
onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise<void> | void;
|
||||
onServerRequirementsChanged?: (
|
||||
context: TojuPluginActivationContext,
|
||||
snapshot: PluginRequirementsSnapshot
|
||||
) => Promise<void> | void;
|
||||
onServerRequirementsChanged?: (context: TojuPluginActivationContext, snapshot: PluginRequirementsSnapshot) => Promise<void> | void;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -579,9 +580,20 @@ interface ChannelPermissionOverride {
|
||||
## Full Plugin API Types
|
||||
|
||||
```ts
|
||||
interface PluginApiProfileUpdate { displayName: string; description?: string }
|
||||
interface PluginApiAvatarUpdate { avatarUrl: string; avatarMime: string; avatarHash: string }
|
||||
interface PluginApiChannelRequest { name: string; id?: string; position?: number }
|
||||
interface PluginApiProfileUpdate {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
}
|
||||
interface PluginApiAvatarUpdate {
|
||||
avatarUrl: string;
|
||||
avatarMime: string;
|
||||
avatarHash: string;
|
||||
}
|
||||
interface PluginApiChannelRequest {
|
||||
name: string;
|
||||
id?: string;
|
||||
position?: number;
|
||||
}
|
||||
interface PluginApiServerSettingsUpdate {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -590,10 +602,24 @@ interface PluginApiServerSettingsUpdate {
|
||||
password?: string;
|
||||
maxUsers?: number;
|
||||
}
|
||||
interface PluginApiPluginUserRequest { displayName: string; id?: string; avatarUrl?: string }
|
||||
interface PluginApiMessageAsPluginUserRequest { pluginUserId: string; content: string; channelId?: string }
|
||||
interface PluginApiAudioClipRequest { url: string; volume?: number }
|
||||
interface PluginApiCustomStreamRequest { stream: MediaStream; label?: string }
|
||||
interface PluginApiPluginUserRequest {
|
||||
displayName: string;
|
||||
id?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
interface PluginApiMessageAsPluginUserRequest {
|
||||
pluginUserId: string;
|
||||
content: string;
|
||||
channelId?: string;
|
||||
}
|
||||
interface PluginApiAudioClipRequest {
|
||||
url: string;
|
||||
volume?: number;
|
||||
}
|
||||
interface PluginApiCustomStreamRequest {
|
||||
stream: MediaStream;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
type PluginApiActionSource = 'composerAction' | 'toolbarAction' | 'profileAction' | 'manual';
|
||||
interface PluginApiActionContext {
|
||||
@@ -660,13 +686,41 @@ interface PluginApiMessageBusSubscription {
|
||||
handler: (event: PluginApiMessageBusEnvelope) => void;
|
||||
}
|
||||
|
||||
interface PluginApiPageContribution { label: string; path: string; render: () => HTMLElement | string }
|
||||
interface PluginApiSettingsPageContribution { label: string; settingsKey?: string; order?: number; render: () => HTMLElement | string }
|
||||
interface PluginApiPanelContribution { label: string; order?: number; render: () => HTMLElement | string }
|
||||
interface PluginApiChannelSectionContribution { label: string; type?: 'audio' | 'video' | 'custom'; order?: number }
|
||||
interface PluginApiActionContribution { label: string; icon?: string; run: (context: PluginApiActionContext) => Promise<void> | void }
|
||||
interface PluginApiEmbedRendererContribution { embedType: string; render: (payload: unknown) => HTMLElement | string }
|
||||
interface PluginApiDomMountRequest { target: Element | string; element: HTMLElement; position?: InsertPosition }
|
||||
interface PluginApiPageContribution {
|
||||
label: string;
|
||||
path: string;
|
||||
render: () => HTMLElement | string;
|
||||
}
|
||||
interface PluginApiSettingsPageContribution {
|
||||
label: string;
|
||||
settingsKey?: string;
|
||||
order?: number;
|
||||
render: () => HTMLElement | string;
|
||||
}
|
||||
interface PluginApiPanelContribution {
|
||||
label: string;
|
||||
order?: number;
|
||||
render: () => HTMLElement | string;
|
||||
}
|
||||
interface PluginApiChannelSectionContribution {
|
||||
label: string;
|
||||
type?: 'audio' | 'video' | 'custom';
|
||||
order?: number;
|
||||
}
|
||||
interface PluginApiActionContribution {
|
||||
label: string;
|
||||
icon?: string;
|
||||
run: (context: PluginApiActionContext) => Promise<void> | void;
|
||||
}
|
||||
interface PluginApiEmbedRendererContribution {
|
||||
embedType: string;
|
||||
render: (payload: unknown) => HTMLElement | string;
|
||||
}
|
||||
interface PluginApiDomMountRequest {
|
||||
target: Element | string;
|
||||
element: HTMLElement;
|
||||
position?: InsertPosition;
|
||||
}
|
||||
|
||||
interface TojuClientPluginApi {
|
||||
readonly context: { getCurrent: () => PluginApiActionContext };
|
||||
@@ -890,10 +944,7 @@ Capabilities: `messages.read`, `messages.send`, `messages.editOwn`, `messages.de
|
||||
```js
|
||||
const visibleMessages = api.messages.readCurrent();
|
||||
|
||||
const sent = api.messages.send(
|
||||
'Build completed successfully. Docs are ready for review.',
|
||||
'general'
|
||||
);
|
||||
const sent = api.messages.send('Build completed successfully. Docs are ready for review.', 'general');
|
||||
|
||||
api.messages.edit(sent.id, 'Build completed successfully. Docs and plugin examples are ready.');
|
||||
api.messages.delete(sent.id);
|
||||
@@ -1115,88 +1166,110 @@ Desktop uses Electron's local database when available, with renderer localStorag
|
||||
|
||||
Capabilities:
|
||||
|
||||
| Method | Required capability |
|
||||
| --- | --- |
|
||||
| `registerAppPage` | `ui.pages` |
|
||||
| `registerSettingsPage` | `ui.settings` |
|
||||
| `registerSidePanel` | `ui.sidePanel` |
|
||||
| Method | Required capability |
|
||||
| ------------------------ | -------------------- |
|
||||
| `registerAppPage` | `ui.pages` |
|
||||
| `registerSettingsPage` | `ui.settings` |
|
||||
| `registerSidePanel` | `ui.sidePanel` |
|
||||
| `registerChannelSection` | `ui.channelsSection` |
|
||||
| `registerComposerAction` | `ui.pages` |
|
||||
| `registerProfileAction` | `ui.pages` |
|
||||
| `registerToolbarAction` | `ui.pages` |
|
||||
| `registerEmbedRenderer` | `ui.embeds` |
|
||||
| `mountElement` | `ui.dom` |
|
||||
| `registerComposerAction` | `ui.pages` |
|
||||
| `registerProfileAction` | `ui.pages` |
|
||||
| `registerToolbarAction` | `ui.pages` |
|
||||
| `registerEmbedRenderer` | `ui.embeds` |
|
||||
| `mountElement` | `ui.dom` |
|
||||
|
||||
Register side panel:
|
||||
|
||||
```js
|
||||
context.subscriptions.push(api.ui.registerSidePanel('summary', {
|
||||
label: 'Plugin Summary',
|
||||
order: 10,
|
||||
render: () => {
|
||||
const root = document.createElement('aside');
|
||||
const heading = document.createElement('h2');
|
||||
const text = document.createElement('p');
|
||||
context.subscriptions.push(
|
||||
api.ui.registerSidePanel('summary', {
|
||||
label: 'Plugin Summary',
|
||||
order: 10,
|
||||
render: () => {
|
||||
const root = document.createElement('aside');
|
||||
const heading = document.createElement('h2');
|
||||
const text = document.createElement('p');
|
||||
|
||||
heading.textContent = 'Plugin Summary';
|
||||
text.textContent = 'No active tasks.';
|
||||
root.append(heading, text);
|
||||
return root;
|
||||
}
|
||||
}));
|
||||
heading.textContent = 'Plugin Summary';
|
||||
text.textContent = 'No active tasks.';
|
||||
root.append(heading, text);
|
||||
return root;
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
Use `registerSidePanel` for content that belongs in the server sidebar plugin area. Do not query `[data-testid="plugin-room-side-panel"]` and pass it to `mountElement`; that route-specific host may not exist while the plugin activates.
|
||||
|
||||
Register toolbar action for the View plugins menu:
|
||||
|
||||
```js
|
||||
context.subscriptions.push(
|
||||
api.ui.registerToolbarAction('quick-status', {
|
||||
icon: 'QS',
|
||||
label: 'Quick Status',
|
||||
run: (actionContext) => {
|
||||
api.logger.info('Quick Status clicked', {
|
||||
serverId: actionContext.server?.id,
|
||||
source: actionContext.source
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
Register app page:
|
||||
|
||||
```js
|
||||
context.subscriptions.push(api.ui.registerAppPage('dashboard', {
|
||||
label: 'Build Dashboard',
|
||||
path: '/plugins/example.build-dashboard/dashboard',
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const title = document.createElement('h1');
|
||||
const button = document.createElement('button');
|
||||
const output = document.createElement('p');
|
||||
context.subscriptions.push(
|
||||
api.ui.registerAppPage('dashboard', {
|
||||
label: 'Build Dashboard',
|
||||
path: '/plugins/example.build-dashboard/dashboard',
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const title = document.createElement('h1');
|
||||
const button = document.createElement('button');
|
||||
const output = document.createElement('p');
|
||||
|
||||
title.textContent = 'Build Dashboard';
|
||||
button.type = 'button';
|
||||
button.textContent = 'Send status';
|
||||
output.textContent = 'Idle.';
|
||||
title.textContent = 'Build Dashboard';
|
||||
button.type = 'button';
|
||||
button.textContent = 'Send status';
|
||||
output.textContent = 'Idle.';
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const message = api.messages.send('Build dashboard status: ready.');
|
||||
output.textContent = `Sent message ${message.id}`;
|
||||
});
|
||||
button.addEventListener('click', () => {
|
||||
const message = api.messages.send('Build dashboard status: ready.');
|
||||
output.textContent = `Sent message ${message.id}`;
|
||||
});
|
||||
|
||||
root.append(title, button, output);
|
||||
return root;
|
||||
}
|
||||
}));
|
||||
root.append(title, button, output);
|
||||
return root;
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
Register actions:
|
||||
|
||||
```js
|
||||
context.subscriptions.push(api.ui.registerComposerAction('insert-template', {
|
||||
label: 'Insert Template',
|
||||
icon: 'file-text',
|
||||
run: (actionContext) => {
|
||||
api.messages.send(
|
||||
'Template: Please review the latest build notes.',
|
||||
actionContext.textChannel?.id
|
||||
);
|
||||
}
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.registerComposerAction('insert-template', {
|
||||
label: 'Insert Template',
|
||||
icon: 'file-text',
|
||||
run: (actionContext) => {
|
||||
api.messages.send('Template: Please review the latest build notes.', actionContext.textChannel?.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(api.ui.registerToolbarAction('post-standup', {
|
||||
label: 'Post Standup',
|
||||
icon: 'megaphone',
|
||||
run: () => {
|
||||
api.messages.send('Standup starts now. Join the voice channel when ready.');
|
||||
}
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.registerToolbarAction('post-standup', {
|
||||
label: 'Post Standup',
|
||||
icon: 'megaphone',
|
||||
run: () => {
|
||||
api.messages.send('Standup starts now. Join the voice channel when ready.');
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
Mount DOM directly:
|
||||
@@ -1210,11 +1283,13 @@ banner.textContent = 'Plugin banner mounted in chat messages.';
|
||||
const target = document.querySelector('app-chat-messages');
|
||||
|
||||
if (target) {
|
||||
context.subscriptions.push(api.ui.mountElement('chat-banner', {
|
||||
target,
|
||||
element: banner,
|
||||
position: 'afterbegin'
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.mountElement('chat-banner', {
|
||||
target,
|
||||
element: banner,
|
||||
position: 'afterbegin'
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1224,56 +1299,58 @@ Global overlay example:
|
||||
const badge = document.createElement('div');
|
||||
badge.textContent = 'Plugin active';
|
||||
|
||||
context.subscriptions.push(api.ui.mountElement('global-badge', {
|
||||
target: 'body',
|
||||
element: badge,
|
||||
position: 'beforeend'
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.mountElement('global-badge', {
|
||||
target: 'body',
|
||||
element: badge,
|
||||
position: 'beforeend'
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
`mountElement` tags the element with plugin ownership metadata, replaces duplicate mounts for the same plugin/id, and removes it on disposal/unload.
|
||||
|
||||
## Capability Cheat Sheet
|
||||
|
||||
| API call group | Capabilities |
|
||||
| --- | --- |
|
||||
| `profile.getCurrent` | `profile.read` |
|
||||
| `profile.update`, `profile.updateAvatar` | `profile.write` |
|
||||
| `users.getCurrent`, `users.list`, `users.readMembers` | `users.read` |
|
||||
| `users.kick`, `users.ban`, `server.registerPluginUser` | `users.manage` |
|
||||
| `roles.list` | `roles.read` |
|
||||
| `users.setRole`, `roles.setAssignments` | `roles.manage` |
|
||||
| `server.getCurrent` | `server.read` |
|
||||
| `server.updatePermissions`, `server.updateSettings` | `server.manage` |
|
||||
| `channels.list`, `channels.select` | `channels.read` |
|
||||
| `channels.addAudioChannel`, `channels.addVideoChannel`, `channels.rename`, `channels.remove` | `channels.manage` |
|
||||
| `messages.readCurrent`, `messages.subscribeTyping` | `messages.read` |
|
||||
| `messages.send`, `messages.sendAsPluginUser`, `messages.setTyping` | `messages.send` |
|
||||
| `messages.edit` | `messages.editOwn` |
|
||||
| `messages.delete` | `messages.deleteOwn` |
|
||||
| `messages.moderateDelete` | `messages.moderate` |
|
||||
| `messages.sync` | `messages.sync` |
|
||||
| `events.publishServer` | `events.server.publish` |
|
||||
| `events.subscribeServer` | `events.server.subscribe` |
|
||||
| `events.publishP2p` | `events.p2p.publish` |
|
||||
| `events.subscribeP2p` | `events.p2p.subscribe` |
|
||||
| `messageBus.publish` | `events.p2p.publish`, plus `messages.read` when `includeLatestMessages` is true |
|
||||
| `messageBus.sendLatestMessages` | `events.p2p.publish`, `messages.read` |
|
||||
| `messageBus.subscribe` | `events.p2p.subscribe`, plus `messages.read` when `replayLatest` is true |
|
||||
| `p2p.*` | `p2p.data` |
|
||||
| `media.playAudioClip` | `media.playAudio` |
|
||||
| `media.addCustomAudioStream` | `media.addAudioStream` |
|
||||
| `media.addCustomVideoStream` | `media.addVideoStream` |
|
||||
| `media.setInputVolume`, `media.setOutputVolume` | `audio.volume` |
|
||||
| `clientData.*`, `storage.*` | `storage.local` |
|
||||
| `serverData.read` | `storage.serverData.read` |
|
||||
| `serverData.write`, `serverData.remove` | `storage.serverData.write` |
|
||||
| `ui.registerAppPage`, composer/profile/toolbar actions | `ui.pages` |
|
||||
| `ui.registerSettingsPage` | `ui.settings` |
|
||||
| `ui.registerSidePanel` | `ui.sidePanel` |
|
||||
| `ui.registerChannelSection` | `ui.channelsSection` |
|
||||
| `ui.registerEmbedRenderer` | `ui.embeds` |
|
||||
| `ui.mountElement` | `ui.dom` |
|
||||
| API call group | Capabilities |
|
||||
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `profile.getCurrent` | `profile.read` |
|
||||
| `profile.update`, `profile.updateAvatar` | `profile.write` |
|
||||
| `users.getCurrent`, `users.list`, `users.readMembers` | `users.read` |
|
||||
| `users.kick`, `users.ban`, `server.registerPluginUser` | `users.manage` |
|
||||
| `roles.list` | `roles.read` |
|
||||
| `users.setRole`, `roles.setAssignments` | `roles.manage` |
|
||||
| `server.getCurrent` | `server.read` |
|
||||
| `server.updatePermissions`, `server.updateSettings` | `server.manage` |
|
||||
| `channels.list`, `channels.select` | `channels.read` |
|
||||
| `channels.addAudioChannel`, `channels.addVideoChannel`, `channels.rename`, `channels.remove` | `channels.manage` |
|
||||
| `messages.readCurrent`, `messages.subscribeTyping` | `messages.read` |
|
||||
| `messages.send`, `messages.sendAsPluginUser`, `messages.setTyping` | `messages.send` |
|
||||
| `messages.edit` | `messages.editOwn` |
|
||||
| `messages.delete` | `messages.deleteOwn` |
|
||||
| `messages.moderateDelete` | `messages.moderate` |
|
||||
| `messages.sync` | `messages.sync` |
|
||||
| `events.publishServer` | `events.server.publish` |
|
||||
| `events.subscribeServer` | `events.server.subscribe` |
|
||||
| `events.publishP2p` | `events.p2p.publish` |
|
||||
| `events.subscribeP2p` | `events.p2p.subscribe` |
|
||||
| `messageBus.publish` | `events.p2p.publish`, plus `messages.read` when `includeLatestMessages` is true |
|
||||
| `messageBus.sendLatestMessages` | `events.p2p.publish`, `messages.read` |
|
||||
| `messageBus.subscribe` | `events.p2p.subscribe`, plus `messages.read` when `replayLatest` is true |
|
||||
| `p2p.*` | `p2p.data` |
|
||||
| `media.playAudioClip` | `media.playAudio` |
|
||||
| `media.addCustomAudioStream` | `media.addAudioStream` |
|
||||
| `media.addCustomVideoStream` | `media.addVideoStream` |
|
||||
| `media.setInputVolume`, `media.setOutputVolume` | `audio.volume` |
|
||||
| `clientData.*`, `storage.*` | `storage.local` |
|
||||
| `serverData.read` | `storage.serverData.read` |
|
||||
| `serverData.write`, `serverData.remove` | `storage.serverData.write` |
|
||||
| `ui.registerAppPage`, composer/profile/toolbar actions | `ui.pages` |
|
||||
| `ui.registerSettingsPage` | `ui.settings` |
|
||||
| `ui.registerSidePanel` | `ui.sidePanel` |
|
||||
| `ui.registerChannelSection` | `ui.channelsSection` |
|
||||
| `ui.registerEmbedRenderer` | `ui.embeds` |
|
||||
| `ui.mountElement` | `ui.dom` |
|
||||
|
||||
## Complete Example Plugin
|
||||
|
||||
@@ -1319,25 +1396,31 @@ export function activate(context) {
|
||||
|
||||
api.logger.info('Voice Notes activated');
|
||||
|
||||
context.subscriptions.push(api.messageBus.subscribe({
|
||||
topic: BUS_TOPIC,
|
||||
replayLatest: false,
|
||||
handler: (event) => {
|
||||
api.logger.debug('Received voice notes draft update', event.payload);
|
||||
}
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.messageBus.subscribe({
|
||||
topic: BUS_TOPIC,
|
||||
replayLatest: false,
|
||||
handler: (event) => {
|
||||
api.logger.debug('Received voice notes draft update', event.payload);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(api.ui.registerSidePanel('voice-notes-panel', {
|
||||
label: 'Voice Notes',
|
||||
order: 20,
|
||||
render: () => renderPanel(context)
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.registerSidePanel('voice-notes-panel', {
|
||||
label: 'Voice Notes',
|
||||
order: 20,
|
||||
render: () => renderPanel(context)
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(api.ui.registerAppPage('voice-notes', {
|
||||
label: 'Voice Notes',
|
||||
path: '/plugins/example.voice-notes/voice-notes',
|
||||
render: () => renderPanel(context)
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.registerAppPage('voice-notes', {
|
||||
label: 'Voice Notes',
|
||||
path: '/plugins/example.voice-notes/voice-notes',
|
||||
render: () => renderPanel(context)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function renderPanel(context) {
|
||||
@@ -1352,9 +1435,7 @@ function renderPanel(context) {
|
||||
|
||||
const current = api.context.getCurrent();
|
||||
heading.textContent = 'Voice Notes';
|
||||
meta.textContent = current.voiceChannel
|
||||
? `Connected to ${current.voiceChannel.name}`
|
||||
: 'Not connected to a voice channel.';
|
||||
meta.textContent = current.voiceChannel ? `Connected to ${current.voiceChannel.name}` : 'Not connected to a voice channel.';
|
||||
textarea.rows = 6;
|
||||
textarea.placeholder = 'Write notes from the current voice session.';
|
||||
save.type = 'button';
|
||||
@@ -1363,16 +1444,19 @@ function renderPanel(context) {
|
||||
post.textContent = 'Post Notes';
|
||||
status.textContent = 'Loading draft...';
|
||||
|
||||
void api.serverData.read(DRAFT_KEY).then((value) => {
|
||||
if (value && typeof value === 'object' && typeof value.text === 'string') {
|
||||
textarea.value = value.text;
|
||||
}
|
||||
void api.serverData
|
||||
.read(DRAFT_KEY)
|
||||
.then((value) => {
|
||||
if (value && typeof value === 'object' && typeof value.text === 'string') {
|
||||
textarea.value = value.text;
|
||||
}
|
||||
|
||||
status.textContent = 'Draft loaded.';
|
||||
}).catch((error) => {
|
||||
api.logger.warn('Could not load voice notes draft', error);
|
||||
status.textContent = 'Could not load draft.';
|
||||
});
|
||||
status.textContent = 'Draft loaded.';
|
||||
})
|
||||
.catch((error) => {
|
||||
api.logger.warn('Could not load voice notes draft', error);
|
||||
status.textContent = 'Could not load draft.';
|
||||
});
|
||||
|
||||
save.addEventListener('click', async () => {
|
||||
const draft = {
|
||||
|
||||
@@ -60,24 +60,24 @@ interface PluginApiAvatarUpdate {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `profile.getCurrent()` | `profile.read` | Returns the current `User` or `null`. |
|
||||
| `profile.update(profile)` | `profile.write` | Updates display name and optional description. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------ | --------------- | ------------------------------------------------- |
|
||||
| `profile.getCurrent()` | `profile.read` | Returns the current `User` or `null`. |
|
||||
| `profile.update(profile)` | `profile.write` | Updates display name and optional description. |
|
||||
| `profile.updateAvatar(avatar)` | `profile.write` | Updates avatar URL, MIME type, and hash metadata. |
|
||||
|
||||
## Users and Roles
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `users.getCurrent()` | `users.read` | Returns current `User` or `null`. |
|
||||
| `users.list()` | `users.read` | Returns known users. |
|
||||
| `users.readMembers()` | `users.read` | Returns active room members. |
|
||||
| `users.setRole(userId, role)` | `roles.manage` | Updates a user's role. |
|
||||
| `users.kick(userId)` | `users.manage` | Kicks a user. |
|
||||
| `users.ban(userId, reason?)` | `users.manage` | Bans a user with optional reason. |
|
||||
| `roles.list()` | `roles.read` | Returns room roles. |
|
||||
| `roles.setAssignments(assignments)` | `roles.manage` | Replaces role assignments. |
|
||||
| Method | Capability | Description |
|
||||
| ----------------------------------- | -------------- | --------------------------------- |
|
||||
| `users.getCurrent()` | `users.read` | Returns current `User` or `null`. |
|
||||
| `users.list()` | `users.read` | Returns known users. |
|
||||
| `users.readMembers()` | `users.read` | Returns active room members. |
|
||||
| `users.setRole(userId, role)` | `roles.manage` | Updates a user's role. |
|
||||
| `users.kick(userId)` | `users.manage` | Kicks a user. |
|
||||
| `users.ban(userId, reason?)` | `users.manage` | Bans a user with optional reason. |
|
||||
| `roles.list()` | `roles.read` | Returns room roles. |
|
||||
| `roles.setAssignments(assignments)` | `roles.manage` | Replaces role assignments. |
|
||||
|
||||
## Server
|
||||
|
||||
@@ -98,12 +98,12 @@ interface PluginApiPluginUserRequest {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `server.getCurrent()` | `server.read` | Returns the current `Room` or `null`. |
|
||||
| `server.registerPluginUser(request)` | `users.manage` | Adds a plugin-owned user and returns its id. |
|
||||
| `server.updatePermissions(permissions)` | `server.manage` | Updates partial room permissions. |
|
||||
| `server.updateSettings(settings)` | `server.manage` | Updates room settings. |
|
||||
| Method | Capability | Description |
|
||||
| --------------------------------------- | --------------- | -------------------------------------------- |
|
||||
| `server.getCurrent()` | `server.read` | Returns the current `Room` or `null`. |
|
||||
| `server.registerPluginUser(request)` | `users.manage` | Adds a plugin-owned user and returns its id. |
|
||||
| `server.updatePermissions(permissions)` | `server.manage` | Updates partial room permissions. |
|
||||
| `server.updateSettings(settings)` | `server.manage` | Updates room settings. |
|
||||
|
||||
## Channels
|
||||
|
||||
@@ -115,14 +115,14 @@ interface PluginApiChannelRequest {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `channels.list()` | `channels.read` | Returns current room channels. |
|
||||
| `channels.select(channelId)` | `channels.read` | Selects a channel. |
|
||||
| `channels.addAudioChannel(request)` | `channels.manage` | Adds a voice channel. |
|
||||
| Method | Capability | Description |
|
||||
| ----------------------------------- | ----------------- | ---------------------------------- |
|
||||
| `channels.list()` | `channels.read` | Returns current room channels. |
|
||||
| `channels.select(channelId)` | `channels.read` | Selects a channel. |
|
||||
| `channels.addAudioChannel(request)` | `channels.manage` | Adds a voice channel. |
|
||||
| `channels.addVideoChannel(request)` | `channels.manage` | Registers a video channel section. |
|
||||
| `channels.rename(channelId, name)` | `channels.manage` | Renames a channel. |
|
||||
| `channels.remove(channelId)` | `channels.manage` | Removes a channel. |
|
||||
| `channels.rename(channelId, name)` | `channels.manage` | Renames a channel. |
|
||||
| `channels.remove(channelId)` | `channels.manage` | Removes a channel. |
|
||||
|
||||
## Messages
|
||||
|
||||
@@ -134,17 +134,17 @@ interface PluginApiMessageAsPluginUserRequest {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `messages.readCurrent()` | `messages.read` | Returns current visible messages. |
|
||||
| `messages.send(content, channelId?)` | `messages.send` | Sends a message and returns the created `Message`. |
|
||||
| `messages.sendAsPluginUser(request)` | `messages.send` | Emits a message from a registered plugin user. |
|
||||
| `messages.setTyping(isTyping, channelId?)` | `messages.send` | Broadcasts current typing state for a channel. |
|
||||
| `messages.subscribeTyping(handler)` | `messages.read` | Subscribes to peer typing state. |
|
||||
| `messages.edit(messageId, content)` | `messages.editOwn` | Edits a plugin message. |
|
||||
| `messages.delete(messageId)` | `messages.deleteOwn` | Deletes a plugin message. |
|
||||
| `messages.moderateDelete(messageId)` | `messages.moderate` | Performs a moderation delete. |
|
||||
| `messages.sync(messages)` | `messages.sync` | Syncs an array of messages into state. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------------------ | -------------------- | -------------------------------------------------- |
|
||||
| `messages.readCurrent()` | `messages.read` | Returns current visible messages. |
|
||||
| `messages.send(content, channelId?)` | `messages.send` | Sends a message and returns the created `Message`. |
|
||||
| `messages.sendAsPluginUser(request)` | `messages.send` | Emits a message from a registered plugin user. |
|
||||
| `messages.setTyping(isTyping, channelId?)` | `messages.send` | Broadcasts current typing state for a channel. |
|
||||
| `messages.subscribeTyping(handler)` | `messages.read` | Subscribes to peer typing state. |
|
||||
| `messages.edit(messageId, content)` | `messages.editOwn` | Edits a plugin message. |
|
||||
| `messages.delete(messageId)` | `messages.deleteOwn` | Deletes a plugin message. |
|
||||
| `messages.moderateDelete(messageId)` | `messages.moderate` | Performs a moderation delete. |
|
||||
| `messages.sync(messages)` | `messages.sync` | Syncs an array of messages into state. |
|
||||
|
||||
## Events
|
||||
|
||||
@@ -167,12 +167,12 @@ interface PluginEventEnvelope<TPayload = unknown> {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `events.publishServer(eventName, payload)` | `events.server.publish` | Sends a declared plugin event through the signaling server. |
|
||||
| `events.subscribeServer(subscription)` | `events.server.subscribe` | Subscribes to a declared server plugin event. |
|
||||
| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | Sends a declared plugin event over peer paths. |
|
||||
| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | Registers a P2P event subscription. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------------------ | ------------------------- | ----------------------------------------------------------- |
|
||||
| `events.publishServer(eventName, payload)` | `events.server.publish` | Sends a declared plugin event through the signaling server. |
|
||||
| `events.subscribeServer(subscription)` | `events.server.subscribe` | Subscribes to a declared server plugin event. |
|
||||
| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | Sends a declared plugin event over peer paths. |
|
||||
| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | Registers a P2P event subscription. |
|
||||
|
||||
## Message Bus
|
||||
|
||||
@@ -215,11 +215,11 @@ interface PluginApiMessageBusSubscription {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `messageBus.publish(request)` | `events.p2p.publish`, optionally `messages.read` | Publishes a plugin-bus event, optionally including latest messages. |
|
||||
| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | Sends a latest-message snapshot. |
|
||||
| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, optionally `messages.read` | Subscribes to plugin-bus events, optionally replaying latest messages. |
|
||||
| Method | Capability | Description |
|
||||
| ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `messageBus.publish(request)` | `events.p2p.publish`, optionally `messages.read` | Publishes a plugin-bus event, optionally including latest messages. |
|
||||
| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | Sends a latest-message snapshot. |
|
||||
| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, optionally `messages.read` | Subscribes to plugin-bus events, optionally replaying latest messages. |
|
||||
|
||||
## P2P and Media
|
||||
|
||||
@@ -235,30 +235,30 @@ interface PluginApiCustomStreamRequest {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `p2p.connectedPeers()` | `p2p.data` | Returns connected peer ids. |
|
||||
| `p2p.broadcastData(eventName, payload)` | `p2p.data` | Broadcasts plugin data. |
|
||||
| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | Sends plugin data targeted to a peer. |
|
||||
| `media.playAudioClip(request)` | `media.playAudio` | Plays an audio URL at optional volume. |
|
||||
| `media.addCustomAudioStream(request)` | `media.addAudioStream` | Contributes an audio `MediaStream`. |
|
||||
| `media.addCustomVideoStream(request)` | `media.addVideoStream` | Registers a video `MediaStream` contribution. |
|
||||
| `media.setInputVolume(volume)` | `audio.volume` | Sets local input volume. |
|
||||
| `media.setOutputVolume(volume)` | `audio.volume` | Sets local output volume. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------------------ | ---------------------- | --------------------------------------------- |
|
||||
| `p2p.connectedPeers()` | `p2p.data` | Returns connected peer ids. |
|
||||
| `p2p.broadcastData(eventName, payload)` | `p2p.data` | Broadcasts plugin data. |
|
||||
| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | Sends plugin data targeted to a peer. |
|
||||
| `media.playAudioClip(request)` | `media.playAudio` | Plays an audio URL at optional volume. |
|
||||
| `media.addCustomAudioStream(request)` | `media.addAudioStream` | Contributes an audio `MediaStream`. |
|
||||
| `media.addCustomVideoStream(request)` | `media.addVideoStream` | Registers a video `MediaStream` contribution. |
|
||||
| `media.setInputVolume(volume)` | `audio.volume` | Sets local input volume. |
|
||||
| `media.setOutputVolume(volume)` | `audio.volume` | Sets local output volume. |
|
||||
|
||||
## Storage
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `clientData.read(key)` | `storage.local` | Reads async plugin-local data. |
|
||||
| `clientData.write(key, value)` | `storage.local` | Writes async plugin-local data. |
|
||||
| `clientData.remove(key)` | `storage.local` | Removes async plugin-local data. |
|
||||
| `serverData.read(key)` | `storage.serverData.read` | Reads local per-user/per-server data. |
|
||||
| `serverData.write(key, value)` | `storage.serverData.write` | Writes local per-user/per-server data. |
|
||||
| `serverData.remove(key)` | `storage.serverData.write` | Removes local per-user/per-server data. |
|
||||
| `storage.get(key)` | `storage.local` | Legacy synchronous local read. |
|
||||
| `storage.set(key, value)` | `storage.local` | Legacy synchronous local write. |
|
||||
| `storage.remove(key)` | `storage.local` | Legacy synchronous local remove. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------ | -------------------------- | --------------------------------------- |
|
||||
| `clientData.read(key)` | `storage.local` | Reads async plugin-local data. |
|
||||
| `clientData.write(key, value)` | `storage.local` | Writes async plugin-local data. |
|
||||
| `clientData.remove(key)` | `storage.local` | Removes async plugin-local data. |
|
||||
| `serverData.read(key)` | `storage.serverData.read` | Reads local per-user/per-server data. |
|
||||
| `serverData.write(key, value)` | `storage.serverData.write` | Writes local per-user/per-server data. |
|
||||
| `serverData.remove(key)` | `storage.serverData.write` | Removes local per-user/per-server data. |
|
||||
| `storage.get(key)` | `storage.local` | Legacy synchronous local read. |
|
||||
| `storage.set(key, value)` | `storage.local` | Legacy synchronous local write. |
|
||||
| `storage.remove(key)` | `storage.local` | Legacy synchronous local remove. |
|
||||
|
||||
## UI Contributions
|
||||
|
||||
@@ -306,24 +306,24 @@ interface PluginApiDomMountRequest {
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `ui.registerAppPage(id, contribution)` | `ui.pages` | Adds a plugin app page. |
|
||||
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | Adds a plugin settings page. |
|
||||
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | Adds a side panel. |
|
||||
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | Adds a channel section. |
|
||||
| `ui.registerComposerAction(id, contribution)` | `ui.pages` | Adds a composer action. |
|
||||
| `ui.registerProfileAction(id, contribution)` | `ui.pages` | Adds a profile action. |
|
||||
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | Adds a toolbar action. |
|
||||
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | Adds an embed renderer. |
|
||||
| `ui.mountElement(id, request)` | `ui.dom` | Mounts plugin-owned DOM into a target element or selector. |
|
||||
| Method | Capability | Description |
|
||||
| --------------------------------------------- | -------------------- | --------------------------------------------------------------- |
|
||||
| `ui.registerAppPage(id, contribution)` | `ui.pages` | Adds a plugin app page. |
|
||||
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | Adds a plugin settings page. |
|
||||
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | Adds a side panel. |
|
||||
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | Adds a channel section. |
|
||||
| `ui.registerComposerAction(id, contribution)` | `ui.pages` | Adds a composer action. |
|
||||
| `ui.registerProfileAction(id, contribution)` | `ui.pages` | Adds a profile action. |
|
||||
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | Adds an action tile to the server side panel View plugins menu. |
|
||||
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | Adds an embed renderer. |
|
||||
| `ui.mountElement(id, request)` | `ui.dom` | Mounts plugin-owned DOM into a target element or selector. |
|
||||
|
||||
## Context and Logger
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `context.getCurrent()` | None | Reads current user, server, active text channel, and active voice channel. |
|
||||
| `logger.debug(message, data?)` | None | Writes a debug plugin log entry. |
|
||||
| `logger.info(message, data?)` | None | Writes an info plugin log entry. |
|
||||
| `logger.warn(message, data?)` | None | Writes a warning plugin log entry. |
|
||||
| `logger.error(message, data?)` | None | Writes an error plugin log entry. |
|
||||
| Method | Capability | Description |
|
||||
| ------------------------------ | ---------- | -------------------------------------------------------------------------- |
|
||||
| `context.getCurrent()` | None | Reads current user, server, active text channel, and active voice channel. |
|
||||
| `logger.debug(message, data?)` | None | Writes a debug plugin log entry. |
|
||||
| `logger.info(message, data?)` | None | Writes an info plugin log entry. |
|
||||
| `logger.warn(message, data?)` | None | Writes a warning plugin log entry. |
|
||||
| `logger.error(message, data?)` | None | Writes an error plugin log entry. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,44 +6,44 @@ sidebar_position: 3
|
||||
|
||||
Capabilities protect privileged app surfaces. A plugin must declare a capability in its manifest and the user must grant it before the runtime allows the corresponding API call.
|
||||
|
||||
| Capability | API areas | Notes |
|
||||
| --- | --- | --- |
|
||||
| `profile.read` | `profile.getCurrent()` | Reads the current user. |
|
||||
| `profile.write` | `profile.update()`, `profile.updateAvatar()` | Updates local profile fields and avatar metadata. |
|
||||
| `users.read` | `users.getCurrent()`, `users.list()`, `users.readMembers()` | Reads users and server members. |
|
||||
| `users.manage` | `users.kick()`, `users.ban()`, `server.registerPluginUser()` | Can create plugin users and moderate members. |
|
||||
| `roles.read` | `roles.list()` | Reads server roles. |
|
||||
| `roles.manage` | `roles.setAssignments()`, `users.setRole()` | Changes role assignments or user roles. |
|
||||
| `messages.read` | `messages.readCurrent()`, message bus latest snapshots | Reads current channel messages. |
|
||||
| `messages.send` | `messages.send()`, `messages.sendAsPluginUser()` | Sends messages as the current user or registered plugin user. |
|
||||
| `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. |
|
||||
| `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. |
|
||||
| `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. |
|
||||
| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. |
|
||||
| `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. |
|
||||
| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. |
|
||||
| `server.read` | `server.getCurrent()` | Reads active server. |
|
||||
| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. |
|
||||
| `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. |
|
||||
| `p2p.media` | Reserved peer media features. | Included for media-facing plugins. |
|
||||
| `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. |
|
||||
| `media.addAudioStream` | `media.addCustomAudioStream()` | Adds a custom stream to voice handling. |
|
||||
| `media.addVideoStream` | `media.addCustomVideoStream()` | Registers custom video stream contribution. |
|
||||
| `audio.volume` | `media.setInputVolume()`, `media.setOutputVolume()` | Adjusts local voice volume. |
|
||||
| `audio.effects` | Reserved audio effect features. | Included for audio processing plugins. |
|
||||
| `ui.settings` | `ui.registerSettingsPage()` | Adds settings pages. |
|
||||
| `ui.pages` | `ui.registerAppPage()`, `ui.registerComposerAction()`, `ui.registerProfileAction()`, `ui.registerToolbarAction()` | Adds app pages and actions. |
|
||||
| `ui.sidePanel` | `ui.registerSidePanel()` | Adds side panels. |
|
||||
| `ui.channelsSection` | `ui.registerChannelSection()` | Adds channel sections. |
|
||||
| `ui.embeds` | `ui.registerEmbedRenderer()` | Renders custom embeds. |
|
||||
| `ui.dom` | `ui.mountElement()` | Mounts plugin-owned DOM into app targets. |
|
||||
| `storage.local` | `storage.*`, `clientData.*` | Reads and writes plugin-local data. |
|
||||
| `storage.serverData.read` | `serverData.read()` | Reads local per-user/per-server plugin data. |
|
||||
| `storage.serverData.write` | `serverData.write()`, `serverData.remove()` | Writes or removes local per-user/per-server plugin data. |
|
||||
| `events.server.publish` | `events.publishServer()` | Publishes declared server plugin events. |
|
||||
| `events.server.subscribe` | `events.subscribeServer()` | Subscribes to declared server plugin events. |
|
||||
| `events.p2p.publish` | `events.publishP2p()`, `messageBus.publish()`, `messageBus.sendLatestMessages()` | Publishes declared P2P/plugin bus events. |
|
||||
| `events.p2p.subscribe` | `events.subscribeP2p()`, `messageBus.subscribe()` | Subscribes to declared P2P/plugin bus events. |
|
||||
| Capability | API areas | Notes |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `profile.read` | `profile.getCurrent()` | Reads the current user. |
|
||||
| `profile.write` | `profile.update()`, `profile.updateAvatar()` | Updates local profile fields and avatar metadata. |
|
||||
| `users.read` | `users.getCurrent()`, `users.list()`, `users.readMembers()` | Reads users and server members. |
|
||||
| `users.manage` | `users.kick()`, `users.ban()`, `server.registerPluginUser()` | Can create plugin users and moderate members. |
|
||||
| `roles.read` | `roles.list()` | Reads server roles. |
|
||||
| `roles.manage` | `roles.setAssignments()`, `users.setRole()` | Changes role assignments or user roles. |
|
||||
| `messages.read` | `messages.readCurrent()`, message bus latest snapshots | Reads current channel messages. |
|
||||
| `messages.send` | `messages.send()`, `messages.sendAsPluginUser()` | Sends messages as the current user or registered plugin user. |
|
||||
| `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. |
|
||||
| `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. |
|
||||
| `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. |
|
||||
| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. |
|
||||
| `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. |
|
||||
| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. |
|
||||
| `server.read` | `server.getCurrent()` | Reads active server. |
|
||||
| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. |
|
||||
| `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. |
|
||||
| `p2p.media` | Reserved peer media features. | Included for media-facing plugins. |
|
||||
| `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. |
|
||||
| `media.addAudioStream` | `media.addCustomAudioStream()` | Adds a custom stream to voice handling. |
|
||||
| `media.addVideoStream` | `media.addCustomVideoStream()` | Registers custom video stream contribution. |
|
||||
| `audio.volume` | `media.setInputVolume()`, `media.setOutputVolume()` | Adjusts local voice volume. |
|
||||
| `audio.effects` | Reserved audio effect features. | Included for audio processing plugins. |
|
||||
| `ui.settings` | `ui.registerSettingsPage()` | Adds settings pages. |
|
||||
| `ui.pages` | `ui.registerAppPage()`, `ui.registerComposerAction()`, `ui.registerProfileAction()`, `ui.registerToolbarAction()` | Adds app pages and action entry points, including View plugins menu actions. |
|
||||
| `ui.sidePanel` | `ui.registerSidePanel()` | Adds side panels. |
|
||||
| `ui.channelsSection` | `ui.registerChannelSection()` | Adds channel sections. |
|
||||
| `ui.embeds` | `ui.registerEmbedRenderer()` | Renders custom embeds. |
|
||||
| `ui.dom` | `ui.mountElement()` | Mounts plugin-owned DOM into app targets. |
|
||||
| `storage.local` | `storage.*`, `clientData.*` | Reads and writes plugin-local data. |
|
||||
| `storage.serverData.read` | `serverData.read()` | Reads local per-user/per-server plugin data. |
|
||||
| `storage.serverData.write` | `serverData.write()`, `serverData.remove()` | Writes or removes local per-user/per-server plugin data. |
|
||||
| `events.server.publish` | `events.publishServer()` | Publishes declared server plugin events. |
|
||||
| `events.server.subscribe` | `events.subscribeServer()` | Subscribes to declared server plugin events. |
|
||||
| `events.p2p.publish` | `events.publishP2p()`, `messageBus.publish()`, `messageBus.sendLatestMessages()` | Publishes declared P2P/plugin bus events. |
|
||||
| `events.p2p.subscribe` | `events.subscribeP2p()`, `messageBus.subscribe()` | Subscribes to declared P2P/plugin bus events. |
|
||||
|
||||
## Recommended Practice
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ The manifest file can be named `toju-plugin.json` or `plugin.json`. Entrypoints
|
||||
"schemaVersion": 1,
|
||||
"id": "example.hello-world",
|
||||
"title": "Hello World",
|
||||
"description": "Adds a toolbar action that sends a message.",
|
||||
"description": "Adds a View plugins menu action that sends a message.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"scope": "client",
|
||||
@@ -49,6 +49,7 @@ export function activate(context) {
|
||||
api.logger.info('Hello World activated');
|
||||
|
||||
const disposable = api.ui.registerToolbarAction('hello', {
|
||||
icon: 'HI',
|
||||
label: 'Hello',
|
||||
run: () => api.messages.send('Hello from my plugin')
|
||||
});
|
||||
@@ -65,15 +66,17 @@ export function deactivate(context) {
|
||||
}
|
||||
```
|
||||
|
||||
`registerToolbarAction()` adds an action tile to the server side panel's View plugins menu. Use `icon` for the tile badge and keep the `label` short enough to scan in a grid.
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
| Hook | When it runs | Use it for |
|
||||
| --- | --- | --- |
|
||||
| `activate(context)` | During explicit plugin activation. | Register UI, subscribe to events, initialize state. |
|
||||
| `ready(context)` | After the load-order pass has activated ready plugins. | Cross-plugin coordination that needs other plugins loaded. |
|
||||
| `deactivate(context)` | During unload or reload. | Flush state and log shutdown. Disposables are also cleaned up by the host. |
|
||||
| `onPluginDataChanged(context, event)` | When plugin data changes are observed. | React to plugin-scoped persistence changes. |
|
||||
| `onServerRequirementsChanged(context, snapshot)` | When server plugin requirements change. | Adapt to required, optional, blocked, or incompatible server plugins. |
|
||||
| Hook | When it runs | Use it for |
|
||||
| ------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| `activate(context)` | During explicit plugin activation. | Register UI, subscribe to events, initialize state. |
|
||||
| `ready(context)` | After the load-order pass has activated ready plugins. | Cross-plugin coordination that needs other plugins loaded. |
|
||||
| `deactivate(context)` | During unload or reload. | Flush state and log shutdown. Disposables are also cleaned up by the host. |
|
||||
| `onPluginDataChanged(context, event)` | When plugin data changes are observed. | React to plugin-scoped persistence changes. |
|
||||
| `onServerRequirementsChanged(context, snapshot)` | When server plugin requirements change. | Adapt to required, optional, blocked, or incompatible server plugins. |
|
||||
|
||||
## Cleanup
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ sidebar_position: 5
|
||||
"schemaVersion": 1,
|
||||
"id": "example.toolbar-message",
|
||||
"title": "Toolbar Message",
|
||||
"description": "Adds a toolbar action that sends a reusable message.",
|
||||
"description": "Adds a View plugins menu action that sends a reusable message.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"scope": "client",
|
||||
@@ -33,13 +33,18 @@ sidebar_position: 5
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.ui.registerToolbarAction('standup-message', {
|
||||
label: 'Standup',
|
||||
run: () => api.messages.send('Standup: yesterday, today, blocked')
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.ui.registerToolbarAction('standup-message', {
|
||||
icon: 'ST',
|
||||
label: 'Standup',
|
||||
run: (actionContext) => api.messages.send('Standup: yesterday, today, blocked', actionContext.textChannel?.id)
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The action appears as a tile in the server side panel's View plugins menu and runs with `source: 'toolbarAction'`.
|
||||
|
||||
## Settings Page Plugin
|
||||
|
||||
```json
|
||||
@@ -67,19 +72,21 @@ export function activate(context) {
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.ui.registerSettingsPage('preferences', {
|
||||
label: 'Example Preferences',
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const button = document.createElement('button');
|
||||
context.subscriptions.push(
|
||||
api.ui.registerSettingsPage('preferences', {
|
||||
label: 'Example Preferences',
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.type = 'button';
|
||||
button.textContent = 'Remember preference';
|
||||
button.onclick = () => api.storage.set('enabled', true);
|
||||
root.append(button);
|
||||
return root;
|
||||
}
|
||||
}));
|
||||
button.type = 'button';
|
||||
button.textContent = 'Remember preference';
|
||||
button.onclick = () => api.storage.set('enabled', true);
|
||||
root.append(button);
|
||||
return root;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -99,13 +106,7 @@ A server-scoped plugin can be installed as a server requirement and auto-install
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": { "minimumTojuVersion": "1.0.0" },
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": [
|
||||
"server.read",
|
||||
"users.manage",
|
||||
"ui.sidePanel",
|
||||
"media.playAudio",
|
||||
"messages.send"
|
||||
],
|
||||
"capabilities": ["server.read", "users.manage", "ui.sidePanel", "media.playAudio", "messages.send"],
|
||||
"pluginUser": {
|
||||
"displayName": "Soundboard",
|
||||
"label": "Audio helper"
|
||||
@@ -121,23 +122,25 @@ export function activate(context) {
|
||||
displayName: 'Soundboard'
|
||||
});
|
||||
|
||||
context.subscriptions.push(api.ui.registerSidePanel('sounds', {
|
||||
label: 'Soundboard',
|
||||
render: () => {
|
||||
const panel = document.createElement('div');
|
||||
const button = document.createElement('button');
|
||||
context.subscriptions.push(
|
||||
api.ui.registerSidePanel('sounds', {
|
||||
label: 'Soundboard',
|
||||
render: () => {
|
||||
const panel = document.createElement('div');
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.type = 'button';
|
||||
button.textContent = 'Play chime';
|
||||
button.onclick = async () => {
|
||||
await api.media.playAudioClip({ url: './chime.wav', volume: 0.7 });
|
||||
api.messages.sendAsPluginUser({ pluginUserId: botId, content: 'Played chime' });
|
||||
};
|
||||
button.type = 'button';
|
||||
button.textContent = 'Play chime';
|
||||
button.onclick = async () => {
|
||||
await api.media.playAudioClip({ url: './chime.wav', volume: 0.7 });
|
||||
api.messages.sendAsPluginUser({ pluginUserId: botId, content: 'Played chime' });
|
||||
};
|
||||
|
||||
panel.append(button);
|
||||
return panel;
|
||||
}
|
||||
}));
|
||||
panel.append(button);
|
||||
return panel;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -162,12 +165,14 @@ export function activate(context) {
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.messageBus.subscribe({
|
||||
topic: 'poll:votes',
|
||||
replayLatest: true,
|
||||
latestMessageLimit: 20,
|
||||
handler: (event) => api.logger.info('Vote received', event.payload)
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
api.messageBus.subscribe({
|
||||
topic: 'poll:votes',
|
||||
replayLatest: true,
|
||||
latestMessageLimit: 20,
|
||||
handler: (event) => api.logger.info('Vote received', event.payload)
|
||||
})
|
||||
);
|
||||
|
||||
api.messageBus.publish({
|
||||
topic: 'poll:votes',
|
||||
@@ -192,10 +197,12 @@ export function activate(context) {
|
||||
badge.style.right = '1rem';
|
||||
badge.style.bottom = '1rem';
|
||||
|
||||
context.subscriptions.push(context.api.ui.mountElement('active-badge', {
|
||||
target: 'body',
|
||||
element: badge
|
||||
}));
|
||||
context.subscriptions.push(
|
||||
context.api.ui.mountElement('active-badge', {
|
||||
target: 'body',
|
||||
element: badge
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ Plugins add features to MetoYou. They can add pages, buttons, panels, settings,
|
||||
|
||||
## Types of Plugins
|
||||
|
||||
| Type | What it means |
|
||||
| --- | --- |
|
||||
| Client plugin | Installed for your app. It follows you across servers when active. |
|
||||
| Server plugin | Installed for a specific server. It may be required, recommended, optional, blocked, or incompatible. |
|
||||
| Library plugin | Shared plugin code used by other plugins. It is not normally something users interact with directly. |
|
||||
| Type | What it means |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| Client plugin | Installed for your app. It follows you across servers when active. |
|
||||
| Server plugin | Installed for a specific server. It may be required, recommended, optional, blocked, or incompatible. |
|
||||
| Library plugin | Shared plugin code used by other plugins. It is not normally something users interact with directly. |
|
||||
|
||||
## Install from the Plugin Store
|
||||
|
||||
@@ -26,6 +26,10 @@ Plugins add features to MetoYou. They can add pages, buttons, panels, settings,
|
||||
|
||||
Server-scoped plugins installed to the server you are currently viewing are enabled and activated automatically after install, so their panels, actions, or embeds can appear immediately.
|
||||
|
||||
## Use Plugin Actions
|
||||
|
||||
When plugins add quick actions to a server, the server side panel shows a View plugins link in the plugin area. Open it to see a grid of plugin action tiles. Selecting a tile runs that plugin's action in the current server and channel context.
|
||||
|
||||
## Install a Local Plugin
|
||||
|
||||
Desktop builds can discover local plugin folders from the app data plugins directory.
|
||||
@@ -40,12 +44,12 @@ Desktop builds can discover local plugin folders from the app data plugins direc
|
||||
|
||||
When a server uses plugins, MetoYou may show a prompt.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| Required | You must install the plugin to join or continue using that server. |
|
||||
| Recommended | The server suggests the plugin, but you can choose. |
|
||||
| Optional | The plugin is available for the server, but not required. |
|
||||
| Blocked | The server marks the plugin as not allowed. |
|
||||
| Status | Meaning |
|
||||
| ------------ | --------------------------------------------------------------------------------- |
|
||||
| Required | You must install the plugin to join or continue using that server. |
|
||||
| Recommended | The server suggests the plugin, but you can choose. |
|
||||
| Optional | The plugin is available for the server, but not required. |
|
||||
| Blocked | The server marks the plugin as not allowed. |
|
||||
| Incompatible | The plugin version does not work with your app version or the server requirement. |
|
||||
|
||||
Required plugins are still installed locally on your device. The signaling server stores requirement metadata only; it does not run plugin code.
|
||||
@@ -56,13 +60,13 @@ Plugins must ask for capabilities before using sensitive features.
|
||||
|
||||
Examples:
|
||||
|
||||
| Capability area | Why a plugin might ask |
|
||||
| --- | --- |
|
||||
| Messages | Send messages, read current messages, moderate messages, or render embeds. |
|
||||
| Users and roles | Read member lists, create plugin users, or manage users. |
|
||||
| Voice and media | Play audio, add an audio stream, add a video stream, or adjust volume. |
|
||||
| UI | Add pages, settings pages, side panels, toolbar buttons, or DOM elements. |
|
||||
| Storage | Save plugin preferences locally or per server. |
|
||||
| Capability area | Why a plugin might ask |
|
||||
| --------------- | -------------------------------------------------------------------------- |
|
||||
| Messages | Send messages, read current messages, moderate messages, or render embeds. |
|
||||
| Users and roles | Read member lists, create plugin users, or manage users. |
|
||||
| Voice and media | Play audio, add an audio stream, add a video stream, or adjust volume. |
|
||||
| UI | Add pages, settings pages, side panels, toolbar buttons, or DOM elements. |
|
||||
| Storage | Save plugin preferences locally or per server. |
|
||||
|
||||
Only grant capabilities to plugins you trust.
|
||||
|
||||
@@ -79,4 +83,4 @@ The Plugin Manager lets you:
|
||||
|
||||
## Plugin Safety Notes
|
||||
|
||||
Plugins are browser-safe JavaScript modules loaded by the client. They do not run on the signaling server. A plugin can only call privileged MetoYou APIs when its manifest declares the capability and you grant it.
|
||||
Plugins are browser-safe JavaScript modules loaded by the client. They do not run on the signaling server. A plugin can only call privileged MetoYou APIs when its manifest declares the capability and you grant it.
|
||||
|
||||
Reference in New Issue
Block a user