mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-07-08 13:33:11 +02:00
* refactor: wholesale UI/data refactor of world to display in feed-like format * fix: openapi schema * fix: remove console log * fix: restrict 'generatedTitle' from being passed-in via topics API * fix(deps): bumping themes for world refactor support * fix: /world title and description update * fix: missing handleIgnoreWatch in world client side js
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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..."
|
||||
}
|
||||
@@ -84,6 +84,8 @@ PostObject:
|
||||
description: A topic identifier
|
||||
title:
|
||||
type: string
|
||||
generatedTitle:
|
||||
type: number
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
|
||||
@@ -214,6 +214,8 @@ TopicObjectSlim:
|
||||
description: A category identifier
|
||||
title:
|
||||
type: string
|
||||
generatedTitle:
|
||||
type: number
|
||||
slug:
|
||||
type: string
|
||||
mainPid:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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('<p class="quote-inline"')).join('\n');
|
||||
const sentences = tokenizer.sentences(prettified, { sanitize: true, newline_boundaries: true });
|
||||
title = sentences.shift();
|
||||
generatedTitle = 1;
|
||||
}
|
||||
|
||||
// Remove any custom emoji from title
|
||||
@@ -246,6 +248,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
tags,
|
||||
content: mainPost.content,
|
||||
sourceContent: mainPost.sourceContent,
|
||||
generatedTitle,
|
||||
_activitypub: mainPost._activitypub,
|
||||
});
|
||||
unprocessed.shift();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
96
src/views/partials/feed/item.tpl
Normal file
96
src/views/partials/feed/item.tpl
Normal file
@@ -0,0 +1,96 @@
|
||||
<li component="category/topic" data-tid="{./topic.tid}" class="shadow-sm mb-3 rounded-2 border posts-list-item {{{ if ./deleted }}} deleted{{{ else }}}{{{ if ./topic.deleted }}} deleted{{{ end }}}{{{ end }}}{{{ if ./topic.scheduled }}} scheduled{{{ end }}}" data-pid="{./pid}" data-uid="{./uid}" data-index="{./index}">
|
||||
{{{ if (showThumbs && ./topic.thumbs.length)}}}
|
||||
<div class="p-1 position-relative">
|
||||
<div class="overflow-hidden rounded-1" style="max-height: 300px;">
|
||||
<a href="{config.relative_path}/topic/{./topic.slug}">
|
||||
<img class="w-100" src="{./topic.thumbs.0.url}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div component="topic/thumb/list" class="position-absolute end-0 bottom-0 p-3 d-flex gap-2 pe-none {{{ if greaterthan(./topic.thumbs.length, "4") }}}thumbs-collapsed{{{ end }}}">
|
||||
{{{ each ./topic.thumbs }}}
|
||||
{{{ if (@index != 0) }}}
|
||||
<img class="rounded-1" style="max-height: 64px; object-fit: contain;" src="{./url}">
|
||||
{{{ end }}}
|
||||
{{{ end }}}
|
||||
{{{ if greaterthan(./topic.thumbs.length, "4") }}}
|
||||
<div class="btn btn-light fw-semibold d-flex align-items-center" style="max-height:64px; contain;">+{increment(./topic.thumbs.length, "-3")}</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
</div>
|
||||
{{{ end }}}
|
||||
|
||||
<div class="d-flex gap-3 p-3">
|
||||
<div class="d-none d-lg-block">
|
||||
<a class="lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "40px", true, "not-responsive")}</a>
|
||||
</div>
|
||||
<div class="post-body d-flex flex-column gap-2 flex-grow-1 hover-parent" style="min-width: 0px;">
|
||||
<div class="d-flex flex-column gap-2 post-info">
|
||||
<div class="d-flex gap-2 text-truncate">
|
||||
<div class="text-sm">
|
||||
<div class="post-author d-flex align-items-center gap-1">
|
||||
<a class="d-inline d-lg-none lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "16px", true, "not-responsive")}</a>
|
||||
<a class="lh-normal fw-semibold text-nowrap" href="{config.relative_path}/user/{./user.userslug}">{./user.displayname}</a>
|
||||
</div>
|
||||
<span class="timeago text-muted lh-normal" title="{./timestampISO}"></span>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
{{{ if (./category.cid != "-1") }}}
|
||||
{buildCategoryLabel(./category, "a", "border text-xs flex-shrink-0")}
|
||||
{{{ end }}}
|
||||
{{{ if showSelect }}}
|
||||
<div class="checkbox ms-auto" style="max-width:max-content">
|
||||
<i component="topic/select" class="fa text-muted pointer fa-square-o p-1"></i>
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{{ if !./topic.generatedTitle }}}
|
||||
<a class="lh-1 topic-title fw-semibold fs-5 text-reset line-clamp-2" href="{config.relative_path}/topic/{./topic.slug}">
|
||||
{./topic.title}
|
||||
</a>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div component="post/content" class="content text-sm text-break position-relative truncate-post-content">
|
||||
<a href="{config.relative_path}/post/{./pid}" class="stretched-link"></a>
|
||||
{./content}
|
||||
</div>
|
||||
<div class="position-relative hover-visible">
|
||||
<button component="show/more" class="btn btn-light btn-sm rounded-pill position-absolute start-50 translate-middle-x bottom-0 z-1 hidden ff-secondary">[[feed:see-more]]</button>
|
||||
</div>
|
||||
<hr class="my-2"/>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{config.relative_path}/post/{{{ if ./topic.teaserPid }}}{encodeURIComponent(./topic.teaserPid)}{{{ else }}}{encodeURIComponent(./pid)){{{ end }}}" class="btn btn-link btn-sm text-body {{{ if !./isMainPost }}}invisible{{{ end }}}"><i class="fa-fw fa-regular fa-message text-muted"></i> {humanReadableNumber(./topic.postcount)}</a>
|
||||
|
||||
<a href="#" data-pid="{./pid}" data-action="bookmark" data-bookmarked="{./bookmarked}" data-bookmarks="{./bookmarks}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-bookmark {{{ if ./bookmarked }}}fa text-primary{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="bookmark-count">{humanReadableNumber(./bookmarks)}</span></a>
|
||||
|
||||
<a href="#" data-pid="{./pid}" data-action="upvote" data-upvoted="{./upvoted}" data-upvotes="{./upvotes}" class="btn btn-link btn-sm text-body"><i class="fa-fw fa-heart {{{ if ./upvoted }}}fa text-danger{{{ else }}}fa-regular text-muted{{{ end }}}"></i> <span component="upvote-count">{humanReadableNumber(./upvotes)}</span></a>
|
||||
|
||||
<a href="#" data-pid="{./pid}" data-is-main="{./isMainPost}" data-tid="{./tid}" data-action="reply" class="btn btn-link btn-sm text-body"><i class="fa-fw fa fa-reply text-muted"></i> [[topic:reply]]</a>
|
||||
</div>
|
||||
{{{ if ./topic.teaser }}}
|
||||
<div class="d-flex flex-column gap-2 mt-1 text-xs border-start ps-3">
|
||||
{{{ if (./replies && (./replies != "1")) }}}
|
||||
<a href="{config.relative_path}/post/{./pid}" class="text-capitalize fw-semibold text-secondary">[[global:read-more]] →</a>
|
||||
{{{ end }}}
|
||||
<div class="d-inline-flex flex-column px-3 py-2 rounded gap-2 bg-body-tertiary align-self-start">
|
||||
<div class="d-flex align-items-top gap-2">
|
||||
<a class="text-decoration-none avatar-tooltip" title="{./topic.teaser.user.displayname}" href="{config.relative_path}/user/{./topic.teaser.user.userslug}">{buildAvatar(./topic.teaser.user, "18px", true)} {./topic.teaser.user.displayname}</a>
|
||||
<a class="permalink text-muted timeago text-xs" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./index }}}/{./index}{{{ end }}}" title="{./topic.teaser.timestampISO}" aria-label="[[global:lastpost]]"></a>
|
||||
</div>
|
||||
<div class="post-content text-xs text-break line-clamp-sm-2 lh-sm position-relative flex-fill">
|
||||
<a class="stretched-link" tabindex="-1" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./topic.teaser.index }}}/{./topic.teaser.index}{{{ end }}}" aria-label="[[global:lastpost]]"></a>
|
||||
{./topic.teaser.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{ end }}}
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
Reference in New Issue
Block a user