Skip to content

Commit

Permalink
Merge pull request #1238 from Retrospring/fix/counter-jank
Browse files Browse the repository at this point in the history
  • Loading branch information
raccube authored Oct 2, 2023
2 parents 99b336c + d39f370 commit fa74a29
Show file tree
Hide file tree
Showing 20 changed files with 146 additions and 46 deletions.
3 changes: 2 additions & 1 deletion app/controllers/answer_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ def unpin
private

def mark_notifications_as_read
Notification.where(recipient_id: current_user.id, new: true)
updated = Notification.where(recipient_id: current_user.id, new: true)
.and(Notification.where(type: "Notification::QuestionAnswered", target_id: @answer.id)
.or(Notification.where(type: "Notification::Commented", target_id: @answer.comments.pluck(:id)))
.or(Notification.where(type: "Notification::Smiled", target_id: @answer.smiles.pluck(:id)))
.or(Notification.where(type: "Notification::CommentSmiled", target_id: @answer.comment_smiles.pluck(:id))))
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
current_user.touch(:notifications_updated_at) if updated.positive?
end
end
17 changes: 6 additions & 11 deletions app/controllers/inbox_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
class InboxController < ApplicationController
before_action :authenticate_user!

after_action :mark_inbox_entries_as_read, only: %i[show]

def show # rubocop:disable Metrics/MethodLength
find_author
find_inbox_entries
Expand All @@ -19,14 +17,11 @@ def show # rubocop:disable Metrics/MethodLength
@delete_id = find_delete_id
@disabled = true if @inbox.empty?

respond_to do |format|
format.html { render "show" }
format.turbo_stream do
render "show", layout: false, status: :see_other
mark_inbox_entries_as_read

# rubocop disabled as just flipping a flag doesn't need to have validations to be run
@inbox.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
end
respond_to do |format|
format.html
format.turbo_stream
end
end

Expand Down Expand Up @@ -85,8 +80,8 @@ def filter_author_chain(query)
# rubocop:disable Rails/SkipsModelValidations
def mark_inbox_entries_as_read
# using .dup to not modify @inbox -- useful in tests
@inbox&.dup&.update_all(new: false)
current_user.touch(:inbox_updated_at)
updated = @inbox&.dup&.update_all(new: false)
current_user.touch(:inbox_updated_at) if updated.positive?
end
# rubocop:enable Rails/SkipsModelValidations

Expand Down
7 changes: 3 additions & 4 deletions app/controllers/notifications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!

after_action :mark_notifications_as_read, only: %i[index]

TYPE_MAPPINGS = {
"answer" => Notification::QuestionAnswered.name,
"comment" => Notification::Commented.name,
Expand All @@ -18,6 +16,7 @@ def index
@notifications = cursored_notifications_for(type: @type, last_id: params[:last_id])
paginate_notifications
@counters = count_unread_by_type
mark_notifications_as_read

respond_to do |format|
format.html
Expand Down Expand Up @@ -52,8 +51,8 @@ def count_unread_by_type
# rubocop:disable Rails/SkipsModelValidations
def mark_notifications_as_read
# using .dup to not modify @notifications -- useful in tests
@notifications&.dup&.update_all(new: false)
current_user.touch(:notifications_updated_at)
updated = @notifications&.dup&.update_all(new: false)
current_user.touch(:notifications_updated_at) if updated.positive?
end
# rubocop:enable Rails/SkipsModelValidations

Expand Down
9 changes: 6 additions & 3 deletions app/controllers/settings/export_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ def create

private

# rubocop:disable Rails/SkipsModelValidations
def mark_notifications_as_read
Notification::DataExported
.where(recipient: current_user, new: true)
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
updated = Notification::DataExported
.where(recipient: current_user, new: true)
.update_all(new: false)
current_user.touch(:notifications_updated_at) if updated.positive?
end
# rubocop:enable Rails/SkipsModelValidations
end
3 changes: 2 additions & 1 deletion app/helpers/bootstrap_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def nav_entry(body, path, options = {})
badge_attr: {},
icon: nil,
class: "",
id: nil,
hotkey: nil,
}.merge(options)

Expand All @@ -34,7 +35,7 @@ def nav_entry(body, path, options = {})
body += " #{content_tag(:span, options[:badge], class: badge_class, **options[:badge_attr])}".html_safe
end

content_tag(:li, link_to(body.html_safe, path, class: "nav-link", data: { hotkey: options[:hotkey] }), class: classes)
content_tag(:li, link_to(body.html_safe, path, class: "nav-link", data: { hotkey: options[:hotkey] }), class: classes, id: options[:id])
end

def list_group_item(body, path, options = {})
Expand Down
1 change: 1 addition & 0 deletions app/models/answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Answer < ApplicationRecord

after_create do
Inbox.where(user: self.user, question: self.question).destroy_all
user.touch :inbox_updated_at # rubocop:disable Rails/SkipsModelValidations

Notification.notify self.question.user, self unless self.question.user == self.user or self.question.user.nil?
Subscription.subscribe self.user, self
Expand Down
12 changes: 12 additions & 0 deletions app/models/inbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ class Inbox < ApplicationRecord
!user.privacy_allow_anonymous_questions?
end

