3232use PKP \security \Role ;
3333use PKP \stageAssignment \StageAssignment ;
3434use PKP \user \User ;
35+ use PKP \mail \mailables \DiscussionSubmission ;
36+ use PKP \mail \mailables \DiscussionReview ;
37+ use PKP \mail \mailables \DiscussionCopyediting ;
38+ use PKP \mail \mailables \DiscussionProduction ;
39+
3540
3641class Repository
3742{
@@ -46,7 +51,7 @@ public function countOpenPerStage(int $submissionId, ?array $participantIds = nu
4651 {
4752 $ counts = EditorialTask::withAssoc (Application::ASSOC_TYPE_SUBMISSION , $ submissionId )
4853 ->when ($ participantIds !== null , function ($ q ) use ($ participantIds ) {
49- $ q ->withUserIds ($ participantIds );
54+ $ q ->withParticipantIds ($ participantIds );
5055 })
5156 ->withClosed (false )
5257 ->selectRaw ('stage_id, COUNT(stage_id) as count ' )
@@ -96,15 +101,21 @@ public function addQuery(int $submissionId, int $stageId, string $title, string
96101 'seq ' => $ maxSeq + 1 ,
97102 'createdBy ' => $ fromUser ->getId (),
98103 'type ' => EditorialTaskType::DISCUSSION ->value ,
99- EditorialTask::ATTRIBUTE_PARTICIPANTS => array_map (fn (int $ participantId ) => ['userId ' => $ participantId ], array_unique ($ participantUserIds )),
104+ EditorialTask::ATTRIBUTE_PARTICIPANTS => array_map (
105+ fn (int $ participantId ) => ['userId ' => $ participantId ],
106+ array_unique ($ participantUserIds )
107+ ),
100108 'title ' => $ title ,
101109 ]);
102110
103- Note::create ([
111+ // Head note for this discussion, with a capturable messageId
112+ $ headNote = Note::create ([
104113 'assocType ' => Application::ASSOC_TYPE_QUERY ,
105114 'assocId ' => $ task ->id ,
106115 'contents ' => $ content ,
107116 'userId ' => $ fromUser ->getId (),
117+ 'isHeadnote ' => true ,
118+ 'messageId ' => Note::generateMessageId (),
108119 ]);
109120
110121 // Add task for assigned participants
@@ -113,8 +124,24 @@ public function addQuery(int $submissionId, int $stageId, string $title, string
113124 /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
114125 $ notificationSubscriptionSettingsDao = DAORegistry::getDAO ('NotificationSubscriptionSettingsDAO ' );
115126
127+ // need submission + context + stage mailables to send capturable email
128+ $ submission = Repo::submission ()->get ($ submissionId );
129+ $ application = Application::get ();
130+ $ request = $ application ->getRequest ();
131+ $ context = $ request ?->getContext();
132+
133+ $ mailableMap = [
134+ WORKFLOW_STAGE_ID_SUBMISSION => DiscussionSubmission::class,
135+ WORKFLOW_STAGE_ID_INTERNAL_REVIEW => DiscussionReview::class,
136+ WORKFLOW_STAGE_ID_EXTERNAL_REVIEW => DiscussionReview::class,
137+ WORKFLOW_STAGE_ID_EDITING => DiscussionCopyediting::class,
138+ WORKFLOW_STAGE_ID_PRODUCTION => DiscussionProduction::class,
139+ ];
140+
141+ $ mailableClass = $ mailableMap [$ stageId ] ?? null ;
142+
116143 foreach ($ task ->participants ()->get () as $ participant ) {
117- $ notificationMgr ->createNotification (
144+ $ notification = $ notificationMgr ->createNotification (
118145 $ participant ->userId ,
119146 Notification::NOTIFICATION_TYPE_NEW_QUERY ,
120147 $ contextId ,
@@ -123,7 +150,8 @@ public function addQuery(int $submissionId, int $stageId, string $title, string
123150 Notification::NOTIFICATION_LEVEL_TASK
124151 );
125152
126- if (!$ sendEmail ) {
153+ if (
154+ !$ sendEmail || !$ notification || !$ mailableClass || !$ submission || !$ context ) {
127155 continue ;
128156 }
129157
@@ -138,11 +166,20 @@ public function addQuery(int $submissionId, int $stageId, string $title, string
138166 }
139167
140168 $ recipient = $ participant ->user ;
141- $ mailable = new Mailable ();
142- $ mailable ->to ($ recipient ->getEmail (), $ recipient ->getFullName ());
143- $ mailable ->from ($ fromUser ->getEmail (), $ fromUser ->getFullName ());
144- $ mailable ->subject ($ title );
145- $ mailable ->body ($ content );
169+ if (!$ recipient ) {
170+ continue ;
171+ }
172+
173+ /** @var \PKP\mail\Mailable $mailable */
174+ $ mailable = new $ mailableClass ($ context , $ submission );
175+
176+ $ mailable
177+ ->sender ($ fromUser )
178+ ->recipients ([$ recipient ])
179+ ->subject ($ title )
180+ ->body ($ content )
181+ ->allowUnsubscribe ($ notification )
182+ ->allowCapturableReply ($ headNote ->messageId );
146183
147184 Mail::send ($ mailable );
148185 }
@@ -240,12 +277,158 @@ private function taskAlreadyCreatedFromTemplate(int $submissionId, int $template
240277 public function deleteBySubmissionId (int $ submissionId ): void
241278 {
242279 $ editorialTasks = EditorialTask::withAssoc (PKPApplication::ASSOC_TYPE_SUBMISSION , $ submissionId )->get ();
243- $ taskIds = $ editorialTasks ->pluck ('query_id ' )->all ();
280+ $ taskIds = $ editorialTasks ->pluck ('id ' )->all ();
244281
245282 if (!empty ($ taskIds )) {
246- EditorialTask::whereIn ('query_id ' , $ taskIds )->delete ();
283+ EditorialTask::whereIn ('edit_task_id ' , $ taskIds )->delete ();
247284 Note::whereIn ('assoc_id ' , $ taskIds )->delete ();
248285 Notification::whereIn ('assoc_id ' , $ taskIds )->delete ();
249286 }
250287 }
288+
289+ public function notifyParticipantsOnNote (Note $ note ): void
290+ {
291+ // Only discussion notes
292+ if ($ note ->assocType !== PKPApplication::ASSOC_TYPE_QUERY ) {
293+ return ;
294+ }
295+
296+ // skip headnote initial email handled when query is created
297+ if ($ note ->isHeadnote ?? false ) {
298+ return ;
299+ }
300+
301+ $ task = EditorialTask::find ($ note ->assocId );
302+ if (!$ task ) {
303+ return ;
304+ }
305+
306+ $ submission = Repo::submission ()->get ($ task ->assocId );
307+ if (!$ submission ) {
308+ return ;
309+ }
310+
311+ $ application = Application::get ();
312+ $ request = $ application ->getRequest ();
313+ $ context = $ request ->getContext ();
314+ if (!$ context ) {
315+ return ;
316+ }
317+
318+ $ sender = Repo::user ()->get ($ note ->userId ?? null );
319+ if (!$ sender ) {
320+ return ;
321+ }
322+
323+ $ headNote = Repo::note ()->getHeadNote ($ task ->id );
324+ $ threadAnchorMessageId = $ headNote ?->messageId;
325+ $ title = $ headNote ?->title ?: $ task ->title ;
326+ $ subject = $ title
327+ ? __ ('common.re ' ) . ' ' . $ title
328+ : __ ('common.re ' );
329+
330+
331+ $ participantIds = $ task ->participants ()
332+ ->pluck ('user_id ' )
333+ ->all ();
334+
335+ if (empty ($ participantIds )) {
336+ return ;
337+ }
338+
339+ /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
340+ $ notificationSubscriptionSettingsDao = DAORegistry::getDAO ('NotificationSubscriptionSettingsDAO ' );
341+
342+ $ notificationManager = new NotificationManager ();
343+
344+ // attachments for this note (if any)
345+ $ submissionFiles = Repo::submissionFile ()
346+ ->getCollector ()
347+ ->filterByAssoc (PKPApplication::ASSOC_TYPE_NOTE , [$ note ->id ])
348+ ->filterBySubmissionIds ([$ submission ->getId ()])
349+ ->getMany ();
350+
351+ // Stage -> mailable map (same as StageMailable)
352+ $ mailableMap = [
353+ WORKFLOW_STAGE_ID_SUBMISSION => DiscussionSubmission::class,
354+ WORKFLOW_STAGE_ID_INTERNAL_REVIEW => DiscussionReview::class,
355+ WORKFLOW_STAGE_ID_EXTERNAL_REVIEW => DiscussionReview::class,
356+ WORKFLOW_STAGE_ID_EDITING => DiscussionCopyediting::class,
357+ WORKFLOW_STAGE_ID_PRODUCTION => DiscussionProduction::class,
358+ ];
359+
360+ if (!isset ($ mailableMap [$ task ->stageId ])) {
361+ return ;
362+ }
363+
364+ $ mailableClass = $ mailableMap [$ task ->stageId ];
365+
366+ foreach ($ participantIds as $ userId ) {
367+ if ($ userId === $ sender ->getId ()) {
368+ continue ;
369+ }
370+
371+ // clear previous "query activity" notifications for this user/query
372+ Notification::withAssoc (PKPApplication::ASSOC_TYPE_QUERY , $ task ->id )
373+ ->withUserId ($ userId )
374+ ->withType (Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY )
375+ ->withContextId ((int ) $ context ->getId ())
376+ ->delete ();
377+
378+ $ recipient = Repo::user ()->get ($ userId );
379+ if (!$ recipient ) {
380+ continue ;
381+ }
382+
383+ // create notification
384+ $ notification = $ notificationManager ->createNotification (
385+ $ userId ,
386+ Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY ,
387+ (int ) $ context ->getId (),
388+ PKPApplication::ASSOC_TYPE_QUERY ,
389+ $ task ->id ,
390+ Notification::NOTIFICATION_LEVEL_TASK
391+ );
392+
393+ if (!$ notification ) {
394+ continue ;
395+ }
396+
397+ // respect email notification settings
398+ $ blocked = $ notificationSubscriptionSettingsDao ->getNotificationSubscriptionSettings (
399+ NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY ,
400+ $ userId ,
401+ (int ) $ context ->getId ()
402+ );
403+
404+ if (in_array (Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY , $ blocked )) {
405+ continue ;
406+ }
407+
408+ /** @var \PKP\mail\Mailable $mailable */
409+ $ mailable = new $ mailableClass ($ context , $ submission );
410+
411+ $ mailable
412+ ->sender ($ sender )
413+ ->recipients ([$ recipient ])
414+ ->subject ($ subject )
415+ ->body ($ note ->contents )
416+ ->allowUnsubscribe ($ notification )
417+ ->allowCapturableReply (
418+ $ note ->messageId ,
419+ $ threadAnchorMessageId && $ threadAnchorMessageId !== $ note ->messageId ? $ threadAnchorMessageId : null ,
420+ $ threadAnchorMessageId ? [$ threadAnchorMessageId ] : []
421+ );
422+
423+ $ submissionFiles ->each (
424+ fn ($ item ) => $ mailable ->attachSubmissionFile (
425+ $ item ->getId (),
426+ $ item ->getData ('name ' )
427+ )
428+ );
429+
430+ Mail::send ($ mailable );
431+ }
432+ }
433+
251434}
0 commit comments