diff --git a/install/package.json b/install/package.json index fd579151a7..1b054eaba0 100644 --- a/install/package.json +++ b/install/package.json @@ -95,7 +95,7 @@ "nodebb-plugin-spam-be-gone": "0.7.13", "nodebb-rewards-essentials": "0.2.1", "nodebb-theme-lavender": "5.3.2", - "nodebb-theme-persona": "11.3.38", + "nodebb-theme-persona": "11.3.39", "nodebb-theme-slick": "1.4.23", "nodebb-theme-vanilla": "12.1.17", "nodebb-widget-essentials": "5.0.9", diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json index 3f974d70dc..f1ee75200e 100644 --- a/public/language/en-GB/global.json +++ b/public/language/en-GB/global.json @@ -28,7 +28,7 @@ "pagination": "Pagination", "pagination.out_of": "%1 out of %2", - "pagination.enter_index": "Enter index", + "pagination.enter_index": "Go to post index", "header.admin": "Admin", "header.categories": "Categories", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index fc7984f6de..060e916c19 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -207,5 +207,7 @@ "timeago_earlier": "%1 earlier", "first-post": "First post", "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have any other posts in this topic", "post-quick-reply": "Post quick reply" } diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index a2b2956e4e..88e76e9b7a 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -54,15 +54,20 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct e.stopPropagation(); }); - paginationBlockEl.off('shown.bs.dropdown', '.dropdown').on('shown.bs.dropdown', '.dropdown', function () { - setTimeout(function () { + paginationBlockEl.off('shown.bs.dropdown', '.wrapper').on('shown.bs.dropdown', '.wrapper', function () { + setTimeout(async function () { $('.pagination-block input').focus(); + const postCountInTopic = await socket.emit('topics.getPostCountInTopic', ajaxify.data.tid); + if (postCountInTopic > 0) { + paginationBlockEl.find('#myNextPostBtn').removeAttr('disabled'); + } }, 100); }); paginationBlockEl.find('.pageup').off('click').on('click', navigator.scrollUp); paginationBlockEl.find('.pagedown').off('click').on('click', navigator.scrollDown); paginationBlockEl.find('.pagetop').off('click').on('click', navigator.toTop); paginationBlockEl.find('.pagebottom').off('click').on('click', navigator.toBottom); + paginationBlockEl.find('#myNextPostBtn').off('click').on('click', gotoMyNextPost); paginationBlockEl.find('input').on('keydown', function (e) { if (e.which === 13) { @@ -90,6 +95,40 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct navigator.update(0); }; + let lastNextIndex = 0; + async function gotoMyNextPost() { + async function getNext(startIndex) { + return await socket.emit('topics.getMyNextPostIndex', { + tid: ajaxify.data.tid, + index: Math.max(1, startIndex), + sort: config.topicPostSort, + }); + } + if (ajaxify.data.template.topic) { + let nextIndex = await getNext(index); + if (lastNextIndex === nextIndex) { // handles last post in pagination + console.log('fail', nextIndex, lastNextIndex); + nextIndex = await getNext(nextIndex); + } + if (nextIndex) { + lastNextIndex = nextIndex; + $(window).one('action:ajaxify.end', function () { + if (paginationBlockEl.find('.dropdown-menu').is(':hidden')) { + paginationBlockEl.find('.dropdown-toggle').dropdown('toggle'); + } + }); + navigator.scrollToIndex(nextIndex, true, 0); + } else { + alerts.alert({ + message: '[[topic:no-more-next-post]]', + type: 'info', + }); + + lastNextIndex = 1; + } + } + } + function clampTop(newTop) { const parent = thumb.parent(); const parentOffset = parent.offset(); diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 71798a3311..c69bb63f3f 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -1,9 +1,14 @@ 'use strict'; +const _ = require('lodash'); + +const db = require('../database'); +const posts = require('../posts'); const topics = require('../topics'); const user = require('../user'); const meta = require('../meta'); const privileges = require('../privileges'); +const cache = require('../cache'); const SocketTopics = module.exports; @@ -54,4 +59,52 @@ SocketTopics.isModerator = async function (socket, tid) { return await user.isModerator(socket.uid, cid); }; +SocketTopics.getMyNextPostIndex = async function (socket, data) { + if (!data || !data.tid || !data.index || !data.sort) { + throw new Error('[[error:invalid-data]]'); + } + + async function getTopicPids(index) { + const topicSet = data.sort === 'most_votes' ? `tid:${data.tid}:posts:votes` : `tid:${data.tid}:posts`; + const reverse = data.sort === 'newest_to_oldest' || data.sort === 'most_votes'; + const cacheKey = `np:s:${topicSet}:r:${String(reverse)}:tid:${data.tid}:pids`; + const topicPids = cache.get(cacheKey); + if (topicPids) { + return topicPids.slice(index - 1); + } + const pids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](topicSet, 0, -1); + cache.set(cacheKey, pids, 30000); + return pids.slice(index - 1); + } + + async function getUserPids() { + const cid = await topics.getTopicField(data.tid, 'cid'); + const cacheKey = `np:cid:${cid}:uid:${socket.uid}:pids`; + const userPids = cache.get(cacheKey); + if (userPids) { + return userPids; + } + const pids = await db.getSortedSetRange(`cid:${cid}:uid:${socket.uid}:pids`, 0, -1); + cache.set(cacheKey, pids, 30000); + return pids; + } + + const [topicPids, userPidsInCategory] = await Promise.all([ + getTopicPids(data.index), + getUserPids(), + ]); + const userPidsInTopic = _.intersection(topicPids, userPidsInCategory); + if (!userPidsInTopic.length) { + return 0; + } + return await posts.getPidIndex(userPidsInTopic[0], data.tid, data.sort); +}; + +SocketTopics.getPostCountInTopic = async function (socket, tid) { + if (!socket.uid || !tid) { + return 0; + } + return await db.sortedSetScore(`tid:${tid}:posters`, socket.uid); +}; + require('../promisify')(SocketTopics);