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