mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: add tools to recent/unread (#8477)
* feat: add tools to recent/unread * fix: open api spec * fix: more api spec
This commit is contained in:
committed by
GitHub
parent
14eafcb6b8
commit
658dd03b03
@@ -3810,6 +3810,10 @@ paths:
|
||||
type: number
|
||||
canPost:
|
||||
type: boolean
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
@@ -3871,6 +3875,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selectedFilter:
|
||||
type: object
|
||||
properties:
|
||||
@@ -3882,6 +3888,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
terms:
|
||||
type: array
|
||||
items:
|
||||
@@ -3946,6 +3954,8 @@ paths:
|
||||
properties:
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
nextStart:
|
||||
type: number
|
||||
topics:
|
||||
@@ -4199,6 +4209,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selectedFilter:
|
||||
type: object
|
||||
properties:
|
||||
@@ -4210,6 +4222,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
- $ref: components/schemas/Pagination.yaml#/Pagination
|
||||
- $ref: components/schemas/Breadcrumbs.yaml#/Breadcrumbs
|
||||
- $ref: components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -5492,6 +5506,10 @@ paths:
|
||||
type: number
|
||||
canPost:
|
||||
type: boolean
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
@@ -5553,6 +5571,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selectedFilter:
|
||||
type: object
|
||||
properties:
|
||||
@@ -5564,6 +5584,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
terms:
|
||||
type: array
|
||||
items:
|
||||
@@ -5620,6 +5642,10 @@ paths:
|
||||
type: number
|
||||
canPost:
|
||||
type: boolean
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
@@ -5694,6 +5720,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selectedFilter:
|
||||
type: object
|
||||
properties:
|
||||
@@ -5705,6 +5733,8 @@ paths:
|
||||
type: boolean
|
||||
filter:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
terms:
|
||||
type: array
|
||||
items:
|
||||
@@ -5817,6 +5847,8 @@ paths:
|
||||
type: boolean
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
rssFeedUrl:
|
||||
type: string
|
||||
feeds:disableRSS:
|
||||
|
||||
@@ -4,25 +4,17 @@ define('forum/category', [
|
||||
'forum/infinitescroll',
|
||||
'share',
|
||||
'navigator',
|
||||
'forum/category/tools',
|
||||
'topicList',
|
||||
'sort',
|
||||
], function (infinitescroll, share, navigator, categoryTools, topicList, sort) {
|
||||
], function (infinitescroll, share, navigator, topicList, sort) {
|
||||
var Category = {};
|
||||
|
||||
$(window).on('action:ajaxify.start', function (ev, data) {
|
||||
if (!String(data.url).startsWith('category/')) {
|
||||
navigator.disable();
|
||||
|
||||
removeListeners();
|
||||
}
|
||||
});
|
||||
|
||||
function removeListeners() {
|
||||
categoryTools.removeListeners();
|
||||
topicList.removeListeners();
|
||||
}
|
||||
|
||||
Category.init = function () {
|
||||
var cid = ajaxify.data.cid;
|
||||
|
||||
@@ -30,8 +22,6 @@ define('forum/category', [
|
||||
|
||||
share.addShareHandlers(ajaxify.data.name);
|
||||
|
||||
categoryTools.init(cid);
|
||||
|
||||
topicList.init('category', loadTopicsAfter);
|
||||
|
||||
sort.handleSort('categoryTopicSort', 'user.setCategorySort', 'category/' + ajaxify.data.slug);
|
||||
|
||||
@@ -9,9 +9,7 @@ define('forum/category/tools', [
|
||||
], function (topicSelect, components, translator) {
|
||||
var CategoryTools = {};
|
||||
|
||||
CategoryTools.init = function (cid) {
|
||||
CategoryTools.cid = cid;
|
||||
|
||||
CategoryTools.init = function () {
|
||||
topicSelect.init(updateDropdownOptions);
|
||||
|
||||
handlePinnedTopicSort();
|
||||
@@ -36,7 +34,7 @@ define('forum/category/tools', [
|
||||
if (!tids.length) {
|
||||
return app.alertError('[[error:no-topics-selected]]');
|
||||
}
|
||||
socket.emit('topics.lock', { tids: tids, cid: CategoryTools.cid }, onCommandComplete);
|
||||
socket.emit('topics.lock', { tids: tids }, onCommandComplete);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -45,7 +43,7 @@ define('forum/category/tools', [
|
||||
if (!tids.length) {
|
||||
return app.alertError('[[error:no-topics-selected]]');
|
||||
}
|
||||
socket.emit('topics.unlock', { tids: tids, cid: CategoryTools.cid }, onCommandComplete);
|
||||
socket.emit('topics.unlock', { tids: tids }, onCommandComplete);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -54,7 +52,7 @@ define('forum/category/tools', [
|
||||
if (!tids.length) {
|
||||
return app.alertError('[[error:no-topics-selected]]');
|
||||
}
|
||||
socket.emit('topics.pin', { tids: tids, cid: CategoryTools.cid }, onCommandComplete);
|
||||
socket.emit('topics.pin', { tids: tids }, onCommandComplete);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -63,7 +61,7 @@ define('forum/category/tools', [
|
||||
if (!tids.length) {
|
||||
return app.alertError('[[error:no-topics-selected]]');
|
||||
}
|
||||
socket.emit('topics.unpin', { tids: tids, cid: CategoryTools.cid }, onCommandComplete);
|
||||
socket.emit('topics.unpin', { tids: tids }, onCommandComplete);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -92,13 +90,17 @@ define('forum/category/tools', [
|
||||
if (!tids.length) {
|
||||
return app.alertError('[[error:no-topics-selected]]');
|
||||
}
|
||||
move.init(tids, cid, onCommandComplete);
|
||||
move.init(tids, null, onCommandComplete);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
components.get('topic/move-all').on('click', function () {
|
||||
var cid = ajaxify.data.cid;
|
||||
if (!ajaxify.data.template.category) {
|
||||
return app.alertError('[[error:invalid-data]]');
|
||||
}
|
||||
require(['forum/topic/move'], function (move) {
|
||||
move.init(null, cid, function (err) {
|
||||
if (err) {
|
||||
@@ -110,7 +112,7 @@ define('forum/category/tools', [
|
||||
});
|
||||
});
|
||||
|
||||
$('.category').on('click', '[component="topic/merge"]', function () {
|
||||
components.get('topic/merge').on('click', function () {
|
||||
require(['forum/topic/merge'], function (merge) {
|
||||
merge.init();
|
||||
});
|
||||
@@ -138,7 +140,7 @@ define('forum/category/tools', [
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('topics.' + command, { tids: tids, cid: CategoryTools.cid }, onDeletePurgeComplete);
|
||||
socket.emit('topics.' + command, { tids: tids }, onDeletePurgeComplete);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -259,7 +261,7 @@ define('forum/category/tools', [
|
||||
return memo;
|
||||
}, 0);
|
||||
|
||||
if (!ajaxify.data.privileges.isAdminOrMod || numPinned < 2) {
|
||||
if ((!app.user.isAdmin && !app.user.isMod) || numPinned < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ define('topicList', [
|
||||
'handleBack',
|
||||
'topicSelect',
|
||||
'categorySearch',
|
||||
], function (infinitescroll, handleBack, topicSelect, categorySearch) {
|
||||
'forum/category/tools',
|
||||
], function (infinitescroll, handleBack, topicSelect, categorySearch, categoryTools) {
|
||||
var TopicList = {};
|
||||
var templateName = '';
|
||||
|
||||
@@ -24,6 +25,7 @@ define('topicList', [
|
||||
|
||||
$(window).on('action:ajaxify.start', function () {
|
||||
TopicList.removeListeners();
|
||||
categoryTools.removeListeners();
|
||||
});
|
||||
|
||||
TopicList.init = function (template, cb) {
|
||||
@@ -32,6 +34,8 @@ define('topicList', [
|
||||
templateName = template;
|
||||
loadTopicsCallback = cb || loadTopicsAfter;
|
||||
|
||||
categoryTools.init();
|
||||
|
||||
TopicList.watchForNewPosts();
|
||||
|
||||
TopicList.handleCategorySelection();
|
||||
|
||||
@@ -32,6 +32,9 @@ module.exports = function (Categories) {
|
||||
'cid:' + cid + ':tids',
|
||||
'cid:' + cid + ':tids:pinned',
|
||||
'cid:' + cid + ':tids:posts',
|
||||
'cid:' + cid + ':tids:votes',
|
||||
'cid:' + cid + ':tids:lastposttime',
|
||||
'cid:' + cid + ':recent_tids',
|
||||
'cid:' + cid + ':pids',
|
||||
'cid:' + cid + ':read_by_uid',
|
||||
'cid:' + cid + ':uid:watch:state',
|
||||
|
||||
@@ -95,6 +95,7 @@ categoryController.get = async function (req, res, next) {
|
||||
categoryData.description = translator.escape(categoryData.description);
|
||||
categoryData.privileges = userPrivileges;
|
||||
categoryData.showSelect = userPrivileges.editable;
|
||||
categoryData.showTopicTools = userPrivileges.editable;
|
||||
categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss';
|
||||
if (parseInt(req.uid, 10)) {
|
||||
categories.markAsRead([cid], req.uid);
|
||||
|
||||
@@ -72,21 +72,25 @@ helpers.buildFilters = function (url, filter, query) {
|
||||
url: url + helpers.buildQueryString(query.cid, '', query.term),
|
||||
selected: filter === '',
|
||||
filter: '',
|
||||
icon: 'fa-book',
|
||||
}, {
|
||||
name: '[[unread:new-topics]]',
|
||||
url: url + helpers.buildQueryString(query.cid, 'new', query.term),
|
||||
selected: filter === 'new',
|
||||
filter: 'new',
|
||||
icon: 'fa-clock-o',
|
||||
}, {
|
||||
name: '[[unread:watched-topics]]',
|
||||
url: url + helpers.buildQueryString(query.cid, 'watched', query.term),
|
||||
selected: filter === 'watched',
|
||||
filter: 'watched',
|
||||
icon: 'fa-bell-o',
|
||||
}, {
|
||||
name: '[[unread:unreplied-topics]]',
|
||||
url: url + helpers.buildQueryString(query.cid, 'unreplied', query.term),
|
||||
selected: filter === 'unreplied',
|
||||
filter: 'unreplied',
|
||||
icon: 'fa-reply',
|
||||
}];
|
||||
};
|
||||
|
||||
|
||||
@@ -37,11 +37,12 @@ recentController.getData = async function (req, url, sort) {
|
||||
states.push(categories.watchStates.ignoring);
|
||||
}
|
||||
|
||||
const [settings, categoryData, rssToken, canPost] = await Promise.all([
|
||||
const [settings, categoryData, rssToken, canPost, isPrivileged] = await Promise.all([
|
||||
user.getSettings(req.uid),
|
||||
helpers.getCategoriesByStates(req.uid, cid, states),
|
||||
user.auth.getFeedToken(req.uid),
|
||||
canPostTopic(req.uid),
|
||||
user.isPrivileged(req.uid),
|
||||
]);
|
||||
|
||||
const start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||
@@ -60,6 +61,8 @@ recentController.getData = async function (req, url, sort) {
|
||||
});
|
||||
|
||||
data.canPost = canPost;
|
||||
data.showSelect = isPrivileged;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = categoryData.categories;
|
||||
data.allCategoriesUrl = url + helpers.buildQueryString('', filter, '');
|
||||
data.selectedCategory = categoryData.selectedCategory || null;
|
||||
|
||||
@@ -22,9 +22,10 @@ unreadController.get = async function (req, res, next) {
|
||||
if (!filterData.filters[filter]) {
|
||||
return next();
|
||||
}
|
||||
const [watchedCategories, userSettings] = await Promise.all([
|
||||
const [watchedCategories, userSettings, isPrivileged] = await Promise.all([
|
||||
getWatchedCategories(req.uid, cid, filter),
|
||||
user.getSettings(req.uid),
|
||||
user.isPrivileged(req.uid),
|
||||
]);
|
||||
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
@@ -48,7 +49,8 @@ unreadController.get = async function (req, res, next) {
|
||||
req.query.page = Math.max(1, Math.min(data.pageCount, page));
|
||||
return helpers.redirect(res, '/unread?' + querystring.stringify(req.query));
|
||||
}
|
||||
|
||||
data.showSelect = isPrivileged;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = watchedCategories.categories;
|
||||
data.allCategoriesUrl = 'unread' + helpers.buildQueryString('', filter, '');
|
||||
data.selectedCategory = watchedCategories.selectedCategory;
|
||||
|
||||
@@ -191,9 +191,8 @@ SocketHelpers.rescindUpvoteNotification = async function (pid, fromuid) {
|
||||
websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
|
||||
};
|
||||
|
||||
SocketHelpers.emitToTopicAndCategory = function (event, data) {
|
||||
websockets.in('topic_' + data.tid).emit(event, data);
|
||||
websockets.in('category_' + data.cid).emit(event, data);
|
||||
SocketHelpers.emitToTopicAndCategory = async function (event, data, uids) {
|
||||
uids.forEach(toUid => websockets.in('uid_' + toUid).emit(event, data));
|
||||
};
|
||||
|
||||
require('../promisify')(SocketHelpers);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
const user = require('../../user');
|
||||
const topics = require('../../topics');
|
||||
const categories = require('../../categories');
|
||||
const privileges = require('../../privileges');
|
||||
@@ -12,6 +13,8 @@ module.exports = function (SocketTopics) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const uids = await user.getUidsFromSet('users:online', 0, -1);
|
||||
|
||||
await async.eachLimit(data.tids, 10, async function (tid) {
|
||||
const canMove = await privileges.topics.isAdminOrMod(tid, socket.uid);
|
||||
if (!canMove) {
|
||||
@@ -21,7 +24,8 @@ module.exports = function (SocketTopics) {
|
||||
data.uid = socket.uid;
|
||||
await topics.tools.move(tid, data);
|
||||
|
||||
socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData);
|
||||
const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids);
|
||||
socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData, notifyUids);
|
||||
if (!topicData.deleted) {
|
||||
socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved_your_topic');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../../user');
|
||||
const topics = require('../../topics');
|
||||
const events = require('../../events');
|
||||
const privileges = require('../../privileges');
|
||||
@@ -65,17 +66,21 @@ module.exports = function (SocketTopics) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
if (!data || !Array.isArray(data.tids) || !data.cid) {
|
||||
if (!data || !Array.isArray(data.tids)) {
|
||||
throw new Error('[[error:invalid-tid]]');
|
||||
}
|
||||
|
||||
if (typeof topics.tools[action] !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const uids = await user.getUidsFromSet('users:online', 0, -1);
|
||||
|
||||
await Promise.all(data.tids.map(async function (tid) {
|
||||
const title = await topics.getTopicField(tid, 'title');
|
||||
const data = await topics.tools[action](tid, socket.uid);
|
||||
socketHelpers.emitToTopicAndCategory(event, data);
|
||||
const notifyUids = await privileges.categories.filterUids('topics:read', data.cid, uids);
|
||||
socketHelpers.emitToTopicAndCategory(event, data, notifyUids);
|
||||
await logTopicAction(action, socket, tid, title);
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -48,7 +48,11 @@ module.exports = function (Topics) {
|
||||
'cid:' + topicData.cid + ':tids',
|
||||
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
|
||||
], timestamp, topicData.tid),
|
||||
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', 0, topicData.tid),
|
||||
db.sortedSetsAdd([
|
||||
'topics:views', 'topics:posts', 'topics:votes',
|
||||
'cid:' + topicData.cid + ':tids:votes',
|
||||
'cid:' + topicData.cid + ':tids:posts',
|
||||
], 0, topicData.tid),
|
||||
categories.updateRecentTid(topicData.cid, topicData.tid),
|
||||
user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp),
|
||||
db.incrObjectField('category:' + topicData.cid, 'topic_count'),
|
||||
|
||||
@@ -18,12 +18,6 @@ module.exports = function (Topics) {
|
||||
deleterUid: uid,
|
||||
deletedTimestamp: Date.now(),
|
||||
}),
|
||||
db.sortedSetsRemove([
|
||||
'topics:recent',
|
||||
'topics:posts',
|
||||
'topics:views',
|
||||
'topics:votes',
|
||||
], tid),
|
||||
removeTopicPidsFromCid(tid),
|
||||
]);
|
||||
};
|
||||
@@ -55,16 +49,11 @@ module.exports = function (Topics) {
|
||||
}
|
||||
|
||||
Topics.restore = async function (tid) {
|
||||
const topicData = await Topics.getTopicData(tid);
|
||||
await Topics.deleteTopicFields(tid, [
|
||||
'deleterUid', 'deletedTimestamp',
|
||||
]);
|
||||
await Promise.all([
|
||||
Topics.setTopicField(tid, 'deleted', 0),
|
||||
Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp']),
|
||||
Topics.updateRecent(tid, topicData.lastposttime),
|
||||
db.sortedSetAddBulk([
|
||||
['topics:posts', topicData.postcount, tid],
|
||||
['topics:views', topicData.viewcount, tid],
|
||||
['topics:votes', parseInt(topicData.votes, 10) || 0, tid],
|
||||
]),
|
||||
addTopicPidsToCid(tid),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -60,9 +60,7 @@ module.exports = function (Topics) {
|
||||
|
||||
await db.sortedSetAdd('cid:' + topicData.cid + ':tids:lastposttime', lastposttime, tid);
|
||||
|
||||
if (!topicData.deleted) {
|
||||
await Topics.updateRecent(tid, lastposttime);
|
||||
}
|
||||
|
||||
if (!topicData.pinned) {
|
||||
await db.sortedSetAdd('cid:' + topicData.cid + ':tids', lastposttime, tid);
|
||||
|
||||
49
src/upgrades/1.14.1/readd_deleted_recent_topics.js
Normal file
49
src/upgrades/1.14.1/readd_deleted_recent_topics.js
Normal file
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
|
||||
const batch = require('../../batch');
|
||||
|
||||
module.exports = {
|
||||
name: 'Re add deleted topics to topics:recent',
|
||||
timestamp: Date.UTC(2018, 9, 11),
|
||||
method: async function () {
|
||||
const progress = this.progress;
|
||||
|
||||
await batch.processSortedSet('topics:tid', async function (tids) {
|
||||
progress.incr(tids.length);
|
||||
const topicData = await db.getObjectsFields(
|
||||
tids.map(tid => 'topic:' + tid),
|
||||
['tid', 'lastposttime', 'viewcount', 'postcount', 'upvotes', 'downvotes']
|
||||
);
|
||||
topicData.forEach((t) => {
|
||||
if (t.hasOwnProperty('upvotes') && t.hasOwnProperty('downvotes')) {
|
||||
t.votes = parseInt(t.upvotes, 10) - parseInt(t.downvotes, 10);
|
||||
}
|
||||
});
|
||||
|
||||
await db.sortedSetAdd('topics:recent',
|
||||
topicData.map(t => t.lastposttime),
|
||||
topicData.map(t => t.tid)
|
||||
);
|
||||
|
||||
await db.sortedSetAdd('topics:views',
|
||||
topicData.map(t => t.viewcount || 0),
|
||||
topicData.map(t => t.tid)
|
||||
);
|
||||
|
||||
await db.sortedSetAdd('topics:posts',
|
||||
topicData.map(t => t.postcount || 0),
|
||||
topicData.map(t => t.tid)
|
||||
);
|
||||
|
||||
await db.sortedSetAdd('topics:votes',
|
||||
topicData.map(t => t.votes || 0),
|
||||
topicData.map(t => t.tid)
|
||||
);
|
||||
}, {
|
||||
progress: progress,
|
||||
batchSize: 500,
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user