2013-02-05 14:16:51 -05:00
class TopicUser < ActiveRecord :: Base
belongs_to :user
belongs_to :topic
2013-02-07 16:45:24 +01:00
2015-01-07 18:20:10 +11:00
# used for serialization
attr_accessor :post_action_data
2013-05-29 18:11:04 +10:00
scope :tracking , lambda { | topic_id |
where ( topic_id : topic_id )
2015-08-12 23:00:16 +02:00
. where ( " COALESCE(topic_users.notification_level, :regular) >= :tracking " ,
regular : TopicUser . notification_levels [ :regular ] ,
tracking : TopicUser . notification_levels [ :tracking ] )
2013-05-29 18:11:04 +10:00
}
2013-03-06 15:17:07 -05:00
# Class methods
class << self
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
# Enums
def notification_levels
2016-01-08 16:23:52 +05:30
@notification_levels || = Enum . new ( muted : 0 ,
regular : 1 ,
tracking : 2 ,
watching : 3 )
2013-03-06 15:17:07 -05:00
end
2013-02-07 16:45:24 +01:00
2013-03-06 15:17:07 -05:00
def notification_reasons
2016-01-08 16:23:52 +05:30
@notification_reasons || = Enum . new ( created_topic : 1 ,
user_changed : 2 ,
user_interacted : 3 ,
created_post : 4 ,
auto_watch : 5 ,
auto_watch_category : 6 ,
auto_mute_category : 7 ,
auto_track_category : 8 ,
plugin_changed : 9 )
2013-03-06 15:17:07 -05:00
end
def auto_track ( user_id , topic_id , reason )
if TopicUser . where ( user_id : user_id , topic_id : topic_id , notifications_reason_id : nil ) . exists?
change ( user_id , topic_id ,
notification_level : notification_levels [ :tracking ] ,
2013-02-05 14:16:51 -05:00
notifications_reason_id : reason
2013-03-06 15:17:07 -05:00
)
2013-02-05 14:16:51 -05:00
2015-05-04 12:21:00 +10:00
MessageBus . publish ( " /topic/ #{ topic_id } " , {
2013-03-06 15:17:07 -05:00
notification_level_change : notification_levels [ :tracking ] ,
notifications_reason_id : reason
} , user_ids : [ user_id ] )
end
2013-02-05 14:16:51 -05:00
end
2015-11-18 22:24:46 +01:00
def auto_watch ( user_id , topic_id )
topic_user = TopicUser . find_or_initialize_by ( user_id : user_id , topic_id : topic_id )
topic_user . notification_level = notification_levels [ :watching ]
topic_user . notifications_reason_id = notification_reasons [ :auto_watch ]
topic_user . save
end
2013-03-06 15:17:07 -05:00
# Find the information specific to a user in a forum topic
def lookup_for ( user , topics )
# If the user isn't logged in, there's no last read posts
return { } if user . blank? || topics . blank?
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
topic_ids = topics . map ( & :id )
create_lookup ( TopicUser . where ( topic_id : topic_ids , user_id : user . id ) )
end
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
def create_lookup ( topic_users )
topic_users = topic_users . to_a
result = { }
return result if topic_users . blank?
2015-08-12 23:00:16 +02:00
topic_users . each { | ftu | result [ ftu . topic_id ] = ftu }
2013-03-06 15:17:07 -05:00
result
2013-02-05 14:16:51 -05:00
end
2014-03-26 12:20:41 -07:00
def get ( topic , user )
topic = topic . id if topic . is_a? ( Topic )
user = user . id if user . is_a? ( User )
TopicUser . find_by ( topic_id : topic , user_id : user )
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
# Change attributes for a user (creates a record when none is present). First it tries an update
# since there's more likely to be an existing record than not. If the update returns 0 rows affected
# it then creates the row instead.
def change ( user_id , topic_id , attrs )
# Sometimes people pass objs instead of the ids. We can handle that.
2013-06-28 12:18:04 +10:00
topic_id = topic_id . id if topic_id . is_a? ( :: Topic )
user_id = user_id . id if user_id . is_a? ( :: User )
topic_id = topic_id . to_i
user_id = user_id . to_i
2013-03-06 15:17:07 -05:00
TopicUser . transaction do
attrs = attrs . dup
if attrs [ :notification_level ]
attrs [ :notifications_changed_at ] || = DateTime . now
attrs [ :notifications_reason_id ] || = TopicUser . notification_reasons [ :user_changed ]
end
attrs_array = attrs . to_a
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
attrs_sql = attrs_array . map { | t | " #{ t [ 0 ] } = ? " } . join ( " , " )
vals = attrs_array . map { | t | t [ 1 ] }
2013-07-01 20:45:52 +02:00
rows = TopicUser . where ( topic_id : topic_id , user_id : user_id ) . update_all ( [ attrs_sql , * vals ] )
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
if rows == 0
now = DateTime . now
2016-02-18 16:57:22 +11:00
auto_track_after = UserOption . where ( user_id : user_id ) . pluck ( :auto_track_topics_after_msecs ) . first
2015-08-21 20:39:21 +02:00
auto_track_after || = SiteSetting . default_other_auto_track_topics_after_msecs
2013-02-07 16:45:24 +01:00
2015-09-18 11:27:56 +10:00
if auto_track_after > = 0 && auto_track_after < = ( attrs [ :total_msecs_viewed ] . to_i || 0 )
2013-03-06 15:17:07 -05:00
attrs [ :notification_level ] || = notification_levels [ :tracking ]
end
2013-02-05 14:16:51 -05:00
2013-06-28 12:18:04 +10:00
TopicUser . create ( attrs . merge! ( user_id : user_id , topic_id : topic_id , first_visited_at : now , last_visited_at : now ) )
2013-05-21 20:43:43 -07:00
else
observe_after_save_callbacks_for topic_id , user_id
2013-02-05 14:16:51 -05:00
end
end
2014-06-25 09:45:12 +10:00
if attrs [ :notification_level ]
2015-08-12 23:00:16 +02:00
MessageBus . publish ( " /topic/ #{ topic_id } " , { notification_level_change : attrs [ :notification_level ] } , user_ids : [ user_id ] )
2014-06-25 09:45:12 +10:00
end
2013-03-06 15:17:07 -05:00
rescue ActiveRecord :: RecordNotUnique
# In case of a race condition to insert, do nothing
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
def track_visit! ( topic , user )
2014-03-26 12:20:41 -07:00
topic_id = topic . is_a? ( Topic ) ? topic . id : topic
user_id = user . is_a? ( User ) ? user . id : topic
2013-10-04 17:00:23 +10:00
2013-03-06 15:17:07 -05:00
now = DateTime . now
2015-08-12 23:00:16 +02:00
rows = TopicUser . where ( topic_id : topic_id , user_id : user_id ) . update_all ( last_visited_at : now )
2013-03-06 15:17:07 -05:00
if rows == 0
2013-10-04 17:00:23 +10:00
TopicUser . create ( topic_id : topic_id , user_id : user_id , last_visited_at : now , first_visited_at : now )
2013-05-21 20:45:03 -07:00
else
2013-10-04 17:00:23 +10:00
observe_after_save_callbacks_for topic_id , user_id
2013-03-06 15:17:07 -05:00
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
# Update the last read and the last seen post count, but only if it doesn't exist.
# This would be a lot easier if psql supported some kind of upsert
2016-02-18 16:57:22 +11:00
UPDATE_TOPIC_USER_SQL = " UPDATE topic_users
2013-03-06 15:17:07 -05:00
SET
2014-09-12 16:59:25 +10:00
last_read_post_number = GREATEST ( :post_number , tu . last_read_post_number ) ,
2014-10-31 09:40:35 +11:00
highest_seen_post_number = t . highest_post_number ,
2014-09-12 16:59:25 +10:00
total_msecs_viewed = LEAST ( tu . total_msecs_viewed + :msecs , 86400000 ) ,
2013-03-06 15:17:07 -05:00
notification_level =
case when tu . notifications_reason_id is null and ( tu . total_msecs_viewed + :msecs ) >
2016-02-18 16:57:22 +11:00
coalesce ( uo . auto_track_topics_after_msecs , :threshold ) and
coalesce ( uo . auto_track_topics_after_msecs , :threshold ) > = 0 then
2013-03-06 15:17:07 -05:00
:tracking
else
tu . notification_level
end
FROM topic_users tu
join topics t on t . id = tu . topic_id
join users u on u . id = :user_id
2016-02-18 16:57:22 +11:00
join user_options uo on uo . user_id = :user_id
2013-03-06 15:17:07 -05:00
WHERE
tu . topic_id = topic_users . topic_id AND
tu . user_id = topic_users . user_id AND
tu . topic_id = :topic_id AND
tu . user_id = :user_id
RETURNING
2013-05-30 16:19:12 +10:00
topic_users . notification_level , tu . notification_level old_level , tu . last_read_post_number
2016-02-18 16:57:22 +11:00
"
def update_last_read ( user , topic_id , post_number , msecs , opts = { } )
return if post_number . blank?
msecs = 0 if msecs . to_i < 0
args = {
user_id : user . id ,
topic_id : topic_id ,
post_number : post_number ,
now : DateTime . now ,
msecs : msecs ,
tracking : notification_levels [ :tracking ] ,
threshold : SiteSetting . default_other_auto_track_topics_after_msecs
}
# In case anyone seens "highest_seen_post_number" and gets confused, like I do.
# highest_seen_post_number represents the highest_post_number of the topic when
# the user visited it. It may be out of alignment with last_read, meaning
# ... user visited the topic but did not read the posts
#
# 86400000 = 1 day
rows = exec_sql ( UPDATE_TOPIC_USER_SQL , args ) . values
2013-03-06 15:17:07 -05:00
if rows . length == 1
before = rows [ 0 ] [ 1 ] . to_i
after = rows [ 0 ] [ 0 ] . to_i
2013-05-30 16:19:12 +10:00
before_last_read = rows [ 0 ] [ 2 ] . to_i
if before_last_read < post_number
2014-01-24 15:19:20 -05:00
# The user read at least one new post
2014-02-26 15:37:42 -05:00
TopicTrackingState . publish_read ( topic_id , post_number , user . id , after )
2015-07-07 12:31:07 -04:00
user . update_posts_read! ( post_number - before_last_read , mobile : opts [ :mobile ] )
2013-05-30 16:19:12 +10:00
end
2013-03-06 15:17:07 -05:00
if before != after
2015-08-12 23:00:16 +02:00
MessageBus . publish ( " /topic/ #{ topic_id } " , { notification_level_change : after } , user_ids : [ user . id ] )
2013-03-06 15:17:07 -05:00
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
if rows . length == 0
2014-01-24 15:19:20 -05:00
# The user read at least one post in a topic that they haven't viewed before.
2014-02-26 15:37:42 -05:00
args [ :new_status ] = notification_levels [ :regular ]
2016-02-18 16:57:22 +11:00
if ( user . user_option . auto_track_topics_after_msecs || SiteSetting . default_other_auto_track_topics_after_msecs ) == 0
2014-02-26 15:37:42 -05:00
args [ :new_status ] = notification_levels [ :tracking ]
end
TopicTrackingState . publish_read ( topic_id , post_number , user . id , args [ :new_status ] )
2015-07-07 12:31:07 -04:00
user . update_posts_read! ( post_number , mobile : opts [ :mobile ] )
2013-05-30 16:19:12 +10:00
2014-10-31 09:40:35 +11:00
exec_sql ( " INSERT INTO topic_users (user_id, topic_id, last_read_post_number, highest_seen_post_number, last_visited_at, first_visited_at, notification_level)
2014-02-26 15:37:42 -05:00
SELECT :user_id , :topic_id , :post_number , ft . highest_post_number , :now , :now , :new_status
2013-03-06 15:17:07 -05:00
FROM topics AS ft
JOIN users u on u . id = :user_id
WHERE ft . id = :topic_id
AND NOT EXISTS ( SELECT 1
FROM topic_users AS ftu
WHERE ftu . user_id = :user_id and ftu . topic_id = :topic_id ) " ,
args )
2014-06-25 09:45:12 +10:00
2015-08-12 23:00:16 +02:00
MessageBus . publish ( " /topic/ #{ topic_id } " , { notification_level_change : args [ :new_status ] } , user_ids : [ user . id ] )
2013-03-06 15:17:07 -05:00
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
2013-05-21 20:43:43 -07:00
def observe_after_save_callbacks_for ( topic_id , user_id )
TopicUser . where ( topic_id : topic_id , user_id : user_id ) . each do | topic_user |
UserActionObserver . instance . after_save topic_user
end
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
2015-01-08 14:35:56 +11:00
def self . update_post_action_cache ( opts = { } )
user_id = opts [ :user_id ]
2015-06-18 09:58:32 +10:00
post_id = opts [ :post_id ]
2015-01-08 14:35:56 +11:00
topic_id = opts [ :topic_id ]
action_type = opts [ :post_action_type ]
action_type_name = " liked " if action_type == :like
action_type_name = " bookmarked " if action_type == :bookmark
raise ArgumentError , " action_type " if action_type && ! action_type_name
unless action_type_name
update_post_action_cache ( opts . merge ( post_action_type : :like ) )
update_post_action_cache ( opts . merge ( post_action_type : :bookmark ) )
return
end
builder = SqlBuilder . new <<SQL
UPDATE topic_users tu
SET #{action_type_name} = x.state
FROM (
SELECT CASE WHEN EXISTS (
SELECT 1
FROM post_actions pa
JOIN posts p on p . id = pa . post_id
JOIN topics t ON t . id = p . topic_id
WHERE pa . deleted_at IS NULL AND
p . deleted_at IS NULL AND
t . deleted_at IS NULL AND
pa . post_action_type_id = :action_type_id AND
tu2 . topic_id = t . id AND
tu2 . user_id = pa . user_id
LIMIT 1
) THEN true ELSE false END state , tu2 . topic_id , tu2 . user_id
FROM topic_users tu2
/ *where* /
) x
WHERE x . topic_id = tu . topic_id AND x . user_id = tu . user_id AND x . state != tu . #{action_type_name}
SQL
if user_id
builder . where ( " tu2.user_id = :user_id " , user_id : user_id )
end
if topic_id
builder . where ( " tu2.topic_id = :topic_id " , topic_id : topic_id )
end
2015-06-18 09:58:32 +10:00
if post_id
builder . where ( " tu2.topic_id IN (SELECT topic_id FROM posts WHERE id = :post_id) " , post_id : post_id )
builder . where ( " tu2.user_id IN (SELECT user_id FROM post_actions
WHERE post_id = :post_id AND
post_action_type_id = :action_type_id ) " )
end
2015-01-08 14:35:56 +11:00
builder . exec ( action_type_id : PostActionType . types [ action_type ] )
end
2015-09-07 11:57:50 +10:00
# cap number of unread topics at count, bumping up highest_seen / last_read if needed
def self . cap_unread! ( user_id , count )
sql = <<SQL
UPDATE topic_users tu
SET last_read_post_number = max_number ,
highest_seen_post_number = max_number
FROM (
SELECT MAX ( post_number ) max_number , p . topic_id FROM posts p
WHERE deleted_at IS NULL
GROUP BY p . topic_id
) m
WHERE tu . user_id = :user_id AND
m . topic_id = tu . topic_id AND
tu . topic_id IN (
#{TopicTrackingState.report_raw_sql(skip_new: true, select: "topics.id")}
offset :count
)
SQL
TopicUser . exec_sql ( sql , user_id : user_id , count : count )
end
2013-07-04 11:47:12 +10:00
def self . ensure_consistency! ( topic_id = nil )
2015-02-03 15:59:26 +11:00
update_post_action_cache ( topic_id : topic_id )
2015-01-08 14:35:56 +11:00
2014-08-11 10:26:46 +10:00
# TODO this needs some reworking, when we mark stuff skipped
# we up these numbers so they are not in-sync
# the simple fix is to add a column here, but table is already quite big
# long term we want to split up topic_users and allow for this better
2013-07-04 11:47:12 +10:00
builder = SqlBuilder . new <<SQL
2014-08-11 10:26:46 +10:00
2013-04-05 15:29:46 +11:00
UPDATE topic_users t
SET
2014-08-11 10:26:46 +10:00
last_read_post_number = LEAST ( GREATEST ( last_read , last_read_post_number ) , max_post_number ) ,
2014-10-31 09:40:35 +11:00
highest_seen_post_number = LEAST ( max_post_number , GREATEST ( t . highest_seen_post_number , last_read ) )
2013-04-05 15:29:46 +11:00
FROM (
2013-04-08 11:12:52 +10:00
SELECT topic_id , user_id , MAX ( post_number ) last_read
2013-04-05 15:29:46 +11:00
FROM post_timings
GROUP BY topic_id , user_id
) as X
2013-04-08 13:01:58 +10:00
JOIN (
SELECT p . topic_id , MAX ( p . post_number ) max_post_number from posts p
GROUP BY p . topic_id
) as Y on Y . topic_id = X . topic_id
2013-07-04 11:47:12 +10:00
/ *where* /
2013-04-05 15:29:46 +11:00
SQL
2013-07-04 11:47:12 +10:00
builder . where <<SQL
X . topic_id = t . topic_id AND
X . user_id = t . user_id AND
(
2014-08-11 10:26:46 +10:00
last_read_post_number < > LEAST ( GREATEST ( last_read , last_read_post_number ) , max_post_number ) OR
2014-10-31 09:40:35 +11:00
highest_seen_post_number < > LEAST ( max_post_number , GREATEST ( t . highest_seen_post_number , last_read ) )
2013-07-04 11:47:12 +10:00
)
SQL
if topic_id
builder . where ( " t.topic_id = :topic_id " , topic_id : topic_id )
end
builder . exec
2013-04-05 15:29:46 +11:00
end
2013-02-05 14:16:51 -05:00
end
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: topic_users
#
# user_id :integer not null
# topic_id :integer not null
# posted :boolean default(FALSE), not null
# last_read_post_number :integer
2014-10-31 09:40:35 +11:00
# highest_seen_post_number :integer
2013-05-24 12:48:32 +10:00
# last_visited_at :datetime
# first_visited_at :datetime
# notification_level :integer default(1), not null
# notifications_changed_at :datetime
# notifications_reason_id :integer
# total_msecs_viewed :integer default(0), not null
# cleared_pinned_at :datetime
2013-08-13 22:09:27 +02:00
# id :integer not null, primary key
2014-02-07 11:07:36 +11:00
# last_emailed_post_number :integer
2015-02-04 16:34:25 +11:00
# liked :boolean default(FALSE)
# bookmarked :boolean default(FALSE)
2013-05-24 12:48:32 +10:00
#
# Indexes
#
2014-05-28 11:49:50 +10:00
# index_topic_users_on_topic_id_and_user_id (topic_id,user_id) UNIQUE
2014-11-20 14:53:15 +11:00
# index_topic_users_on_user_id_and_topic_id (user_id,topic_id) UNIQUE
2013-05-24 12:48:32 +10:00
#