discourse/app/models/notification.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

312 lines
9.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-03-01 07:07:44 -05:00
class Notification < ActiveRecord::Base
2013-02-05 14:16:51 -05:00
belongs_to :user
belongs_to :topic
MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS = 24
2013-02-05 14:16:51 -05:00
validates_presence_of :data
validates_presence_of :notification_type
scope :unread, lambda { where(read: false) }
2017-07-27 21:20:09 -04:00
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')
2017-07-27 21:20:09 -04:00
.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
# if we have manually set the notification to high_priority on create then
# make sure that is respected
self.high_priority = 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
2013-03-01 07:07:44 -05:00
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,
FEATURE: Send notifications for time-based and At Desktop bookmark reminders (#9071) * This PR implements the scheduling and notification system for bookmark reminders. Every 5 minutes a schedule runs to check any reminders that need to be sent before now, limited to **300** reminders at a time. Any leftover reminders will be sent in the next run. This is to avoid having to deal with fickle sidekiq and reminders in the far-flung future, which would necessitate having a background job anyway to clean up any missing `enqueue_at` reminders. * If a reminder is sent its `reminder_at` time is cleared and the `reminder_last_sent_at` time is filled in. Notifications are only user-level notifications for now. * All JavaScript and frontend code related to displaying the bookmark reminder notification is contained here. The reminder functionality is now re-enabled in the bookmark modal as well. * This PR also implements the "Remind me next time I am at my desktop" bookmark reminder functionality. When the user is on a mobile device they are able to select this option. When they choose this option we set a key in Redis saying they have a pending at desktop reminder. The next time they change devices we check if the new device is desktop, and if it is we send reminders using a DistributedMutex. There is also a job to ensure consistency of these reminders in Redis (in case Redis drops the ball) and the at desktop reminders expire after 20 days. * Also in this PR is a fix to delete all Bookmarks for a user via `UserDestroyer`
2020-03-11 20:16:00 -04:00
membership_request_consolidated: 23,
bookmark_reminder: 24
)
2013-02-05 14:16:51 -05:00
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)
2018-05-25 21:11:10 -04:00
Notification
.where(
user_id: user.id,
topic_id: topic_id,
post_number: post_numbers,
read: false
)
.update_all(read: true)
2013-02-05 14:16:51 -05:00
end
def self.read(user, notification_ids)
2018-05-25 21:11:10 -04:00
Notification
.where(
id: notification_ids,
user_id: user.id,
read: false
)
2017-07-27 21:20:09 -04:00
.update_all(read: true)
end
2013-02-05 14:16:51 -05:00
def self.interesting_after(min_date)
2017-07-27 21:20:09 -04:00
result = where("created_at > ?", min_date)
.includes(:topic)
.visible
.unread
.limit(20)
.order("CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1
2013-03-01 07:07:44 -05:00
WHEN notification_type = #{Notification.types[:mentioned]} THEN 2
2013-02-05 14:16:51 -05:00
ELSE 3
END, created_at DESC").to_a
# Remove any duplicates by type and topic
if result.present?
2013-02-07 10:45:24 -05:00
seen = {}
2013-02-05 14:16:51 -05:00
to_remove = Set.new
result.each do |r|
seen[r.notification_type] ||= Set.new
if seen[r.notification_type].include?(r.topic_id)
2013-02-07 10:45:24 -05:00
to_remove << r.id
2013-02-05 14:16:51 -05:00
else
seen[r.notification_type] << r.topic_id
end
end
2017-07-27 21:20:09 -04:00
result.reject! { |r| to_remove.include?(r.id) }
2013-02-05 14:16:51 -05:00
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
2013-02-05 14:16:51 -05:00
# 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?
2015-09-03 23:34:21 -04:00
parsed.with_indifferent_access
end
2013-02-05 14:16:51 -05:00
end
def url
topic.relative_url(post_number) if topic.present?
2013-02-05 14:16:51 -05:00
end
def post
return if topic_id.blank? || post_number.blank?
Post.find_by(topic_id: topic_id, post_number: post_number)
2013-02-05 14:16:51 -05:00
end
def self.recent_report(user, count = nil)
return unless user && user.user_option
count ||= 10
notifications = user.notifications
2017-07-27 21:20:09 -04:00
.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
2017-07-27 21:20:09 -04:00
notifications.uniq(&:id).sort do |x, y|
if x.unread_high_priority? && !y.unread_high_priority?
2014-02-13 01:27:35 -05:00
-1
elsif y.unread_high_priority? && !x.unread_high_priority?
2014-02-13 01:27:35 -05:00
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
2013-02-05 14:16:51 -05:00
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
#
2018-07-16 02:18:07 -04:00
# idx_notifications_speedup_unread_count (user_id,notification_type) WHERE (NOT read)
2015-09-17 20:41:10 -04:00
# index_notifications_on_post_action_id (post_action_id)
2019-10-30 07:59:59 -04:00
# index_notifications_on_topic_id_and_post_number (topic_id,post_number)
2015-09-17 20:41:10 -04:00
# 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))
#