diff --git a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 b/app/assets/javascripts/discourse/components/dropdown-button.js.es6
index 3747452e35d..99d211bbe38 100644
--- a/app/assets/javascripts/discourse/components/dropdown-button.js.es6
+++ b/app/assets/javascripts/discourse/components/dropdown-button.js.es6
@@ -24,8 +24,11 @@ export default Ember.Component.extend(StringBuffer, {
}.on('willDestroyElement'),
renderString(buffer) {
+ const title = this.get('title');
+ if (title) {
+ buffer.push("
" + title + "
");
+ }
- buffer.push("" + this.get('title') + "
");
buffer.push("");
diff --git a/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6
new file mode 100644
index 00000000000..c486b9432a0
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6
@@ -0,0 +1,9 @@
+import ObjectController from "discourse/controllers/object";
+
+export default ObjectController.extend({
+
+ stopNotificiationsText: function() {
+ return I18n.t("topic.unsubscribe.stop_notifications", { title: this.get("model.fancyTitle") });
+ }.property("model.fancyTitle"),
+
+})
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index 779822ee39a..152aabadde9 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -10,6 +10,7 @@ export default function() {
this.route('fromParamsNear', { path: '/:nearPost' });
});
this.resource('topicBySlug', { path: '/t/:slug' });
+ this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' });
this.resource('discovery', { path: '/' }, function() {
// top
diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
index bfb8468cd7f..6de3d07c546 100644
--- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
@@ -2,32 +2,32 @@
export default Discourse.Route.extend({
// Avoid default model hook
- model: function(p) { return p; },
+ model(params) { return params; },
- setupController: function(controller, params) {
+ setupController(controller, params) {
params = params || {};
params.track_visit = true;
- var topic = this.modelFor('topic'),
- postStream = topic.get('postStream');
- var topicController = this.controllerFor('topic'),
- topicProgressController = this.controllerFor('topic-progress'),
- composerController = this.controllerFor('composer');
+ const self = this,
+ topic = this.modelFor('topic'),
+ postStream = topic.get('postStream'),
+ topicController = this.controllerFor('topic'),
+ topicProgressController = this.controllerFor('topic-progress'),
+ composerController = this.controllerFor('composer');
// I sincerely hope no topic gets this many posts
if (params.nearPost === "last") { params.nearPost = 999999999; }
- var self = this;
params.forceLoad = true;
- postStream.refresh(params).then(function () {
+ postStream.refresh(params).then(function () {
// TODO we are seeing errors where closest post is null and this is exploding
// we need better handling and logging for this condition.
// The post we requested might not exist. Let's find the closest post
- var closestPost = postStream.closestPostForPostNumber(params.nearPost || 1),
- closest = closestPost.get('post_number'),
- progress = postStream.progressIndexOfPost(closestPost);
+ const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1),
+ closest = closestPost.get('post_number'),
+ progress = postStream.progressIndexOfPost(closestPost);
topicController.setProperties({
'model.currentPost': closest,
@@ -43,6 +43,7 @@ export default Discourse.Route.extend({
Ember.run.scheduleOnce('afterRender', function() {
self.appEvents.trigger('post:highlight', closest);
});
+
Discourse.URL.jumpToPost(closest);
if (topic.present('draft')) {
diff --git a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6
new file mode 100644
index 00000000000..10e77c47b48
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6
@@ -0,0 +1,23 @@
+import PostStream from "discourse/models/post-stream";
+
+export default Discourse.Route.extend({
+ model(params) {
+ const topic = this.store.createRecord("topic", { id: params.id });
+ return PostStream.loadTopicView(params.id).then(json => {
+ topic.updateFromJson(json);
+ return topic;
+ });
+ },
+
+ afterModel(topic) {
+ // hide the notification reason text
+ topic.set("details.notificationReasonText", null);
+ },
+
+ actions: {
+ didTransition() {
+ this.controllerFor("application").set("showFooter", true);
+ return true;
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs
new file mode 100644
index 00000000000..d153867e871
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs
@@ -0,0 +1,8 @@
+
+
+ {{{stopNotificiationsText}}}
+
+
+ {{i18n "topic.unsubscribe.change_notification_state"}} {{topic-notifications-button topic=model}}
+
+
diff --git a/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6
new file mode 100644
index 00000000000..a8469728173
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6
@@ -0,0 +1,3 @@
+export default Discourse.View.extend({
+ classNames: ["topic-unsubscribe"]
+});
diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss
index 18701110bc0..873278a71a3 100644
--- a/app/assets/stylesheets/common/base/topic.scss
+++ b/app/assets/stylesheets/common/base/topic.scss
@@ -63,3 +63,15 @@
// Top of bullet aligns with top of line - adjust line height to vertically align bullet.
line-height: 0.8;
}
+
+.topic-unsubscribe {
+ .notification-options {
+ display: inline-block;
+ .dropdown-toggle {
+ float: none;
+ }
+ .dropdown-menu {
+ bottom: initial;
+ }
+ }
+}
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 88e30c39246..01d960e5428 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -24,11 +24,12 @@ class TopicsController < ApplicationController
:bulk,
:reset_new,
:change_post_owners,
- :bookmark]
+ :bookmark,
+ :unsubscribe]
before_filter :consider_user_for_promotion, only: :show
- skip_before_filter :check_xhr, only: [:show, :feed]
+ skip_before_filter :check_xhr, only: [:show, :unsubscribe, :feed]
def id_for_slug
topic = Topic.find_by(slug: params[:slug].downcase)
@@ -94,6 +95,26 @@ class TopicsController < ApplicationController
raise ex
end
+ def unsubscribe
+ @topic_view = TopicView.new(params[:topic_id], current_user)
+
+ if slugs_do_not_match || (!request.format.json? && params[:slug].blank?)
+ return redirect_to @topic_view.topic.unsubscribe_url, status: 301
+ end
+
+ tu = TopicUser.find_by(user_id: current_user.id, topic_id: params[:topic_id])
+
+ if tu.notification_level > TopicUser.notification_levels[:regular]
+ tu.notification_level = TopicUser.notification_levels[:regular]
+ else
+ tu.notification_level = TopicUser.notification_levels[:muted]
+ end
+
+ tu.save!
+
+ perform_show_response
+ end
+
def wordpress
params.require(:best)
params.require(:topic_id)
@@ -476,6 +497,7 @@ class TopicsController < ApplicationController
format.html do
@description_meta = @topic_view.topic.excerpt
store_preloaded("topic_#{@topic_view.topic.id}", MultiJson.dump(topic_view_serializer))
+ render :show
end
format.json do
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index da27294d7f4..438e63f6f30 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -292,6 +292,7 @@ class UserNotifications < ActionMailer::Base
context: context,
username: username,
add_unsubscribe_link: true,
+ unsubscribe_url: post.topic.unsubscribe_url,
allow_reply_by_email: allow_reply_by_email,
use_site_subject: use_site_subject,
add_re_to_subject: add_re_to_subject,
@@ -306,9 +307,7 @@ class UserNotifications < ActionMailer::Base
}
# If we have a display name, change the from address
- if from_alias.present?
- email_opts[:from_alias] = from_alias
- end
+ email_opts[:from_alias] = from_alias if from_alias.present?
TopicUser.change(user.id, post.topic_id, last_emailed_post_number: post.post_number)
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 60ad4cc567c..30bd8fc343a 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -716,6 +716,10 @@ class Topic < ActiveRecord::Base
url
end
+ def unsubscribe_url
+ "#{url}/unsubscribe"
+ end
+
def clear_pin_for(user)
return unless user.present?
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb
index 9876d0d3908..b900c9b0a60 100644
--- a/app/models/topic_user.rb
+++ b/app/models/topic_user.rb
@@ -7,8 +7,9 @@ class TopicUser < ActiveRecord::Base
scope :tracking, lambda { |topic_id|
where(topic_id: topic_id)
- .where("COALESCE(topic_users.notification_level, :regular) >= :tracking",
- regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
+ .where("COALESCE(topic_users.notification_level, :regular) >= :tracking",
+ regular: TopicUser.notification_levels[:regular],
+ tracking: TopicUser.notification_levels[:tracking])
}
# Class methods
@@ -58,13 +59,9 @@ class TopicUser < ActiveRecord::Base
def create_lookup(topic_users)
topic_users = topic_users.to_a
-
result = {}
return result if topic_users.blank?
-
- topic_users.each do |ftu|
- result[ftu.topic_id] = ftu
- end
+ topic_users.each { |ftu| result[ftu.topic_id] = ftu }
result
end
@@ -113,11 +110,9 @@ class TopicUser < ActiveRecord::Base
end
if attrs[:notification_level]
- MessageBus.publish("/topic/#{topic_id}",
- {notification_level_change: attrs[:notification_level]}, user_ids: [user_id])
+ MessageBus.publish("/topic/#{topic_id}", { notification_level_change: attrs[:notification_level] }, user_ids: [user_id])
end
-
rescue ActiveRecord::RecordNotUnique
# In case of a race condition to insert, do nothing
end
@@ -127,7 +122,7 @@ class TopicUser < ActiveRecord::Base
user_id = user.is_a?(User) ? user.id : topic
now = DateTime.now
- rows = TopicUser.where({topic_id: topic_id, user_id: user_id}).update_all({last_visited_at: now})
+ rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all(last_visited_at: now)
if rows == 0
TopicUser.create(topic_id: topic_id, user_id: user_id, last_visited_at: now, first_visited_at: now)
else
@@ -196,7 +191,7 @@ class TopicUser < ActiveRecord::Base
end
if before != after
- MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id])
+ MessageBus.publish("/topic/#{topic_id}", { notification_level_change: after }, user_ids: [user.id])
end
end
@@ -220,7 +215,7 @@ class TopicUser < ActiveRecord::Base
WHERE ftu.user_id = :user_id and ftu.topic_id = :topic_id)",
args)
- MessageBus.publish("/topic/#{topic_id}", {notification_level_change: args[:new_status]}, user_ids: [user.id])
+ MessageBus.publish("/topic/#{topic_id}", { notification_level_change: args[:new_status] }, user_ids: [user.id])
end
end
diff --git a/app/views/email/notification.html.erb b/app/views/email/notification.html.erb
index c2292f6be64..bf5e2869c04 100644
--- a/app/views/email/notification.html.erb
+++ b/app/views/email/notification.html.erb
@@ -1,31 +1,29 @@
>
-<%= render :partial => 'email/post', :locals => {:post => post} %>
+ <%= render partial: 'email/post', locals: { post: post } %>
-<% if context_posts.present? %>
-
-
- <%= t "user_notifications.previous_discussion" %>
+ <% if context_posts.present? %>
+
- <% context_posts.each do |p| %>
- <%= render :partial => 'email/post', :locals => {:post => p} %>
+
+
+ <%= t "user_notifications.previous_discussion" %>
+
+ <% context_posts.each do |p| %>
+ <%= render partial: 'email/post', locals: { post: p } %>
+ <% end %>
<% end %>
-<% end %>
-
+
+
+
+
-
-
+
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index c83886feab7..95a37b51852 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -981,6 +981,9 @@ en:
search: "There are no more search results."
topic:
+ unsubscribe:
+ stop_notifications: "You will stop receiving notifications for {{title}}."
+ change_notification_state: "You can change your notification state"
filter_to: "{{post_count}} posts in topic"
create: 'New Topic'
create_long: 'Create a new Topic'
@@ -1014,7 +1017,6 @@ en:
new_posts:
one: "there is 1 new post in this topic since you last read it"
other: "there are {{count}} new posts in this topic since you last read it"
-
likes:
one: "there is 1 like in this topic"
other: "there are {{count}} likes in this topic"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 7d10fb4eaf5..b9199354361 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1849,7 +1849,10 @@ en:
subject_template: "Downloading remote images disabled"
text_body_template: "The `download_remote_images_to_local` setting was disabled because the disk space limit at `download_remote_images_threshold` was reached."
- unsubscribe_link: "To unsubscribe from these emails, visit your [user preferences](%{user_preferences_url})."
+ unsubscribe_link: |
+ To unsubscribe from these emails, visit your [user preferences](%{user_preferences_url}).
+
+ To stop receiving notifications about this particular topic, [click here](%{unsubscribe_url}).
subject_re: "Re: "
subject_pm: "[PM] "
diff --git a/config/routes.rb b/config/routes.rb
index 1998644ffcd..d4f87ce11a2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -434,10 +434,12 @@ Discourse::Application.routes.draw do
# Topic routes
get "t/id_for/:slug" => "topics#id_for_slug"
get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/}
- get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: {topic_id: /\d+/}
get "t/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/}
- get "t/:slug/:topic_id/summary" => "topics#show", defaults: {summary: true}, constraints: {topic_id: /\d+/, post_number: /\d+/}
- get "t/:topic_id/summary" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/}
+ get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: {topic_id: /\d+/}
+ get "t/:slug/:topic_id/summary" => "topics#show", defaults: {summary: true}, constraints: {topic_id: /\d+/}
+ get "t/:slug/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: {topic_id: /\d+/}
+ get "t/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: {topic_id: /\d+/}
+ get "t/:topic_id/summary" => "topics#show", constraints: {topic_id: /\d+/}
put "t/:slug/:topic_id" => "topics#update", constraints: {topic_id: /\d+/}
put "t/:slug/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/}
put "t/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/}
diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb
index 5e1428d9696..bacc5f1cf1e 100644
--- a/lib/email/message_builder.rb
+++ b/lib/email/message_builder.rb
@@ -21,20 +21,21 @@ module Email
@to = to
@opts = opts || {}
- @template_args = {site_name: SiteSetting.email_prefix.presence || SiteSetting.title,
- base_url: Discourse.base_url,
- user_preferences_url: "#{Discourse.base_url}/my/preferences" }.merge!(@opts)
+ @template_args = {
+ site_name: SiteSetting.email_prefix.presence || SiteSetting.title,
+ base_url: Discourse.base_url,
+ user_preferences_url: "#{Discourse.base_url}/my/preferences",
+ }.merge!(@opts)
if @template_args[:url].present?
if @opts[:include_respond_instructions] == false
@template_args[:respond_instructions] = ''
else
- @template_args[:respond_instructions] =
- if allow_reply_by_email?
- I18n.t('user_notifications.reply_by_email', @template_args)
- else
- I18n.t('user_notifications.visit_link_to_respond', @template_args)
- end
+ @template_args[:respond_instructions] = if allow_reply_by_email?
+ I18n.t('user_notifications.reply_by_email', @template_args)
+ else
+ I18n.t('user_notifications.visit_link_to_respond', @template_args)
+ end
end
end
end
@@ -56,15 +57,15 @@ module Email
def html_part
return unless html_override = @opts[:html_override]
- if @opts[:add_unsubscribe_link]
+ if @opts[:add_unsubscribe_link]
if response_instructions = @template_args[:respond_instructions]
respond_instructions = PrettyText.cook(response_instructions).html_safe
html_override.gsub!("%{respond_instructions}", respond_instructions)
end
unsubscribe_link = PrettyText.cook(I18n.t('unsubscribe_link', template_args)).html_safe
- html_override.gsub!("%{unsubscribe_link}",unsubscribe_link)
+ html_override.gsub!("%{unsubscribe_link}", unsubscribe_link)
end
styled = Email::Styles.new(html_override)
diff --git a/spec/components/email/message_builder_spec.rb b/spec/components/email/message_builder_spec.rb
index eb03bf53c01..7515dee03fc 100644
--- a/spec/components/email/message_builder_spec.rb
+++ b/spec/components/email/message_builder_spec.rb
@@ -168,7 +168,8 @@ describe Email::MessageBuilder do
let(:message_with_unsubscribe) { Email::MessageBuilder.new(to_address,
body: 'hello world',
- add_unsubscribe_link: true) }
+ add_unsubscribe_link: true,
+ unsubscribe_url: "/t/1234/unsubscribe") }
it "has an List-Unsubscribe header" do
expect(message_with_unsubscribe.header_args['List-Unsubscribe']).to be_present