From 548e2fdc114132fc01845c157a3c1166dd593818 Mon Sep 17 00:00:00 2001 From: Fathi Ben Nasr Date: Fri, 14 Aug 2026 11:04:25 +0000 Subject: [PATCH] Let plugins register a navigation entry A plugin that adds a screen has no way to offer a way into it. The folder list toolbar holds Compose and Contacts, and nothing else can be added to it, so plugins reach into the UI themselves: polling for an existing button, replacing its icon and hanging a menu off it. That is fragile, it varies per plugin, and it puts screens somewhere users do not look for them. rl.addNavEntry({icon, title, hash}) appends an entry to that toolbar, rendered with the same .btn classes so it follows the active theme. click may be given instead of hash for a popup rather than a route. Three lines of template, one observableArray, one function, and no change to existing behaviour when no plugin registers anything. --- dev/Common/Plugins.js | 30 +++++++++++++++++++ dev/View/User/MailBox/FolderList.js | 2 ++ .../templates/Views/User/MailFolderList.html | 3 ++ 3 files changed, 35 insertions(+) diff --git a/dev/Common/Plugins.js b/dev/Common/Plugins.js index 47c658d2c..577c4bff0 100644 --- a/dev/Common/Plugins.js +++ b/dev/Common/Plugins.js @@ -1,3 +1,4 @@ +import ko from 'ko'; import { settingsAddViewModel } from 'Screen/AbstractSettings'; import { SettingsGet } from 'Common/Globals'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; @@ -15,6 +16,35 @@ rl.pluginRemoteRequest = (callback, action, parameters, timeout) => { rl.app.Remote.request('Plugin' + action, callback, parameters, timeout); }; +/** + * Navigation entries contributed by plugins, rendered in the folder list + * toolbar beside Compose and Contacts. + * @type {Array} + */ +export const pluginNavEntries = ko.observableArray(); + +/** + * Register a navigation entry for a screen a plugin provides. + * + * Without this a plugin that adds a screen has to reach into the folder list + * template itself, which several already do, each in its own way. + * + * @param {Object} entry + * @param {string} entry.icon character or glyph shown on the button + * @param {string} entry.title tooltip + * @param {string=} entry.hash location hash to navigate to + * @param {Function=} entry.click called instead of setting the hash + */ +rl.addNavEntry = entry => { + if (entry && entry.icon) { + pluginNavEntries.push({ + icon: entry.icon, + title: entry.title || '', + click: entry.click || (() => { hasher.setHash(entry.hash || ''); }) + }); + } +}; + /** * @param {Function} SettingsViewModelClass * @param {string} labelName diff --git a/dev/View/User/MailBox/FolderList.js b/dev/View/User/MailBox/FolderList.js index d8f60fe78..e5aa33f8e 100644 --- a/dev/View/User/MailBox/FolderList.js +++ b/dev/View/User/MailBox/FolderList.js @@ -7,6 +7,7 @@ import { mailBox, settings } from 'Common/Links'; import { addComputablesTo } from 'External/ko'; import { AppUserStore } from 'Stores/User/App'; +import { pluginNavEntries } from 'Common/Plugins'; import { SettingsUserStore } from 'Stores/User/Settings'; import { FolderUserStore } from 'Stores/User/Folder'; import { MessageUserStore } from 'Stores/User/Message'; @@ -36,6 +37,7 @@ export class MailFolderList extends AbstractViewLeft { this.moveAction = moveAction; this.allowContacts = AppUserStore.allowContacts(); + this.pluginNavEntries = pluginNavEntries; this.foldersFilter = foldersFilter; diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html index 3103049a1..7bc47edda 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html @@ -5,6 +5,9 @@ 📇 + + +