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:
parent
0ededb1454
commit
73760c77d9
|
@ -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")) {
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
uploadProgress=uploadProgress
|
||||
groupsMentioned=(action "groupsMentioned")
|
||||
cannotSeeMention=(action "cannotSeeMention")
|
||||
hereMention=(action "hereMention")
|
||||
importQuote=(action "importQuote")
|
||||
togglePreview=(action "togglePreview")
|
||||
processPreview=showPreview
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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]) }
|
||||
|
|
Loading…
Reference in New Issue