127 lines
No EOL
6.4 KiB
Markdown
127 lines
No EOL
6.4 KiB
Markdown
# Frontend Plugins
|
|
|
|
If you want to change the functionality of Waffle's frontend, you can use plugins.
|
|
|
|
## Overview
|
|
|
|
Plugins are pieces of Javascript that run on the web frontend. They get access to most of the functionality of the app.
|
|
|
|
Waffle also supports loading CSS using the `app:stylesheets` localstorage item (array of strings which contain css code).
|
|
|
|
Here's some general info about plugins:
|
|
- There is no dedicated plugin API yet. Plugins simply get access to internal objects. This is because it is very difficult to create a good plugin API which covers most usecases. This also means plugins are liable to break with updates.
|
|
- The less "intrusive" the change, the less likely it is to break with an update: if your plugin uses `Proxy` on a bunch of internal functions and relies on weird behavior, it is very likely to break with an update.
|
|
- Never use `window.__waffle` in your plugin. It is designed for easy access into internal objects from the browser devtools. Plugins get the same access through the objects passed in `plugin.main`. The difference is that the plugin context can have certain differences in implementation and structure to ensure compatibility and safety.
|
|
|
|
## Loading
|
|
|
|
The client supports 3 ways of loading plugins:
|
|
1. Automatic loading: the client iterates over all of the strings in the `app:javascript` localstorage array and executes them as Javascript.
|
|
2. Manual loading: done using a script that runs before the frontend's javascript, such as a userscript, a script injected in the HTML, or a WebExtension.
|
|
3. Pasting in the console: pasting the code of a plugin in the console. Useful for development, or when you don't have access to localstorage or userscripts.
|
|
|
|
All plugins that are correctly implemented will be compatible with both methods. localstorage has limited space on most browsers, so you may not be able to store big plugins, or a bunch of them.
|
|
|
|
For automatic loading, you generally want to minify the script before appending it to the `app:javascript` localstorage array.
|
|
|
|
Adding a script to the `app:javascript` localstorage array is done as follows (you can paste this code in the browser devtools console):
|
|
```javascript
|
|
(() => {
|
|
const code = `YOUR_CODE_GOES_HERE`;
|
|
window.__waffle.storage.setItem("app:javascript", [...window.__waffle.storage.getItem("app:javascript"), code]);
|
|
})();
|
|
```
|
|
Most browsers also support editing localstorage directly from the devtools.
|
|
|
|
Likewise, adding a style to `app:stylesheets` is done as follows:
|
|
```javascript
|
|
(() => {
|
|
const code = `YOUR_STYLE_GOES_HERE`;
|
|
window.__waffle.storage.setItem("app:stylesheets", [...window.__waffle.storage.getItem("app:stylesheets"), code]);
|
|
})();
|
|
```
|
|
|
|
## Examples
|
|
|
|
### Print the selected channel
|
|
|
|
Plugin that prints the name of the currently selected channel each time it changes:
|
|
```javascript
|
|
// We wrap our plugin in a function that runs immediately so that we don't pollute the scope.
|
|
(() => {
|
|
let unsubscribers = [];
|
|
|
|
// Called when the app is loaded.
|
|
// This does not guarantee the gateway connection being alive, so you should probably use the `gatewayStatus` store to determine when you can use it.
|
|
const pluginMain = (context) => {
|
|
// the context variable contains all objects and methods required to interact with the app.
|
|
// here, you could add `console.log(context)` to inspect the context object and see what you can do.
|
|
const { stores } = context;
|
|
|
|
// Here, we subscribe to the selectedChannel store.
|
|
// Stores are mutable containers that have a subscribe() method.
|
|
// When the value of the store changes, it notifies all of the subscribers.
|
|
// The subscribe() method returns a function which you must call when you're done with using the store.
|
|
// Here, we're putting that value in an array which we will iterate over in the pluginDispose() function.
|
|
unsubscribers.push(stores.selectedChannel.subscribe(channel => {
|
|
if (channel && channel.name) {
|
|
console.log(channel.name);
|
|
}
|
|
}));
|
|
};
|
|
|
|
// Called when the plugin should unload and reverse all of its effects.
|
|
const pluginDispose = (context) => {
|
|
// Call all cleanup functions/unsubscribers.
|
|
// See above for explanation on why this needs to be done.
|
|
unsubscribers.forEach(e => e());
|
|
unsubscribers.length = 0;
|
|
};
|
|
|
|
// Plugins are objects that follow this format.
|
|
const plugin = {
|
|
name: "selectedChannelPrint",
|
|
description: "Prints the selected channel to the console each time it changes",
|
|
main: pluginMain,
|
|
dispose: pluginDispose
|
|
};
|
|
|
|
// __wafflePluginLoaders is an array present on the window object or in the current scope of execution.
|
|
// It is an array of functions which, when called, return a plugin.
|
|
// While __wafflePluginLoaders is usually an array, you should not treat it as such.
|
|
// Certain methods of plugin loading may actually set __wafflePluginLoaders to an empty object with a push() function.
|
|
// Thus, the only function you should assume to exist on __wafflePluginLoaders should be push().
|
|
const pluginLoaders = window.__wafflePluginLoaders || __wafflePluginLoaders || [];
|
|
pluginLoaders.push(() => plugin); // window.__wafflePluginLoaders is an array of functions which return a plugin
|
|
})();
|
|
```
|
|
|
|
### Message signature
|
|
|
|
Plugin that adds a signature after every message. This demonstrates the store.pipe() method and leaves some comments out for the sake of showcasing more ways to structure your plugin.
|
|
Note: please don't use this in actual chats.
|
|
|
|
```javascript
|
|
(window.__wafflePluginLoaders || __wafflePluginLoaders || []).push(() => ({
|
|
name: "messageSignature",
|
|
description: "Adds a signature to every message before sending",
|
|
state: {
|
|
cleanup: []
|
|
},
|
|
options: {
|
|
signature: "\n\n-Waffle user"
|
|
},
|
|
main(waffle) {
|
|
// store.pipe() allows us to add a function to "intercept" the store value before it reaches all of its subscribers.
|
|
// As with store.subscribe(), store.pipe() returns a function that removes the pipe once we're done with it.
|
|
this.state.cleanup.push(waffle.stores.sendMessageAction.pipe(message => {
|
|
message.content = `${message.content}${this.options.signature}`;
|
|
return message;
|
|
}));
|
|
},
|
|
dispose(_waffle) {
|
|
this.state.cleanup.forEach(f => f());
|
|
this.state.cleanup.length = 0;
|
|
}
|
|
}));
|
|
``` |