Make the support drawer core's notification drawer (sow-nodebb-plugin-support#10)
The first version of this drawer built its own rows in JS. This one renders core's `partials/notifications_list` partial with entries the support plugin now returns in core's notification shape, so the markup is literally the notification drawer's: avatar block, stretched link, muted timeago, per-entry read dot, All/Unread filter row, and the same two footer buttons. The handlers mirror core's too — only the API paths differ, because the list is plugin-owned (core's unread count is one aggregate with no hook to carve a type out of it). Opening the box no longer marks everything read: like the bell, an entry is read when you click it or use "Mark all read", and the dot toggles both ways. Deletes the bespoke row builder and its hand-rolled relative-time helper; `$.timeago` is what core uses for JS-inserted rows. The dropdown reuses the `.notifications-dropdown` class this theme already styles, so it needs no CSS of its own. Hardcoded English is gone (the code-review finding on the first version): every string is a translation key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+91
-58
@@ -208,56 +208,77 @@
|
||||
});
|
||||
}
|
||||
|
||||
function clearSupportBadge() {
|
||||
/*
|
||||
* 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 = '0';
|
||||
badge.classList.add('hidden');
|
||||
badge.textContent = count > 99 ? '99+' : String(count);
|
||||
badge.classList.toggle('hidden', count <= 0);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSupportMenu(menuEl, entries) {
|
||||
if (!menuEl) {
|
||||
return;
|
||||
}
|
||||
function adjustSupportBadge(delta) {
|
||||
const badge = document.querySelector('[component="support/count"]');
|
||||
const current = parseInt(badge && badge.textContent, 10) || 0;
|
||||
setSupportBadge(Math.max(0, current + delta));
|
||||
}
|
||||
|
||||
const relativePath = (window.config && window.config.relative_path) || '';
|
||||
|
||||
// remove the loading row and any previously injected entries; the
|
||||
// empty-state and footer/divider rows are server-rendered and stay put
|
||||
const loading = menuEl.querySelector('[data-wg-support-loading]');
|
||||
if (loading) {
|
||||
loading.remove();
|
||||
}
|
||||
toArray(menuEl.querySelectorAll('.wg-support-item')).forEach((li) => {
|
||||
li.remove();
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const emptyEl = menuEl.querySelector('[data-wg-support-empty]');
|
||||
const footerEl = menuEl.querySelector('[data-wg-support-footer]');
|
||||
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');
|
||||
|
||||
const list = (entries || []).slice(0, 10);
|
||||
if (emptyEl) {
|
||||
emptyEl.classList.toggle('hidden', list.length > 0);
|
||||
}
|
||||
|
||||
list.forEach((entry) => {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'dropdown-item wg-support-item text-truncate';
|
||||
link.href = relativePath + entry.path;
|
||||
link.textContent = entry.bodyShort;
|
||||
if (!entry.read) {
|
||||
link.classList.add('wg-support-item--unread');
|
||||
}
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'wg-support-item';
|
||||
li.appendChild(link);
|
||||
|
||||
if (footerEl) {
|
||||
menuEl.insertBefore(li, footerEl);
|
||||
} else {
|
||||
menuEl.appendChild(li);
|
||||
}
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -347,27 +368,39 @@
|
||||
});
|
||||
});
|
||||
|
||||
// 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"]', function () {
|
||||
const menuEl = this.querySelector('[data-wg-support-menu]');
|
||||
.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.get('/plugins/support/notifications', {}, function (err, data) {
|
||||
if (err) {
|
||||
renderSupportMenu(menuEl, []);
|
||||
return;
|
||||
}
|
||||
renderSupportMenu(menuEl, data && data.notifications);
|
||||
// ponytail: unlike core's unread/notification badges, this one
|
||||
// gets no live socket push, so it only catches up on the next
|
||||
// full page load after the box is opened here. Add a socket
|
||||
// event if that gap matters.
|
||||
if (data && data.unreadCount) {
|
||||
api.post('/plugins/support/notifications/read', {}, function () {});
|
||||
clearSupportBadge();
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user