loomio/loomio

View on GitHub
app/models/discussion_reader.rb

Summary

Maintainability
A
25 mins
Test Coverage
class DiscussionReader < ApplicationRecord
  include CustomCounterCache::Model
  include HasVolume

  extend HasTokens
  initialized_with_token :token

  belongs_to :user
  belongs_to :discussion
  belongs_to :inviter, class_name: 'User'

  delegate :message_channel, to: :user

  scope :dangling, -> { joins('left join discussions on discussions.id = discussion_id left join users on users.id = user_id').where('discussions.id is null or users.id is null') }

  scope :active, -> { where("discussion_readers.revoked_at IS NULL") }

  scope :guests, -> { active.where('discussion_readers.guest': true) }
  scope :admins, -> { active.where('discussion_readers.admin': true) }

  scope :redeemable, -> { guests.where('discussion_readers.accepted_at IS NULL') }

  scope :redeemable_by, -> (user_id) { redeemable.joins(:user).where("user_id = ? OR users.email_verified = false", user_id) }

  update_counter_cache :discussion, :seen_by_count
  update_counter_cache :discussion, :members_count

  def self.for(user:, discussion:)
    if user&.is_logged_in?
      find_or_initialize_by(user_id: user.id, discussion_id: discussion.id) do |dr|
        m = user.memberships.find_by(group_id: discussion.group_id)
        dr.volume = (m && m.volume) || 'normal'
      end
    else
      new(discussion: discussion)
    end
  end

  def self.for_model(model, actor = nil)
    self.for(user: actor || model.author, discussion: model.discussion)
  end

  def update_reader(ranges: nil, volume: nil, participate: false, dismiss: false)
    viewed!(ranges, persist: false)     if ranges
    set_volume!(volume, persist: false) if volume && (volume != :loud || user.email_on_participation?)
    dismiss!(persist: false)            if dismiss
    save!                               if changed?
    self
  end

  def viewed!(ranges = [], persist: true)
    mark_as_read(ranges) unless has_read?(ranges)
    assign_attributes(last_read_at: Time.now)
    save if persist
  end

  def has_read?(ranges = [])
    RangeSet.includes?(read_ranges, ranges)
  end

  def mark_as_read(ranges)
    ranges = RangeSet.to_ranges(ranges)
    return if ranges.empty?
    self.read_ranges = read_ranges.concat(ranges)
  end

  def dismiss!(persist: true)
    self.dismissed_at = Time.zone.now
    save if persist
  end

  def recall!(persist: true)
    self.dismissed_at = nil
    save if persist
  end

  def computed_volume
    if persisted?
      volume || membership&.volume || 'normal'
    else
      membership.volume
    end
  end

  def discussion_reader_volume
    self[:volume]
  end

  def discussion_reader_user_id
    self.user_id
  end

  def read_ranges
    RangeSet.parse(self.read_ranges_string)
  end

  def read_ranges=(ranges)
    ranges = RangeSet.reduce(ranges)
    self.read_ranges_string = RangeSet.serialize(ranges)
  end

  def first_unread_sequence_id
    Array(unread_ranges.first).first.to_i
  end

  # maybe yagni, because the client should do this locally
  def unread_ranges
    RangeSet.subtract_ranges(discussion.ranges, read_ranges)
  end

  def read_ranges_string
    self[:read_ranges_string] ||= begin
      if last_read_sequence_id == 0
        ""
      else
        "#{[discussion.first_sequence_id, 1].max}-#{last_read_sequence_id}"
      end
    end
  end

  def read_items_count
    RangeSet.length(read_ranges)
  end

  def unread_items_count
    RangeSet.length(unread_ranges)
  end

  private
  def membership
    @membership ||= discussion.group.membership_for(user)
  end
end