class TopicUser < ActiveRecord::Base belongs_to :user belongs_to :topic module NotificationLevel WATCHING = 3 TRACKING = 2 REGULAR = 1 MUTED = 0 end module NotificationReasons CREATED_TOPIC = 1 USER_CHANGED = 2 USER_INTERACTED = 3 CREATED_POST = 4 end def self.auto_track(user_id, topic_id, reason) if exec_sql("select 1 from topic_users where user_id = ? and topic_id = ? and notifications_reason_id is null", user_id, topic_id).count == 1 self.change(user_id, topic_id, notification_level: NotificationLevel::TRACKING, notifications_reason_id: reason ) MessageBus.publish("/topic/#{topic_id}", { notification_level_change: NotificationLevel::TRACKING, notifications_reason_id: reason }, user_ids: [user_id]) end end # Find the information specific to a user in a forum topic def self.lookup_for(user, topics) # If the user isn't logged in, there's no last read posts return {} if user.blank? return {} if topics.blank? topic_ids = topics.map {|ft| ft.id} create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id)) end def self.create_lookup(topic_users) topic_users = topic_users.to_a result = {} return result if topic_users.blank? topic_users.each do |ftu| result[ftu.topic_id] = ftu end result end def self.get(topic,user) if Topic === topic topic = topic.id end if User === user user = user.id end TopicUser.where('topic_id = ? and user_id = ?', topic, user).first end # 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 self.change(user_id, topic_id, attrs) # Sometimes people pass objs instead of the ids. We can handle that. topic_id = topic_id.id if topic_id.is_a?(Topic) user_id = user_id.id if user_id.is_a?(User) TopicUser.transaction do attrs = attrs.dup attrs[:starred_at] = DateTime.now if attrs[:starred_at].nil? && attrs[:starred] if attrs[:notification_level] attrs[:notifications_changed_at] ||= DateTime.now attrs[:notifications_reason_id] ||= TopicUser::NotificationReasons::USER_CHANGED end attrs_array = attrs.to_a attrs_sql = attrs_array.map {|t| "#{t[0]} = ?"}.join(", ") vals = attrs_array.map {|t| t[1]} rows = TopicUser.update_all([attrs_sql, *vals], ["topic_id = ? and user_id = ?", topic_id.to_i, user_id]) if rows == 0 now = DateTime.now auto_track_after = self.exec_sql("select auto_track_topics_after_msecs from users where id = ?", user_id).values[0][0] auto_track_after ||= SiteSetting.auto_track_topics_after auto_track_after = auto_track_after.to_i if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0) attrs[:notification_level] ||= TopicUser::NotificationLevel::TRACKING end TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id.to_i, first_visited_at: now ,last_visited_at: now)) end end rescue ActiveRecord::RecordNotUnique # In case of a race condition to insert, do nothing end def self.track_visit!(topic,user) now = DateTime.now rows = exec_sql_row_count( "update topic_users set last_visited_at=? where topic_id=? and user_id=?", now, topic.id, user.id ) if rows == 0 exec_sql('insert into topic_users(topic_id, user_id, last_visited_at, first_visited_at) values(?,?,?,?)', topic.id, user.id, now, now) end end # 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 def self.update_last_read(user, topic_id, post_number, msecs) 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: TopicUser::NotificationLevel::TRACKING, threshold: SiteSetting.auto_track_topics_after } rows = exec_sql("UPDATE topic_users SET last_read_post_number = greatest(:post_number, tu.last_read_post_number), seen_post_count = t.highest_post_number, total_msecs_viewed = tu.total_msecs_viewed + :msecs, notification_level = case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > coalesce(u.auto_track_topics_after_msecs,:threshold) and coalesce(u.auto_track_topics_after_msecs, :threshold) >= 0 then :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 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 topic_users.notification_level, tu.notification_level old_level ", args).values if rows.length == 1 before = rows[0][1].to_i after = rows[0][0].to_i if before != after MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id]) end end if rows.length == 0 self args[:tracking] = TopicUser::NotificationLevel::TRACKING args[:regular] = TopicUser::NotificationLevel::REGULAR args[:site_setting] = SiteSetting.auto_track_topics_after exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, seen_post_count, last_visited_at, first_visited_at, notification_level) SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, case when coalesce(u.auto_track_topics_after_msecs, :site_setting) = 0 then :tracking else :regular end 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) end end end