Post/parent (#13133)

* post.parent wip

* feat: post parent

handle delete/restore/purge privileges etc.

* remove whitespace

* fix: this in each

* lint

* up harmony fix text break
This commit is contained in:
Barış Soner Uşaklı
2025-02-06 14:37:50 -05:00
committed by GitHub
parent f07f380167
commit c2f7754ae0
12 changed files with 109 additions and 28 deletions

View File

@@ -108,7 +108,7 @@
"nodebb-plugin-spam-be-gone": "2.3.0",
"nodebb-plugin-web-push": "0.7.2",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "2.0.14",
"nodebb-theme-harmony": "2.0.15",
"nodebb-theme-lavender": "7.1.17",
"nodebb-theme-peace": "2.2.38",
"nodebb-theme-persona": "14.0.12",

View File

@@ -50,6 +50,12 @@ body.page-user-chats {
}
}
[component="chat/message/parent"] {
[component="chat/message/parent/content"] > p:last-child {
margin-bottom: 0;
}
}
.expanded-chat {
.chat-content {
.message-body {

View File

@@ -264,15 +264,28 @@ define('forum/topic', [
}
function addParentHandler() {
components.get('topic').on('click', '[component="post/parent"]', function (e) {
const toPid = $(this).attr('data-topid');
function gotoPost(event, toPid) {
const toPost = $('[component="topic"]>[component="post"][data-pid="' + toPid + '"]');
if (toPost.length) {
e.preventDefault();
event.preventDefault();
navigator.scrollToIndex(toPost.attr('data-index'), true);
return false;
}
}
components.get('topic').on('click', '[component="post/parent"]', function (e) {
const parentEl = $(this);
const contentEl = parentEl.find('[component="post/parent/content"]');
if (contentEl.length) {
contentEl.toggleClass('line-clamp-1');
parentEl.find('.timeago').toggleClass('hidden');
parentEl.toggleClass('flex-column').toggleClass('flex-row');
} else {
return gotoPost(e, parentEl.attr('data-topid'));
}
});
components.get('topic').on('click', '[component="post/parent"] .timeago', function (e) {
return gotoPost(e, $(this).parents('[data-parent-pid]').attr('data-parent-pid'));
});
}
@@ -298,7 +311,7 @@ define('forum/topic', [
destroyed = true;
}
$(window).one('action:ajaxify.start', destroyTooltip);
$('[component="topic"]').on('mouseenter', '[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () {
$('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () {
const link = $(this);
destroyed = false;

View File

@@ -159,6 +159,17 @@ define('forum/topic/events', [
});
}
});
const parentEl = $(`[component="post/parent"][data-parent-pid="${data.post.pid}"]`);
if (parentEl.length) {
parentEl.find('[component="post/parent/content"]').html(
translator.unescape(data.post.content)
);
parentEl.find('img:not(.not-responsive)').addClass('img-fluid');
parentEl.find('[component="post/parent/content]" img:not(.emoji)').each(function () {
images.wrapImageInLink($(this));
});
}
} else {
hooks.fire('action:posts.edited', data);
}
@@ -185,26 +196,39 @@ define('forum/topic/events', [
require(['forum/topic/replies'], function (replies) {
replies.onPostPurged(postData);
});
$(`[component="post/parent"][data-parent-pid="${postData.pid}"]`).remove();
}
function togglePostDeleteState(data) {
const postEl = components.get('post', 'pid', data.pid);
if (!postEl.length) {
return;
const { isAdminOrMod } = ajaxify.data.privileges;
const isSelfPost = String(data.uid) === String(app.user.uid);
const isDeleted = !!data.deleted;
if (postEl.length) {
postEl.toggleClass('deleted');
postTools.toggle(data.pid, isDeleted);
if (!isAdminOrMod && !isSelfPost) {
postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted);
if (isDeleted) {
postEl.find('[component="post/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
postEl.find('[component="post/content"]').html(translator.unescape(data.content));
}
}
}
postEl.toggleClass('deleted');
const isDeleted = postEl.hasClass('deleted');
postTools.toggle(data.pid, isDeleted);
if (!ajaxify.data.privileges.isAdminOrMod && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) {
postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted);
if (isDeleted) {
postEl.find('[component="post/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
postEl.find('[component="post/content"]').html(translator.unescape(data.content));
}
const parentEl = $(`[component="post/parent"][data-parent-pid="${data.pid}"]`);
if (parentEl.length) {
parentEl.each((i, el) => {
const $parent = $(el);
if (isDeleted) {
$parent.find('[component="post/parent/content"]').translateHtml('[[topic:post-is-deleted]]');
} else {
$parent.find('[component="post/parent/content"]').html(translator.unescape(data.content));
}
});
}
}

View File

@@ -36,6 +36,7 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f
'downvote:disabled': ajaxify.data['downvote:disabled'],
'reputation:disabled': ajaxify.data['reputation:disabled'],
loggedIn: !!app.user.uid,
hideParent: true,
hideReplies: config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true,
};
app.parseAndTranslate('topic', 'posts', tplData, async function (html) {

View File

@@ -137,7 +137,7 @@ module.exports = function (Messaging) {
parentMids = parentMids.filter((mid, idx) => canView[idx]);
const parentMessages = await Messaging.getMessagesFields(parentMids, [
'fromuid', 'content', 'timestamp', 'deleted',
'mid', 'fromuid', 'content', 'timestamp', 'deleted',
]);
const parentUids = _.uniq(parentMessages.map(msg => msg && msg.fromuid));
const usersMap = _.zipObject(

View File

@@ -28,7 +28,7 @@ module.exports = function (Posts) {
deleted: isDeleting ? 1 : 0,
deleterUid: isDeleting ? uid : 0,
});
const postData = await Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp']);
const postData = await Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp', 'deleted']);
const topicData = await topics.getTopicFields(postData.tid, ['tid', 'cid', 'pinned']);
postData.cid = topicData.cid;
await Promise.all([

View File

@@ -45,7 +45,7 @@ privsPosts.get = async function (pids, uid) {
const privileges = cids.map((cid, i) => {
const isAdminOrMod = results.isAdmin || isModerator[cid];
const editable = (privData['posts:edit'][cid] && (results.isOwner[i] || results.isModerator)) || results.isAdmin;
const editable = (privData['posts:edit'][cid] && (results.isOwner[i] || results.isModerator[i])) || results.isAdmin;
const viewDeletedPosts = results.isOwner[i] || privData['posts:view_deleted'][cid] || results.isAdmin;
const viewHistory = results.isOwner[i] || privData['posts:history'][cid] || results.isAdmin;

View File

@@ -233,7 +233,7 @@ module.exports = function (Topics) {
posts.getUserInfoForPosts([postOwner], uid),
]);
await Promise.all([
Topics.addParentPosts([postData]),
Topics.addParentPosts([postData], uid),
Topics.syncBacklinks(postData),
Topics.markAsRead([tid], uid),
]);

View File

@@ -12,6 +12,7 @@ const meta = require('../meta');
const activitypub = require('../activitypub');
const plugins = require('../plugins');
const utils = require('../utils');
const privileges = require('../privileges');
const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g');
@@ -129,7 +130,7 @@ module.exports = function (Topics) {
getPostUserData('uid', async uids => await posts.getUserInfoForPosts(uids, uid)),
getPostUserData('editor', async uids => await user.getUsersFields(uids, ['uid', 'username', 'userslug'])),
getPostReplies(postData, uid),
Topics.addParentPosts(postData),
Topics.addParentPosts(postData, uid),
]);
postData.forEach((postObj, i) => {
@@ -178,7 +179,7 @@ module.exports = function (Topics) {
});
};
Topics.addParentPosts = async function (postData) {
Topics.addParentPosts = async function (postData, callerUid) {
let parentPids = postData
.filter(p => p && p.hasOwnProperty('toPid') && (activitypub.helpers.isUri(p.toPid) || utils.isNumber(p.toPid)))
.map(postObj => postObj.toPid);
@@ -187,16 +188,40 @@ module.exports = function (Topics) {
return;
}
parentPids = _.uniq(parentPids);
const parentPosts = await posts.getPostsFields(parentPids, ['uid']);
const postPrivileges = await privileges.posts.get(parentPids, callerUid);
const pidToPrivs = _.zipObject(parentPids, postPrivileges);
parentPids = parentPids.filter(p => pidToPrivs[p]['topics:read']);
const parentPosts = await posts.getPostsFields(parentPids, ['uid', 'pid', 'timestamp', 'content', 'deleted']);
const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid));
const userData = await user.getUsersFields(parentUids, ['username']);
const userData = await user.getUsersFields(parentUids, ['username', 'userslug', 'picture']);
const usersMap = _.zipObject(parentUids, userData);
await Promise.all(parentPosts.map(async (parentPost) => {
const postPrivs = pidToPrivs[parentPost.pid];
if (parentPost.deleted && String(parentPost.uid) !== String(callerUid, 10) && !postPrivs['posts:view_deleted']) {
parentPost.content = `<p>[[topic:post-is-deleted]]</p>`;
return;
}
const foundPost = postData.find(p => String(p.pid) === String(parentPost.pid));
if (foundPost) {
parentPost.content = foundPost.content;
return;
}
parentPost = await posts.parsePost(parentPost);
}));
const parents = {};
parentPosts.forEach((post, i) => {
if (usersMap[post.uid]) {
parents[parentPids[i]] = {
uid: post.uid,
pid: post.pid,
content: post.content,
user: usersMap[post.uid],
timestamp: post.timestamp,
timestampISO: post.timestampISO,
username: usersMap[post.uid].username,
displayname: usersMap[post.uid].displayname,
};

View File

@@ -8,6 +8,6 @@
</div>
<span class="chat-timestamp text-muted timeago text-nowrap hidden" title="{messages.parent.timestampISO}"></span>
</div>
<div component="chat/message/parent/content" class="text-muted line-clamp-1 w-100">{messages.parent.content}</div>
<div component="chat/message/parent/content" class="text-muted line-clamp-1 text-break w-100">{messages.parent.content}</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<div component="post/parent" data-parent-pid="{./parent.pid}" data-uid="{./parent.uid}" class="btn btn-ghost btn-sm d-flex gap-2 text-start flex-row mb-2" style="font-size: 13px;">
<div class="d-flex gap-2 text-nowrap">
<div><i class="fa fa-sm fa-reply opacity-50"></i></div>
<div class="d-flex flex-nowrap gap-1 align-items-center">
<a href="{config.relative_path}/user/{./parent.user.userslug}" class="text-decoration-none lh-1">{buildAvatar(./parent.user, "14px", true, "not-responsive align-middle")}</a>
<a class="fw-semibold" href="{config.relative_path}/user/{./parent.user.userslug}">{./parent.user.displayname}</a>
</div>
<a href="{config.relative_path}/post/{./parent.pid}" class="text-muted timeago text-nowrap hidden" title="{./parent.timestampISO}"></a>
</div>
<div component="post/parent/content" class="text-muted line-clamp-1 text-break w-100">{./parent.content}</div>
</div>