<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[11921] sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers: Translate: Introduce notification on rejection and opt-in/opt-out in a thread</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/11921">11921</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/11921","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>amieiro</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2022-06-21 14:15:05 +0000 (Tue, 21 Jun 2022)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Translate: Introduce notification on rejection and opt-in/opt-out in a thread

See:
- https://github.com/GlotPress/gp-translation-helpers/pull/55
- https://github.com/GlotPress/gp-translation-helpers/pull/61

props akirk, spiraltee</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationdiscussionphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetscsstranslationdiscussioncss">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetstemplatestranslationdiscussioncommentsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgpnotificationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgptranslationhelpersphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclasswporgnotificationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjstranslationhelpersjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationdiscussionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php 2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php   2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -278,12 +278,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // We can't do much if the comment was posted logged out.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( empty( $commentdata['comment_author'] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( empty( $commentdata['user_id'] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return $approved;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // If our user has already contributed translations, approve comment.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $user_current_translations = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->gp_translations WHERE user_id = %s AND status = 'current'", $commentdata['comment_author'] ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $user_current_translations = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->gp_translations WHERE user_id = %s AND status = 'current'", $commentdata['user_id'] ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $user_current_translations ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $approved = true;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetscsstranslationdiscussioncss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css     2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css       2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -82,7 +82,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> .comment-content {
</span><span class="cx" style="display: block; padding: 0 10px">        text-align: start;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px"> .alignright {
</span><span class="cx" style="display: block; padding: 0 10px">        float: right;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+}
+.optin-message-for-each-discussion {
+       padding: 0.5rem 1rem;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of file
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetstemplatestranslationdiscussioncommentsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php      2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php        2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -114,6 +114,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        $post_obj
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                echo '<div class="optin-message-for-each-discussion">';
+               echo wp_kses(
+                       GP_Notifications::optin_message_for_each_discussion( $original_id ),
+                       array(
+                               'a' => array(
+                                       'href'             => array(),
+                                       'class'            => array(),
+                                       'data-original-id' => array(),
+                                       'data-opt-type'    => array(),
+                               ),
+                       )
+               );
+               echo '</div>';
</ins><span class="cx" style="display: block; padding: 0 10px">         } else {
</span><span class="cx" style="display: block; padding: 0 10px">                /* translators: Log in URL. */
</span><span class="cx" style="display: block; padding: 0 10px">                echo sprintf( __( 'You have to be <a href="%s">logged in</a> to comment.' ), esc_html( wp_login_url() ) );
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgpnotificationsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php       2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php 2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9,6 +9,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class GP_Notifications {
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Stores the related comments to the first one when the validator makes a bulk rejection.
+        *
+        * @since 0.0.2
+        * @var array
+        */
+       private static array $related_comments = array();
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Sends notifications when a new comment in the discussion is stored using the WP REST API.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -27,6 +34,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ( '0' !== $comment->comment_parent ) ) { // Notify to the thread only if the comment is in a thread.
</span><span class="cx" style="display: block; padding: 0 10px">                                        self::send_emails_to_thread_commenters( $comment, $comment_meta );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                if ( ( '0' === $comment->comment_parent ) && array_key_exists( 'reject_reason', $comment_meta ) && ( ! empty( $comment_meta['reject_reason'] ) ) ) {  // Notify a rejection without parent comments.
+                                       self::send_rejection_email_to_translator( $comment, $comment_meta );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $root_comment      = self::get_root_comment_in_a_thread( $comment );
</span><span class="cx" style="display: block; padding: 0 10px">                                $root_comment_meta = get_comment_meta( $root_comment->comment_ID );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( array_key_exists( 'comment_topic', $root_comment_meta ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -93,6 +103,26 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Sends the reject notification to the translator.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return void
+        */
+       public static function send_rejection_email_to_translator( WP_Comment $comment, array $comment_meta ) {
+               $translation_id = $comment_meta['translation_id'];
+               $translation    = GP::$translation->get( $translation_id );
+               $translator     = get_user_by( 'id', $translation->user_id_last_modified );
+               if ( false === $translator ) {
+                       $translator = get_user_by( 'id', $translation->user_id );
+               }
+               self::send_emails( $comment, $comment_meta, array( $translator->user_email ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Sends an email to the GlotPress admins.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -118,7 +148,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool Whether the email has been sent or not.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public static function send_emails_to_validators( WP_Comment $comment, array $comment_meta ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $project = self::get_project_to_translate( $comment );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $post    = get_post( $comment->comment_post_ID );
+               $project = self::get_project_from_post( $post );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $email_addresses = self::get_validators_email_addresses( $project->path );
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -170,8 +201,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array  $comments        Array with the parent comments to the posted comment.
-        * @param string $email_address_to_ignore Email from the posted comment.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array       $comments                Array with the parent comments to the posted comment.
+        * @param string|null $email_address_to_ignore Email from the posted comment.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The emails to be notified from the thread comments.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -251,13 +282,16 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return $email_addresses;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $admin_email_addresses = $wpdb->get_results(
-                       "SELECT user_email FROM {$wpdb->users}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         try {
+                       $admin_email_addresses = $wpdb->get_results(
+                               "SELECT user_email FROM {$wpdb->users}
</ins><span class="cx" style="display: block; padding: 0 10px">                         INNER JOIN {$wpdb->gp_permissions}
</span><span class="cx" style="display: block; padding: 0 10px">                        ON {$wpdb->users}.ID = {$wpdb->gp_permissions}.user_id
</span><span class="cx" style="display: block; padding: 0 10px">                        WHERE action='admin'"
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                );
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 );
+               } catch ( Exception $e ) {
+                       $admin_email_addresses = array();
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $admin_email_addresses as $admin ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $email_addresses[] = $admin->user_email;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -294,7 +328,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ( null === $comment ) || ( null === $comment_meta ) || ( empty( $email_addresses ) ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $original        = self::get_original( $comment );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $email_addresses = self::remove_commenter_email_address( $comment, $email_addresses );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $email_addresses = self::remove_optout_discussion_email_addresses( $original->id, $email_addresses );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $headers = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'Content-Type: text/html; charset=UTF-8',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -327,7 +363,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string|null
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public static function get_email_body( WP_Comment $comment, array $comment_meta ): string {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $project  = self::get_project_to_translate( $comment );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $post     = get_post( $comment->comment_post_ID );
+               $project  = self::get_project_from_post( $post );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $original = self::get_original( $comment );
</span><span class="cx" style="display: block; padding: 0 10px">                $output   = '';
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -350,34 +387,63 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'a' => array( 'href' => array() ),
</span><span class="cx" style="display: block; padding: 0 10px">                        )
</span><span class="cx" style="display: block; padding: 0 10px">                ) . '<br/>';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! empty( self::$related_comments ) ) {
+                       $output .= wp_kses(
+                       /* translators: The number of different translations related with the comment. */
+                               sprintf( __( 'This comment affects to <strong>%1$d different translations</strong>.', 'glotpress' ), count( self::$related_comments ) + 1 ),
+                               array(
+                                       'a'      => array( 'href' => array() ),
+                                       'strong' => array(),
+                               )
+                       ) . '<br/>';
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">                 $output .= '<br>';
</span><span class="cx" style="display: block; padding: 0 10px">                $output .= esc_html__( 'It would be nice if you have some time to review this comment and reply to it if needed.', 'glotpress' );
</span><span class="cx" style="display: block; padding: 0 10px">                $output .= '<br><br>';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $output .= '- ' . wp_kses(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( array_key_exists( 'locale', $comment_meta ) && ( ! empty( $comment_meta['locale'][0] ) ) ) {
+                       /* translators: The translation locale for the comment. */
+                       $output .= '- ' . wp_kses( sprintf( __( '<strong>Locale:</strong> %s', 'glotpress' ), $comment_meta['locale'][0] ), array( 'strong' => array() ) ) . '<br/>';
+               }
+               if ( empty( self::$related_comments ) ) { // Only show original and translation strings if we don't have related comments (bulk rejection).
+                       /* translators: The original string to translate. */
+                       $output .= '- ' . wp_kses( sprintf( __( '<strong>Original string:</strong> %s', 'glotpress' ), $original->singular ), array( 'strong' => array() ) ) . '<br/>';
+                       if ( array_key_exists( 'translation_id', $comment_meta ) && $comment_meta['translation_id'][0] ) {
+                               $translation_id = $comment_meta['translation_id'][0];
+                               $translation    = GP::$translation->get( $translation_id );
+                               // todo: add the plurals.
+                               if ( ! is_null( $translation ) ) {
+                                       /* translators: The translation string. */
+                                       $output .= '- ' . wp_kses( sprintf( __( '<strong>Translation string:</strong> %s', 'glotpress' ), $translation->translation_0 ), array( 'strong' => array() ) ) . '<br/>';
+                               }
+                       }
+               }
+               /* translators: The comment made. */
+               $output .= '- ' . wp_kses( sprintf( __( '<strong>Comment:</strong> %s', 'glotpress' ), $comment->comment_content ), array( 'strong' => array() ) ) . '<br/>';
+               if ( empty( self::$related_comments ) ) {
+                       $output .= '- ' . __( '<strong>Discussion URL:</strong>' ) . '<br/>';
+               } else {
+                       $output .= '- ' . __( '<strong>Discussion URLs:</strong>' ) . '<br/>';
+               }
+               $output .= '&nbsp;&nbsp;&nbsp; - ' . wp_kses(
</ins><span class="cx" style="display: block; padding: 0 10px">                         /* translators: The discussion URL where the user can find the comment. */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        sprintf( __( '<strong>Discussion URL:</strong> <a href="%1$s">%1$s</a>', 'glotpress' ), $url ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 sprintf( __( '<a href="%1$s">%1$s</a>', 'glotpress' ), $url ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'strong' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                                'a'      => array( 'href' => array() ),
</span><span class="cx" style="display: block; padding: 0 10px">                        )
</span><span class="cx" style="display: block; padding: 0 10px">                ) . '<br/>';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( array_key_exists( 'locale', $comment_meta ) && ( ! empty( $comment_meta['locale'][0] ) ) ) {
-                       /* translators: The translation locale for the comment. */
-                       $output .= '- ' . wp_kses( sprintf( __( '<strong>Locale:</strong> %s', 'glotpress' ), $comment_meta['locale'][0] ), array( 'strong' => array() ) ) . '<br/>';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         foreach ( self::$related_comments as $related_comment ) {
+                       $original = self::get_original( $related_comment );
+                       $url      = GP_Route_Translation_Helpers::get_permalink( $project->path, $original->id );
+                       $output  .= '&nbsp;&nbsp;&nbsp; - ' . wp_kses(
+                               /* translators: The discussion URL where the user can find the comment. */
+                               sprintf( __( '<a href="%1$s">%1$s</a>', 'glotpress' ), $url ),
+                               array(
+                                       'strong' => array(),
+                                       'a'      => array( 'href' => array() ),
+                               )
+                       ) . '<br/>';
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /* translators: The original string to translate. */
-               $output .= '- ' . wp_kses( sprintf( __( '<strong>Original string:</strong> %s', 'glotpress' ), $original->singular ), array( 'strong' => array() ) ) . '<br/>';
-               if ( array_key_exists( 'translation_id', $comment_meta ) && $comment_meta['translation_id'][0] ) {
-                       $translation_id = $comment_meta['translation_id'][0];
-                       $translation    = GP::$translation->get( $translation_id );
-                       // todo: add the plurals.
-                       if ( ! is_null( $translation ) ) {
-                               /* translators: The translation string. */
-                               $output .= '- ' . wp_kses( sprintf( __( '<strong>Translation string:</strong> %s', 'glotpress' ), $translation->translation_0 ), array( 'strong' => array() ) ) . '<br/>';
-                       }
-               }
-               /* translators: The comment made. */
-               $output .= '- ' . wp_kses( sprintf( __( '<strong>Comment:</strong> %s', 'glotpress' ), $comment->comment_content ), array( 'strong' => array() ) );
</del><span class="cx" style="display: block; padding: 0 10px">                 $output .= '<br><br>';
</span><span class="cx" style="display: block; padding: 0 10px">                $output .= esc_html__( 'Have a nice day!', 'glotpress' );
</span><span class="cx" style="display: block; padding: 0 10px">                $output .= '<br><br>';
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -433,17 +499,47 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Removes the opt-out emails in the current discussion.
+        *
+        * @since 0.0.2
+        *
+        * @param int   $original_id     The id of the original string used for the discussion.
+        * @param array $email_addresses A list of emails.
+        *
+        * @return array
+        */
+       public static function remove_optout_discussion_email_addresses( int $original_id, array $email_addresses ): array {
+               foreach ( $email_addresses as $email_address ) {
+                       $user            = get_user_by( 'email', $email_address );
+                       $is_user_opt_out = ! empty(
+                               get_users(
+                                       array(
+                                               'meta_key'   => 'gp_opt_out',
+                                               'meta_value' => $original_id,
+                                               'include'    => array( $user->ID ),
+                                       )
+                               )
+                       );
+                       if ( $is_user_opt_out ) {
+                               $index = array_search( $email_address, $email_addresses, true );
+                               unset( $email_addresses[ $index ] );
+                       }
+               }
+
+               return array_values( $email_addresses );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Gets the project that the translated string belongs to.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param WP_Post $post The post object.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return GP_Project|bool The project that the translated string belongs to.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function get_project_to_translate( WP_Comment $comment ) {
-               $post_id = $comment->comment_post_ID;
-               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private static function get_project_from_post( WP_Post $post ) {
+               $terms = wp_get_object_terms( $post->ID, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( empty( $terms ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -456,6 +552,33 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Adds a related comment (to the first one) when the validator makes a bulk rejection.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The related comment to add.
+        *
+        * @return void
+        */
+       public static function add_related_comment( WP_Comment $comment ) {
+               self::$related_comments[] = $comment;
+       }
+
+       /**
+        * Gets the project the original_id belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param int $original_id The id of the original string used for the discussion.
+        *
+        * @return GP_Project The project the original_id belongs to.
+        */
+       public static function get_project_from_original_id( int $original_id ): GP_Project {
+               $original = GP::$original->get( $original_id );
+               return GP::$project->get( $original->project_id );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Gets the original string that the translated string belongs to.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -464,7 +587,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return GP_Thing|false The original string that the translated string belongs to.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function get_original( WP_Comment $comment ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public static function get_original( WP_Comment $comment ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $post_id = $comment->comment_post_ID;
</span><span class="cx" style="display: block; padding: 0 10px">                $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</span><span class="cx" style="display: block; padding: 0 10px">                if ( empty( $terms ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -473,4 +596,175 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return GP::$original->get( $terms[0]->slug );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Gets the post_id for the discussion of an original_id.
+        *
+        * If the post doesn't exist, the result is 0.
+        *
+        * @param int $original_id The id of the original string used for the discussion.
+        *
+        * @return int The post_id for the discussion of an original_id.
+        */
+       public static function get_post_id( int $original_id ): int {
+                       $gp_posts = get_posts(
+                               array(
+                                       'tax_query'        => array(
+                                               array(
+                                                       'taxonomy' => Helper_Translation_Discussion::LINK_TAXONOMY,
+                                                       'terms'    => $original_id,
+                                                       'field'    => 'slug',
+                                               ),
+                                       ),
+                                       'post_type'        => Helper_Translation_Discussion::POST_TYPE,
+                                       'posts_per_page'   => 1,
+                                       'post_status'      => Helper_Translation_Discussion::POST_STATUS,
+                                       'suppress_filters' => false,
+                               )
+                       );
+
+               return ! empty( $gp_posts ) ? $gp_posts[0]->ID : 0;
+       }
+
+       /**
+        * Returns if the given user is an GlotPress admin or not.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_User $user A user object.
+        *
+        * @return bool
+        */
+       public static function is_user_an_gp_admin( WP_User $user ): bool {
+               global $wpdb;
+               try {
+                       $db_email_addresses = $wpdb->get_results(
+                               "
+                       SELECT user_email FROM {$wpdb->users} 
+                       INNER JOIN {$wpdb->gp_permissions}
+                       ON {$wpdb->users}.ID = {$wpdb->gp_permissions}.user_id 
+                       WHERE action='admin'",
+                               ARRAY_N
+                       );
+                       foreach ( $db_email_addresses as $email_address ) {
+                               $email_addresses[] = $email_address[0];
+                       }
+               } catch ( Exception $e ) {
+                       $email_addresses = array();
+               }
+               if ( empty( $email_addresses ) || empty( array_intersect( array( $user->user_email ), $email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Returns if the given user is an GlotPress validator for the post or not.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_User $user        A user object.
+        * @param int     $original_id The id of the original string used for the discussion.
+        *
+        * @return bool
+        */
+       public static function is_user_an_gp_validator( WP_User $user, int $original_id ): bool {
+               $project         = self::get_project_from_original_id( $original_id );
+               $email_addresses = self::get_validators_email_addresses( $project->path );
+               if ( empty( $email_addresses ) || empty( array_intersect( array( $user->user_email ), $email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Indicates whether an e-mail address is opt-out in a discussion.
+        *
+        * @since 0.0.2
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool True if the user has opt-out, otherwise false.
+        */
+       public static function is_user_opt_out_in_discussion( int $original_id, WP_User $user ): bool {
+               return ! empty(
+                       get_users(
+                               array(
+                                       'meta_key'   => 'gp_opt_out',
+                                       'meta_value' => $original_id,
+                                       'include'    => array( $user->ID ),
+                               )
+                       )
+               );
+       }
+
+       /**
+        * Gets the opt-in/oup-out message to show at the bottom of the discussions.
+        *
+        * @since 0.0.2
+        *
+        * @param int $original_id The id of the original string used for the discussion.
+        *
+        * @return string The opt-in/oup-out message to show at the bottom of the discussions.
+        */
+       public static function optin_message_for_each_discussion( int $original_id ): string {
+               $post_id = self::get_post_id( $original_id );
+               /**
+                * Filters the optin message that will be showed in each discussion.
+                *
+                * @since 0.0.2
+                *
+                * @param string $message     The opt-in/oup-out message to show at the bottom of the discussions.
+                * @param int    $original_id The id of the original string used for the discussion.
+                */
+               $message = apply_filters( 'gp_get_optin_message_for_each_discussion', '', $original_id );
+               if ( $message ) {
+                       return $message;
+               }
+               $user            = wp_get_current_user();
+               $is_user_opt_out = self::is_user_opt_out_in_discussion( $original_id, $user );
+               if ( ! $is_user_opt_out ) {
+                       $comments = get_comments(
+                               array(
+                                       'user_id'            => $user->ID,
+                                       'post_id'            => $post_id,
+                                       'status'             => 'approve',
+                                       'type'               => 'comment',
+                                       'include_unapproved' => array( $user->ID ),
+                               )
+                       );
+               }
+
+               if ( $is_user_opt_out ) {  // Opt-out user.
+                       $output  = __( 'You will not receive notifications for this discussion because you have opt-out to get notifications for it. ' );
+                       $output .= ' <a href="#" class="opt-in-discussion" data-original-id="' . $original_id . '" data-opt-type="optin">' . __( 'Start receiving notifications for this discussion.' ) . '</a>';
+                       return $output;
+               }
+               if ( $comments && ( ! self::is_user_an_gp_admin( $user ) ) && ( ! self::is_user_an_gp_validator( $user, $original_id ) ) ) { // Regular user with comments.
+                       $output  = __( 'You are going to receive notifications for the threads where you have participated. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_gp_admin( $user ) && self::is_user_an_gp_validator( $user, $original_id ) ) {  // Admin and validator user.
+                       $output  = __( 'You are going to receive notifications because you are a GlotPress administrator and a validator for this project and language. ' );
+                       $output .= __( 'You will not receive notifications if another administrator or another validator participate in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_gp_admin( $user ) ) {   // Admin user.
+                       $output  = __( 'You are going to receive notifications because you are a GlotPress administrator. ' );
+                       $output .= __( 'You will not receive notifications if another administrator participate in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_gp_validator( $user, $original_id ) ) { // Validator user.
+                       $output  = __( 'You are going to receive notifications because you are a GlotPress validator for this project and language. ' );
+                       $output .= __( 'You will not receive notifications if another validator participate in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       return $output;
+               }
+               return __( 'You will not receive notifications for this discussion. We will send you notifications as soon as you get involved.' ); // Regular user without comments.
+
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgptranslationhelpersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php 2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php   2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -62,6 +62,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'transition_comment_status', array( 'GP_Notifications', 'on_comment_status_change' ), 10, 3 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'gp_pre_tmpl_load', array( $this, 'register_reject_feedback_js' ), 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_ajax_reject_with_feedback', array( $this, 'reject_with_feedback' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                add_action( 'wp_ajax_optout_discussion_notifications', array( $this, 'optout_discussion_notifications' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_thickbox();
</span><span class="cx" style="display: block; padding: 0 10px">                gp_enqueue_style( 'thickbox' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -159,7 +160,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $translation_helpers_settings = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'th_url' => gp_url_project( $args['project'], gp_url_join( $args['locale_slug'], $args['translation_set_slug'], '-get-translation-helpers' ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'th_url'   => gp_url_project( $args['project'], gp_url_join( $args['locale_slug'], $args['translation_set_slug'], '-get-translation-helpers' ) ),
+                       'ajax_url' => admin_url( 'admin-ajax.php' ),
+                       'nonce'    => wp_create_nonce( 'gp_optin_optout' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'gp_head', array( $this, 'css_and_js' ), 10 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -394,20 +397,58 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $first_translation_id = array_shift( $translation_id_array );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Post comment on discussion page for the first string
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $save_feedback = $this->insert_reject_comment( $reject_comment, $first_original_id, $reject_reason, $first_translation_id, $locale_slug, $_SERVER );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $first_comment_id = $this->insert_reject_comment( $reject_comment, $first_original_id, $reject_reason, $first_translation_id, $locale_slug, $_SERVER );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $original_id_array ) && ! empty( $translation_id_array ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        // For other strings post link to the comment.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $reject_comment = get_comment_link( $save_feedback );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $reject_comment = get_comment_link( $first_comment_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                         foreach ( $original_id_array as $index => $single_original_id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $this->insert_reject_comment( $reject_comment, $single_original_id, $reject_reason, $translation_id_array[ $index ], $locale_slug, $_SERVER );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $comment_id = $this->insert_reject_comment( $reject_comment, $single_original_id, $reject_reason, $translation_id_array[ $index ], $locale_slug, $_SERVER );
+                               $comment    = get_comment( $comment_id );
+                               GP_Notifications::add_related_comment( $comment );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( $first_comment_id ) {
+                       $comment = get_comment( $first_comment_id );
+                       GP_Notifications::init( $comment, null, null );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_send_json_success();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Adds or removes metadata to a user, related with the opt-in/opt-out status in a discussion
+        *
+        * It receives thought Ajax this data:
+        * - nonce.
+        * - originalId. The id of the original string related with the discussion.
+        * - optType:
+        *   - optout. Add the metadata, to opt-out from the notifications.
+        *   - optin. Removes the metadata, to opt-in from the notifications. Default status.
+        *
+        * @since 0.0.2
+        *
+        * @return void
+        */
+       public function optout_discussion_notifications() {
+               $nonce = sanitize_text_field( $_POST['data']['nonce'] );
+               if ( ! wp_verify_nonce( $nonce, 'gp_optin_optout' ) ) {
+                       wp_send_json_error( esc_html__( 'Invalid nonce.' ), 403 );
+               } else {
+                       $user_id     = get_current_user_id();
+                       $original_id = sanitize_text_field( $_POST['data']['originalId'] );
+                       $opt_type    = sanitize_text_field( $_POST['data']['optType'] );
+                       if ( 'optout' === $opt_type ) {
+                               add_user_meta( $user_id, 'gp_opt_out', $original_id );
+                       } elseif ( 'optin' === $opt_type ) {
+                               delete_user_meta( $user_id, 'gp_opt_out', $original_id );
+                       }
+                        wp_send_json_success();
+               }
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Inserts rejection feedback as WordPress comment
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclasswporgnotificationsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php    2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php      2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -33,7 +33,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        add_filter(
</span><span class="cx" style="display: block; padding: 0 10px">                                'gp_notification_admin_email_addresses',
</span><span class="cx" style="display: block; padding: 0 10px">                                function ( $email_addresses, $comment, $comment_meta ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        return self::get_author_email_address( $comment, $comment_meta );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $original               = GP_Notifications::get_original( $comment );
+                                       $email_addresses        = self::get_author_email_address( $original->id );
+                                       $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
+                                       $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
+                                       // If one author has a comment in the thread, we don't need to inform to any author, because this author will be notified in the thread.
+                                       if ( ( ! empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
+                                               return array();
+                                       }
+                                       return $email_addresses;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 },
</span><span class="cx" style="display: block; padding: 0 10px">                                10,
</span><span class="cx" style="display: block; padding: 0 10px">                                3
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -41,7 +49,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        add_filter(
</span><span class="cx" style="display: block; padding: 0 10px">                                'gp_notification_validator_email_addresses',
</span><span class="cx" style="display: block; padding: 0 10px">                                function ( $email_addresses, $comment, $comment_meta ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        return self::get_validator_email_addresses( $comment, $comment_meta );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $email_addresses        = self::get_validator_email_addresses( $comment, $comment_meta );
+                                       $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
+                                       $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
+                                       // If one validator (GTE/PTE/CLPTE) has a comment in the thread, we don't need to inform to any validator, because this validator will be notified in the thread.
+                                       if ( ! empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
+                                               return array();
+                                       }
+                                       return $email_addresses;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 },
</span><span class="cx" style="display: block; padding: 0 10px">                                10,
</span><span class="cx" style="display: block; padding: 0 10px">                                3
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -71,6 +86,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        add_filter(
+                               'gp_get_optin_message_for_each_discussion',
+                               function ( $message, $original_id ) {
+                                       return self::optin_message_for_each_discussion( $original_id );
+                               },
+                               10,
+                               2
+                       );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -88,17 +111,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array    The validators' emails.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public static function get_validator_email_addresses( WP_Comment $comment, array $comment_meta ): array {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $locale                 = $comment_meta['locale'][0];
-               $email_addresses        = self::get_gte_email_addresses( $locale );
-               $email_addresses        = array_merge( $email_addresses, self::get_pte_email_addresses_by_project_and_locale( $comment, $locale ) );
-               $email_addresses        = array_merge( $email_addresses, self::get_clpte_email_addresses_by_project( $comment ) );
-               $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
-               $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
-               // Set the email addresses array as empty if one GTE/PTE/CLPTE has a comment in the thread.
-               if ( ! empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
-                       return array();
-               }
-               return $email_addresses;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $locale          = $comment_meta['locale'][0];
+               $email_addresses = self::get_gte_email_addresses( $locale );
+               $original        = GP_Notifications::get_original( $comment );
+               $email_addresses = array_merge( $email_addresses, self::get_pte_email_addresses_by_project_and_locale( $original->id, $locale ) );
+               return array_merge( $email_addresses, self::get_clpte_email_addresses_by_project( $original->id ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -150,13 +167,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
-        * @param string     $locale  The locale. E.g. 'zh-tw'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int    $original_id The id of the original string used for the discussion.
+        * @param string $locale  The locale. E.g. 'zh-tw'.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The project translation editors (PTE) emails.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public static function get_pte_email_addresses_by_project_and_locale( $comment, $locale ): array {
-               return self::get_pte_clpte_email_addresses_by_project_and_locale( $comment, $locale );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public static function get_pte_email_addresses_by_project_and_locale( int $original_id, string $locale ): array {
+               return self::get_pte_clpte_email_addresses_by_project_and_locale( $original_id, $locale );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -164,12 +181,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $original_id The id of the original string used for the discussion.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The cross language project translation editors (CLPTE) emails.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public static function get_clpte_email_addresses_by_project( $comment ): array {
-               return self::get_pte_clpte_email_addresses_by_project_and_locale( $comment, 'all-locales' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public static function get_clpte_email_addresses_by_project( int $original_id ): array {
+               return self::get_pte_clpte_email_addresses_by_project_and_locale( $original_id, 'all-locales' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -177,12 +194,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
-        * @param string     $locale  The locale. E.g. 'zh-tw'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int    $original_id The id of the original string used for the discussion.
+        * @param string $locale      The locale. E.g. 'zh-tw'.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The PTE/CLPTE emails for the project and locale.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function get_pte_clpte_email_addresses_by_project_and_locale( WP_Comment $comment, string $locale ): array {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private static function get_pte_clpte_email_addresses_by_project_and_locale( int $original_id, string $locale ): array {
</ins><span class="cx" style="display: block; padding: 0 10px">                 global $wpdb;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( 'all-locales' === $locale ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -195,8 +212,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return array();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $project = self::get_project_to_translate( $comment );
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $project = self::get_project_from_original_id( $original_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 // todo: remove the deleted users in the SQL query.
</span><span class="cx" style="display: block; padding: 0 10px">                $translation_editors = $wpdb->get_results(
</span><span class="cx" style="display: block; padding: 0 10px">                        $wpdb->prepare(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -225,17 +241,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Themes: only one email.
</span><span class="cx" style="display: block; padding: 0 10px">         * Plugins: all the plugin authors.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Other projects: the special users, available at $i18n_email.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment      The comment object.
-        * @param array      $comment_meta The meta values for the comment.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $original_id The id of the original string used for the discussion.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The email addresses for the author of a theme or a plugin.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public static function get_author_email_address( WP_Comment $comment, array $comment_meta ): array {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public static function get_author_email_address( int $original_id ): array {
</ins><span class="cx" style="display: block; padding: 0 10px">                 global $wpdb;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $email_addresses = array();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $project         = self::get_project_to_translate( $comment );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $project         = GP_Notifications::get_project_from_original_id( $original_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( 'wp-themes' === substr( $project->path, 0, 9 ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $author = $wpdb->get_row(
</span><span class="cx" style="display: block; padding: 0 10px">                                $wpdb->prepare(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -267,12 +283,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! ( ( 'wp-themes' === substr( $project->path, 0, 9 ) ) || ( 'wp-plugins' === substr( $project->path, 0, 10 ) ) ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $email_addresses = self::$i18n_email;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
-               $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
-               // If one author has a comment in the thread or if one validator is the commenter, we don't need to inform any other validator.
-               if ( ( true !== empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
-                       return array();
-               }
</del><span class="cx" style="display: block; padding: 0 10px">                 return $email_addresses;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -287,8 +297,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string|null The email body message.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public static function get_email_body( WP_Comment $comment, ?array $comment_meta ): ?string {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $project  = self::get_project_to_translate( $comment );
-               $original = self::get_original( $comment );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $project  = self::get_project_from_post_id( $comment->comment_post_ID );
+               $original = self::get_original( $comment->comment_post_ID );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $output   = esc_html__( 'Hi:' );
</span><span class="cx" style="display: block; padding: 0 10px">                $output  .= '<br><br>';
</span><span class="cx" style="display: block; padding: 0 10px">                $output  .= esc_html__( 'There is a new comment in a discussion of the WordPress translation system that may be of interest to you.' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -324,13 +334,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $post_id The id of the shadow post used for the discussion.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return GP_Project The project the translated string belongs to.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return false|GP_Project The project the translated string belongs to.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function get_project_to_translate( WP_Comment $comment ): GP_Project {
-               $post_id = $comment->comment_post_ID;
-               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private static function get_project_from_post_id( int $post_id ) {
+               $terms = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( empty( $terms ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -343,7 +352,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // If the parent project is not a main project, get the parent project. We need to do this
</span><span class="cx" style="display: block; padding: 0 10px">                // because we have 3 levels of projects. E.g. wp-plugins->akismet->stable and the PTE are
</span><span class="cx" style="display: block; padding: 0 10px">                // assigned to the second level.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ( ! is_null( $project->parent_project_id ) ) && ( ! in_array( $project->parent_project_id, $main_projects, true ) ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ( ! is_null( $project->parent_project_id ) ) && ( ! in_array( $project->parent_project_id, $main_projects, false ) ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         $project = GP::$project->get( $project->parent_project_id );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">                return $project;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -350,6 +359,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Gets the project the original_id belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param int $original_id The id of the original string used for the discussion.
+        *
+        * @return GP_Project The project the original_id belongs to.
+        */
+       public static function get_project_from_original_id( int $original_id ): GP_Project {
+               $original      = GP::$original->get( $original_id );
+               $project_id    = $original->project_id;
+               $project       = GP::$project->get( $project_id );
+               $main_projects = self::get_main_projects();
+
+               // If the parent project is not a main project, get the parent project. We need to do this
+               // because we have 3 levels of projects. E.g. wp-plugins->akismet->stable and the PTE are
+               // assigned to the second level.
+               if ( ( ! is_null( $project->parent_project_id ) ) && ( ! in_array( $project->parent_project_id, $main_projects, false ) ) ) {
+                       $project = GP::$project->get( $project->parent_project_id );
+               }
+               return $project;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Gets the id of the main projects without parent projects.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -359,9 +392,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private static function get_main_projects():array {
</span><span class="cx" style="display: block; padding: 0 10px">                global $wpdb;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $main_projects = $wpdb->get_col( "SELECT id FROM {$wpdb->gp_projects} WHERE parent_project_id IS NULL" );
-
-               return $main_projects;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $wpdb->get_col( "SELECT id FROM {$wpdb->gp_projects} WHERE parent_project_id IS NULL" );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -369,13 +400,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 0.0.2
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param WP_Comment $comment The comment object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $post_id The id of the shadow post used for the discussion.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return GP_Thing|false The original string that the translated string belongs to.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function get_original( WP_Comment $comment ) {
-               $post_id = $comment->comment_post_ID;
-               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private static function get_original( int $post_id ) {
+               $terms = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( empty( $terms ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -394,9 +424,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        private static function get_opted_in_email_addresses( array $email_addresses ): array {
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $email_addresses as $email_address ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $user            = get_user_by( 'email', $email_address );
-                       $gp_default_sort = get_user_option( 'gp_default_sort', $user->ID );
-                       if ( 'on' != gp_array_get( $gp_default_sort, 'notifications_optin', 'off' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( self::is_global_optout_email_address( $email_address ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $index = array_search( $email_address, $email_addresses, true );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( false !== $index ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        unset( $email_addresses[ $index ] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -405,4 +433,223 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">                return array_values( $email_addresses );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Indicates whether a user is globally opt-out.
+        *
+        * @since 0.0.2
+        *
+        * @param string $email_address The user's email address.
+        *
+        * @return bool Whether a user wis globally opt-out.
+        */
+       private static function is_global_optout_email_address( string $email_address ): bool {
+               $user            = get_user_by( 'email', $email_address );
+               $gp_default_sort = get_user_option( 'gp_default_sort', $user->ID );
+               if ( 'on' != gp_array_get( $gp_default_sort, 'notifications_optin', 'off' ) ) {
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Indicates if the given user is a GTE at translate.wordpress.org.
+        *
+        * @since 0.0.2
+        *
+        * @todo Cache the GTE email addresses, because getting it made a lot of queries, slowing down the load time.
+        *
+        * @param WP_User $user A user object.
+        *
+        * @return bool Whether the user is GTE for any of the languages to which the comments in the post belong.
+        */
+       public static function is_user_an_wporg_gte( WP_User $user ): bool {
+               $locales             = GP_Locales::locales();
+               $gte_email_addresses = array();
+               foreach ( $locales as $locale ) {
+                       $gte_email_addresses = array_merge( $gte_email_addresses, self::get_gte_email_addresses( $locale->slug ) );
+               }
+               if ( empty( array_intersect( array( $user->user_email ), $gte_email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Indicates if the given user is PTE for the project and for any of the languages to which the comments in the post belong.
+        *
+        * @since 0.0.2
+        *
+        * @todo Cache the PTE email addresses for each project, because getting it made a lot of queries, slowing down the load time.
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool Whether the user is PTE for the project and for any of the languages to which the comments in the post belong.
+        */
+       public static function is_user_an_wporg_pte_for_the_project( int $original_id, WP_User $user ): bool {
+               $locales             = GP_Locales::locales();
+               $pte_email_addresses = array();
+               foreach ( $locales as $locale ) {
+                       $pte_email_addresses = array_merge( $pte_email_addresses, self::get_pte_email_addresses_by_project_and_locale( $original_id, $locale->slug ) );
+               }
+               if ( empty( $pte_email_addresses ) || empty( array_intersect( array( $user->user_email ), $pte_email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Indicates if the given user is CLPTE for the project to which the post belong.
+        *
+        * @since 0.0.2
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool Whether the user is a CLPTE for the project to which the post belong.
+        */
+       public static function is_user_an_wporg_clpte_for_the_project( int $original_id, WP_User $user ): bool {
+               $clpte_email_addresses = self::get_clpte_email_addresses_by_project( $original_id );
+               if ( empty( $clpte_email_addresses ) || empty( array_intersect( array( $user->user_email ), $clpte_email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Indicates if the given user is the author for the project to which the post belong.
+        *
+        * Only works with plugins and themes.
+        *
+        * @since 0.0.2
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool Whether the user is the author for the project to which the post belong.
+        */
+       public static function is_user_an_author_of_the_project( int $original_id, WP_User $user ): bool {
+               $author_email_addresses = self::get_author_email_address( $original_id );
+               if ( empty( $author_email_addresses ) || empty( array_intersect( array( $user->user_email ), $author_email_addresses ) ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Indicates if the given user is a special user for projects different that themes and plugins.
+        *
+        * @since 0.0.2
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool Whether the user is a special user or not for projects different than themes and plugins.
+        */
+       public static function is_an_special_user_in_a_special_project( int $original_id, WP_User $user ):bool {
+               $project = self::get_project_from_original_id( $original_id );
+               if ( 'wp-themes' !== substr( $project->path, 0, 9 ) && ( 'wp-plugins' !== substr( $project->path, 0, 10 ) ) ) {
+                       if ( empty( self::$i18n_email ) || empty( array_intersect( array( $user->user_email ), self::$i18n_email ) ) ) {
+                               return false;
+                       }
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Indicates if the given user has made a comment in the discussion.
+        *
+        * @since 0.0.2
+        *
+        * @param int     $original_id The id of the original string used for the discussion.
+        * @param WP_User $user        A user object.
+        *
+        * @return bool Whether the user has made a comment in the discussion.
+        */
+       private static function is_user_a_commenter_in_the_discussion( int $original_id, WP_User $user ):bool {
+               $post_id  = GP_Notifications::get_post_id( $original_id );
+               $comments = get_comments(
+                       array(
+                               'post_id'            => $post_id,
+                               'user_id'            => $user->ID,
+                               'status'             => 'approve',
+                               'type'               => 'comment',
+                               'include_unapproved' => array( get_current_user_id() ),
+                       )
+               );
+               if ( empty( $comments ) ) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Gets the opt-in/oup-out message to show at the bottom of the discussions at translate.wordpress.org.
+        *
+        * @param int $original_id The id of the original string used for the discussion.
+        *
+        * @return string The message to show at the bottom of the discussions at translate.wordpress.org.
+        */
+       public static function optin_message_for_each_discussion( int $original_id ): string {
+               $user = wp_get_current_user();
+
+               if ( self::is_global_optout_email_address( $user->user_email ) ) {
+                       $output  = __( 'You will not receive notifications because you have not yet opted-in. ' );
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Start receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( GP_Notifications::is_user_opt_out_in_discussion( $original_id, $user ) ) {
+                       $output  = __( 'You will not receive notifications for this discussion because you have opt-out to get notifications for it. ' );
+                       $output .= ' <a href="#" class="opt-in-discussion" data-original-id="' . $original_id . '" data-opt-type="optin">' . __( 'Start receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_wporg_gte( $user ) ) {
+                       $output  = __( 'You are going to receive notifications for the questions in your language because you are a GTE. ' );
+                       $output .= __( 'You will not receive notifications if another GTE or PTE for your language or CLPTE participates in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_wporg_pte_for_the_project( $original_id, $user ) ) {
+                       $output  = __( 'You are going to receive notifications for the questions in your language because you are a PTE. ' );
+                       $output .= __( 'You will not receive notifications if another GTE or PTE for your language or CLPTE participates in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_wporg_clpte_for_the_project( $original_id, $user ) ) {
+                       $output  = __( 'You are going to receive notifications for the questions because you are a CLPTE. ' );
+                       $output .= __( 'You will not receive notifications if another GTE or PTE for their language or CLPTE participates in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_an_special_user_in_a_special_project( $original_id, $user ) ) {
+                       $output  = __( 'You are going to receive notifications for some questions (typos and more context) because you are a special user. ' );
+                       $output .= __( 'You will not receive notifications if another special user participates in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_an_author_of_the_project( $original_id, $user ) ) {
+                       $output  = __( 'You are going to receive notifications for some questions (typos and more context) because you are an author. ' );
+                       $output .= __( 'You will not receive notifications if another author participates in a thread where you do not take part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               if ( self::is_user_a_commenter_in_the_discussion( $original_id, $user ) ) {
+                       $output  = __( 'You are going to receive notifications for some threads where you have taken part. ' );
+                       $output .= ' <a href="#" class="opt-out-discussion" data-original-id="' . $original_id . '" data-opt-type="optout">' . __( 'Stop receiving notifications for this discussion.' ) . '</a>';
+                       $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+                       return $output;
+               }
+               $output  = __( 'You will not receive notifications for this discussion. We will send you notifications as soon as you get involved.' );
+               $output .= ' <a href="https://translate.wordpress.org/settings/">' . __( 'Stop receiving notifications.' ) . '</a>';
+               return $output;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjstranslationhelpersjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js 2022-06-16 02:15:44 UTC (rev 11920)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js   2022-06-21 14:15:05 UTC (rev 11921)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,4 +1,4 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/* global $gp, window */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global $gp, window, $gp_translation_helpers_settings */
</ins><span class="cx" style="display: block; padding: 0 10px"> $gp.translation_helpers = (
</span><span class="cx" style="display: block; padding: 0 10px">        function( $ ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -13,7 +13,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $( $gp.translation_helpers.table )
</span><span class="cx" style="display: block; padding: 0 10px">                                        .on( 'beforeShow', '.editor', $gp.translation_helpers.hooks.initial_fetch )
</span><span class="cx" style="display: block; padding: 0 10px">                                        .on( 'click', '.helpers-tabs li', $gp.translation_helpers.hooks.tab_select )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        .on( 'click', 'a.comment-reply-link', $gp.translation_helpers.hooks.reply_comment_form );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 .on( 'click', 'a.comment-reply-link', $gp.translation_helpers.hooks.reply_comment_form )
+                                       .on( 'click', 'a.opt-out-discussion,a.opt-in-discussion', $gp.translation_helpers.hooks.optin_optout_discussion );
</ins><span class="cx" style="display: block; padding: 0 10px">                         },
</span><span class="cx" style="display: block; padding: 0 10px">                        initial_fetch: function( $element ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                var $helpers = $element.find( '.translation-helpers' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -80,6 +81,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        $comment.text( 'Reply' );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                        },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        optin_optout_discussion: function( $link ) {
+                               var data = {
+                                       action: 'optout_discussion_notifications',
+                                       data: {
+                                               nonce: $gp_translation_helpers_settings.nonce,
+                                               originalId: $link.attr( 'data-original-id' ),
+                                               optType: $link.attr( 'data-opt-type' ),
+                                       },
+                               };
+                               $.ajax(
+                                       {
+                                               type: 'POST',
+                                               url: $gp_translation_helpers_settings.ajax_url,
+                                               data: data,
+                                       }
+                               ).done(
+                                       function() {
+                                               $gp.translation_helpers.fetch( 'discussion' );
+                                       }
+                               );
+                       },
</ins><span class="cx" style="display: block; padding: 0 10px">                         hooks: {
</span><span class="cx" style="display: block; padding: 0 10px">                                initial_fetch: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $gp.translation_helpers.initial_fetch( $( this ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -94,6 +116,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        $gp.translation_helpers.reply_comment_form( $( this ) );
</span><span class="cx" style="display: block; padding: 0 10px">                                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                optin_optout_discussion: function( event ) {
+                                       event.preventDefault();
+                                       $gp.translation_helpers.optin_optout_discussion( $( this ) );
+                                       return false;
+                               },
</ins><span class="cx" style="display: block; padding: 0 10px">                         },
</span><span class="cx" style="display: block; padding: 0 10px">                };
</span><span class="cx" style="display: block; padding: 0 10px">        }( jQuery )
</span></span></pre>
</div>
</div>

</body>
</html>