fix: Improve plugin ui entry points, Fix chat scroll, fix notifications, fix user rights
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user