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 +202 -26 1.91.62.1.0 View file →
@@ -74,8 +74,12 @@
74 74
75 75 $notification = NotificationUser::where('user_id', $user->ID)
76 76 ->where('notification_id', $notificationId)
77 77 ->first();
78 + if (!$notification) {
79 + throw new \Exception(esc_html__('Notification could not be found', 'fluent-boards'), 404);
80 + }
81 +
78 82 $notification->marked_read_at = current_time('mysql');
79 83 $notification->save();
80 84 return $notification;
81 85 }
@@ -125,8 +129,91 @@
125 129
126 130 return $settings;
127 131 }
128 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 +
129 216 public function updateBoardNotificationSettings($newSettings, $id)
130 217 {
131 218 $userId = get_current_user_id();
132 219 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
@@ -132,13 +219,23 @@
132 219 $boardSettings = $this->getBoardNotificationSettingsOfUser($id, $userId);
133 220 if(empty($boardSettings)){
134 221 return;
135 222 }
136 - foreach ($newSettings as $index => $setting)
137 - {
138 - $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);
139 235 }
140 - $boardSettings->preferences = $newSettings;
236 +
237 + $boardSettings->preferences = $filteredSettings;
141 238 $boardSettings->save();
142 239
143 240 }
144 241
@@ -161,8 +258,33 @@
161 258
162 259 return $wathersToSendEmail;
163 260 }
164 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 +
165 287 public function checkIfEmailEnable($userId, $emailPurpose, $boardId)
166 288 {
167 289 if(
168 290 $this->checkIfEmailEnabled($boardId, $userId, $emailPurpose)
@@ -190,49 +312,68 @@
190 312
191 313 return false;
192 314 }
193 315
194 - 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)
195 327 {
196 - $globalSettings = $this->getGlobalNotificationSettingsOfUser($userId);
328 + $boardId = absint($boardId);
329 + $mentionedUserIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionedUserIds))));
197 330
198 - if($globalSettings){
199 - $preferences = maybe_unserialize($globalSettings['value']);
200 - if(!array_key_exists($purpose, $preferences)){
201 - $preferences[$purpose] = true;
202 - $globalSettings->preferences = $preferences;
203 - $globalSettings->save();
204 - return true;
205 - }
206 - return $preferences[$purpose];
331 + if (!$boardId || !$mentionedUserIds) {
332 + return [];
207 333 }
208 334
209 - 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 + }));
210 348 }
211 349
212 350 public function mentionInComment($comment, $mentionedUserIds)
213 351 {
214 - $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 + }));
215 357
216 - $uniqueIds = array_filter($uniqueIds, function($value) {
217 - return (int)$value !== get_current_user_id();
218 - });
358 + if (!$uniqueIds) {
359 + return;
360 + }
219 361
220 - //sending emails to mentioned users
221 - $mentionedUserEmails = User::whereIn('ID', $uniqueIds)->pluck('user_email');
222 - $this->sendMailAfterMention($comment->id, $mentionedUserEmails);
362 + // Queued delivery revalidates membership and preferences before sending.
363 + $this->sendMailAfterMention($comment->id, $uniqueIds);
223 364
224 365 //sending desktop notifications
225 366 do_action('fluent_boards/mention_comment_notification', $comment, $uniqueIds);
226 367 }
227 368
228 - public function sendMailAfterMention($commentId, $usersToSendEmail)
369 + public function sendMailAfterMention($commentId, $recipientUserIds)
229 370 {
230 371 $current_user_id = get_current_user_id();
231 372
232 373 /* this will run in background as soon as possible */
233 374 /* sending Model or Model Instance won't work here */
234 - 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');
235 376 }
236 377
237 378 public function getUnreadNotificationsOfTasks($task)
238 379 {
@@ -249,7 +390,42 @@
249 390 })
250 391 ->with('activitist')->orderBy('created_at', 'desc')->count();
251 392 return $unreadNotifications;
252 393 }
253 -}
254 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 + }
255 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 +}