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