diff --git a/.travis.yml b/.travis.yml index 74af7c06c6..3df2303ba6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Gemfile b/Gemfile index b5b83c2c4f..4eb299700e 100644 --- a/Gemfile +++ b/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" diff --git a/Gemfile.lock b/Gemfile.lock index cae1f354ba..3b83a8324f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d44e38bb77..03893e4e02 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -922,6 +922,11 @@ div.comment_form_container textarea { width: 100%; } +span.comment_unread { + color: red; + font-weight: 600; +} + /* trees */ .tree, diff --git a/app/controllers/replies_controller.rb b/app/controllers/replies_controller.rb new file mode 100644 index 0000000000..27b1920948 --- /dev/null +++ b/app/controllers/replies_controller.rb @@ -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 diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index d9ae71d286..527528af5c 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -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 @@ def hide 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 @@ def verify_user_can_submit_stories 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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cd66040b68..4e4e323fcb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -79,6 +79,17 @@ def right_header_links @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 @@ def time_ago_in_words_label(time, options = {}) 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 diff --git a/app/helpers/replies_helper.rb b/app/helpers/replies_helper.rb new file mode 100644 index 0000000000..96dd9c70ee --- /dev/null +++ b/app/helpers/replies_helper.rb @@ -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 diff --git a/app/helpers/stories_helper.rb b/app/helpers/stories_helper.rb index c6d1547309..3ab0a8a30a 100644 --- a/app/helpers/stories_helper.rb +++ b/app/helpers/stories_helper.rb @@ -16,4 +16,12 @@ def show_guidelines? 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 diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/read_ribbon.rb b/app/models/read_ribbon.rb new file mode 100644 index 0000000000..5f73b50c70 --- /dev/null +++ b/app/models/read_ribbon.rb @@ -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 diff --git a/app/models/replying_comment.rb b/app/models/replying_comment.rb new file mode 100644 index 0000000000..b2cb41ab4a --- /dev/null +++ b/app/models/replying_comment.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 66358f6813..42cec1a187 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -505,6 +505,10 @@ def update_unread_message_count! 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 " << diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 11aaecf377..9614e42b3e 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -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 %> diff --git a/app/views/comments/threads.html.erb b/app/views/comments/threads.html.erb index 9b05a05821..49e256fd68 100644 --- a/app/views/comments/threads.html.erb +++ b/app/views/comments/threads.html.erb @@ -1,4 +1,3 @@ - <% @threads.each do |thread| %>
+ <% @replies.each do |reply| %> +-
+ <%= render "comments/comment",
+ comment: reply.comment,
+ show_story: true,
+ is_unread: reply.is_unread,
+ show_tree_lines: false %>
+
+ <% end %>
+
+ +<% if @replies.count > RepliesController::REPLIES_PER_PAGE && @filter != 'unread'%> ++