summaryrefslogtreecommitdiff
path: root/app/models/discussion.rb
blob: 203e14f1227b5d49335b2f703e751b90abae6075 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# frozen_string_literal: true

# A non-diff discussion on an issue, merge request, commit, or snippet, consisting of `DiscussionNote` notes.
#
# A discussion of this type can be resolvable.
class Discussion
  include GlobalID::Identification
  include ResolvableDiscussion

  # Bump this if we need to refresh the cached versions of discussions
  CACHE_VERSION = 1

  attr_reader :notes, :context_noteable

  delegate  :created_at,
            :project,
            :author,
            :noteable,
            :commit_id,
            :confidential?,
            :for_commit?,
            :for_design?,
            :for_merge_request?,
            :noteable_ability_name,
            :to_ability_name,
            :editable?,
            :resolved_by_id,
            :system_note_with_references_visible_for?,
            :resource_parent,
            :save,
            to: :first_note

  def declarative_policy_delegate
    first_note
  end

  def project_id
    project&.id
  end

  def self.build(notes, context_noteable = nil)
    notes.first.discussion_class(context_noteable).new(notes, context_noteable)
  end

  def self.build_collection(notes, context_noteable = nil)
    grouped_notes = notes.group_by { |n| n.discussion_id(context_noteable) }
    grouped_notes.values.map { |notes| build(notes, context_noteable) }
  end

  def self.lazy_find(discussion_id)
    BatchLoader.for(discussion_id).batch do |discussion_ids, loader|
      results = Note.where(discussion_id: discussion_ids).fresh.to_a.group_by(&:discussion_id)
      results.each do |discussion_id, notes|
        next if notes.empty?

        loader.call(discussion_id, Discussion.build(notes))
      end
    end
  end

  # Returns an alphanumeric discussion ID based on `build_discussion_id`
  def self.discussion_id(note)
    Digest::SHA1.hexdigest(build_discussion_id(note).join("-"))
  end

  # Returns an array of discussion ID components
  def self.build_discussion_id(note)
    [*base_discussion_id(note), SecureRandom.hex]
  end

  def self.base_discussion_id(note)
    noteable_id = note.noteable_id || note.commit_id
    [:discussion, note.noteable_type.try(:underscore), noteable_id]
  end

  # When notes on a commit are displayed in context of a merge request that contains that commit,
  # these notes are to be displayed as if they were part of one discussion, even though they were actually
  # individual notes on the commit with different discussion IDs, so that it's clear that these are not
  # notes on the merge request itself.
  #
  # To turn a list of notes into a list of discussions, they are grouped by discussion ID, so to
  # get these out-of-context notes to end up in the same discussion, we need to get them to return the same
  # `discussion_id` when this grouping happens. To enable this, `Note#discussion_id` calls out
  # to the `override_discussion_id` method on the appropriate `Discussion` subclass, as determined by
  # the `discussion_class` method on `Note` or a subclass of `Note`.
  #
  # If no override is necessary, return `nil`.
  # For the case described above, see `OutOfContextDiscussion.override_discussion_id`.
  def self.override_discussion_id(note)
    nil
  end

  def self.note_class
    DiscussionNote
  end

  def initialize(notes, context_noteable = nil)
    @notes = notes
    @context_noteable = context_noteable
  end

  def on_image?
    false
  end

  def ==(other)
    other.class == self.class &&
      other.context_noteable == self.context_noteable &&
      other.id == self.id &&
      other.notes == self.notes
  end

  def last_updated_at
    last_note.created_at
  end

  def last_updated_by
    last_note.author
  end

  def updated?
    last_updated_at != created_at
  end

  def id
    first_note.discussion_id(context_noteable)
  end

  def reply_id
    # To reply to this discussion, we need the actual discussion_id from the database,
    # not the potentially overwritten one based on the noteable.
    first_note.discussion_id
  end

  alias_method :to_param, :id

  def diff_discussion?
    false
  end

  def individual_note?
    false
  end

  def can_convert_to_discussion?
    false
  end

  def last_note
    @last_note ||= notes.last
  end

  def collapsed?
    resolved?
  end

  def expanded?
    !collapsed?
  end

  def reply_attributes
    first_note.slice(:type, :noteable_type, :noteable_id, :commit_id, :discussion_id)
  end

  def cache_key
    # Need to use the notes' cache key so cache will be invalidated when note
    # within a discussion has been deleted or has different data after post
    # processing of content.
    notes_sha = Digest::SHA1.hexdigest(notes.map(&:post_processed_cache_key).join(':'))

    [
      CACHE_VERSION,
      id,
      notes_sha,
      resolved_at
    ].join(':')
  end
end