mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-07 22:45:46 +01:00
Tag follow (#12041)
* feat: tag follow * on tag delete remove it from following users * feat: on tag rename update user followed tags and move the * add new filter on /notifications * feat: openapi updates * chore: up themes * chore: up peace * refactor: remove unused title
This commit is contained in:
committed by
GitHub
parent
cf50b0fe49
commit
40d290c1a9
@@ -102,10 +102,10 @@
|
||||
"nodebb-plugin-ntfy": "1.7.2",
|
||||
"nodebb-plugin-spam-be-gone": "2.2.0",
|
||||
"nodebb-rewards-essentials": "0.2.3",
|
||||
"nodebb-theme-harmony": "1.1.63",
|
||||
"nodebb-theme-harmony": "1.1.64",
|
||||
"nodebb-theme-lavender": "7.1.3",
|
||||
"nodebb-theme-peace": "2.1.19",
|
||||
"nodebb-theme-persona": "13.2.32",
|
||||
"nodebb-theme-peace": "2.1.20",
|
||||
"nodebb-theme-persona": "13.2.33",
|
||||
"nodebb-widget-essentials": "7.0.13",
|
||||
"nodemailer": "6.9.5",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
"all": "All",
|
||||
"topics": "Topics",
|
||||
"tags": "Tags",
|
||||
"replies": "Replies",
|
||||
"chat": "Chats",
|
||||
"group-chat": "Group Chats",
|
||||
@@ -50,6 +51,12 @@
|
||||
"user_posted_to_multiple" : "<strong>%1</strong>, <strong>%2</strong> and %3 others have posted replies to: <strong>%4</strong>",
|
||||
"user_posted_topic": "<strong>%1</strong> has posted a new topic: <strong>%2</strong>",
|
||||
"user_edited_post" : "<strong>%1</strong> has edited a post in <strong>%2</strong>",
|
||||
|
||||
"user_posted_topic_with_tag": "<strong>%1</strong> has posted a new topic with tag <strong>%2</strong>",
|
||||
"user_posted_topic_with_tag_dual": "<strong>%1</strong> has posted a new topic with tags <strong>%2</strong> and <strong>%3</strong>",
|
||||
"user_posted_topic_with_tag_triple": "<strong>%1</strong> has posted a new topic with tags <strong>%2</strong>, <strong>%3</strong> and <strong>%4</strong>",
|
||||
"user_posted_topic_with_tag_multiple": "<strong>%1</strong> has posted a new topic with tags <strong>%2</strong>",
|
||||
|
||||
"user_started_following_you": "<strong>%1</strong> started following you.",
|
||||
"user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.",
|
||||
"user_started_following_you_triple": "<strong>%1</strong>, <strong>%2</strong> and <strong>%3</strong> started following you.",
|
||||
@@ -77,6 +84,7 @@
|
||||
"notification_and_email": "Notification & Email",
|
||||
"notificationType_upvote": "When someone upvotes your post",
|
||||
"notificationType_new-topic": "When someone you follow posts a topic",
|
||||
"notificationType_new-topic-with-tag": "When a topic is posted with a tag you follow",
|
||||
"notificationType_new-reply": "When a new reply is posted in a topic you are watching",
|
||||
"notificationType_post-edit": "When a post is edited in a topic you are watching",
|
||||
"notificationType_follow": "When someone starts following you",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"account/topics": "Topics created by %1",
|
||||
"account/groups": "%1's Groups",
|
||||
"account/watched_categories": "%1's Watched Categories",
|
||||
"account/watched-tags": "%1's Watched Tags",
|
||||
"account/bookmarks": "%1's Bookmarked Posts",
|
||||
"account/settings": "User Settings",
|
||||
"account/settings-of": "Changing settings of %1",
|
||||
|
||||
@@ -7,5 +7,11 @@
|
||||
"enter_tags_here_short": "Enter tags...",
|
||||
"no_tags": "There are no tags yet.",
|
||||
"select_tags": "Select Tags",
|
||||
"tag-whitelist": "Tag Whitelist"
|
||||
"tag-whitelist": "Tag Whitelist",
|
||||
"watching": "Watching",
|
||||
"not-watching": "Not Watching",
|
||||
"watching.description": "Notify me of new topics.",
|
||||
"not-watching.description": "Do not notify me of new topics.",
|
||||
"following-tag.message": "You will now be receiving notifications when somebody posts a topic with this tag.",
|
||||
"not-following-tag.message": "You will not receive notifications when somebody posts a topic with this tag."
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
"reputation": "Reputation",
|
||||
"bookmarks":"Bookmarks",
|
||||
"watched_categories": "Watched categories",
|
||||
"watched-tags": "Watched tags",
|
||||
"change_all": "Change All",
|
||||
"watched": "Watched",
|
||||
"ignored": "Ignored",
|
||||
|
||||
@@ -80,6 +80,9 @@ Settings:
|
||||
notificationType_new-topic:
|
||||
type: string
|
||||
description: Notification type for new topics
|
||||
notificationType_new-topic-with-tag:
|
||||
type: string
|
||||
description: Notification type for new topics with followed tag
|
||||
notificationType_follow:
|
||||
type: string
|
||||
description: Notification type for another user following you
|
||||
|
||||
@@ -391,6 +391,8 @@ UserObjectFull:
|
||||
type: number
|
||||
categoriesWatched:
|
||||
type: number
|
||||
tagsWatched:
|
||||
type: number
|
||||
downvoted:
|
||||
type: number
|
||||
followers:
|
||||
|
||||
@@ -262,6 +262,8 @@ paths:
|
||||
$ref: 'read/user/userslug/followers.yaml'
|
||||
"/api/user/{userslug}/categories":
|
||||
$ref: 'read/user/userslug/categories.yaml'
|
||||
"/api/user/{userslug}/tags":
|
||||
$ref: 'read/user/userslug/tags.yaml'
|
||||
"/api/user/{userslug}/posts":
|
||||
$ref: 'read/user/userslug/posts.yaml'
|
||||
"/api/user/{userslug}/topics":
|
||||
|
||||
@@ -229,6 +229,9 @@ get:
|
||||
type: string
|
||||
canPost:
|
||||
type: boolean
|
||||
isFollowing:
|
||||
type: boolean
|
||||
description: true is user is following this tag
|
||||
showSelect:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
|
||||
30
public/openapi/read/user/userslug/tags.yaml
Normal file
30
public/openapi/read/user/userslug/tags.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
summary: Get user's watched tags
|
||||
description: This route retrieves the list of tags the user is watching
|
||||
parameters:
|
||||
- name: userslug
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: admin
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
|
||||
- type: object
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
- $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
|
||||
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -146,6 +146,8 @@ paths:
|
||||
$ref: 'write/topics/tid/read.yaml'
|
||||
/topics/{tid}/bump:
|
||||
$ref: 'write/topics/tid/bump.yaml'
|
||||
/tags/{tag}/follow:
|
||||
$ref: 'write/tags/tag/follow.yaml'
|
||||
/posts/{pid}:
|
||||
$ref: 'write/posts/pid.yaml'
|
||||
/posts/{pid}/index:
|
||||
|
||||
52
public/openapi/write/tags/tag/follow.yaml
Normal file
52
public/openapi/write/tags/tag/follow.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
put:
|
||||
tags:
|
||||
- tags
|
||||
summary: follow a tag
|
||||
description: This operation follows (or watches) a tag.
|
||||
parameters:
|
||||
- in: path
|
||||
name: tag
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid tag name
|
||||
example: plugins
|
||||
responses:
|
||||
'200':
|
||||
description: Tag successfully followed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
delete:
|
||||
tags:
|
||||
- tags
|
||||
summary: unfollow a tag
|
||||
description: This operation unfollows (or unwatches) a tag.
|
||||
parameters:
|
||||
- in: path
|
||||
name: tag
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid tag name
|
||||
example: plugins
|
||||
responses:
|
||||
'200':
|
||||
description: Tag successfully unwatched
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
56
public/src/client/account/tags.js
Normal file
56
public/src/client/account/tags.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('forum/account/tags', [
|
||||
'forum/account/header', 'alerts', 'api', 'hooks', 'autocomplete',
|
||||
], function (header, alerts, api, hooks, autocomplete) {
|
||||
const Tags = {};
|
||||
|
||||
Tags.init = function () {
|
||||
header.init();
|
||||
|
||||
const tagEl = $('[component="tags/watch"]');
|
||||
tagEl.tagsinput({
|
||||
tagClass: 'badge bg-info',
|
||||
confirmKeys: [13, 44],
|
||||
trimValue: true,
|
||||
});
|
||||
const input = tagEl.siblings('.bootstrap-tagsinput').find('input');
|
||||
autocomplete.tag(input);
|
||||
|
||||
ajaxify.data.tags.forEach(function (tag) {
|
||||
tagEl.tagsinput('add', tag);
|
||||
});
|
||||
|
||||
tagEl.on('itemAdded', function (event) {
|
||||
if (input.length) {
|
||||
input.autocomplete('close');
|
||||
}
|
||||
api.put(`/tags/${event.item}/follow`, {}).then(() => {
|
||||
alerts.alert({
|
||||
alert_id: 'follow_tag',
|
||||
message: '[[tags:following-tag.message]]',
|
||||
type: 'success',
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
hooks.fire('action:tags.changeWatching', { tag: ajaxify.data.tag, type: 'follow' });
|
||||
}).catch(err => alerts.error(err));
|
||||
});
|
||||
|
||||
tagEl.on('itemRemoved', function (event) {
|
||||
api.del(`/tags/${event.item}/follow`, {}).then(() => {
|
||||
alerts.alert({
|
||||
alert_id: 'follow_tag',
|
||||
message: '[[tags:not-following-tag.message]]',
|
||||
type: 'info',
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
hooks.fire('action:tags.changeWatching', { tag: ajaxify.data.tag, type: 'unfollow' });
|
||||
}).catch(err => alerts.error(err));
|
||||
});
|
||||
};
|
||||
|
||||
return Tags;
|
||||
});
|
||||
@@ -1,12 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
define('forum/tag', ['topicList', 'forum/infinitescroll'], function (topicList) {
|
||||
define('forum/tag', [
|
||||
'topicList', 'api', 'alerts', 'hooks', 'translator', 'bootstrap', 'components',
|
||||
], function (topicList, api, alerts, hooks, translator, bootstrap, components) {
|
||||
const Tag = {};
|
||||
|
||||
Tag.init = function () {
|
||||
app.enterRoom('tags');
|
||||
|
||||
topicList.init('tag');
|
||||
|
||||
$('[component="tag/following"]').on('click', function () {
|
||||
changeWatching('follow', 'put');
|
||||
});
|
||||
|
||||
$('[component="tag/not-following"]').on('click', function () {
|
||||
changeWatching('unfollow', 'del');
|
||||
});
|
||||
|
||||
function changeWatching(type, method) {
|
||||
api[method](`/tags/${ajaxify.data.tag}/follow`, {}).then(() => {
|
||||
let message = '';
|
||||
if (type === 'follow') {
|
||||
message = '[[tags:following-tag.message]]';
|
||||
} else if (type === 'unfollow') {
|
||||
message = '[[tags:not-following-tag.message]]';
|
||||
}
|
||||
|
||||
setFollowState(type);
|
||||
|
||||
alerts.alert({
|
||||
alert_id: 'follow_tag',
|
||||
message: message,
|
||||
type: type === 'follow' ? 'success' : 'info',
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
hooks.fire('action:tags.changeWatching', { tag: ajaxify.data.tag, type: type });
|
||||
}).catch(err => alerts.error(err));
|
||||
}
|
||||
|
||||
function setFollowState(state) {
|
||||
const titles = {
|
||||
follow: '[[tags:watching]]',
|
||||
unfollow: '[[tags:not-watching]]',
|
||||
};
|
||||
|
||||
translator.translate(titles[state], function (translatedTitle) {
|
||||
const tooltip = bootstrap.Tooltip.getInstance('[component="tag/watch"]');
|
||||
if (tooltip) {
|
||||
tooltip.setContent({ '.tooltip-inner': translatedTitle });
|
||||
}
|
||||
});
|
||||
|
||||
let menu = components.get('tag/following/menu');
|
||||
menu.toggleClass('hidden', state !== 'follow');
|
||||
components.get('tag/following/check').toggleClass('fa-check', state === 'follow');
|
||||
|
||||
menu = components.get('tag/not-following/menu');
|
||||
menu.toggleClass('hidden', state !== 'unfollow');
|
||||
components.get('tag/not-following/check').toggleClass('fa-check', state === 'unfollow');
|
||||
}
|
||||
};
|
||||
|
||||
return Tag;
|
||||
|
||||
@@ -5,11 +5,11 @@ module.exports = {
|
||||
users: require('./users'),
|
||||
groups: require('./groups'),
|
||||
topics: require('./topics'),
|
||||
tags: require('./tags'),
|
||||
posts: require('./posts'),
|
||||
chats: require('./chats'),
|
||||
categories: require('./categories'),
|
||||
flags: require('./flags'),
|
||||
files: require('./files'),
|
||||
|
||||
utils: require('./utils'),
|
||||
};
|
||||
|
||||
13
src/api/tags.js
Normal file
13
src/api/tags.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const topics = require('../topics');
|
||||
|
||||
const tagsAPI = module.exports;
|
||||
|
||||
tagsAPI.follow = async function (caller, data) {
|
||||
await topics.followTag(data.tag, caller.uid);
|
||||
};
|
||||
|
||||
tagsAPI.unfollow = async function (caller, data) {
|
||||
await topics.unfollowTag(data.tag, caller.uid);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ const accountsController = {
|
||||
edit: require('./accounts/edit'),
|
||||
info: require('./accounts/info'),
|
||||
categories: require('./accounts/categories'),
|
||||
tags: require('./accounts/tags'),
|
||||
settings: require('./accounts/settings'),
|
||||
groups: require('./accounts/groups'),
|
||||
follow: require('./accounts/follow'),
|
||||
|
||||
@@ -178,6 +178,7 @@ async function getCounts(userData, callerUID) {
|
||||
promises.bookmarks = db.sortedSetCard(`uid:${uid}:bookmarks`);
|
||||
promises.uploaded = db.sortedSetCard(`uid:${uid}:uploads`);
|
||||
promises.categoriesWatched = user.getWatchedCategories(uid);
|
||||
promises.tagsWatched = db.sortedSetCard(`uid:${uid}:followed_tags`);
|
||||
promises.blocks = user.getUserField(userData.uid, 'blocksCount');
|
||||
}
|
||||
const counts = await utils.promiseParallel(promises);
|
||||
|
||||
@@ -12,6 +12,7 @@ notificationsController.get = async function (req, res, next) {
|
||||
{ name: '[[notifications:all]]', filter: '' },
|
||||
{ name: '[[global:topics]]', filter: 'new-topic' },
|
||||
{ name: '[[notifications:replies]]', filter: 'new-reply' },
|
||||
{ name: '[[notifications:tags]]', filter: 'new-topic-with-tag' },
|
||||
{ name: '[[notifications:chat]]', filter: 'new-chat' },
|
||||
{ name: '[[notifications:group-chat]]', filter: 'new-group-chat' },
|
||||
{ name: '[[notifications:public-chat]]', filter: 'new-public-chat' },
|
||||
|
||||
25
src/controllers/accounts/tags.js
Normal file
25
src/controllers/accounts/tags.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const tagsController = module.exports;
|
||||
|
||||
tagsController.get = async function (req, res) {
|
||||
if (req.uid !== res.locals.uid) {
|
||||
return helpers.notAllowed(req, res);
|
||||
}
|
||||
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
|
||||
const tagData = await db.getSortedSetRange(`uid:${res.locals.uid}:followed_tags`, 0, -1);
|
||||
|
||||
const payload = {};
|
||||
payload.tags = tagData;
|
||||
payload.title = `[[pages:account/watched-tags, ${username}]]`;
|
||||
payload.breadcrumbs = helpers.buildBreadcrumbs([
|
||||
{ text: username, url: `/user/${userslug}` },
|
||||
{ text: '[[pages:tags]]' },
|
||||
]);
|
||||
|
||||
res.render('account/tags', payload);
|
||||
};
|
||||
@@ -25,12 +25,13 @@ tagsController.getTag = async function (req, res) {
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]', url: '/tags' }, { text: tag }]),
|
||||
title: `[[pages:tag, ${tag}]]`,
|
||||
};
|
||||
const [settings, cids, categoryData, canPost, isPrivileged] = await Promise.all([
|
||||
const [settings, cids, categoryData, canPost, isPrivileged, isFollowing] = await Promise.all([
|
||||
user.getSettings(req.uid),
|
||||
cid || categories.getCidsByPrivilege('categories:cid', req.uid, 'topics:read'),
|
||||
helpers.getSelectedCategory(cid),
|
||||
privileges.categories.canPostTopic(req.uid),
|
||||
user.isPrivileged(req.uid),
|
||||
topics.isFollowingTag(req.params.tag, req.uid),
|
||||
]);
|
||||
const start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||
const stop = start + settings.topicsPerPage - 1;
|
||||
@@ -44,6 +45,7 @@ tagsController.getTag = async function (req, res) {
|
||||
templateData.canPost = canPost;
|
||||
templateData.showSelect = isPrivileged;
|
||||
templateData.showTopicTools = isPrivileged;
|
||||
templateData.isFollowing = isFollowing;
|
||||
templateData.allCategoriesUrl = `tags/${tag}${helpers.buildQueryString(req.query, 'cid', '')}`;
|
||||
templateData.selectedCategory = categoryData.selectedCategory;
|
||||
templateData.selectedCids = categoryData.selectedCids;
|
||||
|
||||
@@ -6,6 +6,7 @@ Write.users = require('./users');
|
||||
Write.groups = require('./groups');
|
||||
Write.categories = require('./categories');
|
||||
Write.topics = require('./topics');
|
||||
Write.tags = require('./tags');
|
||||
Write.posts = require('./posts');
|
||||
Write.chats = require('./chats');
|
||||
Write.flags = require('./flags');
|
||||
|
||||
17
src/controllers/write/tags.js
Normal file
17
src/controllers/write/tags.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../../api');
|
||||
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const Tags = module.exports;
|
||||
|
||||
Tags.follow = async (req, res) => {
|
||||
await api.tags.follow(req, req.params);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Tags.unfollow = async (req, res) => {
|
||||
await api.tags.unfollow(req, req.params);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ const Notifications = module.exports;
|
||||
Notifications.baseTypes = [
|
||||
'notificationType_upvote',
|
||||
'notificationType_new-topic',
|
||||
'notificationType_new-topic-with-tag',
|
||||
'notificationType_new-reply',
|
||||
'notificationType_post-edit',
|
||||
'notificationType_follow',
|
||||
@@ -395,10 +396,10 @@ Notifications.merge = async function (notifications) {
|
||||
}, []);
|
||||
|
||||
differentiators.forEach((differentiator) => {
|
||||
function typeFromUsernames(usernames) {
|
||||
if (usernames.length === 2) {
|
||||
function typeFromLength(items) {
|
||||
if (items.length === 2) {
|
||||
return 'dual';
|
||||
} else if (usernames.length === 3) {
|
||||
} else if (items.length === 3) {
|
||||
return 'triple';
|
||||
}
|
||||
return 'multiple';
|
||||
@@ -419,9 +420,9 @@ Notifications.merge = async function (notifications) {
|
||||
case 'notifications:user_posted_in_public_room': {
|
||||
const usernames = _.uniq(set.map(notifObj => notifObj && notifObj.user && notifObj.user.displayname));
|
||||
if (usernames.length === 2 || usernames.length === 3) {
|
||||
notifObj.bodyShort = `[[${mergeId}_${typeFromUsernames(usernames)}, ${usernames.join(', ')}, ${notifObj.roomIcon}, ${notifObj.roomName}]]`;
|
||||
notifObj.bodyShort = `[[${mergeId}_${typeFromLength(usernames)}, ${usernames.join(', ')}, ${notifObj.roomIcon}, ${notifObj.roomName}]]`;
|
||||
} else if (usernames.length > 3) {
|
||||
notifObj.bodyShort = `[[${mergeId}_${typeFromUsernames(usernames)}, ${usernames.slice(0, 2).join(', ')}, ${usernames.length - 2}, ${notifObj.roomIcon}, ${notifObj.roomName}]]`;
|
||||
notifObj.bodyShort = `[[${mergeId}_${typeFromLength(usernames)}, ${usernames.slice(0, 2).join(', ')}, ${usernames.length - 2}, ${notifObj.roomIcon}, ${notifObj.roomName}]]`;
|
||||
}
|
||||
|
||||
notifObj.path = set[set.length - 1].path;
|
||||
@@ -440,9 +441,9 @@ Notifications.merge = async function (notifications) {
|
||||
titleEscaped = titleEscaped ? (`, ${titleEscaped}`) : '';
|
||||
|
||||
if (numUsers === 2 || numUsers === 3) {
|
||||
notifications[modifyIndex].bodyShort = `[[${mergeId}_${typeFromUsernames(usernames)}, ${usernames.join(', ')}${titleEscaped}]]`;
|
||||
notifications[modifyIndex].bodyShort = `[[${mergeId}_${typeFromLength(usernames)}, ${usernames.join(', ')}${titleEscaped}]]`;
|
||||
} else if (numUsers > 2) {
|
||||
notifications[modifyIndex].bodyShort = `[[${mergeId}_${typeFromUsernames(usernames)}, ${usernames.slice(0, 2).join(', ')}, ${numUsers - 2}${titleEscaped}]]`;
|
||||
notifications[modifyIndex].bodyShort = `[[${mergeId}_${typeFromLength(usernames)}, ${usernames.slice(0, 2).join(', ')}, ${numUsers - 2}${titleEscaped}]]`;
|
||||
}
|
||||
|
||||
notifications[modifyIndex].path = set[set.length - 1].path;
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = function (app, name, middleware, controllers) {
|
||||
setupPageRoute(app, `/${name}/:userslug/groups`, middlewares, controllers.accounts.groups.get);
|
||||
|
||||
setupPageRoute(app, `/${name}/:userslug/categories`, accountMiddlewares, controllers.accounts.categories.get);
|
||||
setupPageRoute(app, `/${name}/:userslug/tags`, accountMiddlewares, controllers.accounts.tags.get);
|
||||
setupPageRoute(app, `/${name}/:userslug/bookmarks`, accountMiddlewares, controllers.accounts.posts.getBookmarks);
|
||||
setupPageRoute(app, `/${name}/:userslug/watched`, accountMiddlewares, controllers.accounts.posts.getWatchedTopics);
|
||||
setupPageRoute(app, `/${name}/:userslug/ignored`, accountMiddlewares, controllers.accounts.posts.getIgnoredTopics);
|
||||
|
||||
@@ -37,6 +37,7 @@ Write.reload = async (params) => {
|
||||
router.use('/api/v3/groups', require('./groups')());
|
||||
router.use('/api/v3/categories', require('./categories')());
|
||||
router.use('/api/v3/topics', require('./topics')());
|
||||
router.use('/api/v3/tags', require('./tags')());
|
||||
router.use('/api/v3/posts', require('./posts')());
|
||||
router.use('/api/v3/chats', require('./chats')());
|
||||
router.use('/api/v3/flags', require('./flags')());
|
||||
|
||||
17
src/routes/write/tags.js
Normal file
17
src/routes/write/tags.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const middleware = require('../../middleware');
|
||||
const controllers = require('../../controllers');
|
||||
const routeHelpers = require('../helpers');
|
||||
|
||||
const { setupApiRoute } = routeHelpers;
|
||||
|
||||
module.exports = function () {
|
||||
const middlewares = [middleware.ensureLoggedIn];
|
||||
|
||||
setupApiRoute(router, 'put', '/:tag/follow', [...middlewares], controllers.write.tags.follow);
|
||||
setupApiRoute(router, 'delete', '/:tag/follow', [...middlewares], controllers.write.tags.unfollow);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -152,6 +152,7 @@ module.exports = function (Topics) {
|
||||
|
||||
if (parseInt(uid, 10) && !topicData.scheduled) {
|
||||
user.notifications.sendTopicNotificationToFollowers(uid, topicData, postData);
|
||||
Topics.notifyTagFollowers(postData, uid);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -229,7 +230,7 @@ module.exports = function (Topics) {
|
||||
topicInfo,
|
||||
] = await Promise.all([
|
||||
posts.getUserInfoForPosts([postData.uid], uid),
|
||||
Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid', 'scheduled']),
|
||||
Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid', 'scheduled', 'tags']),
|
||||
Topics.addParentPosts([postData]),
|
||||
Topics.syncBacklinks(postData),
|
||||
posts.parsePost(postData),
|
||||
|
||||
@@ -10,6 +10,9 @@ const meta = require('../meta');
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const plugins = require('../plugins');
|
||||
const privileges = require('../privileges');
|
||||
const notifications = require('../notifications');
|
||||
const translator = require('../translator');
|
||||
const utils = require('../utils');
|
||||
const batch = require('../batch');
|
||||
const cache = require('../cache');
|
||||
@@ -165,6 +168,18 @@ module.exports = function (Topics) {
|
||||
topicData.map(t => [`topic:${t.tid}`, { tags: t.tags.join(',') }]),
|
||||
);
|
||||
}, {});
|
||||
const followers = await db.getSortedSetRangeWithScores(`tag:${tag}:followers`, 0, -1);
|
||||
if (followers.length) {
|
||||
const userKeys = followers.map(item => `uid:${item.value}:followed_tags`);
|
||||
const scores = await db.sortedSetsScore(userKeys, tag);
|
||||
await db.sortedSetsRemove(userKeys, tag);
|
||||
await db.sortedSetsAdd(userKeys, scores, newTagName);
|
||||
await db.sortedSetAdd(
|
||||
`tag:${newTagName}:followers`,
|
||||
followers.map(item => item.score),
|
||||
followers.map(item => item.value),
|
||||
);
|
||||
}
|
||||
await Topics.deleteTag(tag);
|
||||
await updateTagCount(newTagName);
|
||||
await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]);
|
||||
@@ -207,7 +222,10 @@ module.exports = function (Topics) {
|
||||
if (!Array.isArray(tags) || !tags.length) {
|
||||
return;
|
||||
}
|
||||
await removeTagsFromTopics(tags);
|
||||
await Promise.all([
|
||||
removeTagsFromTopics(tags),
|
||||
removeTagsFromUsers(tags),
|
||||
]);
|
||||
const keys = tags.map(tag => `tag:${tag}:topics`);
|
||||
await db.deleteAll(keys);
|
||||
await db.sortedSetRemove('tags:topic:count', tags);
|
||||
@@ -219,6 +237,7 @@ module.exports = function (Topics) {
|
||||
const deleteKeys = [];
|
||||
tags.forEach((tag) => {
|
||||
deleteKeys.push(`tag:${tag}`);
|
||||
deleteKeys.push(`tag:${tag}:followers`);
|
||||
cids.forEach((cid) => {
|
||||
deleteKeys.push(`cid:${cid}:tag:${tag}:topics`);
|
||||
});
|
||||
@@ -245,6 +264,13 @@ module.exports = function (Topics) {
|
||||
});
|
||||
}
|
||||
|
||||
async function removeTagsFromUsers(tags) {
|
||||
await async.eachLimit(tags, 50, async (tag) => {
|
||||
const uids = await db.getSortedSetRange(`tag:${tag}:followers`, 0, -1);
|
||||
await db.sortedSetsRemove(uids.map(uid => `uid:${uid}:followed_tags`), tag);
|
||||
});
|
||||
}
|
||||
|
||||
Topics.deleteTag = async function (tag) {
|
||||
await Topics.deleteTags([tag]);
|
||||
};
|
||||
@@ -528,4 +554,80 @@ module.exports = function (Topics) {
|
||||
const topics = await Topics.getTopics(tids, uid);
|
||||
return topics.filter(t => t && !t.deleted && parseInt(t.uid, 10) !== parseInt(uid, 10));
|
||||
};
|
||||
|
||||
Topics.isFollowingTag = async function (tag, uid) {
|
||||
return await db.isSortedSetMember(`tag:${tag}:followers`, uid);
|
||||
};
|
||||
|
||||
Topics.getTagFollowers = async function (tag, start = 0, stop = -1) {
|
||||
return await db.getSortedSetRange(`tag:${tag}:followers`, start, stop);
|
||||
};
|
||||
|
||||
Topics.followTag = async (tag, uid) => {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
throw new Error('[[error:not-logged-in]]');
|
||||
}
|
||||
const now = Date.now();
|
||||
await db.sortedSetAddBulk([
|
||||
[`tag:${tag}:followers`, now, uid],
|
||||
[`uid:${uid}:followed_tags`, now, tag],
|
||||
]);
|
||||
plugins.hooks.fire('action:tags.follow', { tag, uid });
|
||||
};
|
||||
|
||||
Topics.unfollowTag = async (tag, uid) => {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
throw new Error('[[error:not-logged-in]]');
|
||||
}
|
||||
await db.sortedSetRemoveBulk([
|
||||
[`tag:${tag}:followers`, uid],
|
||||
[`uid:${uid}:followed_tags`, tag],
|
||||
]);
|
||||
plugins.hooks.fire('action:tags.unfollow', { tag, uid });
|
||||
};
|
||||
|
||||
Topics.notifyTagFollowers = async function (postData, exceptUid) {
|
||||
let { tags } = postData.topic;
|
||||
if (!tags.length) {
|
||||
return;
|
||||
}
|
||||
tags = tags.map(tag => tag.value);
|
||||
|
||||
const [followersOfPoster, allFollowers] = await Promise.all([
|
||||
db.getSortedSetRange(`followers:${exceptUid}`, 0, -1),
|
||||
db.getSortedSetRange(tags.map(tag => `tag:${tag}:followers`), 0, -1),
|
||||
]);
|
||||
const followerSet = new Set(followersOfPoster);
|
||||
// filter out followers of the poster since they get a notification already
|
||||
let followers = _.uniq(allFollowers).filter(uid => !followerSet.has(uid) && uid !== String(exceptUid));
|
||||
followers = await privileges.topics.filterUids('topics:read', postData.topic.tid, followers);
|
||||
if (!followers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displayname } = postData.user;
|
||||
|
||||
const notifBase = 'notifications:user_posted_topic_with_tag';
|
||||
let bodyShort = translator.compile(notifBase, displayname, tags[0]);
|
||||
if (tags.length === 2) {
|
||||
bodyShort = translator.compile(`${notifBase}_dual`, displayname, tags[0], tags[1]);
|
||||
} else if (tags.length === 3) {
|
||||
bodyShort = translator.compile(`${notifBase}_triple`, displayname, tags[0], tags[1], tags[2]);
|
||||
} else if (tags.length > 3) {
|
||||
bodyShort = translator.compile(`${notifBase}_multiple`, displayname, tags.join(', '));
|
||||
}
|
||||
|
||||
const notification = await notifications.create({
|
||||
type: 'new-topic-with-tag',
|
||||
nid: `new_topic:tid:${postData.topic.tid}:uid:${exceptUid}`,
|
||||
subject: bodyShort,
|
||||
bodyShort: bodyShort,
|
||||
bodyLong: postData.content,
|
||||
pid: postData.pid,
|
||||
path: `/post/${postData.pid}`,
|
||||
tid: postData.topic.tid,
|
||||
from: exceptUid,
|
||||
});
|
||||
notifications.push(notification, followers);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -108,8 +108,6 @@ module.exports = function (User) {
|
||||
`uid:${uid}:bookmarks`,
|
||||
`uid:${uid}:tids_read`,
|
||||
`uid:${uid}:tids_unread`,
|
||||
`uid:${uid}:followed_tids`,
|
||||
`uid:${uid}:ignored_tids`,
|
||||
`uid:${uid}:blocked_uids`,
|
||||
`user:${uid}:settings`,
|
||||
`user:${uid}:usernames`,
|
||||
@@ -147,17 +145,39 @@ module.exports = function (User) {
|
||||
db.setRemove('invitation:uids', uid),
|
||||
deleteUserIps(uid),
|
||||
deleteUserFromFollowers(uid),
|
||||
deleteUserFromFollowedTopics(uid),
|
||||
deleteUserFromIgnoredTopics(uid),
|
||||
deleteUserFromFollowedTags(uid),
|
||||
deleteImages(uid),
|
||||
groups.leaveAllGroups(uid),
|
||||
flags.resolveFlag('user', uid, uid),
|
||||
User.reset.cleanByUid(uid),
|
||||
User.email.expireValidation(uid),
|
||||
]);
|
||||
await db.deleteAll([`followers:${uid}`, `following:${uid}`, `user:${uid}`]);
|
||||
await db.deleteAll([
|
||||
`followers:${uid}`, `following:${uid}`, `user:${uid}`,
|
||||
`uid:${uid}:followed_tags`, `uid:${uid}:followed_tids`,
|
||||
`uid:${uid}:ignored_tids`,
|
||||
]);
|
||||
delete deletesInProgress[uid];
|
||||
return userData;
|
||||
};
|
||||
|
||||
async function deleteUserFromFollowedTopics(uid) {
|
||||
const tids = await db.getSortedSetRange(`uid:${uid}:followed_tids`, 0, -1);
|
||||
await db.setsRemove(tids.map(tid => `tid:${tid}:followers`), uid);
|
||||
}
|
||||
|
||||
async function deleteUserFromIgnoredTopics(uid) {
|
||||
const tids = await db.getSortedSetRange(`uid:${uid}:ignored_tids`, 0, -1);
|
||||
await db.setsRemove(tids.map(tid => `tid:${tid}:ignorers`), uid);
|
||||
}
|
||||
|
||||
async function deleteUserFromFollowedTags(uid) {
|
||||
const tags = await db.getSortedSetRange(`uid:${uid}:followed_tags`, 0, -1);
|
||||
await db.sortedSetsRemove(tags.map(tag => `tag:${tag}:followers`), uid);
|
||||
}
|
||||
|
||||
async function deleteVotes(uid) {
|
||||
const [upvotedPids, downvotedPids] = await Promise.all([
|
||||
db.getSortedSetRange(`uid:${uid}:upvote`, 0, -1),
|
||||
|
||||
Reference in New Issue
Block a user