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