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 +216 -31 1.412.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,49 +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);
352 + $currentUserId = get_current_user_id();
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 + }));
206 357
207 - $uniqueIds = array_filter($uniqueIds, function($value) {
208 - return (int)$value !== get_current_user_id();
209 - });
358 + if (!$uniqueIds) {
359 + return;
360 + }
210 361
211 - //sending emails to mentioned users
212 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
213 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
362 + // Queued delivery revalidates membership and preferences before sending.
363 + $this->sendMailAfterMention($comment->id, $uniqueIds);
214 364
215 365 //sending desktop notifications
216 366 do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
217 367 }
218 368
219 - public function sendMailAfterMention($commentId, $usersToSendEmail)
369 + public function sendMailAfterMention($commentId, $recipientUserIds)
220 370 {
221 371 $current_user_id = get_current_user_id();
222 372
223 373 /* this will run in background as soon as possible */
224 374 /* sending Model or Model Instance won't work here */
225 - 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');
226 376 }
227 377
228 378 public function getUnreadNotificationsOfTasks($task)
229 379 {
@@ -228,9 +378,9 @@
228 378 public function getUnreadNotificationsOfTasks($task)
229 379 {
230 380 $userId = get_current_user_id();
231 381 if (!$userId) {
232 - 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);
233 383 }
234 384 $unreadNotifications = Notification::where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
235 385 ->whereHas('users', function($q) use ($userId) {
236 386 $q->where('user_id', $userId)
@@ -240,7 +390,42 @@
240 390 })
241 391 ->with('activitist')->orderBy('created_at', 'desc')->count();
242 392 return $unreadNotifications;
243 393 }
244 -}
245 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 + }
246 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 +}