PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.1
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.1
2.2.1 2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 All 44 releases
← All changes | app/Services/NotificationService.php +334 -32 1.11 → 2.2.1 View file →
@@ -1,25 +1,38 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 +use FluentBoards\App\Models\Board;
5 6 use FluentBoards\App\Models\Meta;
6 7 use FluentBoards\App\Models\Notification;
7 8 use FluentBoards\App\Models\NotificationUser;
8 9 use FluentBoards\App\Models\Relation;
9 10 use FluentBoards\App\Models\Task;
11 +use FluentBoards\App\Models\User;
10 12
11 13 class NotificationService
12 14 {
13 - public function getAllNotifications($per_page, $page)
15 + public function getAllNotifications($per_page, $page, $action = 'all')
14 16 {
17 + $per_page = Sanitize::perPage($per_page);
18 + $page = max(1, (int) $page);
15 19 $user = wp_get_current_user();
16 20
17 - $notifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
21 + $query = $this->getAccessibleNotificationsQuery($user->ID)
18 22 ->whereHas('users', function($q) use ($user) {
19 23 $q->where('user_id', $user->ID);
20 - })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
24 + });
21 25
26 + // Add action filter if not 'all'
27 + if ($action !== 'all') {
28 + $query->where('action', $action);
29 + }
30 +
31 + $notifications = $query->with('activitist', 'task')
32 + ->orderBy('created_at', 'desc')
33 + ->paginate($per_page, ['*'], 'page', $page);
34 +
22 35 foreach ($notifications as $notification){
23 36 $notification->read = $notification->checkReadOrNot();
24 37 }
25 38
@@ -30,13 +43,15 @@
30 43 * @throws \Exception
31 44 */
32 45 public function getAllUnreadNotifications($per_page, $page)
33 46 {
47 + $per_page = Sanitize::perPage($per_page);
48 + $page = max(1, (int) $page);
34 49 $userId = get_current_user_id();
35 50 if (!$userId) {
36 - throw new \Exception('You are not allowed to do that', 403);
51 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
37 52 }
38 - $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
53 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
39 54 ->whereHas('users', function($q) use ($userId) {
40 55 $q->where('user_id', $userId)
41 56 ->whereNull('marked_read_at');
42 57 })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
@@ -64,8 +79,12 @@
64 79
65 80 $notification = NotificationUser::where('user_id', $user->ID)
66 81 ->where('notification_id', $notificationId)
67 82 ->first();
83 + if (!$notification) {
84 + throw new \Exception(esc_html__('Notification could not be found', 'fluent-boards'), 404);
85 + }
86 +
68 87 $notification->marked_read_at = current_time('mysql');
69 88 $notification->save();
70 89 return $notification;
71 90 }
@@ -73,9 +92,9 @@
73 92 public function newNotificationNumber()
74 93 {
75 94 $user = wp_get_current_user();
76 95
77 - $unread_notifications = Notification::query()->where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
96 + $unread_notifications = $this->getAccessibleNotificationsQuery($user->ID)
78 97 ->whereHas('users', function($q) use ($user) {
79 98 $q->where('user_id', $user->ID)
80 99 ->whereNull('marked_read_at');
81 100 });
@@ -82,8 +101,41 @@
82 101
83 102 return $unread_notifications->count();
84 103 }
85 104
105 + /**
106 + * Historical recipients must still have access to the source board and current task.
107 + * Keep legacy subtasks readable through their parent when their board_id is null.
108 + */
109 + private function getAccessibleNotificationsQuery($userId)
110 + {
111 + $query = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION);
112 + if (!$userId) {
113 + return $query->where('id', 0);
114 + }
115 +
116 + $boardIds = Board::select('id');
117 + if (!PermissionManager::isAdmin($userId)) {
118 + $boardIds->whereIn('id', Relation::select('object_id')
119 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
120 + ->where('foreign_id', $userId));
121 + }
122 +
123 + return $query->whereIn('object_id', $boardIds)
124 + ->where(function ($notifications) use ($boardIds) {
125 + $notifications->whereNull('task_id')
126 + ->orWhereHas('task', function ($tasks) use ($boardIds) {
127 + $tasks->where(function ($ownedTasks) use ($boardIds) {
128 + $ownedTasks->whereIn('board_id', $boardIds)
129 + ->orWhere(function ($legacySubtasks) use ($boardIds) {
130 + $legacySubtasks->whereNull('board_id')
131 + ->whereIn('parent_id', Task::select('id')->whereIn('board_id', $boardIds));
132 + });
133 + });
134 + });
135 + });
136 + }
137 +
86 138 public function isCurrentUserObservingTask($task)
87 139 {
88 140 $currentUserId = get_current_user_id();
89 141 $observers = $task->watchers()->get()->pluck('ID');
@@ -115,17 +167,113 @@
115 167
116 168 return $settings;
117 169 }
118 170
171 + /**
172 + * Return board notification preferences with defaults and global watch fallbacks applied.
173 + */
174 + public function getBoardNotificationSettingsWithDefaults($boardId, $userId)
175 + {
176 + $settings = $this->getDefaultBoardNotificationSettings($userId);
177 +
178 + $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
179 + if ($boardSettings && $boardSettings->preferences) {
180 + $preferences = maybe_unserialize($boardSettings->preferences);
181 + if (is_array($preferences)) {
182 + $settings = array_merge($settings, array_intersect_key($preferences, $settings));
183 + }
184 + }
185 +
186 + foreach ($settings as $key => $value) {
187 + $settings[$key] = $this->normalizePreferenceValue($value);
188 + }
189 +
190 + return $settings;
191 + }
192 +
193 + /**
194 + * Check if a board-scoped auto-watch preference is enabled for a user.
195 + */
196 + public function isBoardAutoWatchEnabled($boardId, $userId, $preferenceKey)
197 + {
198 + $settings = $this->getBoardNotificationSettingsWithDefaults($boardId, $userId);
199 +
200 + return array_key_exists($preferenceKey, $settings) && $this->normalizePreferenceValue($settings[$preferenceKey]);
201 + }
202 +
203 + /**
204 + * Get global watch preferences for fallback when a board has no saved watch keys.
205 + */
206 + private function getGlobalWatchNotificationSettings($userId)
207 + {
208 + $watchSettings = $this->getWatchNotificationDefaults();
209 +
210 + $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
211 + if (!$globalSettings || !$globalSettings->value) {
212 + return $watchSettings;
213 + }
214 +
215 + $preferences = maybe_unserialize($globalSettings->value);
216 + if (!is_array($preferences)) {
217 + return $watchSettings;
218 + }
219 +
220 + foreach (array_keys($watchSettings) as $key) {
221 + if (array_key_exists($key, $preferences)) {
222 + $watchSettings[$key] = $this->normalizePreferenceValue($preferences[$key]);
223 + }
224 + }
225 +
226 + return $watchSettings;
227 + }
228 +
229 + private function getDefaultBoardNotificationSettings($userId)
230 + {
231 + return array_merge(
232 + Constant::BOARD_NOTIFICATION_TYPES,
233 + $this->getGlobalWatchNotificationSettings($userId)
234 + );
235 + }
236 +
237 + private function getWatchNotificationDefaults()
238 + {
239 + return [
240 + Constant::GLOBAL_EMAIL_NOTIFICATION_CREATING_TASK => true,
241 + Constant::GLOBAL_EMAIL_NOTIFICATION_COMMENTING => true,
242 + Constant::GLOBAL_EMAIL_NOTIFICATION_ASSIGNING => true,
243 + ];
244 + }
245 +
246 + /**
247 + * Convert stored preference values into strict booleans.
248 + */
249 + private function normalizePreferenceValue($value)
250 + {
251 + return true === $value || 1 === $value || '1' === $value || 'true' === $value;
252 + }
253 +
119 254 public function updateBoardNotificationSettings($newSettings, $id)
120 255 {
121 256 $userId = get_current_user_id();
122 257 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
123 - foreach ($newSettings as $index => $setting)
124 - {
125 - $newSettings[$index] = $setting == 'true' ? true : false;
258 + if(empty($boardSettings)){
259 + return;
126 260 }
127 - $boardSettings->preferences = $newSettings;
261 +
262 + $allowedSettings = array_merge(
263 + Constant::BOARD_NOTIFICATION_TYPES,
264 + $this->getWatchNotificationDefaults()
265 + );
266 + $filteredSettings = [];
267 + foreach ($newSettings as $index => $setting) {
268 + if (!array_key_exists($index, $allowedSettings)) {
269 + continue;
270 + }
271 +
272 + $filteredSettings[$index] = $this->normalizePreferenceValue($setting);
273 + }
274 +
275 + $boardSettings->preferences = $filteredSettings;
128 276 $boardSettings->save();
129 277
130 278 }
131 279
@@ -133,23 +281,48 @@
133 281 $task = Task::findOrFail($taskId);
134 282 $watchers = $task->watchers;
135 283 $currentUserId = get_current_user_id();
136 284
137 - $wathersToSendEmail = array();
285 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
286 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, $emailPurpose);
287 + $eligibleLookup = array_fill_keys($eligibleIds, true);
288 + $watchersToSendEmail = [];
138 289
139 - foreach ($watchers as $watcher){
140 - if($watcher->ID != $currentUserId)
141 - {
142 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
143 - {
144 - $wathersToSendEmail[] = $watcher->user_email;
145 - }
290 + foreach ($watchers as $watcher) {
291 + if (isset($eligibleLookup[$watcher->ID])) {
292 + $watchersToSendEmail[] = $watcher->user_email;
146 293 }
147 294 }
148 295
149 - return $wathersToSendEmail;
296 + return $watchersToSendEmail;
150 297 }
151 298
299 + /**
300 + * Get comment notification recipients as revocable user IDs for async delivery.
301 + *
302 + * @param int $taskId
303 + * @return array
304 + */
305 + public function getCommentRecipientUserIds($taskId)
306 + {
307 + $task = Task::findOrFail(absint($taskId));
308 + $currentUserId = get_current_user_id();
309 + $watchers = $task->watchers;
310 + $watcherIds = array_diff($watchers->pluck('ID')->toArray(), [$currentUserId]);
311 + $eligibleIds = $this->getEmailEnabledUserIds($task->board_id, $watcherIds, Constant::BOARD_EMAIL_COMMENT);
312 + $eligibleLookup = array_fill_keys($eligibleIds, true);
313 + $recipientUserIds = [];
314 +
315 + foreach ($watchers as $watcher) {
316 + $watcherId = absint($watcher->ID);
317 + if (isset($eligibleLookup[$watcherId])) {
318 + $recipientUserIds[] = $watcherId;
319 + }
320 + }
321 +
322 + return array_values(array_unique($recipientUserIds));
323 + }
324 +
152 325 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
153 326 {
154 327 if(
155 328 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -159,8 +332,38 @@
159 332 return false;
160 333 }
161 334 }
162 335
336 + /**
337 + * Resolve board email recipients in one query; missing preference keys default to enabled without writes.
338 + */
339 + public function getEmailEnabledUserIds($boardId, $userIds, $purpose)
340 + {
341 + $boardId = absint($boardId);
342 + $userIds = array_values(array_unique(array_filter(array_map('intval', (array) $userIds))));
343 + if (!$boardId || !$userIds) {
344 + return [];
345 + }
346 +
347 + // Match the first membership used by individual checks, including old duplicates.
348 + $relations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
349 + ->where('object_id', $boardId)
350 + ->whereIn('foreign_id', $userIds)
351 + ->orderBy('id', 'ASC')
352 + ->get(['foreign_id', 'preferences'])
353 + ->unique('foreign_id');
354 +
355 + $enabledUserIds = [];
356 + foreach ($relations as $relation) {
357 + $preferences = (array) maybe_unserialize($relation->preferences);
358 + if (!array_key_exists($purpose, $preferences) || $preferences[$purpose]) {
359 + $enabledUserIds[] = (int) $relation->foreign_id;
360 + }
361 + }
362 +
363 + return array_values(array_unique($enabledUserIds));
364 + }
365 +
163 366 public function checkIfEmailEnabled($boardId, $userId, $purpose)
164 367 {
165 368 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
166 369
@@ -177,24 +380,123 @@
177 380
178 381 return false;
179 382 }
180 383
181 - public function checkIfEmailEnabledGlobally($userId, $purpose)
384 + /**
385 + * Return the supplied user IDs that currently belong to a board.
386 + *
387 + * Mention recipients are board-scoped because comment content is private to
388 + * the board, even when a caller supplies a valid WordPress user ID.
389 + *
390 + * @param int $boardId
391 + * @param array $mentionedUserIds
392 + * @return array
393 + */
394 + public function resolveBoardMentionUserIds($boardId, $mentionedUserIds)
182 395 {
183 - $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
396 + $boardId = absint($boardId);
397 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
184 398
185 - if($globalSettings){
186 - $preferences = maybe_unserialize($globalSettings['value']);
187 - if(!array_key_exists($purpose, $preferences)){
188 - $preferences[$purpose] = true;
189 - $globalSettings->preferences = $preferences;
190 - $globalSettings->save();
191 - return true;
192 - }
193 - return $preferences[$purpose];
399 + if (!$boardId || !$mentionedUserIds) {
400 + return [];
194 401 }
195 402
196 - return true;
403 + $boardMemberIdLookup = [];
404 + $boardRelations = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
405 + ->where('object_id', $boardId)
406 + ->whereIn('foreign_id', $mentionedUserIds)
407 + ->get(['foreign_id']);
408 +
409 + foreach ($boardRelations as $boardRelation) {
410 + $boardMemberIdLookup[absint($boardRelation->foreign_id)] = true;
411 + }
412 +
413 + return array_values(array_filter($mentionedUserIds, function ($mentionedUserId) use ($boardMemberIdLookup) {
414 + return isset($boardMemberIdLookup[$mentionedUserId]);
415 + }));
197 416 }
198 -}
199 417
418 + public function mentionInComment($comment, $mentionedUserIds)
419 + {
420 + $currentUserId = get_current_user_id();
421 + $uniqueIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
422 + $uniqueIds = array_values(array_filter($uniqueIds, function ($mentionedUserId) use ($currentUserId) {
423 + return $mentionedUserId !== $currentUserId;
424 + }));
200 425
426 + if (!$uniqueIds) {
427 + return;
428 + }
429 +
430 + // Queued delivery revalidates membership and preferences before sending.
431 + $this->sendMailAfterMention($comment->id, $uniqueIds);
432 +
433 + //sending desktop notifications
434 + do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
435 + }
436 +
437 + public function sendMailAfterMention($commentId, $recipientUserIds)
438 + {
439 + if (empty($recipientUserIds)) {
440 + return;
441 + }
442 +
443 + $current_user_id = get_current_user_id();
444 +
445 + /* this will run in background as soon as possible */
446 + /* sending Model or Model Instance won't work here */
447 + as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
448 + }
449 +
450 + public function getUnreadNotificationsOfTasks($task)
451 + {
452 + $userId = get_current_user_id();
453 + if (!$userId) {
454 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
455 + }
456 + $unreadNotifications = $this->getAccessibleNotificationsQuery($userId)
457 + ->whereHas('users', function($q) use ($userId) {
458 + $q->where('user_id', $userId)
459 + ->whereNull('marked_read_at');
460 + })->whereHas('task', function($q) use ($task) {
461 + $q->where('id', $task->id);
462 + })
463 + ->with('activitist')->orderBy('created_at', 'desc')->count();
464 + return $unreadNotifications;
465 + }
466 +
467 + public function getUnreadNotificationCountsByTaskIds($taskIds)
468 + {
469 + $userId = get_current_user_id();
470 + if (!$userId) {
471 + throw new \Exception(esc_html__('You are not allowed to do that', 'fluent-boards'), 403);
472 + }
473 +
474 + $taskIds = array_values(array_filter(array_map('intval', (array) $taskIds)));
475 + if (!$taskIds) {
476 + return [];
477 + }
478 +
479 + $rows = $this->getAccessibleNotificationsQuery($userId)
480 + // Board/list views can return many tasks at once, so unread counts are
481 + // grouped in one query instead of issuing a count query per task.
482 + ->selectRaw('task_id, COUNT(*) as unread_count')
483 + ->join(
484 + (new NotificationUser())->getTable(),
485 + 'fbs_notifications.id',
486 + '=',
487 + 'fbs_notification_users.notification_id'
488 + )
489 + ->where('fbs_notification_users.user_id', $userId)
490 + ->whereNull('fbs_notification_users.marked_read_at')
491 + ->whereIn('task_id', $taskIds)
492 + ->groupBy('task_id')
493 + ->get();
494 +
495 + $notificationCounts = [];
496 + foreach ($rows as $row) {
497 + $notificationCounts[(int) $row->task_id] = (int) $row->unread_count;
498 + }
499 +
500 + return $notificationCounts;
501 + }
502 +}