mirror of
https://github.com/discourse/discourse.git
synced 2025-06-03 19:39:30 +08:00
FEATURE: New way to dismiss new topics (#11927)
This is a try to simplify logic around dismiss new topics to have one solution to work in all places - dismiss all-new, dismiss new in a specific category or even in a specific tag.
This commit is contained in:

committed by
GitHub

parent
151193bb11
commit
f39e7fe81d
@ -897,12 +897,8 @@ class TopicsController < ApplicationController
|
||||
if params[:include_subcategories] == 'true'
|
||||
category_ids = category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id))
|
||||
end
|
||||
DismissTopics.new(current_user, Topic.where(category_id: category_ids)).perform!
|
||||
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)
|
||||
TopicTrackingState.publish_dismiss_new(current_user.id, category_id)
|
||||
end
|
||||
else
|
||||
|
60
app/jobs/scheduled/clean_dismissed_topic_users.rb
Normal file
60
app/jobs/scheduled/clean_dismissed_topic_users.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class CleanDismissedTopicUsers < ::Jobs::Scheduled
|
||||
every 1.day
|
||||
|
||||
def execute(args)
|
||||
delete_overdue_dismissals!
|
||||
delete_over_the_limit_dismissals!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_overdue_dismissals!
|
||||
sql = <<~SQL
|
||||
DELETE FROM dismissed_topic_users dtu1
|
||||
USING dismissed_topic_users dtu2
|
||||
JOIN topics ON topics.id = dtu2.topic_id
|
||||
JOIN users ON users.id = dtu2.user_id
|
||||
JOIN categories ON categories.id = topics.category_id
|
||||
LEFT JOIN user_stats ON user_stats.user_id = users.id
|
||||
LEFT JOIN user_options ON user_options.user_id = users.id
|
||||
WHERE topics.created_at < GREATEST(CASE
|
||||
WHEN COALESCE(user_options.new_topic_duration_minutes, :default_duration) = :always THEN users.created_at
|
||||
WHEN COALESCE(user_options.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(users.previous_visit_at,users.created_at)
|
||||
ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(user_options.new_topic_duration_minutes, :default_duration))
|
||||
END, user_stats.new_since, :min_date)
|
||||
AND dtu1.id = dtu2.id
|
||||
SQL
|
||||
sql = DB.sql_fragment(sql,
|
||||
now: DateTime.now,
|
||||
last_visit: User::NewTopicDuration::LAST_VISIT,
|
||||
always: User::NewTopicDuration::ALWAYS,
|
||||
default_duration: SiteSetting.default_other_new_topic_duration_minutes,
|
||||
min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime)
|
||||
DB.exec(sql)
|
||||
end
|
||||
|
||||
def delete_over_the_limit_dismissals!
|
||||
user_ids = DismissedTopicUser.distinct(:user_id).pluck(:user_id)
|
||||
sql = <<~SQL
|
||||
DELETE FROM dismissed_topic_users
|
||||
WHERE dismissed_topic_users.id NOT IN (
|
||||
SELECT valid_dtu.id FROM users
|
||||
LEFT JOIN dismissed_topic_users valid_dtu ON valid_dtu.user_id = users.id
|
||||
AND valid_dtu.topic_id IN (
|
||||
SELECT topic_id FROM dismissed_topic_users dtu2
|
||||
JOIN topics ON topics.id = dtu2.topic_id
|
||||
WHERE dtu2.user_id = users.id
|
||||
ORDER BY topics.created_at DESC
|
||||
LIMIT :max_new_topics
|
||||
)
|
||||
WHERE users.id IN(:user_ids)
|
||||
)
|
||||
SQL
|
||||
sql = DB.sql_fragment(sql, max_new_topics: SiteSetting.max_new_topics, user_ids: user_ids)
|
||||
DB.exec(sql)
|
||||
end
|
||||
end
|
||||
end
|
27
app/models/dismissed_topic_user.rb
Normal file
27
app/models/dismissed_topic_user.rb
Normal file
@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DismissedTopicUser < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :topic
|
||||
|
||||
def self.lookup_for(user, topics)
|
||||
return [] if user.blank? || topics.blank?
|
||||
|
||||
topic_ids = topics.map(&:id)
|
||||
DismissedTopicUser.where(topic_id: topic_ids, user_id: user.id).pluck(:topic_id)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: dismissed_topic_users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :integer
|
||||
# topic_id :integer
|
||||
# created_at :datetime
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_dismissed_topic_users_on_user_id_and_topic_id (user_id,topic_id) UNIQUE
|
||||
#
|
@ -231,6 +231,7 @@ class Topic < ActiveRecord::Base
|
||||
belongs_to :featured_user4, class_name: 'User', foreign_key: :featured_user4_id
|
||||
|
||||
has_many :topic_users
|
||||
has_many :dismissed_topic_users
|
||||
has_many :topic_links
|
||||
has_many :topic_invites
|
||||
has_many :invites, through: :topic_invites, source: :invite
|
||||
@ -250,6 +251,7 @@ class Topic < ActiveRecord::Base
|
||||
# When we want to temporarily attach some data to a forum topic (usually before serialization)
|
||||
attr_accessor :user_data
|
||||
attr_accessor :category_user_data
|
||||
attr_accessor :dismissed
|
||||
|
||||
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
|
||||
attr_accessor :participants
|
||||
|
@ -84,6 +84,7 @@ class TopicList < DraftableList
|
||||
|
||||
# Attach some data for serialization to each topic
|
||||
@topic_lookup = TopicUser.lookup_for(@current_user, @topics) if @current_user
|
||||
@dismissed_topic_users_lookup = DismissedTopicUser.lookup_for(@current_user, @topics) if @current_user
|
||||
|
||||
post_action_type =
|
||||
if @current_user
|
||||
@ -119,6 +120,8 @@ class TopicList < DraftableList
|
||||
ft.category_user_data = @category_user_lookup[ft.category_id]
|
||||
end
|
||||
|
||||
ft.dismissed = @current_user && @dismissed_topic_users_lookup.include?(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 }
|
||||
end
|
||||
|
@ -330,7 +330,7 @@ class TopicTrackingState
|
||||
else
|
||||
TopicQuery.new_filter(Topic, "xxx").where_clause.ast.to_sql.gsub!("'xxx'", treat_as_new_topic_clause) +
|
||||
" AND topics.created_at > :min_new_topic_date" +
|
||||
" AND (category_users.last_seen_at IS NULL OR topics.created_at > category_users.last_seen_at)"
|
||||
" AND dismissed_topic_users.id IS NULL"
|
||||
end
|
||||
|
||||
select = (opts[:select]) || "
|
||||
@ -396,6 +396,7 @@ class TopicTrackingState
|
||||
JOIN categories c ON c.id = topics.category_id
|
||||
LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id
|
||||
LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{opts[:user].id}
|
||||
LEFT JOIN dismissed_topic_users ON dismissed_topic_users.topic_id = topics.id AND dismissed_topic_users.user_id = #{opts[:user].id}
|
||||
WHERE u.id = :user_id AND
|
||||
#{filter_old_unread}
|
||||
topics.archetype <> 'private_message' AND
|
||||
|
@ -75,7 +75,7 @@ class ListableTopicSerializer < BasicTopicSerializer
|
||||
def seen
|
||||
return true if !scope || !scope.user
|
||||
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.dismissed
|
||||
return true if object.created_at < scope.user.user_option.treat_as_new_topic_start_date
|
||||
false
|
||||
end
|
||||
|
38
app/services/dismiss_topics.rb
Normal file
38
app/services/dismiss_topics.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DismissTopics
|
||||
def initialize(user, topics_scope)
|
||||
@user = user
|
||||
@topics_scope = topics_scope
|
||||
end
|
||||
|
||||
def perform!
|
||||
DismissedTopicUser.insert_all(rows) if rows.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rows
|
||||
@rows ||= @topics_scope.where("created_at >= ?", since_date).order(created_at: :desc).limit(SiteSetting.max_new_topics).map do |topic|
|
||||
{
|
||||
topic_id: topic.id,
|
||||
user_id: @user.id,
|
||||
created_at: Time.zone.now
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def since_date
|
||||
new_topic_duration_minutes = @user.user_option&.new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes
|
||||
setting_date =
|
||||
case new_topic_duration_minutes
|
||||
when User::NewTopicDuration::LAST_VISIT
|
||||
@user.previous_visit_at || @user.created_at
|
||||
when User::NewTopicDuration::ALWAYS
|
||||
@user.created_at
|
||||
else
|
||||
new_topic_duration_minutes.minutes.ago
|
||||
end
|
||||
[setting_date, @user.user_stat.new_since, Time.at(SiteSetting.min_new_topics_time).to_datetime].max
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user