after_create do
user.touch(:inbox_updated_at)
end

after_update do
user.touch(:inbox_updated_at)
end

after_destroy do
user.touch(:inbox_updated_at)
end

def answer(answer_content, user)
raise Errors::AnsweringOtherBlockedSelf if question.user&.blocking?(user)
raise Errors::AnsweringSelfBlockedOther if user.blocking?(question.user)
Expand Down
16 changes: 14 additions & 2 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# frozen_string_literal: true

class Notification < ApplicationRecord
belongs_to :recipient, class_name: "User", touch: :notifications_updated_at
belongs_to :recipient, class_name: "User"
belongs_to :target, polymorphic: true

after_create do
recipient.touch(:notifications_updated_at)
end

after_update do
recipient.touch(:notifications_updated_at)
end

after_destroy do
recipient.touch(:notifications_updated_at)
end

class << self
include CursorPaginatable

Expand Down Expand Up @@ -45,7 +57,7 @@ def make_notification(recipient, target, notification_type)

n = notification_type.new(target:,
recipient:,
new: true)
new: true,)
n.save!
n
end
Expand Down
32 changes: 24 additions & 8 deletions app/models/subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,26 @@ def destruct(target)
def notify(source, target)
return nil if source.nil? || target.nil?

muted_by = Relationships::Mute.where(target: source.user).pluck(&:source_id)

# As we will need to notify for each person subscribed,
# it's much faster to bulk insert than to use +Notification.notify+
notifications = Subscription.where(answer: target)
.where.not(user: source.user)
.where.not(user_id: muted_by)
.map do |s|
{ target_id: source.id, target_type: Comment, recipient_id: s.user_id, new: true, type: Notification::Commented, created_at: source.created_at, updated_at: source.created_at }
notifications = Subscription.for(source, target).pluck(:user_id).map do |recipient_id|
{
target_id: source.id,
target_type: Comment,
recipient_id:,
new: true,
type: Notification::Commented,
created_at: source.created_at,
updated_at: source.created_at,
}
end

Notification.insert_all!(notifications) unless notifications.empty? # rubocop:disable Rails/SkipsModelValidations
return if notifications.empty?

# rubocop:disable Rails/SkipsModelValidations
Notification.insert_all!(notifications)
User.where(id: notifications.pluck(:recipient_id)).touch_all(:notifications_updated_at)
# rubocop:enable Rails/SkipsModelValidations
end

def denotify(source, target)
Expand All @@ -48,5 +56,13 @@ def denotify(source, target)
subs = Subscription.where(answer: target)
Notification.where(target:, recipient: subs.map(&:user)).delete_all
end

def for(source, target)
muted_by = Relationships::Mute.where(target: source.user).pluck(&:source_id)

Subscription.where(answer: target)
.where.not(user: source.user)
.where.not(user_id: muted_by)
end
end
end
12 changes: 12 additions & 0 deletions app/views/inbox/show.turbo_stream.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- inbox_count = current_user.unread_inbox_count

= turbo_stream.append "entries" do
- @inbox.each do |i|
= render "inbox/entry", i:
Expand All @@ -10,3 +12,13 @@
params: { last_id: @inbox_last_id, author: @author }.compact,
data: { controller: :hotkey, hotkey: "." },
form: { data: { turbo_stream: true } }

= turbo_stream.update "nav-inbox-desktop" do
= nav_entry t("navigation.inbox"), "/inbox",
badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } },
icon: "inbox", hotkey: "g i"

= turbo_stream.update "nav-inbox-mobile" do
= nav_entry t("navigation.inbox"), "/inbox",
badge: inbox_count, badge_color: "primary", badge_pill: true,
icon: "inbox", icon_only: true
9 changes: 2 additions & 7 deletions app/views/navigation/_desktop.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
DEV
%ul.nav.navbar-nav.me-auto
= nav_entry t("navigation.timeline"), root_path, icon: "home", hotkey: "g t"
= nav_entry t("navigation.inbox"), "/inbox", icon: "inbox", badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } }, hotkey: "g i"
= nav_entry t("navigation.inbox"), "/inbox", icon: "inbox", badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } }, hotkey: "g i", id: "nav-inbox-desktop"

Check warning on line 13 in app/views/navigation/_desktop.html.haml

View workflow job for this annotation

GitHub Actions / haml-lint

[haml-lint] app/views/navigation/_desktop.html.haml#L13

