Show story and comment replies, tracking unread ones
This commit is contained in:
parent
925bdc203c
commit
dd42cca880
|
@ -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
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -922,6 +922,11 @@ div.comment_form_container textarea {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
span.comment_unread {
|
||||
color: red;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* trees */
|
||||
|
||||
.tree,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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 " <<
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<% @threads.each do |thread| %>
|
||||
<ol class="comments comments1">
|
||||
<% comments_by_parent = thread.group_by(&:parent_comment_id) %>
|
||||
|
|
|
@ -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}" %>"><<
|
||||
Page <%= @page - 1 %></a>
|
||||
<% end %>
|
||||
|
||||
<% if @replies.any? %>
|
||||
<% if @page && @page > 1 %>
|
||||
|
|
||||
<% end %>
|
||||
|
||||
<a href="/replies/page/<%= @page + 1 %>">Page
|
||||
<%= @page + 1 %> >></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Scenic.configure do |config|
|
||||
config.database = Scenic::Adapters::Mysql.new
|
||||
end
|
|
@ -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"
|
||||
|
|
|
@ -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
|
17
db/schema.rb
17
db/schema.rb
|
@ -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
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue