FEATURE: Silence Close Notifications User Setting (#26072)

This change creates a user setting that they can toggle if
they don't want to receive unread notifications when someone closes a
topic they have read and are watching/tracking it.
This commit is contained in:
Blake Erickson 2024-03-08 15:14:46 -07:00 committed by GitHub
parent 3f1566eeb1
commit f71e9aad60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 106 additions and 9 deletions

View File

@ -167,6 +167,7 @@ export default class extends Controller {
"tracked_category_ids", "tracked_category_ids",
"watched_first_post_category_ids", "watched_first_post_category_ids",
"watched_precedence_over_muted", "watched_precedence_over_muted",
"topics_unread_when_closed",
]; ];
if (this.siteSettings.tagging_enabled) { if (this.siteSettings.tagging_enabled) {

View File

@ -130,6 +130,7 @@ let userOptionFields = [
"sidebar_link_to_filtered_list", "sidebar_link_to_filtered_list",
"sidebar_show_count_of_new_items", "sidebar_show_count_of_new_items",
"watched_precedence_over_muted", "watched_precedence_over_muted",
"topics_unread_when_closed",
]; ];
export function addSaveableUserOptionField(fieldName) { export function addSaveableUserOptionField(fieldName) {

View File

@ -49,6 +49,11 @@
}} }}
/> />
</div> </div>
<PreferenceCheckbox
@labelKey="user.topics_unread_when_closed"
@checked={{this.model.user_option.topics_unread_when_closed}}
/>
</div> </div>
</div> </div>

View File

@ -7,26 +7,33 @@ class PostTiming < ActiveRecord::Base
validates_presence_of :post_number validates_presence_of :post_number
validates_presence_of :msecs validates_presence_of :msecs
def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number) def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number, user_ids = nil)
# This is done in SQL cause the logic is quite tricky and we want to do this in one db hit # This is done in SQL cause the logic is quite tricky and we want to do this in one db hit
# #
DB.exec( user_ids_condition = user_ids.present? ? "AND user_id = ANY(ARRAY[:user_ids]::int[])" : ""
"INSERT INTO post_timings(topic_id, user_id, post_number, msecs) sql_query = <<-SQL
INSERT INTO post_timings(topic_id, user_id, post_number, msecs)
SELECT :topic_id, user_id, :pretend_read_post_number, 1 SELECT :topic_id, user_id, :pretend_read_post_number, 1
FROM post_timings pt FROM post_timings pt
WHERE topic_id = :topic_id AND WHERE topic_id = :topic_id AND
post_number = :actual_read_post_number AND post_number = :actual_read_post_number
NOT EXISTS ( #{user_ids_condition}
AND NOT EXISTS (
SELECT 1 FROM post_timings pt1 SELECT 1 FROM post_timings pt1
WHERE pt1.topic_id = pt.topic_id AND WHERE pt1.topic_id = pt.topic_id AND
pt1.post_number = :pretend_read_post_number AND pt1.post_number = :pretend_read_post_number AND
pt1.user_id = pt.user_id pt1.user_id = pt.user_id
) )
", SQL
params = {
pretend_read_post_number: pretend_read_post_number, pretend_read_post_number: pretend_read_post_number,
topic_id: topic_id, topic_id: topic_id,
actual_read_post_number: actual_read_post_number, actual_read_post_number: actual_read_post_number,
) }
params[:user_ids] = user_ids if user_ids.present?
DB.exec(sql_query, params)
TopicUser.ensure_consistency!(topic_id) TopicUser.ensure_consistency!(topic_id)
end end

View File

@ -292,6 +292,7 @@ end
# sidebar_show_count_of_new_items :boolean default(FALSE), not null # sidebar_show_count_of_new_items :boolean default(FALSE), not null
# watched_precedence_over_muted :boolean # watched_precedence_over_muted :boolean
# chat_separate_sidebar_mode :integer default(0), not null # chat_separate_sidebar_mode :integer default(0), not null
# topics_unread_when_closed :boolean default(TRUE), not null
# #
# Indexes # Indexes
# #

View File

@ -39,7 +39,8 @@ class UserOptionSerializer < ApplicationSerializer
:seen_popups, :seen_popups,
:sidebar_link_to_filtered_list, :sidebar_link_to_filtered_list,
:sidebar_show_count_of_new_items, :sidebar_show_count_of_new_items,
:watched_precedence_over_muted :watched_precedence_over_muted,
:topics_unread_when_closed
def auto_track_topics_after_msecs def auto_track_topics_after_msecs
object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs

