FEATURE: per-topic unsubscribe option in emails
This commit is contained in:
parent
56abd247e1
commit
6669a2d94d
|
@ -24,8 +24,11 @@ export default Ember.Component.extend(StringBuffer, {
|
|||
}.on('willDestroyElement'),
|
||||
|
||||
renderString(buffer) {
|
||||
const title = this.get('title');
|
||||
if (title) {
|
||||
buffer.push("<h4 class='title'>" + title + "</h4>");
|
||||
}
|
||||
|
||||
buffer.push("<h4 class='title'>" + this.get('title') + "</h4>");
|
||||
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>");
|
||||
buffer.push(this.get('text'));
|
||||
buffer.push("</button>");
|
||||
|
|
|
@ -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"),
|
||||
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
<div class="container">
|
||||
<p>
|
||||
{{{stopNotificiationsText}}}
|
||||
</p>
|
||||
<p>
|
||||
{{i18n "topic.unsubscribe.change_notification_state"}} {{topic-notifications-button topic=model}}
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,3 @@
|
|||
export default Discourse.View.extend({
|
||||
classNames: ["topic-unsubscribe"]
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
<div id='main' class=<%= classes %>>
|
||||
|
||||
<%= render :partial => 'email/post', :locals => {:post => post} %>
|
||||
<%= render partial: 'email/post', locals: { post: post } %>
|
||||
|
||||
<% if context_posts.present? %>
|
||||
<div class='footer'>
|
||||
%{respond_instructions}
|
||||
</div>
|
||||
<hr>
|
||||
<h4 class='.previous-discussion'><%= t "user_notifications.previous_discussion" %></h4>
|
||||
<% if context_posts.present? %>
|
||||
<div class='footer'>%{respond_instructions}</div>
|
||||
|
||||
<% context_posts.each do |p| %>
|
||||
<%= render :partial => 'email/post', :locals => {:post => p} %>
|
||||
<hr>
|
||||
|
||||
<h4 class='.previous-discussion'><%= t "user_notifications.previous_discussion" %></h4>
|
||||
|
||||
<% context_posts.each do |p| %>
|
||||
<%= render partial: 'email/post', locals: { post: p } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<div class='footer'>%{respond_instructions}</div>
|
||||
<div class='footer'>%{unsubscribe_link}</div>
|
||||
|
||||
<div class='footer'>
|
||||
%{respond_instructions}
|
||||
</div>
|
||||
<div class='footer'>
|
||||
%{unsubscribe_link}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div itemscope itemtype="http://schema.org/EmailMessage" style="display:none">
|
||||
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction">
|
||||
<link itemprop="url" href="<%= Discourse.base_url %><%= post.url %>" />
|
||||
<meta itemprop="name" content="<%= t 'read_full_topic' %>"/>
|
||||
</div>
|
||||
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction">
|
||||
<link itemprop="url" href="<%= Discourse.base_url %><%= post.url %>" />
|
||||
<meta itemprop="name" content="<%= t 'read_full_topic' %>"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -981,6 +981,9 @@ en:
|
|||
search: "There are no more search results."
|
||||
|
||||
topic:
|
||||
unsubscribe:
|
||||
stop_notifications: "You will stop receiving notifications for <strong>{{title}}</strong>."
|
||||
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"
|
||||
|
|
|
@ -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] "
|
||||
|
|
|
@ -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+/}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue