feat: Add Plugin Button Registration API documentation and Plugin System implementation summary
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-linux (push) Successful in 2m29s
Build / build-windows (push) Successful in 6m50s
Build / create-release (push) Successful in 52s

- Introduced PLUGIN_BUTTON_API.md detailing button registration, UI locations, and usage examples.
- Added PLUGIN_SYSTEM_IMPLEMENTATION.md summarizing implemented features, usage examples, and API details for the plugin system.
This commit is contained in:
2026-04-22 00:29:14 +10:00
parent 19ce4b43ca
commit 8ccc3ecb78
8 changed files with 600 additions and 18 deletions

View File

@@ -70,7 +70,7 @@ Plugins run with access to your Matrix client and can:
## Developing Plugins
Want to create your own plugin? Check out the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md).
Want to create your own plugin? Check out the [Plugin API Reference](docs/PLUGIN_API.md).
### Quick Start
@@ -101,19 +101,19 @@ Plugins have access to:
- **Matrix Client**: Full matrix-js-sdk client instance
- **React**: For building UI components
- **Commands**: Register custom slash commands
- **UI Hooks**: Inject UI elements
- **Settings**: Add custom settings sections
- **Utilities**: Notifications and helpers
- **UI Buttons**: Inject buttons into 11 locations across the app (nav lists, toolbars, headers, sidebar)
- **UI Renderers**: Custom message and content renderers
- **Settings**: Add custom settings sections per plugin
- **Themes**: Register custom themes that appear in the theme selector
- **Matrix Events**: Hook into raw Matrix events
- **Timers**: Background intervals and timeouts, auto-cleaned on unload
- **Notifications**: System notifications
See the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) for complete API documentation.
See the [Plugin API Reference](docs/PLUGIN_API.md) and [Button Registration API](docs/PLUGIN_BUTTON_API.md) for complete documentation.
## Example Plugins
Check out the `example-plugin/` directory for a simple example demonstrating:
- Matrix client access
- Event listeners
- Custom commands
- Notifications
Check out the `example-plugin/` directory for a simple example, or the `plugins/example-showcase-plugin/` for a full demonstration of all 11 UI button locations.
## Troubleshooting
@@ -147,7 +147,8 @@ Submit your plugins there to make them available in the marketplace!
## Support
- **Documentation**: [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md)
- **Plugin API Reference**: [docs/PLUGIN_API.md](docs/PLUGIN_API.md)
- **Button API Reference**: [docs/PLUGIN_BUTTON_API.md](docs/PLUGIN_BUTTON_API.md)
- **Issues**: https://github.com/Paarrot/cinny-desktop/issues
- **Community**: Join our Matrix room for plugin development help

View File

