FEATURE: Mention @here to notify users in topic (#14900)

Use @here to mention all users that were allowed to topic directly or
through group, who liked topics or read the topic. Only first 10 users
will be notified.
This commit is contained in:
Bianca Nenciu 2021-11-23 22:25:54 +02:00 committed by GitHub
parent 0ededb1454
commit 73760c77d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 197 additions and 7 deletions

View File

@ -561,10 +561,11 @@ export default Component.extend(ComposerUpload, {
_renderUnseenMentions(preview, unseen) {
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
fetchUnseenMentions(unseen, this.get("composer.topic.id")).then(() => {
fetchUnseenMentions(unseen, this.get("composer.topic.id")).then((r) => {
linkSeenMentions(preview, this.siteSettings);
this._warnMentionedGroups(preview);
this._warnCannotSeeMention(preview);
this._warnHereMention(r.here_count);
});
},
@ -639,6 +640,20 @@ export default Component.extend(ComposerUpload, {
});
},
_warnHereMention(hereCount) {
if (!hereCount || hereCount === 0) {
return;
}
later(
this,
() => {
this.hereMention(hereCount);
},
2000
);
},
@bind
_handleImageScaleButtonClick(event) {
if (!event.target.classList.contains("scale-btn")) {

View File

@ -719,6 +719,17 @@ export default Controller.extend({
});
},
hereMention(count) {
this.appEvents.trigger("composer-messages:create", {
extraClass: "custom-body",
templateName: "custom-body",
body: I18n.t("composer.here_mention", {
here: this.siteSettings.here_mention,
count,
}),
});
},
applyUnorderedList() {
this.toolbarEvent.applyList("* ", "list_item");
},

View File

@ -129,6 +129,7 @@
uploadProgress=uploadProgress
groupsMentioned=(action "groupsMentioned")
cannotSeeMention=(action "cannotSeeMention")
hereMention=(action "hereMention")
importQuote=(action "importQuote")
togglePreview=(action "togglePreview")
processPreview=showPreview

View File

@ -494,10 +494,19 @@ class UsersController < ApplicationController
usernames.each(&:downcase!)
cannot_see = []
here_count = nil
topic_id = params[:topic_id]
unless topic_id.blank?
topic = Topic.find_by(id: topic_id)
usernames.each { |username| cannot_see.push(username) unless Guardian.new(User.find_by_username(username)).can_see?(topic) }
if topic_id.present? && topic = Topic.find_by(id: topic_id)
usernames.each do |username|
if !Guardian.new(User.find_by_username(username)).can_see?(topic)
cannot_see.push(username)
end
end
if usernames.include?(SiteSetting.here_mention) && guardian.can_mention_here?
here_count = PostAlerter.new.expand_here_mention(topic.first_post, exclude_ids: [current_user.id]).size
end
end
result = User.where(staged: false)
@ -509,6 +518,7 @@ class UsersController < ApplicationController
valid_groups: groups,
mentionable_groups: mentionable_groups,
cannot_see: cannot_see,
here_count: here_count,
max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention
}
end

View File

@ -285,6 +285,8 @@ class User < ActiveRecord::Base
def self.reserved_username?(username)
username = normalize_username(username)
return true if SiteSetting.here_mention == username
SiteSetting.reserved_usernames.unicode_normalize.split("|").any? do |reserved|
username.match?(/^#{Regexp.escape(reserved).gsub('\*', '.*')}$/)
end

View File

@ -111,9 +111,9 @@ class PostAlerter
notified = [post.user, post.last_editor].uniq
# mentions (users/groups)
mentioned_groups, mentioned_users = extract_mentions(post)
mentioned_groups, mentioned_users, mentioned_here = extract_mentions(post)
if mentioned_groups || mentioned_users
if mentioned_groups || mentioned_users || mentioned_here
mentioned_opts = {}
editor = post.last_editor
@ -131,6 +131,12 @@ class PostAlerter
users = only_allowed_users(users, post)
notified += notify_users(users - notified, :group_mentioned, post, mentioned_opts.merge(group: group))
end
if mentioned_here
users = expand_here_mention(post, exclude_ids: notified.map(&:id))
users = only_allowed_users(users, post)
notified += notify_users(users - notified, :mentioned, post, mentioned_opts)
end
end
# replies
@ -543,6 +549,21 @@ class PostAlerter
end
def expand_here_mention(post, exclude_ids: nil)
posts = Post.where(topic_id: post.topic_id)
posts = posts.where.not(user_id: exclude_ids) if exclude_ids.present?
if post.user.staff?
posts = posts.where(post_type: [Post.types[:regular], Post.types[:whisper]])
else
posts = posts.where(post_type: Post.types[:regular])
end
User.real
.where(id: posts.select(:user_id))
.limit(SiteSetting.max_here_mentioned)
end
# TODO: Move to post-analyzer?
def extract_mentions(post)
mentions = post.raw_mentions
@ -557,7 +578,10 @@ class PostAlerter
users = nil if users.empty?
end
[groups, users]
# @here can be a user mention and then this feature is disabled
here = mentions.include?(SiteSetting.here_mention) && Guardian.new(post.user).can_mention_here?
[groups, users, here]
end
# TODO: Move to post-analyzer?

View File

@ -2105,6 +2105,9 @@ en:
cannot_see_mention:
category: "You mentioned %{username} but they won't be notified because they do not have access to this category. You will need to add them to a group that has access to this category."
private: "You mentioned %{username} but they won't be notified because they are unable to see this personal message. You will need to invite them to this PM."
here_mention:
one: "By mentioning <b>@%{here}</b>, you are about to notify %{count} user are you sure?"
other: "By mentioning <b>@%{here}</b>, you are about to notify %{count} users are you sure?"
duplicate_link: "It looks like your link to <b>%{domain}</b> was already posted in the topic by <b>@%{username}</b> in <a href='%{post_url}'>a reply on %{ago}</a> are you sure you want to post it again?"
reference_topic_title: "RE: %{title}"

View File

@ -1883,6 +1883,9 @@ en:
max_mentions_per_post: "Maximum number of @name notifications anyone can use in a post."
max_users_notified_per_group_mention: "Maximum number of users that may receive a notification if a group is mentioned (if threshold is met no notifications will be raised)"
enable_mentions: "Allow users to mention other users."
here_mention: "Name used for @here mention. Must not be an existent username."
max_here_mentioned: "Maximum number of mentioned people by @here."
min_trust_level_for_here_mention: "The minimum trust level allowed to mention @here."
create_thumbnails: "Create thumbnails and lightbox images that are too large to fit in a post."
@ -2325,6 +2328,7 @@ en:
invalid_css_color: "Invalid color. Enter a color name or hex value."
invalid_email: "Invalid email address."
invalid_username: "There's no user with that username."
valid_username: "There's a user with that username."
invalid_group: "There's no group with that name."
invalid_integer_min_max: "Value must be between %{min} and %{max}."
invalid_integer_min: "Value must be %{min} or greater."

View File

@ -873,6 +873,14 @@ posting:
max_users_notified_per_group_mention: 100
newuser_max_replies_per_topic: 3
newuser_max_mentions_per_post: 2
here_mention:
default: "here"
validator: "NotUsernameValidator"
client: true
max_here_mentioned: 10
min_trust_level_for_here_mention:
default: "2"
enum: "TrustLevelAndStaffSetting"
title_max_word_length:
default: 30
locale_default:

View File

@ -541,6 +541,15 @@ class Guardian
UserAuthToken.hash_token(token) if token
end
def can_mention_here?
return false if SiteSetting.here_mention.blank?
return false if SiteSetting.max_here_mentioned < 1
return false if !authenticated?
return false if User.where(username_lower: SiteSetting.here_mention).exists?
@user.has_trust_level_or_staff?(SiteSetting.min_trust_level_for_here_mention)
end
private
def is_my_own?(obj)

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class NotUsernameValidator
def initialize(opts = {})
@opts = opts
end
def valid_value?(val)
val.blank? || !User.where(username: val).exists?
end
def error_message
I18n.t('site_settings.errors.valid_username')
end
end

View File

@ -3943,4 +3943,44 @@ describe Guardian do
end
end
end
describe "#can_mention_here?" do
it 'returns false if disabled' do
SiteSetting.max_here_mentioned = 0
expect(admin.guardian.can_mention_here?).to eq(false)
end
it 'returns false if disabled' do
SiteSetting.here_mention = ''
expect(admin.guardian.can_mention_here?).to eq(false)
end
it 'works with trust levels' do
SiteSetting.min_trust_level_for_here_mention = 2
expect(trust_level_0.guardian.can_mention_here?).to eq(false)
expect(trust_level_1.guardian.can_mention_here?).to eq(false)
expect(trust_level_2.guardian.can_mention_here?).to eq(true)
expect(trust_level_3.guardian.can_mention_here?).to eq(true)
expect(trust_level_4.guardian.can_mention_here?).to eq(true)
expect(moderator.guardian.can_mention_here?).to eq(true)
expect(admin.guardian.can_mention_here?).to eq(true)
end
it 'works with staff' do
SiteSetting.min_trust_level_for_here_mention = 'staff'
expect(trust_level_4.guardian.can_mention_here?).to eq(false)
expect(moderator.guardian.can_mention_here?).to eq(true)
expect(admin.guardian.can_mention_here?).to eq(true)
end
it 'works with admin' do
SiteSetting.min_trust_level_for_here_mention = 'admin'
expect(trust_level_4.guardian.can_mention_here?).to eq(false)
expect(moderator.guardian.can_mention_here?).to eq(false)
expect(admin.guardian.can_mention_here?).to eq(true)
end
end
end

View File

@ -29,6 +29,7 @@ describe PostAlerter do
fab!(:evil_trout) { Fabricate(:evil_trout) }
fab!(:user) { Fabricate(:user) }
fab!(:tl2_user) { Fabricate(:user, trust_level: TrustLevel[2]) }
def create_post_with_alerts(args = {})
post = Fabricate(:post, args)
@ -343,6 +344,53 @@ describe PostAlerter do
end
end
context '@here' do
let(:topic) { Fabricate(:topic) }
let(:post) { create_post_with_alerts(raw: "Hello @here how are you?", user: tl2_user, topic: topic) }
let(:other_post) { Fabricate(:post, topic: topic) }
before do
Jobs.run_immediately!
end
it 'does not notify unrelated users' do
expect { post }.to change(evil_trout.notifications, :count).by(0)
end
it 'does not work if user here exists' do
Fabricate(:user, username: SiteSetting.here_mention)
expect { post }.to change(other_post.user.notifications, :count).by(0)
end
it 'notifies users who replied' do
post2 = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
post3 = Fabricate(:post, topic: topic)
expect { post }
.to change(other_post.user.notifications, :count).by(1)
.and change(post2.user.notifications, :count).by(0)
.and change(post3.user.notifications, :count).by(1)
end
it 'notifies users who whispered' do
post2 = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
post3 = Fabricate(:post, topic: topic)
tl2_user.grant_admin!
expect { post }
.to change(other_post.user.notifications, :count).by(1)
.and change(post2.user.notifications, :count).by(1)
.and change(post3.user.notifications, :count).by(1)
end
it 'notifies only last max_here_mentioned users' do
SiteSetting.max_here_mentioned = 2
3.times { Fabricate(:post, topic: topic) }
expect { post }.to change { Notification.count }.by(2)
end
end
context '@group mentions' do
fab!(:group) { Fabricate(:group, name: 'group', mentionable_level: Group::ALIAS_LEVELS[:everyone]) }