refactor: announces

store number of announces on post hash, show announces like votes, with tooltip and a way to see all, remove them from topic.events so they dont load all tid:<tid>:posts everytime topic is loaded
This commit is contained in:
Barış Soner Uşaklı
2024-06-17 11:18:48 -04:00
parent 05b7828e33
commit c021e7e80f
12 changed files with 162 additions and 28 deletions

View File

@@ -13,6 +13,6 @@
"help.federating": "Likewise, if users from outside of this forum start following <em>you</em>, then your posts will start appearing on those apps and websites as well.",
"help.next-generation": "This is the next generation of social media, start contributing today!",
"topic-event-announce-ago": "%1 shared <a href=\"%2\">this post</a> %3",
"topic-event-announce-on": "%1 shared <a href=\"%2\">this post</a> on %3"
"announcers": "Announcers",
"announcers-x": "Announcers (%1)"
}

View File

@@ -186,6 +186,10 @@ paths:
$ref: 'write/posts/pid/voters.yaml'
/posts/{pid}/upvoters:
$ref: 'write/posts/pid/upvoters.yaml'
/posts/{pid}/announcers:
$ref: 'write/posts/pid/announcers.yaml'
/posts/{pid}/announcers/tooltip:
$ref: 'write/posts/pid/announcers-tooltip.yaml'
/posts/{pid}/bookmark:
$ref: 'write/posts/pid/bookmark.yaml'
/posts/{pid}/diffs:

View File

@@ -0,0 +1,33 @@
get:
tags:
- posts
summary: get announcers of a post
description: This is used for getting a list of usernames for the announcers tooltip
parameters:
- in: path
name: pid
schema:
type: string
required: true
description: a valid post id
example: 2
responses:
'200':
description: Usernames of announcers of post
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
properties:
otherCount:
type: number
usernames:
type: array
cutoff:
type: number

View File

@@ -0,0 +1,32 @@
get:
tags:
- posts
summary: get announcers of a post
description: This returns the announcers of a post if the user has permission to view them
parameters:
- in: path
name: pid
schema:
type: string
required: true
description: a valid post id
example: 2
responses:
'200':
description: Data about announcers of this post
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
properties:
announceCount:
type: number
announcers:
type: array

View File

