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 +296 -30 1.21 → 2.2.0 View file →
@@ -6,20 +6,32 @@
6 6 use FluentBoards\App\Models\Notification;
7 7 use FluentBoards\App\Models\NotificationUser;
8 8 use FluentBoards\App\Models\Relation;
9 9 use FluentBoards\App\Models\Task;
10 +use FluentBoards\App\Models\User;
10 11
11 12 class NotificationService
12 13 {
13 - public function getAllNotifications($per_page, $page)
14 + public function getAllNotifications($per_page, $page, $action = 'all')
14 15 {
16 + $per_page = Sanitize::perPage($per_page);
17 + $page = max(1, (int) $page);
15 18 $user = wp_get_current_user();
16 19
17 - $notifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
20 + $query = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
18 21 ->whereHas('users', function($q) use ($user) {
19 22 $q->where('user_id', $user->ID);
20 - })->with('activitist', 'task')->orderBy('created_at', 'desc')->paginate($per_page, ['*'], 'page', $page);
23 + });
21 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 +
22 34 foreach ($notifications as $notification){
23 35 $notification->read = $notification->checkReadOrNot();
24 36 }
25 37
@@ -30,11 +42,13 @@
30 42 * @throws \Exception
31 43 */
32 44 public function getAllUnreadNotifications($per_page, $page)
33 45 {
46 + $per_page = Sanitize::perPage($per_page);
47 + $page = max(1, (int) $page);
34 48 $userId = get_current_user_id();
35 49 if (!$userId) {
36 - 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);
37 51 }
38 52 $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
39 53 ->whereHas('users', function($q) use ($userId) {
40 54 $q->where('user_id', $userId)
@@ -64,8 +78,12 @@
64 78
65 79 $notification = NotificationUser::where('user_id', $user->ID)
66 80 ->where('notification_id', $notificationId)
67 81 ->first();
82 + if (!$notification) {
83 + throw new \Exception(esc_html__('Notification could not be found', 'fluent-boards'), 404);
84 + }
85 +
68 86 $notification->marked_read_at = current_time('mysql');
69 87 $notification->save();
70 88 return $notification;
71 89 }
@@ -115,8 +133,91 @@
115 133
116 134 return $settings;
117 135 }
118 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 +
119 220 public function updateBoardNotificationSettings($newSettings, $id)
120 221 {
121 222 $userId = get_current_user_id();
122 223 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
@@ -122,13 +223,23 @@
122 223 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
123 224 if(empty($boardSettings)){
124 225 return;
125 226 }
126 - foreach ($newSettings as $index => $setting)
127 - {
128 - $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);
129 239 }
130 - $boardSettings->preferences = $newSettings;
240 +
241 + $boardSettings->preferences = $filteredSettings;
131 242 $boardSettings->save();
132 243
133 244 }
134 245
@@ -136,23 +247,48 @@
136 247 $task = Task::findOrFail($taskId);
137 248 $watchers = $task->watchers;
138 249 $currentUserId = get_current_user_id();
139 250
140 - $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 = [];
141 255
142 - foreach ($watchers as $watcher){
143 - if($watcher->ID != $currentUserId)
144 - {
145 - if($this->checkIfEmailEnable($watcher->ID, $emailPurpose, $task->board_id))
146 - {
147 - $wathersToSendEmail[] = $watcher->user_email;
148 - }
256 + foreach ($watchers as $watcher) {
257 + if (isset($eligibleLookup[$watcher->ID])) {
258 + $watchersToSendEmail[] = $watcher->user_email;
149 259 }
150 260 }
151 261
152 - return $wathersToSendEmail;
262 + return $watchersToSendEmail;
153 263 }
154 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 +
155 291 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
156 292 {
157 293 if(
158 294 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -162,8 +298,38 @@
162 298 return false;
163 299 }
164 300 }
165 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 +
166 332 public function checkIfEmailEnabled($boardId, $userId, $purpose)
167 333 {
168 334 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
169 335
@@ -180,24 +346,124 @@
180 346
181 347 return false;
182 348 }
183 349
184 - 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)
185 361 {
186 - $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
362 + $boardId = absint($boardId);
363 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
187 364
188 - if($globalSettings){
189 - $preferences = maybe_unserialize($globalSettings['value']);
190 - if(!array_key_exists($purpose, $preferences)){
191 - $preferences[$purpose] = true;
192 - $globalSettings->preferences = $preferences;
193 - $globalSettings->save();
194 - return true;
195 - }
196 - return $preferences[$purpose];
365 + if (!$boardId || !$mentionedUserIds) {
366 + return [];
197 367 }
198 368
199 - 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 + }));
200 382 }
201 -}
202 383
384 + public function mentionInComment($comment, $mentionedUserIds)
385 + {
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 + }));
203 391
392 + if (!$uniqueIds) {
393 + return;
394 + }
395 +
396 + // Queued delivery revalidates membership and preferences before sending.
397 + $this->sendMailAfterMention($comment->id, $uniqueIds);
398 +
399 + //sending desktop notifications
400 + do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
401 + }
402 +
403 + public function sendMailAfterMention($commentId, $recipientUserIds)
404 + {
405 + if (empty($recipientUserIds)) {
406 + return;
407 + }
408 +
409 + $current_user_id = get_current_user_id();
410 +
411 + /* this will run in background as soon as possible */
412 + /* sending Model or Model Instance won't work here */
413 + as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_mention', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
414 + }
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 + }
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 +}