Skip to content

Commit 4682a37

Browse files
authored
Merge pull request #1797 from codidact/0valt/1040/in-page-follow
Follow/unfollow post threads in place if JS is enabled
2 parents 30817e4 + 08f487c commit 4682a37

8 files changed

Lines changed: 180 additions & 64 deletions

File tree

app/assets/javascripts/comments.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,41 @@ $(() => {
339339
$tgt.find('.js-text').text('copy link');
340340
}, 1000);
341341
});
342+
343+
QPixel.DOM.addSelectorListener('click', '.js-follow-comments', async (ev) => {
344+
ev.preventDefault();
345+
346+
const { target } = ev;
347+
348+
if (!QPixel.DOM.isHTMLElement(target)) {
349+
return;
350+
}
351+
352+
const { postId, action } = target.dataset;
353+
354+
if (!postId || !action) {
355+
return;
356+
}
357+
358+
const shouldFollow = action === 'follow';
359+
360+
const data = shouldFollow ?
361+
await QPixel.followComments(postId) :
362+
await QPixel.unfollowComments(postId);
363+
364+
QPixel.handleJSONResponse(data, () => {
365+
target.dataset.action = shouldFollow ? 'unfollow' : 'follow';
366+
367+
const icon = document.createElement('i');
368+
icon.classList.add('fas', 'fa-fw', shouldFollow ? 'fa-bell-slash' : 'fa-bell');
369+
const text = document.createTextNode(` ${shouldFollow ? 'Unfollow' : 'Follow'} new`);
370+
target.replaceChildren(icon, text);
371+
372+
const form = target.closest('form');
373+
374+
if (form) {
375+
form.action = `/comments/post/${postId}/${shouldFollow ? 'unfollow' : 'follow'}`;
376+
}
377+
});
378+
});
342379
});

app/assets/javascripts/qpixel_api.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,16 @@ window.QPixel = {
471471
return data;
472472
},
473473