LineLength: Line is too long. [175/160]
Raw output
app/views/navigation/_desktop.html.haml:13 [W] LineLength: Line is too long. [175/160]
- if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
= nav_entry t("navigation.discover"), discover_path, icon: "compass", hotkey: "g d"
%ul.nav.navbar-nav
Expand All @@ -23,12 +23,7 @@
%li.nav-item.dropdown.d-none.d-sm-block
%a.nav-link.dropdown-toggle{ href: '#', data: { bs_toggle: :dropdown } }
%turbo-frame#notification-desktop-icon
- if notification_count.nil?
%i.fa.fa-bell-o
- else
%i.fa.fa-bell
%span.visually-hidden= t("navigation.notifications")
%span.badge= notification_count
= render "navigation/icons/notifications", notification_count:
.dropdown-menu.dropdown-menu-end.notification-dropdown
%turbo-frame#notifications-dropdown-list
- cache current_user.notification_dropdown_cache_key do
Expand Down
2 changes: 1 addition & 1 deletion app/views/navigation/_mobile.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
= nav_entry t("navigation.timeline"), root_path, icon: 'home', icon_only: true
= nav_entry t("navigation.inbox"), '/inbox',
badge: inbox_count, badge_color: 'primary', badge_pill: true,
icon: 'inbox', icon_only: true
icon: 'inbox', icon_only: true, id: "nav-inbox-mobile"
- if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod?
= nav_entry t("navigation.discover"), discover_path, icon: 'compass', icon_only: true
= nav_entry t("navigation.notifications"), notifications_path("all"), icon: notifications_icon,
Expand Down
6 changes: 6 additions & 0 deletions app/views/navigation/icons/_notifications.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- if notification_count.nil?
%i.fa.fa-bell-o
- else
%i.fa.fa-bell
%span.visually-hidden= t("navigation.notifications")
%span.badge= notification_count
3 changes: 3 additions & 0 deletions app/views/notifications/index.turbo_stream.haml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
params: { last_id: @notifications_last_id },
data: { controller: :hotkey, hotkey: "." },
form: { data: { turbo_stream: true } }

= turbo_stream.update "notification-desktop-icon" do
= render "navigation/icons/notifications", notification_count: current_user.unread_notification_count
13 changes: 13 additions & 0 deletions spec/controllers/ajax/answer_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require "rails_helper"

describe Ajax::AnswerController, :ajax_controller, type: :controller do
include ActiveSupport::Testing::TimeHelpers

let(:question) { FactoryBot.create(:question, user: FactoryBot.build(:user, privacy_allow_stranger_answers: asker_allows_strangers)) }
let(:asker_allows_strangers) { true }

Expand All @@ -26,6 +28,8 @@
end

include_examples "returns the expected response"

include_examples "touches user timestamp", :inbox_updated_at
end

shared_examples "does not create the answer" do
Expand Down Expand Up @@ -325,6 +329,15 @@
user.save
expect { subject }.to(change { Inbox.where(question_id: answer.question.id, user_id: user.id).count }.by(1))
end

it "updates the inbox caching timestamp for the user who answered" do
initial_timestamp = 1.day.ago
answer.user.update(inbox_updated_at: initial_timestamp)
travel_to 1.day.from_now do
# using string representation to avoid precision issues
expect { subject }.to(change { answer.user.reload.inbox_updated_at.to_s }.from(initial_timestamp.to_s).to(DateTime.now))
end
end
end

context "when the answer exists and was not made by the current user" do
Expand Down
14 changes: 14 additions & 0 deletions spec/controllers/ajax/comment_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require "rails_helper"

describe Ajax::CommentController, :ajax_controller, type: :controller do
include ActiveSupport::Testing::TimeHelpers

let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) }

describe "#create" do
Expand All @@ -23,6 +25,18 @@
expect(answer.reload.comments.ids).to include(Comment.last.id)
end

context "a user is subscribed to the answer" do
let(:subscribed_user) { FactoryBot.create(:user) }

it "updates the notification caching timestamp for a subscribed user" do
Subscription.subscribe(subscribed_user, answer)

travel_to(1.day.from_now) do
expect { subject }.to change { subscribed_user.reload.notifications_updated_at }.to(DateTime.now)
end
end
end

include_examples "returns the expected response"
end

Expand Down
9 changes: 3 additions & 6 deletions spec/controllers/inbox_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@
expect { subject }.to change { inbox_entry.reload.new? }.from(true).to(false)
end

it "updates the the timestamp used for caching" do
user.update(inbox_updated_at: original_inbox_updated_at)
travel 1.second do
expect { subject }.to change { user.reload.inbox_updated_at.floor }.from(original_inbox_updated_at.floor).to(Time.now.utc.floor)
end
end
include_examples "touches user timestamp", :inbox_updated_at

context "when requested the turbo stream format" do
subject { get :show, format: :turbo_stream }
Expand Down Expand Up @@ -280,6 +275,8 @@
it "creates an inbox entry" do
expect { subject }.to(change { user.inboxes.count }.by(1))
end

include_examples "touches user timestamp", :inbox_updated_at
end
end
end
6 changes: 4 additions & 2 deletions spec/controllers/settings/export_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
end

context "when user has a new DataExported notification" do
let(:notification) do
let!(:notification) do
Notification::DataExported.create(
target_id: user.id,
target_type: "User::DataExport",
recipient: user,
new: true
new: true,
)
end

it "marks the notification as read" do
expect { subject }.to change { notification.reload.new }.from(true).to(false)
end

include_examples "touches user timestamp", :notifications_updated_at
end
end
end
Expand Down
Loading

0 comments on commit fa74a29

Please sign in to comment.