Merge "Show toasts for more change update events"
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 1744364..9745f9e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -216,7 +216,7 @@
      *     has been loaded, and false if a newer patch has been uploaded in the
      *     meantime. The promise is rejected on network error.
      */
-    fetchIsLatestKnown(change, restAPI) {
+    fetchChangeUpdates(change, restAPI) {
       const knownLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
           Gerrit.PatchSetBehavior.computeAllPatchSets(change));
       return restAPI.getChangeDetail(change._number)
@@ -226,7 +226,11 @@
             }
             const actualLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
                 Gerrit.PatchSetBehavior.computeAllPatchSets(detail));
-            return {isLatest: actualLatest <= knownLatest};
+            return {
+              isLatest: actualLatest <= knownLatest,
+              newStatus: change.status !== detail.status ? detail.status : null,
+              newMessages: change.messages.length < detail.messages.length,
+            };
           });
     },
 
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index e5adfed..7116c5d 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -35,31 +35,37 @@
       assert.equal(get(revisions, '3'), undefined);
     });
 
-    test('fetchIsLatestKnown on latest', done => {
+    test('fetchChangeUpdates on latest', done => {
       const knownChange = {
         revisions: {
           sha1: {description: 'patch 1', _number: 1},
           sha2: {description: 'patch 2', _number: 2},
         },
+        status: 'NEW',
+        messages: [],
       };
       const mockRestApi = {
         getChangeDetail() {
           return Promise.resolve(knownChange);
         },
       };
-      Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
+      Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
           .then(result => {
             assert.isTrue(result.isLatest);
+            assert.isNotOk(result.newStatus);
+            assert.isFalse(result.newMessages);
             done();
           });
     });
 
-    test('fetchIsLatestKnown not on latest', done => {
+    test('fetchChangeUpdates not on latest', done => {
       const knownChange = {
         revisions: {
           sha1: {description: 'patch 1', _number: 1},
           sha2: {description: 'patch 2', _number: 2},
         },
+        status: 'NEW',
+        messages: [],
       };
       const actualChange = {
         revisions: {
@@ -67,15 +73,81 @@
           sha2: {description: 'patch 2', _number: 2},
           sha3: {description: 'patch 3', _number: 3},
         },
+        status: 'NEW',
+        messages: [],
       };
       const mockRestApi = {
         getChangeDetail() {
           return Promise.resolve(actualChange);
         },
       };
-      Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
+      Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
           .then(result => {
             assert.isFalse(result.isLatest);
+            assert.isNotOk(result.newStatus);
+            assert.isFalse(result.newMessages);
+            done();
+          });
+    });
+
+    test('fetchChangeUpdates new status', done => {
+      const knownChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+        status: 'NEW',
+        messages: [],
+      };
+      const actualChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+        status: 'MERGED',
+        messages: [],
+      };
+      const mockRestApi = {
+        getChangeDetail() {
+          return Promise.resolve(actualChange);
+        },
+      };
+      Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+          .then(result => {
+            assert.isTrue(result.isLatest);
+            assert.equal(result.newStatus, 'MERGED');
+            assert.isFalse(result.newMessages);
+            done();
+          });
+    });
+
+    test('fetchChangeUpdates new messages', done => {
+      const knownChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+        status: 'NEW',
+        messages: [],
+      };
+      const actualChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+        status: 'NEW',
+        messages: [{message: 'blah blah'}],
+      };
+      const mockRestApi = {
+        getChangeDetail() {
+          return Promise.resolve(actualChange);
+        },
+      };
+      Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+          .then(result => {
+            assert.isTrue(result.isLatest);
+            assert.isNotOk(result.newStatus);
+            assert.isTrue(result.newMessages);
             done();
           });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index ebb5b01..27bd5e9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -1062,7 +1062,7 @@
         this._handleResponseError(response);
       };
 
-      return this.fetchIsLatestKnown(this.change, this.$.restAPI)
+      return this.fetchChangeUpdates(this.change, this.$.restAPI)
           .then(result => {
             if (!result.isLatest) {
               this.fire('show-alert', {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 30fc742..ea85d86 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -241,7 +241,7 @@
     test('submit change', done => {
       sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sandbox.stub(element, 'fetchIsLatestKnown',
+      sandbox.stub(element, 'fetchChangeUpdates',
           () => { return Promise.resolve({isLatest: true}); });
       element.change = {
         revisions: {
@@ -1264,7 +1264,7 @@
         let sendStub;
 
         setup(() => {
-          sandbox.stub(element, 'fetchIsLatestKnown')
+          sandbox.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: true}));
           sendStub = sandbox.stub(element.$.restAPI, 'getChangeURLAndSend')
               .returns(Promise.resolve({}));
@@ -1293,7 +1293,7 @@
 
       suite('failure modes', () => {
         test('non-latest', () => {
-          sandbox.stub(element, 'fetchIsLatestKnown')
+          sandbox.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: false}));
           const sendStub = sandbox.stub(element.$.restAPI,
               'getChangeURLAndSend');
@@ -1307,7 +1307,7 @@
         });
 
         test('send fails', () => {
-          sandbox.stub(element, 'fetchIsLatestKnown')
+          sandbox.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: true}));
           const sendStub = sandbox.stub(element.$.restAPI,
               'getChangeURLAndSend',
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 1857101..cd56752 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -41,6 +41,14 @@
 
   const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
 
+  const ReloadToastMessage = {
+    NEWER_REVISION: 'A newer patch set has been uploaded',
+    RESTORED: 'This change has been restored',
+    ABANDONED: 'This change has been abandoned',
+    MERGED: 'This change has been merged',
+    NEW_MESSAGE: 'There are new messages on this change',
+  };
+
   Polymer({
     is: 'gr-change-view',
 
@@ -1241,23 +1249,37 @@
       }
 
       this._updateCheckTimerHandle = this.async(() => {
-        this.fetchIsLatestKnown(this._change, this.$.restAPI)
+        this.fetchChangeUpdates(this._change, this.$.restAPI)
             .then(result => {
-              if (result.isLatest) {
-                this._startUpdateCheckTimer();
-              } else {
-                this._cancelUpdateCheckTimer();
-                this.fire('show-alert', {
-                  message: 'A newer patch set has been uploaded.',
-                  // Persist this alert.
-                  dismissOnNavigation: true,
-                  action: 'Reload',
-                  callback: function() {
-                    // Load the current change without any patch range.
-                    Gerrit.Nav.navigateToChange(this._change);
-                  }.bind(this),
-                });
+              let toastMessage = null;
+              if (!result.isLatest) {
+                toastMessage = ReloadToastMessage.NEWER_REVISION;
+              } else if (result.newStatus === this.ChangeStatus.MERGED) {
+                toastMessage = ReloadToastMessage.MERGED;
+              } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+                toastMessage = ReloadToastMessage.ABANDONED;
+              } else if (result.newStatus === this.ChangeStatus.NEW) {
+                toastMessage = ReloadToastMessage.RESTORED;
+              } else if (result.newMessages) {
+                toastMessage = ReloadToastMessage.NEW_MESSAGE;
               }
+
+              if (!toastMessage) {
+                this._startUpdateCheckTimer();
+                return;
+              }
+
+              this._cancelUpdateCheckTimer();
+              this.fire('show-alert', {
+                message: toastMessage,
+                // Persist this alert.
+                dismissOnNavigation: true,
+                action: 'Reload',
+                callback: function() {
+                  // Load the current change without any patch range.
+                  Gerrit.Nav.navigateToChange(this._change);
+                }.bind(this),
+              });
             });
       }, this._serverConfig.change.update_delay * 1000);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index e784857..d8004e5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -120,7 +120,7 @@
 
       test('A toggles overlay when logged in', done => {
         sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-        sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown')
+        sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
             .returns(Promise.resolve({isLatest: true}));
         element._change = {labels: {}};
         const openSpy = sandbox.spy(element, '_openReplyDialog');
@@ -967,7 +967,7 @@
     suite('reply dialog tests', () => {
       setup(() => {
         sandbox.stub(element.$.replyDialog, '_draftChanged');
-        sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown',
+        sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
             () => { return Promise.resolve({isLatest: true}); });
         element._change = {labels: {}};
       });
@@ -1018,7 +1018,7 @@
 
     suite('commit message expand/collapse', () => {
       setup(() => {
-        sandbox.stub(element, 'fetchIsLatestKnown',
+        sandbox.stub(element, 'fetchChangeUpdates',
             () => { return Promise.resolve({isLatest: false}); });
       });
 
@@ -1169,29 +1169,58 @@
         });
 
         test('_startUpdateCheckTimer negative delay', () => {
-          sandbox.stub(element, 'fetchIsLatestKnown');
+          sandbox.stub(element, 'fetchChangeUpdates');
 
           element._serverConfig = {change: {update_delay: -1}};
 
           assert.isTrue(element._startUpdateCheckTimer.called);
-          assert.isFalse(element.fetchIsLatestKnown.called);
+          assert.isFalse(element.fetchChangeUpdates.called);
         });
 
         test('_startUpdateCheckTimer up-to-date', () => {
-          sandbox.stub(element, 'fetchIsLatestKnown',
+          sandbox.stub(element, 'fetchChangeUpdates',
               () => { return Promise.resolve({isLatest: true}); });
 
           element._serverConfig = {change: {update_delay: 12345}};
 
           assert.isTrue(element._startUpdateCheckTimer.called);
-          assert.isTrue(element.fetchIsLatestKnown.called);
+          assert.isTrue(element.fetchChangeUpdates.called);
           assert.equal(element.async.lastCall.args[1], 12345 * 1000);
         });
 
         test('_startUpdateCheckTimer out-of-date shows an alert', done => {
-          sandbox.stub(element, 'fetchIsLatestKnown',
+          sandbox.stub(element, 'fetchChangeUpdates',
               () => { return Promise.resolve({isLatest: false}); });
-          element.addEventListener('show-alert', () => {
+          element.addEventListener('show-alert', e => {
+            assert.equal(e.detail.message,
+                'A newer patch set has been uploaded');
+            done();
+          });
+          element._serverConfig = {change: {update_delay: 12345}};
+        });
+
+        test('_startUpdateCheckTimer new status shows an alert', done => {
+          sandbox.stub(element, 'fetchChangeUpdates')
+              .returns(Promise.resolve({
+                isLatest: true,
+                newStatus: element.ChangeStatus.MERGED,
+              }));
+          element.addEventListener('show-alert', e => {
+            assert.equal(e.detail.message, 'This change has been merged');
+            done();
+          });
+          element._serverConfig = {change: {update_delay: 12345}};
+        });
+
+        test('_startUpdateCheckTimer new messages shows an alert', done => {
+          sandbox.stub(element, 'fetchChangeUpdates')
+              .returns(Promise.resolve({
+                isLatest: true,
+                newMessages: true,
+              }));
+          element.addEventListener('show-alert', e => {
+            assert.equal(e.detail.message,
+                'There are new messages on this change');
             done();
           });
           element._serverConfig = {change: {update_delay: 12345}};
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index b3cf2d7..22b53c8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -86,7 +86,7 @@
         ],
       };
       element.serverConfig = {note_db_enabled: true};
-      sandbox.stub(element, 'fetchIsLatestKnown')
+      sandbox.stub(element, 'fetchChangeUpdates')
           .returns(Promise.resolve({isLatest: true}));
     };
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 7d31ebe..6a41652 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -237,7 +237,7 @@
 
     open(opt_focusTarget) {
       this.knownLatestState = LatestPatchState.CHECKING;
-      this.fetchIsLatestKnown(this.change, this.$.restAPI)
+      this.fetchChangeUpdates(this.change, this.$.restAPI)
           .then(result => {
             this.knownLatestState = result.isLatest ?
                 LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 02bd614..9c19267 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -103,7 +103,7 @@
       eraseDraftCommentStub = sandbox.stub(element.$.storage,
           'eraseDraftComment');
 
-      sandbox.stub(element, 'fetchIsLatestKnown')
+      sandbox.stub(element, 'fetchChangeUpdates')
           .returns(Promise.resolve({isLatest: true}));
 
       // Allow the elements created by dom-repeat to be stamped.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 55acfd5..45def55 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -845,6 +845,7 @@
           this.ListChangesOption.CHANGE_ACTIONS,
           this.ListChangesOption.CURRENT_ACTIONS,
           this.ListChangesOption.DOWNLOAD_COMMANDS,
+          this.ListChangesOption.MESSAGES,
           this.ListChangesOption.SUBMITTABLE,
           this.ListChangesOption.WEB_LINKS
       );