diff --git a/public/language/en-GB/activitypub.json b/public/language/en-GB/activitypub.json index 54fa13b6e3..d591d321ba 100644 --- a/public/language/en-GB/activitypub.json +++ b/public/language/en-GB/activitypub.json @@ -13,6 +13,6 @@ "help.federating": "Likewise, if users from outside of this forum start following you, 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 this post %3", - "topic-event-announce-on": "%1 shared this post on %3" + "announcers": "Announcers", + "announcers-x": "Announcers (%1)" } \ No newline at end of file diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 1031a4ecab..d56b109e15 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -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: diff --git a/public/openapi/write/posts/pid/announcers-tooltip.yaml b/public/openapi/write/posts/pid/announcers-tooltip.yaml new file mode 100644 index 0000000000..c7aa134b63 --- /dev/null +++ b/public/openapi/write/posts/pid/announcers-tooltip.yaml @@ -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 + diff --git a/public/openapi/write/posts/pid/announcers.yaml b/public/openapi/write/posts/pid/announcers.yaml new file mode 100644 index 0000000000..282b8432dd --- /dev/null +++ b/public/openapi/write/posts/pid/announcers.yaml @@ -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 + + diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index b53e46440a..c0745ffb68 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -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) { diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js index 3a2ef5af8e..1149c3d900 100644 --- a/public/src/client/topic/votes.js +++ b/public/src/client/topic/votes.js @@ -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; }); diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 0b750e1498..94156940dc 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -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) => { diff --git a/src/api/posts.js b/src/api/posts.js index 339a6b8943..0af9558823 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -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) { diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 1dc8cf6800..7b5a016d86 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -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); diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index e573bbb9b0..829dd56df9 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -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); diff --git a/src/topics/events.js b/src/topics/events.js index f35b18ea42..ffb089bbef 100644 --- a/src/topics/events.js +++ b/src/topics/events.js @@ -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)), diff --git a/src/views/modals/announcers.tpl b/src/views/modals/announcers.tpl new file mode 100644 index 0000000000..2d95bad25b --- /dev/null +++ b/src/views/modals/announcers.tpl @@ -0,0 +1,5 @@ +