diff --git a/app/controllers/api/v0/reactions_controller.rb b/app/controllers/api/v0/reactions_controller.rb index 4ebc4cb5b..434b333fb 100644 --- a/app/controllers/api/v0/reactions_controller.rb +++ b/app/controllers/api/v0/reactions_controller.rb @@ -15,6 +15,7 @@ module Api reactable_type: params[:reactable_type], category: params[:category] || "like", ) + Notification.send_reaction_notification(@reaction) if @reaction.reactable.user_id != current_user.id render json: { reaction: @reaction.to_json } end diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 139a3340d..cb747ef15 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -112,10 +112,10 @@ class ArticlesController < ApplicationController handle_org_assignment handle_hiring_tag if @article.published - Notification.send_all(@article, "Published") if @article.previous_changes.include?("published") + Notification.send_to_followers(@article, @user.followers, "Published") if @article.saved_changes["published_at"]&.include?(nil) path = @article.path else - Notification.remove_all(@article, "Published") + Notification.remove_all(id: @article.id, class_name: "Article", action: "Published") path = "/#{@article.username}/#{@article.slug}?preview=#{@article.password}" end redirect_to (params[:destination] || path) @@ -132,6 +132,7 @@ class ArticlesController < ApplicationController def destroy authorize @article @article.destroy! + Notification.remove_all(id: @article.id, class_name: "Article", action: "Published") respond_to do |format| format.html { redirect_to "/dashboard", notice: "Article was successfully deleted." } format.json { head :no_content } diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index c4107777e..d7c8fec4d 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -63,6 +63,7 @@ class CommentsController < ApplicationController current_user.update(checked_code_of_conduct: true) end Mention.create_all(@comment) + Notification.send_new_comment_notifications(@comment) if @comment.invalid? @comment.destroy render json: { status: "comment already exists" } @@ -115,6 +116,7 @@ class CommentsController < ApplicationController def destroy authorize @comment @commentable_path = @comment.commentable.path + Notification.remove_all(id: @comment.id, class_name: "Comment") if @comment.is_childless? @comment.destroy else diff --git a/app/controllers/follows_controller.rb b/app/controllers/follows_controller.rb index f07e6b35e..67b4d61a8 100644 --- a/app/controllers/follows_controller.rb +++ b/app/controllers/follows_controller.rb @@ -24,10 +24,12 @@ class FollowsController < ApplicationController User.find(params[:followable_id]) end @result = if params[:verb] == "unfollow" - current_user.stop_following(followable) + follow = current_user.stop_following(followable) + Notification.remove_all(id: follow.id, class_name: "Follow") "unfollowed" else - current_user.follow(followable) + follow = current_user.follow(followable) + Notification.send_new_follower_notification(follow) "followed" end current_user.save diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb index 54526a77d..2eda1c495 100644 --- a/app/controllers/reactions_controller.rb +++ b/app/controllers/reactions_controller.rb @@ -41,26 +41,29 @@ class ReactionsController < ApplicationController def create authorize Reaction Rails.cache.delete "count_for_reactable-#{params[:reactable_type]}-#{params[:reactable_id]}" + category = params[:category] || "like" reaction = Reaction.where( user_id: current_user.id, reactable_id: params[:reactable_id], reactable_type: params[:reactable_type], - category: params[:category] || "like", + category: category, ).first if reaction reaction.user.touch reaction.destroy + Notification.remove_all(id: reaction.id, class_name: "Reaction", action: category) unless reaction.reactable.user_id == current_user.id @result = "destroy" else - Reaction.create!( + reaction = Reaction.create!( user_id: current_user.id, reactable_id: params[:reactable_id], reactable_type: params[:reactable_type], - category: params[:category] || "like", + category: category, ) + Notification.send_reaction_notification(reaction) unless reaction.reactable.user_id == current_user.id @result = "create" end - render json: { result: @result, category: params[:category] || "like" } + render json: { result: @result, category: category } end def cached_user_positive_reactions(user) diff --git a/app/models/article.rb b/app/models/article.rb index 793f3f59d..3382cacad 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -16,7 +16,7 @@ class Article < ApplicationRecord has_many :comments, as: :commentable has_many :buffer_updates has_many :reactions, as: :reactable, dependent: :destroy - has_many :notifications, as: :notifiable + has_many :notifications, as: :notifiable validates :slug, presence: { if: :published? }, format: /\A[0-9a-z-]*\z/, uniqueness: { scope: :user_id } @@ -52,6 +52,7 @@ class Article < ApplicationRecord after_save :bust_cache after_save :update_main_image_background_hex after_save :detect_human_language + after_update :update_notifications, if: Proc.new { |article| article.notifications.length.positive? && !article.saved_changes.empty? } # after_save :send_to_moderator # turned off for now before_destroy :before_destroy_actions @@ -379,6 +380,10 @@ class Article < ApplicationRecord private + def update_notifications + Notification.update_notifications(self, "Published") + end + # def send_to_moderator # ModerationService.new.send_moderation_notification(self) if published # turned off for now diff --git a/app/models/badge_achievement.rb b/app/models/badge_achievement.rb index 73a502da9..781ccb2af 100644 --- a/app/models/badge_achievement.rb +++ b/app/models/badge_achievement.rb @@ -10,6 +10,7 @@ class BadgeAchievement < ApplicationRecord include StreamRails::Activity as_activity + after_create :notify_recipient after_create :send_email_notification before_validation :render_rewarding_context_message_html @@ -54,6 +55,10 @@ class BadgeAchievement < ApplicationRecord private + def notify_recipient + Notification.send_new_badge_notification(self) + end + def send_email_notification if user.class.name == "User" && user.email.present? && user.email_badge_notifications NotifyMailer.new_badge_email(self).deliver diff --git a/app/models/broadcast.rb b/app/models/broadcast.rb index c32e0323e..e8abbdedc 100644 --- a/app/models/broadcast.rb +++ b/app/models/broadcast.rb @@ -4,28 +4,6 @@ class Broadcast < ApplicationRecord validates :title, :type_of, :processed_html, presence: true validates :type_of, inclusion: { in: %w(Announcement Onboarding) } - def self.send_welcome_notification(user_id) - welcome_broadcast = Broadcast.find_by(title: "Welcome Notification") - return if welcome_broadcast == nil - Notification.create( - user_id: user_id, - notifiable_id: welcome_broadcast.id, - notifiable_type: "Broadcast", - action: welcome_broadcast.type_of, - ) - end - - # method not in use; will be in use if we choose to use markdown - def evaluate_markdown - return if body_markdown.blank? - begin - parsed_markdown = MarkdownParser.new(body_markdown) - self.processed_html = get_inner_body(parsed_markdown.finalize) - rescue StandardError => e - errors[:base] << ErrorMessageCleaner.new(e.message).clean - end - end - def get_inner_body(content) Nokogiri::HTML(content).at("body").inner_html end diff --git a/app/models/comment.rb b/app/models/comment.rb index 6e78aaa8e..deec94c97 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -26,6 +26,8 @@ class Comment < ApplicationRecord after_create :send_to_moderator before_save :set_markdown_character_count before_create :adjust_comment_parent_based_on_depth + after_update :update_notifications, if: Proc.new { |comment| comment.saved_changes.include? "body_markdown" } + after_update :remove_notifications, if: :deleted before_validation :evaluate_markdown validate :permissions @@ -152,7 +154,15 @@ class Comment < ApplicationRecord id.to_s(26) end - # notifications + def custom_css + MarkdownParser.new(body_markdown).tags_used.map do |tag| + Rails.application.assets["ltags/#{tag}.css"].to_s + end.join + end + + def title + ActionController::Base.helpers.truncate(ActionController::Base.helpers.strip_tags(processed_html), length: 60) + end def activity_notify user_ids = ancestors.map(&:user_id).to_set @@ -160,12 +170,6 @@ class Comment < ApplicationRecord user_ids.delete(user_id).map { |id| StreamNotifier.new(id).notify } end - def custom_css - MarkdownParser.new(body_markdown).tags_used.map do |tag| - Rails.application.assets["ltags/#{tag}.css"].to_s - end.join - end - def activity_object self end @@ -189,10 +193,6 @@ class Comment < ApplicationRecord end end - def title - ActionController::Base.helpers.truncate(ActionController::Base.helpers.strip_tags(processed_html), length: 60) - end - def video nil end @@ -235,9 +235,17 @@ class Comment < ApplicationRecord private + def update_notifications + Notification.update_notifications(self) + end + + def remove_notifications + Notification.remove_all(id: id, class_name: "Comment") + end + def send_to_moderator return if user && user.comments_count > 10 - ModerationService.new.send_moderation_notification(self) + Notification.send_moderation_notification(self) end def evaluate_markdown diff --git a/app/models/mention.rb b/app/models/mention.rb index fd5c5c889..b904287c1 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -23,7 +23,7 @@ class Mention < ApplicationRecord mentions = [] doc.css(".comment-mentioned-user").each do |link| username = link.text.gsub("@", "").downcase - if user = User.find_by_username(link.text.gsub("@", "").downcase) + if user = User.find_by_username(username) usernames << username mentions << create_mention(user) end @@ -36,12 +36,16 @@ class Mention < ApplicationRecord private def delete_removed_mentions(usernames) - users = User.where(username: usernames) - @notifiable.mentions.where.not(user_id: users.pluck(:id)).destroy_all + user_ids = User.where(username: usernames).pluck(:id) + mentions = @notifiable.mentions.where.not(user_id: user_ids).destroy_all + Notification.remove_each(mentions) unless mentions.blank? end def create_mention(user) - Mention.create(user_id: user.id, mentionable_id: @notifiable.id, mentionable_type: @notifiable.class.name) + mention = Mention.create(user_id: user.id, mentionable_id: @notifiable.id, mentionable_type: @notifiable.class.name) + # mentionable_type = model that created the mention, user = user to be mentioned + Notification.send_mention_notification(mention) + mention end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 65c35dc7e..0bebb28d6 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -2,77 +2,245 @@ class Notification < ApplicationRecord belongs_to :notifiable, polymorphic: true belongs_to :user - include StreamRails::Activity - as_activity - - validates :user_id, presence: true, - uniqueness: { scope: %i[notifiable_id - notifiable_type - action] } + validates :user_id, uniqueness: { scope: %i[notifiable_id notifiable_type action] } class << self - def send_all(notifiable, action) - if notifiable.class.name == "Article" - return if notifiable.created_at < Time.new(2017, 0o7, 0o7, 0o0, 0o0, 0o0, "+00:00") - notifiable.user.followers.each do |follower| - Notification.create( - user_id: follower.id, - notifiable_id: notifiable.id, - notifiable_type: "Article", - action: action, - ) - end - elsif notifiable.class.name == "Broadcast" - if action == "Announcement" - User.all.each do |user| - Notification.create!( - user_id: user.id, - notifiable_id: notifiable.id, - notifiable_type: "Broadcast", - action: action, - ) - end - end + def send_new_follower_notification(follow) + json_data = { user: user_data(follow.follower) } + Notification.create( + user_id: follow.followable.id, + notifiable_id: follow.id, + notifiable_type: follow.class.name, + action: nil, + json_data: json_data, + ) + end + handle_asynchronously :send_new_follower_notification + + def send_to_followers(notifiable, followers, action = nil) + # followers is an array and not an activerecord object + followers.sort_by(&:updated_at).reverse[0..2500].each do |follower| + json_data = { + user: user_data(notifiable.user), + article: article_data(notifiable) + } + Notification.create( + user_id: follower.id, + notifiable_id: notifiable.id, + notifiable_type: notifiable.class.name, + action: action, + json_data: json_data, + ) end end - handle_asynchronously :send_all + handle_asynchronously :send_to_followers - def remove_all(notifiable, action) - Notification.where( + def send_new_comment_notifications(notifiable) + user_ids = notifiable.ancestors.map(&:user_id).to_set + user_ids.add(notifiable.commentable.user.id) if user_ids.empty? + user_ids.delete(notifiable.user_id).each do |user_id| + json_data = { + user: user_data(notifiable.user), + comment: comment_data(notifiable) + } + Notification.create( + user_id: user_id, + notifiable_id: notifiable.id, + notifiable_type: notifiable.class.name, + action: nil, + json_data: json_data, + ) + end + end + handle_asynchronously :send_new_comment_notifications + + def send_new_badge_notification(notifiable) + json_data = { + user: user_data(notifiable.user), + badge_achievement: { + badge_id: notifiable.badge_id, + rewarding_context_message: notifiable.rewarding_context_message, + badge: { + title: notifiable.badge.title, + description: notifiable.badge.description, + badge_image_url: notifiable.badge.badge_image_url + } + } + } + Notification.create( + user_id: notifiable.user.id, + notifiable_id: notifiable.id, + notifiable_type: "BadgeAchievement", + action: nil, + json_data: json_data, + ) + end + handle_asynchronously :send_new_badge_notification + + def send_reaction_notification(notifiable) + json_data = { + user: user_data(notifiable.user), + reaction: { + category: notifiable.category, + reactable_type: notifiable.reactable_type, + reactable_id: notifiable.reactable_id, + reactable: { + path: notifiable.reactable.path, + title: notifiable.reactable.title + }, + updated_at: notifiable.updated_at + } + } + Notification.create( + user_id: notifiable.reactable.user.id, + notifiable_id: notifiable.id, + notifiable_type: "Reaction", + action: notifiable.category, + json_data: json_data, + ) + end + handle_asynchronously :send_reaction_notification + + def send_mention_notification(notifiable) + mentioner = notifiable.mentionable.user + json_data = { + user: user_data(mentioner) + } + json_data[:comment] = comment_data(notifiable.mentionable) if notifiable.mentionable_type == "Comment" + Notification.create( + user_id: notifiable.user_id, + notifiable_id: notifiable.id, + notifiable_type: "Mention", + action: nil, + json_data: json_data, + ) + end + handle_asynchronously :send_mention_notification + + def send_welcome_notification(receiver_id) + welcome_broadcast = Broadcast.find_by(title: "Welcome Notification") + return if welcome_broadcast == nil + dev_account = User.find_by_id(ENV["DEVTO_USER_ID"]) + json_data = { + user: user_data(dev_account), + broadcast: { + processed_html: welcome_broadcast.processed_html + } + } + Notification.create( + user_id: receiver_id, + notifiable_id: welcome_broadcast.id, + notifiable_type: "Broadcast", + action: welcome_broadcast.type_of, + json_data: json_data, + ) + end + handle_asynchronously :send_welcome_notification + + def send_moderation_notification(notifiable) + available_moderators = User.with_role(:trusted).where("last_moderation_notification < ?", 28.hours.ago) + return if available_moderators.empty? + moderator = available_moderators.sample + dev_account = User.find_by_id(ENV["DEVTO_USER_ID"]) + json_data = { + user: user_data(dev_account) + } + json_data[notifiable.class.name.downcase] = send "#{notifiable.class.name.downcase}_data", notifiable + Notification.create( + user_id: moderator.id, notifiable_id: notifiable.id, notifiable_type: notifiable.class.name, - action: action, + action: "Moderation", + json_data: json_data, + ) + moderator.update_column(:last_moderation_notification, Time.current) + end + handle_asynchronously :send_moderation_notification + + def remove_all(notifiable_hash) + Notification.where( + notifiable_id: notifiable_hash[:id], + notifiable_type: notifiable_hash[:class_name], + action: notifiable_hash[:action], ).destroy_all end handle_asynchronously :remove_all - end - def activity_actor - if notifiable.class.name == "Broadcast" || action == "Moderation" - User.find(ApplicationConfig["DEVTO_USER_ID"]) - else - notifiable&.user + def remove_each(notifiable_collection, action = nil) + # only used for mentions since it's an array + notifiable_collection.each do |notifiable| + Notification.where( + notifiable_id: notifiable.id, + notifiable_type: notifiable.class.name, + action: action, + ).destroy_all + end + end + handle_asynchronously :remove_each + + def update_notifications(notifiable, action = nil) + notifications = Notification.where( + notifiable_id: notifiable.id, + notifiable_type: notifiable.class.name, + action: action, + ) + return if notifications.blank? + new_json_data = notifications.first.json_data + new_json_data[notifiable.class.name.downcase] = send("#{notifiable.class.name.downcase}_data", notifiable) + notifications.update_all(json_data: new_json_data) + end + handle_asynchronously :update_notifications + + private + + def user_data(user) + { + id: user.id, + class: { name: "User" }, + name: user.name, + username: user.username, + path: user.path, + profile_image_90: user.profile_image_90 + } + end + + def comment_data(comment) + { + id: comment.id, + class: { name: "Comment" }, + path: comment.path, + processed_html: comment.processed_html, + updated_at: comment.updated_at, + commentable: { + id: comment.commentable.id, + title: comment.commentable.title, + path: comment.commentable.path, + class: { + name: comment.commentable.class.name + } + } + } + end + + def article_data(article) + { + id: article.id, + cached_tag_list_array: article.decorate.cached_tag_list_array, + class: { name: "Article" }, + title: article.title, + path: article.path, + updated_at: article.updated_at + } end end - def activity_object - notifiable - end + # instance methods - def activity_verb - "#{notifiable_type}_#{action}" - end - - def activity_target - "#{notifiable_type.downcase}_#{Time.current.utc}" - end - - def activity_notify - [StreamNotifier.new(user_id).notify] - end - - def remove_from_feed - super - User.find_by(id: user_id)&.touch(:last_notification_activity) + def aggregation_format + if notifiable_type == "Reaction" + "#{created_at.beginning_of_day}-#{created_at.end_of_day}_#{json_data['reaction']['reactable_id']}_#{json_data['reaction']['reactable_type']}" + elsif notifiable_type == "Follow" + "#{created_at.beginning_of_day}-#{created_at.end_of_day}" + end end end diff --git a/app/models/user.rb b/app/models/user.rb index bae79df53..dc57821fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -378,7 +378,7 @@ class User < ApplicationRecord private def send_welcome_notification - Broadcast.send_welcome_notification(id) + Notification.send_welcome_notification(id) end def set_username diff --git a/app/services/article_creation_service.rb b/app/services/article_creation_service.rb index 7010c32f8..d1c3a6095 100644 --- a/app/services/article_creation_service.rb +++ b/app/services/article_creation_service.rb @@ -19,7 +19,7 @@ class ArticleCreationService create_job_opportunity(article) if article.save if article.published - Notification.send_all(article, "Published") + Notification.send_to_followers(article, user.followers, "Published") end end article.decorate diff --git a/db/migrate/20181026214021_add_columns_to_notifications.rb b/db/migrate/20181026214021_add_columns_to_notifications.rb new file mode 100644 index 000000000..12665e70f --- /dev/null +++ b/db/migrate/20181026214021_add_columns_to_notifications.rb @@ -0,0 +1,10 @@ +class AddColumnsToNotifications < ActiveRecord::Migration[5.1] + def change + add_index :notifications, :notifiable_id + add_index :notifications, :user_id + add_index :notifications, :notifiable_type + add_column :notifications, :json_data, :jsonb + add_index :notifications, :json_data, using: :gin + add_column :notifications, :read?, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9fe542718..82f5a3526 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -454,10 +456,16 @@ ActiveRecord::Schema.define(version: 20181116223239) do create_table "notifications", id: :serial, force: :cascade do |t| t.string "action" t.datetime "created_at", null: false + t.jsonb "json_data" t.integer "notifiable_id" t.string "notifiable_type" + t.boolean "read?", default: false t.datetime "updated_at", null: false t.integer "user_id" + t.index ["json_data"], name: "index_notifications_on_json_data", using: :gin + t.index ["notifiable_id"], name: "index_notifications_on_notifiable_id" + t.index ["notifiable_type"], name: "index_notifications_on_notifiable_type" + t.index ["user_id"], name: "index_notifications_on_user_id" end create_table "organizations", id: :serial, force: :cascade do |t| diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb index 165e9367c..fb2069ffe 100644 --- a/spec/factories/articles.rb +++ b/spec/factories/articles.rb @@ -34,4 +34,12 @@ FactoryBot.define do HEREDOC end end + + trait :video do + after(:build) do |article| + article.video = "https://video.com" + article.user.add_role :video_permission + article.save + end + end end diff --git a/spec/features/admin_sends_a_broadcast_spec.rb b/spec/features/admin_sends_a_broadcast_spec.rb deleted file mode 100644 index 1865b164c..000000000 --- a/spec/features/admin_sends_a_broadcast_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# rubocop:disable RSpec/InstanceVariable -require "rails_helper" - -describe "Send a broadcast" do - describe "Onboarding/welcome broadcast" do - describe "sent broadcast" do - before do - @broadcast = FactoryBot.create(:broadcast, :onboarding, :sent) - @new_user = FactoryBot.create(:user) - @welcome_notification = Broadcast.send_welcome_notification(@new_user.id) - end - - it "has a welcome notification" do - expect(@welcome_notification.notifiable).to eq @broadcast - end - - it "is properly sent to the new user" do - expect(@welcome_notification.user).to eq @new_user - end - - it "has a link to the welcome thread" do - expect(@broadcast.processed_html.html_safe).to include("/welcome") - end - end - - describe "unsent broadcast" do - before do - FactoryBot.create(:broadcast, :onboarding) - end - - it "is an unsent broadcast that doesn't create a notification" do - expect(Notification.all.empty?).to be true - end - end - end -end -# rubocop:enable RSpec/InstanceVariable diff --git a/spec/features/user_deletes_an_article_spec.rb b/spec/features/user_deletes_an_article_spec.rb index 240d134de..b1e0b8555 100644 --- a/spec/features/user_deletes_an_article_spec.rb +++ b/spec/features/user_deletes_an_article_spec.rb @@ -1,8 +1,8 @@ require "rails_helper" RSpec.describe "Deleting Article", js: true do - let(:author) { create(:user) } - let(:article) { create(:article, user_id: author.id) } + let(:author) { create(:user) } + let(:article) { create(:article, user_id: author.id) } def delete_article_via_dashboard visit "/dashboard" @@ -13,7 +13,7 @@ RSpec.describe "Deleting Article", js: true do end before do - article + Notification.send_to_followers(article, [author], "Published") end it "author of article deletes own article" do diff --git a/spec/features/user_edits_a_comment_spec.rb b/spec/features/user_edits_a_comment_spec.rb index 117f93cad..834993de1 100644 --- a/spec/features/user_edits_a_comment_spec.rb +++ b/spec/features/user_edits_a_comment_spec.rb @@ -6,7 +6,8 @@ RSpec.describe "Editing A Comment", type: :feature, js: true do let(:new_comment_text) { Faker::Lorem.paragraph } before do - create(:comment, commentable: article, user: user, body_markdown: Faker::Lorem.paragraph) + comment = create(:comment, commentable: article, user: user, body_markdown: Faker::Lorem.paragraph) + Notification.send_new_comment_notifications(comment) sign_in user end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index e48fdc501..9520c42ab 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -2,14 +2,19 @@ require "rails_helper" RSpec.describe Comment, type: :model do - let(:user) { create(:user) } - let(:article) { create(:article, user_id: user.id, published: true) } - let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) } - let(:comment_2) { create(:comment, user_id: user.id, commentable_id: article.id) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:article) { create(:article, user_id: user.id, published: true) } + let(:article_with_video) { create(:article, :video, user_id: user.id, published: true) } # :video is a trait, see articles.rb + let(:comment) { create(:comment, user_id: user2.id, commentable_id: article.id) } + let(:video_comment) { create(:comment, user_id: user2.id, commentable_id: article_with_video.id) } + let(:comment_2) { create(:comment, user_id: user2.id, commentable_id: article.id) } let(:child_comment) do build(:comment, user_id: user.id, commentable_id: article.id, parent_id: comment.id) end + before { Notification.send_new_comment_notifications(comment) } + it "gets proper generated ID code" do expect(comment.id_code_generated).to eq(comment.id.to_s(26)) end @@ -18,8 +23,9 @@ RSpec.describe Comment, type: :model do expect(comment.markdown_character_count).to eq(comment.body_markdown.size) end - context "when comment is already posted " do + context "when comment is already posted" do before do + Notification.send_new_comment_notifications(comment_2) comment_2.update(ancestry: comment.ancestry, body_markdown: comment.body_markdown, commentable_type: comment.commentable_type, @@ -49,25 +55,23 @@ RSpec.describe Comment, type: :model do end it "adds timestamp url if commentable has video and timestamp" do - article.video = "https://video.com" - article.user.add_role(:video_permission) - article.save! - comment.body_markdown = "I like the part at 4:30" - comment.save - expect(comment.processed_html.include?(">4:30")).to eq(true) - comment.body_markdown = "I like the part at 4:30 and 5:50" - comment.save - expect(comment.processed_html.include?(">5:50")).to eq(true) - comment.body_markdown = "I like the part at 5:30 and :55" - comment.save - expect(comment.processed_html.include?(">:55")).to eq(true) - comment.body_markdown = "I like the part at 52:30" - comment.save - expect(comment.processed_html.include?(">52:30")).to eq(true) - comment.body_markdown = "I like the part at 1:52:30 and 1:20" - comment.save - expect(comment.processed_html.include?(">1:52:30")).to eq(true) - expect(comment.processed_html.include?(">1:20")).to eq(true) + Notification.send_new_comment_notifications(video_comment) + video_comment.body_markdown = "I like the part at 4:30" + video_comment.save + expect(video_comment.processed_html.include?(">4:30")).to eq(true) + video_comment.body_markdown = "I like the part at 4:30 and 5:50" + video_comment.save + expect(video_comment.processed_html.include?(">5:50")).to eq(true) + video_comment.body_markdown = "I like the part at 5:30 and :55" + video_comment.save + expect(video_comment.processed_html.include?(">:55")).to eq(true) + video_comment.body_markdown = "I like the part at 52:30" + video_comment.save + expect(video_comment.processed_html.include?(">52:30")).to eq(true) + video_comment.body_markdown = "I like the part at 1:52:30 and 1:20" + video_comment.save + expect(video_comment.processed_html.include?(">1:52:30")).to eq(true) + expect(video_comment.processed_html.include?(">1:20")).to eq(true) end it "does not add timestamp if commentable does not have video" do @@ -163,7 +167,7 @@ RSpec.describe Comment, type: :model do end it "returns the root parent comment's user if root parent comment exists" do - expect(child_comment.parent_user).to eq(user) + expect(child_comment.parent_user).to eq(user2) end end diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb index e46ee4a56..c8b0a53cc 100644 --- a/spec/models/mention_spec.rb +++ b/spec/models/mention_spec.rb @@ -14,6 +14,8 @@ RSpec.describe Mention, type: :model do ) end + before { Notification.send_new_comment_notifications(comment) } + it "creates mention if there is a user mentioned" do comment.body_markdown = "Hello @#{user.username}, you are cool." comment.save diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index f69c21e22..dff35c9d0 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -1,35 +1,30 @@ require "rails_helper" RSpec.describe Notification, type: :model do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:article) { create(:article, user_id: user.id) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:article) { create(:article, user_id: user.id) } + let(:follow_instance) { user.follow(user2) } - it "sends notifications to all followers of article user" do - user2.follow(user) - user3.follow(user) - Notification.send_all_without_delay(article, "Published") - expect(Notification.all.size).to eq(2) + describe "#send_new_follower_notification" do + before { Notification.send_new_follower_notification(follow_instance) } + + it "creates a notification belonging to the person being followed" do + expect(Notification.first.user_id).to eq user2.id + end + + it "creates a notification from the follow instance" do + notifiable_data = { notifiable_id: Notification.first.notifiable_id, notifiable_type: Notification.first.notifiable_type } + follow_data = { notifiable_id: follow_instance.id, notifiable_type: follow_instance.class.name } + expect(notifiable_data).to eq follow_data + end end - # rubocop:disable RSpec/ExampleLength - it "sends sends a broadcast to everyone" do - user2.follow(user) - user3.follow(user) - broadcast = Broadcast.create!( - processed_html: "Hello", title: "test broadcast", type_of: "Announcement", - ) - Notification.send_all_without_delay(broadcast, "Announcement") - expect(Notification.all.size).to eq(3) - end - # rubocop:enable RSpec/ExampleLength - - it "removes all notifications" do - user2.follow(user) - user3.follow(user) - Notification.send_all_without_delay(article, "Published") - Notification.remove_all_without_delay(article, "Published") - expect(Notification.all.size).to eq(0) - end + # describe "#send_to_followers" do + # before do + # user2.follow user + # Notification.send_to_followers(article, user.followers, "Published") + # end + # end end diff --git a/spec/requests/comments_update_spec.rb b/spec/requests/comments_update_spec.rb index f5e71d1e8..fe7b4b8e3 100644 --- a/spec/requests/comments_update_spec.rb +++ b/spec/requests/comments_update_spec.rb @@ -7,6 +7,7 @@ RSpec.describe "CommentsUpdate", type: :request do before do sign_in user + Notification.send_new_comment_notifications(comment) end it "updates ordinary article with proper params" do diff --git a/spec/requests/notifications_spec.rb b/spec/requests/notifications_spec.rb index 5c0db400a..cf2c13e89 100644 --- a/spec/requests/notifications_spec.rb +++ b/spec/requests/notifications_spec.rb @@ -1,21 +1,325 @@ require "rails_helper" -# vcr_option = { -# cassette_name: "getstream-index", -# allow_playback_repeats: "true", -# } - RSpec.describe "NotificationsIndex", type: :request do let(:user) { create(:user) } - describe "GET logged-in notifications index" do - before do - sign_in user - end - - it "renders page with proper sidebar" do + describe "GET notifications" do + xit "renders page with the proper heading" do get "/notifications" expect(response.body).to include("Notifications") end + + context "when signed out" do + xit "renders the signup cue" do + get "/notifications" + expect(response.body).to include "