mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: asserted topics and posts to remote categories will notify and add to unread based on remote category watch state
This commit is contained in:
@@ -70,7 +70,7 @@ define('forum/category', [
|
|||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const state = $this.attr('data-state');
|
const state = $this.attr('data-state');
|
||||||
|
|
||||||
api.put(`/categories/${cid}/watch`, { state }, (err) => {
|
api.put(`/categories/${encodeURIComponent(cid)}/watch`, { state }, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return alerts.error(err);
|
return alerts.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const events = require('../events');
|
|||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const groups = require('../groups');
|
const groups = require('../groups');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
|
const utils = require('../utils');
|
||||||
|
|
||||||
const activitypubApi = require('./activitypub');
|
const activitypubApi = require('./activitypub');
|
||||||
|
|
||||||
@@ -157,7 +158,9 @@ categoriesAPI.getTopics = async (caller, data) => {
|
|||||||
|
|
||||||
categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => {
|
categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => {
|
||||||
let targetUid = caller.uid;
|
let targetUid = caller.uid;
|
||||||
const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)];
|
let cids = Array.isArray(cid) ? cid : [cid];
|
||||||
|
cids = cids.map(cid => (utils.isNumber(cid) ? parseInt(cid, 10) : cid));
|
||||||
|
|
||||||
if (uid) {
|
if (uid) {
|
||||||
targetUid = uid;
|
targetUid = uid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const activitypub = require('../activitypub');
|
const activitypub = require('../activitypub');
|
||||||
|
const utils = require('../utils');
|
||||||
|
|
||||||
module.exports = function (Categories) {
|
module.exports = function (Categories) {
|
||||||
Categories.watchStates = {
|
Categories.watchStates = {
|
||||||
@@ -32,7 +33,11 @@ module.exports = function (Categories) {
|
|||||||
user.getSettings(uid),
|
user.getSettings(uid),
|
||||||
db.sortedSetsScore(keys, uid),
|
db.sortedSetsScore(keys, uid),
|
||||||
]);
|
]);
|
||||||
return states.map(state => state || Categories.watchStates[userSettings.categoryWatchState]);
|
|
||||||
|
const fallbacks = cids.map(cid => (utils.isNumber(cid) ?
|
||||||
|
Categories.watchStates[userSettings.categoryWatchState] : Categories.watchStates.notwatching));
|
||||||
|
|
||||||
|
return states.map((state, idx) => state || fallbacks[idx]);
|
||||||
};
|
};
|
||||||
|
|
||||||
Categories.getIgnorers = async function (cid, start, stop) {
|
Categories.getIgnorers = async function (cid, start, stop) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ const uidToSystemGroup = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
helpers.isUsersAllowedTo = async function (privilege, uids, cid) {
|
helpers.isUsersAllowedTo = async function (privilege, uids, cid) {
|
||||||
|
// Remote categories inherit world pseudo-category privileges
|
||||||
|
if (!utils.isNumber(cid)) {
|
||||||
|
cid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
|
const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
|
||||||
groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`),
|
groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`),
|
||||||
groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`),
|
groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`),
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ module.exports = function (User) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
User.getCategoriesByStates = async function (uid, states) {
|
User.getCategoriesByStates = async function (uid, states) {
|
||||||
const cids = await categories.getAllCidsFromSet('categories:cid');
|
const remoteCids = await db.getObjectValues('handle:cid');
|
||||||
|
const cids = [
|
||||||
|
(await categories.getAllCidsFromSet('categories:cid')),
|
||||||
|
...remoteCids,
|
||||||
|
];
|
||||||
if (!(parseInt(uid, 10) > 0)) {
|
if (!(parseInt(uid, 10) > 0)) {
|
||||||
return cids;
|
return cids;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const user = require('../../src/user');
|
|||||||
const categories = require('../../src/categories');
|
const categories = require('../../src/categories');
|
||||||
const posts = require('../../src/posts');
|
const posts = require('../../src/posts');
|
||||||
const topics = require('../../src/topics');
|
const topics = require('../../src/topics');
|
||||||
|
const api = require('../../src/api');
|
||||||
const activitypub = require('../../src/activitypub');
|
const activitypub = require('../../src/activitypub');
|
||||||
const utils = require('../../src/utils');
|
const utils = require('../../src/utils');
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ describe('Notes', () => {
|
|||||||
await install.giveWorldPrivileges();
|
await install.giveWorldPrivileges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.only('Public objects', () => {
|
describe('Public objects', () => {
|
||||||
it('should pull a remote root-level object by its id and create a new topic', async () => {
|
it('should pull a remote root-level object by its id and create a new topic', async () => {
|
||||||
const { id } = helpers.mocks.note();
|
const { id } = helpers.mocks.note();
|
||||||
const assertion = await activitypub.notes.assert(0, id, { skipChecks: true });
|
const assertion = await activitypub.notes.assert(0, id, { skipChecks: true });
|
||||||
@@ -64,10 +65,11 @@ describe('Notes', () => {
|
|||||||
assert(exists);
|
assert(exists);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Category-specific behaviours', () => {
|
||||||
it('should slot newly created topic in local category if addressed', async () => {
|
it('should slot newly created topic in local category if addressed', async () => {
|
||||||
const { cid } = await categories.create({ name: utils.generateUUID() });
|
const { cid } = await categories.create({ name: utils.generateUUID() });
|
||||||
const { id } = helpers.mocks.note({
|
const { id } = helpers.mocks.note({
|
||||||
cc: ['https://example.org/user/foobar/followers', `${nconf.get('url')}/category/${cid}`],
|
cc: [`${nconf.get('url')}/category/${cid}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const assertion = await activitypub.notes.assert(0, id);
|
const assertion = await activitypub.notes.assert(0, id);
|
||||||
@@ -87,7 +89,7 @@ describe('Notes', () => {
|
|||||||
await activitypub.actors.assertGroup([cid]);
|
await activitypub.actors.assertGroup([cid]);
|
||||||
|
|
||||||
const { id } = helpers.mocks.note({
|
const { id } = helpers.mocks.note({
|
||||||
cc: ['https://example.org/user/foobar/followers', cid],
|
cc: [cid],
|
||||||
});
|
});
|
||||||
|
|
||||||
const assertion = await activitypub.notes.assert(0, id);
|
const assertion = await activitypub.notes.assert(0, id);
|
||||||
@@ -110,6 +112,75 @@ describe('Notes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('User-specific behaviours', () => {
|
||||||
|
let remoteCid;
|
||||||
|
let uid;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
// Remote
|
||||||
|
const { id, actor } = helpers.mocks.group();
|
||||||
|
remoteCid = id;
|
||||||
|
activitypub._cache.set(`0;${id}`, actor);
|
||||||
|
await activitypub.actors.assertGroup([id]);
|
||||||
|
|
||||||
|
// User
|
||||||
|
uid = await user.create({ username: utils.generateUUID() });
|
||||||
|
await topics.markAllRead(uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show up in my unread if it is in cid -1', async () => {
|
||||||
|
const { id } = helpers.mocks.note();
|
||||||
|
const assertion = await activitypub.notes.assert(0, id, { skipChecks: 1 });
|
||||||
|
assert(assertion);
|
||||||
|
|
||||||
|
const unread = await topics.getTotalUnread(uid);
|
||||||
|
assert.strictEqual(unread, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show up in my recent/unread if I am tracking the remote category', async () => {
|
||||||
|
await api.categories.setWatchState({ uid }, {
|
||||||
|
cid: remoteCid,
|
||||||
|
state: categories.watchStates.tracking,
|
||||||
|
uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = helpers.mocks.note({
|
||||||
|
cc: [remoteCid],
|
||||||
|
});
|
||||||
|
const assertion = await activitypub.notes.assert(0, id);
|
||||||
|
assert(assertion);
|
||||||
|
|
||||||
|
const unread = await topics.getTotalUnread(uid);
|
||||||
|
assert.strictEqual(unread, 1);
|
||||||
|
|
||||||
|
await topics.markAllRead(uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show up in recent/unread and notify me if I am watching the remote category', async () => {
|
||||||
|
await api.categories.setWatchState({ uid }, {
|
||||||
|
cid: remoteCid,
|
||||||
|
state: categories.watchStates.watching,
|
||||||
|
uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id, note } = helpers.mocks.note({
|
||||||
|
cc: [remoteCid],
|
||||||
|
});
|
||||||
|
const assertion = await activitypub.notes.assert(0, id);
|
||||||
|
assert(assertion);
|
||||||
|
|
||||||
|
const unread = await topics.getTotalUnread(uid);
|
||||||
|
assert.strictEqual(unread, 1);
|
||||||
|
|
||||||
|
// Notification inbox delivery is async so can't test directly
|
||||||
|
const exists = await db.exists(`notifications:new_topic:tid:${assertion.tid}:uid:${note.attributedTo}`);
|
||||||
|
assert(exists);
|
||||||
|
|
||||||
|
await topics.markAllRead(uid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Private objects', () => {
|
describe('Private objects', () => {
|
||||||
let recipientUid;
|
let recipientUid;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user