Highlight messages with positive and negative scores

http://imgur.com/a/NPi95

Quick and dirty fix via parsing server-generated messages
For the long-term fix, Issue 6347 filed.

Highlighted messages have a colored left border.

Chrome CI labels are ignored:
'Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'.

Bug: Issue 6302
Change-Id: I5f10bcbe71718082b583d183d36d76a18b82d7e2
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 1809ed7..d30a888 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -36,6 +36,9 @@
       :host(.expanded) {
         cursor: auto;
       }
+      :host > div {
+        padding: 0 var(--default-horizontal-margin);
+      }
       gr-avatar {
         position: absolute;
         left: var(--default-horizontal-margin);
@@ -125,8 +128,14 @@
       .replyContainer {
         padding: .5em 0 1em;
       }
+      .positiveVote {
+        box-shadow: inset 0 4.4em #d4ffd4;
+      }
+      .negativeVote {
+        box-shadow: inset 0 4.4em #ffd4d4;
+      }
     </style>
-    <div class$="[[_computeClass(_expanded, showAvatar)]]">
+    <div class$="[[_computeClass(_expanded, showAvatar, message)]]">
       <gr-avatar account="[[author]]" image-size="100"></gr-avatar>
       <div class="contentContainer">
         <div class="author" on-tap="_handleAuthorTap">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 919ef6b..559217a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -14,6 +14,10 @@
 (function() {
   'use strict';
 
+  const CI_LABELS = ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'];
+  const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /;
+  const LABEL_TITLE_SCORE_PATTERN = /([A-Za-z0-9-]+)([+-]\d+)/;
+
   Polymer({
     is: 'gr-message',
 
@@ -161,10 +165,41 @@
       return event.type === 'REVIEWER_UPDATE';
     },
 
-    _computeClass(expanded, showAvatar) {
+    _isMessagePositive(message) {
+      if (!message.message) { return null; }
+      const line = message.message.split('\n', 1)[0];
+      const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+      if (!line.match(patchSetPrefix)) { return null;}
+      const scoresRaw = line.split(patchSetPrefix)[1];
+      if (!scoresRaw) { return null; }
+      const scores = scoresRaw.split(' ');
+      if (!scores.length) { return null; }
+      const {min, max} = scores
+          .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+          .filter(ms => ms && ms.length === 3)
+          .filter(([, label]) => !CI_LABELS.includes(label))
+          .map(([, , score]) => score)
+          .map(s => parseInt(s, 10))
+          .reduce(({min, max}, s) =>
+              ({min: (s < min ? s : min), max: (s > max ? s : max)}),
+              {min: 0, max: 0});
+      if (max - min === 0) {
+        return 0;
+      } else {
+        return (max + min) > 0 ? 1 : -1;
+      }
+    },
+
+    _computeClass(expanded, showAvatar, message) {
       const classes = [];
       classes.push(expanded ? 'expanded' : 'collapsed');
       classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
+      const scoreQuality = this._isMessagePositive(message);
+      if (scoreQuality === 1) {
+        classes.push('positiveVote');
+      } else if (scoreQuality === -1) {
+        classes.push('negativeVote');
+      }
       return classes.join(' ');
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 8638edd..96f5dfb 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -35,11 +35,13 @@
   suite('gr-message tests', () => {
     let element;
 
-    setup(() => {
+    setup(done => {
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(false); },
+        getConfig() { return Promise.resolve({}); },
       });
       element = fixture('basic');
+      flush(done);
     });
 
     test('reply event', done => {
@@ -162,5 +164,37 @@
       delete message.updated_by;
       assert.isNotOk(element._computeShowOnBehalfOf(message));
     });
+
+    ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
+      test(`${label} ignored for color voting`, () => {
+        element.message = {
+          author: {},
+          expanded: false,
+          message: `Patch Set 1: ${label}+1`,
+        };
+        assert.isNotOk(
+            Polymer.dom(element.root).querySelector('.negativeVote'));
+        assert.isNotOk(
+            Polymer.dom(element.root).querySelector('.positiveVote'));
+      });
+    });
+
+    test('negative vote', () => {
+      element.message = {
+        author: {},
+        expanded: false,
+        message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Ready+1',
+      };
+      assert.isOk(Polymer.dom(element.root).querySelector('.negativeVote'));
+    });
+
+    test('positive vote', () => {
+      element.message = {
+        author: {},
+        expanded: false,
+        message: 'Patch Set 1: Verified-1 Code-Review+2 Trybot-Ready-1',
+      };
+      assert.isOk(Polymer.dom(element.root).querySelector('.positiveVote'));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 6338c96..95ca60e 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -33,8 +33,7 @@
         margin-bottom: .35em;
       }
       .header,
-      #messageControlsContainer,
-      gr-message {
+      #messageControlsContainer {
         padding: 0 var(--default-horizontal-margin);
       }
       .highlighted {