474+
followComments: async (postId) => {
475+
const resp = await QPixel.fetchJSON(`/comments/post/${postId}/follow`, {}, {
476+
headers: { 'Accept': 'application/json' }
477+
});
478+
479+
const data = await resp.json();
480+
481+
return data;
482+
},
483+
474484
undeleteComment: async (id) => {
475485
const resp = await QPixel.fetchJSON(`/comments/${id}/delete`, {}, {
476486
headers: { 'Accept': 'application/json' },
@@ -482,6 +492,16 @@ window.QPixel = {
482492
return data;
483493
},
484494

495+
unfollowComments: async (postId) => {
496+
const resp = await QPixel.fetchJSON(`/comments/post/${postId}/unfollow`, {}, {
497+
headers: { 'Accept': 'application/json' }
498+
});
499+
500+
const data = await resp.json();
501+
502+
return data;
503+
},
504+
485505
lockThread: async (id) => {
486506
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/restrict`, {
487507
type: 'lock'

app/assets/javascripts/qpixel_dom.js

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,6 @@ QPixel.DOM = {
44
_delegatedListeners: [],
55
_eventListeners: {},
66

7-
/**
8-
* Add a delegated event listener. Use when an event listener is required that will fire for elements added to the
9-
* DOM dynamically after the delegated listener is added.
10-
* @param {string} event An event name to listen for.
11-
* @param {string} selector A CSS selector representing elements on which to apply the listener.
12-
* @param {EventCallback} callback A callback function to pass to the event listener.
13-
*/
147
addDelegatedListener: (event, selector, callback) => {
158
if (!QPixel.DOM._eventListeners[event]) {
169
const listener = (ev) => {
@@ -26,23 +19,12 @@ QPixel.DOM = {
2619
QPixel.DOM._delegatedListeners.push({ event, selector, callback });
2720
},
2821

29-
/**
30-
* Convenience method. Add an event listener to _all_ elements that currently match a selector.
31-
* @param {string} event An event name to listen for.
32-
* @param {string} selector A CSS selector representing elements on which to apply the listener.
33-
* @param {EventCallback} callback A callback function to pass to the event listener.
34-
*/
3522
addSelectorListener: (event, selector, callback) => {
3623
document.querySelectorAll(selector).forEach((el) => {
3724
el.addEventListener(event, callback);
3825
});
3926
},
4027

41-
/**
42-
* Smoothly fade an element out of view, then remove it.
43-
* @param {HTMLElement} element The element to fade out.
44-
* @param {number} duration A duration for the effect in milliseconds.
45-
*/
4628
fadeOut: (element, duration) => {
4729
element.style.transition = `${duration}ms`;
4830
element.style.opacity = '0';
@@ -51,12 +33,10 @@ QPixel.DOM = {
5133
}, duration);
5234
},
5335

54-
/**
55-
* Helper to set the visibility of an element or array of elements. Uses display: none so should work with screen
56-
* readers.
57-
* @param {HTMLElement|HTMLElement[]} elements An element or array of elements to set visibility for.
58-
* @param {boolean} visible Whether or not the elements should be visible.
59-
*/
36+
isHTMLElement: (node) => {
37+
return node instanceof HTMLElement;
38+
},
39+
6040
setVisible: (elements, visible) => {
6141
if (!Array.isArray(elements)) {
6242
elements = [elements];

app/controllers/comments_controller.rb

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class CommentsController < ApplicationController
44
before_action :authenticate_user!, except: [:post, :show, :thread, :thread_content]
55

66
before_action :set_comment, only: [:update, :destroy, :undelete, :show]
7-
before_action :set_post, only: [:create_thread]
7+
before_action :set_post, only: [:create_thread, :post_follow, :post_unfollow]
88
before_action :set_thread,
99
only: [:create, :thread, :thread_content, :thread_rename, :thread_restrict, :thread_unrestrict,
1010
:thread_followers]
@@ -264,29 +264,33 @@ def thread_unrestrict
264264

265265
def post
266266
@post = Post.find(params[:post_id])
267-
@comment_threads = if current_user&.at_least_moderator? || current_user&.post_privilege?('flag_curate', @post)
268-
CommentThread
269-
else
270-
CommentThread.undeleted
271-
end.where(post: @post).order(deleted: :asc, archived: :asc, reply_count: :desc)
267+
@comment_threads = CommentThread.accessible_to(current_user, @post)
268+
.where(post: @post)
269+
.order(deleted: :asc, archived: :asc, reply_count: :desc)
272270
respond_to do |format|
273271
format.html { render layout: false }
274272
format.json { render json: @comment_threads }
275273
end
276274
end
277275

278276
def post_follow
279-
@post = Post.find(params[:post_id])
280277
if ThreadFollower.where(post: @post, user: current_user).none?
281278
ThreadFollower.create(post: @post, user: current_user)
282279
end
283-
redirect_to post_path(@post)
280+
281+
respond_to do |format|
282+
format.html { redirect_to post_path(@post) }
283+
format.json { render json: { status: 'success' } }
284+
end
284285
end
285286

286287
def post_unfollow
287-
@post = Post.find(params[:post_id])
288288
ThreadFollower.where(post: @post, user: current_user).destroy_all
289-
redirect_to post_path(@post)
289+
290+
respond_to do |format|
291+
format.html { redirect_to post_path(@post) }
292+
format.json { render json: { status: 'success' } }
293+
end
290294
end
291295

292296
def pingable
@@ -302,7 +306,7 @@ def comment_params
302306
end
303307

304308
def set_comment
305-
@comment = Comment.unscoped.find params[:id]
309+
@comment = Comment.unscoped.find(params[:id])
306310
end
307311

308312
def set_post

app/models/comment_thread.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ class CommentThread < ApplicationRecord
1616

1717
after_create :create_follower
1818

19+
# Gets threads appropriately scoped for a given user & post
20+
# @param user [User, nil] user to check
21+
# @para post [Post] post to check
22+
# @return [ActiveRecord::Relation<CommentThread>]
23+
def self.accessible_to(user, post)
24+
if user&.at_least_moderator? || user&.post_privilege?('flag_curate', post)
25+
CommentThread
26+
else
27+
CommentThread.undeleted
28+
end
29+
end
30+
1931
# Is the thread read-only (can't be edited)?
2032
# @return [Boolean] check result
2133
def read_only?

app/views/posts/_expanded.html.erb

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,16 @@
297297
<% unless post.locked? %>
298298
<% if !post.deleted %>
299299
<%= link_to delete_post_path(post), method: :post,
300-
data: { confirm: 'Are you sure you want to delete this post?' }, class: "tools--item is-danger",
300+
data: { confirm: 'Are you sure you want to delete this post?' },
301+
class: "tools--item is-danger",
301302
role: 'button', 'aria-label': 'Delete this post' do %>
302303
<i class="fa fa-trash"></i>
303304
Delete
304305
<% end %>
305306
<% else %>
306307
<%= link_to restore_post_path(post), method: :post,
307-
data: { confirm: 'Restore this post, making it visible to regular users?' }, class: "tools--item is-danger is-filled",
308+
data: { confirm: 'Restore this post, making it visible to regular users?' },
309+
class: "tools--item is-danger is-filled",
308310
role: 'button', 'aria-label': 'Restore this post' do %>
309311
<i class="fa fa-undo"></i>
310312
Restore
@@ -313,7 +315,11 @@
313315
<% end %>
314316
<% end %>
315317
<% if check_your_privilege('flag_curate') %>
316-
<a href="javascript:void(0);" data-modal="#mod-tools-<%= post.id %>" class="tools--item" role="button" aria-label="Open Moderator Tools">
318+
<a href="javascript:void(0)"
319+
data-modal="#mod-tools-<%= post.id %>"
320+
class="tools--item"
321+
role="button"
322+
aria-label="Open Moderator Tools">
317323
<i class="fa fa-wrench"></i>
318324
Tools
319325
</a>
@@ -429,7 +435,9 @@
429435
<input class="form-element js-flag-comment" id="flag-post-<%= post.id %>">
430436
</div>
431437
<div class="widget--footer">
432-
<button class="flag-link button is-filled is-muted" data-post-type="<%= is_question ? 'Question' : 'Answer' %>" data-post-id="<%= post.id %>">
438+
<button class="flag-link button is-filled is-muted"
439+
data-post-type="<%= is_question ? 'Question' : 'Answer' %>"
440+
data-post-id="<%= post.id %>">
433441
Flag for attention
434442
</button>
435443
</div>
@@ -501,7 +509,10 @@
501509

502510
<% if is_top_level && post.children.undeleted.length >= SiteSetting["TableOfContentsThreshold"] && SiteSetting["TableOfContentsThreshold"] != -1 %>
503511
<div class="toc has-margin-left-4" id="toc-toggle">
504-
<button class="toc--header" data-toggle="#toc-toggle" data-toggle-property="class" data-toggle-value="is-active">Table of Contents</button>
512+
<button class="toc--header"
513+
data-toggle="#toc-toggle"
514+
data-toggle-property="class"
515+
data-toggle-value="is-active">Table of Contents</button>
505516
<% sorted_answers = post.children.sort_by { |answer| answer.score }.reverse! %>
506517
<% sorted_answers.each do |answer| %>
507518
<% next if answer.deleted? && !at_least_moderator? %>
@@ -532,23 +543,7 @@
532543
<%= pluralize(public_count, 'comment thread') %>
533544
</h4>
534545
<% if user_signed_in? %>
535-
<% if post.followed_by?(current_user) %>
536-
<%= link_to unfollow_post_comments_path(post_id: post.id), method: :post,
537-
class: "button is-muted is-outlined is-small",
538-
title: 'Don\'t follow new comment threads on this post',
539-
role: 'button',
540-
'aria-label': 'Unfollow new comment threads on this post' do %>
541-
<i class="fas fa-fw fa-bell-slash"></i> Unfollow new
542-
<% end %>
543-
<% else %>
544-
<%= link_to follow_post_comments_path(post_id: post.id), method: :post,
545-
class: "button is-muted is-outlined is-small",
546-
title: 'Follow all new comment threads on this post',
547-
role: 'button',
548-
'aria-label': 'Follow all new comment threads on this post' do %>
549-
<i class="fas fa-fw fa-bell"></i> Follow new
550-
<% end %>
551-
<% end %>
546+
<%= render 'posts/follow_comments_link', post: post, user: current_user %>
552547
<% end %>
553548
</div>
554549
<div class="post--comments-container" role="list">
@@ -558,7 +553,10 @@
558553
</div>
559554
<div class="post--comments-links has-margin-top-1">
560555
<% if available_count > [comment_threads.count, 5].min %>
561-
<a href="#" class="js-more-comments button is-muted is-small" data-post-id="<%= post.id %>" role="button" aria-label="Show more comment threads">
556+
<a href="#" class="js-more-comments button is-muted is-small"
557+
data-post-id="<%= post.id %>"
558+
role="button"
559+
aria-label="Show more comment threads">
562560
Show more
563561
</a>
564562
<% end %>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<%#
2+
"Helper for rendering the follow/unfollow comments buttons
3+
4+
Variables:
5+
post : Post to create the link for
6+
user : User to check the following status for
7+
"%>
8+
9+
<%
10+
is_followed = post.followed_by?(user)
11+
action_path = is_followed ?
12+
unfollow_post_comments_path(post_id: post.id) :
13+
follow_post_comments_path(post_id: post.id)
14+
text = "#{is_followed ? 'Unfollow' : 'Follow'} new"
15+
title = 'Switch following new comment threads on this post'
16+
%>
17+
18+
<%= form_tag action_path, method: :post do %>
19+
<%= button_tag type: :submit,
20+
class: "button is-muted is-outlined is-small js-follow-comments",
21+
data: { post_id: post.id, action: is_followed ? 'unfollow' : 'follow' },
22+
aria: { label: title },
23+
title: title do %>
24+
<i class="fas fa-fw <%= is_followed ? 'fa-bell-slash' : 'fa-bell' %>"></i> <%= text %>
25+
<% end %>
26+
<% end %>

0 commit comments

Comments
 (0)