Show story and comment replies, tracking unread ones

This commit is contained in:
Hunter Madison 2018-01-31 08:43:07 -05:00 committed by Peter Bhat Harkins
parent 925bdc203c
commit dd42cca880
22 changed files with 264 additions and 6 deletions

View File

@ -3,7 +3,9 @@ language: ruby
services:
- mysql
env:
- DATABASE_URL=mysql2://root:@localhost/lobsters_test
global:
- DATABASE_URL=mysql2://root:@localhost/lobsters_dev
- RAILS_ENV=test
before_script:
- ./bin/rails db:create
- ./bin/rails db:schema:load

View File

@ -10,6 +10,9 @@ gem "mysql2", ">= 0.3.14"
# uncomment to use PostgreSQL
# gem "pg"
gem 'scenic'
gem 'scenic-mysql'
gem "uglifier", ">= 1.3.0"
gem "jquery-rails", "~> 4.3"
gem "dynamic_form"

View File

@ -132,6 +132,11 @@ GEM
rspec-support (3.6.0)
ruby-enum (0.7.1)
i18n
scenic (1.4.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
scenic-mysql (0.1.0)
scenic (>= 1.3)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -175,6 +180,8 @@ DEPENDENCIES
rotp
rqrcode
rspec-rails (~> 3.6)
scenic
scenic-mysql
sqlite3
uglifier (>= 1.3.0)
unicorn

View File

@ -922,6 +922,11 @@ div.comment_form_container textarea {
width: 100%;
}
span.comment_unread {
color: red;
font-weight: 600;
}
/* trees */
.tree,

View File

@ -0,0 +1,54 @@
class RepliesController < ApplicationController
REPLIES_PER_PAGE = 25
before_action :require_logged_in_user_or_400
after_action :update_read_ribbons
def show
@page = params[:page].to_i
if @page == 0
@page = 1
elsif @page < 0 || @page > (2 ** 32)
raise ActionController::RoutingError.new("page out of bounds")
end
@filter = params[:filter] || 'unread'
case @filter
when 'comments'
@heading = @title = "Your Comment Replies"
@replies = ReplyingComment
.comment_replies_for(@user.id)
.offset((@page - 1) * REPLIES_PER_PAGE)
.limit(REPLIES_PER_PAGE)
when 'stories'
@heading = @title = "Your Story Replies"
@replies = ReplyingComment
.story_replies_for(@user.id)
.offset((@page - 1) * REPLIES_PER_PAGE)
.limit(REPLIES_PER_PAGE)
when 'all'
@heading = @title = "All Your Replies"
@replies = ReplyingComment
.for_user(@user.id)
.offset((@page - 1) * REPLIES_PER_PAGE)
.limit(REPLIES_PER_PAGE)
else
@heading = @title = "Your Unread Replies"
@replies = ReplyingComment.unread_replies_for(@user.id)
end
end
private
def update_read_ribbons
return unless @filter == 'unread'
stories = @replies.pluck(:story_id).uniq
stories.each do |story|
ribbon = ReadRibbon.find_by(user_id: @user.id, story_id: story)
ribbon.updated_at = Time.now
ribbon.save!
end
end
end

View File

@ -8,6 +8,7 @@ class StoriesController < ApplicationController
before_action :find_user_story, :only => [ :destroy, :edit, :undelete,
:update ]
before_action :find_story!, :only => [ :suggest, :submit_suggestions ]
around_action :track_story_reads, only: [ :show ], if: -> { @user.present? }
def create
@title = "Submit Story"
@ -292,6 +293,7 @@ class StoriesController < ApplicationController
end
HiddenStory.hide_story_for_user(story.id, @user.id)
ReadRibbon.hide_replies_for(story.id, @user.id)
render :plain => "ok"
end
@ -407,4 +409,12 @@ private
return redirect_to "/"
end
end
def track_story_reads
story = Story.where(short_id: params[:id]).first!
@ribbon = ReadRibbon.where(user_id: @user.id, story_id: story.id).first_or_create
yield
@ribbon.updated_at = Time.now
@ribbon.save
end
end

View File

@ -79,6 +79,17 @@ module ApplicationHelper
@right_header_links = {}
if @user
if (count = @user.unread_replies_count) > 0
@right_header_links.merge!({ "/replies" => {
:class => [ "new_messages" ],
:title => "Replies (#{count})",
} })
else
@right_header_links.merge!({
"/replies" => { :title => "Replies" }
})
end
if (count = @user.unread_message_count) > 0
@right_header_links.merge!({ "/messages" => {
:class => [ "new_messages" ],
@ -170,6 +181,12 @@ module ApplicationHelper
ago = "#{years} year#{years == 1 ? "" : "s"} ago"
end
raw(content_tag(:span, ago, :title => time.strftime("%F %T %z")))
span_class = ''
if options[:mark_unread]
span_class += 'comment_unread'
end
raw(content_tag(:span, ago, title: time.strftime("%F %T %z"), class: span_class))
end
end

View File

@ -0,0 +1,11 @@
module RepliesHelper
def link_to_filter(name)
title = name.titleize
if @filter != name
link_to(title, replies_path(filter: name))
else
title
end
end
end

View File

@ -16,4 +16,12 @@ module StoriesHelper
false
end
def is_unread?(comment)
if !@user || !@ribbon
return false
end
(comment.created_at > @ribbon.updated_at) && (comment.user_id != @user.id)
end
end

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

10
app/models/read_ribbon.rb Normal file
View File

@ -0,0 +1,10 @@
class ReadRibbon < ApplicationRecord
belongs_to :user
belongs_to :story
def self.hide_replies_for(story_id, user_id)
ribbon = find_by(user_id: user_id, story_id: story_id)
ribbon.is_following = false
ribbon.save!
end
end

View File

@ -0,0 +1,16 @@
class ReplyingComment < ApplicationRecord
attribute :is_unread, :boolean
belongs_to :comment
scope :for_user, ->(user_id) { where(user_id: user_id).order(comment_created_at: :desc) }
scope :unread_replies_for, ->(user_id) { for_user(user_id).where(is_unread: true) }
scope :comment_replies_for, ->(user_id) { for_user(user_id).where('parent_comment_id is not null') }
scope :story_replies_for, ->(user_id) { for_user(user_id).where('parent_comment_id is null') }
protected
# This is a view, not a real table
def readonly?
true
end
end

View File

@ -505,6 +505,10 @@ class User < ActiveRecord::Base
self.received_messages.unread.count)
end
def unread_replies_count
ReplyingComment.where(user_id: self.id, is_unread: true).count
end
def votes_for_others
self.votes.joins(:story, :comment).
where("comments.user_id <> votes.user_id AND " <<

View File

@ -73,8 +73,11 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
<% elsif comment.is_from_email? %>
e-mailed
<% end %>
<% if defined?(is_unread) && is_unread %>
<% mark_unread = true %>
<% end %>
<%= time_ago_in_words_label((comment.has_been_edited? ?
comment.updated_at : comment.created_at)) %>
comment.updated_at : comment.created_at), strip_about: true, mark_unread: mark_unread) %>
<% end %>
<% if !comment.previewing %>

View File

@ -1,4 +1,3 @@
<% @threads.each do |thread| %>
<ol class="comments comments1">
<% comments_by_parent = thread.group_by(&:parent_comment_id) %>

View File

@ -0,0 +1,44 @@
<div class="box wide">
<div class="legend right">
<%= link_to_filter('unread') %> |
<%= link_to_filter('all') %> |
<%= link_to_filter('comments') %> |
<%= link_to_filter('stories') %>
</div>
<div class="legend">
<%= @heading %>
</div>
</div>
<ol class="comments comments1">
<% @replies.each do |reply| %>
<li class="comments_subtree">
<%= render "comments/comment",
comment: reply.comment,
show_story: true,
is_unread: reply.is_unread,
show_tree_lines: false %>
<ol class="comments"></ol>
</li>
<% end %>
</ol>
<% if @replies.count > RepliesController::REPLIES_PER_PAGE && @filter != 'unread'%>
<div class="morelink">
<% if @page && @page > 1 %>
<a href="/replies<%= @page == 2 ? "" : "/page/#{@page - 1}" %>">&lt;&lt;
Page <%= @page - 1 %></a>
<% end %>
<% if @replies.any? %>
<% if @page && @page > 1 %>
|
<% end %>
<a href="/replies/page/<%= @page + 1 %>">Page
<%= @page + 1 %> &gt;&gt;</a>
<% end %>
</div>
<% end %>

View File

@ -56,7 +56,8 @@
:show_story => (comment.story_id != @story.id),
:show_tree_lines => true,
:was_merged => (comment.story_id != @story.id),
:children => children %>
:children => children,
:is_unread => is_unread?(comment) %>
<% if ancestors.present? %>
<div class="comment_siblings_tree_line"></div>

View File

@ -0,0 +1,3 @@
Scenic.configure do |config|
config.database = Scenic::Adapters::Mysql.new
end

View File

@ -33,6 +33,9 @@ Lobsters::Application.routes.draw do
get "/threads" => "comments#threads"
get "/threads/:user" => "comments#threads"
get "/replies" => "replies#show"
get "/replies/page/:page" => "replies#show"
get "/login" => "login#index"
post "/login" => "login#login"
post "/logout" => "login#logout"

View File

@ -0,0 +1,13 @@
class AddReadNotificationSupport < ActiveRecord::Migration[5.1]
def change
create_table :read_ribbons do |t|
t.boolean :is_following, default: true
t.timestamps
end
add_reference :read_ribbons, :user, index: true
add_reference :read_ribbons, :story, index: true
create_view :replying_comments
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180124143340) do
ActiveRecord::Schema.define(version: 20180130235553) do
create_table "comments", id: :integer, unsigned: true, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
t.datetime "created_at", null: false
@ -115,6 +115,16 @@ ActiveRecord::Schema.define(version: 20180124143340) do
t.index ["created_at"], name: "index_moderations_on_created_at"
end
create_table "read_ribbons", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.boolean "is_following", default: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id"
t.bigint "story_id"
t.index ["story_id"], name: "index_read_ribbons_on_story_id"
t.index ["user_id"], name: "index_read_ribbons_on_user_id"
end
create_table "saved_stories", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -236,4 +246,9 @@ ActiveRecord::Schema.define(version: 20180124143340) do
t.index ["user_id", "story_id"], name: "user_id_story_id"
end
create_view "replying_comments", sql_definition: <<-SQL
select `lobsters_dev`.`read_ribbons`.`user_id` AS `user_id`,`lobsters_dev`.`comments`.`id` AS `comment_id`,`lobsters_dev`.`read_ribbons`.`story_id` AS `story_id`,`lobsters_dev`.`comments`.`parent_comment_id` AS `parent_comment_id`,`lobsters_dev`.`comments`.`created_at` AS `comment_created_at`,`parent_comments`.`user_id` AS `parent_comment_author_id`,`lobsters_dev`.`comments`.`user_id` AS `comment_author_id`,`lobsters_dev`.`stories`.`user_id` AS `story_author_id`,(`lobsters_dev`.`read_ribbons`.`updated_at` < `lobsters_dev`.`comments`.`created_at`) AS `is_unread` from (((`lobsters_dev`.`read_ribbons` join `lobsters_dev`.`comments` on((`lobsters_dev`.`comments`.`story_id` = `lobsters_dev`.`read_ribbons`.`story_id`))) join `lobsters_dev`.`stories` on((`lobsters_dev`.`stories`.`id` = `lobsters_dev`.`comments`.`story_id`))) left join `lobsters_dev`.`comments` `parent_comments` on((`parent_comments`.`id` = `lobsters_dev`.`comments`.`parent_comment_id`))) where ((`lobsters_dev`.`read_ribbons`.`is_following` = 1) and (`lobsters_dev`.`comments`.`user_id` <> `lobsters_dev`.`read_ribbons`.`user_id`) and ((`parent_comments`.`user_id` = `lobsters_dev`.`read_ribbons`.`user_id`) or (isnull(`parent_comments`.`user_id`) and (`lobsters_dev`.`stories`.`user_id` = `lobsters_dev`.`read_ribbons`.`user_id`))) and ((`lobsters_dev`.`comments`.`upvotes` - `lobsters_dev`.`comments`.`downvotes`) < 0) and ((`parent_comments`.`upvotes` - `parent_comments`.`downvotes`) < 0))
SQL
end

View File

@ -0,0 +1,27 @@
SELECT
read_ribbons.user_id,
comments.id as comment_id,
read_ribbons.story_id as story_id,
comments.parent_comment_id,
comments.created_at as comment_created_at,
parent_comments.user_id as parent_comment_author_id,
comments.user_id as comment_author_id,
stories.user_id as story_author_id,
(read_ribbons.updated_at < comments.created_at) as is_unread
FROM
read_ribbons
JOIN
comments ON comments.story_id = read_ribbons.story_id
JOIN
stories ON stories.id = comments.story_id
LEFT JOIN
comments parent_comments ON parent_comments.id = comments.parent_comment_id
WHERE
read_ribbons.is_following = 1
AND comments.user_id != read_ribbons.user_id
AND
(parent_comments.user_id = read_ribbons.user_id
OR (parent_comments.user_id IS NULL
AND stories.user_id = read_ribbons.user_id))
AND (comments.upvotes - comments.downvotes) < 0
AND (parent_comments.upvotes - parent_comments.downvotes) < 0;