FEATURE: Dismiss new per category (#8330)
Ability to dismiss new topics per category.
This commit is contained in:
parent
d095c2cee7
commit
6e1fe22a9d
|
@ -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")
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -847,7 +847,23 @@ class TopicsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_new
|
def reset_new
|
||||||
current_user.user_stat.update_column(:new_since, Time.now)
|
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)
|
||||||
|
end
|
||||||
render body: nil
|
render body: nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,21 +872,15 @@ 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
|
muted: CategoryUser.notification_levels[:muted],
|
||||||
WHERE c.id = topics.category_id
|
regular: TopicUser.notification_levels[:regular],
|
||||||
AND c.id <> :category_id
|
category_id: category_id || -1)
|
||||||
AND (COALESCE(cu.notification_level, :muted) = :muted)
|
|
||||||
AND (COALESCE(tu.notification_level, :regular) <= :regular)
|
|
||||||
)", user_id: user.id,
|
|
||||||
muted: CategoryUser.notification_levels[:muted],
|
|
||||||
regular: TopicUser.notification_levels[:regular],
|
|
||||||
category_id: category_id || -1)
|
|
||||||
else
|
else
|
||||||
category_ids = [
|
category_ids = [
|
||||||
SiteSetting.default_categories_watching.split("|"),
|
SiteSetting.default_categories_watching.split("|"),
|
||||||
|
@ -897,20 +892,15 @@ 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
|
muted: CategoryUser.notification_levels[:muted],
|
||||||
AND cu.category_id = topics.category_id
|
regular: CategoryUser.notification_levels[:regular],
|
||||||
AND cu.notification_level = :muted
|
tracking: TopicUser.notification_levels[:tracking],
|
||||||
AND cu.category_id <> :category_id
|
category_id: category_id || -1)
|
||||||
AND (tu.notification_level IS NULL OR tu.notification_level < :tracking)
|
|
||||||
)", user_id: user.id,
|
|
||||||
muted: CategoryUser.notification_levels[:muted],
|
|
||||||
tracking: TopicUser.notification_levels[:tracking],
|
|
||||||
category_id: category_id || -1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
list
|
list
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue