diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b96165695..42657668a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -53,7 +53,8 @@ module ApplicationHelper name = h(user.name(options[:format])) if user.active? || (User.current.admin? && user.logged?) only_path = options[:only_path].nil? ? true : options[:only_path] - link_to name, user_url(user, :only_path => only_path), :class => user.css_classes + css_classes = options[:class] ? "#{user.css_classes} #{options[:class]}" : user.css_classes + link_to name, user_url(user, :only_path => only_path), :class => css_classes else name end @@ -1093,9 +1094,6 @@ module ApplicationHelper if p = Project.visible.find_by_id(oid) link = link_to_project(p, {:only_path => only_path}, :class => 'project') end - when 'user' - u = User.visible.find_by(:id => oid, :type => 'User') - link = link_to_user(u, :only_path => only_path) if u end elsif sep == ':' name = remove_double_quotes(identifier) @@ -1170,14 +1168,15 @@ module ApplicationHelper if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first link = link_to_project(p, {:only_path => only_path}, :class => 'project') end - when 'user' - u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase) - link = link_to_user(u, :only_path => only_path) if u end - elsif sep == "@" - name = remove_double_quotes(identifier) - u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase) - link = link_to_user(u, :only_path => only_path) if u + end + + if link.nil? && $~ + user = User.mentioned_user($~.named_captures.symbolize_keys) + if user + css_classes = (user.notify_mentioned_user?(obj) ? 'notified' : nil) if obj + link = link_to_user(user, :only_path => only_path, :class => css_classes) + end end end (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) diff --git a/app/models/document.rb b/app/models/document.rb index 4f562fc70..5cc89de4d 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -63,7 +63,7 @@ class Document < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end private diff --git a/app/models/issue.rb b/app/models/issue.rb index 63bc8cabe..366988514 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1045,8 +1045,7 @@ class Issue < ActiveRecord::Base notified += project.users.preload(:preference).select(&:notify_about_high_priority_issues?) if priority.high? notified.uniq! # Remove users that can not view the issue - notified.reject! {|user| !visible?(user)} - notified + notified.select {|user| user.allowed_to_view_notify_target?(self)} end # Returns the email addresses that should be notified diff --git a/app/models/journal.rb b/app/models/journal.rb index 191fac972..1a072ce97 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -145,10 +145,7 @@ class Journal < ActiveRecord::Base def notified_users notified = journalized.notified_users - if private_notes? - notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} - end - notified + notified.select{ |u| u.allowed_to_view_notify_target?(self) } end def recipients diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 550c149cb..8919f6e60 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -68,7 +68,7 @@ class Mailer < ActionMailer::Base end # Builds a mail for notifying user about a new issue - def issue_add(user, issue) + def issue_add(user, issue, mentioned) redmine_headers 'Project' => issue.project.identifier, 'Issue-Tracker' => issue.tracker.name, 'Issue-Id' => issue.id, @@ -82,7 +82,11 @@ class Mailer < ActionMailer::Base @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue) subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]" subject += " (#{issue.status.name})" if Setting.show_status_changes_in_mail_subject? - subject += " #{issue.subject}" + if mentioned + subject = l(:text_mentioned, :id => subject, :author => @author) + else + subject += " #{issue.subject}" + end mail :to => user, :subject => subject end @@ -93,13 +97,15 @@ class Mailer < ActionMailer::Base # Mailer.deliver_issue_add(issue) def self.deliver_issue_add(issue) users = issue.notified_users | issue.notified_watchers + mentioned_users = mentioned_users(issue.description, issue) + users |= mentioned_users users.each do |user| - issue_add(user, issue).deliver_later + issue_add(user, issue, mentioned_users.include?(user)).deliver_later end end # Builds a mail for notifying user about an issue update - def issue_edit(user, journal) + def issue_edit(user, journal, mentioned) issue = journal.journalized redmine_headers 'Project' => issue.project.identifier, 'Issue-Tracker' => issue.tracker.name, @@ -111,7 +117,11 @@ class Mailer < ActionMailer::Base @author = journal.user s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] " s += "(#{issue.status.name}) " if journal.new_value_for('status_id') && Setting.show_status_changes_in_mail_subject? - s += issue.subject + if mentioned + s = l(:text_mentioned, :id => s, :author => @author) + else + s += issue.subject + end @issue = issue @user = user @journal = journal @@ -128,11 +138,13 @@ class Mailer < ActionMailer::Base # Mailer.deliver_issue_edit(journal) def self.deliver_issue_edit(journal) users = journal.notified_users | journal.notified_watchers + mentioned_users = mentioned_users(journal.notes, journal) + users |= mentioned_users users.select! do |user| journal.notes? || journal.visible_details(user).any? end users.each do |user| - issue_edit(user, journal).deliver_later + issue_edit(user, journal, mentioned_users.include?(user)).deliver_later end end @@ -203,7 +215,7 @@ class Mailer < ActionMailer::Base end # Builds a mail to user about a new news. - def news_added(user, news) + def news_added(user, news, mentioned) redmine_headers 'Project' => news.project.identifier @author = news.author message_id news @@ -211,8 +223,14 @@ class Mailer < ActionMailer::Base @news = news @user = user @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + subject = "[#{news.project.name}] #{l(:label_news)}" + if mentioned + subject = l(:text_mentioned, :id => subject, :author => @author) + else + subject += " #{news.title}" + end mail :to => user, - :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}" + :subject => subject end # Notifies users about new news @@ -221,13 +239,16 @@ class Mailer < ActionMailer::Base # Mailer.deliver_news_added(news) def self.deliver_news_added(news) users = news.notified_users | news.notified_watchers_for_added_news + mentioned_users = mentioned_users(news.description, news) + users |= mentioned_users + users.each do |user| - news_added(user, news).deliver_later + news_added(user, news, mentioned_users.include?(user)).deliver_later end end # Builds a mail to user about a new news comment. - def news_comment_added(user, comment) + def news_comment_added(user, comment, mentioned) news = comment.commented redmine_headers 'Project' => news.project.identifier @author = comment.author @@ -237,8 +258,14 @@ class Mailer < ActionMailer::Base @comment = comment @user = user @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + subject = + if mentioned + l(:text_mentioned, :id => "Re: [#{news.project.name}] #{l(:label_news)}", :author => @user) + else + "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}" + end mail :to => user, - :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}" + :subject => subject end # Notifies users about a new comment on a news @@ -248,13 +275,15 @@ class Mailer < ActionMailer::Base def self.deliver_news_comment_added(comment) news = comment.commented users = news.notified_users | news.notified_watchers + mentioned_users = mentioned_users(comment.content, news) + users |= mentioned_users users.each do |user| - news_comment_added(user, comment).deliver_later + news_comment_added(user, comment, mentioned_users.include?(user)).deliver_later end end # Builds a mail to user about a new message. - def message_posted(user, message) + def message_posted(user, message, mentioned) redmine_headers 'Project' => message.project.identifier, 'Topic-Id' => (message.parent_id || message.id) @author = message.author @@ -263,8 +292,14 @@ class Mailer < ActionMailer::Base @message = message @user = user @message_url = url_for(message.event_url) + subject = + if mentioned + l(:text_mentioned, :id => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}]", :author => @user) + else + "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" + end mail :to => user, - :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" + :subject => subject end # Notifies users about a new forum message. @@ -275,9 +310,11 @@ class Mailer < ActionMailer::Base users = message.notified_users users |= message.root.notified_watchers users |= message.board.notified_watchers + mentioned_users = mentioned_users(message.content, message) + users |= mentioned_users users.each do |user| - message_posted(user, message).deliver_later + message_posted(user, message, mentioned_users.include?(user)).deliver_later end end @@ -780,4 +817,15 @@ class Mailer < ActionMailer::Base @references_objects ||= [] @references_objects << object end + + def self.mentioned_users(text, obj) + users = [] + return users if text.blank? + text.scan(ApplicationHelper::LINKS_RE) do |_| + target = User.mentioned_user($~.named_captures.symbolize_keys) + next if target.blank? || users.include?(target) + users << target if target.notify_mentioned_user?(obj) + end + users + end end diff --git a/app/models/message.rb b/app/models/message.rb index f48785447..a1e1221c3 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -114,7 +114,7 @@ class Message < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end private diff --git a/app/models/news.rb b/app/models/news.rb index 3ad43bff7..0a4fa4042 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -56,7 +56,7 @@ class News < ActiveRecord::Base end def notified_users - project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)} + project.users.select {|user| user.notify_about?(self) && user.allowed_to_view_notify_target?(self)} end def recipients diff --git a/app/models/user.rb b/app/models/user.rb index 76f479bcd..4fc1cd05b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -823,6 +823,47 @@ class User < Principal RequestStore.store[:current_user] ||= User.anonymous end + # Return the mentioned user to based on the match data + # of ApplicationHelper::LINKS_RE. + # user:jsmith -> Link to user with login jsmith + # @jsmith -> Link to user with login jsmith + # user#2 -> Link to user with id 2 + def self.mentioned_user(match_data) + return nil if match_data[:esc] + sep = match_data[:sep1] || match_data[:sep2] || match_data[:sep3] || match_data[:sep4] + identifier = match_data[:identifier1] || match_data[:identifier2] || match_data[:identifier3] + prefix = match_data[:prefix] + if (sep == '#' || sep == '##') && prefix == 'user' + User.visible.find_by(:id => identifier.to_i, :type => 'User') + elsif sep == '@' || (sep == ':' && prefix == 'user') + name = identifier.gsub(%r{^"(.*)"$}, "\\1") + User.find_by_login(CGI.unescapeHTML(name).downcase) + end + end + + # Return true if notify the mentioned user. + def notify_mentioned_user?(object) + self.active? && + self.mail.present? && + self.mail_notification.present? && self.mail_notification != 'none' && + self.allowed_to_view_notify_target?(object) + end + + # Return true if the user is allowed to view the notify target. + def allowed_to_view_notify_target?(object) + case object + when Journal + self.allowed_to_view_notify_target?(object.journalized) && + (!object.private_notes? || self.allowed_to?(:view_private_notes, object.journalized.project)) + when Comment + self.allowed_to_view_notify_target?(object.commented) + when nil + false + else + object.visible?(self) + end + end + # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only # one anonymous user per database. def self.anonymous diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index d38c5ca7d..ef2c28c5a 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -53,7 +53,7 @@ class WikiContent < ActiveRecord::Base end def notified_users - project.notified_users.reject {|user| !visible?(user)} + project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) } end # Returns the mail addresses of users that should be notified diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb index 04ea54a0a..f6dba0881 100644 --- a/app/views/news/show.html.erb +++ b/app/views/news/show.html.erb @@ -44,7 +44,7 @@

<%= avatar(comment.author) %><%= authoring comment.created_on, comment.author %>

- <%= textilizable(comment.comments) %> + <%= textilizable(comment, :comments) %>
<% end if @comments.any? %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 3f0fd2d77..7fada9ba0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1180,6 +1180,7 @@ en: text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages text_issue_added: "Issue %{id} has been reported by %{author}." text_issue_updated: "Issue %{id} has been updated by %{author}." + text_mentioned: "You have been mentioned in %{id} by %{author}." text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content? text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?" text_issue_category_destroy_assignments: Remove category assignments diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e04091a10..b979f40c5 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -137,6 +137,7 @@ div.modal .box p {margin: 0.3em 0;} a, a:link, a:visited{ color: #169; text-decoration: none; } a:hover, a:active{ color: #c61a1a; text-decoration: underline;} a img{ border: 0; } +a.user.notified, a.user.notified:link, a.user.notified:visited {padding: 2px; border-radius: 3px; background-color: #bae9f5} a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }