waffle/doc/plugins.md
2022-09-28 17:03:56 +03:00

6.4 KiB

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):

(() => {
    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:

(() => {
    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:

// 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.

(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;
    }
}));