discourse/app/models/notification.rb
Martin Brennan b79ea986ac
FEATURE: High priority bookmark reminder notifications (#9290)
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.
2020-04-01 09:09:20 +10:00

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))
#