+
diff --git a/install/package.json b/install/package.json index 199c140eb7..53bcd6ae40 100644 --- a/install/package.json +++ b/install/package.json @@ -107,10 +107,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.46", + "nodebb-theme-harmony": "2.2.1", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.49", - "nodebb-theme-persona": "14.1.26", + "nodebb-theme-persona": "14.2.0", "nodebb-widget-essentials": "7.0.42", "nodemailer": "7.0.13", "nprogress": "0.2.0", diff --git a/public/language/en-GB/category.json b/public/language/en-GB/category.json index faa58d2406..8d0cde6eae 100644 --- a/public/language/en-GB/category.json +++ b/public/language/en-GB/category.json @@ -1,8 +1,8 @@ { "category": "Category", "subcategories": "Subcategories", - "uncategorized": "Uncategorized", - "uncategorized.description": "Topics that do not strictly fit in with any existing categories", + "uncategorized": "World", + "uncategorized.description": "Topics from outside of this forum. Views and opinions represented here may not reflect those of this forum and its members.", "handle.description": "This category can be followed from the open social web via the handle %1", "new-topic-button": "New Topic", "guest-login-post": "Log in to post", diff --git a/public/language/en-GB/world.json b/public/language/en-GB/world.json index 7fdb1569f2..9c9971a842 100644 --- a/public/language/en-GB/world.json +++ b/public/language/en-GB/world.json @@ -1,7 +1,8 @@ { "name": "World", - "popular": "Popular topics", - "recent": "All topics", + "latest": "Latest", + "popular": "Popular", + "recent": "All", "help": "Help", "help.title": "What is this page?", @@ -16,6 +17,5 @@ "onboard.why": "There's a lot that goes on outside of this forum, and not all of it is relevant to your interests. That's why following people is the best way to signal that you want to see more from someone.", "onboard.how": "In the meantime, you can click on the shortcut buttons at the top to see what else this forum knows about, and start discovering some new content!", - "show-categories": "Show categories", - "hide-categories": "Hide categories" + "category-search": "Find a category..." } \ No newline at end of file diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 1904cded51..80c9a06cd3 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -84,6 +84,8 @@ PostObject: description: A topic identifier title: type: string + generatedTitle: + type: number cid: type: number description: A category identifier diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 30ba9d3a77..64c47f0daa 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -214,6 +214,8 @@ TopicObjectSlim: description: A category identifier title: type: string + generatedTitle: + type: number slug: type: string mainPid: diff --git a/public/openapi/read/world.yaml b/public/openapi/read/world.yaml index 8bf706add3..d5d95461e5 100644 --- a/public/openapi/read/world.yaml +++ b/public/openapi/read/world.yaml @@ -24,22 +24,14 @@ get: type: array items: type: string - topicCount: - type: number - topics: - type: array - items: - $ref: ../components/schemas/TopicObject.yaml#/TopicObject - selectedTag: - type: object - properties: - label: - type: string - nullable: true - selectedTags: - type: array - items: - type: string + posts: + $ref: ../components/schemas/PostsObject.yaml#/PostsObject + showThumbs: + type: boolean + showTopicTools: + type: boolean + showSelect: + type: boolean isWatched: type: boolean isTracked: @@ -51,40 +43,8 @@ get: hasFollowers: type: boolean nullable: true - feeds:disableRSS: - type: number - rssFeedUrl: - type: string - reputation:disabled: - type: number title: type: string - privileges: - type: object - properties: - topics:create: - type: boolean - topics:read: - type: boolean - topics:tag: - type: boolean - topics:schedule: - type: boolean - read: - type: boolean - posts:view_deleted: - type: boolean - cid: - type: string - uid: - type: number - description: A user identifier - editable: - type: boolean - view_deleted: - type: boolean - isAdminOrMod: - type: boolean categories: type: array items: diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js index f298cbe85d..58a257f293 100644 --- a/public/src/client/category/tools.js +++ b/public/src/client/category/tools.js @@ -12,8 +12,8 @@ define('forum/category/tools', [ ], function (topicSelect, threadTools, components, api, bootbox, alerts) { const CategoryTools = {}; - CategoryTools.init = function () { - topicSelect.init(updateDropdownOptions); + CategoryTools.init = function (containerEl) { + topicSelect.init(updateDropdownOptions, containerEl); handlePinnedTopicSort(); diff --git a/public/src/client/world.js b/public/src/client/world.js index 111f8d7cca..bde0285db1 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -1,17 +1,23 @@ 'use strict'; -define('forum/world', ['topicList', 'search', 'sort', 'hooks', 'alerts', 'api', 'bootbox'], function (topicList, search, sort, hooks, alerts, api, bootbox) { +define('forum/world', [ + 'forum/infinitescroll', 'search', 'sort', 'hooks', + 'alerts', 'api', 'bootbox', 'helpers', 'forum/category/tools', +], function (infinitescroll, search, sort, hooks, alerts, api, bootbox, helpers, categoryTools) { const World = {}; + $(window).on('action:ajaxify.start', function () { + categoryTools.removeListeners(); + }); + World.init = function () { app.enterRoom('world'); - topicList.init('world'); + categoryTools.init($('#world-feed')); sort.handleSort('categoryTopicSort', 'world'); - handleIgnoreWatch(-1); + handleButtons(); handleHelp(); - handleCategories(); search.enableQuickSearch({ searchElements: { @@ -28,35 +34,108 @@ define('forum/world', ['topicList', 'search', 'sort', 'hooks', 'alerts', 'api', hideOnNoMatches: false, }); + if (!config.usePagination) { + infinitescroll.init((direction) => { + const posts = Array.from(document.querySelectorAll('[component="post"]')); + const afterEl = direction > 0 ? posts.pop() : posts.shift(); + const after = (parseInt(afterEl.getAttribute('data-index'), 10) || 0) + (direction > 0 ? 1 : 0); + if (after < config.topicsPerPage) { + return; + } + + loadTopicsAfter(after, direction, (payload) => { + app.parseAndTranslate(ajaxify.data.template.name, 'posts', payload, function (html) { + const listEl = document.getElementById('world-feed'); + $(listEl).append(html); + html.find('.timeago').timeago(); + }); + }); + }); + } + + ajaxify.data.categories.forEach(function (category) { + handleIgnoreWatch(category.cid); + }); + hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics }); hooks.fire('action:category.loaded', { cid: ajaxify.data.cid }); }; - function handleIgnoreWatch(cid) { - $('[component="category/watching"], [component="category/tracking"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { - const $this = $(this); - const state = $this.attr('data-state'); + function calculateNextPage(after, direction) { + return Math.floor(after / config.topicsPerPage) + (direction > 0 ? 1 : 0); + } - api.put(`/categories/${cid}/watch`, { state }, (err) => { + function loadTopicsAfter(after, direction, callback) { + callback = callback || function () {}; + const query = utils.params(); + query.page = calculateNextPage(after, direction); + infinitescroll.loadMoreXhr(query, callback); + } + + function handleButtons() { + const feedEl = $('#world-feed'); + + feedEl.on('click', '[data-action="bookmark"]', function () { + const $this = $(this); + const isBookmarked = $this.attr('data-bookmarked') === 'true'; + const pid = $this.attr('data-pid'); + const bookmarkCount = parseInt($this.attr('data-bookmarks'), 10); + const method = isBookmarked ? 'del' : 'put'; + + api[method](`/posts/${pid}/bookmark`, undefined, function (err) { + if (err) { + return alerts.error(err); + } + const type = isBookmarked ? 'unbookmark' : 'bookmark'; + const newBookmarkCount = bookmarkCount + (isBookmarked ? -1 : 1); + $this.find('[component="bookmark-count"]').text( + helpers.humanReadableNumber(newBookmarkCount) + ); + $this.attr('data-bookmarks', newBookmarkCount); + $this.attr('data-bookmarked', isBookmarked ? 'false' : 'true'); + $this.find('i').toggleClass('fa text-primary', !isBookmarked) + .toggleClass('fa-regular text-muted', isBookmarked); + hooks.fire(`action:post.${type}`, { pid: pid }); + }); + }); + + feedEl.on('click', '[data-action="upvote"]', function () { + const $this = $(this); + const isUpvoted = $this.attr('data-upvoted') === 'true'; + const pid = $this.attr('data-pid'); + const upvoteCount = parseInt($this.attr('data-upvotes'), 10); + const method = isUpvoted ? 'del' : 'put'; + const delta = 1; + api[method](`/posts/${pid}/vote`, { delta }, function (err) { if (err) { return alerts.error(err); } - $('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); - $('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + const newUpvoteCount = upvoteCount + (isUpvoted ? -1 : 1); + $this.find('[component="upvote-count"]').text( + helpers.humanReadableNumber(newUpvoteCount) + ); + $this.attr('data-upvotes', newUpvoteCount); + $this.attr('data-upvoted', isUpvoted ? 'false' : 'true'); + $this.find('i').toggleClass('fa text-danger', !isUpvoted) + .toggleClass('fa-regular text-muted', isUpvoted); - $('[component="category/tracking/menu"]').toggleClass('hidden', state !== 'tracking'); - $('[component="category/tracking/check"]').toggleClass('fa-check', state === 'tracking'); - - $('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); - $('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); - - $('[component="category/ignoring/menu"]').toggleClass('hidden', state !== 'ignoring'); - $('[component="category/ignoring/check"]').toggleClass('fa-check', state === 'ignoring'); - - alerts.success('[[category:' + state + '.message]]'); + hooks.fire('action:post.toggleVote', { + pid: pid, + delta: delta, + unvote: method === 'del', + }); }); }); + + feedEl.on('click', '[data-action="reply"]', function () { + const $this = $(this); + const isMain = $this.attr('data-is-main') === 'true'; + app.newReply({ + tid: $this.attr('data-tid'), + pid: !isMain ? $this.attr('data-pid') : undefined, + }).catch(alerts.error); + }); } function handleHelp() { @@ -82,36 +161,34 @@ define('forum/world', ['topicList', 'search', 'sort', 'hooks', 'alerts', 'api', }); } - function handleCategories() { - // const optionsEl = document.getElementById('category-options'); - // const dropdownEl = optionsEl.querySelector('ul'); - const showEl = document.getElementById('show-categories'); - const hideEl = document.getElementById('hide-categories'); - const categoriesEl = document.querySelector('.categories-list'); - if (![showEl, hideEl, categoriesEl].every(Boolean)) { - return; - } + function handleIgnoreWatch(cid) { + const category = $('[data-cid="' + cid + '"]'); + category.find( + '[component="category/watching"], [component="category/tracking"], [component="category/ignoring"], [component="category/notwatching"]' + ).on('click', async (e) => { + const state = e.currentTarget.getAttribute('data-state'); + const { uid } = ajaxify.data; - const update = () => { - showEl.classList.toggle('hidden', visibility); - hideEl.classList.toggle('hidden', !visibility); - categoriesEl.classList.toggle('hidden', !visibility); - localStorage.setItem('world:show-categories', visibility); - }; - - let visibility = localStorage.getItem('world:show-categories'); - console.log('got value', visibility); - visibility = visibility ? visibility === 'true' : true; // localStorage values are strings - update(); - - showEl.addEventListener('click', () => { - visibility = true; - update(); + const { modified } = await api.put(`/categories/${encodeURIComponent(cid)}/watch`, { state, uid }); + updateDropdowns(modified, state); + alerts.success('[[category:' + state + '.message]]'); }); + } - hideEl.addEventListener('click', () => { - visibility = false; - update(); + function updateDropdowns(modified_cids, state) { + modified_cids.forEach(function (cid) { + const category = $('[data-cid="' + cid + '"]'); + category.find('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); + category.find('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + + category.find('[component="category/tracking/menu"]').toggleClass('hidden', state !== 'tracking'); + category.find('[component="category/tracking/check"]').toggleClass('fa-check', state === 'tracking'); + + category.find('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); + category.find('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); + + category.find('[component="category/ignoring/menu"]').toggleClass('hidden', state !== 'ignoring'); + category.find('[component="category/ignoring/check"]').toggleClass('fa-check', state === 'ignoring'); }); } diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index 1dd231b8c9..50d38c4ba7 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -7,8 +7,8 @@ define('topicSelect', ['components'], function (components) { let topicsContainer; - TopicSelect.init = function (onSelect) { - topicsContainer = $('[component="category"]'); + TopicSelect.init = function (onSelect, containerEl) { + topicsContainer = containerEl || $('[component="category"]'); topicsContainer.on('selectstart', '[component="topic/select"]', function (ev) { ev.preventDefault(); }); diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 32d801f57e..12b72494b8 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -132,6 +132,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { return { tid, count: 0 }; } + let generatedTitle; if (hasTid) { mainPid = await topics.getTopicField(tid, 'mainPid'); } else { @@ -184,6 +185,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { prettified = prettified.split('\n').filter(line => !line.startsWith('
{ tags, content: mainPost.content, sourceContent: mainPost.sourceContent, + generatedTitle, _activitypub: mainPost._activitypub, }); unprocessed.shift(); diff --git a/src/api/topics.js b/src/api/topics.js index 054602e7a2..45be990a9b 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -56,6 +56,7 @@ topicsAPI.create = async function (caller, data) { const payload = { ...data }; delete payload.tid; + delete payload.generatedTitle; payload.tags = payload.tags || []; apiHelpers.setDefaultPostData(caller, payload); const isScheduling = parseInt(data.timestamp, 10) > payload.timestamp; diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 3ca7c037f7..2052c708a3 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -1,23 +1,19 @@ 'use strict'; -const nconf = require('nconf'); +const _ = require('lodash'); +const meta = require('../../meta'); const user = require('../../user'); const topics = require('../../topics'); +const posts = require('../../posts'); const categories = require('../../categories'); -const privileges = require('../../privileges'); const translator = require('../../translator'); -const meta = require('../../meta'); const pagination = require('../../pagination'); const utils = require('../../utils'); const helpers = require('../helpers'); const controller = module.exports; -const validSorts = [ - 'recently_replied', 'recently_created', 'most_posts', 'most_votes', 'most_views', -]; - controller.list = async function (req, res) { if (!req.uid) { return helpers.redirect(res, '/recent?cid=-1', false); @@ -28,20 +24,14 @@ controller.list = async function (req, res) { const start = Math.max(0, (page - 1) * topicsPerPage); const stop = start + topicsPerPage - 1; - const [userPrivileges, tagData, userSettings, rssToken] = await Promise.all([ - privileges.categories.get('-1', req.uid), - helpers.getSelectedTag(req.query.tag), - user.getSettings(req.uid), - user.auth.getFeedToken(req.uid), - ]); - const sort = validSorts.includes(req.query.sort) ? req.query.sort : userSettings.categoryTopicSort; + const userSettings = await user.getSettings(req.uid); const targetUid = await user.getUidByUserslug(req.query.author); - const cidQuery = { + let cidQuery = { uid: req.uid, cid: '-1', start: start, stop: stop, - sort: sort, + sort: req.query.sort, settings: userSettings, query: req.query, tag: req.query.tag, @@ -49,45 +39,84 @@ controller.list = async function (req, res) { }; const data = await categories.getCategoryById(cidQuery); delete data.children; + data.sort = req.query.sort; - let tids = await categories.getTopicIds(cidQuery); - tids = await categories.sortTidsBySet(tids, sort); // sorting not handled if cid is -1 - data.topicCount = tids.length; - data.topics = await topics.getTopicsByTids(tids, { uid: req.uid }); - topics.calculateTopicIndices(data.topics, start); + let tids; + if (req.query.sort === 'popular') { + cidQuery = { + ...cidQuery, + cids: ['-1'], + sort: 'posts', + term: req.query.term || 'day', + }; + delete cidQuery.cid; + ({ tids } = await topics.getSortedTopics(cidQuery)); + tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined); + } else { + tids = await categories.getTopicIds(cidQuery); + } + const mainPids = await topics.getMainPids(tids); + const postData = await posts.getPostSummaryByPids(mainPids, req.uid, { + stripTags: false, + extraFields: ['bookmarks'], + }); + const uniqTids = _.uniq(postData.map(p => p.tid)); + const [topicData, { upvotes }, bookmarkStatus] = await Promise.all([ + topics.getTopicsFields(uniqTids, ['tid', 'numThumbs', 'thumbs', 'mainPid']), + posts.getVoteStatusByPostIDs(mainPids, req.uid), + posts.hasBookmarked(mainPids, req.uid), + ]); + + const thumbs = await topics.thumbs.load(topicData); + const tidToThumbs = _.zipObject(uniqTids, thumbs); + const teasers = await topics.getTeasers(postData.map(p => p.topic), { uid: req.uid }); + postData.forEach((p, index) => { + p.pid = encodeURIComponent(p.pid); + if (p.topic) { + p.topic = { ...p.topic }; + p.topic.thumbs = tidToThumbs[p.tid]; + p.topic.postcount = Math.max(0, p.topic.postcount - 1); + p.topic.teaser = teasers[index]; + } + p.upvoted = upvotes[index]; + p.bookmarked = bookmarkStatus[index]; + if (!p.isMainPost) { + p.repliedString = translator.compile('feed:replied-in-ago', p.topic.title, p.timestampISO); + } + p.index = start + index; + }); + data.showThumbs = req.loggedIn || meta.config.privateUploads !== 1; + data.posts = postData; + data.showTopicTools = true; + data.showSelect = true; // Tracked/watched categories let cids = await user.getCategoriesByStates(req.uid, [ categories.watchStates.tracking, categories.watchStates.watching, ]); cids = cids.filter(cid => !utils.isNumber(cid)); - const categoryData = await categories.getCategories(cids); + const [categoryData, watchState] = await Promise.all([ + categories.getCategories(cids), + categories.getWatchState(cids, req.uid), + ]); data.categories = categories.getTree(categoryData, 0); await Promise.all([ categories.getRecentTopicReplies(categoryData, req.uid, req.query), categories.setUnread(data.categories, cids, req.uid), ]); - data.categories.forEach((category) => { + data.categories.forEach((category, idx) => { if (category) { helpers.trimChildren(category); helpers.setCategoryTeaser(category); + category.isWatched = watchState[idx] === categories.watchStates.watching; + category.isTracked = watchState[idx] === categories.watchStates.tracking; + category.isNotWatched = watchState[idx] === categories.watchStates.notwatching; + category.isIgnored = watchState[idx] === categories.watchStates.ignoring; } }); data.title = translator.escape(data.name); - data.privileges = userPrivileges; - data.selectedTag = tagData.selectedTag; - data.selectedTags = tagData.selectedTags; - - data.breadcrumbs = helpers.buildBreadcrumbs([{ text: `[[pages:world]]` }]); - data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0; - data['reputation:disabled'] = meta.config['reputation:disabled']; - if (!meta.config['feeds:disableRSS']) { - data.rssFeedUrl = `${nconf.get('url')}/category/${data.cid}.rss`; - if (req.loggedIn) { - data.rssFeedUrl += `?uid=${req.uid}&token=${rssToken}`; - } - } + data.breadcrumbs = helpers.buildBreadcrumbs([]); const pageCount = Math.max(1, Math.ceil(data.topicCount / topicsPerPage)); data.pagination = pagination.create(page, pageCount, req.query); diff --git a/src/posts/summary.js b/src/posts/summary.js index 5995514eb6..1f0df7b04c 100644 --- a/src/posts/summary.js +++ b/src/posts/summary.js @@ -93,7 +93,7 @@ module.exports = function (Posts) { async function getTopicAndCategories(tids) { const topicsData = await topics.getTopicsFields(tids, [ - 'uid', 'tid', 'title', 'cid', 'tags', 'slug', + 'uid', 'tid', 'title', 'generatedTitle', 'cid', 'tags', 'slug', 'deleted', 'scheduled', 'postcount', 'mainPid', 'teaserPid', ]); diff --git a/src/topics/create.js b/src/topics/create.js index 098fba2d41..7bead5e8b8 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -30,6 +30,7 @@ module.exports = function (Topics) { cid: data.cid, mainPid: 0, title: data.title, + generatedTitle: data.generatedTitle, slug: `${tid}/${slugify(data.title) || 'topic'}`, timestamp: timestamp, lastposttime: 0, @@ -47,6 +48,10 @@ module.exports = function (Topics) { topicData.numThumbs = thumbs.length; } + if (data.generatedTitle && utils.isNumber(data.generatedTitle)) { + topicData.generatedTitle = data.generatedTitle; + } + const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data }); topicData = result.topic; await db.setObject(`topic:${topicData.tid}`, topicData); diff --git a/src/topics/data.js b/src/topics/data.js index a5801e0475..91e2adbc08 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -13,7 +13,7 @@ const intFields = [ 'viewcount', 'postercount', 'followercount', 'deleted', 'locked', 'pinned', 'pinExpiry', 'timestamp', 'upvotes', 'downvotes', - 'lastposttime', 'deleterUid', + 'lastposttime', 'deleterUid', 'generatedTitle', ]; module.exports = function (Topics) { diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 2112fe3ad1..3fc3ce5fd3 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -29,6 +29,7 @@ module.exports = function (Topics) { params.tags = [params.tags]; } data.tids = await getTids(params); + data.tids = await getInbox(data.tids, params); data.tids = await sortTids(data.tids, params); data.tids = await filterTids(data.tids.slice(0, meta.config.recentMaxTopics), params); data.topicCount = data.tids.length; @@ -72,6 +73,33 @@ module.exports = function (Topics) { return tids; } + async function getInbox(tids, params) { + if (params.cids && !params.cids.includes('-1')) { + return tids; + } + + let inbox; + if (params.term !== 'alltime') { + const method = params.sort === 'old' ? + 'getSortedSetRangeByScore' : + 'getSortedSetRevRangeByScore'; + inbox = await db[method]( + `uid:${params.uid}:inbox`, + 0, + 1000, + '+inf', + Date.now() - Topics.getSinceFromTerm(params.term) + ); + } else { + const method = params.sort === 'old' ? + 'getSortedSetRange' : + 'getSortedSetRevRange'; + inbox = await db[method](`uid:${params.uid}:inbox`, 0, meta.config.recentMaxTopics - 1); + } + + return tids.concat(inbox); + } + function sortToSet(sort) { const map = { recent: 'topics:recent', @@ -243,7 +271,11 @@ module.exports = function (Topics) { } async function filterTids(tids, params) { - const { filter, uid } = params; + let { filter, uid, cids } = params; + cids = cids && cids.map(String); + if (cids && cids.length === 1 && cids.includes('-1')) { + cids = undefined; + } if (filter === 'new') { tids = await Topics.filterNewTids(tids, uid); @@ -271,13 +303,11 @@ module.exports = function (Topics) { const isCidIgnored = _.zipObject(topicCids, ignoredCids); topicData = filtered; - const cids = params.cids && params.cids.map(String); const { tags } = params; tids = topicData.filter(t => ( t && t.cid && !isCidIgnored[t.cid] && - (cids || parseInt(t.cid, 10) !== -1) && (!cids || cids.includes(String(t.cid))) && (!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag))) )).map(t => t.tid); diff --git a/src/views/partials/feed/item.tpl b/src/views/partials/feed/item.tpl new file mode 100644 index 0000000000..f46b9e87c5 --- /dev/null +++ b/src/views/partials/feed/item.tpl @@ -0,0 +1,96 @@ +