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:
Krzysztof Kotlarek
2021-02-04 11:27:34 +11:00
committed by GitHub
parent 151193bb11
commit f39e7fe81d
17 changed files with 317 additions and 33 deletions

View File

@ -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

View 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

View 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
#

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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