Merge commit '1b4e0c87260b4d2c5eb9b0b0f954480182ff2a96' into v4.x

This commit is contained in:
Misty Release Bot
2025-02-09 01:01:53 +00:00
51 changed files with 340 additions and 275 deletions

View File

@@ -1,3 +1,34 @@
#### v4.0.2 (2025-02-02)
##### Chores
* up persona (0298a3af)
* up harmony (d77d2055)
* up themes, closes #13102 (6672de00)
* incrementing version number - v4.0.1 (a461b758)
* update changelog for v4.0.1 (3dbd2b30)
* incrementing version number - v4.0.0 (c1eaee45)
##### New Features
* allow selecting empty for custom selects (be62ae24)
* add uid to post.parent (4d733590)
* add description and keywords to api/config (933c18f4)
##### Bug Fixes
* bad logic that invisibly broke outgoing user follows completely (51e660d5)
* closes #13096, fix regression from renaming language files (0b92d525)
##### Refactors
* remove old comment (d4a1b4da)
##### Tests
* fix schema (ef5ae006)
* fix schema (47734d4c)
#### v4.0.1 (2025-01-29)
##### Chores

View File

@@ -108,10 +108,10 @@
"nodebb-plugin-spam-be-gone": "2.3.0",
"nodebb-plugin-web-push": "0.7.2",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "2.0.7",
"nodebb-theme-harmony": "2.0.18",
"nodebb-theme-lavender": "7.1.17",
"nodebb-theme-peace": "2.2.36",
"nodebb-theme-persona": "14.0.8",
"nodebb-theme-peace": "2.2.38",
"nodebb-theme-persona": "14.0.14",
"nodebb-widget-essentials": "7.0.32",
"nodemailer": "6.9.16",
"nprogress": "0.2.0",
@@ -200,4 +200,4 @@
"url": "https://github.com/barisusakli"
}
]
}
}

View File

@@ -13,6 +13,8 @@
"settings.mobileTopicTeasers": "Show topic teasers on mobile",
"settings.stickyToolbar": "Sticky toolbar",
"settings.stickyToolbar.help": "The toolbar on topic and category pages will stick to the top of the page",
"settings.topicSidebarTools": "Topic sidebar tools",
"settings.topicSidebarTools.help": "This option will move the topic tools to the sidebar on desktop",
"settings.autohideBottombar": "Auto hide bottom bar",
"settings.autohideBottombar.help": "The bottom bar on mobile view will be hidden when the page is scrolled down",
"settings.openSidebars": "Open sidebars",

View File

@@ -27,6 +27,8 @@ get:
type: string
keywords:
type: string
brand:logo:
type: string
titleLayout:
type: string
showSiteTitle:

View File

@@ -27,6 +27,8 @@ get:
type: string
keywords:
type: string
brand:logo:
type: string
titleLayout:
type: string
showSiteTitle:

View File

@@ -50,19 +50,6 @@ get:
type: array
items:
$ref: ../../components/schemas/PostObject.yaml#/PostDataObject
events:
type: array
items:
type: object
properties:
type:
type: string
id:
type: number
timestamp:
type: number
timestampISO:
type: string
category:
$ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
tagWhitelist:

View File

@@ -154,11 +154,9 @@ body {
}
.dropdown-left {
[component="category-selector-search"] { left:0!important; }
.dropdown-menu { --bs-position: start; }
}
.dropdown-right {
[component="category-selector-search"] { right:0!important; }
.dropdown-menu { --bs-position: end; }
}

View File

