diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6be3f690..8058f2350d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +#### v4.10.1 (2026-03-25) + +##### Chores + +* incrementing version number - v4.10.0 (5b703104) +* update changelog for v4.10.0 (c480df9e) +* incrementing version number - v4.9.2 (e6846052) +* incrementing version number - v4.9.1 (72e44c86) +* incrementing version number - v4.9.0 (3fdd1bef) +* incrementing version number - v4.8.1 (713ae0c0) +* incrementing version number - v4.8.0 (3fac737a) +* incrementing version number - v4.7.2 (cd419d8a) +* incrementing version number - v4.7.1 (afb88805) +* incrementing version number - v4.7.0 (e82d40f8) +* incrementing version number - v4.6.3 (9fc5b0f3) +* incrementing version number - v4.6.2 (f98747db) +* incrementing version number - v4.6.1 (f47aa678) +* incrementing version number - v4.6.0 (ee395bc5) +* incrementing version number - v4.5.2 (ad2da639) +* incrementing version number - v4.5.1 (69f4b61f) +* incrementing version number - v4.5.0 (f05c5d06) +* incrementing version number - v4.4.6 (074043ad) +* incrementing version number - v4.4.5 (6f106923) +* incrementing version number - v4.4.4 (d323af44) +* incrementing version number - v4.4.3 (d354c2eb) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* add email share (43e7f0ab) + +##### Bug Fixes + +* #14123, aria-hidden fixes (72f48fd9) +* #14121, use normalizedPath when uploading (a10471fc) +* key name (52e42685) +* #14108, reset filter on notif dropdown open (ad1433e1) +* #14116, don't return ban reason if login credentials are incorrect (9bcef6b5) +* share url for ap posts, fallback to window.location.href if pid doesnt exist (361134f9) + +##### Refactors + +* work with different line-clamp values (9b885162) + #### v4.10.0 (2026-03-19) ##### Chores diff --git a/install/package.json b/install/package.json index 42a5dda294..01011ea150 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.62", + "nodebb-theme-harmony": "2.2.63", "nodebb-theme-lavender": "7.1.21", - "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.33", + "nodebb-theme-peace": "2.2.58", + "nodebb-theme-persona": "14.2.34", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.3", "nprogress": "0.2.0", diff --git a/public/openapi/read/notifications.yaml b/public/openapi/read/notifications.yaml index 015b6fae33..8dd723aabf 100644 --- a/public/openapi/read/notifications.yaml +++ b/public/openapi/read/notifications.yaml @@ -71,6 +71,11 @@ get: type: boolean readClass: type: string + unreadNids: + type: array + description: An array of notification ids that are unread. + items: + type: string filters: $ref: ../components/schemas/NotificationFilters.yaml#/FiltersArray regularFilters: diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 4893cea2fd..94a2d25307 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -17,6 +17,7 @@ module.exports = function (utils, Benchpress, relative_path) { generateCategoryBackground, generateChildrenCategories, generateTopicClass, + generateGroupDisplayName, membershipBtn, spawnPrivilegeStates, localeToHTML, @@ -167,6 +168,10 @@ module.exports = function (utils, Benchpress, relative_path) { return fields.filter(field => !!topic[field]).join(' '); } + function generateGroupDisplayName(group) { + return group.system ? group.displayName.replace(/-/g, ' ') : group.displayName; + } + // Groups helpers function membershipBtn(groupObj, btnClass = '') { if (groupObj.isMember && groupObj.name !== 'administrators') { diff --git a/src/analytics.js b/src/analytics.js index e054e2e733..e8e60c5e68 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -38,11 +38,7 @@ Analytics.init = async function () { runOnAllNodes: true, onTick: async () => { if (Analytics.pause) return; - publishLocalAnalytics(); - if (runJobs) { - await sleep(2000); - await Analytics.writeData(); - } + await Analytics.writeLocalData(); }, }); @@ -63,6 +59,14 @@ Analytics.init = async function () { } }; +Analytics.writeLocalData = async function () { + publishLocalAnalytics(); + if (runJobs) { + await sleep(2000); + await Analytics.writeData(); + } +}; + function publishLocalAnalytics() { pubsub.publish('analytics:publish', { local: local, @@ -185,6 +189,12 @@ Analytics.writeData = async function () { incrByBulk.push(['analytics:pageviews:ap', total.apPageViews, today.getTime()]); incrByBulk.push(['analytics:pageviews:ap:month', total.apPageViews, month.getTime()]); total.apPageViews = 0; + if (!metrics.includes('pageviews:ap')) { + metrics.push('pageviews:ap'); + } + if (!metrics.includes('pageviews:ap:month')) { + metrics.push('pageviews:ap:month'); + } } if (total.uniquevisitors > 0) { diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index 92cc980fa2..4949ef619f 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -148,8 +148,11 @@ async function upgradePlugins(unattended = false) { if (['y', 'Y', 'yes', 'YES'].includes(result.upgrade)) { console.log('\nUpgrading packages...'); const args = packageManagerInstallArgs.concat(found.map(suggestObj => `${suggestObj.name}@${suggestObj.suggested}`)); - - cproc.execFileSync(packageManagerExecutable, args, { stdio: 'ignore' }); + const options = { stdio: 'ignore' }; + if (process.platform === 'win32') { + options.shell = true; + } + cproc.execFileSync(packageManagerExecutable, args, options); } else { console.log(`${chalk.yellow('Package upgrades skipped')}. Check for upgrades at any time by running "${chalk.green('./nodebb upgrade -p')}".`); } diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index 301851ca36..00b94f0acb 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -61,6 +61,7 @@ notificationsController.get = async function (req, res, next) { const data = await user.notifications.getAllWithCounts(req.uid, selectedFilter.filter); let notifications = await user.notifications.getNotifications(data.nids, req.uid); + const unreadNids = notifications.filter(n => n && n.nid && !n.read).map(n => n.nid); allFilters.forEach((filterData) => { if (filterData && filterData.filter) { filterData.count = data.counts[filterData.filter] || 0; @@ -72,6 +73,7 @@ notificationsController.get = async function (req, res, next) { res.render('notifications', { notifications: notifications, + unreadNids, pagination: pagination.create(page, pageCount, req.query), filters: allFilters, regularFilters: regularFilters, diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index fab6b8d8cc..c388023840 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -221,6 +221,7 @@ authenticationController.login = async (req, res, next) => { } const loginWith = meta.config.allowLoginWith || 'username-email'; + req.body = req.body || {}; req.body.username = String(req.body.username).trim(); const errorHandler = res.locals.noScriptErrors || helpers.noScriptErrors; try { diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 23a03d87e5..29167fcfb9 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -20,6 +20,7 @@ const activitypub = require('../activitypub'); const topicsController = module.exports; const url = nconf.get('url'); +const base_url = nconf.get('base_url'); const relative_path = nconf.get('relative_path'); const upload_url = nconf.get('upload_url'); const validSorts = ['oldest_to_newest', 'newest_to_oldest', 'most_votes']; @@ -356,8 +357,13 @@ function addOGImageTag(res, image) { } if (!imageUrl.startsWith('http')) { - // (https://domain.com/forum) + (/assets/uploads) + (/files/imagePath) - imageUrl = url + path.posix.join(upload_url, imageUrl); + if (imageUrl.startsWith(`${relative_path}${upload_url}`)) { + // (https://domain.com) + imageUrl (which starts with /relative_path/upload_url) + imageUrl = base_url + imageUrl; + } else { + // (https://domain.com/forum) + (/assets/uploads) + (/files/imagePath) + imageUrl = url + path.posix.join(upload_url, imageUrl); + } } res.locals.metaTags.push({ diff --git a/src/database/mongo/sorted/add.js b/src/database/mongo/sorted/add.js index bc3a8bc8ec..82539ee8c8 100644 --- a/src/database/mongo/sorted/add.js +++ b/src/database/mongo/sorted/add.js @@ -83,7 +83,9 @@ module.exports = function (module) { if (!utils.isNumber(item[1])) { throw new Error(`[[error:invalid-score, ${item[1]}]]`); } - bulk.find({ _key: item[0], value: String(item[2]) }).upsert().updateOne({ $set: { score: parseFloat(item[1]) } }); + bulk.find({ _key: item[0], value: String(item[2]) }) + .upsert() + .updateOne({ $set: { score: parseFloat(item[1]) } }); }); await bulk.execute(); }; diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 85e0b63d07..e3df40bad5 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -27,31 +27,27 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - await db.query({ - name: 'ensureLegacyObjectType1', + const res = await tryUpsert(db, { + name: 'ensureLegacyObjectType_upsert', text: ` -INSERT INTO "legacy_object" ("_key", "type") -VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) - ON CONFLICT - DO NOTHING`, + INSERT INTO "legacy_object" ("_key", "type") + VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "type"`, values: [key, type], }); - const res = await db.query({ - name: 'ensureLegacyObjectType2', - text: ` -SELECT "type" - FROM "legacy_object_live" - WHERE "_key" = $1::TEXT`, - values: [key], - }); - if (res.rows[0].type !== type) { throw new Error(`database: cannot insert ${JSON.stringify(key)} as ${type} because it already exists as ${res.rows[0].type}`); } }; helpers.ensureLegacyObjectsType = async function (db, keys, type) { + keys = [...new Set(keys)]; + if (!keys.length) { + return; + } await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` @@ -60,38 +56,45 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - await db.query({ - name: 'ensureLegacyObjectsType1', + const res = await tryUpsert(db, { + name: 'ensureLegacyObjectsType_upsert', text: ` INSERT INTO "legacy_object" ("_key", "type") SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE - FROM UNNEST($1::TEXT[]) k - ON CONFLICT - DO NOTHING`, +FROM UNNEST($1::TEXT[]) k + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "_key", "type"`, values: [keys, type], }); - const res = await db.query({ - name: 'ensureLegacyObjectsType2', - text: ` -SELECT "_key", "type" - FROM "legacy_object_live" - WHERE "_key" = ANY($1::TEXT[])`, - values: [keys], - }); - const invalid = res.rows.filter(r => r.type !== type); if (invalid.length) { const parts = invalid.map(r => `${JSON.stringify(r._key)} is ${r.type}`); throw new Error(`database: cannot insert multiple objects as ${type} because they already exist: ${parts.join(', ')}`); } - - const missing = keys.filter(k => !res.rows.some(r => r._key === k)); - - if (missing.length) { - throw new Error(`database: failed to insert keys for objects: ${JSON.stringify(missing)}`); - } }; +async function tryUpsert(db, queryConfig) { + let res; + const savepoint = `upsert_${Math.random().toString(36).substring(7)}`; + try { + await db.query(`SAVEPOINT ${savepoint}`); + res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); + } catch (err) { + if (err.code === '23505') { // retry if failed due to error: unique constraint + // Roll back to the savepoint to prevent + // error: current transaction is aborted, commands ignored until end of transaction block + await db.query(`ROLLBACK TO SAVEPOINT ${savepoint}`); + res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); + } else { + throw err; + } + } + return res; +} + helpers.noop = function () {}; diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 351fe3e059..50581a6ed4 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -551,16 +551,29 @@ RETURNING "score" s`, return []; } + // Deduplicate by (key, value) pair, summing increments for duplicates + const seen = new Map(); + const deduped = []; + data.forEach(([key, increment, value]) => { + value = helpers.valueToString(value); + increment = parseFloat(increment); + const mapKey = `${key}\0${value}`; + if (seen.has(mapKey)) { + deduped[seen.get(mapKey)][1] += increment; + } else { + seen.set(mapKey, deduped.length); + deduped.push([key, increment, value]); + } + }); + return await module.transaction(async (client) => { - await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset'); + await helpers.ensureLegacyObjectsType(client, deduped.map(item => item[0]), 'zset'); const values = []; const queryParams = []; let paramIndex = 1; - data.forEach(([key, increment, value]) => { - value = helpers.valueToString(value); - increment = parseFloat(increment); + deduped.forEach(([key, increment, value]) => { values.push(key, value, increment); queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`); paramIndex += 3; diff --git a/src/database/postgres/sorted/add.js b/src/database/postgres/sorted/add.js index 6f87416089..3db65cfde9 100644 --- a/src/database/postgres/sorted/add.js +++ b/src/database/postgres/sorted/add.js @@ -114,8 +114,10 @@ INSERT INTO "legacy_zset" ("_key", "value", "score") } keys.push(item[0]); scores.push(item[1]); - values.push(item[2]); + values.push(helpers.valueToString(item[2])); }); + const compositeKeys = keys.map((k, i) => `${k}\0${values[i]}`); + helpers.removeDuplicateValues(compositeKeys, keys, values, scores); await module.transaction(async (client) => { await helpers.ensureLegacyObjectsType(client, keys, 'zset'); await client.query({ diff --git a/src/groups/data.js b/src/groups/data.js index bef4d82fec..e585b653b5 100644 --- a/src/groups/data.js +++ b/src/groups/data.js @@ -133,9 +133,6 @@ module.exports = function (Groups) { if (hasField('name')) { group.nameEncoded = encodeURIComponent(group.name); group.displayName = validator.escape(String(group.name)); - if (Groups.systemGroups.includes(group.name)) { - group.displayName = group.displayName.replace(/-/g, ' '); - } } if (hasField('description')) { group.description = validator.escape(String(group.description || '')); diff --git a/src/meta/configs.js b/src/meta/configs.js index f686b3dd72..56071868c4 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -6,6 +6,7 @@ const path = require('path'); const winston = require('winston'); const db = require('../database'); +const file = require('../file'); const pubsub = require('../pubsub'); const Meta = require('./index'); const translator = require('../translator'); @@ -212,20 +213,17 @@ async function getLogoSize(data) { if (!data['brand:logo']) { return; } + const x50Path = path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png'); let size; - try { - size = await image.size(path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png')); - } catch (err) { - if (err.code === 'ENOENT') { - // For whatever reason the x50 logo wasn't generated, gracefully error out - winston.warn('[logo] The email-safe logo doesn\'t seem to have been created, please re-upload your site logo.'); - size = { - height: 0, - width: 0, - }; - } else { - throw err; - } + if (await file.exists(x50Path)) { + size = await image.size(x50Path); + } else { + // For whatever reason the x50 logo wasn't generated, gracefully error out + winston.warn('[logo] The email-safe logo doesn\'t seem to have been created, please re-upload your site logo.'); + size = { + height: 0, + width: 0, + }; } data['brand:emailLogo'] = nconf.get('url') + path.join(nconf.get('upload_url'), 'system', 'site-logo-x50.png'); data['brand:emailLogo:height'] = size.height; diff --git a/src/meta/tags.js b/src/meta/tags.js index c68fe95327..bd5a5b613a 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -21,6 +21,7 @@ Tags.parse = async (req, data, meta, link) => { name: 'viewport', // https://stackoverflow.com/a/77815388 for resizes-content content: 'width=device-width, initial-scale=1.0, interactive-widget=resizes-content', + noEscape: true, }, { name: 'content-type', content: 'text/html; charset=UTF-8', diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 84d34dfbd0..bf68c380c4 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -17,14 +17,22 @@ const controllers = { const middleware = module.exports; middleware.buildHeader = helpers.try(async (req, res, next) => { - res.locals.renderAdminHeader = true; + await doBuildHeader(req, res); + next(); +}); + +middleware.buildHeaderAsync = async (req, res) => { + await doBuildHeader(req, res); +}; + +async function doBuildHeader(req, res) { + res.locals.renderHeaderType = 'admin'; if (req.method === 'GET') { await require('./index').applyCSRFasync(req, res); } - + await plugins.hooks.fire('filter:middleware.buildAdminHeader', { req: req, locals: res.locals }); res.locals.config = await controllers.admin.loadConfig(req); - next(); -}); +} middleware.checkPrivileges = helpers.try(async (req, res, next) => { // Kick out guests, obviously diff --git a/src/middleware/header.js b/src/middleware/header.js index 383ef8e94e..b462a82695 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -19,7 +19,7 @@ middleware.buildHeaderAsync = async (req, res) => { }; async function doBuildHeader(req, res) { - res.locals.renderHeader = true; + res.locals.renderHeaderType = 'client'; res.locals.isAPI = false; if (req.method === 'GET') { await require('./index').applyCSRFasync(req, res); diff --git a/src/middleware/render.js b/src/middleware/render.js index f348236884..3cbac1f22a 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -139,9 +139,9 @@ module.exports = function (middleware) { } async function loadHeaderFooterData(req, res, options) { - if (res.locals.renderHeader) { + if (res.locals.renderHeaderType === 'client') { return await loadClientHeaderFooterData(req, res, options); - } else if (res.locals.renderAdminHeader) { + } else if (res.locals.renderHeaderType === 'admin') { return await loadAdminHeaderFooterData(req, res, options); } return null; @@ -382,13 +382,13 @@ module.exports = function (middleware) { async function renderHeaderFooter(method, req, res, options, headerFooterData) { let str = ''; - if (res.locals.renderHeader) { + if (res.locals.renderHeaderType === 'client') { if (method === 'renderHeader') { str = await renderHeader(req, res, options, headerFooterData); } else if (method === 'renderFooter') { str = await renderFooter(req, res, options, headerFooterData); } - } else if (res.locals.renderAdminHeader) { + } else if (res.locals.renderHeaderType === 'admin') { if (method === 'renderHeader') { str = await renderAdminHeader(req, res, options, headerFooterData); } else if (method === 'renderFooter') { @@ -400,7 +400,7 @@ module.exports = function (middleware) { function getLang(req, res) { let language = (res.locals.config && res.locals.config.userLang) || 'en-GB'; - if (res.locals.renderAdminHeader) { + if (res.locals.renderHeaderType === 'admin') { language = (res.locals.config && res.locals.config.acpLang) || 'en-GB'; } return req.query.lang ? validator.escape(String(req.query.lang)) : language; diff --git a/src/start.js b/src/start.js index 00a129e33f..89a1683703 100644 --- a/src/start.js +++ b/src/start.js @@ -149,7 +149,7 @@ async function shutdown(code) { try { await require('./webserver').destroy(); winston.info('[app] Web server closed to connections.'); - await require('./analytics').writeData(); + await require('./analytics').writeLocalData(); winston.info('[app] Live analytics saved.'); const db = require('./database'); await db.delete('locks'); diff --git a/src/topics/create.js b/src/topics/create.js index c96dadbd4b..d2b709333c 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -259,14 +259,14 @@ module.exports = function (Topics) { return postData; }; - async function onNewPost({ pid, tid, uid: postOwner }, { uid, handle }) { + async function onNewPost({ pid, tid, content, uid: postOwner }, { uid, handle }) { const [[postData], [userInfo]] = await Promise.all([ posts.getPostSummaryByPids([pid], uid, { extraFields: ['attachments'] }), posts.getUserInfoForPosts([postOwner], uid), ]); await Promise.all([ Topics.addParentPosts([postData], uid), - Topics.syncBacklinks(postData), + Topics.syncBacklinks({ ...postData, content }), Topics.markAsRead([tid], uid), ]); if (utils.isNumber(postOwner) && postData.category.cid === -1) { diff --git a/src/topics/posts.js b/src/topics/posts.js index 535d53ff1d..1e8526f0a6 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -14,7 +14,7 @@ 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'); +const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/([a-fA-F0-9-]+)(?=\\/|$|\\s)`, 'g'); module.exports = function (Topics) { Topics.onNewPostMade = async function (postData) { @@ -447,29 +447,27 @@ module.exports = function (Topics) { throw new Error('[[error:invalid-data]]'); } - - let { content } = postData; + let { pid, uid, content } = postData; // ignore lines that start with `>` content = (content || '').split('\n').filter(line => !line.trim().startsWith('>')).join('\n'); // Scan post content for topic links const matches = [...content.matchAll(backlinkRegex)]; - if (!matches) { - return 0; - } - const { pid, uid, tid } = postData; - let add = _.uniq(matches.map(match => match[1]).map(tid => parseInt(tid, 10))); + let add = _.uniq(matches.map(match => match[1])); - const now = Date.now(); - const topicsExist = await Topics.exists(add); - const current = (await db.getSortedSetMembers(`pid:${pid}:backlinks`)).map(tid => parseInt(tid, 10)); + const [topicsExist, current] = await Promise.all([ + Topics.exists(add), + db.getSortedSetMembers(`pid:${pid}:backlinks`), + ]); const remove = current.filter(tid => !add.includes(tid)); - add = add.filter((_tid, idx) => topicsExist[idx] && !current.includes(_tid) && tid !== _tid); + const postTid = String(postData.tid); + add = add.filter((_tid, idx) => topicsExist[idx] && !current.includes(_tid) && postTid !== _tid); // Remove old backlinks await db.sortedSetRemove(`pid:${pid}:backlinks`, remove); // Add new backlinks + const now = Date.now(); await db.sortedSetAdd(`pid:${pid}:backlinks`, add.map(() => now), add); await Promise.all(add.map(async (tid) => { await Topics.events.log(tid, { @@ -479,6 +477,6 @@ module.exports = function (Topics) { }); })); - return add.length + (current - remove); + return add.length + (current.length - remove.length); }; }; diff --git a/src/user/picture.js b/src/user/picture.js index fe8a99c9a8..c60c95b721 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -144,7 +144,7 @@ module.exports = function (User) { const uploadedImage = await image.uploadImage(filename, `profile/uid-${updateUid}`, { uid: updateUid, path: normalizedPath, - name: 'profileAvatar', + name: `profileAvatar${extension}`, }); await User.updateProfile(callerUid, { diff --git a/src/views/post-queue.tpl b/src/views/post-queue.tpl index 7366500f6e..569bfb68e3 100644 --- a/src/views/post-queue.tpl +++ b/src/views/post-queue.tpl @@ -100,9 +100,9 @@ {{{ end }}} -