View File

@ -90,6 +90,21 @@ TopicStatusUpdater =
# actually read the topic # actually read the topic
PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number) PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number)
end end
if status.closed? && status.enabled?
sql_query = <<-SQL
SELECT DISTINCT post_timings.user_id
FROM post_timings
JOIN user_options ON user_options.user_id = post_timings.user_id
WHERE post_timings.topic_id = :topic_id
AND user_options.topics_unread_when_closed = 'f'
SQL
user_ids = DB.query_single(sql_query, topic_id: topic.id)
if user_ids.present?
PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number, user_ids)
end
end
end end
def message_for(status) def message_for(status)

View File

@ -52,6 +52,7 @@ class UserUpdater
sidebar_link_to_filtered_list sidebar_link_to_filtered_list
sidebar_show_count_of_new_items sidebar_show_count_of_new_items
watched_precedence_over_muted watched_precedence_over_muted
topics_unread_when_closed
] ]
NOTIFICATION_SCHEDULE_ATTRS = -> do NOTIFICATION_SCHEDULE_ATTRS = -> do

View File

@ -1774,6 +1774,7 @@ en:
after_10_minutes: "after 10 minutes" after_10_minutes: "after 10 minutes"
notification_level_when_replying: "When I post in a topic, set that topic to" notification_level_when_replying: "When I post in a topic, set that topic to"
topics_unread_when_closed: "Consider topics unread when they are closed"
invited: invited:
title: "Invites" title: "Invites"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddTopicsUnreadWhenClosedToUserOptions < ActiveRecord::Migration[7.0]
def change
add_column :user_options, :topics_unread_when_closed, :boolean, default: true, null: false
end
end

View File

@ -794,6 +794,9 @@
}, },
"seen_popups": { "seen_popups": {
"type": ["array", "null"] "type": ["array", "null"]
},
"topics_unread_when_closed": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@ -828,7 +831,8 @@
"text_size_seq", "text_size_seq",
"title_count_mode", "title_count_mode",
"timezone", "timezone",
"skip_new_user_tips" "skip_new_user_tips",
"topics_unread_when_closed"
] ]
} }
}, },

View File

@ -214,11 +214,20 @@ module PageObjects
find(".topic-notifications-button .select-kit-header").click find(".topic-notifications-button .select-kit-header").click
end end
def click_admin_menu_button
find("#topic-footer-buttons .topic-admin-menu-button").click
end
def watch_topic def watch_topic
click_notifications_button click_notifications_button
find('li[data-name="watching"]').click find('li[data-name="watching"]').click
end end
def close_topic
click_admin_menu_button
find(".topic-admin-popup-menu ul.topic-admin-menu-topic li.topic-admin-close").click
end
def has_read_post?(post) def has_read_post?(post)
post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3) post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3)
end end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
describe "Topics unread when closed", type: :system do
fab!(:topics) { Fabricate.times(10, :post).map(&:topic) }
let(:topic_list) { PageObjects::Components::TopicList.new }
let(:topic_page) { PageObjects::Pages::Topic.new }
context "when closing a topic" do
fab!(:admin)
fab!(:user)
it "close notifications do not appear when disabled" do
user.user_option.update!(topics_unread_when_closed: false)
sign_in(user)
topic = topics.third
topic_page.visit_topic(topic)
topic_page.watch_topic
expect(topic_page).to have_read_post(1)
# Close the topic as an admin
TopicStatusUpdater.new(topic, admin).update!("closed", true)
# Check that the user did not receive a new post notification badge
visit("/latest")
expect(topic_list).to have_no_unread_badge(topics.third)
end
it "close notifications appear when enabled (the default)" do
user.user_option.update!(topics_unread_when_closed: true)
sign_in(user)
topic = topics.third
topic_page.visit_topic(topic)
topic_page.watch_topic
expect(topic_page).to have_read_post(1)
# Close the topic as an admin
TopicStatusUpdater.new(topic, admin).update!("closed", true)
# Check that the user did receive a new post notification badge
visit("/latest")
expect(topic_list).to have_unread_badge(topics.third)
end
end
end