@@ -141,6 +141,10 @@ define('forum/topic/postTools', [
votes.showVotes(getData($(this), 'data-pid'));
});
postContainer.on('click', '[component="post/announce-count"]', function () {
votes.showAnnouncers(getData($(this), 'data-pid'));
});
postContainer.on('click', '[component="post/flag"]', function () {
const pid = getData($(this), 'data-pid');
require(['flags'], function (flags) {

View File

@@ -13,6 +13,9 @@ define('forum/topic/votes', [
components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip);
components.get('topic').on('mouseleave', '[data-pid] [component="post/vote-count"]', destroyTooltip);
}
components.get('topic').on('mouseenter', '[data-pid] [component="post/announce-count"]', loadDataAndCreateTooltip);
components.get('topic').on('mouseleave', '[data-pid] [component="post/announce-count"]', destroyTooltip);
};
function canSeeVotes() {
@@ -43,8 +46,11 @@ define('forum/topic/votes', [
tooltip.dispose();
$this.attr('title', '');
}
const path = $this.attr('component') === 'post/vote-count' ?
`/posts/${encodeURIComponent(pid)}/upvoters` :
`/posts/${encodeURIComponent(pid)}/announcers/tooltip`;
api.get(`/posts/${encodeURIComponent(pid)}/upvoters`, {}, function (err, data) {
api.get(path, {}, function (err, data) {
if (err) {
return alerts.error(err);
}
@@ -132,6 +138,24 @@ define('forum/topic/votes', [
});
};
Votes.showAnnouncers = async function (pid) {
const data = await api.get(`/posts/${encodeURIComponent(pid)}/announcers`, {})
.catch(err => alerts.error(err));
const html = await app.parseAndTranslate('modals/announcers', data);
const dialog = bootbox.dialog({
title: `[[activitypub:announcers-x, ${data.announceCount}]]`,
message: html,
className: 'announce-modal',
show: true,
onEscape: true,
backdrop: true,
});
dialog.on('click', function () {
dialog.modal('hide');
});
};
return Votes;
});

View File

@@ -365,14 +365,24 @@ Notes.announce.list = async ({ pid, tid }) => {
Notes.announce.add = async (pid, actor, timestamp = Date.now()) => {
await db.sortedSetAdd(`pid:${pid}:announces`, timestamp, actor);
await posts.setPostField(pid, 'announces', await db.sortedSetCard(`pid:${pid}:announces`));
};
Notes.announce.remove = async (pid, actor) => {
await db.sortedSetRemove(`pid:${pid}:announces`, actor);
const count = await db.sortedSetCard(`pid:${pid}:announces`);
if (count > 0) {
await posts.setPostField(pid, 'announces', count);
} else {
await db.deleteObjectField(`post:${pid}`, 'announces');
}
};
Notes.announce.removeAll = async (pid) => {
await db.delete(`pid:${pid}:announces`);
await Promise.all([
db.delete(`pid:${pid}:announces`),
db.deleteObjectField(`post:${pid}`, 'announces'),
]);
};
Notes.delete = async (pids) => {

View File

@@ -364,9 +364,13 @@ postsAPI.getUpvoters = async function (caller, data) {
throw new Error('[[error:no-privileges]]');
}
let upvotedUids = (await posts.getUpvotedUidsByPids([pid]))[0];
const upvotedUids = (await posts.getUpvotedUidsByPids([pid]))[0];
return await getTooltipData(upvotedUids);
};
async function getTooltipData(uids) {
const cutoff = 6;
if (!upvotedUids.length) {
if (!uids.length) {
return {
otherCount: 0,
usernames: [],
@@ -374,17 +378,41 @@ postsAPI.getUpvoters = async function (caller, data) {
};
}
let otherCount = 0;
if (upvotedUids.length > cutoff) {
otherCount = upvotedUids.length - (cutoff - 1);
upvotedUids = upvotedUids.slice(0, cutoff - 1);
if (uids.length > cutoff) {
otherCount = uids.length - (cutoff - 1);
uids = uids.slice(0, cutoff - 1);
}
const usernames = await user.getUsernamesByUids(upvotedUids);
const usernames = await user.getUsernamesByUids(uids);
return {
otherCount,
usernames,
cutoff,
};
}
postsAPI.getAnnouncers = async (caller, data) => {
if (!data.pid) {
throw new Error('[[error:invalid-data]]');
}
if (!meta.config.activitypubEnabled) {
return [];
}
const { pid } = data;
const cid = await posts.getCidByPid(pid);
if (!await privileges.categories.isUserAllowedTo('topics:read', cid, caller.uid)) {
throw new Error('[[error:no-privileges]]');
}
const notes = require('../activitypub/notes');
const announcers = await notes.announce.list({ pid });
const uids = announcers.map(ann => ann.actor);
if (data.tooltip) {
return await getTooltipData(uids);
}
return {
announceCount: uids.length,
announcers: await user.getUsersFields(uids, ['username', 'userslug', 'picture']),
};
};
async function canSeeVotes(uid, cids) {

View File

@@ -141,6 +141,16 @@ Posts.getUpvoters = async (req, res) => {
helpers.formatApiResponse(200, res, data);
};
Posts.getAnnouncers = async (req, res) => {
const data = await api.posts.getAnnouncers(req, { pid: req.params.pid, tooltip: 0 });
helpers.formatApiResponse(200, res, data);
};
Posts.getAnnouncersTooltip = async (req, res) => {
const data = await api.posts.getAnnouncers(req, { pid: req.params.pid, tooltip: 1 });
helpers.formatApiResponse(200, res, data);
};
Posts.bookmark = async (req, res) => {
const data = await mock(req);
await api.posts.bookmark(req, data);

View File

@@ -29,6 +29,8 @@ module.exports = function () {
setupApiRoute(router, 'get', '/:pid/voters', [middleware.assert.post], controllers.write.posts.getVoters);
setupApiRoute(router, 'get', '/:pid/upvoters', [middleware.assert.post], controllers.write.posts.getUpvoters);
setupApiRoute(router, 'get', '/:pid/announcers', [middleware.assert.post], controllers.write.posts.getAnnouncers);
setupApiRoute(router, 'get', '/:pid/announcers/tooltip', [middleware.assert.post], controllers.write.posts.getAnnouncersTooltip);
setupApiRoute(router, 'put', '/:pid/bookmark', middlewares, controllers.write.posts.bookmark);
setupApiRoute(router, 'delete', '/:pid/bookmark', middlewares, controllers.write.posts.unbookmark);

View File

@@ -10,7 +10,6 @@ const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
const privileges = require('../privileges');
const activitypub = require('../activitypub');
const utils = require('../utils');
const helpers = require('../helpers');
@@ -69,10 +68,6 @@ Events._types = {
icon: 'fa-code-fork',
translation: async (event, language) => translateEventArgs(event, language, 'topic:user-forked-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
announce: {
icon: 'fa-share-alt',
translation: async (event, language) => translateEventArgs(event, language, 'activitypub:topic-event-announce', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
};
Events.init = async () => {
@@ -175,19 +170,6 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
});
}
// Add post announces
const announces = await activitypub.notes.announce.list({ tid });
announces.forEach(({ actor, pid, timestamp }) => {
events.push({
type: 'announce',
uid: actor,
href: `/post/${encodeURIComponent(pid)}`,
pid,
timestamp,
});
timestamps.push(timestamp);
});
const [users, fromCategories, userSettings] = await Promise.all([
getUserInfo(events.map(event => event.uid).filter(Boolean)),
getCategoryInfo(events.map(event => event.fromCid).filter(Boolean)),

View File

@@ -0,0 +1,5 @@
<div class="mb-3">
{{{ each announcers }}}
<a class="text-decoration-none" href="{config.relative_path}/user/{./userslug}">{buildAvatar(@value, "24px", true)}</a>
{{{ end }}}
</div>