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 }
@@ -250,6 +287,152 @@ public function deleteBySubmissionId(int $submissionId): void
250287 }
251288 }
252289
290+ public function notifyParticipantsOnNote (Note $ note ): void
291+ {
292+ // Only discussion notes
293+ if ($ note ->assocType !== PKPApplication::ASSOC_TYPE_QUERY ) {
294+ return ;
295+ }
296+
297+ // skip headnote initial email handled when query is created
298+ if ($ note ->isHeadnote ?? false ) {
299+ return ;
300+ }
301+
302+ $ task = EditorialTask::find ($ note ->assocId );
303+ if (!$ task ) {
304+ return ;
305+ }
306+
307+ $ submission = Repo::submission ()->get ($ task ->assocId );
308+ if (!$ submission ) {
309+ return ;
310+ }
311+
312+ $ application = Application::get ();
313+ $ request = $ application ->getRequest ();
314+ $ context = $ request ->getContext ();
315+ if (!$ context ) {
316+ return ;
317+ }
318+
319+ $ sender = Repo::user ()->get ($ note ->userId ?? null );
320+ if (!$ sender ) {
321+ return ;
322+ }
323+
324+ $ headNote = Repo::note ()->getHeadNote ($ task ->id );
325+ $ threadAnchorMessageId = $ headNote ?->messageId;
326+ $ title = $ headNote ?->title ?: $ task ->title ;
327+ $ subject = $ title
328+ ? __ ('common.re ' ) . ' ' . $ title
329+ : __ ('common.re ' );
330+
331+
332+ $ participantIds = $ task ->participants ()
333+ ->pluck ('user_id ' )
334+ ->all ();
335+
336+ if (empty ($ participantIds )) {
337+ return ;
338+ }
339+
340+ /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
341+ $ notificationSubscriptionSettingsDao = DAORegistry::getDAO ('NotificationSubscriptionSettingsDAO ' );
342+
343+ $ notificationManager = new NotificationManager ();
344+
345+ // attachments for this note (if any)
346+ $ submissionFiles = Repo::submissionFile ()
347+ ->getCollector ()
348+ ->filterByAssoc (PKPApplication::ASSOC_TYPE_NOTE , [$ note ->id ])
349+ ->filterBySubmissionIds ([$ submission ->getId ()])
350+ ->getMany ();
351+
352+ // Stage -> mailable map (same as StageMailable)
353+ $ mailableMap = [
354+ WORKFLOW_STAGE_ID_SUBMISSION => DiscussionSubmission::class,
355+ WORKFLOW_STAGE_ID_INTERNAL_REVIEW => DiscussionReview::class,
356+ WORKFLOW_STAGE_ID_EXTERNAL_REVIEW => DiscussionReview::class,
357+ WORKFLOW_STAGE_ID_EDITING => DiscussionCopyediting::class,
358+ WORKFLOW_STAGE_ID_PRODUCTION => DiscussionProduction::class,
359+ ];
360+
361+ if (!isset ($ mailableMap [$ task ->stageId ])) {
362+ return ;
363+ }
364+
365+ $ mailableClass = $ mailableMap [$ task ->stageId ];
366+
367+ foreach ($ participantIds as $ userId ) {
368+ if ($ userId === $ sender ->getId ()) {
369+ continue ;
370+ }
371+
372+ // clear previous "query activity" notifications for this user/query
373+ Notification::withAssoc (PKPApplication::ASSOC_TYPE_QUERY , $ task ->id )
374+ ->withUserId ($ userId )
375+ ->withType (Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY )
376+ ->withContextId ((int ) $ context ->getId ())
377+ ->delete ();
378+
379+ $ recipient = Repo::user ()->get ($ userId );
380+ if (!$ recipient ) {
381+ continue ;
382+ }
383+
384+ // create notification
385+ $ notification = $ notificationManager ->createNotification (
386+ $ userId ,
387+ Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY ,
388+ (int ) $ context ->getId (),
389+ PKPApplication::ASSOC_TYPE_QUERY ,
390+ $ task ->id ,
391+ Notification::NOTIFICATION_LEVEL_TASK
392+ );
393+
394+ if (!$ notification ) {
395+ continue ;
396+ }
397+
398+ // respect email notification settings
399+ $ blocked = $ notificationSubscriptionSettingsDao ->getNotificationSubscriptionSettings (
400+ NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY ,
401+ $ userId ,
402+ (int ) $ context ->getId ()
403+ );
404+
405+ if (in_array (Notification::NOTIFICATION_TYPE_QUERY_ACTIVITY , $ blocked )) {
406+ continue ;
407+ }
408+
409+ /** @var \PKP\mail\Mailable $mailable */
410+ $ mailable = new $ mailableClass ($ context , $ submission );
411+
412+ $ mailable
413+ ->sender ($ sender )
414+ ->recipients ([$ recipient ])
415+ ->subject ($ subject )
416+ ->body ($ note ->contents )
417+ ->allowUnsubscribe ($ notification )
418+ ->allowCapturableReply (
419+ $ note ->messageId ,
420+ $ threadAnchorMessageId && $ threadAnchorMessageId !== $ note ->messageId ? $ threadAnchorMessageId : null ,
421+ $ threadAnchorMessageId ? [$ threadAnchorMessageId ] : []
422+ );
423+
424+ $ submissionFiles ->each (
425+ fn ($ item ) => $ mailable ->attachSubmissionFile (
426+ $ item ->getId (),
427+ $ item ->getData ('name ' )
428+ )
429+ );
430+
431+ Mail::send ($ mailable );
432+ }
433+ }
434+
435+
253436 public function removeParticipantFromSubmissionTasks (int $ submissionId , int $ userId , int $ contextId ): void
254437 {
255438 $ user = Repo::user ()->get ($ userId );
0 commit comments