1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Models;
7
8use App\Events\NotificationDeleteEvent;
9use App\Events\NotificationReadEvent;
10use App\Libraries\Notification\BatchIdentities;
11use DB;
12use Illuminate\Database\Eloquent\Builder;
13
14class UserNotification extends Model
15{
16 const DELIVERY_OFFSETS = [
17 'push' => 0,
18 'mail' => 1,
19 ];
20
21 protected $casts = [
22 'is_read' => 'boolean',
23 ];
24
25 public static function batchDestroy(int $userId, BatchIdentities $batchIdentities)
26 {
27 $notificationIds = $batchIdentities->getNotificationIds();
28 $identities = $batchIdentities->getIdentities();
29
30 if (empty($notificationIds)) {
31 return;
32 }
33
34 $now = now();
35 $userNotificationQuery = static::where(['user_id' => $userId]);
36 // obtain and filter valid user notification ids
37 $ids = $userNotificationQuery->clone()
38 ->whereIn('notification_id', $notificationIds)
39 ->where('created_at', '<=', $now)
40 ->pluck('id')
41 ->all();
42
43 if (empty($ids)) {
44 return;
45 }
46
47 $readCount = 0;
48
49 foreach (array_chunk($ids, 1000) as $chunkedIds) {
50 $unreadCountQuery = $userNotificationQuery->clone()
51 ->hasPushDelivery()
52 ->where('is_read', false)
53 ->whereIn('id', $chunkedIds);
54 $unreadCountInitial = $unreadCountQuery->count();
55 $userNotificationQuery->clone()
56 ->whereIn('id', $chunkedIds)
57 ->delete();
58
59 $unreadCountCurrent = $unreadCountQuery->count();
60 $readCount += $unreadCountInitial - $unreadCountCurrent;
61 }
62
63 (new NotificationDeleteEvent($userId, [
64 'notifications' => $identities,
65 'read_count' => $readCount,
66 'timestamp' => $now,
67 ]))->broadcast();
68 }
69
70 public static function batchMarkAsRead(User $user, BatchIdentities $batchIdentities)
71 {
72 $ids = $batchIdentities->getNotificationIds();
73 $identities = $batchIdentities->getIdentities();
74 $now = now();
75
76 if (empty($ids)) {
77 return;
78 } else if ($ids instanceof Builder) {
79 $instance = new static();
80 $tableName = $instance->getTable();
81 // force mysql optimizer to optimize properly with a fake multi-table update
82 // https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization.html
83 // FIXME: this is supposedly fixed by mysql 8.0.22
84 $itemsQuery = $instance->getConnection()
85 ->table(DB::raw("{$tableName}, (SELECT 1) dummy"))
86 ->where('user_id', $user->getKey())
87 ->where('is_read', false)
88 ->whereIn('notification_id', $ids);
89 // raw builder doesn't have model scope magic.
90 $instance->scopeHasPushDelivery($itemsQuery);
91
92 $count = $itemsQuery->update(['is_read' => true, 'updated_at' => $now]);
93 } else {
94 $count = $user
95 ->userNotifications()
96 ->hasPushDelivery()
97 ->where('is_read', false)
98 ->whereIn('notification_id', $ids)
99 ->update(['is_read' => true, 'updated_at' => $now]);
100 }
101
102 if ($count > 0) {
103 (new NotificationReadEvent($user->getKey(), ['notifications' => $identities, 'read_count' => $count, 'timestamp' => $now]))->broadcast();
104 }
105 }
106
107 public static function deliveryMask(string $type): int
108 {
109 return 1 << self::DELIVERY_OFFSETS[$type];
110 }
111
112 public function isDelivery(string $type): bool
113 {
114 $mask = static::deliveryMask($type);
115
116 return ($this->delivery & $mask) === $mask;
117 }
118
119 public function isMail(): bool
120 {
121 return $this->isDelivery('mail');
122 }
123
124 public function isPush(): bool
125 {
126 return $this->isDelivery('push');
127 }
128
129 public function notification()
130 {
131 return $this->belongsTo(Notification::class);
132 }
133
134 public function scopeHasMailDelivery($query)
135 {
136 return $query->where('delivery', '&', static::deliveryMask('mail'));
137 }
138
139 public function scopeHasPushDelivery($query)
140 {
141 return $query->where('delivery', '&', static::deliveryMask('push'));
142 }
143
144 public function user()
145 {
146 return $this->belongsTo(User::class, 'user_id');
147 }
148}