mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 08:33:39 +08:00
b79ea986ac
Introduce the concept of "high priority notifications" which include PM and bookmark reminder notifications. Now bookmark reminder notifications act in the same way as PM notifications (float to top of recent list, show in the green bubble) and most instances of unread_private_messages in the UI have been replaced with unread_high_priority_notifications. The user email digest is changed to just have a section about unread high priority notifications, the unread PM section has been removed. A high_priority boolean column has been added to the Notification table and relevant indices added to account for it. unread_private_messages has been kept on the User model purely for backwards compat, but now just returns unread_high_priority_notifications count so this may cause some inconsistencies in the UI.
310 lines
9.1 KiB
Ruby
310 lines
9.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Notification < ActiveRecord::Base
|
|
belongs_to :user
|
|
belongs_to :topic
|
|
|
|
MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS = 24
|
|
|
|
validates_presence_of :data
|
|
validates_presence_of :notification_type
|
|
|
|
scope :unread, lambda { where(read: false) }
|
|
scope :recent, lambda { |n = nil| n ||= 10; order('notifications.created_at desc').limit(n) }
|
|
scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id')
|
|
.where('topics.id IS NULL OR topics.deleted_at IS NULL') }
|
|
|
|
scope :filter_by_consolidation_data, ->(notification_type, data) {
|
|
notifications = where(notification_type: notification_type)
|
|
|
|
case notification_type
|
|
when types[:liked], types[:liked_consolidated]
|
|
key = "display_username"
|
|
consolidation_window = SiteSetting.likes_notification_consolidation_window_mins.minutes.ago
|
|
when types[:private_message]
|
|
key = "topic_title"
|
|
consolidation_window = MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS.hours.ago
|
|
when types[:membership_request_consolidated]
|
|
key = "group_name"
|
|
consolidation_window = MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS.hours.ago
|
|
end
|
|
|
|
notifications = notifications.where("created_at > ? AND data::json ->> '#{key}' = ?", consolidation_window, data[key.to_sym]) if data[key&.to_sym].present?
|
|
notifications = notifications.where("data::json ->> 'username2' IS NULL") if notification_type == types[:liked]
|
|
|
|
notifications
|
|
}
|
|
|
|
attr_accessor :skip_send_email
|
|
|
|
after_commit :refresh_notification_count, on: [:create, :update, :destroy]
|
|
|
|
after_commit(on: :create) do
|
|
DiscourseEvent.trigger(:notification_created, self)
|
|
send_email unless NotificationConsolidator.new(self).consolidate!
|
|
end
|
|
|
|
before_create do
|
|
self.high_priority = Notification.high_priority_types.include?(self.notification_type)
|
|
end
|
|
|
|
def self.purge_old!
|
|
return if SiteSetting.max_notifications_per_user == 0
|
|
|
|
DB.exec(<<~SQL, SiteSetting.max_notifications_per_user)
|
|
DELETE FROM notifications n1
|
|
USING (
|
|
SELECT * FROM (
|
|
SELECT
|
|
user_id,
|
|
id,
|
|
rank() OVER (PARTITION BY user_id ORDER BY id DESC)
|
|
FROM notifications
|
|
) AS X
|
|
WHERE rank = ?
|
|
) n2
|
|
WHERE n1.user_id = n2.user_id AND n1.id < n2.id
|
|
SQL
|
|
end
|
|
|
|
def self.ensure_consistency!
|
|
DB.exec(<<~SQL)
|
|
DELETE
|
|
FROM notifications n
|
|
WHERE high_priority
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM posts p
|
|
JOIN topics t ON t.id = p.topic_id
|
|
WHERE p.deleted_at IS NULL
|
|
AND t.deleted_at IS NULL
|
|
AND p.post_number = n.post_number
|
|
AND t.id = n.topic_id
|
|
)
|
|
SQL
|
|
end
|
|
|
|
def self.types
|
|
@types ||= Enum.new(mentioned: 1,
|
|
replied: 2,
|
|
quoted: 3,
|
|
edited: 4,
|
|
liked: 5,
|
|
private_message: 6,
|
|
invited_to_private_message: 7,
|
|
invitee_accepted: 8,
|
|
posted: 9,
|
|
moved_post: 10,
|
|
linked: 11,
|
|
granted_badge: 12,
|
|
invited_to_topic: 13,
|
|
custom: 14,
|
|
group_mentioned: 15,
|
|
group_message_summary: 16,
|
|
watching_first_post: 17,
|
|
topic_reminder: 18,
|
|
liked_consolidated: 19,
|
|
post_approved: 20,
|
|
code_review_commit_approved: 21,
|
|
membership_request_accepted: 22,
|
|
membership_request_consolidated: 23,
|
|
bookmark_reminder: 24
|
|
)
|
|
end
|
|
|
|
def self.high_priority_types
|
|
@high_priority_types ||= [
|
|
types[:private_message],
|
|
types[:bookmark_reminder]
|
|
]
|
|
end
|
|
|
|
def self.normal_priority_types
|
|
@normal_priority_types ||= types.reject { |_k, v| high_priority_types.include?(v) }.values
|
|
end
|
|
|
|
def self.mark_posts_read(user, topic_id, post_numbers)
|
|
Notification
|
|
.where(
|
|
user_id: user.id,
|
|
topic_id: topic_id,
|
|
post_number: post_numbers,
|
|
read: false
|
|
)
|
|
.update_all(read: true)
|
|
end
|
|
|
|
def self.read(user, notification_ids)
|
|
Notification
|
|
.where(
|
|
id: notification_ids,
|
|
user_id: user.id,
|
|
read: false
|
|
)
|
|
.update_all(read: true)
|
|
end
|
|
|
|
def self.interesting_after(min_date)
|
|
result = where("created_at > ?", min_date)
|
|
.includes(:topic)
|
|
.visible
|
|
.unread
|
|
.limit(20)
|
|
.order("CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1
|
|
WHEN notification_type = #{Notification.types[:mentioned]} THEN 2
|
|
ELSE 3
|
|
END, created_at DESC").to_a
|
|
|
|
# Remove any duplicates by type and topic
|
|
if result.present?
|
|
seen = {}
|
|
to_remove = Set.new
|
|
|
|
result.each do |r|
|
|
seen[r.notification_type] ||= Set.new
|
|
if seen[r.notification_type].include?(r.topic_id)
|
|
to_remove << r.id
|
|
else
|
|
seen[r.notification_type] << r.topic_id
|
|
end
|
|
end
|
|
result.reject! { |r| to_remove.include?(r.id) }
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
# Clean up any notifications the user can no longer see. For example, if a topic was previously
|
|
# public then turns private.
|
|
def self.remove_for(user_id, topic_id)
|
|
Notification.where(user_id: user_id, topic_id: topic_id).delete_all
|
|
end
|
|
|
|
# Be wary of calling this frequently. O(n) JSON parsing can suck.
|
|
def data_hash
|
|
@data_hash ||= begin
|
|
return {} if data.blank?
|
|
|
|
parsed = JSON.parse(data)
|
|
return {} if parsed.blank?
|
|
|
|
parsed.with_indifferent_access
|
|
end
|
|
end
|
|
|
|
def url
|
|
topic.relative_url(post_number) if topic.present?
|
|
end
|
|
|
|
def post
|
|
return if topic_id.blank? || post_number.blank?
|
|
Post.find_by(topic_id: topic_id, post_number: post_number)
|
|
end
|
|
|
|
def self.recent_report(user, count = nil)
|
|
return unless user && user.user_option
|
|
|
|
count ||= 10
|
|
notifications = user.notifications
|
|
.visible
|
|
.recent(count)
|
|
.includes(:topic)
|
|
|
|
if user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never]
|
|
[
|
|
Notification.types[:liked],
|
|
Notification.types[:liked_consolidated]
|
|
].each do |notification_type|
|
|
notifications = notifications.where(
|
|
'notification_type <> ?', notification_type
|
|
)
|
|
end
|
|
end
|
|
|
|
notifications = notifications.to_a
|
|
|
|
if notifications.present?
|
|
|
|
ids = DB.query_single(<<~SQL, limit: count.to_i)
|
|
SELECT n.id FROM notifications n
|
|
WHERE
|
|
n.high_priority = TRUE AND
|
|
n.user_id = #{user.id.to_i} AND
|
|
NOT read
|
|
ORDER BY n.id ASC
|
|
LIMIT :limit
|
|
SQL
|
|
|
|
if ids.length > 0
|
|
notifications += user
|
|
.notifications
|
|
.order('notifications.created_at DESC')
|
|
.where(id: ids)
|
|
.joins(:topic)
|
|
.limit(count)
|
|
end
|
|
|
|
notifications.uniq(&:id).sort do |x, y|
|
|
if x.unread_high_priority? && !y.unread_high_priority?
|
|
-1
|
|
elsif y.unread_high_priority? && !x.unread_high_priority?
|
|
1
|
|
else
|
|
y.created_at <=> x.created_at
|
|
end
|
|
end.take(count)
|
|
else
|
|
[]
|
|
end
|
|
|
|
end
|
|
|
|
def unread_high_priority?
|
|
self.high_priority? && !read
|
|
end
|
|
|
|
def post_id
|
|
Post.where(topic: topic_id, post_number: post_number).pluck_first(:id)
|
|
end
|
|
|
|
protected
|
|
|
|
def refresh_notification_count
|
|
if user_id
|
|
User.find_by(id: user_id)&.publish_notifications_state
|
|
end
|
|
end
|
|
|
|
def send_email
|
|
NotificationEmailer.process_notification(self) if !skip_send_email
|
|
end
|
|
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: notifications
|
|
#
|
|
# id :integer not null, primary key
|
|
# notification_type :integer not null
|
|
# user_id :integer not null
|
|
# data :string(1000) not null
|
|
# read :boolean default(FALSE), not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# topic_id :integer
|
|
# post_number :integer
|
|
# post_action_id :integer
|
|
# high_priority :boolean default(FALSE), not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# idx_notifications_speedup_unread_count (user_id,notification_type) WHERE (NOT read)
|
|
# index_notifications_on_post_action_id (post_action_id)
|
|
# index_notifications_on_topic_id_and_post_number (topic_id,post_number)
|
|
# index_notifications_on_user_id_and_created_at (user_id,created_at)
|
|
# index_notifications_on_user_id_and_topic_id_and_post_number (user_id,topic_id,post_number)
|
|
# index_notifications_read_or_not_high_priority (user_id,id DESC,read,topic_id) WHERE (read OR (high_priority = false))
|
|
# index_notifications_unique_unread_high_priority (user_id,id) UNIQUE WHERE ((NOT read) AND (high_priority = true))
|
|
#
|