2019-05-03 08:17:27 +10:00
# frozen_string_literal: true
2013-02-25 19:42:20 +03:00
#
2015-09-02 20:25:18 +02:00
# Helps us find topics.
# Returns a TopicList object containing the topics found.
2013-02-05 14:16:51 -05:00
#
2015-09-02 20:25:18 +02:00
2013-02-05 14:16:51 -05:00
class TopicQuery
2021-08-02 12:41:41 +08:00
include PrivateMessageLists
2018-09-03 06:45:32 +02:00
PG_MAX_INT ||= 2_147_483_647
2021-06-17 08:20:09 +10:00
DEFAULT_PER_PAGE_COUNT ||= 30
2017-02-15 15:25:43 -05:00
2018-08-14 17:01:04 +10:00
def self . validators
@validators ||=
begin
2023-01-20 12:52:49 -06:00
int = lambda { | x | Integer === x || ( String === x && x . match? ( /\A-?[0-9]+\z/ )) }
2018-08-15 14:56:24 +10:00
zero_up_to_max_int = lambda { | x | int . call ( x ) && x . to_i . between? ( 0 , PG_MAX_INT ) }
2023-01-16 19:20:19 +02:00
array_or_string = lambda { | x | Array === x || String === x }
{
2023-09-07 12:38:03 +08:00
before : zero_up_to_max_int ,
bumped_before : zero_up_to_max_int ,
2023-01-16 19:20:19 +02:00
max_posts : zero_up_to_max_int ,
min_posts : zero_up_to_max_int ,
page : zero_up_to_max_int ,
tags : array_or_string ,
}
2018-09-03 06:45:32 +02:00
end
2018-08-14 17:01:04 +10:00
end
def self . validate? ( option , value )
if fn = validators [ option . to_sym ]
fn . call ( value )
else
true
end
end
2017-02-15 17:27:10 -05:00
def self . public_valid_options
2022-07-29 00:03:53 +01:00
# For these to work in Ember, add them to `controllers/discovery-sortable.js`
2017-02-15 17:27:10 -05:00
@public_valid_options ||= % i [
page
2017-03-02 14:54:26 -05:00
before
2017-03-02 15:11:38 -05:00
bumped_before
2017-02-15 15:25:43 -05:00
topic_ids
category
order
ascending
2017-02-15 17:27:10 -05:00
min_posts
max_posts
2017-02-15 15:25:43 -05:00
status
2017-02-15 17:27:10 -05:00
filter
2017-02-15 15:25:43 -05:00
state
search
2017-02-15 17:27:10 -05:00
q
2020-08-06 16:33:45 +10:00
f
2023-08-18 07:44:04 +03:00
subset
2017-02-15 15:25:43 -05:00
group_name
2017-02-15 17:27:10 -05:00
tags
match_all_tags
no_subcategories
2021-10-06 15:18:42 -04:00
no_tags
exclude_tag
2023-01-09 12:10:19 +00:00
]
2017-02-15 17:27:10 -05:00
end
def self . valid_options
@valid_options ||=
public_valid_options +
% i [
except_topic_ids
limit
page
per_page
visible
2018-01-15 16:13:29 +11:00
guardian
2018-03-13 15:59:12 -04:00
no_definitions
2020-09-14 12:07:35 +01:00
destination_category_id
2022-02-11 14:46:23 +02:00
include_all_pms
2020-09-14 12:07:35 +01:00
include_pms
2023-01-09 12:10:19 +00:00
]
2017-02-15 15:25:43 -05:00
end
2014-04-16 12:05:54 -04:00
# Maps `order` to a columns in `topics`
2013-11-13 14:17:06 -05:00
SORTABLE_MAPPING = {
"likes" => "like_count" ,
2014-10-03 13:16:53 +10:00
"op_likes" => "op_likes" ,
2013-11-13 14:17:06 -05:00
"views" => "views" ,
"posts" => "posts_count" ,
2014-08-18 13:26:12 -04:00
"activity" => "bumped_at" ,
2013-11-14 15:50:36 -05:00
"posters" => "participant_count" ,
2014-08-27 12:42:54 -04:00
"category" => "category_id" ,
"created" => "created_at" ,
2013-11-13 14:17:06 -05:00
}
2015-12-21 11:43:17 -05:00
cattr_accessor :results_filter_callbacks
self . results_filter_callbacks = []
2017-02-15 15:25:43 -05:00
attr_accessor :options , :user , :guardian
def self . add_custom_filter ( key , & blk )
@custom_filters ||= {}
valid_options << key
2017-02-15 17:27:10 -05:00
public_valid_options << key
2017-02-15 15:25:43 -05:00
@custom_filters [ key ] = blk
end
def self . remove_custom_filter ( key )
@custom_filters . delete ( key )
2017-02-15 17:27:10 -05:00
public_valid_options . delete ( key )
2017-02-15 15:25:43 -05:00
valid_options . delete ( key )
@custom_filters = nil if @custom_filters . length == 0
end
def self . apply_custom_filters ( results , topic_query )
if @custom_filters
@custom_filters . each { | key , filter | results = filter . call ( results , topic_query ) }
end
results
end
2013-07-16 21:20:18 +02:00
def initialize ( user = nil , options = {})
2017-02-15 15:25:43 -05:00
options . assert_valid_keys ( TopicQuery . valid_options )
2015-09-21 15:14:05 -04:00
@options = options . dup
2013-07-16 21:20:18 +02:00
@user = user
2018-01-15 16:13:29 +11:00
@guardian = options [ :guardian ] || Guardian . new ( @user )
2013-02-05 14:16:51 -05:00
end
2014-02-21 14:17:45 -05:00
def joined_topic_user ( list = nil )
( list || Topic ) . joins (
"LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{ @user . id . to_i } )" ,
)
end
2018-11-12 13:04:30 +11:00
def get_pm_params ( topic )
if topic . private_message?
my_group_ids =
topic
. topic_allowed_groups
. joins (
"
LEFT JOIN group_users gu
ON topic_allowed_groups.group_id = gu.group_id
AND gu.user_id = #{ @user . id . to_i }
" ,
)
. where ( "gu.group_id IS NOT NULL" )
. pluck ( :group_id )
target_group_ids = topic . topic_allowed_groups . pluck ( :group_id )
target_users = topic . topic_allowed_users
if my_group_ids . present?
# strip out users in groups you already belong to
target_users =
target_users . joins (
2020-12-11 10:56:26 +11:00
"LEFT JOIN group_users gu ON gu.user_id = topic_allowed_users.user_id AND #{ DB . sql_fragment ( "gu.group_id IN (?)" , my_group_ids ) } " ,
2018-11-12 13:04:30 +11:00
) . where ( "gu.group_id IS NULL" )
end
target_user_ids =
target_users . where ( "NOT topic_allowed_users.user_id = ?" , @user . id ) . pluck ( :user_id )
{
topic : topic ,
my_group_ids : my_group_ids ,
target_group_ids : target_group_ids ,
target_user_ids : target_user_ids ,
}
end
end
def list_related_for ( topic , pm_params : nil )
return if ! topic . private_message?
return if @user . blank?
2022-09-26 13:58:40 +10:00
2022-10-05 10:50:20 +10:00
return if ! @user . in_any_groups? ( SiteSetting . personal_message_enabled_groups_map )
2018-11-12 13:04:30 +11:00
builder = SuggestedTopicsBuilder . new ( topic )
pm_params = pm_params || get_pm_params ( topic )
if pm_params [ :my_group_ids ]. present?
builder . add_results (
related_messages_group (
pm_params . merge (
count : [ 6 , builder . results_left ]. max ,
exclude : builder . excluded_topic_ids ,
2023-01-09 12:10:19 +00:00
),
),
2018-11-12 13:04:30 +11:00
)
else
builder . add_results (
related_messages_user (
pm_params . merge (
count : [ 6 , builder . results_left ]. max ,
exclude : builder . excluded_topic_ids ,
2023-01-09 12:10:19 +00:00
),
),
2018-11-12 13:04:30 +11:00
)
end
params = { unordered : true }
params [ :preload_posters ] = true
create_list ( :suggested , params , builder . results )
end
2013-02-05 14:16:51 -05:00
# Return a list of suggested topics for a topic
2023-03-28 15:52:17 +11:00
# The include_random param was added so plugins can generate a suggested topics list without the random topics
def list_suggested_for ( topic , pm_params : nil , include_random : true )
2018-01-23 12:05:44 -05:00
# Don't suggest messages unless we have a user, and private messages are
# enabled.
2022-09-26 13:58:40 +10:00
if topic . private_message? &&
2022-10-05 10:50:20 +10:00
( @user . blank? || ! @user . in_any_groups? ( SiteSetting . personal_message_enabled_groups_map ))
2022-09-26 13:58:40 +10:00
return
end
2016-02-03 18:50:05 +11:00
2013-07-12 14:38:20 -04:00
builder = SuggestedTopicsBuilder . new ( topic )
2013-02-05 14:16:51 -05:00
2018-11-12 13:04:30 +11:00
pm_params = pm_params || get_pm_params ( topic )
2016-02-03 18:50:05 +11:00
2023-03-13 15:37:49 -03:00
if DiscoursePluginRegistry . list_suggested_for_providers . any?
DiscoursePluginRegistry . list_suggested_for_providers . each do | provider |
suggested = provider . call ( topic , pm_params , self )
builder . add_results ( suggested [ :result ] ) if suggested && ! suggested [ :result ]. blank?
end
end
2013-07-12 14:38:20 -04:00
# When logged in we start with different results
2013-07-16 21:20:18 +02:00
if @user
2016-02-03 18:50:05 +11:00
if topic . private_message?
2018-10-29 16:09:58 +11:00
unless builder . full?
2016-02-03 18:50:05 +11:00
builder . add_results ( new_messages ( pm_params . merge ( count : builder . results_left )))
2023-01-09 12:10:19 +00:00
end
2016-02-03 18:50:05 +11:00
2018-10-29 16:09:58 +11:00
unless builder . full?
builder . add_results ( unread_messages ( pm_params . merge ( count : builder . results_left )))
2023-01-09 12:10:19 +00:00
end
2016-02-03 18:50:05 +11:00
else
2023-06-05 09:06:43 +09:00
if @user . new_new_view_enabled?
builder . add_results (
new_and_unread_results (
topic :,
per_page : builder . results_left ,
max_age : SiteSetting . suggested_topics_unread_max_days_old ,
),
)
else
builder . add_results (
unread_results (
topic : topic ,
per_page : builder . results_left ,
max_age : SiteSetting . suggested_topics_unread_max_days_old ,
),
:high ,
)
2019-04-16 17:51:57 +10:00
2023-06-05 09:06:43 +09:00
unless builder . full?
builder . add_results ( new_results ( topic : topic , per_page : builder . category_results_left ))
end
2023-01-09 12:10:19 +00:00
end
2016-02-03 18:50:05 +11:00
end
2013-02-05 14:16:51 -05:00
end
2018-10-29 10:47:59 +11:00
if ! topic . private_message?
2023-03-28 15:52:17 +11:00
if include_random && ! builder . full?
2016-02-03 18:50:05 +11:00
builder . add_results (
random_suggested ( topic , builder . results_left , builder . excluded_topic_ids ),
)
2023-01-09 12:10:19 +00:00
end
2016-02-03 18:50:05 +11:00
end
params = { unordered : true }
params [ :preload_posters ] = true if topic . private_message?
create_list ( :suggested , params , builder . results )
2013-02-05 14:16:51 -05:00
end
2013-03-27 16:17:49 -04:00
# The latest view of topics
def list_latest
2014-10-08 12:44:47 -04:00
create_list ( :latest , {}, latest_results )
2013-02-05 14:16:51 -05:00
end
2023-03-03 09:46:21 +08:00
def list_filter
2023-04-11 08:48:07 +08:00
topics_filter =
2023-05-03 12:40:00 +08:00
TopicsFilter . new (
guardian : @guardian ,
scope : latest_results ( include_muted : false , skip_ordering : true ),
)
2023-04-11 08:48:07 +08:00
results = topics_filter . filter_from_query_string ( @options [ :q ] )
if ! topics_filter . topic_notification_levels . include? ( NotificationLevels . all [ :muted ] )
results = remove_muted_topics ( results , @user )
end
2023-03-30 12:57:23 +08:00
2023-05-03 12:40:00 +08:00
results = apply_ordering ( results ) if results . order_values . empty?
2023-03-30 12:57:23 +08:00
create_list ( :filter , {}, results )
2023-03-03 09:46:21 +08:00
end
2013-02-05 14:16:51 -05:00
def list_read
2013-04-02 16:52:51 -04:00
create_list ( :read , unordered : true ) do | topics |
2017-04-26 21:56:37 +05:30
topics . where ( "tu.last_visited_at IS NOT NULL" ) . order ( "tu.last_visited_at DESC" )
2013-02-05 14:16:51 -05:00
end
end
def list_new
2023-02-27 15:11:01 +03:00
if @user &. new_new_view_enabled?
2023-08-18 07:44:04 +03:00
list =
case @options [ :subset ]
when "topics"
new_results
when "replies"
unread_results
else
new_and_unread_results
end
create_list ( :new , { unordered : true }, list )
2023-02-27 15:11:01 +03:00
else
create_list ( :new , { unordered : true }, new_results )
end
2013-02-05 14:16:51 -05:00
end
def list_unread
2015-02-23 16:50:52 +11:00
create_list ( :unread , { unordered : true }, unread_results )
2013-02-05 14:16:51 -05:00
end
2021-08-10 18:30:34 +04:00
def list_unseen
create_list ( :unseen , { unordered : true }, unseen_results )
end
2013-02-05 14:16:51 -05:00
def list_posted
2014-03-29 15:26:13 -07:00
create_list ( :posted ) { | l | l . where ( "tu.posted" ) }
2013-02-05 14:16:51 -05:00
end
2015-01-25 15:53:11 +11:00
def list_bookmarks
create_list ( :bookmarks ) { | l | l . where ( "tu.bookmarked" ) }
end
2013-12-27 18:10:35 +01:00
def list_top_for ( period )
2021-07-23 13:52:35 -04:00
score_column = TopTopic . score_column_for_period ( period )
2013-12-24 00:50:36 +01:00
create_list ( :top , unordered : true ) do | topics |
2020-05-08 00:34:53 +05:30
topics = remove_muted_categories ( topics , @user )
2021-07-23 13:52:35 -04:00
topics = topics . joins ( :top_topic ) . where ( "top_topics. #{ score_column } > 0" )
2014-09-05 15:20:39 +10:00
if period == :yearly && @user . try ( :trust_level ) == TrustLevel [ 0 ]
2021-07-23 13:52:35 -04:00
topics . order ( <<~ SQL )
CASE WHEN (
COALESCE ( topics . pinned_at , '1900-01-01' ) > COALESCE ( tu . cleared_pinned_at , '1900-01-01' )
) THEN 0 ELSE 1 END ,
top_topics . #{score_column} DESC,
topics . bumped_at DESC
SQL
2014-01-18 19:03:09 +01:00
else
2021-07-23 13:52:35 -04:00
topics . order ( <<~ SQL )
COALESCE ( top_topics . #{score_column}, 0) DESC, topics.bumped_at DESC
SQL
2014-01-18 19:03:09 +01:00
end
2013-12-24 00:50:36 +01:00
end
2014-01-14 01:02:14 +01:00
end
2013-07-24 17:15:21 -04:00
def list_topics_by ( user )
2015-03-23 18:12:37 -04:00
@options [ :filtered_to_user ] = user . id
2013-07-24 17:15:21 -04:00
create_list ( :user_topics ) { | topics | topics . where ( user_id : user . id ) }
end
2018-03-14 19:40:28 +08:00
def list_group_topics ( group )
list =
default_results . where (
"
topics.user_id IN (
2022-11-01 14:05:13 -05:00
SELECT user_id FROM group_users gu WHERE gu.group_id = ?
2018-03-14 19:40:28 +08:00
)
2022-11-01 14:05:13 -05:00
" ,
group . id . to_i ,
)
2018-03-14 19:40:28 +08:00
create_list ( :group_topics , {}, list )
end
2015-02-23 16:50:52 +11:00
def list_category_topic_ids ( category )
2015-02-25 14:24:25 +11:00
query = default_results ( category : category . id )
2018-03-22 16:38:53 -04:00
pinned_ids =
query
. where ( "topics.pinned_at IS NOT NULL AND topics.category_id = ?" , category . id )
. limit ( nil )
. order ( "pinned_at DESC" )
. pluck ( :id )
non_pinned_ids =
query . where ( "topics.pinned_at IS NULL OR topics.category_id <> ?" , category . id ) . pluck ( :id )
2015-06-10 14:36:47 -04:00
( pinned_ids + non_pinned_ids )
2013-02-05 14:16:51 -05:00
end
2013-02-27 19:36:12 -08:00
def list_new_in_category ( category )
2014-02-04 15:55:30 -05:00
create_list ( :new_in_category , unordered : true , category : category . id ) do | list |
list . by_newest . first ( 25 )
2014-01-17 23:52:06 +01:00
end
2013-02-27 19:36:12 -08:00
end
2021-06-02 09:06:29 +10:00
def self . new_filter ( list , treat_as_new_topic_start_date : nil , treat_as_new_topic_clause_sql : nil )
if treat_as_new_topic_start_date
list =
list . where ( "topics.created_at >= :created_at" , created_at : treat_as_new_topic_start_date )
else
list = list . where ( "topics.created_at >= #{ treat_as_new_topic_clause_sql } " )
end
2013-05-23 15:21:07 +10:00
list . where ( "tu.last_read_post_number IS NULL" ) . where (
"COALESCE(tu.notification_level, :tracking) >= :tracking" ,
tracking : TopicUser . notification_levels [ :tracking ] ,
)
end
2022-06-30 10:18:12 +10:00
def self . unread_filter ( list , whisperer : false )
col_name = whisperer ? "highest_staff_post_number" : "highest_post_number"
2016-12-02 17:03:31 +11:00
2017-05-25 15:07:12 -04:00
list . where ( "tu.last_read_post_number < topics. #{ col_name } " ) . where (
2017-11-20 11:49:09 +08:00
"COALESCE(tu.notification_level, :regular) >= :tracking" ,
regular : TopicUser . notification_levels [ :regular ] ,
tracking : TopicUser . notification_levels [ :tracking ] ,
)
2013-05-21 16:39:51 +10:00
end
2022-06-01 14:54:42 +08:00
# Any changes here will need to be reflected in `lib/topic-list-tracked-filter.js` for the `isTrackedTopic` function on
# the client side. The `f=tracked` query param is not heavily used so we do not want to be querying for a topic's
# tracked status by default. Instead, the client will handle the filtering when the `f=tracked` query params is present.
2020-09-25 12:39:37 -07:00
def self . tracked_filter ( list , user_id )
2022-06-01 12:09:58 +08:00
tracked_category_ids_sql = <<~ SQL
SELECT cd . category_id FROM category_users cd
WHERE cd . user_id = :user_id AND cd . notification_level >= :tracking
SQL
has_sub_sub_categories = SiteSetting . max_category_nesting == 3
2020-09-25 12:39:37 -07:00
sql = +<<~ SQL
topics . category_id IN (
2022-06-01 12:09:58 +08:00
SELECT
c . id
FROM categories c
#{has_sub_sub_categories ? "LEFT JOIN categories parent_categories ON parent_categories.id = c.parent_category_id" : ""}
2022-06-08 10:45:59 +08:00
WHERE ( c . id IN ( #{tracked_category_ids_sql}))
2022-06-01 12:09:58 +08:00
OR c . parent_category_id IN ( #{tracked_category_ids_sql})
#{has_sub_sub_categories ? "OR (parent_categories.id IS NOT NULL AND parent_categories.parent_category_id IN (#{tracked_category_ids_sql}))" : ""}
2020-10-07 12:15:28 -04:00
)
2020-09-25 12:39:37 -07:00
SQL
sql << <<~ SQL if SiteSetting . tagging_enabled
OR topics . id IN (
SELECT tt . topic_id FROM topic_tags tt WHERE tt . tag_id IN (
SELECT tu . tag_id
FROM tag_users tu
WHERE tu . user_id = :user_id AND tu . notification_level >= :tracking
)
)
SQL
list . where ( sql , user_id : user_id , tracking : NotificationLevels . all [ :tracking ] )
end
2015-02-23 16:50:52 +11:00
def prioritize_pinned_topics ( topics , options )
2019-05-03 08:17:27 +10:00
pinned_clause =
if options [ :category_id ]
+ "topics.category_id = #{ options [ :category_id ]. to_i } AND"
else
+ "pinned_globally AND "
end
2015-02-23 16:50:52 +11:00
pinned_clause << " pinned_at IS NOT NULL "
2019-05-03 08:17:27 +10:00
2015-02-23 16:50:52 +11:00
if @user
pinned_clause << " AND (topics.pinned_at > tu.cleared_pinned_at OR tu.cleared_pinned_at IS NULL)"
end
unpinned_topics = topics . where ( "NOT ( #{ pinned_clause } )" )
2021-08-19 14:43:58 +03:00
pinned_topics = topics . dup . offset ( nil ) . where ( pinned_clause ) . reorder ( pinned_at : :desc )
2015-02-23 16:50:52 +11:00
per_page = options [ :per_page ] || per_page_setting
limit = per_page unless options [ :limit ] == false
page = options [ :page ]. to_i
if page == 0
( pinned_topics + unpinned_topics ) [ 0 ... limit ] if limit
else
2017-08-31 12:06:56 +08:00
offset = ( page * per_page ) - pinned_topics . length
2023-02-16 10:40:11 +01:00
offset = 0 if offset <= 0
2015-02-26 14:48:56 +11:00
unpinned_topics . offset ( offset ) . to_a
2015-02-23 16:50:52 +11:00
end
end
2015-01-08 16:44:27 -05:00
def create_list ( filter , options = {}, topics = nil )
2021-10-12 10:25:03 +05:30
options [ :filter ] ||= filter
2015-01-08 16:44:27 -05:00
topics ||= default_results ( options )
topics = yield ( topics ) if block_given?
2023-04-07 13:01:42 -03:00
topics =
DiscoursePluginRegistry . apply_modifier ( :topic_query_create_list_topics , topics , options , self )
2015-02-23 16:50:52 +11:00
options = options . merge ( @options )
2015-12-23 11:09:17 +11:00
if %w[activity default] . include? ( options [ :order ] || "activity" ) && ! options [ :unordered ] &&
filter != :private_messages
2015-02-23 16:50:52 +11:00
topics = prioritize_pinned_topics ( topics , options )
end
2016-02-03 18:50:05 +11:00
topics = topics . to_a
if options [ :preload_posters ]
user_ids = []
topics . each do | ft |
2023-02-27 17:20:00 +01:00
user_ids << ft . user_id << ft . last_post_user_id << ft . featured_user_ids <<
ft . allowed_user_ids
2016-02-03 18:50:05 +11:00
end
2020-07-17 10:48:08 +01:00
user_lookup = UserLookup . new ( user_ids )
2017-09-14 12:07:35 +08:00
2019-06-05 18:22:47 +10:00
# memoize for loop so we don't keep looking these up
translations = TopicPostersSummary . translations
2016-02-03 18:50:05 +11:00
topics . each do | t |
2017-09-14 12:07:35 +08:00
t . posters = t . posters_summary ( user_lookup : user_lookup , translations : translations )
2016-02-03 18:50:05 +11:00
end
end
topics . each do | t |
2023-05-31 19:32:06 +05:30
if filter == :private_messages
t . allowed_user_ids = t . allowed_users . map { | u | u . id }
t . allowed_group_ids = t . allowed_groups . map { | g | g . id }
else
t . allowed_user_ids = []
t . allowed_group_ids = []
end
2015-02-23 16:50:52 +11:00
end
2016-02-03 18:50:05 +11:00
list = TopicList . new ( filter , @user , topics , options . merge ( @options ))
2018-08-15 01:22:03 +02:00
list . per_page = options [ :per_page ] || per_page_setting
2015-01-08 16:44:27 -05:00
list
end
def latest_results ( options = {})
result = default_results ( options )
2021-08-10 18:30:34 +04:00
result = remove_muted ( result , @user , options )
result = apply_shared_drafts ( result , get_category_id ( options [ :category ] ), options )
# plugins can remove topics here:
self . class . results_filter_callbacks . each do | filter_callback |
result = filter_callback . call ( :latest , result , @user , options )
end
result
end
def unseen_results ( options = {})
result = default_results ( options )
2022-06-30 10:18:12 +10:00
result = unseen_filter ( result , @user . first_seen_at , @user . whisperer? ) if @user
2021-08-10 18:30:34 +04:00
result = remove_muted ( result , @user , options )
2018-12-07 12:44:23 +00:00
result = apply_shared_drafts ( result , get_category_id ( options [ :category ] ), options )
2015-12-21 11:43:17 -05:00
# plugins can remove topics here:
self . class . results_filter_callbacks . each do | filter_callback |
result = filter_callback . call ( :latest , result , @user , options )
end
2015-01-08 16:44:27 -05:00
result
end
def unread_results ( options = {})
2017-05-25 15:07:12 -04:00
result =
TopicQuery . unread_filter (
default_results ( options . reverse_merge ( unordered : true )),
2022-06-30 10:18:12 +10:00
whisperer : @user &. whisperer? ,
2015-01-08 16:44:27 -05:00
) . order ( "CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END" )
2015-12-21 11:43:17 -05:00
2023-02-16 16:02:09 +03:00
result = apply_max_age_limit ( result , options )
2019-04-05 12:44:36 +11:00
2015-12-21 11:43:17 -05:00
self . class . results_filter_callbacks . each do | filter_callback |
result = filter_callback . call ( :unread , result , @user , options )
end
2015-01-08 16:44:27 -05:00
suggested_ordering ( result , options )
end
def new_results ( options = {})
2015-02-23 16:50:52 +11:00
# TODO does this make sense or should it be ordered on created_at
# it is ordering on bumped_at now
2021-06-02 09:06:29 +10:00
result =
TopicQuery . new_filter (
default_results ( options . reverse_merge ( unordered : true )),
treat_as_new_topic_start_date : @user . user_option . treat_as_new_topic_start_date ,
)
2021-08-10 18:30:34 +04:00
result = remove_muted ( result , @user , options )
2021-02-04 11:27:34 +11:00
result = remove_dismissed ( result , @user )
2015-12-21 11:43:17 -05:00
self . class . results_filter_callbacks . each do | filter_callback |
result = filter_callback . call ( :new , result , @user , options )
end
2015-01-08 16:44:27 -05:00
suggested_ordering ( result , options )
end
2023-02-16 16:02:09 +03:00
def new_and_unread_results ( options = {})
base = default_results ( options . reverse_merge ( unordered : true ))
new_results =
TopicQuery . new_filter (
base ,
treat_as_new_topic_start_date : @user . user_option . treat_as_new_topic_start_date ,
)
2023-06-05 09:06:43 +09:00
2023-02-16 16:02:09 +03:00
new_results = remove_muted ( new_results , @user , options )
new_results = remove_dismissed ( new_results , @user )
unread_results =
apply_max_age_limit ( TopicQuery . unread_filter ( base , whisperer : @user &. whisperer? ), options )
base . joins_values . concat ( new_results . joins_values , unread_results . joins_values )
base . joins_values . uniq!
results = base . merge ( new_results . or ( unread_results ))
results = results . order ( "CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END" )
suggested_ordering ( results , options )
end
2013-02-05 14:16:51 -05:00
protected
2014-12-15 11:54:26 -05:00
def per_page_setting
2021-06-17 08:20:09 +10:00
DEFAULT_PER_PAGE_COUNT
2014-12-15 11:54:26 -05:00
end
2018-03-26 10:43:30 -04:00
def apply_shared_drafts ( result , category_id , options )
2019-08-29 11:35:31 +10:00
# PERF: avoid any penalty if there are no shared drafts enabled
# on some sites the cost can be high eg: gearbox
return result if SiteSetting . shared_drafts_category == ""
2018-03-28 15:36:12 -04:00
drafts_category_id = SiteSetting . shared_drafts_category . to_i
viewing_shared = category_id && category_id == drafts_category_id
2018-12-06 18:59:29 +00:00
2021-02-01 16:16:34 +02:00
if guardian . can_see_shared_draft?
2020-12-14 16:08:20 -03:00
if options [ :destination_category_id ]
destination_category_id = get_category_id ( options [ :destination_category_id ] )
topic_ids = SharedDraft . where ( category_id : destination_category_id ) . pluck ( :topic_id )
return result . where ( id : topic_ids )
end
return result . includes ( :shared_draft ) . references ( :shared_draft ) if viewing_shared
elsif viewing_shared
return (
result . joins ( "LEFT OUTER JOIN shared_drafts sd ON sd.topic_id = topics.id" ) . where (
"sd.id IS NULL" ,
)
2023-01-09 12:10:19 +00:00
)
2018-03-26 10:43:30 -04:00
end
2020-12-14 16:08:20 -03:00
result . where ( "topics.category_id != ?" , drafts_category_id )
2013-11-14 15:50:36 -05:00
end
2013-11-13 14:17:06 -05:00
2023-05-03 12:40:00 +08:00
def apply_ordering ( result , options = {})
2013-11-14 15:50:36 -05:00
sort_column = SORTABLE_MAPPING [ options [ :order ]] || "default"
sort_dir = ( options [ :ascending ] == "true" ) ? "ASC" : "DESC"
2014-10-03 13:16:53 +10:00
# If we are sorting in the default order desc, we should consider including pinned
# topics. Otherwise, just use bumped_at.
if sort_column == "default"
2015-01-05 17:39:49 +11:00
if sort_dir == "DESC"
# If something requires a custom order, for example "unread" which sorts the least read
# to the top, do nothing
return result if options [ :unordered ]
2014-10-03 13:16:53 +10:00
end
2013-11-13 14:17:06 -05:00
sort_column = "bumped_at"
2018-06-07 13:28:18 +08:00
end
2014-10-03 13:16:53 +10:00
2016-02-25 10:22:23 -06:00
# If we are sorting by category, actually use the name
if sort_column == "category_id"
# TODO forces a table scan, slow
2021-07-23 13:52:35 -04:00
return result . references ( :categories ) . order ( <<~ SQL )
CASE WHEN categories . id = #{SiteSetting.uncategorized_category_id.to_i} THEN '' ELSE categories.name END #{sort_dir}
SQL
2016-02-25 10:22:23 -06:00
end
2013-11-13 14:17:06 -05:00
if sort_column == "op_likes"
return (
result . includes ( :first_post ) . order (
"(SELECT like_count FROM posts p3 WHERE p3.topic_id = topics.id AND p3.post_number = 1) #{ sort_dir } " ,
)
2023-01-09 12:10:19 +00:00
)
2013-11-13 14:17:06 -05:00
end
2014-06-18 11:23:31 +10:00
if sort_column . start_with? ( "custom_fields" )
field = sort_column . split ( "." ) [ 1 ]
return (
result . order (
"(SELECT CASE WHEN EXISTS (SELECT true FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = ' #{ field } ') THEN (SELECT value::integer FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = ' #{ field } ') ELSE 0 END) #{ sort_dir } " ,
)
2023-01-09 12:10:19 +00:00
)
2014-06-18 11:23:31 +10:00
end
2013-07-16 21:20:18 +02:00
result . order ( "topics. #{ sort_column } #{ sort_dir } " )
end
2018-06-07 13:28:18 +08:00
2013-07-16 21:20:18 +02:00
def get_category_id ( category_id_or_slug )
2019-10-28 15:20:27 +00:00
return nil unless category_id_or_slug . present?
2013-07-16 21:20:18 +02:00
category_id = category_id_or_slug . to_i
2019-10-28 15:20:27 +00:00
if category_id == 0
2023-02-13 12:39:45 +08:00
category_id = Category . where ( slug : category_id_or_slug , parent_category_id : nil ) . pick ( :id )
2019-10-28 15:20:27 +00:00
end
2014-06-18 11:23:31 +10:00
category_id
2018-06-07 13:28:18 +08:00
end
2013-02-05 14:16:51 -05:00
2015-03-23 18:12:37 -04:00
# Create results based on a bunch of default options
def default_results ( options = {})
options . reverse_merge! ( @options )
2021-06-17 08:20:09 +10:00
options . reverse_merge! ( per_page : per_page_setting ) unless options [ :limit ] == false
2015-03-23 18:12:37 -04:00
2023-01-17 16:50:15 +08:00
# Whether to include unlisted (visible = false) topics
viewing_own_topics = @user && @user . id == options [ :filtered_to_user ]
if options [ :visible ]. nil?
options [ :visible ] = true if @user . nil? || @user . regular?
options [ :visible ] = false if @guardian . can_see_unlisted_topics? || viewing_own_topics
end
2013-03-06 15:17:07 -05:00
2013-07-16 21:20:18 +02:00
# Start with a list of all topics
2023-03-03 09:46:21 +08:00
result = Topic . includes ( :category )
2013-03-06 15:17:07 -05:00
2018-06-07 13:28:18 +08:00
if @user
2014-02-21 14:17:45 -05:00
result =
result . joins (
"LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{ @user . id . to_i } )" ,
2014-02-26 11:09:02 -05:00
) . references ( "tu" )
2018-06-07 13:28:18 +08:00
end
2014-06-18 11:23:31 +10:00
category_id = get_category_id ( options [ :category ] )
2014-10-08 12:44:47 -04:00
@options [ :category_id ] = category_id
2014-06-18 11:23:31 +10:00
if category_id
if options [ :no_subcategories ]
2021-09-28 10:05:00 +08:00
result = result . where ( "topics.category_id = ?" , category_id )
2014-06-18 11:23:31 +10:00
else
2021-09-28 10:05:00 +08:00
result = result . where ( "topics.category_id IN (?)" , Category . subcategory_ids ( category_id ))
2020-10-07 14:19:48 -04:00
if ! SiteSetting . show_category_definitions_in_topic_lists
2021-09-28 10:05:00 +08:00
result =
2023-03-13 19:33:26 +00:00
result . where (
"categories.topic_id IS DISTINCT FROM topics.id OR topics.category_id = ?" ,
category_id ,
)
2020-10-07 14:19:48 -04:00
end
2013-11-08 15:05:14 -05:00
end
2014-06-18 11:23:31 +10:00
result = result . references ( :categories )
2018-06-07 13:28:18 +08:00
2016-11-01 12:18:31 -04:00
if ! @options [ :order ]
2021-10-12 10:25:03 +05:30
filter = ( options [ :filter ] || options [ :f ] )
2016-11-01 12:18:31 -04:00
# category default sort order
2019-10-21 11:32:27 +01:00
sort_order , sort_ascending =
2023-02-13 12:39:45 +08:00
Category . where ( id : category_id ) . pick ( :sort_order , :sort_ascending )
2023-09-05 19:05:30 +01:00
if sort_order && ( filter . blank? || %w[default latest unseen] . include? ( filter . to_s ))
2016-11-01 12:18:31 -04:00
options [ :order ] = sort_order
options [ :ascending ] = !! sort_ascending ? "true" : "false"
2020-07-07 09:56:38 +02:00
else
options [ :order ] = "default"
options [ :ascending ] = "false"
2016-11-01 12:18:31 -04:00
end
2013-11-08 15:05:14 -05:00
end
2018-06-07 13:28:18 +08:00
end
2013-11-08 15:05:14 -05:00
2023-04-06 06:58:35 +08:00
if SiteSetting . tagging_enabled
2023-08-03 10:48:41 +08:00
# Use `preload` here instead since `includes` can end up calling `eager_load` which can unnecessarily lead to
# joins on the `topic_tags` and `tags` table leading to a much slower query.
result = result . preload ( :tags )
2023-04-06 06:58:35 +08:00
result = filter_by_tags ( result )
end
2023-05-03 12:40:00 +08:00
result = apply_ordering ( result , options ) if ! options [ :skip_ordering ]
2020-09-14 12:07:35 +01:00
2023-03-22 23:31:33 +02:00
all_listable_topics =
@guardian . filter_allowed_categories (
Topic . unscoped . listable_topics ,
category_id_column : "categories.id" ,
)
2020-09-14 12:07:35 +01:00
2022-02-11 14:46:23 +02:00
if options [ :include_pms ] || options [ :include_all_pms ]
all_pm_topics =
if options [ :include_all_pms ] && @guardian . is_admin?
Topic . unscoped . private_messages
else
Topic . unscoped . private_messages_for_user ( @user )
end
2020-09-14 12:07:35 +01:00
result = result . merge ( all_listable_topics . or ( all_pm_topics ))
else
result = result . merge ( all_listable_topics )
end
2018-03-13 15:59:12 -04:00
2014-02-11 10:06:20 +11:00
# Don't include the category topics if excluded
if options [ :no_definitions ]
2014-02-04 15:55:30 -05:00
result = result . where ( "COALESCE(categories.topic_id, 0) <> topics.id" )
end
2013-07-16 21:20:18 +02:00
result = result . limit ( options [ :per_page ] ) unless options [ :limit ] == false
2015-03-23 18:12:37 -04:00
result = result . visible if options [ :visible ]
2013-12-24 00:50:36 +01:00
result =
result . where . not ( topics : { id : options [ :except_topic_ids ] }) . references ( :topics ) if options [
:except_topic_ids
]
2016-05-09 16:33:55 -04:00
if options [ :page ]
offset = options [ :page ]. to_i * options [ :per_page ]
2016-05-11 13:39:21 -04:00
result = result . offset ( offset ) if offset > 0
2016-05-09 16:33:55 -04:00
end
2013-07-16 21:20:18 +02:00
if options [ :topic_ids ]
2013-08-16 14:53:40 +02:00
result = result . where ( "topics.id in (?)" , options [ :topic_ids ] ) . references ( :topics )
2013-05-28 17:52:52 +10:00
end
2020-09-10 15:49:11 +01:00
if search = options [ :search ]. presence
2018-02-20 20:47:44 +11:00
result =
result . where (
"topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{ Search . ts_query ( term : search . to_s ) } )" ,
)
2014-05-16 00:31:45 +10:00
end
2014-07-17 09:29:09 +10:00
# NOTE protect against SYM attack can be removed with Ruby 2.2
#
state = options [ :state ]
if @user && state && TopicUser . notification_levels . keys . map ( & :to_s ) . include? ( state )
level = TopicUser . notification_levels [ state . to_sym ]
result =
result . where (
"topics.id IN (
SELECT topic_id
FROM topic_users
WHERE user_id = ? AND
notification_level = ?)" ,
@user . id ,
level ,
)
end
2017-03-02 14:54:26 -05:00
if before = options [ :before ]
if ( before = before . to_i ) > 0
result = result . where ( "topics.created_at < ?" , before . to_i . days . ago )
end
2018-06-07 13:28:18 +08:00
end
2017-03-02 14:54:26 -05:00
2017-03-02 15:11:38 -05:00
if bumped_before = options [ :bumped_before ]
if ( bumped_before = bumped_before . to_i ) > 0
result = result . where ( "topics.bumped_at < ?" , bumped_before . to_i . days . ago )
end
2018-06-07 13:28:18 +08:00
end
2017-03-02 15:11:38 -05:00
2014-01-13 14:40:21 +11:00
if status = options [ :status ]
2023-03-03 09:46:21 +08:00
result =
2023-03-30 09:00:42 +08:00
TopicsFilter . new ( scope : result , guardian : @guardian ) . filter_status (
status : options [ :status ] ,
2023-03-03 09:46:21 +08:00
category_id : options [ :category ] ,
2023-03-30 09:00:42 +08:00
)
2018-06-07 13:28:18 +08:00
end
2014-01-13 14:40:21 +11:00
2020-08-06 16:33:45 +10:00
if ( filter = ( options [ :filter ] || options [ :f ] )) && @user
2015-01-07 18:20:10 +11:00
action = ( PostActionType . types [ :like ] if filter == "liked" )
if action
result =
result . where (
"topics.id IN (SELECT pp.topic_id
2015-01-07 13:58:34 +11:00
FROM post_actions pa
JOIN posts pp ON pp.id = pa.post_id
WHERE pa.user_id = :user_id AND
2015-01-07 18:20:10 +11:00
pa.post_action_type_id = :action AND
2015-01-07 13:58:34 +11:00
pa.deleted_at IS NULL
)" ,
user_id : @user . id ,
2015-01-07 18:20:10 +11:00
action : action ,
2018-06-07 13:28:18 +08:00
)
2015-01-07 13:58:34 +11:00
end
2020-07-23 10:30:08 +10:00
2020-09-25 12:39:37 -07:00
result = TopicQuery . tracked_filter ( result , @user . id ) if filter == "tracked"
2018-06-07 13:28:18 +08:00
end
2015-01-07 13:58:34 +11:00
2014-06-05 15:30:24 +02:00
result = result . where ( "topics.posts_count <= ?" , options [ :max_posts ] ) if options [
:max_posts
]. present?
result = result . where ( "topics.posts_count >= ?" , options [ :min_posts ] ) if options [
:min_posts
]. present?
2017-02-15 15:25:43 -05:00
result = TopicQuery . apply_custom_filters ( result , self )
2020-09-14 12:07:35 +01:00
result
2013-02-05 14:16:51 -05:00
end
2021-08-10 18:30:34 +04:00
def remove_muted ( list , user , options )
2023-04-11 08:48:07 +08:00
if options && ( options [ :include_muted ]. nil? || options [ :include_muted ] ) &&
options [ :state ] != "muted"
list = remove_muted_topics ( list , user )
end
2021-08-10 18:30:34 +04:00
list = remove_muted_categories ( list , user , exclude : options [ :category ] )
2021-11-16 12:10:50 +05:30
TopicQuery . remove_muted_tags ( list , user , options )
2021-08-10 18:30:34 +04:00
end
2015-11-02 09:20:22 +11:00
def remove_muted_topics ( list , user )
if user
2015-11-02 14:59:10 +11:00
list =
list . where (
"COALESCE(tu.notification_level,1) > :muted" ,
muted : TopicUser . notification_levels [ :muted ] ,
)
2015-11-02 09:20:22 +11:00
end
list
2018-06-07 13:28:18 +08:00
end
2019-09-06 18:12:13 +02:00
2014-07-29 14:34:54 +10:00
def remove_muted_categories ( list , user , opts = nil )
2014-06-18 11:23:31 +10:00
category_id = get_category_id ( opts [ :exclude ] ) if opts
2018-06-07 13:28:18 +08:00
2019-11-19 08:04:24 +05:30
if user
2023-06-27 14:49:34 +10:00
watched_tag_ids =
2023-07-04 15:08:29 +10:00
if user . watched_precedence_over_muted
2023-06-27 14:49:34 +10:00
TagUser
. where ( user : user )
. where ( "notification_level >= ?" , TopicUser . notification_levels [ :watching ] )
. pluck ( :tag_id )
else
[]
end
# OR watched_topic_tags.id IS NOT NULL",
2019-11-14 11:16:13 +11:00
list =
2023-06-27 14:49:34 +10:00
list . references ( "cu" ) . joins (
"LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{ user . id } " ,
)
if watched_tag_ids . present?
list =
list . joins (
"LEFT JOIN topic_tags watched_topic_tags ON watched_topic_tags.topic_id = topics.id AND #{ DB . sql_fragment ( "watched_topic_tags.tag_id IN (?)" , watched_tag_ids ) } " ,
2019-11-14 11:16:13 +11:00
)
2023-06-27 14:49:34 +10:00
end
list =
list . where (
"topics.category_id = :category_id
2022-02-17 10:42:02 +11:00
OR
(COALESCE(category_users.notification_level, :default) <> :muted AND (topics.category_id IS NULL OR topics.category_id NOT IN(:indirectly_muted_category_ids)))
2023-06-27 14:49:34 +10:00
#{ watched_tag_ids . present? ? "OR watched_topic_tags.id IS NOT NULL" : "" }
2019-11-19 08:04:24 +05:30
OR tu.notification_level > :regular" ,
2023-06-27 14:49:34 +10:00
category_id : category_id || - 1 ,
default : CategoryUser . default_notification_level ,
indirectly_muted_category_ids :
CategoryUser . indirectly_muted_category_ids ( user ) . presence || [- 1 ] ,
muted : CategoryUser . notification_levels [ :muted ] ,
regular : TopicUser . notification_levels [ :regular ] ,
)
2019-11-19 08:04:24 +05:30
elsif SiteSetting . mute_all_categories_by_default
category_ids = [
SiteSetting . default_categories_watching . split ( "|" ),
SiteSetting . default_categories_tracking . split ( "|" ),
2020-08-20 00:35:04 +05:30
SiteSetting . default_categories_watching_first_post . split ( "|" ),
2022-06-20 06:49:33 +03:00
SiteSetting . default_categories_normal . split ( "|" ),
2019-11-19 08:04:24 +05:30
]. flatten . map ( & :to_i )
category_ids << category_id if category_id . present? && category_ids . exclude? ( category_id )
2023-03-22 23:31:33 +02:00
list = list . where ( "categories.id IN (?)" , category_ids ) if category_ids . present?
2019-11-19 07:48:16 +05:30
else
category_ids = SiteSetting . default_categories_muted . split ( "|" ) . map ( & :to_i )
category_ids -= [ category_id ] if category_id . present? && category_ids . include? ( category_id )
2023-03-22 23:31:33 +02:00
list = list . where ( "categories.id NOT IN (?)" , category_ids ) if category_ids . present?
2015-11-02 09:20:22 +11:00
end
2014-02-03 16:05:49 +11:00
2018-06-07 13:28:18 +08:00
list
end
2019-09-06 18:12:13 +02:00
2021-11-16 12:10:50 +05:30
def self . remove_muted_tags ( list , user , opts = {})
2020-08-20 10:40:03 +05:30
if ! SiteSetting . tagging_enabled || SiteSetting . remove_muted_tags_from_latest == "never"
2019-05-27 19:44:24 +03:00
return list
end
2020-08-20 10:40:03 +05:30
muted_tag_ids = []
if user . present?
muted_tag_ids = TagUser . lookup ( user , :muted ) . pluck ( :tag_id )
else
2020-08-26 23:05:29 +05:30
muted_tag_names = SiteSetting . default_tags_muted . split ( "|" )
2022-12-23 04:29:17 +08:00
muted_tag_ids = Tag . where ( name : muted_tag_names ) . pluck ( :id ) if muted_tag_names . present?
2020-08-20 10:40:03 +05:30
end
2019-05-27 19:44:24 +03:00
return list if muted_tag_ids . blank?
# if viewing the topic list for a muted tag, show all the topics
2019-09-06 18:12:13 +02:00
if ! opts [ :no_tags ] && opts [ :tags ]. present?
2020-07-20 11:01:29 +01:00
if TagUser
. lookup ( user , :muted )
. joins ( :tag )
. where ( "lower(tags.name) = ?" , opts [ :tags ]. first . downcase )
. exists?
return list
2023-01-09 12:10:19 +00:00
end
2019-05-27 19:44:24 +03:00
end
2023-07-04 15:08:29 +10:00
query_params = { tag_ids : muted_tag_ids }
if user && ! opts [ :skip_categories ]
query_params [ :regular ] = CategoryUser . notification_levels [ :regular ]
query_params [ :watching_or_infinite ] = if user . watched_precedence_over_muted ||
SiteSetting . watched_precedence_over_muted
CategoryUser . notification_levels [ :watching ]
else
99
end
end
2019-06-03 05:23:23 +03:00
if SiteSetting . remove_muted_tags_from_latest == "always"
2019-05-27 19:44:24 +03:00
list =
list . where (
"
NOT EXISTS(
SELECT 1
FROM topic_tags tt
WHERE tt.tag_id IN (:tag_ids)
2023-06-27 14:49:34 +10:00
AND tt.topic_id = topics.id
#{ user && ! opts [ :skip_categories ] ? "AND COALESCE(category_users.notification_level, :regular) < :watching_or_infinite" : "" } )" ,
2023-07-04 15:08:29 +10:00
query_params ,
2019-05-27 19:44:24 +03:00
)
else
list =
list . where (
"
EXISTS (
SELECT 1
FROM topic_tags tt
2023-06-27 14:49:34 +10:00
WHERE (tt.tag_id NOT IN (:tag_ids)
AND tt.topic_id = topics.id)
#{ user && ! opts [ :skip_categories ] ? "OR COALESCE(category_users.notification_level, :regular) >= :watching_or_infinite" : "" }
2019-05-27 19:44:24 +03:00
) OR NOT EXISTS (SELECT 1 FROM topic_tags tt WHERE tt.topic_id = topics.id)" ,
2023-07-04 15:08:29 +10:00
query_params ,
2019-05-27 19:44:24 +03:00
)
2016-04-25 15:55:15 -04:00
end
2018-06-07 13:28:18 +08:00
end
2014-02-03 16:05:49 +11:00
2021-02-04 11:27:34 +11:00
def remove_dismissed ( list , user )
2019-11-14 11:16:13 +11:00
if user
2021-07-30 17:00:48 +08:00
list . joins ( <<~ SQL ) . where ( "dismissed_topic_users.id IS NULL" )
LEFT JOIN dismissed_topic_users
ON dismissed_topic_users . topic_id = topics . id
AND dismissed_topic_users . user_id = #{user.id.to_i}
SQL
else
list
2019-11-14 11:16:13 +11:00
end
end
2016-02-03 18:50:05 +11:00
def new_messages ( params )
2021-06-02 09:06:29 +10:00
TopicQuery . new_filter (
messages_for_groups_or_user ( params [ :my_group_ids ] ),
treat_as_new_topic_start_date : Time . at ( SiteSetting . min_new_topics_time ) . to_datetime ,
2016-02-03 18:50:05 +11:00
) . limit ( params [ :count ] )
end
def unread_messages ( params )
2018-10-29 16:09:58 +11:00
query =
TopicQuery . unread_filter (
2017-09-15 22:45:01 +08:00
messages_for_groups_or_user ( params [ :my_group_ids ] ),
2022-06-30 10:18:12 +10:00
whisperer : @user . whisperer? ,
2020-09-03 14:02:15 +08:00
)
first_unread_pm_at =
if params [ :my_group_ids ]. present?
GroupUser . where ( user_id : @user . id , group_id : params [ :my_group_ids ] ) . minimum (
:first_unread_pm_at ,
)
else
2023-02-13 12:39:45 +08:00
UserStat . where ( user_id : @user . id ) . pick ( :first_unread_pm_at )
2020-09-03 14:02:15 +08:00
end
query = query . where ( "topics.updated_at >= ?" , first_unread_pm_at ) if first_unread_pm_at
query = query . limit ( params [ :count ] ) if params [ :count ]
2018-10-29 16:09:58 +11:00
query
2017-09-14 11:12:59 +08:00
end
2017-09-15 22:21:05 +08:00
def related_messages_user ( params )
2017-09-15 22:45:01 +08:00
messages = messages_for_user . limit ( params [ :count ] )
messages = allowed_messages ( messages , params )
end
def related_messages_group ( params )
2017-05-25 15:07:12 -04:00
messages = messages_for_groups_or_user ( params [ :my_group_ids ] ) . limit ( params [ :count ] )
2017-09-15 22:45:01 +08:00
messages = allowed_messages ( messages , params )
2018-06-07 13:28:18 +08:00
end
2017-09-15 22:45:01 +08:00
def allowed_messages ( messages , params )
user_ids = ( params [ :target_user_ids ] || [] )
group_ids = (( params [ :target_group_ids ] - params [ :my_group_ids ] ) || [] )
2017-09-14 11:12:59 +08:00
2017-09-15 22:45:01 +08:00
if user_ids . present?
messages =
messages . joins (
"
LEFT JOIN topic_allowed_users ta2
ON topics.id = ta2.topic_id
2020-12-11 10:56:26 +11:00
AND #{ DB . sql_fragment ( "ta2.user_id IN (?)" , user_ids ) }
2017-09-15 22:45:01 +08:00
" ,
)
end
if group_ids . present?
messages =
messages . joins (
"
LEFT JOIN topic_allowed_groups tg2
ON topics.id = tg2.topic_id
2020-12-11 10:56:26 +11:00
AND #{ DB . sql_fragment ( "tg2.group_id IN (?)" , group_ids ) }
2017-09-15 22:45:01 +08:00
" ,
)
2016-02-03 18:50:05 +11:00
end
messages =
if user_ids . present? && group_ids . present?
messages . where ( "ta2.topic_id IS NOT NULL OR tg2.topic_id IS NOT NULL" )
elsif user_ids . present?
messages . where ( "ta2.topic_id IS NOT NULL" )
elsif group_ids . present?
2017-09-15 22:45:01 +08:00
messages . where ( "tg2.topic_id IS NOT NULL" )
2016-02-03 18:50:05 +11:00
end
end
def messages_for_groups_or_user ( group_ids )
2017-09-15 22:45:01 +08:00
if group_ids . present?
2017-09-13 22:22:33 +08:00
base_messages . joins (
"
2017-09-15 22:45:01 +08:00
LEFT JOIN (
2017-09-13 22:22:33 +08:00
SELECT * FROM topic_allowed_groups _tg
LEFT JOIN group_users gu
ON gu.user_id = #{ @user . id . to_i }
AND gu.group_id = _tg.group_id
2020-12-11 10:56:26 +11:00
WHERE #{ DB . sql_fragment ( "gu.group_id IN (?)" , group_ids ) }
2017-09-13 22:22:33 +08:00
) tg ON topics.id = tg.topic_id
" ,
) . where ( "tg.topic_id IS NOT NULL" )
2018-06-07 13:28:18 +08:00
else
2016-02-03 18:50:05 +11:00
messages_for_user
end
2018-06-07 13:28:18 +08:00
end
2016-02-03 18:50:05 +11:00
def messages_for_user
base_messages . joins (
"
LEFT JOIN topic_allowed_users ta
ON topics.id = ta.topic_id
AND ta.user_id = #{ @user . id . to_i }
" ,
2017-05-22 18:05:38 +08:00
) . where ( "ta.topic_id IS NOT NULL" )
2016-02-03 18:50:05 +11:00
end
2014-02-03 16:05:49 +11:00
2014-01-28 18:15:36 -05:00
def base_messages
query =
2014-02-04 12:26:38 -05:00
Topic . where ( "topics.archetype = ?" , Archetype . private_message ) . joins (
"LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{ @user . id . to_i } " ,
)
2018-01-15 15:32:25 +11:00
query = query . includes ( :tags ) if SiteSetting . tagging_enabled
2014-01-28 18:15:36 -05:00
query . order ( "topics.bumped_at DESC" )
end
2013-02-05 14:16:51 -05:00
2014-07-29 14:34:54 +10:00
def random_suggested ( topic , count , excluded_topic_ids = [] )
result = default_results ( unordered : true , per_page : count ) . where ( closed : false , archived : false )
2013-07-12 14:38:20 -04:00
if SiteSetting . limit_suggested_to_category
2013-07-18 14:47:59 -04:00
excluded_topic_ids += Category . where ( id : topic . category_id ) . pluck ( :id )
else
excluded_topic_ids += Category . topic_ids . to_a
end
result =
result . where ( "topics.id NOT IN (?)" , excluded_topic_ids ) unless excluded_topic_ids . empty?
2013-02-27 18:30:14 -05:00
2018-01-15 15:32:25 +11:00
result = remove_muted_categories ( result , @user )
2020-11-24 09:16:10 -03:00
result = remove_muted_topics ( result , @user )
2015-02-25 17:19:12 +11:00
2015-03-03 10:20:42 +11:00
# If we are in a category, prefer it for the random results
2013-07-16 21:20:18 +02:00
if topic . category_id
2015-03-03 10:20:42 +11:00
result =
result . order ( "CASE WHEN topics.category_id = #{ topic . category_id . to_i } THEN 0 ELSE 1 END" )
2013-02-05 14:16:51 -05:00
end
2013-08-08 13:18:52 -04:00
# Best effort, it over selects, however if you have a high number
# of muted categories there is tiny chance we will not select enough
# in particular this can happen if current category is empty and tons
# of muted, big edge case
2018-06-07 13:28:18 +08:00
#
2013-08-08 13:18:52 -04:00
# we over select in case cache is stale
max = ( count * 1 . 3 ) . to_i
ids = SiteSetting . limit_suggested_to_category ? [] : RandomTopicSelector . next ( max )
ids . concat ( RandomTopicSelector . next ( max , topic . category ))
2018-06-07 13:28:18 +08:00
2013-08-08 13:18:52 -04:00
result . where ( id : ids . uniq )
end
2015-02-23 16:50:52 +11:00
def suggested_ordering ( result , options )
2013-11-14 15:50:36 -05:00
# Prefer unread in the same category
2013-08-08 13:18:52 -04:00
if options [ :topic ] && options [ :topic ]. category_id
2015-02-23 16:50:52 +11:00
result =
result . order (
"CASE WHEN topics.category_id = #{ options [ :topic ]. category_id . to_i } THEN 0 ELSE 1 END" ,
)
2013-08-08 13:18:52 -04:00
end
2017-09-15 22:45:01 +08:00
2018-03-22 16:38:53 -04:00
result . order ( "topics.bumped_at DESC" )
2018-06-07 13:28:18 +08:00
end
2021-08-10 18:30:34 +04:00
private
2022-06-30 10:18:12 +10:00
def unseen_filter ( list , user_first_seen_at , whisperer )
2021-08-10 18:30:34 +04:00
list = list . where ( "topics.bumped_at >= ?" , user_first_seen_at )
2022-06-30 10:18:12 +10:00
col_name = whisperer ? "highest_staff_post_number" : "highest_post_number"
2021-08-10 18:30:34 +04:00
list . where ( "tu.last_read_post_number IS NULL OR tu.last_read_post_number < topics. #{ col_name } " )
end
2023-02-16 16:02:09 +03:00
def apply_max_age_limit ( results , options )
if @user
# micro optimisation so we don't load up all of user stats which we do not need
unread_at =
DB . query_single ( "select first_unread_at from user_stats where user_id = ?" , @user . id ) . first
if max_age = options [ :max_age ]
max_age_date = max_age . days . ago
unread_at ||= max_age_date
unread_at = unread_at > max_age_date ? unread_at : max_age_date
end
# perf note, in the past we tried doing this in a subquery but performance was
# terrible, also tried with a join and it was bad
results = results . where ( "topics.updated_at >= ?" , unread_at )
end
results
end
2023-03-24 11:17:55 +08:00
def filter_by_tags ( result )
tags_arg = @options [ :tags ]
if tags_arg && tags_arg . size > 0
tags_arg = tags_arg . split if String === tags_arg
tags_query = tags_arg [ 0 ]. is_a? ( String ) ? Tag . where_name ( tags_arg ) : Tag . where ( id : tags_arg )
tags = tags_query . select ( :id , :target_tag_id ) . map { | t | t . target_tag_id || t . id } . uniq
if ActiveModel :: Type :: Boolean . new . cast ( @options [ :match_all_tags ] )
# ALL of the given tags:
if tags_arg . length == tags . length
tags . each_with_index do | tag , index |
sql_alias = [ "t" , index ]. join
result =
result . joins (
"INNER JOIN topic_tags #{ sql_alias } ON #{ sql_alias } .topic_id = topics.id AND #{ sql_alias } .tag_id = #{ tag } " ,
)
end
else
result = result . none # don't return any results unless all tags exist in the database
end
else
# ANY of the given tags:
result = result . joins ( :tags ) . where ( "tags.id in (?)" , tags )
end
# TODO: this is very side-effecty and should be changed
# It is done cause further up we expect normalized tags
@options [ :tags ] = tags
elsif @options [ :no_tags ]
# the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags"))
result = result . where . not ( id : TopicTag . distinct . select ( :topic_id ))
end
if @options [ :exclude_tag ]. present? &&
! DiscourseTagging . hidden_tag_names ( @guardian ) . include? ( @options [ :exclude_tag ] )
result = result . where ( <<~ SQL , name : @options [ :exclude_tag ] )
topics . id NOT IN (
SELECT topic_tags . topic_id
FROM topic_tags
INNER JOIN tags ON tags . id = topic_tags . tag_id
WHERE tags . name = :name
)
SQL
end
result
end
2013-02-05 14:16:51 -05:00
end