@@ -50,6 +50,12 @@ body.page-user-chats {
}
}
[component="chat/message/parent"] {
[component="chat/message/parent/content"] > p:last-child {
margin-bottom: 0;
}
}
.expanded-chat {
.chat-content {
.message-body {

View File

@@ -24,11 +24,9 @@
}
}
.dropdown-left {
[component="category-selector-search"] { left:0!important; }
.dropdown-menu { --bs-position: start; }
}
.dropdown-right {
[component="category-selector-search"] { right:0!important; }
.dropdown-menu { --bs-position: end; }
}

View File

@@ -136,11 +136,13 @@ define('admin/extend/plugins', [
require(['compare-versions'], function (compareVersions) {
const currentVersion = parent.find('.currentVersion').text();
if (payload.version !== 'latest' && compareVersions.compare(payload.version, currentVersion, '>')) {
if (payload.version && payload.version !== 'latest' && compareVersions.compare(payload.version, currentVersion, '>')) {
upgrade(pluginID, btn, payload.version);
} else if (payload.version === 'latest') {
confirmInstall(pluginID, function () {
upgrade(pluginID, btn, payload.version);
} else if (payload.version === 'latest' || payload.version === null) {
confirmInstall(pluginID, function (confirm) {
if (confirm) {
upgrade(pluginID, btn, payload.version);
}
});
} else {
bootbox.alert(translator.compile('admin/extend/plugins:alert.incompatible', app.config.version, payload.version));

View File

@@ -24,7 +24,7 @@ define('forum/topic', [
bootbox, clipboard
) {
const Topic = {};
let tid = 0;
let tid = '0';
let currentUrl = '';
$(window).on('action:ajaxify.start', function (ev, data) {
@@ -38,8 +38,8 @@ define('forum/topic', [
});
Topic.init = async function () {
const tidChanged = !tid || parseInt(tid, 10) !== parseInt(ajaxify.data.tid, 10);
tid = ajaxify.data.tid;
const tidChanged = tid === '0' || String(tid) !== String(ajaxify.data.tid);
tid = String(ajaxify.data.tid);
currentUrl = ajaxify.currentPage;
hooks.fire('action:topic.loading');
@@ -264,15 +264,32 @@ define('forum/topic', [
}
function addParentHandler() {
components.get('topic').on('click', '[component="post/parent"]', function (e) {
const toPid = $(this).attr('data-topid');
function gotoPost(event, toPid) {
const toPost = $('[component="topic"]>[component="post"][data-pid="' + toPid + '"]');
if (toPost.length) {
e.preventDefault();
event.preventDefault();
navigator.scrollToIndex(toPost.attr('data-index'), true);
return false;
}
}
components.get('topic').on('click', '[component="post/parent"]', function (e) {
const parentEl = $(this);
const contentEl = parentEl.find('[component="post/parent/content"]');
if (contentEl.length) {
const isCollapsed = contentEl.hasClass('line-clamp-1');
contentEl.toggleClass('line-clamp-1');
parentEl.find('.timeago').toggleClass('hidden');
parentEl.toggleClass('flex-column').toggleClass('flex-row');
if (isCollapsed) {
return false;
}
} else {
return gotoPost(e, parentEl.attr('data-topid'));
}
});
components.get('topic').on('click', '[component="post/parent"] .timeago', function (e) {
return gotoPost(e, $(this).parents('[data-parent-pid]').attr('data-parent-pid'));
});
}
@@ -298,7 +315,7 @@ define('forum/topic', [
destroyed = true;
}
$(window).one('action:ajaxify.start', destroyTooltip);
$('[component="topic"]').on('mouseenter', '[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () {
$('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () {
const link = $(this);
destroyed = false;

View File

@@ -80,7 +80,7 @@ define('forum/topic/events', [
function updateBookmarkCount(data) {
$('[data-pid="' + data.post.pid + '"] .bookmarkCount').filter(function (index, el) {
return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10);
return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid);
}).html(data.post.bookmarks).attr('data-bookmarks', data.post.bookmarks);
}
@@ -88,14 +88,14 @@ define('forum/topic/events', [
if (
ajaxify.data.category &&
ajaxify.data.category.slug &&
parseInt(data.tid, 10) === parseInt(ajaxify.data.tid, 10)
String(data.tid) === String(ajaxify.data.tid)
) {
ajaxify.go('category/' + ajaxify.data.category.slug, null, true);
}
}
function onTopicMoved(data) {
if (data && data.slug && parseInt(data.tid, 10) === parseInt(ajaxify.data.tid, 10)) {
if (data && data.slug && String(data.tid) === String(ajaxify.data.tid)) {
ajaxify.go('topic/' + data.slug, null, true);
}
}
@@ -159,6 +159,17 @@ define('forum/topic/events', [
});
}
});
const parentEl = $(`[component="post/parent"][data-parent-pid="${data.post.pid}"]`);
if (parentEl.length) {
parentEl.find('[component="post/parent/content"]').html(
translator.unescape(data.post.content)
);
parentEl.find('img:not(.not-responsive)').addClass('img-fluid');
parentEl.find('[component="post/parent/content]" img:not(.emoji)').each(function () {
images.wrapImageInLink($(this));
});
}
} else {
hooks.fire('action:posts.edited', data);
}
@@ -173,7 +184,7 @@ define('forum/topic/events', [
}
function onPostPurged(postData) {
if (!postData || parseInt(postData.tid, 10) !== parseInt(ajaxify.data.tid, 10)) {
if (!postData || String(postData.tid) !== String(ajaxify.data.tid)) {
return;
}
components.get('post', 'pid', postData.pid).fadeOut(500, function () {
@@ -185,32 +196,45 @@ define('forum/topic/events', [
require(['forum/topic/replies'], function (replies) {
replies.onPostPurged(postData);
});
$(`[component="post/parent"][data-parent-pid="${postData.pid}"]`).remove();
}
function togglePostDeleteState(data) {
const postEl = components.get('post', 'pid', data.pid);
if (!postEl.length) {
return;
const { isAdminOrMod } = ajaxify.data.privileges;
const isSelfPost = String(data.uid) === String(app.user.uid);
const isDeleted = !!data.deleted;
if (postEl.length) {
postEl.toggleClass('deleted');
postTools.toggle(data.pid, isDeleted);
if (!isAdminOrMod && !isSelfPost) {
postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted);
if (isDeleted) {
postEl.find('[component="post/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
postEl.find('[component="post/content"]').html(translator.unescape(data.content));
}
}
}
postEl.toggleClass('deleted');
const isDeleted = postEl.hasClass('deleted');
postTools.toggle(data.pid, isDeleted);
if (!ajaxify.data.privileges.isAdminOrMod && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) {
postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted);
if (isDeleted) {
postEl.find('[component="post/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
postEl.find('[component="post/content"]').html(translator.unescape(data.content));
}
const parentEl = $(`[component="post/parent"][data-parent-pid="${data.pid}"]`);
if (parentEl.length) {
parentEl.each((i, el) => {
const $parent = $(el);
if (isDeleted) {
$parent.find('[component="post/parent/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
$parent.find('[component="post/parent/content"]').html(translator.unescape(data.content));
}
});
}
}
function togglePostBookmark(data) {
const el = $('[data-pid="' + data.post.pid + '"] [component="post/bookmark"]').filter(function (index, el) {
return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10);
return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid);
});
if (!el.length) {
return;
@@ -234,7 +258,7 @@ define('forum/topic/events', [
function onNewNotification(data) {
const tid = ajaxify.data.tid;
if (data && data.tid && parseInt(data.tid, 10) === parseInt(tid, 10)) {
if (data && data.tid && String(data.tid) === String(tid)) {
socket.emit('topics.markTopicNotificationsRead', [tid]);
}
}

View File

@@ -74,7 +74,7 @@ define('forum/topic/move-post', [
const tidInput = moveModal.find('#topicId');
let targetTid = null;
if (ajaxify.data.template.topic && ajaxify.data.tid &&
parseInt(ajaxify.data.tid, 10) !== fromTid
String(ajaxify.data.tid) !== String(fromTid)
) {
targetTid = ajaxify.data.tid;
}
@@ -98,8 +98,8 @@ define('forum/topic/move-post', [
}
const targetTid = getTargetTid();
if (postSelect.pids.length) {
if (targetTid && parseInt(targetTid, 10) !== parseInt(fromTid, 10)) {
api.get('/topics/' + targetTid, {}).then(function (data) {
if (targetTid && String(targetTid) !== String(fromTid)) {
api.get(`/topics/${targetTid}`, {}).then(function (data) {
if (!data || !data.tid) {
return alerts.error('[[error:no-topic]]');
}
@@ -123,7 +123,7 @@ define('forum/topic/move-post', [
}
const targetTid = getTargetTid();
if (postSelect.pids.length && targetTid &&
parseInt(targetTid, 10) !== parseInt(fromTid, 10)
String(targetTid) !== String(fromTid)
) {
moveCommit.removeAttr('disabled');
} else {
@@ -150,7 +150,7 @@ define('forum/topic/move-post', [
});
});
if (data.pids.length && ajaxify.data.template.topic &&
parseInt(data.tid, 10) === parseInt(ajaxify.data.tid, 10)) {
String(data.tid) === String(ajaxify.data.tid)) {
ajaxify.go(`/post/${data.pids[0]}`);
}
closeMoveModal();

View File

@@ -22,7 +22,7 @@ define('forum/topic/posts', [
!data ||
!data.posts ||
!data.posts.length ||
parseInt(data.posts[0].tid, 10) !== parseInt(ajaxify.data.tid, 10)
String(data.posts[0].tid) !== String(ajaxify.data.tid)
) {
return;
}

View File

@@ -36,6 +36,7 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f
'downvote:disabled': ajaxify.data['downvote:disabled'],
'reputation:disabled': ajaxify.data['reputation:disabled'],
loggedIn: !!app.user.uid,
hideParent: true,
hideReplies: config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true,
};
app.parseAndTranslate('topic', 'posts', tplData, async function (html) {

View File

@@ -294,7 +294,7 @@ define('forum/topic/threadTools', [
ThreadTools.setLockedState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
if (String(data.tid) !== threadEl.attr('data-tid')) {
return;
}
@@ -322,7 +322,7 @@ define('forum/topic/threadTools', [
ThreadTools.setDeleteState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
if (String(data.tid) !== threadEl.attr('data-tid')) {
return;
}
@@ -356,7 +356,7 @@ define('forum/topic/threadTools', [
ThreadTools.setPinnedState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
if (String(data.tid) !== threadEl.attr('data-tid')) {
return;
}

View File

@@ -21,16 +21,12 @@ define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, boots
return;
}
const toggleVisibility = searchEl.parent('[component="category/dropdown"]').length > 0 ||
searchEl.parent('[component="category-selector"]').length > 0;
const toggleVisibility = searchEl.parents('[component="category/dropdown"]').length > 0 ||
searchEl.parents('[component="category-selector"]').length > 0;
el.on('show.bs.dropdown', function () {
if (toggleVisibility) {
el.find('.dropdown-toggle').css({ visibility: 'hidden' });
searchEl.removeClass('hidden');
searchEl.css({
'z-index': el.find('.dropdown-toggle').css('z-index') + 1,
});
}
function doSearch() {
@@ -61,7 +57,6 @@ define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, boots
el.on('hide.bs.dropdown', function () {
if (toggleVisibility) {
el.find('.dropdown-toggle').css({ visibility: 'inherit' });
searchEl.addClass('hidden');
}

View File

@@ -11,7 +11,7 @@ define('groupSearch', function () {
if (!searchEl.length) {
return;
}
const toggleVisibility = searchEl.parent('[component="group-selector"]').length > 0;
const toggleVisibility = searchEl.parents('[component="group-selector"]').length > 0;
const groupEls = el.find('[component="group-list"] [data-name]');
el.on('show.bs.dropdown', function () {
@@ -31,11 +31,7 @@ define('groupSearch', function () {
el.find('[component="group-list"] [component="group-no-matches"]').toggleClass('hidden', !noMatch);
}
if (toggleVisibility) {
el.find('.dropdown-toggle').css({ visibility: 'hidden' });
searchEl.removeClass('hidden');
searchEl.css({
'z-index': el.find('.dropdown-toggle').css('z-index') + 1,
});
}
searchEl.on('click', function (ev) {
@@ -52,7 +48,6 @@ define('groupSearch', function () {
el.on('hide.bs.dropdown', function () {
if (toggleVisibility) {
el.find('.dropdown-toggle').css({ visibility: 'inherit' });
searchEl.addClass('hidden');
}
searchEl.off('click').find('input').off('keyup');

View File

@@ -27,16 +27,12 @@ define('tagFilter', ['hooks', 'alerts', 'bootstrap'], function (hooks, alerts, b
}
initialTags = selectedTags.slice();
const toggleSearchVisibilty = searchEl.parent('[component="tag/filter"]').length &&
const toggleSearchVisibilty = searchEl.parents('[component="tag/filter"]').length &&
app.user.privileges['search:tags'];
el.on('show.bs.dropdown', function () {
if (toggleSearchVisibilty) {
el.find('.dropdown-toggle').css({ visibility: 'hidden' });
searchEl.removeClass('hidden');
searchEl.css({
'z-index': el.find('.dropdown-toggle').css('z-index') + 1,
});
}
function doSearch() {
@@ -67,7 +63,6 @@ define('tagFilter', ['hooks', 'alerts', 'bootstrap'], function (hooks, alerts, b
el.on('hidden.bs.dropdown', function () {
if (toggleSearchVisibilty) {
el.find('.dropdown-toggle').css({ visibility: 'inherit' });
searchEl.addClass('hidden');
}

View File

@@ -187,12 +187,12 @@ Mocks.post = async (objects) => {
}
switch (true) {
case image && image.hasOwnProperty('url') && image.url && mime.getType(image.url).startsWith('image/'): {
case image && image.hasOwnProperty('url') && image.url: {
image = image.url;
break;
}
case image && typeof image === 'string' && mime.getType(image).startsWith('image/'): {
case image && typeof image === 'string': {
// no change
break;
}
@@ -201,6 +201,13 @@ Mocks.post = async (objects) => {
image = null;
}
}
if (image) {
const parsed = new URL(image);
if (!mime.getType(parsed.pathname).startsWith('image/')) {
activitypub.helpers.log(`[activitypub/mocks.post] Received image not identified as image due to MIME type: ${image}`);
image = null;
}
}
const payload = {
uid,

View File

@@ -60,6 +60,7 @@ topicsAPI.create = async function (caller, data) {
}
const payload = { ...data };
delete payload.tid;
payload.tags = payload.tags || [];
apiHelpers.setDefaultPostData(caller, payload);
const isScheduling = parseInt(data.timestamp, 10) > payload.timestamp;
@@ -98,6 +99,7 @@ topicsAPI.reply = async function (caller, data) {
throw new Error('[[error:invalid-data]]');
}
const payload = { ...data };
delete payload.pid;
apiHelpers.setDefaultPostData(caller, payload);
await meta.blacklist.test(caller.ip);

View File

@@ -17,7 +17,6 @@ require('./data')(Categories);
require('./create')(Categories);
require('./delete')(Categories);
require('./topics')(Categories);
require('./unread')(Categories);
require('./activeusers')(Categories);
require('./recentreplies')(Categories);
require('./update')(Categories);

View File

@@ -1,46 +0,0 @@
'use strict';
const db = require('../database');
module.exports = function (Categories) {
Categories.markAsRead = async function (cids, uid) {
// TODO: remove in 4.0
console.warn('[deprecated] Categories.markAsRead deprecated');
if (!Array.isArray(cids) || !cids.length || parseInt(uid, 10) <= 0) {
return;
}
let keys = cids.map(cid => `cid:${cid}:read_by_uid`);
const hasRead = await db.isMemberOfSets(keys, uid);
keys = keys.filter((key, index) => !hasRead[index]);
await db.setsAdd(keys, uid);
};
Categories.markAsUnreadForAll = async function (cid) {
// TODO: remove in 4.0
console.warn('[deprecated] Categories.markAsUnreadForAll deprecated');
if (!parseInt(cid, 10)) {
return;
}
await db.delete(`cid:${cid}:read_by_uid`);
};
Categories.hasReadCategories = async function (cids, uid) {
// TODO: remove in 4.0
console.warn('[deprecated] Categories.hasReadCategories deprecated, see Categories.setUnread');
if (parseInt(uid, 10) <= 0) {
return cids.map(() => false);
}
const sets = cids.map(cid => `cid:${cid}:read_by_uid`);
return await db.isMemberOfSets(sets, uid);
};
Categories.hasReadCategory = async function (cid, uid) {
// TODO: remove in 4.0
console.warn('[deprecated] Categories.hasReadCategory deprecated, see Categories.setUnread');
if (parseInt(uid, 10) <= 0) {
return false;
}
return await db.isSetMember(`cid:${cid}:read_by_uid`, uid);
};
};

View File

@@ -34,6 +34,7 @@ apiController.loadConfig = async function (req) {
browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')),
description: validator.escape(String(meta.config.description || '')),
keywords: validator.escape(String(meta.config.keywords || '')),
'brand:logo': validator.escape(String(meta.config['brand:logo'])),
titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '&#123;').replace(/}/g, '&#125;'),
showSiteTitle: meta.config.showSiteTitle === 1,
maintenanceMode: meta.config.maintenanceMode === 1,

View File

@@ -65,7 +65,7 @@ recentController.getData = async function (req, url, sort) {
data.title = meta.config.homePageTitle || '[[pages:home]]';
} else {
data.title = `[[pages:${url}]]`;
data.breadcrumbs = helpers.buildBreadcrumbs([{ text: `[[activitypub:world-title]]` }]);
data.breadcrumbs = helpers.buildBreadcrumbs([{ text: `[[${url}:title]]` }]);
}
const query = { ...req.query };

View File

@@ -137,7 +137,7 @@ module.exports = function (Messaging) {
parentMids = parentMids.filter((mid, idx) => canView[idx]);
const parentMessages = await Messaging.getMessagesFields(parentMids, [
'fromuid', 'content', 'timestamp', 'deleted',
'mid', 'fromuid', 'content', 'timestamp', 'deleted',
]);
const parentUids = _.uniq(parentMessages.map(msg => msg && msg.fromuid));
const usersMap = _.zipObject(

View File

@@ -22,6 +22,7 @@ const Notifications = module.exports;
// ttlcache for email-only chat notifications
const notificationCache = ttlCache({
max: 1000,
ttl: (meta.config.notificationSendDelay || 60) * 1000,
noDisposeOnSet: true,
dispose: sendEmail,
@@ -242,17 +243,25 @@ async function pushToUids(uids, notification) {
if (notification.type) {
results = await getUidsBySettings(data.uids);
}
await sendNotification(results.uidsToNotify);
const delayNotificationTypes = ['new-chat', 'new-group-chat', 'new-public-chat'];
if (delayNotificationTypes.includes(notification.type)) {
const cacheKey = `${notification.mergeId}|${results.uidsToEmail.join(',')}`;
if (notificationCache.has(cacheKey)) {
if (results.uidsToEmail.length) {
const delayNotificationTypes = ['new-chat', 'new-group-chat', 'new-public-chat'];
if (delayNotificationTypes.includes(notification.type)) {
const cacheKey = `${notification.mergeId}|${results.uidsToEmail.join(',')}`;
const payload = notificationCache.get(cacheKey);
notification.bodyLong = [payload.notification.bodyLong, notification.bodyLong].join('\n');
let { bodyLong } = notification;
if (payload !== undefined) {
bodyLong = [payload.notification.bodyLong, bodyLong].join('\n');
}
notificationCache.set(cacheKey, { uids: results.uidsToEmail, notification: { ...notification, bodyLong } });
if (notification.bodyLong.length >= 1000) {
notificationCache.delete(cacheKey);
}
} else {
await sendEmail({ uids: results.uidsToEmail, notification });
}
notificationCache.set(cacheKey, { uids: results.uidsToEmail, notification });
} else {
await sendEmail({ uids: results.uidsToEmail, notification });
}
plugins.hooks.fire('action:notification.pushed', {
@@ -264,8 +273,7 @@ async function pushToUids(uids, notification) {
}
async function sendEmail({ uids, notification }, mergeId, reason) {
// Only act on cache item expiry
if (reason && reason !== 'stale') {
if ((reason && reason === 'set') || !uids.length) {
return;
}

View File

@@ -28,7 +28,7 @@ module.exports = function (Posts) {
deleted: isDeleting ? 1 : 0,
deleterUid: isDeleting ? uid : 0,
});
const postData = await Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp']);
const postData = await Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp', 'deleted']);
const topicData = await topics.getTopicFields(postData.tid, ['tid', 'cid', 'pinned']);
postData.cid = topicData.cid;
await Promise.all([

View File

@@ -61,9 +61,9 @@ module.exports = function (Posts) {
const tid = String(filter.tid);
postData = postData.filter(item => item.data.tid && String(item.data.tid) === tid);
} else if (Array.isArray(filter.tid)) {
const tids = filter.tid.map(tid => parseInt(tid, 10));
const tids = filter.tid.map(String);
postData = postData.filter(
item => item.data.tid && tids.includes(parseInt(item.data.tid, 10))
item => item.data.tid && tids.includes(String(item.data.tid))
);
}

View File

@@ -200,9 +200,9 @@ module.exports = function (Posts) {
}
async function updateTopicPosters(postData, toUid) {
const postsByTopic = _.groupBy(postData, p => parseInt(p.tid, 10));
const postsByTopic = _.groupBy(postData, p => String(p.tid));
await async.eachOf(postsByTopic, async (posts, tid) => {
const postsByUser = _.groupBy(posts, p => parseInt(p.uid, 10));
const postsByUser = _.groupBy(posts, p => String(p.uid));
await db.sortedSetIncrBy(`tid:${tid}:posters`, posts.length, toUid);
await async.eachOf(postsByUser, async (posts, uid) => {
await db.sortedSetIncrBy(`tid:${tid}:posters`, -posts.length, uid);

View File

@@ -45,7 +45,7 @@ privsPosts.get = async function (pids, uid) {
const privileges = cids.map((cid, i) => {
const isAdminOrMod = results.isAdmin || isModerator[cid];
const editable = (privData['posts:edit'][cid] && (results.isOwner[i] || results.isModerator)) || results.isAdmin;
const editable = (privData['posts:edit'][cid] && (results.isOwner[i] || results.isModerator[i])) || results.isAdmin;
const viewDeletedPosts = results.isOwner[i] || privData['posts:view_deleted'][cid] || results.isAdmin;
const viewHistory = results.isOwner[i] || privData['posts:history'][cid] || results.isAdmin;

View File

@@ -147,6 +147,6 @@ async function shutdown(code) {
} catch (err) {
winston.error(err.stack);
return process.exit(code || 0);
process.exit(code || 0);
}
}

View File

@@ -233,7 +233,7 @@ module.exports = function (Topics) {
posts.getUserInfoForPosts([postOwner], uid),
]);
await Promise.all([
Topics.addParentPosts([postData]),
Topics.addParentPosts([postData], uid),
Topics.syncBacklinks(postData),
Topics.markAsRead([tid], uid),
]);

View File

@@ -91,7 +91,7 @@ module.exports = function (Topics) {
};
Topics.movePostToTopic = async function (callerUid, pid, tid, forceScheduled = false) {
tid = parseInt(tid, 10);
tid = String(tid);
const topicData = await Topics.getTopicFields(tid, ['tid', 'scheduled']);
if (!topicData.tid) {
throw new Error('[[error:no-topic]]');
@@ -109,7 +109,7 @@ module.exports = function (Topics) {
throw new Error('[[error:cant-move-from-scheduled-to-existing]]');
}
if (postData.tid === tid) {
if (String(postData.tid) === String(tid)) {
throw new Error('[[error:cant-move-to-same-topic]]');
}

View File

@@ -192,7 +192,6 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev
topicData.thumbs = thumbs[0];
topicData.posts = posts;
topicData.events = events;
topicData.posts.forEach((p) => {
p.events = events.filter(
event => event.timestamp >= p.eventStart && event.timestamp < p.eventEnd

View File

@@ -21,7 +21,7 @@ module.exports = function (Topics) {
}
const otherTids = tids.sort((a, b) => a - b)
.filter(tid => tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10));
.filter(tid => tid && String(tid) !== String(mergeIntoTid));
for (const tid of otherTids) {
/* eslint-disable no-await-in-loop */

View File

@@ -12,6 +12,7 @@ const meta = require('../meta');
const activitypub = require('../activitypub');
const plugins = require('../plugins');
const utils = require('../utils');
const privileges = require('../privileges');
const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g');
@@ -129,7 +130,7 @@ module.exports = function (Topics) {
getPostUserData('uid', async uids => await posts.getUserInfoForPosts(uids, uid)),
getPostUserData('editor', async uids => await user.getUsersFields(uids, ['uid', 'username', 'userslug'])),
getPostReplies(postData, uid),
Topics.addParentPosts(postData),
Topics.addParentPosts(postData, uid),
]);
postData.forEach((postObj, i) => {
@@ -178,7 +179,7 @@ module.exports = function (Topics) {
});
};
Topics.addParentPosts = async function (postData) {
Topics.addParentPosts = async function (postData, callerUid) {
let parentPids = postData
.filter(p => p && p.hasOwnProperty('toPid') && (activitypub.helpers.isUri(p.toPid) || utils.isNumber(p.toPid)))
.map(postObj => postObj.toPid);
@@ -187,18 +188,40 @@ module.exports = function (Topics) {
return;
}
parentPids = _.uniq(parentPids);
const parentPosts = await posts.getPostsFields(parentPids, ['uid']);
const postPrivileges = await privileges.posts.get(parentPids, callerUid);
const pidToPrivs = _.zipObject(parentPids, postPrivileges);
parentPids = parentPids.filter(p => pidToPrivs[p]['topics:read']);
const parentPosts = await posts.getPostsFields(parentPids, ['uid', 'pid', 'timestamp', 'content', 'deleted']);
const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid));
const userData = await user.getUsersFields(parentUids, ['username']);
const userData = await user.getUsersFields(parentUids, ['username', 'userslug', 'picture']);
const usersMap = _.zipObject(parentUids, userData);
await Promise.all(parentPosts.map(async (parentPost) => {
const postPrivs = pidToPrivs[parentPost.pid];
if (parentPost.deleted && String(parentPost.uid) !== String(callerUid, 10) && !postPrivs['posts:view_deleted']) {
parentPost.content = `<p>[[topic:post-is-deleted]]</p>`;
return;
}
const foundPost = postData.find(p => String(p.pid) === String(parentPost.pid));
if (foundPost) {
parentPost.content = foundPost.content;
return;
}
parentPost = await posts.parsePost(parentPost);
}));
const parents = {};
parentPosts.forEach((post, i) => {
if (usersMap[post.uid]) {
parents[parentPids[i]] = {
uid: post.uid,
username: usersMap[post.uid].username,
displayname: usersMap[post.uid].displayname,
pid: post.pid,
content: post.content,
user: usersMap[post.uid],
timestamp: post.timestamp,
timestampISO: post.timestampISO,
};
}
});

View File

@@ -55,7 +55,7 @@ Thumbs.get = async function (tids, options) {
};
}
const isDraft = !await topics.exists(tids);
const isDraft = (await topics.exists(tids)).map(exists => !exists);
if (!meta.config.allowTopicsThumbnail || !tids.length) {
return singular ? [] : tids.map(() => []);
@@ -71,9 +71,10 @@ Thumbs.get = async function (tids, options) {
if (!options.thumbsOnly) {
// Add uploaded media to thumb sets
const mainPidUploads = await Promise.all(mainPids.map(async pid => await posts.uploads.list(pid)));
const mainPidUploads = await Promise.all(mainPids.map(posts.uploads.list));
mainPidUploads.forEach((uploads, idx) => {
uploads = uploads.map(path => `/${path}`);
uploads = uploads.map(upath => path.join(path.sep, `${upath}`));
uploads = uploads.filter((upload) => {
const type = mime.getType(upload);
return !thumbs[idx].includes(upload) && type && type.startsWith('image/');

View File

@@ -9,19 +9,23 @@
<button type="button" class="btn btn-ghost btn-sm dropdown-toggle w-100" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span component="group-selector-selected">{group.displayName}</span> <span class="caret"></span>
</button>
<div component="group-selector-search" class="hidden position-absolute w-100">
<input type="text" class="form-control" autocomplete="off">
<div class="dropdown-menu p-1">
<div component="group-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="group-list" class="list-unstyled mb-0 text-sm dropdown-menu-end group-dropdown-menu overflow-auto ghost-scrollbar" role="menu" style="max-height: 500px;">
<li component="group-no-matches" role="presentation" class="group hidden">
<a class="dropdown-item rounded-1" role="menuitem">[[search:no-matches]]</a>
</li>
{{{ each groupNames }}}
<li role="presentation" class="group" data-name="{groupNames.displayName}">
<a class="dropdown-item rounded-1" href="{config.relative_path}/admin/manage/groups/{groupNames.encodedName}" role="menuitem">{groupNames.displayName}</a>
</li>
{{{ end }}}
</ul>
</div>
<ul component="group-list" class="dropdown-menu dropdown-menu-end group-dropdown-menu overflow-auto p-1" role="menu" style="max-height: 500px;">
<li component="group-no-matches" role="presentation" class="group hidden">
<a class="dropdown-item rounded-1" role="menuitem">[[search:no-matches]]</a>
</li>
{{{ each groupNames }}}
<li role="presentation" class="group" data-name="{groupNames.displayName}">
<a class="dropdown-item rounded-1" href="{config.relative_path}/admin/manage/groups/{groupNames.encodedName}" role="menuitem">{groupNames.displayName}</a>
</li>
{{{ end }}}
</ul>
</div>
</div>
<div class="col-12 col-md-4 px-0 px-md-3 ">
@@ -157,26 +161,30 @@
<button type="button" class="btn btn-ghost btn-sm d-flex gap-2 align-items-center flex-fill dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-fw fa-lock text-primary"></i> <span>[[admin/manage/groups:privileges]]</span> <span class="caret"></span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu dropdown-menu-end ghost-scrollbar" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item" role="menuitem">[[search:no-matches]]</a>
</li>
{{{each categories}}}
<li role="presentation" class="category {{{ if categories.disabledClass }}}disabled{{{ end }}}" data-cid="{categories.cid}" data-name="{categories.name}" data-parent-cid="{categories.parentCid}">
<a class="dropdown-item rounded-1" role="menuitem">{categories.level}
<span component="category-markup">
<div class="category-item d-inline-block">
{buildCategoryIcon(@value, "24px", "rounded-circle")}
{./name}
</div>
</span>
</a>
</li>
{{{end}}}
</ul>
</div>
<ul component="category/list" class="dropdown-menu category-dropdown-menu dropdown-menu-end p-1" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item" role="menuitem">[[search:no-matches]]</a>
</li>
{{{each categories}}}
<li role="presentation" class="category {{{ if categories.disabledClass }}}disabled{{{ end }}}" data-cid="{categories.cid}" data-name="{categories.name}" data-parent-cid="{categories.parentCid}">
<a class="dropdown-item rounded-1" role="menuitem">{categories.level}
<span component="category-markup">
<div class="category-item d-inline-block">
{buildCategoryIcon(@value, "24px", "rounded-circle")}
{./name}
</div>
</span>
</a>
</li>
{{{end}}}
</ul>
</div>
</div>

View File

@@ -1,19 +1,21 @@
<form type="form">
<div class="form-group">
<div component="category-selector" class="btn-group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span component="category-selector-selected">[[topic:thread-tools.select-category]]</span> <span class="caret"></span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control" placeholder="[[search:type-to-search]]" autocomplete="off">
<div class="mb-3">
<div component="category-selector" class="btn-group">
<button type="button" class="btn btn-ghost border dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span component="category-selector-selected">[[topic:thread-tools.select-category]]</span> <span class="caret"></span>
</button>
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="dropdown-menu category-dropdown-menu" role="menu">
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item" role="menuitem">[[search:no-matches]]</a>
<a class="dropdown-item rounded-1" role="menuitem">[[search:no-matches]]</a>
</li>
{{{ each categories }}}
<li role="presentation" class="category {{{if categories.disabledClass}}}disabled{{{end}}}" data-cid="{categories.cid}" data-name="{categories.name}">
<a class="dropdown-item" role="menuitem">{categories.level}
<a class="dropdown-item rounded-1" role="menuitem">{categories.level}
<span component="category-markup">
<div class="category-item d-inline-block">
{buildCategoryIcon(@value, "24px", "rounded-circle")}
@@ -26,7 +28,8 @@
</ul>
</div>
</div>
</form>
</div>
{{{ if message }}}
<div>{message}</div>
{{{ end }}}

View File

@@ -11,25 +11,30 @@
</span>
</span> <span class="caret"></span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<ul component="category/list" class="dropdown-menu category-dropdown-menu p-1" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item rounded-1" role="menu-item">[[search:no-matches]]</a>
</li>
{{{each categoryItems}}}
<li role="presentation" class="category {{{ if ./disabledClass }}}disabled {{{ end }}}" data-cid="{./cid}" data-name="{./name}" data-parent-cid="{./parentCid}">
<a href="#" class="dropdown-item rounded-1" role="menu-item">{./level}
<span component="category-markup" style="{{{ if ./match }}}font-weight: bold;{{{end}}}">
<div class="category-item d-inline-flex align-items-center gap-1">
{{{ if ./icon }}}
{buildCategoryIcon(@value, "24px", "rounded-circle")}
{{{ end }}}
{./name}
</div>
</span>
</a>
</li>
{{{ end }}}
</ul>
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item rounded-1" role="menu-item">[[search:no-matches]]</a>
</li>
{{{each categoryItems}}}
<li role="presentation" class="category {{{ if ./disabledClass }}}disabled {{{ end }}}" data-cid="{./cid}" data-name="{./name}" data-parent-cid="{./parentCid}">
<a href="#" class="dropdown-item rounded-1" role="menu-item">{./level}
<span component="category-markup" style="{{{ if ./match }}}font-weight: bold;{{{end}}}">
<div class="category-item d-inline-flex align-items-center gap-1">
{{{ if ./icon }}}
{buildCategoryIcon(@value, "24px", "rounded-circle")}
{{{ end }}}
{./name}
</div>
</span>
</a>
</li>
{{{ end }}}
</ul>
</div>

View File

@@ -1,25 +0,0 @@
<div component="category-selector" class="btn-group">
<button type="button" class="btn btn-light btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-fw fa-lock text-primary"></i> <span>[[admin/manage/groups:privileges]]</span> <span class="caret"></span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<ul component="category/list" class="dropdown-menu category-dropdown-menu dropdown-menu-end p-1" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item" role="menuitem">[[search:no-matches]]</a>
</li>
{{{each categories}}}
<li role="presentation" class="category {{{ if categories.disabledClass }}}disabled{{{ end }}}" data-cid="{categories.cid}" data-name="{categories.name}" data-parent-cid="{categories.parentCid}">
<a class="dropdown-item rounded-1" role="menuitem">{categories.level}
<span component="category-markup">
<div class="category-item d-inline-block">
{buildCategoryIcon(@value, "24px", "rounded-circle")}
{./name}
</div>
</span>
</a>
</li>
{{{end}}}
</ul>
</div>

View File

@@ -11,7 +11,7 @@
<span class="input-group-text"><i class="fa fa-search"></i></span>
</div>
<div class="quick-search-container dropdown-menu d-block p-2 hidden w-100">
<div class="quick-search-container dropdown-menu d-block p-2 hidden">
<div class="text-center loading-indicator"><i class="fa fa-spinner fa-spin"></i></div>
<div class="quick-search-results-container"></div>
</div>

View File

@@ -9,11 +9,12 @@
<span class="d-none d-md-inline fw-semibold">[[unread:all-categories]]</span>{{{ end }}}
</button>
<div component="category-selector-search" class="hidden position-absolute" style="min-width: 120px;">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
<li role="presentation" class="category" data-cid="all">
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem" href="{{{ if allCategoriesUrl }}}{config.relative_path}/{allCategoriesUrl}{{{ else }}}#{{{ end }}}">

View File

@@ -12,11 +12,11 @@
</span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
<li component="category/no-matches" role="presentation" class="category hidden">
<a class="dropdown-item rounded-1" role="menuitem">[[search:no-matches]]</a>

View File

@@ -8,6 +8,6 @@
</div>
<span class="chat-timestamp text-muted timeago text-nowrap hidden" title="{messages.parent.timestampISO}"></span>
</div>
<div component="chat/message/parent/content" class="text-muted line-clamp-1 w-100">{messages.parent.content}</div>
<div component="chat/message/parent/content" class="text-muted line-clamp-1 text-break w-100">{messages.parent.content}</div>
</div>
</div>

View File

@@ -23,11 +23,12 @@
{{{ end }}}
<span class="caret text-primary opacity-75"></span>
</button>
<div component="category-selector-search" class="hidden position-absolute">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<div class="dropdown-menu p-1">
<div component="category-selector-search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
{{{each categoryItems}}}
<li role="presentation" class="category {{{ if ../disabledClass }}}disabled{{{ end }}}" data-cid="{../cid}" data-parent-cid="{../parentCid}" data-name="{../name}">

View File

@@ -10,11 +10,11 @@
{{{ end }}}
</button>
<div component="tag/filter/search" class="hidden position-absolute" style="min-width: 120px;">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
</div>
<div class="dropdown-menu p-1">
<div component="tag/filter/search" class="p-1 hidden">
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
<hr class="mt-2 mb-0"/>
</div>
<ul component="tag/filter/list" class="list-unstyled mb-0 text-sm overflow-auto ghost-scrollbar" role="menu" style="max-height: 500px;" role="menu">
<li role="presentation" data-tag="">
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem" href="#">

View File

@@ -0,0 +1,12 @@
<div component="post/parent" data-parent-pid="{./parent.pid}" data-uid="{./parent.uid}" class="btn btn-ghost btn-sm d-flex gap-2 text-start flex-row mb-2" style="font-size: 13px;">
<div class="d-flex gap-2 text-nowrap">
<div><i class="fa fa-sm fa-reply opacity-50"></i></div>
<div class="d-flex flex-nowrap gap-1 align-items-center">
<a href="{config.relative_path}/user/{./parent.user.userslug}" class="text-decoration-none lh-1">{buildAvatar(./parent.user, "14px", true, "not-responsive align-middle")}</a>
<a class="fw-semibold" href="{config.relative_path}/user/{./parent.user.userslug}">{./parent.user.displayname}</a>
</div>
<a href="{config.relative_path}/post/{./parent.pid}" class="text-muted timeago text-nowrap hidden" title="{./parent.timestampISO}"></a>
</div>
<div component="post/parent/content" class="text-muted line-clamp-1 text-break w-100">{./parent.content}</div>
</div>

View File

@@ -69,11 +69,16 @@ server.on('connection', (conn) => {
});
});
exports.destroy = function (callback) {
server.close(callback);
for (const connection of Object.values(connections)) {
connection.destroy();
}
exports.destroy = function () {
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) reject(err);
else resolve();
});
for (const connection of Object.values(connections)) {
connection.destroy();
}
});
};
exports.getConnectionCount = function () {

View File

@@ -77,8 +77,14 @@ describe('Admin Controllers', () => {
it('should load admin dashboard', async () => {
await groups.join('administrators', adminUid);
const today = new Date();
const end = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
today.setDate(today.getDate() - 1);
const start = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const dashboards = [
'/admin', '/admin/dashboard/logins', '/admin/dashboard/users', '/admin/dashboard/topics', '/admin/dashboard/searches',
'/admin', '/admin/dashboard/logins', '/admin/dashboard/users', '/admin/dashboard/topics',
'/admin/dashboard/searches', `/admin/dashboard/searches?start=${start}&end=${end}`,
];
await async.each(dashboards, async (url) => {
const { response, body } = await request.get(`${nconf.get('url')}${url}`, { jar: jar });