Take the support drawer back out of the topbar (sow-nodebb-plugin-support#10)

Support case notifications now live in the notification bell, as a "Cases"
filter button added by nodebb-plugin-support on core's own hooks. The plugin
owns all of it, so this theme's second bell — its icon, drawer, client code,
language keys and tests — comes out.

The theme was the wrong home for it. The drawer's markup and JS lived here, so
whether the support plugin could notify anyone depended on which theme a forum
ran. Nothing about support belongs in a theme.

The `/support` link is unaffected; it comes from the ACP navigation, which
this theme already renders in both the desktop bar and the mobile drawer.

Checked in a browser on the dev stack: no support icon in the topbar, and the
bell's new tab filters a mixed notification list correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 23:28:00 +02:00
co-authored by Claude Opus 5
parent 9d4adca127
commit cc799475f2
6 changed files with 8 additions and 254 deletions
-110
View File
@@ -208,80 +208,6 @@
});
}
/*
* Support inbox drawer. The support plugin owns its own notification list
* (core's unread count is one aggregate with no hook to carve a type out
* of it), but everything the user sees is core's: the rows are rendered
* with core's own `partials/notifications_list` partial and driven by the
* same handlers as the bell — filter buttons, per-entry read dot, mark all
* read. Only the API paths differ.
*/
function setSupportBadge(count) {
toArray(document.querySelectorAll('[component="support/count"]')).forEach((badge) => {
badge.textContent = count > 99 ? '99+' : String(count);
badge.classList.toggle('hidden', count <= 0);
});
}
function adjustSupportBadge(delta) {
const badge = document.querySelector('[component="support/count"]');
const current = parseInt(badge && badge.textContent, 10) || 0;
setSupportBadge(Math.max(0, current + delta));
}
// Same shape as core's markNotification: PUT marks read, DELETE marks unread.
function markSupportEntry(entryEl, read) {
const nid = encodeURIComponent(entryEl.attr('data-nid'));
const path = '/plugins/support/notifications/' + nid + '/read';
require(['api'], function (api) {
(read ? api.put(path) : api.del(path)).then(function () {
entryEl.toggleClass('unread', !read);
entryEl.find('.mark-read .unread').toggleClass('hidden', read);
entryEl.find('.mark-read .read').toggleClass('hidden', !read);
adjustSupportBadge(read ? -1 : 1);
});
});
}
function renderSupportMenu(triggerEl) {
const dropdownEl = triggerEl.parent().find('.dropdown-menu');
const listEl = dropdownEl.find('[component="support/list"]');
dropdownEl.find('[data-filter="all"]').addClass('active');
dropdownEl.find('[data-filter="unread"]').removeClass('active');
require(['api', 'translator'], function (api, translator) {
api.get('/plugins/support/notifications', {}).then(function (data) {
const notifications = (data && data.notifications) || [];
const relativePath = (window.config && window.config.relative_path) || '';
notifications.forEach((notification) => {
notification.path = relativePath + notification.path;
notification.timeagoLong = $.timeago(new Date(notification.datetime));
});
app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) {
listEl.html(html);
setSupportBadge((data && data.unreadCount) || 0);
// core's partial says "no notifications"; this box only ever holds
// support activity, so say that instead.
translator.translate('[[westgate:support-inbox.empty]]', function (text) {
listEl.find('.no-notifs .fw-semibold').text(text);
});
listEl.off('click').on('click', '[component="notifications/item/link"]', function () {
const entryEl = $(this).parents('[data-nid]');
if (entryEl.hasClass('unread')) {
markSupportEntry(entryEl, true);
}
triggerEl.dropdown('toggle');
});
listEl.on('click', '.mark-read', function () {
const entryEl = $(this).parents('[data-nid]');
markSupportEntry(entryEl, entryEl.hasClass('unread'));
});
});
});
});
}
function getWestgateTopbar() {
if (!document || typeof document.querySelector !== 'function') {
return null;
@@ -367,48 +293,12 @@
renderUnreadMenu(menuEl, []);
});
});
// ponytail: unlike core's bell, this badge gets no socket push, so it
// only catches up when the drawer is opened or the page reloads. Add a
// socket event in the support plugin if that gap ever matters.
$(document)
.off('show.bs.dropdown.westgateSupport')
.on('show.bs.dropdown.westgateSupport', '.wg-topbar [component="support"] [data-bs-toggle="dropdown"]', function () {
renderSupportMenu($(this));
});
$(document)
.off('click.westgateSupport')
.on('click.westgateSupport', '.wg-topbar [component="support"] .mark-all-read', function (event) {
event.preventDefault();
const entries = $(this).closest('.dropdown-menu').find('[data-nid]');
require(['api'], function (api) {
api.post('/plugins/support/notifications/read', {}).then(function () {
setSupportBadge(0);
entries.removeClass('unread');
entries.find('.mark-read .unread').addClass('hidden');
entries.find('.mark-read .read').removeClass('hidden');
});
});
})
// Core's own filter behaviour: hide read rows client-side, nothing refetched.
.on('click.westgateSupport', '.wg-topbar [component="support"] [data-filter]', function () {
const dropdownEl = $(this).closest('.dropdown-menu');
dropdownEl.find('[data-filter]').removeClass('active');
this.classList.add('active');
const unreadOnly = this.getAttribute('data-filter') === 'unread';
dropdownEl.find('[data-nid]').each(function () {
this.classList.toggle('hidden', unreadOnly && !this.classList.contains('unread'));
});
dropdownEl.find('.no-notifs').toggleClass('hidden', dropdownEl.find('[data-nid]:not(.hidden)').length !== 0);
});
}
window.westgateTheme = window.westgateTheme || {};
window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables;
window.westgateTheme.initTopbar = initWestgateTopbar;
window.westgateTheme.renderUnreadMenu = renderUnreadMenu;
window.westgateTheme.renderSupportMenu = renderSupportMenu;
$(document).ready(function () {
wrapWestgateWikiTables(document);