FEATURE: Dismiss new per category (#8330)

Ability to dismiss new topics per category.
This commit is contained in:
Krzysztof Kotlarek 2019-11-14 11:16:13 +11:00 committed by GitHub
parent d095c2cee7
commit 6e1fe22a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 135 additions and 34 deletions

View File

@ -84,7 +84,7 @@ const controllerOpts = {
resetNew() { resetNew() {
this.topicTrackingState.resetNew(); this.topicTrackingState.resetNew();
Topic.resetNew().then(() => this.send("refresh")); Topic.resetNew(this.category, !this.noSubcategories).then(() => this.send("refresh"));
}, },
dismissReadPosts() { dismissReadPosts() {
@ -106,7 +106,7 @@ const controllerOpts = {
@discourseComputed("model.filter", "model.topics.length") @discourseComputed("model.filter", "model.topics.length")
showResetNew(filter, topicsLength) { showResetNew(filter, topicsLength) {
return filter === "new" && topicsLength > 0; return this.isFilterPage(filter, "new") && topicsLength > 0;
}, },
@discourseComputed("model.filter", "model.topics.length") @discourseComputed("model.filter", "model.topics.length")

View File

@ -765,8 +765,9 @@ Topic.reopenClass({
}); });
}, },
resetNew() { resetNew(category, include_subcategories) {
return ajax("/topics/reset-new", { type: "PUT" }); const data = category ? { category_id: category.id, include_subcategories } : {};
return ajax("/topics/reset-new", { type: "PUT", data });
}, },
idForSlug(slug) { idForSlug(slug) {

View File

@ -847,7 +847,23 @@ class TopicsController < ApplicationController
end end
def reset_new def reset_new
if params[:category_id].present?
category_ids = [params[:category_id]]
if params[:include_subcategories] == 'true'
category_ids = category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id))
end
category_ids.each do |category_id|
current_user
.category_users
.where(category_id: category_id)
.first_or_initialize
.update!(last_seen_at: Time.zone.now)
end
else
current_user.user_stat.update_column(:new_since, Time.now) current_user.user_stat.update_column(:new_since, Time.now)
end
render body: nil render body: nil
end end

View File

@ -217,6 +217,16 @@ class CategoryUser < ActiveRecord::Base
Hash[*notification_levels.flatten] Hash[*notification_levels.flatten]
end end
def self.lookup_for(user, category_ids)
return {} if user.blank? || category_ids.blank?
create_lookup(CategoryUser.where(category_id: category_ids, user_id: user.id))
end
def self.create_lookup(category_users)
category_users.each_with_object({}) do |category_user, acc|
acc[category_user.category_id] = category_user
end
end
end end
# == Schema Information # == Schema Information

View File

@ -135,6 +135,7 @@ class Topic < ActiveRecord::Base
# When we want to temporarily attach some data to a forum topic (usually before serialization) # When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data attr_accessor :user_data
attr_accessor :category_user_data
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
attr_accessor :participants attr_accessor :participants

View File

@ -83,6 +83,7 @@ class TopicList
# Attach some data for serialization to each topic # Attach some data for serialization to each topic
@topic_lookup = TopicUser.lookup_for(@current_user, @topics) if @current_user @topic_lookup = TopicUser.lookup_for(@current_user, @topics) if @current_user
@category_user_lookup = CategoryUser.lookup_for(@current_user, @topics.map(&:category_id).uniq) if @current_user
post_action_type = post_action_type =
if @current_user if @current_user
@ -114,6 +115,7 @@ class TopicList
@topics.each do |ft| @topics.each do |ft|
ft.user_data = @topic_lookup[ft.id] if @topic_lookup.present? ft.user_data = @topic_lookup[ft.id] if @topic_lookup.present?
ft.category_user_data = @category_user_lookup[ft.category_id] if @category_user_lookup.present?
if ft.user_data && post_action_lookup && actions = post_action_lookup[ft.id] if ft.user_data && post_action_lookup && actions = post_action_lookup[ft.id]
ft.user_data.post_action_data = { post_action_type => actions } ft.user_data.post_action_data = { post_action_type => actions }

View File

@ -59,6 +59,7 @@ class ListableTopicSerializer < BasicTopicSerializer
def seen def seen
return true if !scope || !scope.user return true if !scope || !scope.user
return true if object.user_data && !object.user_data.last_read_post_number.nil? return true if object.user_data && !object.user_data.last_read_post_number.nil?
return true if object.category_user_data&.last_seen_at && object.created_at < object.category_user_data.last_seen_at
return true if object.created_at < scope.user.user_option.treat_as_new_topic_start_date return true if object.created_at < scope.user.user_option.treat_as_new_topic_start_date
false false
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLastSeenToCategoryUsers < ActiveRecord::Migration[6.0]
def change
add_column :category_users, :last_seen_at, :datetime
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexToLastSeenAtOnCategoryUsers < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
if !index_exists?(:category_users, [:user_id, :last_seen_at])
add_index :category_users, [:user_id, :last_seen_at], algorithm: :concurrently
end
end
def down
remove_index :category_users, [:user_id, :last_seen_at]
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class ChangeNotificationLevel < ActiveRecord::Migration[6.0]
def up
change_column :category_users, :notification_level, :integer, null: true
end
def down
change_column :category_users, :notification_level, :integer, null: false
end
end

View File

@ -517,6 +517,7 @@ class TopicQuery
result = remove_muted_topics(result, @user) result = remove_muted_topics(result, @user)
result = remove_muted_categories(result, @user, exclude: options[:category]) result = remove_muted_categories(result, @user, exclude: options[:category])
result = remove_muted_tags(result, @user, options) result = remove_muted_tags(result, @user, options)
result = remove_already_seen_for_category(result, @user)
self.class.results_filter_callbacks.each do |filter_callback| self.class.results_filter_callbacks.each do |filter_callback|
result = filter_callback.call(:new, result, @user, options) result = filter_callback.call(:new, result, @user, options)
@ -871,18 +872,12 @@ class TopicQuery
if SiteSetting.mute_all_categories_by_default if SiteSetting.mute_all_categories_by_default
if user if user
list = list.references("cu") list = list
.where(" .references("cu")
NOT EXISTS ( .joins("LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}")
SELECT 1 .where("topics.category_id = :category_id
FROM categories c OR COALESCE(category_users.notification_level, :muted) <> :muted
LEFT OUTER JOIN category_users cu OR tu.notification_level > :regular",
ON c.id = cu.category_id AND cu.user_id = :user_id
WHERE c.id = topics.category_id
AND c.id <> :category_id
AND (COALESCE(cu.notification_level, :muted) = :muted)
AND (COALESCE(tu.notification_level, :regular) <= :regular)
)", user_id: user.id,
muted: CategoryUser.notification_levels[:muted], muted: CategoryUser.notification_levels[:muted],
regular: TopicUser.notification_levels[:regular], regular: TopicUser.notification_levels[:regular],
category_id: category_id || -1) category_id: category_id || -1)
@ -897,18 +892,13 @@ class TopicQuery
list = list.where("topics.category_id IN (?)", category_ids) if category_ids.present? list = list.where("topics.category_id IN (?)", category_ids) if category_ids.present?
end end
elsif user elsif user
list = list.references("cu") list = list
.where(" .references("cu")
NOT EXISTS ( .joins("LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}")
SELECT 1 .where("COALESCE(category_users.notification_level, :regular) <> :muted
FROM category_users cu OR category_users.category_id = :category_id OR tu.notification_level >= :tracking",
WHERE cu.user_id = :user_id
AND cu.category_id = topics.category_id
AND cu.notification_level = :muted
AND cu.category_id <> :category_id
AND (tu.notification_level IS NULL OR tu.notification_level < :tracking)
)", user_id: user.id,
muted: CategoryUser.notification_levels[:muted], muted: CategoryUser.notification_levels[:muted],
regular: CategoryUser.notification_levels[:regular],
tracking: TopicUser.notification_levels[:tracking], tracking: TopicUser.notification_levels[:tracking],
category_id: category_id || -1) category_id: category_id || -1)
end end
@ -949,6 +939,15 @@ class TopicQuery
end end
end end
def remove_already_seen_for_category(list, user)
if user
list = list
.where("category_users.last_seen_at IS NULL OR topics.created_at > category_users.last_seen_at")
end
list
end
def new_messages(params) def new_messages(params)
query = TopicQuery query = TopicQuery
.new_filter(messages_for_groups_or_user(params[:my_group_ids]), Time.at(SiteSetting.min_new_topics_time).to_datetime) .new_filter(messages_for_groups_or_user(params[:my_group_ids]), Time.at(SiteSetting.min_new_topics_time).to_datetime)

View File

@ -273,6 +273,19 @@ describe TopicQuery do
end end
end end
context 'already seen categories' do
it 'is removed from new and visible on latest lists' do
category = Fabricate(:category_with_definition)
topic = Fabricate(:topic, category: category)
CategoryUser.create!(user_id: user.id,
category_id: category.id,
last_seen_at: topic.created_at
)
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
end
context 'muted tags' do context 'muted tags' do
it 'is removed from new and latest lists' do it 'is removed from new and latest lists' do
SiteSetting.tagging_enabled = true SiteSetting.tagging_enabled = true

View File

@ -2347,6 +2347,31 @@ RSpec.describe TopicsController do
user.reload user.reload
expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date) expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
end end
context 'category' do
fab!(:category) { Fabricate(:category) }
fab!(:subcategory) { Fabricate(:category, parent_category_id: category.id) }
it 'updates last_seen_at for main category' do
sign_in(user)
category_user = CategoryUser.create!(category_id: category.id, user_id: user.id)
subcategory_user = CategoryUser.create!(category_id: subcategory.id, user_id: user.id)
put "/topics/reset-new.json?category_id=#{category.id}"
expect(category_user.reload.last_seen_at).not_to be_nil
expect(subcategory_user.reload.last_seen_at).to be_nil
end
it 'updates last_seen_at for main category and subcategories' do
sign_in(user)
category_user = CategoryUser.create!(category_id: category.id, user_id: user.id)
subcategory_user = CategoryUser.create!(category_id: subcategory.id, user_id: user.id)
put "/topics/reset-new.json?category_id=#{category.id}&include_subcategories=true"
expect(category_user.reload.last_seen_at).not_to be_nil
expect(subcategory_user.reload.last_seen_at).not_to be_nil
end
end
end end
describe '#feature_stats' do describe '#feature_stats' do