Mute notifications for a comment thread (#1636)
* Use idempotent update instead of toggle * Update editor guide with new comment instructions * Render the settings button on page load * Add settings button to comment dropdown * Update styles to properly render * Add comment mute function and settings page * Clean up erb spacing * Remove unneccessary user_id param * Add receive_notifications for policy spec * Update name for accuracy * Add request spec for comments_mute * Remove unused permitted attributes * Add actually used permitted attributes
This commit is contained in:
parent
9d72091f42
commit
c4677071a7
16 changed files with 166 additions and 22 deletions
|
|
@ -57,12 +57,24 @@ function addRelevantButtonsToArticle(user) {
|
|||
}
|
||||
}
|
||||
var commentsContainer = document.getElementById('comments-container');
|
||||
if (commentsContainer && user.trusted) {
|
||||
var modButts = document.getElementsByClassName('mod-actions');
|
||||
for (var i = 0; i < modButts.length; i++) {
|
||||
var butt = modButts[i];
|
||||
butt.className = 'mod-actions';
|
||||
butt.style.display = 'inline-block';
|
||||
if (commentsContainer) {
|
||||
var settingsButts = document.getElementsByClassName('comment-actions');
|
||||
|
||||
for (var i = 0; i < settingsButts.length; i++) {
|
||||
var butt = settingsButts[i];
|
||||
if (parseInt(butt.dataset.userId) === user.id) {
|
||||
butt.className = 'comment-actions';
|
||||
butt.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
if (user.trusted) {
|
||||
var modButts = document.getElementsByClassName('mod-actions');
|
||||
for (var i = 0; i < modButts.length; i++) {
|
||||
var butt = modButts[i];
|
||||
butt.className = 'mod-actions';
|
||||
butt.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,7 +652,6 @@ a.header-link{
|
|||
top: 40px;
|
||||
right: 10px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
|
|
@ -662,6 +661,7 @@ a.header-link{
|
|||
border-radius: 3px;
|
||||
background: white;
|
||||
z-index:20;
|
||||
width: 130px;
|
||||
box-shadow: $shadow;
|
||||
&.showing{
|
||||
display: block;
|
||||
|
|
@ -671,11 +671,12 @@ a.header-link{
|
|||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
width: 93px;
|
||||
width: 98px;
|
||||
height: 14px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover { background-color: #f1f1f1 }
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ class ArticleMutesController < ApplicationController
|
|||
after_action :verify_authorized
|
||||
|
||||
def update
|
||||
@article = Article.find_by(id: params[:id], user_id: current_user.id)
|
||||
@article = Article.find_by(id: params[:id])
|
||||
authorize @article
|
||||
@article.update(receive_notifications: !@article.receive_notifications)
|
||||
@article.update(receive_notifications: permitted_attributes(@article)[:receive_notifications])
|
||||
redirect_to "/dashboard"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
11
app/controllers/comment_mutes_controller.rb
Normal file
11
app/controllers/comment_mutes_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class CommentMutesController < ApplicationController
|
||||
after_action :verify_authorized
|
||||
|
||||
def update
|
||||
@comment = Comment.find_by(id: params[:id])
|
||||
authorize @comment
|
||||
related_comments_ids = @comment.subtree.union(@comment.ancestors).where(user_id: @comment.user_id).pluck(:id)
|
||||
Comment.where(id: related_comments_ids).update_all(receive_notifications: permitted_attributes(@comment)[:receive_notifications])
|
||||
redirect_to "#{@comment.path}/settings"
|
||||
end
|
||||
end
|
||||
|
|
@ -148,6 +148,12 @@ class CommentsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def settings
|
||||
@comment = Comment.find(params[:id_code].to_i(26))
|
||||
authorize @comment
|
||||
render :settings
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class ArticlePolicy < ApplicationPolicy
|
|||
def permitted_attributes
|
||||
%i[title body_html body_markdown main_image published canonical_url
|
||||
description allow_small_edits allow_big_edits tag_list publish_under_org
|
||||
video video_code video_source_url video_thumbnail_url]
|
||||
video video_code video_source_url video_thumbnail_url receive_notifications]
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -19,12 +19,16 @@ class CommentPolicy < ApplicationPolicy
|
|||
edit?
|
||||
end
|
||||
|
||||
def settings?
|
||||
edit?
|
||||
end
|
||||
|
||||
def preview?
|
||||
true
|
||||
end
|
||||
|
||||
def permitted_attributes_for_update
|
||||
%i[body_markdown]
|
||||
%i[body_markdown receive_notifications]
|
||||
end
|
||||
|
||||
def permitted_attributes_for_preview
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@
|
|||
<a href="<%= comment.path %>">
|
||||
Permalink
|
||||
</a>
|
||||
<span class="mod-actions hidden" style="display:none">
|
||||
<a href="<%=comment.path%>/mod" class="edit-butt" rel="nofollow" style="color:#0a0a0a">Moderate</a>
|
||||
<span class="comment-actions hidden" data-user-id="<%= comment.user_id %>" style="display: none;">
|
||||
<a href="<%= comment.path %>/settings" rel="nofollow" style="color:#0a0a0a;" data-no-instant>Settings</a>
|
||||
</span>
|
||||
<span class="mod-actions hidden" style="display: none;">
|
||||
<a href="<%=comment.path%>/mod" rel="nofollow" style="color:#0a0a0a">Moderate</a>
|
||||
</span>
|
||||
<a href="/report-abuse?url=https://dev.to<%= comment.path %>">Report Abuse</a>
|
||||
</div>
|
||||
|
|
@ -69,7 +72,7 @@
|
|||
<img class="voted-heart" src="<%= asset_path("emoji/emoji-one-heart.png") %>"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions" data-comment-id="<%= comment.id %>" data-path="<%=commentable.path%>/comments/<%= comment.id_code_generated %>">
|
||||
<div class="actions" data-comment-id="<%= comment.id %>" data-path="<%= commentable.path %>/comments/<%= comment.id_code_generated %>">
|
||||
<span class="current-user-actions hidden" style="display:none">
|
||||
<a data-no-instant href="<%=commentable.path%>/comments/<%= comment.id_code_generated %>/delete_confirm" class="edit-butt" rel="nofollow">DELETE</a>
|
||||
<a href="<%=commentable.path%>/comments/<%= comment.id_code_generated %>/edit" class="edit-butt" rel="nofollow">EDIT</a>
|
||||
|
|
|
|||
38
app/views/comments/settings.html.erb
Normal file
38
app/views/comments/settings.html.erb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<style>
|
||||
.pill{
|
||||
padding:4px 20px;
|
||||
border-radius:100px;
|
||||
color:white;
|
||||
margin-right:5px;
|
||||
font-size:14px;
|
||||
border: 2px solid transparent;
|
||||
margin-top: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
.mute{
|
||||
background-color: #EE7453;
|
||||
color: #ffffff;
|
||||
}
|
||||
.unmute{
|
||||
/* inverted background color */
|
||||
background-color: #1395b8;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container{
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<h2>Settings for: <a href="<%= @comment.path %>"><%= @comment.title %></a></h2>
|
||||
|
||||
<% if @comment.receive_notifications %>
|
||||
<p>This will mute all notifications for this comment and any of your comments in the thread.</p>
|
||||
<% else %>
|
||||
<p>All notifications for this comment and any of your comments in the thread are currently muted.</p>
|
||||
<% end %>
|
||||
<%= form_for(@comment, url: "/comment_mutes/#{@comment.id}", method: "patch", html: { class: "mute-form" }) do |f| %>
|
||||
<%= f.hidden_field :receive_notifications, value: !@comment.receive_notifications %>
|
||||
<%= f.submit @comment.receive_notifications ? "MUTE NOTIFICATIONS" : "NOTIFICATIONS MUTED", class: "cta pill #{@comment.receive_notifications ? "mute" : "unmute"}" %>
|
||||
<% end %>
|
||||
<br>
|
||||
</div>
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
<a href="<%= article.path %>/edit" class="pill cta green">EDIT</a>
|
||||
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
|
||||
<%= form_for(article, url: "/article_mutes/#{article.id}", method: "patch", html: { class: "mute-form" }) do |f| %>
|
||||
<%= f.hidden_field :receive_notifications, value: !article.receive_notifications %>
|
||||
<%= f.submit article.receive_notifications ? "MUTE NOTIFICATIONS" : "NOTIFICATIONS MUTED", class: "cta pill #{article.receive_notifications ? "mute" : "unmute"}" %>
|
||||
<% end %>
|
||||
<% if article.published %>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
<code>{% tag git %}</code>
|
||||
|
||||
<h3><strong>dev.to Comment Embed</strong></h3>
|
||||
<p>All you need is the <code>ID</code> at the end of a comment URL. To get the comment link, click the top right arrow on a comment and then click "Permalink". Here's an example:</p>
|
||||
<p>All you need is the <code>ID</code> at the end of a comment URL. To get the comment link, click either the timestamp or the menu button in the top right corner on a comment and then click "Permalink". Here's an example:</p>
|
||||
<code>{% devcomment 2d1a %}</code>
|
||||
|
||||
<h3><strong>dev.to Podcast Episode Embed</strong></h3>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ Rails.application.routes.draw do
|
|||
resources :articles, only: %i[update create destroy]
|
||||
resources :article_mutes, only: %i[update]
|
||||
resources :comments, only: %i[create update destroy]
|
||||
resources :comment_mutes, only: %i[update]
|
||||
resources :users, only: [:update]
|
||||
resources :reactions, only: %i[index create]
|
||||
resources :feedback_messages, only: %i[index create]
|
||||
|
|
@ -291,6 +292,7 @@ Rails.application.routes.draw do
|
|||
get "/:username/comment/:id_code/edit" => "comments#edit"
|
||||
get "/:username/comment/:id_code/delete_confirm" => "comments#delete_confirm"
|
||||
get "/:username/comment/:id_code/mod" => "moderations#comment"
|
||||
get "/:username/comment/:id_code/settings", to: "comments#settings"
|
||||
|
||||
get "/:username/:slug/:view" => "stories#show",
|
||||
constraints: { view: /moderate/ }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe ArticlePolicy do
|
|||
let(:valid_attributes) do
|
||||
%i[title body_html body_markdown main_image published
|
||||
description allow_small_edits allow_big_edits tag_list publish_under_org
|
||||
video video_code video_source_url video_thumbnail_url]
|
||||
video video_code video_source_url video_thumbnail_url receive_notifications]
|
||||
end
|
||||
|
||||
context "when user is not signed-in" do
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe CommentPolicy do
|
|||
end
|
||||
|
||||
let(:valid_attributes_for_update) do
|
||||
%i[body_markdown]
|
||||
%i[body_markdown receive_notifications]
|
||||
end
|
||||
|
||||
context "when user is not signed-in" do
|
||||
|
|
@ -25,9 +25,7 @@ RSpec.describe CommentPolicy do
|
|||
it { is_expected.to permit_actions(%i[create]) }
|
||||
it { is_expected.to forbid_actions(%i[edit update destroy delete_confirm]) }
|
||||
|
||||
it do
|
||||
expect(comment_policy).to permit_mass_assignment_of(valid_attributes_for_create).for_action(:create)
|
||||
end
|
||||
it { is_expected.to permit_mass_assignment_of(valid_attributes_for_create).for_action(:create) }
|
||||
|
||||
context "with banned status" do
|
||||
before { user.add_role :banned }
|
||||
|
|
|
|||
68
spec/requests/comment_mutes_spec.rb
Normal file
68
spec/requests/comment_mutes_spec.rb
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "CommentMutes", type: :request do
|
||||
let(:original_commenter) { create(:user) }
|
||||
let(:other_commenter) { create(:user) }
|
||||
let(:article) { create(:article) }
|
||||
let(:parent_comment_by_og) { create(:comment, commentable: article, user: original_commenter) }
|
||||
let(:child_of_parent_by_other) { create(:comment, commentable: article, user: other_commenter, ancestry: parent_comment_by_og.id.to_s) }
|
||||
let(:child_of_child_by_og) { create(:comment, commentable: article, user: original_commenter, ancestry: "#{parent_comment_by_og.id}/#{child_of_parent_by_other.id}") }
|
||||
let(:child_of_child_of_child_by_other) { create(:comment, commentable: article, user: other_commenter, ancestry: "#{parent_comment_by_og.id}/#{child_of_parent_by_other.id}/#{child_of_child_by_og.id}") }
|
||||
let(:child_of_child_of_child_by_og) { create(:comment, commentable: article, user: original_commenter, ancestry: "#{parent_comment_by_og.id}/#{child_of_parent_by_other.id}/#{child_of_child_by_og.id}/#{child_of_child_by_other.id}") }
|
||||
let(:child_of_child_by_other) { create(:comment, commentable: article, user: other_commenter, ancestry: "#{parent_comment_by_og.id}/#{child_of_parent_by_other.id}") }
|
||||
let(:child2_of_child_of_child_by_og) { create(:comment, commentable: article, user: original_commenter, ancestry: "#{parent_comment_by_og.id}/#{child_of_parent_by_other.id}/#{child_of_child_by_other.id}") }
|
||||
let(:parent_comment_by_other) { create(:comment, commentable: article, user: other_commenter) }
|
||||
|
||||
describe "PATCH /comment_mutes/:id" do
|
||||
context "when an article has two parent comments by two different people" do
|
||||
before do
|
||||
parent_comment_by_og
|
||||
parent_comment_by_other
|
||||
end
|
||||
|
||||
it "mutes the parent comment" do
|
||||
sign_in original_commenter
|
||||
patch "/comment_mutes/#{parent_comment_by_og.id}", params: { comment: { receive_notifications: "false" } }
|
||||
expect(parent_comment_by_og.reload.receive_notifications).to be false
|
||||
end
|
||||
|
||||
it "does not mute the someone else's parent comment" do
|
||||
sign_in original_commenter
|
||||
patch "/comment_mutes/#{parent_comment_by_og.id}", params: { comment: { receive_notifications: "false" } }
|
||||
expect(parent_comment_by_other.reload.receive_notifications).to be true
|
||||
end
|
||||
|
||||
it "unmutes the parent comment if already muted" do
|
||||
sign_in original_commenter
|
||||
parent_comment_by_og.update(receive_notifications: false)
|
||||
patch "/comment_mutes/#{parent_comment_by_og.id}", params: { comment: { receive_notifications: "true" } }
|
||||
expect(parent_comment_by_og.reload.receive_notifications).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context "when an article has a single comment thread with multiple commenters" do
|
||||
before do
|
||||
child_of_child_of_child_by_og
|
||||
child_of_child_of_child_by_other
|
||||
child2_of_child_of_child_by_og
|
||||
parent_comment_by_other
|
||||
sign_in original_commenter
|
||||
patch "/comment_mutes/#{parent_comment_by_og.id}", params: { comment: { receive_notifications: "false" } }
|
||||
end
|
||||
|
||||
it "mutes all of the original commenter's comments in a single thread" do
|
||||
user_ids_of_muted_comments = Comment.where(receive_notifications: false).pluck(:user_id)
|
||||
expect(user_ids_of_muted_comments.uniq).to eq [original_commenter.id]
|
||||
end
|
||||
|
||||
it "does not mute someone else's comment of a different thread" do
|
||||
expect(parent_comment_by_other.receive_notifications).to be true
|
||||
end
|
||||
|
||||
it "does not mute the other commenter's comments in the same thread" do
|
||||
results = parent_comment_by_og.subtree.where(user: other_commenter).pluck(:receive_notifications)
|
||||
expect(results.uniq).to eq [true]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# http://localhost:3000/api/comments?a_id=23
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "ArticlesApi", type: :request do
|
||||
RSpec.describe "CommentsApi", type: :request do
|
||||
let(:article) { create(:article) }
|
||||
|
||||
before do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue