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 +299 -30 1.13 → 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,17 +133,113 @@
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);
123 - foreach ($newSettings as $index => $setting)
124 - {
125 - $newSettings[$index] = $setting == 'true' ? true : false;
224 + if(empty($boardSettings)){
225 + return;
126 226 }
127 - $boardSettings->preferences = $newSettings;
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);
239 + }
240 +
241 + $boardSettings->preferences = $filteredSettings;
128 242 $boardSettings->save();
129 243
130 244 }
131 245
@@ -133,23 +247,48 @@
133 247 $task = Task::findOrFail($taskId);
134 248 $watchers = $task->watchers;
135 249 $currentUserId = get_current_user_id();
136 250
137 - $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 = [];
138 255
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 - }
256 + foreach ($watchers as $watcher) {
257 + if (isset($eligibleLookup[$watcher->ID])) {
258 + $watchersToSendEmail[] = $watcher->user_email;
146 259 }
147 260 }
148 261
149 - return $wathersToSendEmail;
262 + return $watchersToSendEmail;
150 263 }
151 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 +
152 291 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
153 292 {
154 293 if(
155 294 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -159,8 +298,38 @@
159 298 return false;
160 299 }
161 300 }
162 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 +
163 332 public function checkIfEmailEnabled($boardId, $userId, $purpose)
164 333 {
165 334 $boardSettings = $this->getBoardNotificationSettingsOfUser($boardId, $userId);
166 335
@@ -177,24 +346,124 @@
177 346
178 347 return false;
179 348 }
180 349
181 - 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)
182 361 {
183 - $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
362 + $boardId = absint($boardId);
363 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
184 364
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];
365 + if (!$boardId || !$mentionedUserIds) {
366 + return [];
194 367 }
195 368
196 - 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 + }));
197 382 }
198 -}
199 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 + }));
200 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 +}