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