diff --git a/CHANGELOG.md b/CHANGELOG.md index a76bef6d2d..2808bece2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/install/package.json b/install/package.json index 84bbc866ba..cd00e60504 100644 --- a/install/package.json +++ b/install/package.json @@ -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" } ] -} \ No newline at end of file +} diff --git a/public/language/en-GB/themes/harmony.json b/public/language/en-GB/themes/harmony.json index 01038d7641..e3ba514912 100644 --- a/public/language/en-GB/themes/harmony.json +++ b/public/language/en-GB/themes/harmony.json @@ -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", diff --git a/public/openapi/read/admin/config.yaml b/public/openapi/read/admin/config.yaml index 4b3fa7ffdc..2a72e6d6e1 100644 --- a/public/openapi/read/admin/config.yaml +++ b/public/openapi/read/admin/config.yaml @@ -27,6 +27,8 @@ get: type: string keywords: type: string + brand:logo: + type: string titleLayout: type: string showSiteTitle: diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml index 626e590e42..d6e012bdae 100644 --- a/public/openapi/read/config.yaml +++ b/public/openapi/read/config.yaml @@ -27,6 +27,8 @@ get: type: string keywords: type: string + brand:logo: + type: string titleLayout: type: string showSiteTitle: diff --git a/public/openapi/read/topic/topic_id.yaml b/public/openapi/read/topic/topic_id.yaml index ded3e1f568..302d39dbcc 100644 --- a/public/openapi/read/topic/topic_id.yaml +++ b/public/openapi/read/topic/topic_id.yaml @@ -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: diff --git a/public/scss/admin/admin.scss b/public/scss/admin/admin.scss index a07f7779ee..f58af12f41 100644 --- a/public/scss/admin/admin.scss +++ b/public/scss/admin/admin.scss @@ -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; } } diff --git a/public/scss/chats.scss b/public/scss/chats.scss index e9bd116a90..d61965088e 100644 --- a/public/scss/chats.scss +++ b/public/scss/chats.scss @@ -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 { diff --git a/public/scss/generics.scss b/public/scss/generics.scss index db5aaa4675..808d427ba5 100644 --- a/public/scss/generics.scss +++ b/public/scss/generics.scss @@ -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; } } diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index 6fb820a946..a70a6d17f0 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -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)); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 0bef2268df..9e78d13cad 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -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; diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 73e1a91efc..5b45deeb54 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -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]); } } diff --git a/public/src/client/topic/move-post.js b/public/src/client/topic/move-post.js index 3c154b9374..62863f16bf 100644 --- a/public/src/client/topic/move-post.js +++ b/public/src/client/topic/move-post.js @@ -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(); diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index b88dc73608..3f9840ebcf 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -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; } diff --git a/public/src/client/topic/replies.js b/public/src/client/topic/replies.js index 878c86f952..dc7ef8daef 100644 --- a/public/src/client/topic/replies.js +++ b/public/src/client/topic/replies.js @@ -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) { diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index a66b293a73..38db844cdd 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -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; } diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js index 8c7461dcdf..8fa7a9f184 100644 --- a/public/src/modules/categorySearch.js +++ b/public/src/modules/categorySearch.js @@ -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'); } diff --git a/public/src/modules/groupSearch.js b/public/src/modules/groupSearch.js index 66e19a1665..8d25844d7f 100644 --- a/public/src/modules/groupSearch.js +++ b/public/src/modules/groupSearch.js @@ -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'); diff --git a/public/src/modules/tagFilter.js b/public/src/modules/tagFilter.js index f752b0df7e..ea8f8f887f 100644 --- a/public/src/modules/tagFilter.js +++ b/public/src/modules/tagFilter.js @@ -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'); } diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 95274fae89..3ea4c03494 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -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, diff --git a/src/api/topics.js b/src/api/topics.js index 81c1ee876a..b9066786a0 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -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); diff --git a/src/categories/index.js b/src/categories/index.js index 51eea283c0..ef1879ab31 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -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); diff --git a/src/categories/unread.js b/src/categories/unread.js deleted file mode 100644 index 48d80bb29d..0000000000 --- a/src/categories/unread.js +++ /dev/null @@ -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); - }; -}; diff --git a/src/controllers/api.js b/src/controllers/api.js index d0120b692e..f55b411bfa 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -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, '{').replace(/}/g, '}'), showSiteTitle: meta.config.showSiteTitle === 1, maintenanceMode: meta.config.maintenanceMode === 1, diff --git a/src/controllers/recent.js b/src/controllers/recent.js index d4d577cabd..5699fee1b7 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -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 }; diff --git a/src/messaging/data.js b/src/messaging/data.js index aff649f225..aa96e11a67 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -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( diff --git a/src/notifications.js b/src/notifications.js index 1d020e536b..7f7bb5268a 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -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; } diff --git a/src/posts/delete.js b/src/posts/delete.js index a56fb17f3e..6ea4b55453 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -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([ diff --git a/src/posts/queue.js b/src/posts/queue.js index 91fab2e6e9..cc3b1078c8 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -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)) ); } diff --git a/src/posts/user.js b/src/posts/user.js index b92aca8a9a..d096b0fa50 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -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); diff --git a/src/privileges/posts.js b/src/privileges/posts.js index e8b68771de..e289cb8414 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -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; diff --git a/src/start.js b/src/start.js index 677230e235..99f3b662c5 100644 --- a/src/start.js +++ b/src/start.js @@ -147,6 +147,6 @@ async function shutdown(code) { } catch (err) { winston.error(err.stack); - return process.exit(code || 0); + process.exit(code || 0); } } diff --git a/src/topics/create.js b/src/topics/create.js index 2d06d22123..96113c3e5a 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -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), ]); diff --git a/src/topics/fork.js b/src/topics/fork.js index 07111eecd1..d86322a9df 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -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]]'); } diff --git a/src/topics/index.js b/src/topics/index.js index 46483d441a..07600e6311 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -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 diff --git a/src/topics/merge.js b/src/topics/merge.js index 1a06adefbb..7812a3d4e4 100644 --- a/src/topics/merge.js +++ b/src/topics/merge.js @@ -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 */ diff --git a/src/topics/posts.js b/src/topics/posts.js index 353dcddb1e..bbbeb9c636 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -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 = `
[[topic:post-is-deleted]]
`; + 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, }; } }); diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index 3a47033afc..1fe985a270 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -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/'); diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl index 114d5e8692..79304a1e23 100644 --- a/src/views/admin/manage/group.tpl +++ b/src/views/admin/manage/group.tpl @@ -9,19 +9,23 @@ -