diff --git a/PLUGINS.md b/PLUGINS.md index e92b017..7a21844 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -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 diff --git a/README.md b/README.md index d166e48..1e56494 100644 --- a/README.md +++ b/README.md @@ -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\\` + - **Linux**: `~/.config/Paarrot/plugins//` + - **macOS**: `~/Library/Application Support/Paarrot/plugins//` +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/) diff --git a/cinny b/cinny index 9a463fa..64e252d 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 9a463facce94c66b371dedc54025bcac966ec883 +Subproject commit 64e252da94b4839dcae6c0a2002cde32d7ac5713 diff --git a/API-QUICKSTART.md b/docs/API-QUICKSTART.md similarity index 100% rename from API-QUICKSTART.md rename to docs/API-QUICKSTART.md diff --git a/API.md b/docs/API.md similarity index 100% rename from API.md rename to docs/API.md diff --git a/PLUGIN_API.md b/docs/PLUGIN_API.md similarity index 90% rename from PLUGIN_API.md rename to docs/PLUGIN_API.md index 5cc66d1..b1d89c4 100644 --- a/PLUGIN_API.md +++ b/docs/PLUGIN_API.md @@ -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; +} + +interface UIButtonPosition { + before?: string; + after?: string; + group?: string; + order?: number; +} +``` + +--- + ### Settings Define settings that users can configure in the UI. diff --git a/docs/PLUGIN_BUTTON_API.md b/docs/PLUGIN_BUTTON_API.md new file mode 100644 index 0000000..8f53914 --- /dev/null +++ b/docs/PLUGIN_BUTTON_API.md @@ -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!') +}; +``` diff --git a/PLUGIN_SYSTEM_IMPLEMENTATION.md b/docs/PLUGIN_SYSTEM_IMPLEMENTATION.md similarity index 88% rename from PLUGIN_SYSTEM_IMPLEMENTATION.md rename to docs/PLUGIN_SYSTEM_IMPLEMENTATION.md index 7716ef3..2860941 100644 --- a/PLUGIN_SYSTEM_IMPLEMENTATION.md +++ b/docs/PLUGIN_SYSTEM_IMPLEMENTATION.md @@ -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; };