@@ -29,3 +29,50 @@ To build the app locally, run:
To start local dev server, run:
* `npm run dev`
## Plugin System
Paarrot includes a plugin system for extending and customising the app with JavaScript modules.
### Installing Plugins
1. Drop a plugin folder into your plugins directory:
- **Windows**: `%APPDATA%\paarrot\plugins\<plugin-name>\`
- **Linux**: `~/.config/Paarrot/plugins/<plugin-name>/`
- **macOS**: `~/Library/Application Support/Paarrot/plugins/<plugin-name>/`
2. Enable it in **Settings → Plugins → Installed**
### What Plugins Can Do
- Register custom slash commands
- Intercept and modify messages
- Inject buttons into 11 UI locations (nav lists, toolbars, headers, sidebar, menus)
- Register custom themes
- Hook into raw Matrix events
- Run background tasks
- Show system notifications
### Plugin Locations (UI Buttons)
Plugins can inject buttons as **nav list rows** or **icon buttons** across the app:
| Location | Style | Where |
|---|---|---|
| `channel-list` | Nav row | Space channel list |
| `home-section` | Nav row | Home panel, above room list |
| `direct-messages` | Nav row | DMs panel, below "Create Chat" |
| `sidebar-actions` | Icon | Left sidebar — above Explore and above Search |
| `text-composer-toolbar` | Icon | Message composer toolbar |
| `composer-actions` | Icon | Beside the `+` attach button |
| `room-header` | Icon | Room header bar |
| `room-menu` | Icon | Room ⋮ dropdown |
| `message-actions` | Icon | Message hover bar |
| `user-menu` | Icon | Right-click on user avatar |
| `search-notification-section` | Icon | Notifications page header |
### Documentation
- [Plugin System Overview](PLUGINS.md)
- [Full Plugin API Reference](docs/PLUGIN_API.md)
- [Button Registration API](docs/PLUGIN_BUTTON_API.md)
- [Example Plugins](plugins/)

2
cinny

Submodule cinny updated: 9a463facce...64e252da94

View File

View File

@@ -9,7 +9,8 @@ Complete reference for developing plugins for Paarrot/Cinny Desktop.
- [Plugin Context API](#plugin-context-api)
- [Commands](#commands)
- [Message Interceptors](#message-interceptors)
- [Custom Renderers](#custom-renderers)
- [UI: Button Registration](#ui-button-registration)
- [UI: Custom Renderers](#custom-renderers)
- [Settings](#settings)
- [Themes](#themes)
- [Matrix Events](#matrix-events)
@@ -322,6 +323,8 @@ ctx.ui.registerRenderer("image", (data, defaultRenderer) => {
```typescript
interface UIAPI {
registerButton(button: UIButtonDefinition): void;
unregisterButton(id: string): void;
registerRenderer(
type: string,
renderer: (data: any, defaultRenderer?: () => ReactNode) => ReactNode
@@ -336,6 +339,97 @@ type CustomRenderer = (
---
### UI: Button Registration
Inject buttons into various parts of the Paarrot UI. Buttons render in two visual styles depending on location — **nav list rows** (icon + label, full width) or **icon buttons** (compact, toolbar-style).
See [PLUGIN_BUTTON_API.md](PLUGIN_BUTTON_API.md) for the complete reference including positioning and grouping examples.
#### Register a Button
```javascript
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
onClick: () => ctx.log('clicked!')
});
```
#### UI Locations — Nav List Rows
| Location | Where it appears |
|---|---|
| `channel-list` | Space channel list, below Lobby and Message Search |
| `home-section` | Home panel, inside the Rooms category above the room list |
| `direct-messages` | DMs panel, below "Create Chat" and above the CHATS dropdown |
#### UI Locations — Icon Buttons
| Location | Where it appears |
|---|---|
| `text-composer-toolbar` | Message composer toolbar (alongside emoji, sticker buttons) |
| `composer-actions` | Left side of the composer, beside the `+` attach button |
| `room-header` | Top room header bar, before the ⋮ menu |
| `room-menu` | Room ⋮ dropdown menu |
| `message-actions` | Message hover action bar |
| `user-menu` | Right-click popup on the user avatar |
| `search-notification-section` | Notifications page header (right side) |
| `sidebar-actions` | Left sidebar — above Explore Servers icon, and above Search icon in the sticky bottom section |
#### Positioning
```javascript
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
position: {
after: 'emoji-picker-button',
group: 'my-tools',
order: 1
},
onClick: () => ctx.log('clicked!')
});
```
#### Unregister
```javascript
ctx.ui.unregisterButton('my-button');
// All buttons are automatically unregistered on plugin unload
```
#### API Reference
```typescript
type UILocation =
| 'channel-list' | 'direct-messages' | 'home-section'
| 'text-composer-toolbar' | 'composer-actions'
| 'room-header' | 'room-menu' | 'message-actions'
| 'user-menu' | 'search-notification-section' | 'sidebar-actions';
interface UIButtonDefinition {
id: string;
location: UILocation;
label: string;
icon?: string;
position?: UIButtonPosition;
onClick?: () => void | Promise<void>;
}
interface UIButtonPosition {
before?: string;
after?: string;
group?: string;
order?: number;
}
```
---
### Settings
Define settings that users can configure in the UI.

432
docs/PLUGIN_BUTTON_API.md Normal file
View File

@@ -0,0 +1,432 @@
# Plugin Button Registration API
An easy way to register plugin buttons in various UI locations with positioning and grouping support.
## Example Plugins
- **[example-button-plugin/](example-button-plugin/)** - Simple examples showing basic usage patterns
- **[example-showcase-plugin/](example-showcase-plugin/)** - Complete demonstration of ALL 11 locations ⭐
## UI Locations
Buttons render in two visual styles depending on location — **nav list rows** (icon + label, full width) or **icon buttons** (compact, toolbar/header style).
### Nav List Rows
These render as full-width list entries matching the style of built-in items like "Create Room" and "Message Search":
| Location | Where it appears |
|---|---|
| `channel-list` | Space channel list, below Lobby and Message Search |
| `home-section` | Home panel, inside the Rooms category above the room list |
| `direct-messages` | Direct Messages panel, below "Create Chat" and above the CHATS dropdown |
### Icon Buttons
These render as compact icon buttons inline with other toolbar/header controls:
| Location | Where it appears |
|---|---|
| `text-composer-toolbar` | Message composer toolbar (alongside emoji, sticker buttons) |
| `composer-actions` | Left side of the composer, beside the `+` attach button |
| `room-header` | Top room header bar, before the ⋮ menu button |
| `room-menu` | Room ⋮ dropdown menu |
| `message-actions` | Message hover action bar |
| `user-menu` | Right-click popup on the user avatar |
| `search-notification-section` | Notifications page header (right side) |
| `sidebar-actions` | Left sidebar — appears in **two** places: above the Explore Servers icon, and above the Search icon in the sticky bottom section |
> 💡 **Tip:** Install the [example-showcase-plugin](example-showcase-plugin/) to see exactly where each location appears in the UI!
## Basic Usage
### Simple Button
```javascript
module.exports = {
name: 'my-plugin',
version: '1.0.0',
onLoad: (context) => {
context.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '🎨',
onClick: () => {
context.log('Button clicked!');
}
});
},
onUnload: () => {
// Buttons are automatically cleaned up on plugin unload
}
};
```
## Positioning
### Before/After Positioning
Place your button before or after existing buttons:
```javascript
context.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '✨',
position: {
before: 'emoji-picker-button', // Place before emoji picker
// OR
after: 'sticker-button' // Place after sticker button
}
});
```
### Grouping Buttons
Group related buttons together:
```javascript
context.ui.registerButton({
id: 'action-1',
location: 'text-composer-toolbar',
label: 'Action 1',
icon: '1⃣',
position: {
group: 'my-plugin-tools',
order: 1 // Lower numbers appear first
}
});
context.ui.registerButton({
id: 'action-2',
location: 'text-composer-toolbar',
label: 'Action 2',
icon: '2⃣',
position: {
group: 'my-plugin-tools',
order: 2
}
});
```
### Combined Positioning and Grouping
Place a group before/after other elements:
```javascript
context.ui.registerButton({
id: 'tool-1',
location: 'text-composer-toolbar',
label: 'Tool 1',
icon: '🔧',
position: {
group: 'my-tools',
after: 'emoji-picker-button',
order: 1
}
});
context.ui.registerButton({
id: 'tool-2',
location: 'text-composer-toolbar',
label: 'Tool 2',
icon: '🔨',
position: {
group: 'my-tools',
order: 2
}
});
```
## Complete Example
```javascript
module.exports = {
name: 'custom-tools',
version: '1.0.0',
onLoad: (context) => {
// Nav list row — appears below "Create Chat" in the DMs panel
context.ui.registerButton({
id: 'dm-favorites',
location: 'direct-messages',
label: 'Favourites',
icon: '⭐',
onClick: () => context.log('Favourites clicked!')
});
// Nav list row — appears in the space channel list
context.ui.registerButton({
id: 'quick-access',
location: 'channel-list',
label: 'Quick Access',
icon: '⚡',
onClick: () => context.log('Quick access clicked!')
});
// Icon button — appears in the message composer toolbar
context.ui.registerButton({
id: 'format-bold',
location: 'text-composer-toolbar',
label: 'Bold',
icon: '𝐁',
position: {
group: 'custom-formatting',
order: 1
},
onClick: () => context.log('Bold clicked!')
});
// Icon button — appears in the room header
context.ui.registerButton({
id: 'special-search',
location: 'room-header',
label: 'Special Search',
icon: '🔍',
position: { after: 'search-button' },
onClick: () => context.log('Special search clicked!')
});
// Sidebar icon — appears above Explore and above Search
context.ui.registerButton({
id: 'plugin-panel',
location: 'sidebar-actions',
label: 'Plugin Panel',
icon: '🧩',
onClick: () => context.log('Plugin panel clicked!')
});
},
onUnload: () => {
// All registered buttons are automatically cleaned up
}
};
```
## Unregistering Buttons
Buttons are automatically unregistered when the plugin is unloaded. To manually unregister:
```javascript
context.ui.unregisterButton('my-button-id');
```
## TypeScript Types
```typescript
import type { UIButtonDefinition, UIButtonPosition, UILocation } from '@paarrot/plugin-manager';
const button: UIButtonDefinition = {
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '🎨',
position: {
group: 'my-tools',
after: 'emoji-picker',
order: 1
},
onClick: () => console.log('Clicked!')
};
```
## Basic Usage
### Simple Button
```javascript
module.exports = {
name: 'my-plugin',
version: '1.0.0',
onLoad: (context) => {
context.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '🎨',
onClick: () => {
context.log('Button clicked!');
}
});
},
onUnload: () => {
// Buttons are automatically cleaned up on plugin unload
}
};
```
## Positioning
### Before/After Positioning
Place your button before or after existing buttons:
```javascript
context.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '✨',
position: {
before: 'emoji-picker-button', // Place before emoji picker
// OR
after: 'sticker-button' // Place after sticker button
}
});
```
### Grouping Buttons
Group related buttons together:
```javascript
context.ui.registerButton({
id: 'action-1',
location: 'text-composer-toolbar',
label: 'Action 1',
icon: '1⃣',
position: {
group: 'my-plugin-tools',
order: 1 // Lower numbers appear first
}
});
context.ui.registerButton({
id: 'action-2',
location: 'text-composer-toolbar',
label: 'Action 2',
icon: '2⃣',
position: {
group: 'my-plugin-tools',
order: 2
}
});
```
### Combined Positioning and Grouping
Place a group before/after other elements:
```javascript
context.ui.registerButton({
id: 'tool-1',
location: 'text-composer-toolbar',
label: 'Tool 1',
icon: '🔧',
position: {
group: 'my-tools',
after: 'emoji-picker-button',
order: 1
}
});
context.ui.registerButton({
id: 'tool-2',
location: 'text-composer-toolbar',
label: 'Tool 2',
icon: '🔨',
position: {
group: 'my-tools',
order: 2
}
});
```
## Complete Example
```javascript
module.exports = {
name: 'custom-tools',
version: '1.0.0',
onLoad: (context) => {
// Nav list row — appears below "Create Chat" in the DMs panel
context.ui.registerButton({
id: 'dm-favorites',
location: 'direct-messages',
label: 'Favourites',
icon: '⭐',
onClick: () => context.log('Favourites clicked!')
});
// Nav list row — appears in the space channel list
context.ui.registerButton({
id: 'quick-access',
location: 'channel-list',
label: 'Quick Access',
icon: '⚡',
onClick: () => context.log('Quick access clicked!')
});
// Icon button — appears in the message composer toolbar
context.ui.registerButton({
id: 'format-bold',
location: 'text-composer-toolbar',
label: 'Bold',
icon: '𝐁',
position: {
group: 'custom-formatting',
order: 1
},
onClick: () => context.log('Bold clicked!')
});
// Icon button — appears in the room header
context.ui.registerButton({
id: 'special-search',
location: 'room-header',
label: 'Special Search',
icon: '🔍',
position: { after: 'search-button' },
onClick: () => context.log('Special search clicked!')
});
// Sidebar icon — appears above Explore and above Search
context.ui.registerButton({
id: 'plugin-panel',
location: 'sidebar-actions',
label: 'Plugin Panel',
icon: '🧩',
onClick: () => context.log('Plugin panel clicked!')
});
},
onUnload: () => {
// All registered buttons are automatically cleaned up
}
};
```
## Unregistering Buttons
Buttons are automatically unregistered when the plugin is unloaded. To manually unregister:
```javascript
context.ui.unregisterButton('my-button-id');
```
## TypeScript Types
```typescript
import type { UIButtonDefinition, UIButtonPosition, UILocation } from '@paarrot/plugin-manager';
const button: UIButtonDefinition = {
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Action',
icon: '🎨',
position: {
group: 'my-tools',
after: 'emoji-picker',
order: 1
},
onClick: () => console.log('Clicked!')
};
```

View File

@@ -16,11 +16,17 @@ All requested features have been implemented! 🎉
- Full MessageContext with content, roomId, metadata
- Async support for interceptors
### 3. ✅ Custom Renderers
- **Register**: `ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { /* render */ })`
- **Unregister**: `ctx.ui.unregisterRenderer("message")`
- Fallback to default renderer support
- Discord-mod-tier customization enabled
### 3. ✅ Custom Renderers & Button Registration
- **Register renderer**: `ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { /* render */ })`
- **Unregister renderer**: `ctx.ui.unregisterRenderer("message")`
- **Register button**: `ctx.ui.registerButton({ id, location, label, icon, onClick })`
- **Unregister button**: `ctx.ui.unregisterButton(id)`
- 11 UI locations across nav lists, toolbars, headers, sidebar, and menus
- Nav list rows (icon + label) for `channel-list`, `home-section`, `direct-messages`
- Icon buttons for all other locations
- Positioning and grouping support (`before`, `after`, `group`, `order`)
- Auto-cleanup on plugin unload
- See [PLUGIN_BUTTON_API.md](PLUGIN_BUTTON_API.md) for full reference
### 4. ✅ Settings UI Per Plugin
- **Define schema**: `ctx.settings.define({ theme: { type: "select", options: [...] } })`
@@ -162,6 +168,8 @@ interface PluginContext {
};
ui: {
registerButton: (button: UIButtonDefinition) => void;
unregisterButton: (id: string) => void;
registerRenderer: (type: string, renderer: CustomRenderer) => void;
unregisterRenderer: (type: string) => void;
};