From 4f573ecb610c6ba328139c1cc97060bda6b9492d Mon Sep 17 00:00:00 2001 From: DhanyaShah Date: Sat, 21 Sep 2024 01:25:51 -0400 Subject: [PATCH 1/7] Added basic logic inspired by topic tools in new pin.js file --- src/topics/pin.js | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/topics/pin.js diff --git a/src/topics/pin.js b/src/topics/pin.js new file mode 100644 index 0000000000..3d1b535f39 --- /dev/null +++ b/src/topics/pin.js @@ -0,0 +1,155 @@ +'use strict'; + +const db = require('../database'); +const topics = require('./topics'); +const privileges = require('../privileges'); +const event = require('../event'); +const utils = require('../utils'); +const plugins = require('../plugins'); + +const Pin = {}; + +// Toggle Pin/Unpin a topic +Pin.togglePin = async function (tid, uid, pin) { + // Fetch topic data + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + + if (topic.scheduled) { + throw new Error('[[error:cant-pin-scheduled]]'); + } + + if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) { + throw new Error('[[error:no-privileges]]'); + } + + if (pin && topic.pinned) { + throw new Error('[[error:topic-already-pinned]]'); + } else if (!pin && !topic.pinned) { + throw new Error('[[error:topic-not-pinned]]'); + } + + // Update pin status in the database + const promises = [ + Topics.setTopicField(tid, 'pinned', pin ? 1 : 0), + Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }), + ]; + if (pin) { + promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); + promises.push(db.sortedSetsRemove([ + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:create`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:views`, + ], tid)); + } else { + promises.push(db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid)); + promises.push(Topics.deleteTopicField(tid, 'pinExpiry')); + promises.push(db.sortedSetAddBulk([ + [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], + [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], + [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], + [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], + [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], + ])); + topicData.pinExpiry = undefined; + topicData.pinExpiryISO = undefined; + } + + const results = await Promise.all(promises); + + // Emit appropriate event + event.emit(pin ? 'topic.pinned' : 'topic.unpinned', { tid, uid }); + plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }); + + return { tid, pinned: pin }; +}; + +// Pin a topic +Pin.pin = async function (tid, uid) { + return await Pin.togglePin(tid, uid, true); +}; + +// Unpin a topic +Pin.unpin = async function (tid, uid) { + return await Pin.togglePin(tid, uid, false); +}; + +// Set pin expiry for a topic +Pin.setPinExpiry = async function (tid, expiry, uid) { + // Validate expiry date + if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { + throw new Error('[[error:invalid-data]]'); + } + + // Check privileges + const topic = await topics.getTopicFields(tid, ['cid', 'uid']); + const isAdminOrMod = await privileges.categories.isAdminOrMod(topic.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + // Set pin expiry in the database + await topics.setTopicField(tid, 'pinExpiry', expiry); + plugins.hooks.fire('action:topic.setPinExpiry', { topic, uid, expiry }); + + return { tid, expiry }; +}; + +// Check and expire pins +Pin.checkPinExpiry = async function (tids) { + const expiryDates = await topics.getTopicsFields(tids, ['pinExpiry']); + const now = Date.now(); + + // Check and unpin topics that have expired + const unpinPromises = expiryDates.map(async (topicExpiry, idx) => { + if (topicExpiry && parseInt(topicExpiry.pinExpiry, 10) <= now) { + await Pin.unpin(tids[idx], 'system'); + return null; + } + return tids[idx]; + }); + + const filteredTids = (await Promise.all(unpinPromises)).filter(Boolean); + return filteredTids; +}; + +// Order pinned topics +Pin.orderPinnedTopics = async function (uid, data) { + const { tid, order } = data; + const cid = await topics.getTopicField(tid, 'cid'); + + if (!cid || !tid || !utils.isNumber(order) || order < 0) { + throw new Error('[[error:invalid-data]]'); + } + + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); + const currentIndex = pinnedTids.indexOf(String(tid)); + if (currentIndex === -1) { + return; + } + + const newOrder = pinnedTids.length - order - 1; + // Move tid to the specified order + if (pinnedTids.length > 1) { + pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); + } + + await db.sortedSetAddBulk( + `cid:${cid}:tids:pinned`, + pinnedTids.map((tid, index) => index), + pinnedTids + ); + + return pinnedTids; +}; + +module.exports = Pin; From 73044bec5febbaca35a304bacda066e46958eba8 Mon Sep 17 00:00:00 2001 From: Sofian Syed Date: Sat, 21 Sep 2024 23:47:02 -0400 Subject: [PATCH 2/7] Changes made to pin, postTools, and posts files but no functoinal changes --- public/src/client/topic/postTools.js | 17 +++++ public/src/client/topic/posts.js | 11 +++ src/topics/pin.js | 100 ++++----------------------- 3 files changed, 40 insertions(+), 88 deletions(-) diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index f8d2ca8933..595000129a 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -266,6 +266,23 @@ define('forum/topic/postTools', [ postContainer.on('click', '[component="post/chat"]', function () { openChat($(this)); }); + + // This is added logic that is done in order to pin a post when we click watched + // THis should be changed to be done when we click on the pin button when implemented. + postContainer.on('click', '[component="topic/watched"]', function () { + const tid = $(this).closest('[data-tid]').data('tid'); + const isPinned = $(this).hasClass('pinned'); + + socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { + if (err) { + app.alertError(err.message); + } else { + $(this).toggleClass('pinned', !isPinned); + app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); + } + }.bind(this)); + }); + } async function onReplyClicked(button, tid) { diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index c800704b79..4b57a564c3 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -34,6 +34,17 @@ define('forum/topic/posts', [ data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1000; data.posts[0].timestampISO = utils.toISOString(data.posts[0].timestamp); + // This next code block is added for making it + const isPinned = data.posts[0].pinned; + const tid = data.posts[0].tid; + socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { + if (err) { + app.alertError(err.message); + } else { + app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); + } + }); + Posts.modifyPostsByPrivileges(data.posts); updatePostCounts(data.posts); diff --git a/src/topics/pin.js b/src/topics/pin.js index 3d1b535f39..13f07b5d42 100644 --- a/src/topics/pin.js +++ b/src/topics/pin.js @@ -11,31 +11,33 @@ const Pin = {}; // Toggle Pin/Unpin a topic Pin.togglePin = async function (tid, uid, pin) { - // Fetch topic data - const topicData = await Topics.getTopicData(tid); + const topicData = await topics.getTopicData(tid); // Use lowercase 'topics' if (!topicData) { throw new Error('[[error:no-topic]]'); } - if (topic.scheduled) { + if (topicData.scheduled) { throw new Error('[[error:cant-pin-scheduled]]'); } - if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) { + const isAdminOrMod = uid === 'system' || await privileges.topics.isAdminOrMod(tid, uid); + if (!isAdminOrMod) { throw new Error('[[error:no-privileges]]'); } - if (pin && topic.pinned) { + if (pin && topicData.pinned) { throw new Error('[[error:topic-already-pinned]]'); - } else if (!pin && !topic.pinned) { + } else if (!pin && !topicData.pinned) { throw new Error('[[error:topic-not-pinned]]'); } // Update pin status in the database const promises = [ - Topics.setTopicField(tid, 'pinned', pin ? 1 : 0), - Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }), + topics.setTopicField(tid, 'pinned', pin ? 1 : 0), + event.emit(pin ? 'topic.pinned' : 'topic.unpinned', { tid, uid }), + plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }), ]; + if (pin) { promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); promises.push(db.sortedSetsRemove([ @@ -47,7 +49,7 @@ Pin.togglePin = async function (tid, uid, pin) { ], tid)); } else { promises.push(db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid)); - promises.push(Topics.deleteTopicField(tid, 'pinExpiry')); + promises.push(topics.deleteTopicField(tid, 'pinExpiry')); promises.push(db.sortedSetAddBulk([ [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], @@ -59,11 +61,7 @@ Pin.togglePin = async function (tid, uid, pin) { topicData.pinExpiryISO = undefined; } - const results = await Promise.all(promises); - - // Emit appropriate event - event.emit(pin ? 'topic.pinned' : 'topic.unpinned', { tid, uid }); - plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }); + await Promise.all(promises); return { tid, pinned: pin }; }; @@ -78,78 +76,4 @@ Pin.unpin = async function (tid, uid) { return await Pin.togglePin(tid, uid, false); }; -// Set pin expiry for a topic -Pin.setPinExpiry = async function (tid, expiry, uid) { - // Validate expiry date - if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { - throw new Error('[[error:invalid-data]]'); - } - - // Check privileges - const topic = await topics.getTopicFields(tid, ['cid', 'uid']); - const isAdminOrMod = await privileges.categories.isAdminOrMod(topic.cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - - // Set pin expiry in the database - await topics.setTopicField(tid, 'pinExpiry', expiry); - plugins.hooks.fire('action:topic.setPinExpiry', { topic, uid, expiry }); - - return { tid, expiry }; -}; - -// Check and expire pins -Pin.checkPinExpiry = async function (tids) { - const expiryDates = await topics.getTopicsFields(tids, ['pinExpiry']); - const now = Date.now(); - - // Check and unpin topics that have expired - const unpinPromises = expiryDates.map(async (topicExpiry, idx) => { - if (topicExpiry && parseInt(topicExpiry.pinExpiry, 10) <= now) { - await Pin.unpin(tids[idx], 'system'); - return null; - } - return tids[idx]; - }); - - const filteredTids = (await Promise.all(unpinPromises)).filter(Boolean); - return filteredTids; -}; - -// Order pinned topics -Pin.orderPinnedTopics = async function (uid, data) { - const { tid, order } = data; - const cid = await topics.getTopicField(tid, 'cid'); - - if (!cid || !tid || !utils.isNumber(order) || order < 0) { - throw new Error('[[error:invalid-data]]'); - } - - const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - - const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); - const currentIndex = pinnedTids.indexOf(String(tid)); - if (currentIndex === -1) { - return; - } - - const newOrder = pinnedTids.length - order - 1; - // Move tid to the specified order - if (pinnedTids.length > 1) { - pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); - } - - await db.sortedSetAddBulk( - `cid:${cid}:tids:pinned`, - pinnedTids.map((tid, index) => index), - pinnedTids - ); - - return pinnedTids; -}; - module.exports = Pin; From b7f0899ce412578e54291762cbb14e212eeca4cb Mon Sep 17 00:00:00 2001 From: Sofian Syed Date: Sun, 22 Sep 2024 22:38:33 -0400 Subject: [PATCH 3/7] Comments and small changes for debugging for pin backend files --- public/src/client/topic/postTools.js | 17 +++++++++++++++-- public/src/client/topic/posts.js | 13 +++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 595000129a..c1a465d3ad 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -267,21 +267,34 @@ define('forum/topic/postTools', [ openChat($(this)); }); - // This is added logic that is done in order to pin a post when we click watched - // THis should be changed to be done when we click on the pin button when implemented. + // This code block is added to pinning or unpinning a post when we click the watched button, + // but this should be updated to trigger when the actual pin button is implemented. + // Listen for a click event on the button within the post container. + // Note: "component='topic/watched'" is used to identify the watched button. postContainer.on('click', '[component="topic/watched"]', function () { + + // Get the topic tid of post. const tid = $(this).closest('[data-tid]').data('tid'); + + // Determine whether the post is currently pinned. const isPinned = $(this).hasClass('pinned'); + // Emit a 'topics.pin' event to the server to toggle the pin state of the topic. socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { + + // Error message for privaledges. if (err) { app.alertError(err.message); } else { + // This visually updates the post to reflect its pinned or unpinned state. $(this).toggleClass('pinned', !isPinned); + + // Success message app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); } }.bind(this)); }); + } diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index 4b57a564c3..6c084553ab 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -30,12 +30,17 @@ define('forum/topic/posts', [ data.loggedIn = !!app.user.uid; data.privileges = ajaxify.data.privileges; - // if not a scheduled topic, prevent timeago in future by setting timestamp to 1 sec behind now - data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1000; + // If the topic is not scheduled, adjust the timestamp to avoid future dates. + // This ensures that the post appears as if it was created slightly in the past. + // The timestamp is set 1 second behind the current time to prevent the timeago + // Plugin from showing it as being in the future. + data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1; + // Convert the updated timestamp into ISO format to be used for display in the UI. data.posts[0].timestampISO = utils.toISOString(data.posts[0].timestamp); - // This next code block is added for making it - const isPinned = data.posts[0].pinned; + // This block was added to manage the pin/unpin functionality for the topic. + // We extract the current 'pinned' state of the post and tid to send the + // appropriate pin/unpin action via socket. const tid = data.posts[0].tid; socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { if (err) { From ae0851ed42ca404649a930804a79222d3c79f335 Mon Sep 17 00:00:00 2001 From: Sofian Syed Date: Tue, 24 Sep 2024 14:22:12 -0400 Subject: [PATCH 4/7] pin.js file added in src/socket.io/topic directory to handle events in backend and debugging attempts for existing pin backend logic --- public/src/client/topic/postTools.js | 25 ++++------- public/src/client/topic/posts.js | 16 +++---- src/socket.io/topics/pin.js | 30 ++++++++++++++ src/topics/pin.js | 62 ++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/socket.io/topics/pin.js diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index c1a465d3ad..ce0223d26b 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -267,34 +267,25 @@ define('forum/topic/postTools', [ openChat($(this)); }); - // This code block is added to pinning or unpinning a post when we click the watched button, - // but this should be updated to trigger when the actual pin button is implemented. - // Listen for a click event on the button within the post container. - // Note: "component='topic/watched'" is used to identify the watched button. - postContainer.on('click', '[component="topic/watched"]', function () { - - // Get the topic tid of post. + // This code block is added to pinning or unpinning a post when we click it + // Listen for a click event on the button within the post container. sends message to server + postContainer.on('click', '[component="post/pin-button"]', function () { const tid = $(this).closest('[data-tid]').data('tid'); - - // Determine whether the post is currently pinned. const isPinned = $(this).hasClass('pinned'); - - // Emit a 'topics.pin' event to the server to toggle the pin state of the topic. + + console.log('Pin button clicked for tid:', tid, 'Current pin state:', isPinned); + socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { - - // Error message for privaledges. if (err) { + console.error('Error while pinning topic:', err.message); app.alertError(err.message); } else { - // This visually updates the post to reflect its pinned or unpinned state. + console.log('Successfully toggled pin for tid:', tid); $(this).toggleClass('pinned', !isPinned); - - // Success message app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); } }.bind(this)); }); - } diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index 6c084553ab..4ca628dabf 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -30,17 +30,13 @@ define('forum/topic/posts', [ data.loggedIn = !!app.user.uid; data.privileges = ajaxify.data.privileges; - // If the topic is not scheduled, adjust the timestamp to avoid future dates. - // This ensures that the post appears as if it was created slightly in the past. - // The timestamp is set 1 second behind the current time to prevent the timeago - // Plugin from showing it as being in the future. - data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1; - // Convert the updated timestamp into ISO format to be used for display in the UI. + // if not a scheduled topic, prevent timeago in future by setting timestamp to 1 sec behind now + data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1000; data.posts[0].timestampISO = utils.toISOString(data.posts[0].timestamp); - // This block was added to manage the pin/unpin functionality for the topic. - // We extract the current 'pinned' state of the post and tid to send the - // appropriate pin/unpin action via socket. + // Handling the pin status of the post dynamically and the loading and rendering of posts + // when new messages are sent from the server + const isPinned = data.posts[0].pinned; const tid = data.posts[0].tid; socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { if (err) { @@ -49,7 +45,7 @@ define('forum/topic/posts', [ app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); } }); - + Posts.modifyPostsByPrivileges(data.posts); updatePostCounts(data.posts); diff --git a/src/socket.io/topics/pin.js b/src/socket.io/topics/pin.js new file mode 100644 index 0000000000..764c0f98fc --- /dev/null +++ b/src/socket.io/topics/pin.js @@ -0,0 +1,30 @@ +'use strict'; + +const topics = require('../../topics'); +const privileges = require('../../privileges'); +const Pin = require('../../pin'); +// I think we need to use this in some central socket file but don't know where that is. +module.exports = function (SocketTopics) { + // Handler for pinning/unpinning topics + SocketTopics.pinTopic = async function (socket, data) { + // Check if the user is authenticated + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + + // Validate data + if (!data || !data.tid || typeof data.pin === 'undefined') { + throw new Error('[[error:invalid-data]]'); + } + + // Check privileges + const isAdminOrMod = await privileges.topics.isAdminOrMod(data.tid, socket.uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + // Toggle pin/unpin + const result = await Pin.togglePin(data.tid, socket.uid, data.pin); + return { tid: data.tid, pinned: result.pinned }; + }; +}; diff --git a/src/topics/pin.js b/src/topics/pin.js index 13f07b5d42..5402ef312b 100644 --- a/src/topics/pin.js +++ b/src/topics/pin.js @@ -76,4 +76,66 @@ Pin.unpin = async function (tid, uid) { return await Pin.togglePin(tid, uid, false); }; +// Below are extra utility functions added by Dhanya which Sofian had initially deleted but may be useful. +// Set pin expiry for a topic +Pin.setPinExpiry = async function (tid, expiry, uid) { + // Validate expiry date + if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { + throw new Error('[[error:invalid-data]]'); + } + // Check privileges + const topic = await topics.getTopicFields(tid, ['cid', 'uid']); + const isAdminOrMod = await privileges.categories.isAdminOrMod(topic.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + // Set pin expiry in the database + await topics.setTopicField(tid, 'pinExpiry', expiry); + plugins.hooks.fire('action:topic.setPinExpiry', { topic, uid, expiry }); + return { tid, expiry }; +}; +// Check and expire pins +Pin.checkPinExpiry = async function (tids) { + const expiryDates = await topics.getTopicsFields(tids, ['pinExpiry']); + const now = Date.now(); + // Check and unpin topics that have expired + const unpinPromises = expiryDates.map(async (topicExpiry, idx) => { + if (topicExpiry && parseInt(topicExpiry.pinExpiry, 10) <= now) { + await Pin.unpin(tids[idx], 'system'); + return null; + } + return tids[idx]; + }); + const filteredTids = (await Promise.all(unpinPromises)).filter(Boolean); + return filteredTids; +}; +// Order pinned topics +Pin.orderPinnedTopics = async function (uid, data) { + const { tid, order } = data; + const cid = await topics.getTopicField(tid, 'cid'); + if (!cid || !tid || !utils.isNumber(order) || order < 0) { + throw new Error('[[error:invalid-data]]'); + } + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); + const currentIndex = pinnedTids.indexOf(String(tid)); + if (currentIndex === -1) { + return; + } + const newOrder = pinnedTids.length - order - 1; + // Move tid to the specified order + if (pinnedTids.length > 1) { + pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); + } + await db.sortedSetAddBulk( + `cid:${cid}:tids:pinned`, + pinnedTids.map((tid, index) => index), + pinnedTids + ); + return pinnedTids; +}; + module.exports = Pin; From 773a993b6696dc51438e6c35f3e7374ff7ef78a8 Mon Sep 17 00:00:00 2001 From: DhanyaShah Date: Wed, 9 Oct 2024 17:28:44 -0400 Subject: [PATCH 5/7] Changed Backend Logic+Fixed Linting Errors --- dump.rdb | Bin 62681 -> 132990 bytes nodebb-theme-quickstart | 1 - public/src/admin/manage/categories.js | 2 +- public/src/client/topic/postTools.js | 21 ---- public/src/client/topic/posts.js | 17 ---- src/controllers/mods.js | 1 - src/socket.io/topics/pin.js | 30 ------ src/topics/pin.js | 141 -------------------------- src/topics/tools.js | 118 ++++++++------------- 9 files changed, 42 insertions(+), 289 deletions(-) delete mode 160000 nodebb-theme-quickstart delete mode 100644 src/socket.io/topics/pin.js delete mode 100644 src/topics/pin.js diff --git a/dump.rdb b/dump.rdb index 4c10d67a4c5e0c6d8c26798123453f8fa74ce5ed..77181fa3a2bd6ccf9cc79d4c5d91029e10920514 100644 GIT binary patch literal 132990 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 delta 18346 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 h)Sj=h)Bj!qq3#H3uabpQJT0RmA`R diff --git a/nodebb-theme-quickstart b/nodebb-theme-quickstart deleted file mode 160000 index 7cb42621dc..0000000000 --- a/nodebb-theme-quickstart +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7cb42621dcb572bd2effd68d6672594e0702e0d2 diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index 78402944e2..65ecc94b56 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -318,4 +318,4 @@ define('admin/manage/categories', [ } return Categories; -}); \ No newline at end of file +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index ce0223d26b..f8d2ca8933 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -266,27 +266,6 @@ define('forum/topic/postTools', [ postContainer.on('click', '[component="post/chat"]', function () { openChat($(this)); }); - - // This code block is added to pinning or unpinning a post when we click it - // Listen for a click event on the button within the post container. sends message to server - postContainer.on('click', '[component="post/pin-button"]', function () { - const tid = $(this).closest('[data-tid]').data('tid'); - const isPinned = $(this).hasClass('pinned'); - - console.log('Pin button clicked for tid:', tid, 'Current pin state:', isPinned); - - socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { - if (err) { - console.error('Error while pinning topic:', err.message); - app.alertError(err.message); - } else { - console.log('Successfully toggled pin for tid:', tid); - $(this).toggleClass('pinned', !isPinned); - app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); - } - }.bind(this)); - }); - } async function onReplyClicked(button, tid) { diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index 4ca628dabf..bdb78b3908 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -29,23 +29,6 @@ define('forum/topic/posts', [ data.loggedIn = !!app.user.uid; data.privileges = ajaxify.data.privileges; - - // if not a scheduled topic, prevent timeago in future by setting timestamp to 1 sec behind now - data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1000; - data.posts[0].timestampISO = utils.toISOString(data.posts[0].timestamp); - - // Handling the pin status of the post dynamically and the loading and rendering of posts - // when new messages are sent from the server - const isPinned = data.posts[0].pinned; - const tid = data.posts[0].tid; - socket.emit('topics.pin', { tid: tid, pin: !isPinned }, function (err) { - if (err) { - app.alertError(err.message); - } else { - app.alertSuccess('Topic ' + (!isPinned ? 'pinned' : 'unpinned') + ' successfully'); - } - }); - Posts.modifyPostsByPrivileges(data.posts); updatePostCounts(data.posts); diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 05a4178160..f00a7f14e9 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -54,7 +54,6 @@ modsController.flags.list = async function (req, res) { const AdminModeratedCidVal = adminModCid(isAdminOrGlobalMod, moderatedCids.length); if ((!(isAdminOrGlobalMod || !!moderatedCids.length))) { - console.log('KAREN GONZALEZ'); return helpers.notAllowed(req, res); } diff --git a/src/socket.io/topics/pin.js b/src/socket.io/topics/pin.js deleted file mode 100644 index 764c0f98fc..0000000000 --- a/src/socket.io/topics/pin.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const topics = require('../../topics'); -const privileges = require('../../privileges'); -const Pin = require('../../pin'); -// I think we need to use this in some central socket file but don't know where that is. -module.exports = function (SocketTopics) { - // Handler for pinning/unpinning topics - SocketTopics.pinTopic = async function (socket, data) { - // Check if the user is authenticated - if (!socket.uid) { - throw new Error('[[error:no-privileges]]'); - } - - // Validate data - if (!data || !data.tid || typeof data.pin === 'undefined') { - throw new Error('[[error:invalid-data]]'); - } - - // Check privileges - const isAdminOrMod = await privileges.topics.isAdminOrMod(data.tid, socket.uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - - // Toggle pin/unpin - const result = await Pin.togglePin(data.tid, socket.uid, data.pin); - return { tid: data.tid, pinned: result.pinned }; - }; -}; diff --git a/src/topics/pin.js b/src/topics/pin.js deleted file mode 100644 index 5402ef312b..0000000000 --- a/src/topics/pin.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; - -const db = require('../database'); -const topics = require('./topics'); -const privileges = require('../privileges'); -const event = require('../event'); -const utils = require('../utils'); -const plugins = require('../plugins'); - -const Pin = {}; - -// Toggle Pin/Unpin a topic -Pin.togglePin = async function (tid, uid, pin) { - const topicData = await topics.getTopicData(tid); // Use lowercase 'topics' - if (!topicData) { - throw new Error('[[error:no-topic]]'); - } - - if (topicData.scheduled) { - throw new Error('[[error:cant-pin-scheduled]]'); - } - - const isAdminOrMod = uid === 'system' || await privileges.topics.isAdminOrMod(tid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - - if (pin && topicData.pinned) { - throw new Error('[[error:topic-already-pinned]]'); - } else if (!pin && !topicData.pinned) { - throw new Error('[[error:topic-not-pinned]]'); - } - - // Update pin status in the database - const promises = [ - topics.setTopicField(tid, 'pinned', pin ? 1 : 0), - event.emit(pin ? 'topic.pinned' : 'topic.unpinned', { tid, uid }), - plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }), - ]; - - if (pin) { - promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); - promises.push(db.sortedSetsRemove([ - `cid:${topicData.cid}:tids`, - `cid:${topicData.cid}:tids:create`, - `cid:${topicData.cid}:tids:posts`, - `cid:${topicData.cid}:tids:votes`, - `cid:${topicData.cid}:tids:views`, - ], tid)); - } else { - promises.push(db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid)); - promises.push(topics.deleteTopicField(tid, 'pinExpiry')); - promises.push(db.sortedSetAddBulk([ - [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], - [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], - [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], - [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], - [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], - ])); - topicData.pinExpiry = undefined; - topicData.pinExpiryISO = undefined; - } - - await Promise.all(promises); - - return { tid, pinned: pin }; -}; - -// Pin a topic -Pin.pin = async function (tid, uid) { - return await Pin.togglePin(tid, uid, true); -}; - -// Unpin a topic -Pin.unpin = async function (tid, uid) { - return await Pin.togglePin(tid, uid, false); -}; - -// Below are extra utility functions added by Dhanya which Sofian had initially deleted but may be useful. -// Set pin expiry for a topic -Pin.setPinExpiry = async function (tid, expiry, uid) { - // Validate expiry date - if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { - throw new Error('[[error:invalid-data]]'); - } - // Check privileges - const topic = await topics.getTopicFields(tid, ['cid', 'uid']); - const isAdminOrMod = await privileges.categories.isAdminOrMod(topic.cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - // Set pin expiry in the database - await topics.setTopicField(tid, 'pinExpiry', expiry); - plugins.hooks.fire('action:topic.setPinExpiry', { topic, uid, expiry }); - return { tid, expiry }; -}; -// Check and expire pins -Pin.checkPinExpiry = async function (tids) { - const expiryDates = await topics.getTopicsFields(tids, ['pinExpiry']); - const now = Date.now(); - // Check and unpin topics that have expired - const unpinPromises = expiryDates.map(async (topicExpiry, idx) => { - if (topicExpiry && parseInt(topicExpiry.pinExpiry, 10) <= now) { - await Pin.unpin(tids[idx], 'system'); - return null; - } - return tids[idx]; - }); - const filteredTids = (await Promise.all(unpinPromises)).filter(Boolean); - return filteredTids; -}; -// Order pinned topics -Pin.orderPinnedTopics = async function (uid, data) { - const { tid, order } = data; - const cid = await topics.getTopicField(tid, 'cid'); - if (!cid || !tid || !utils.isNumber(order) || order < 0) { - throw new Error('[[error:invalid-data]]'); - } - const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } - const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); - const currentIndex = pinnedTids.indexOf(String(tid)); - if (currentIndex === -1) { - return; - } - const newOrder = pinnedTids.length - order - 1; - // Move tid to the specified order - if (pinnedTids.length > 1) { - pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); - } - await db.sortedSetAddBulk( - `cid:${cid}:tids:pinned`, - pinnedTids.map((tid, index) => index), - pinnedTids - ); - return pinnedTids; -}; - -module.exports = Pin; diff --git a/src/topics/tools.js b/src/topics/tools.js index cadeb95563..390129dea3 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -8,7 +8,6 @@ const categories = require('../categories'); const user = require('../user'); const plugins = require('../plugins'); const privileges = require('../privileges'); -const utils = require('../utils'); module.exports = function (Topics) { @@ -110,126 +109,91 @@ module.exports = function (Topics) { return topicData; } - topicTools.pin = async function (tid, uid) { - return await togglePin(tid, uid, true); - }; + // Pin a topic + topicTools.pin = async (tid, uid) => togglePin(tid, uid, true); - topicTools.unpin = async function (tid, uid) { - return await togglePin(tid, uid, false); - }; + // Unpin a topic + topicTools.unpin = async (tid, uid) => togglePin(tid, uid, false); + // Set pin expiry topicTools.setPinExpiry = async (tid, expiry, uid) => { - if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { + if (isNaN(expiry) || expiry <= Date.now()) { throw new Error('[[error:invalid-data]]'); } - const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid']); - const isAdminOrMod = await privileges.categories.isAdminOrMod(topicData.cid, uid); + const { cid } = await Topics.getTopicFields(tid, ['cid']); + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); if (!isAdminOrMod) { throw new Error('[[error:no-privileges]]'); } await Topics.setTopicField(tid, 'pinExpiry', expiry); - plugins.hooks.fire('action:topic.setPinExpiry', { topic: _.clone(topicData), uid: uid }); + plugins.hooks.fire('action:topic.setPinExpiry', { tid, uid }); }; + // Check pin expiry topicTools.checkPinExpiry = async (tids) => { - const expiry = (await topics.getTopicsFields(tids, ['pinExpiry'])).map(obj => obj.pinExpiry); const now = Date.now(); + const expiry = await topics.getTopicsFields(tids, ['pinExpiry']); tids = await Promise.all(tids.map(async (tid, idx) => { - if (expiry[idx] && parseInt(expiry[idx], 10) <= now) { + if (expiry[idx] && expiry[idx] <= now) { await togglePin(tid, 'system', false); return null; } - return tid; })); return tids.filter(Boolean); }; + // Toggle pin state async function togglePin(tid, uid, pin) { - const topicData = await Topics.getTopicData(tid); - if (!topicData) { - throw new Error('[[error:no-topic]]'); - } + const { cid, scheduled, lastposttime, timestamp, postcount, votes, viewcount } = await Topics.getTopicData(tid); + if (!cid) throw new Error('[[error:no-topic]]'); + if (scheduled) throw new Error('[[error:cant-pin-scheduled]]'); + if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) throw new Error('[[error:no-privileges]]'); - if (topicData.scheduled) { - throw new Error('[[error:cant-pin-scheduled]]'); - } - - if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) { - throw new Error('[[error:no-privileges]]'); - } + await Topics.setTopicField(tid, 'pinned', pin ? 1 : 0); + Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }); - const promises = [ - Topics.setTopicField(tid, 'pinned', pin ? 1 : 0), - Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }), - ]; if (pin) { - promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); - promises.push(db.sortedSetsRemove([ - `cid:${topicData.cid}:tids`, - `cid:${topicData.cid}:tids:create`, - `cid:${topicData.cid}:tids:posts`, - `cid:${topicData.cid}:tids:votes`, - `cid:${topicData.cid}:tids:views`, - ], tid)); + await db.sortedSetAdd(`cid:${cid}:tids:pinned`, Date.now(), tid); + await db.sortedSetsRemove([`cid:${cid}:tids`, `cid:${cid}:tids:create`, `cid:${cid}:tids:posts`, `cid:${cid}:tids:votes`, `cid:${cid}:tids:views`], tid); } else { - promises.push(db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid)); - promises.push(Topics.deleteTopicField(tid, 'pinExpiry')); - promises.push(db.sortedSetAddBulk([ - [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], - [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], - [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], - [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], - [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], - ])); - topicData.pinExpiry = undefined; - topicData.pinExpiryISO = undefined; + await db.sortedSetRemove(`cid:${cid}:tids:pinned`, tid); + await db.sortedSetAddBulk([ + [`cid:${cid}:tids`, lastposttime, tid], + [`cid:${cid}:tids:create`, timestamp, tid], + [`cid:${cid}:tids:posts`, postcount, tid], + [`cid:${cid}:tids:votes`, votes || 0, tid], + [`cid:${cid}:tids:views`, viewcount, tid], + ]); + await Topics.deleteTopicField(tid, 'pinExpiry'); } - const results = await Promise.all(promises); - - topicData.isPinned = pin; // deprecate in v2.0 - topicData.pinned = pin; - topicData.events = results[1]; - - plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }); - - return topicData; + plugins.hooks.fire('action:topic.pin', { tid, uid }); + return { tid, pinned: pin }; } - topicTools.orderPinnedTopics = async function (uid, data) { - const { tid, order } = data; + // Order pinned topics + topicTools.orderPinnedTopics = async (uid, { tid, order }) => { const cid = await Topics.getTopicField(tid, 'cid'); - - if (!cid || !tid || !utils.isNumber(order) || order < 0) { - throw new Error('[[error:invalid-data]]'); - } + if (!cid || order < 0) throw new Error('[[error:invalid-data]]'); const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } + if (!isAdminOrMod) throw new Error('[[error:no-privileges]]'); const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); const currentIndex = pinnedTids.indexOf(String(tid)); - if (currentIndex === -1) { - return; - } - const newOrder = pinnedTids.length - order - 1; - // moves tid to index order in the array + if (currentIndex === -1) return; + + const newOrder = Math.max(0, pinnedTids.length - order - 1); if (pinnedTids.length > 1) { - pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); + pinnedTids.splice(newOrder, 0, pinnedTids.splice(currentIndex, 1)[0]); } - await db.sortedSetAdd( - `cid:${cid}:tids:pinned`, - pinnedTids.map((tid, index) => index), - pinnedTids - ); + await db.sortedSetAdd(`cid:${cid}:tids:pinned`, pinnedTids.map((_, index) => index), pinnedTids); }; topicTools.move = async function (tid, data) { From 2983337a6cd5355bfebb7e765d392772cfbea196 Mon Sep 17 00:00:00 2001 From: DhanyaShah Date: Thu, 10 Oct 2024 22:04:37 -0400 Subject: [PATCH 6/7] Cleaned up backend + completed test suite --- dump.rdb | Bin 132990 -> 120002 bytes src/topics/index.js | 20 ++++++- src/topics/tools.js | 126 +++++++++++++++++++++++++++++++------------- test/topics.js | 75 +++++++++++++++++++------- 4 files changed, 164 insertions(+), 57 deletions(-) diff --git a/dump.rdb b/dump.rdb index 77181fa3a2bd6ccf9cc79d4c5d91029e10920514..19e42a7cb7b43cb091575036d4708d1bb038b6d8 100644 GIT binary patch delta 18209 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 delta 25322 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 { + // if (a.pinned && !b.pinned) return -1; + // if (!a.pinned && b.pinned) return 1; + // // Default sort (e.g., by date) + // return b.lastposttime - a.lastposttime; + // }); + + Topics.calculateTopicIndices(topics, start); return { topics: topics, nextStart: stop + 1 }; }; @@ -53,7 +63,6 @@ Topics.getTopics = async function (tids, options) { if (typeof options === 'object') { uid = options.uid; } - tids = await privileges.topics.filterTids('topics:read', tids, uid); return await Topics.getTopicsByTids(tids, options); }; @@ -106,6 +115,15 @@ Topics.getTopicsByTids = async function (tids, options) { } }); + + // // Sort pinned topics to the top + // topics.sort((a, b) => { + // if (a.pinned && !b.pinned) return -1; + // if (!a.pinned && b.pinned) return 1; + // // Default sort (e.g., by last post time) + // return b.lastposttime - a.lastposttime; + // }); + return { topics, teasers, diff --git a/src/topics/tools.js b/src/topics/tools.js index 390129dea3..db083adf59 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -109,33 +109,80 @@ module.exports = function (Topics) { return topicData; } + const max_pinned = 5; + // Pin a topic topicTools.pin = async (tid, uid) => togglePin(tid, uid, true); // Unpin a topic topicTools.unpin = async (tid, uid) => togglePin(tid, uid, false); + // Toggle pin state + async function togglePin(tid, uid, pin) { + const { cid, scheduled } = await Topics.getTopicData(tid); + + // validate whether pinning is possible + if (!cid) throw new Error('[[error:no-topic]]'); + if (scheduled) throw new Error('[[error:cant-pin-scheduled]]'); + if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) throw new Error('[[error:no-privileges]]'); + + // Get the current number of pinned topics in the category + const pinnedTopicsCount = await db.sortedSetCard(`cid:${cid}:tids:pinned`); + if (pin && pinnedTopicsCount >= max_pinned) { + throw new Error(`[[error:max-pinned-limit-reached]]`); + } + + // Set the 'pinned' field for the topic and log action in event log + await Topics.setTopicField(tid, 'pinned', pin ? 1 : 0); + Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }); + + // Update database + if (pin) { + await pinActions(cid, tid); + } else { + await unpinActions(cid, tid); + } + + // Track count + if (pin) { + const pinCount = await Topics.getTopicField(tid, 'pinCount') || 0; + await Topics.setTopicField(tid, 'pinCount', pinCount + 1); + } + // Track pin history + await db.listAppend(`topic:${tid}:pinHistory`, JSON.stringify({ + uid, + action: pin ? 'pinned' : 'unpinned', + timestamp: Date.now(), + })); + // Update db with user who pinned topic + await Topics.setTopicField(tid, pin ? 'pinnedBy' : 'unpinnedBy', uid); + + // Trigger hook + plugins.hooks.fire('action:topic.pin', { tid, uid }); + return { tid, pinned: pin }; + } + // Set pin expiry topicTools.setPinExpiry = async (tid, expiry, uid) => { + // Ensure timestamp is valid if (isNaN(expiry) || expiry <= Date.now()) { throw new Error('[[error:invalid-data]]'); } - const { cid } = await Topics.getTopicFields(tid, ['cid']); - const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); - if (!isAdminOrMod) { - throw new Error('[[error:no-privileges]]'); - } + await checkAdminOrModPrivileges(cid, uid); + // Set pin exipiry and trigger hook for the same await Topics.setTopicField(tid, 'pinExpiry', expiry); plugins.hooks.fire('action:topic.setPinExpiry', { tid, uid }); }; // Check pin expiry topicTools.checkPinExpiry = async (tids) => { + // Get current timstamp and expirty time for tids const now = Date.now(); const expiry = await topics.getTopicsFields(tids, ['pinExpiry']); + // Check if any topics have expired - if so unpin it tids = await Promise.all(tids.map(async (tid, idx) => { if (expiry[idx] && expiry[idx] <= now) { await togglePin(tid, 'system', false); @@ -144,58 +191,65 @@ module.exports = function (Topics) { return tid; })); + // Filter out unpinned topics return tids.filter(Boolean); }; - // Toggle pin state - async function togglePin(tid, uid, pin) { - const { cid, scheduled, lastposttime, timestamp, postcount, votes, viewcount } = await Topics.getTopicData(tid); - if (!cid) throw new Error('[[error:no-topic]]'); - if (scheduled) throw new Error('[[error:cant-pin-scheduled]]'); - if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) throw new Error('[[error:no-privileges]]'); - - await Topics.setTopicField(tid, 'pinned', pin ? 1 : 0); - Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }); - - if (pin) { - await db.sortedSetAdd(`cid:${cid}:tids:pinned`, Date.now(), tid); - await db.sortedSetsRemove([`cid:${cid}:tids`, `cid:${cid}:tids:create`, `cid:${cid}:tids:posts`, `cid:${cid}:tids:votes`, `cid:${cid}:tids:views`], tid); - } else { - await db.sortedSetRemove(`cid:${cid}:tids:pinned`, tid); - await db.sortedSetAddBulk([ - [`cid:${cid}:tids`, lastposttime, tid], - [`cid:${cid}:tids:create`, timestamp, tid], - [`cid:${cid}:tids:posts`, postcount, tid], - [`cid:${cid}:tids:votes`, votes || 0, tid], - [`cid:${cid}:tids:views`, viewcount, tid], - ]); - await Topics.deleteTopicField(tid, 'pinExpiry'); - } - - plugins.hooks.fire('action:topic.pin', { tid, uid }); - return { tid, pinned: pin }; - } - // Order pinned topics topicTools.orderPinnedTopics = async (uid, { tid, order }) => { + // Get category of the topic const cid = await Topics.getTopicField(tid, 'cid'); if (!cid || order < 0) throw new Error('[[error:invalid-data]]'); - const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); - if (!isAdminOrMod) throw new Error('[[error:no-privileges]]'); + await checkAdminOrModPrivileges(cid, uid); + // Get current order of topics const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); const currentIndex = pinnedTids.indexOf(String(tid)); if (currentIndex === -1) return; + // Calculate new order position const newOrder = Math.max(0, pinnedTids.length - order - 1); if (pinnedTids.length > 1) { pinnedTids.splice(newOrder, 0, pinnedTids.splice(currentIndex, 1)[0]); } + // Only reorder if necessary + if (currentIndex !== newOrder && pinnedTids.length > 1) { + const [movedTid] = pinnedTids.splice(currentIndex, 1); + pinnedTids.splice(newOrder, 0, movedTid); + } + + // Update pinned topics list with new order await db.sortedSetAdd(`cid:${cid}:tids:pinned`, pinnedTids.map((_, index) => index), pinnedTids); }; + async function pinActions(cid, tid) { + await db.sortedSetAdd(`cid:${cid}:tids:pinned`, Date.now(), tid); + await db.sortedSetsRemove([`cid:${cid}:tids`, `cid:${cid}:tids:create`, `cid:${cid}:tids:posts`, `cid:${cid}:tids:votes`, `cid:${cid}:tids:views`], tid); + } + + async function unpinActions(cid, tid) { + const { lastposttime, timestamp, postcount, votes, viewcount } = await Topics.getTopicData(tid); + await db.sortedSetRemove(`cid:${cid}:tids:pinned`, tid); + await db.sortedSetAddBulk([ + [`cid:${cid}:tids`, lastposttime, tid], + [`cid:${cid}:tids:create`, timestamp, tid], + [`cid:${cid}:tids:posts`, postcount, tid], + [`cid:${cid}:tids:votes`, votes || 0, tid], + [`cid:${cid}:tids:views`, viewcount, tid], + ]); + await Topics.deleteTopicField(tid, 'pinExpiry'); + } + + // Helper function to check admin or moderator privileges + async function checkAdminOrModPrivileges(cid, uid) { + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + } + topicTools.move = async function (tid, data) { const cid = parseInt(data.cid, 10); const topicData = await Topics.getTopicData(tid); diff --git a/test/topics.js b/test/topics.js index 8a32e445f5..6d667f3bbd 100644 --- a/test/topics.js +++ b/test/topics.js @@ -699,6 +699,48 @@ describe('Topic\'s', () => { const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); assert.strictEqual(pinned, 0); }); + + it('should persist pinned topics after filtering or searching', async () => { + await topics.tools.pin(newTopic.tid, adminUid); + const searchResults = await topics.search(topic.tid, 'test'); + const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); + assert.strictEqual(pinned, 1); // Ensure topic remains pinned after search + }); + + const jar = request.jar(); + + it('should persist pinned topics across sessions', async () => { + await apiTopics.pin({ uid: adminUid }, { tids: [newTopic.tid], cid: categoryObj.cid }); + await helpers.loginUser('admin', '123456'); + await helpers.logoutUser(jar); + const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); + assert.strictEqual(pinned, 1); // Ensure topic remains pinned after logging out and back in + }); + + it('should persist pinned topics across different accounts', async () => { + await apiTopics.pin({ uid: adminUid }, { tids: [newTopic.tid], cid: categoryObj.cid }); + await helpers.logoutUser(jar); + await helpers.loginUser(fooUid); + const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); + assert.strictEqual(pinned, 1); // Ensure topic remains pinned for other users + }); + + it('should restrict pin access for regular user', async () => { + try { + await apiTopics.pin({ uid: fooUid }, { tids: [newTopic.tid], cid: categoryObj.cid }); + assert.fail('Regular user should not be able to pin topics'); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-privileges]]'); + } + }); + + it('should unpin topic after expiry', async () => { + const expiry = Date.now() + 1000; // 1 second expiry + await topics.tools.setPinExpiry(newTopic.tid, expiry, adminUid); + await sleep(1100); // Wait for expiry + const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); + assert.strictEqual(pinned, 1); + }); it('should move all topics', (done) => { socketTopics.moveAll({ uid: adminUid }, { cid: moveCid, currentCid: categoryObj.cid }, (err) => { @@ -853,14 +895,14 @@ describe('Topic\'s', () => { }); const socketTopics = require('../src/socket.io/topics'); - it('should error with invalid data', (done) => { + it('should error with null data', (done) => { socketTopics.orderPinnedTopics({ uid: adminUid }, null, (err) => { assert.equal(err.message, '[[error:invalid-data]]'); done(); }); }); - it('should error with invalid data', (done) => { + it('should error with array of nulls', (done) => { socketTopics.orderPinnedTopics({ uid: adminUid }, [null, null], (err) => { assert.equal(err.message, '[[error:invalid-data]]'); done(); @@ -873,7 +915,7 @@ describe('Topic\'s', () => { done(); }); }); - + it('should not do anything if topics are not pinned', (done) => { socketTopics.orderPinnedTopics({ uid: adminUid }, { tid: tid3, order: 1 }, (err) => { assert.ifError(err); @@ -885,25 +927,18 @@ describe('Topic\'s', () => { }); }); - it('should order pinned topics', (done) => { - db.getSortedSetRevRange(`cid:${topic.categoryId}:tids:pinned`, 0, -1, (err, pinnedTids) => { - assert.ifError(err); - assert.equal(pinnedTids[0], tid2); - assert.equal(pinnedTids[1], tid1); - socketTopics.orderPinnedTopics({ uid: adminUid }, { tid: tid1, order: 0 }, (err) => { - assert.ifError(err); - db.getSortedSetRevRange(`cid:${topic.categoryId}:tids:pinned`, 0, -1, (err, pinnedTids) => { - assert.ifError(err); - assert.equal(pinnedTids[0], tid1); - assert.equal(pinnedTids[1], tid2); - done(); - }); - }); - }); + it('should order pinned topics with latest pinned on top', async () => { + // Unpin any existing pinned topics first + const pinnedTopics = await db.getSortedSetRange(`cid:${categoryObj.cid}:tids:pinned`, 0, -1); + await Promise.all(pinnedTopics.map(tid => topics.tools.unpin(tid, adminUid))); + + // Now pin the topic and check if it's on top + await topics.tools.pin(tid3, adminUid); + const updatedPinnedTopics = await db.getSortedSetRange(`cid:${categoryObj.cid}:tids:pinned`, 0, -1); + assert.strictEqual(updatedPinnedTopics[0], tid3.toString()); // tid3 should be on top }); }); - describe('.ignore', () => { let newTid; let uid; @@ -2502,7 +2537,7 @@ describe('Topic\'s', () => { it('should remove from topics:scheduled on purge', async () => { const score = await db.sortedSetScore('topics:scheduled', topicData.tid); assert(!score); - }); + }); }); }); From 9bfe597652d587fec2ee263300bde0f4dd91cde9 Mon Sep 17 00:00:00 2001 From: DhanyaShah Date: Thu, 10 Oct 2024 23:09:01 -0400 Subject: [PATCH 7/7] fixed Linting errors --- src/topics/index.js | 20 -------------------- src/topics/tools.js | 10 +++++----- test/topics.js | 8 +------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/topics/index.js b/src/topics/index.js index ff8a5f3227..5137a1737e 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -44,16 +44,6 @@ Topics.exists = async function (tids) { Topics.getTopicsFromSet = async function (set, uid, start, stop) { const tids = await db.getSortedSetRevRange(set, start, stop); const topics = await Topics.getTopics(tids, uid); - - // Sort pinned posts to the top - // topics.sort((a, b) => { - // if (a.pinned && !b.pinned) return -1; - // if (!a.pinned && b.pinned) return 1; - // // Default sort (e.g., by date) - // return b.lastposttime - a.lastposttime; - // }); - - Topics.calculateTopicIndices(topics, start); return { topics: topics, nextStart: stop + 1 }; }; @@ -114,16 +104,6 @@ Topics.getTopicsByTids = async function (tids, options) { userObj.fullname = undefined; } }); - - - // // Sort pinned topics to the top - // topics.sort((a, b) => { - // if (a.pinned && !b.pinned) return -1; - // if (!a.pinned && b.pinned) return 1; - // // Default sort (e.g., by last post time) - // return b.lastposttime - a.lastposttime; - // }); - return { topics, teasers, diff --git a/src/topics/tools.js b/src/topics/tools.js index db083adf59..e092d7680e 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -233,11 +233,11 @@ module.exports = function (Topics) { const { lastposttime, timestamp, postcount, votes, viewcount } = await Topics.getTopicData(tid); await db.sortedSetRemove(`cid:${cid}:tids:pinned`, tid); await db.sortedSetAddBulk([ - [`cid:${cid}:tids`, lastposttime, tid], - [`cid:${cid}:tids:create`, timestamp, tid], - [`cid:${cid}:tids:posts`, postcount, tid], - [`cid:${cid}:tids:votes`, votes || 0, tid], - [`cid:${cid}:tids:views`, viewcount, tid], + [`cid:${cid}:tids`, lastposttime, tid], + [`cid:${cid}:tids:create`, timestamp, tid], + [`cid:${cid}:tids:posts`, postcount, tid], + [`cid:${cid}:tids:votes`, votes || 0, tid], + [`cid:${cid}:tids:views`, viewcount, tid], ]); await Topics.deleteTopicField(tid, 'pinExpiry'); } diff --git a/test/topics.js b/test/topics.js index 55f30b18d7..0390607cc8 100644 --- a/test/topics.js +++ b/test/topics.js @@ -699,7 +699,6 @@ describe('Topic\'s', () => { const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); assert.strictEqual(pinned, 0); }); - it('should persist pinned topics after filtering or searching', async () => { await topics.tools.pin(newTopic.tid, adminUid); const searchResults = await topics.search(topic.tid, 'test'); @@ -708,7 +707,6 @@ describe('Topic\'s', () => { }); const jar = request.jar(); - it('should persist pinned topics across sessions', async () => { await apiTopics.pin({ uid: adminUid }, { tids: [newTopic.tid], cid: categoryObj.cid }); await helpers.loginUser('admin', '123456'); @@ -716,7 +714,6 @@ describe('Topic\'s', () => { const pinned = await topics.getTopicField(newTopic.tid, 'pinned'); assert.strictEqual(pinned, 1); // Ensure topic remains pinned after logging out and back in }); - it('should persist pinned topics across different accounts', async () => { await apiTopics.pin({ uid: adminUid }, { tids: [newTopic.tid], cid: categoryObj.cid }); await helpers.logoutUser(jar); @@ -733,7 +730,6 @@ describe('Topic\'s', () => { assert.strictEqual(err.message, '[[error:no-privileges]]'); } }); - it('should unpin topic after expiry', async () => { const expiry = Date.now() + 1000; // 1 second expiry await topics.tools.setPinExpiry(newTopic.tid, expiry, adminUid); @@ -935,7 +931,6 @@ describe('Topic\'s', () => { done(); }); }); - it('should not do anything if topics are not pinned', (done) => { socketTopics.orderPinnedTopics({ uid: adminUid }, { tid: tid3, order: 1 }, (err) => { assert.ifError(err); @@ -951,7 +946,6 @@ describe('Topic\'s', () => { // Unpin any existing pinned topics first const pinnedTopics = await db.getSortedSetRange(`cid:${categoryObj.cid}:tids:pinned`, 0, -1); await Promise.all(pinnedTopics.map(tid => topics.tools.unpin(tid, adminUid))); - // Now pin the topic and check if it's on top await topics.tools.pin(tid3, adminUid); const updatedPinnedTopics = await db.getSortedSetRange(`cid:${categoryObj.cid}:tids:pinned`, 0, -1); @@ -2557,7 +2551,7 @@ describe('Topic\'s', () => { it('should remove from topics:scheduled on purge', async () => { const score = await db.sortedSetScore('topics:scheduled', topicData.tid); assert(!score); - }); + }); }); });