Merge changes from topics "gr-file-list-to-ts", "gr-smart-search-to-ts"

* changes:
  Convert files to typescript
  Rename files to preserve history
  Convert files to typescript
  Rename files to preserve history
  Convert gr-smart-search to typescript
  Rename files to preserve history
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 8a71c1c..ddc4f79 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -125,7 +125,8 @@
     extractMailExample("DeleteVoteHtml.soy");
     extractMailExample("Footer.soy");
     extractMailExample("FooterHtml.soy");
-    extractMailExample("HeaderHtml.soy");
+    extractMailExample("ChangeHeader.soy");
+    extractMailExample("ChangeHeaderHtml.soy");
     extractMailExample("HttpPasswordUpdate.soy");
     extractMailExample("HttpPasswordUpdateHtml.soy");
     extractMailExample("InboundEmailRejection.soy");
diff --git a/java/com/google/gerrit/server/mail/EmailSettings.java b/java/com/google/gerrit/server/mail/EmailSettings.java
index c411af5..1ac6eb6 100644
--- a/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -38,6 +38,7 @@
   public final Encryption encryption;
   public final long fetchInterval; // in milliseconds
   public final boolean sendNewPatchsetEmails;
+  public final boolean isAttentionSetEnabled;
 
   @Inject
   EmailSettings(@GerritServerConfig Config cfg) {
@@ -60,5 +61,6 @@
             TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS),
             TimeUnit.MILLISECONDS);
     sendNewPatchsetEmails = cfg.getBoolean("change", null, "sendNewPatchsetEmails", true);
+    isAttentionSetEnabled = cfg.getBoolean("change", null, "enableAttentionSet", false);
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 6637f99..8d76e23 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
 
 import com.google.common.base.Splitter;
@@ -128,6 +129,10 @@
   /** Format the message body by calling {@link #appendText(String)}. */
   @Override
   protected void format() throws EmailException {
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("ChangeHeaderHtml"));
+    }
+    appendText(textTemplate("ChangeHeader"));
     formatChange();
     appendText(textTemplate("ChangeFooter"));
     if (useHtml()) {
@@ -505,6 +510,13 @@
     for (Account.Id attentionUser : currentAttentionSet) {
       footers.add(MailHeader.ATTENTION.withDelimiter() + getNameEmailFor(attentionUser));
     }
+    // Since this would be user visible, only show it if attention set is enabled
+    if (args.settings.isAttentionSetEnabled && !currentAttentionSet.isEmpty()) {
+      // We need names rather than account ids / emails to make it user readable.
+      soyContext.put(
+          "attentionSet",
+          currentAttentionSet.stream().map(this::getNameFor).collect(toImmutableSet()));
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
index 623bdc2..1b58057 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
@@ -45,6 +45,8 @@
     "AddToAttentionSetHtml.soy",
     "ChangeFooter.soy",
     "ChangeFooterHtml.soy",
+    "ChangeHeader.soy",
+    "ChangeHeaderHtml.soy",
     "ChangeSubject.soy",
     "Comment.soy",
     "CommentHtml.soy",
@@ -60,7 +62,6 @@
     "InboundEmailRejectionHtml.soy",
     "Footer.soy",
     "FooterHtml.soy",
-    "HeaderHtml.soy",
     "HttpPasswordUpdate.soy",
     "HttpPasswordUpdateHtml.soy",
     "Merged.soy",
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 1eb274b..bef5317 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -116,9 +116,6 @@
     if (messageId == null) {
       throw new IllegalStateException("All emails must have a messageId");
     }
-    if (useHtml()) {
-      appendHtml(soyHtmlTemplate("HeaderHtml"));
-    }
     format();
     appendText(textTemplate("Footer"));
     if (useHtml()) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 3b77dd9..3430047 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -461,9 +461,12 @@
       // If we naively execute postUpdate even if the change is already merged when updateChange
       // being, then we are subject to a race where postUpdate steps are run twice if two submit
       // processes run at the same time.
-      logger.atFine().log("Skipping post-update steps for change %s", getId());
+      logger.atFine().log(
+          "Skipping post-update steps for change %s; submitter is %s", getId(), submitter);
       return;
     }
+    logger.atFine().log(
+        "Begin post-update steps for change %s; submitter is %s", getId(), submitter);
     postUpdateImpl(ctx);
 
     if (command != null) {
@@ -483,6 +486,9 @@
       }
     }
 
+    logger.atFine().log(
+        "Begin sending emails for submitting change %s; submitter is %s", getId(), submitter);
+
     // Assume the change must have been merged at this point, otherwise we would
     // have failed fast in one of the other steps.
     try {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 08e70fb..61eef63 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -1363,6 +1364,75 @@
   }
 
   @Test
+  @GerritConfig(name = "change.enableAttentionSet", value = "true")
+  public void attentionSetEmailHeader() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestAccount user2 = accountCreator.user2();
+    // Add user and user2 to the attention set.
+    change(r)
+        .current()
+        .review(
+            ReviewInput.create().reviewer(user.email()).reviewer(accountCreator.user2().email()));
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
+        .contains(
+            "Attention is currently required from: "
+                + user2.fullName()
+                + ", "
+                + user.fullName()
+                + ".");
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody())
+        .contains(
+            "Attention is currently required from: "
+                + user2.fullName()
+                + ", "
+                + user.fullName()
+                + ".");
+    sender.clear();
+
+    // Irrelevant reply, User and User2 are still in the attention set.
+    change(r).current().review(ReviewInput.approve());
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
+        .contains(
+            "Attention is currently required from: "
+                + user2.fullName()
+                + ", "
+                + user.fullName()
+                + ".");
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody())
+        .contains(
+            "Attention is currently required from: "
+                + user2.fullName()
+                + ", "
+                + user.fullName()
+                + ".");
+    sender.clear();
+
+    // Abandon the change which removes user from attention set; there is an email but without the
+    // attention footer.
+    change(r).abandon();
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
+        .doesNotContain("Attention is currently required");
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody())
+        .doesNotContain("Attention is currently required");
+    sender.clear();
+  }
+
+  @Test
+  @GerritConfig(name = "change.enableAttentionSet", value = "false")
+  public void noReferenceToAttentionSetInEmailsWhenDisabled() throws Exception {
+    PushOneCommit.Result r = createChange();
+    // Add user and to the attention set.
+    change(r).addReviewer(user.id().toString());
+
+    // Attention set is not referenced.
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
+        .doesNotContain("Attention is currently required");
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody())
+        .doesNotContain("Attention is currently required");
+    sender.clear();
+  }
+
+  @Test
   public void attentionSetWithEmailFilter() throws Exception {
     PushOneCommit.Result r = createChange();
 
diff --git a/polygerrit-ui/app/constants/messages.ts b/polygerrit-ui/app/constants/messages.ts
index 15692bd..5b4a534 100644
--- a/polygerrit-ui/app/constants/messages.ts
+++ b/polygerrit-ui/app/constants/messages.ts
@@ -15,10 +15,6 @@
  * limitations under the License.
  */
 
-/** @desc Default message shown when no threads in gr-thread-list */
-export const NO_THREADS_MSG =
-  'There are no inline comment threads on any diff for this change.';
-
 /** @desc Message shown when no threads in gr-thread-list for robot comments */
 export const NO_ROBOT_COMMENTS_THREADS_MSG =
   'There are no findings for this patchset.';
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index f0b8372..6af8960 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -637,6 +637,7 @@
           logged-in="[[_loggedIn]]"
           only-show-robot-comments-with-human-reply=""
           on-thread-list-modified="_handleReloadDiffComments"
+          unresolved-only
         ></gr-thread-list>
       </template>
       <template
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 245c65f..2e9189e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -61,7 +61,7 @@
   @property({type: Object})
   _labelValues?: LabelValuesMap;
 
-  getLabelValues(): LabelNameToValuesMap {
+  getLabelValues(includeDefaults = true): LabelNameToValuesMap {
     const labels: LabelNameToValuesMap = {};
     if (this.shadowRoot === null || !this.change) {
       return labels;
@@ -106,8 +106,12 @@
         prevValNum = prevVal;
       }
 
+      const defValNum = this._getDefaultValue(this.change.labels, label);
+
       if (selectedVal !== prevValNum) {
-        labels[label] = selectedVal;
+        if (includeDefaults || !!prevValNum || selectedVal !== defValNum) {
+          labels[label] = selectedVal;
+        }
       }
     }
     return labels;
@@ -126,6 +130,12 @@
     return numberValue;
   }
 
+  _getDefaultValue(labels?: LabelNameToInfoMap, labelName?: string) {
+    if (!labelName || !labels?.[labelName]) return undefined;
+    const labelInfo = labels[labelName] as DetailedLabelInfo;
+    return labelInfo.default_value;
+  }
+
   _getVoteForAccount(
     labels: LabelNameToInfoMap | undefined,
     labelName: string,
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
index ffc17cd..ae639e1 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
@@ -99,6 +99,22 @@
     });
   });
 
+  test('getLabelValues includeDefaults', async () => {
+    element.change = {
+      _number: '123',
+      labels: {
+        'Code-Review': {
+          values: {'0': 'meh', '+1': 'good', '-1': 'bad'},
+          default_value: 0,
+        },
+      },
+    };
+    await flush();
+
+    assert.deepEqual(element.getLabelValues(true), {'Code-Review': 0});
+    assert.deepEqual(element.getLabelValues(false), {});
+  });
+
   test('_getVoteForAccount', () => {
     const labelName = 'Code-Review';
     assert.strictEqual(element._getVoteForAccount(
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 50bf411..b3478b1 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -321,6 +321,11 @@
   @property({type: Boolean})
   _reviewersMutated = false;
 
+  /**
+   * Signifies that the user has changed their vote on a label or (if they have
+   * not yet voted on a label) if a selected vote is different from the default
+   * vote.
+   */
   @property({type: Boolean})
   _labelsChanged = false;
 
@@ -950,7 +955,9 @@
     '_ccs.*',
     'change',
     'draftCommentThreads',
-    '_includeComments'
+    '_includeComments',
+    '_labelsChanged',
+    'draft'
   )
   _computeNewAttention(
     currentUser?: AccountInfo,
@@ -961,7 +968,9 @@
     ccs?: PolymerDeepPropertyChange<AccountInfoInput[], AccountInfoInput[]>,
     change?: ChangeInfo,
     draftCommentThreads?: CommentThread[],
-    includeComments?: boolean
+    includeComments?: boolean,
+    _labelsChanged?: boolean,
+    draft?: boolean
   ) {
     if (
       currentUser === undefined ||
@@ -977,6 +986,10 @@
     // The draft comments are only relevant for the attention set as long as the
     // user actually plans to publish their drafts.
     draftCommentThreads = includeComments ? draftCommentThreads : [];
+    const hasDraft = draftCommentThreads.length > 0 || !!draft;
+    const hasVote = !!_labelsChanged;
+    const isOwner = this._isOwner(currentUser, change);
+    const isUploader = this._uploader?._account_id === currentUser._account_id;
     this._attentionCcsCount = removeServiceUsers(ccs.base).length;
     this._currentAttentionSet = new Set(
       Object.keys(change.attention_set || {}).map(
@@ -991,24 +1004,21 @@
       );
       // Remove the current user.
       newAttention.delete(currentUser._account_id);
-      // Add all new reviewers.
+      // Add all new reviewers, but not the current reviewer, if they are also
+      // sending a draft or a label vote.
+      const notIsReviewerAndHasDraftOrLabel = (r: AccountInfo) =>
+        !(r._account_id === currentUser._account_id && (hasDraft || hasVote));
       reviewers.base
         .filter(r => r._pendingAdd && r._account_id)
+        .filter(notIsReviewerAndHasDraftOrLabel)
         .forEach(r => newAttention.add(r._account_id!));
-      // Add the uploader, if someone else replies.
-      if (
-        this._uploader &&
-        this._uploader._account_id !== currentUser._account_id
-      ) {
-        // An uploader must have an _account_id.
-        newAttention.add(this._uploader._account_id!);
-      }
-      // Add the owner, if someone else replies. Also add the owner, if the
-      // attention set would otherwise be empty.
-      if (change.owner) {
-        if (!this._isOwner(currentUser, change)) {
-          // A change owner must have an _account_id.
-          newAttention.add(change.owner._account_id!);
+      // Add owner and uploader, if someone else replies.
+      if (hasDraft || hasVote) {
+        if (this._uploader?._account_id && !isUploader) {
+          newAttention.add(this._uploader._account_id);
+        }
+        if (change.owner?._account_id && !isOwner) {
+          newAttention.add(change.owner._account_id);
         }
       }
     } else {
@@ -1368,7 +1378,7 @@
 
   _handleLabelsChanged() {
     this._labelsChanged =
-      Object.keys(this.$.labelScores.getLabelValues()).length !== 0;
+      Object.keys(this.$.labelScores.getLabelValues(false)).length !== 0;
   }
 
   _isState(knownLatestState?: LatestPatchState, value?: LatestPatchState) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 4b0bc24..c56a5c9 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -412,6 +412,7 @@
               data-label="Edit"
               data-action-type="change"
               data-action-key="edit"
+              has-tooltip=""
               title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
               role="button"
               tabindex="0"
@@ -552,14 +553,14 @@
         </template>
         <template
           is="dom-if"
-          if="[[_computeShowAttentionTip(account, _owner, _currentAttentionSet, _newAttentionSet)]]"
+          if="[[_computeShowAttentionTip(_account, _owner, _currentAttentionSet, _newAttentionSet)]]"
         >
           <div class="attentionTip">
             <iron-icon
               class="pointer"
               icon="gr-icons:lightbulb-outline"
             ></iron-icon>
-            Be mindful of requiring attention from too many reviewers.
+            Be mindful of requiring attention from too many users.
           </div>
         </template>
       </section>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 7af3792..7a89f3d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -283,6 +283,33 @@
     checkComputeAttention('MERGED', 1, [22, 33], 1, [22, 33], [], [22, 33]);
   });
 
+  test('computeNewAttention when adding reviewers', () => {
+    const user = {_account_id: 1};
+    const reviewers = {base: [
+      {_account_id: 1, _pendingAdd: true},
+      {_account_id: 2, _pendingAdd: true},
+    ]};
+    const change = {
+      owner: {_account_id: 5},
+      status: 'NEW',
+      attention_set: {},
+    };
+    element.change = change;
+    element._reviewers = reviewers.base;
+    flush();
+
+    element._computeNewAttention(user, reviewers, [], change, [], true);
+    assert.sameMembers([...element._newAttentionSet], [1, 2]);
+
+    // If the user votes on the change, then they should not be added to the
+    // attention set, even if they have just added themselves as reviewer.
+    // But voting should also add the owner (5).
+    const labelsChanged = true;
+    element._computeNewAttention(
+        user, reviewers, [], change, [], true, labelsChanged);
+    assert.sameMembers([...element._newAttentionSet], [2, 5]);
+  });
+
   test('computeNewAttentionAccounts', () => {
     element._reviewers = [
       {_account_id: 123, display_name: 'Ernie'},
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 7585d16..6a32834 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -24,12 +24,14 @@
 import {htmlTemplate} from './gr-thread-list_html';
 import {parseDate} from '../../../utils/date-util';
 
-import {NO_THREADS_MSG} from '../../../constants/messages';
 import {CommentSide, SpecialFilePath} from '../../../constants/constants';
 import {customElement, observe, property} from '@polymer/decorators';
-import {CommentThread, isDraft, UIRobot} from '../../../utils/comment-util';
-import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
+import {
+  PolymerSpliceChange,
+  PolymerDeepPropertyChange,
+} from '@polymer/polymer/interfaces';
 import {ChangeInfo} from '../../../types/common';
+import {CommentThread, isDraft, UIRobot} from '../../../utils/comment-util';
 
 interface CommentThreadWithInfo {
   thread: CommentThread;
@@ -64,8 +66,19 @@
   @property({type: Array})
   _sortedThreads: CommentThread[] = [];
 
+  @property({
+    computed:
+      '_computeDisplayedThreads(_sortedThreads.*, unresolvedOnly, ' +
+      '_draftsOnly, onlyShowRobotCommentsWithHumanReply)',
+    type: Array,
+  })
+  _displayedThreads: CommentThread[] = [];
+
+  // thread-list is used in multiple places like the change log, hence
+  // keeping the default to be false. When used in comments tab, it's
+  // set as true.
   @property({type: Boolean})
-  _unresolvedOnly = false;
+  unresolvedOnly = false;
 
   @property({type: Boolean})
   _draftsOnly = false;
@@ -76,13 +89,53 @@
   @property({type: Boolean})
   hideToggleButtons = false;
 
-  @property({type: String})
-  emptyThreadMsg = NO_THREADS_MSG;
-
   _computeShowDraftToggle(loggedIn?: boolean) {
     return loggedIn ? 'show' : '';
   }
 
+  _showEmptyThreadsMessage(
+    threads: CommentThread[],
+    displayedThreads: CommentThread[],
+    unresolvedOnly: boolean
+  ) {
+    if (!threads || !displayedThreads) return false;
+    return !threads.length || (unresolvedOnly && !displayedThreads.length);
+  }
+
+  _computeEmptyThreadsMessage(threads: CommentThread[]) {
+    return !threads.length ? 'No comments.' : 'No unresolved comments';
+  }
+
+  _showPartyPopper(threads: CommentThread[]) {
+    return !!threads.length;
+  }
+
+  _computeResolvedCommentsMessage(
+    threads: CommentThread[],
+    displayedThreads: CommentThread[],
+    unresolvedOnly: boolean
+  ) {
+    if (unresolvedOnly && threads.length && !displayedThreads.length) {
+      return (
+        `Show ${threads.length} resolved comment` +
+        (threads.length > 1 ? 's' : '')
+      );
+    }
+    return '';
+  }
+
+  _showResolvedCommentsButton(
+    threads: CommentThread[],
+    displayedThreads: CommentThread[],
+    unresolvedOnly: boolean
+  ) {
+    return unresolvedOnly && threads.length && !displayedThreads.length;
+  }
+
+  _handleResolvedCommentsMessageClick() {
+    this.unresolvedOnly = !this.unresolvedOnly;
+  }
+
   _compareThreads(c1: CommentThreadWithInfo, c2: CommentThreadWithInfo) {
     if (c1.thread.path !== c2.thread.path) {
       // '/PATCHSET' will not come before '/COMMIT' when sorting
@@ -166,6 +219,7 @@
   ) {
     if (!threads || threads.length === 0) {
       this._sortedThreads = [];
+      this._displayedThreads = [];
       return;
     }
     // We only want to sort on thread additions / removals to avoid
@@ -196,14 +250,34 @@
       .map(threadInfo => threadInfo.thread);
   }
 
+  _computeDisplayedThreads(
+    sortedThreadsRecord?: PolymerDeepPropertyChange<
+      CommentThread[],
+      CommentThread[]
+    >,
+    unresolvedOnly?: boolean,
+    draftsOnly?: boolean,
+    onlyShowRobotCommentsWithHumanReply?: boolean
+  ) {
+    if (!sortedThreadsRecord || !sortedThreadsRecord.base) return [];
+    return sortedThreadsRecord.base.filter(t =>
+      this._shouldShowThread(
+        t,
+        unresolvedOnly,
+        draftsOnly,
+        onlyShowRobotCommentsWithHumanReply
+      )
+    );
+  }
+
   _isFirstThreadWithFileName(
-    sortedThreads: CommentThread[],
+    displayedThreads: CommentThread[],
     thread: CommentThread,
     unresolvedOnly?: boolean,
     draftsOnly?: boolean,
     onlyShowRobotCommentsWithHumanReply?: boolean
   ) {
-    const threads = sortedThreads.filter(t =>
+    const threads = displayedThreads.filter(t =>
       this._shouldShowThread(
         t,
         unresolvedOnly,
@@ -219,13 +293,13 @@
   }
 
   _shouldRenderSeparator(
-    sortedThreads: CommentThread[],
+    displayedThreads: CommentThread[],
     thread: CommentThread,
     unresolvedOnly?: boolean,
     draftsOnly?: boolean,
     onlyShowRobotCommentsWithHumanReply?: boolean
   ) {
-    const threads = sortedThreads.filter(t =>
+    const threads = displayedThreads.filter(t =>
       this._shouldShowThread(
         t,
         unresolvedOnly,
@@ -240,7 +314,7 @@
     return (
       index > 0 &&
       this._isFirstThreadWithFileName(
-        sortedThreads,
+        displayedThreads,
         thread,
         unresolvedOnly,
         draftsOnly,
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index d74c985..e55f98a 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -57,15 +57,23 @@
       border-top: 1px solid var(--border-color);
       margin-top: var(--spacing-xl);
     }
+    .resolved-comments-message {
+      color: var(--link-color);
+      cursor: pointer;
+    }
+    .show-resolved-comments {
+      box-shadow: none;
+      padding-left: var(--spacing-m);
+    }
   </style>
   <template is="dom-if" if="[[!hideToggleButtons]]">
     <div class="header">
       <div class="toggleItem">
         <paper-toggle-button
           id="unresolvedToggle"
-          checked="{{_unresolvedOnly}}"
+          checked="{{!unresolvedOnly}}"
           on-tap="_onTapUnresolvedToggle"
-          >Only unresolved threads</paper-toggle-button
+          >All comments</paper-toggle-button
         >
       </div>
       <div
@@ -75,48 +83,63 @@
           id="draftToggle"
           checked="{{_draftsOnly}}"
           on-tap="_onTapUnresolvedToggle"
-          >Only threads with drafts</paper-toggle-button
+          >Comments with drafts</paper-toggle-button
         >
       </div>
     </div>
   </template>
   <div id="threads">
-    <template is="dom-if" if="[[!threads.length]]">
-      [[emptyThreadMsg]]
+    <template
+      is="dom-if"
+      if="[[_showEmptyThreadsMessage(threads, _displayedThreads, unresolvedOnly)]]"
+    >
+      <div>
+        <span>
+          <template is="dom-if" if="[[_showPartyPopper(threads)]]">
+            <span> \&#x1F389 </span>
+          </template>
+          [[_computeEmptyThreadsMessage(threads, _displayedThreads,
+          unresolvedOnly)]]
+          <template is="dom-if" if="[[_showResolvedCommentsButton(threads, _displayedThreads, unresolvedOnly)]]">
+            <gr-button
+              class="show-resolved-comments"
+              link
+              on-click="_handleResolvedCommentsMessageClick">
+                [[_computeResolvedCommentsMessage(threads, _displayedThreads,
+                unresolvedOnly)]]
+            </gr-button>
+          </template>
+        </span>
+      </div>
     </template>
     <template
       is="dom-repeat"
-      items="[[_sortedThreads]]"
+      items="[[_displayedThreads]]"
       as="thread"
       initial-count="10"
       target-framerate="60"
     >
       <template
         is="dom-if"
-        if="[[_shouldShowThread(thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+        if="[[_shouldRenderSeparator(_displayedThreads, thread, unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
       >
-        <template
-          is="dom-if"
-          if="[[_shouldRenderSeparator(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
-        >
-          <div class="thread-separator"></div>
-        </template>
-        <gr-comment-thread
-          show-file-path=""
-          change-num="[[changeNum]]"
-          comments="[[thread.comments]]"
-          comment-side="[[thread.commentSide]]"
-          show-file-name="[[_isFirstThreadWithFileName(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
-          project-name="[[change.project]]"
-          is-on-parent="[[_isOnParent(thread.commentSide)]]"
-          line-num="[[thread.line]]"
-          patch-num="[[thread.patchNum]]"
-          path="[[thread.path]]"
-          root-id="{{thread.rootId}}"
-          on-thread-changed="_handleCommentsChanged"
-          on-thread-discard="_handleThreadDiscard"
-        ></gr-comment-thread>
+        <div class="thread-separator"></div>
       </template>
+      <gr-comment-thread
+        show-file-path=""
+        change-num="[[changeNum]]"
+        comments="[[thread.comments]]"
+        comment-side="[[thread.commentSide]]"
+        show-file-name="[[_isFirstThreadWithFileName(_displayedThreads, thread, unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+        project-name="[[change.project]]"
+        is-on-parent="[[_isOnParent(thread.commentSide)]]"
+        line-num="[[thread.line]]"
+        patch-num="[[thread.patchNum]]"
+        path="[[thread.path]]"
+        root-id="{{thread.rootId}}"
+        on-thread-changed="_handleCommentsChanged"
+        on-thread-discard="_handleThreadDiscard"
+      ></gr-comment-thread>
     </template>
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index bad3a99..efc072f 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -18,7 +18,6 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-thread-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {NO_THREADS_MSG} from '../../../constants/messages.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-thread-list');
@@ -287,13 +286,23 @@
     assert.equal(getVisibleThreads().length, element.threads.length);
   });
 
+  test('show unresolved threads if unresolvedOnly is set', done => {
+    element.unresolvedOnly = true;
+    flush();
+    const unresolvedThreads = element.threads.filter(t => t.comments.some(
+        c => c.unresolved
+    ));
+    assert.equal(getVisibleThreads().length, unresolvedThreads.length);
+    done();
+  });
+
   test('showing file name takes visible threads into account', () => {
     assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
-        element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+        element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
         element.onlyShowRobotCommentsWithHumanReply), true);
-    element._unresolvedOnly = true;
+    element.unresolvedOnly = true;
     assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
-        element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+        element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
         element.onlyShowRobotCommentsWithHumanReply), false);
   });
 
@@ -539,7 +548,7 @@
     });
   });
 
-  test('toggle unresolved only shows unresolved comments', () => {
+  test('toggle unresolved shows all comments', () => {
     MockInteractions.tap(element.shadowRoot.querySelector(
         '#unresolvedToggle'));
     flush();
@@ -617,18 +626,9 @@
     });
 
     test('default empty message should show', () => {
-      assert.equal(
-          element.shadowRoot.querySelector('#threads').textContent.trim(),
-          NO_THREADS_MSG
-      );
-    });
-
-    test('can override empty message', () => {
-      element.emptyThreadMsg = 'test';
-      assert.equal(
-          element.shadowRoot.querySelector('#threads').textContent.trim(),
-          'test'
-      );
+      assert.isTrue(
+          element.shadowRoot.querySelector('#threads').textContent.trim()
+              .includes('No comments.'));
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 61db314..e7c9a4a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -231,9 +231,6 @@
   @property({type: Boolean})
   _loggedIn = false;
 
-  @property({type: Boolean})
-  _loading = false;
-
   @property({type: String})
   _errorMessage: string | null = null;
 
@@ -331,7 +328,7 @@
     this.clear();
     if (!this.path) throw new Error('Missing required "path" property.');
     if (!this.changeNum) throw new Error('Missing required "changeNum" prop.');
-    this._loading = true;
+    this.diff = undefined;
     this._errorMessage = null;
     const whitespaceLevel = this._getIgnoreWhitespace();
 
@@ -381,7 +378,6 @@
     } finally {
       this.reporting.timeEnd(TimingLabel.TOTAL);
     }
-    this._loading = false;
   }
 
   private _getLayers(path: string, changeNum: NumericChangeId): DiffLayer[] {
@@ -411,12 +407,12 @@
     const changeNum = this.changeNum;
     const path = this.path;
     // Coverage providers do not provide data for EDIT and PARENT patch sets.
-    const basePatchNum = isNumber(this.patchRange.basePatchNum)
-      ? this.patchRange.basePatchNum
-      : undefined;
-    const patchNum = isNumber(this.patchRange.patchNum)
-      ? this.patchRange.patchNum
-      : undefined;
+
+    const toNumberOnly = (patchNum: PatchSetNum) =>
+      isNumber(patchNum) ? patchNum : undefined;
+
+    const basePatchNum = toNumberOnly(this.patchRange.basePatchNum);
+    const patchNum = toNumberOnly(this.patchRange.patchNum);
     this.$.jsAPI
       .getCoverageAnnotationApi()
       .then(coverageAnnotationApi => {
@@ -430,8 +426,8 @@
               !coverageRanges ||
               changeNum !== this.changeNum ||
               path !== this.path ||
-              basePatchNum !== this.patchRange.basePatchNum ||
-              patchNum !== this.patchRange.patchNum
+              basePatchNum !== toNumberOnly(this.patchRange.basePatchNum) ||
+              patchNum !== toNumberOnly(this.patchRange.patchNum)
             ) {
               return;
             }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
index c92fd5c..d1564b0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
@@ -32,7 +32,6 @@
     view-mode="[[viewMode]]"
     line-of-interest="[[lineOfInterest]]"
     logged-in="[[_loggedIn]]"
-    loading="[[_loading]]"
     error-message="[[_errorMessage]]"
     base-image="[[_baseImage]]"
     revision-image="[[_revisionImage]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index e3bda36..cd2c860 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -388,6 +388,13 @@
     sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
     element.patchRange = {};
 
+    // Needs to be set to something first for it to cancel.
+    element.diff = {
+      content: [{
+        a: ['foo'],
+      }],
+    };
+
     element.reload();
     assert.isTrue(cancelStub.called);
   });
@@ -824,7 +831,7 @@
   test('delegates cancel()', () => {
     const stub = sinon.stub(element.$.diff, 'cancel');
     element.patchRange = {};
-    element.reload();
+    element.cancel();
     assert.isTrue(stub.calledOnce);
     assert.equal(stub.lastCall.args.length, 0);
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 022ac80..7c317bb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -191,8 +191,9 @@
   @property({type: Object})
   lineOfInterest?: LineOfInterest;
 
-  @property({type: Boolean, observer: '_loadingChanged'})
-  loading = false;
+  /** True when diff is changed, until the content is done rendering. */
+  @property({type: Boolean})
+  _loading = false;
 
   @property({type: Boolean})
   loggedIn = false;
@@ -787,12 +788,6 @@
     this.clearDiffContent();
   }
 
-  _loadingChanged(newValue?: boolean) {
-    if (newValue) {
-      this._cleanup();
-    }
-  }
-
   _lineWrappingObserver() {
     this._prefsChanged(this.prefs);
   }
@@ -831,8 +826,9 @@
   }
 
   _diffChanged(newValue?: DiffInfo) {
+    this._loading = true;
+    this._cleanup();
     if (newValue) {
-      this._cleanup();
       this._diffLength = this.getDiffLength(newValue);
       this._debounceRenderDiffTable();
     }
@@ -890,6 +886,7 @@
   }
 
   _handleRenderContent() {
+    this._loading = false;
     this._unobserveIncrementalNodes();
     this._incrementalNodeObserver = (dom(
       this
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index 3c4642c..e9de9e7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -446,7 +446,7 @@
 
           <template
             is="dom-if"
-            if="[[showNoChangeMessage(loading, prefs, _diffLength, diff)]]"
+            if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
           >
             <div class="whitespace-change-only-message">
               This file only contains whitespace changes. Modify the whitespace
@@ -457,7 +457,7 @@
       </gr-diff-highlight>
     </gr-diff-selection>
   </div>
-  <div class$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
+  <div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
     [[_newlineWarning]]
   </div>
   <div id="loadingError" class$="[[_computeErrorClass(errorMessage)]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 0eddbc0..da2881e 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -172,9 +172,7 @@
     // we want to check before showing reloading toast
     const _accountKey = accountKey(this.account);
     this.dispatchEventThroughTarget('show-alert', {
-      detail: {
-        message: 'Reloading page...',
-      },
+      message: 'Reloading page...',
     });
     const reviewInput: Partial<ReviewInput> = {};
     reviewInput.reviewers = [
@@ -204,9 +202,7 @@
     if (!this.change || !(this.account?._account_id || this.account?.email))
       throw new Error('Missing change or account.');
     this.dispatchEventThroughTarget('show-alert', {
-      detail: {
-        message: 'Reloading page...',
-      },
+      message: 'Reloading page...',
     });
     this.$.restAPI
       .removeChangeReviewer(
@@ -241,10 +237,8 @@
   _handleClickAddToAttentionSet() {
     if (!this.change || !this.account._account_id) return;
     this.dispatchEventThroughTarget('show-alert', {
-      detail: {
-        message: 'Saving attention set update ...',
-        dismissOnNavigation: true,
-      },
+      message: 'Saving attention set update ...',
+      dismissOnNavigation: true,
     });
 
     // We are deliberately updating the UI before making the API call. It is a
@@ -273,10 +267,8 @@
   _handleClickRemoveFromAttentionSet() {
     if (!this.change || !this.account._account_id) return;
     this.dispatchEventThroughTarget('show-alert', {
-      detail: {
-        message: 'Saving attention set update ...',
-        dismissOnNavigation: true,
-      },
+      message: 'Saving attention set update ...',
+      dismissOnNavigation: true,
     });
 
     // We are deliberately updating the UI before making the API call. It is a
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index dca8115..5bcf0b8 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -477,6 +477,9 @@
   revert_submission?: ActionInfo;
   abandon?: ActionInfo;
   submit?: ActionInfo;
+  topic?: ActionInfo;
+  hashtags?: ActionInfo;
+  assignee?: ActionInfo;
 }
 
 /**
diff --git a/resources/com/google/gerrit/server/mail/HeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
similarity index 62%
copy from resources/com/google/gerrit/server/mail/HeaderHtml.soy
copy to resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 4710d8c..fde69f1 100644
--- a/resources/com/google/gerrit/server/mail/HeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -1,5 +1,5 @@
 /**
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,5 +16,17 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-{template .HeaderHtml}
+{template .ChangeHeader kind="text"}
+  {@param attentionSet: ?}
+  {if $attentionSet}
+    Attention is currently required from:{sp}
+    {for $attentionSetUser in $attentionSet}
+      {$attentionSetUser}
+      // add commas or dot.
+      {if isLast($attentionSetUser)}.
+      {else},{sp}
+      {/if}
+    {/for}
+    {\n}
+  {/if}
 {/template}
diff --git a/resources/com/google/gerrit/server/mail/HeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
similarity index 62%
rename from resources/com/google/gerrit/server/mail/HeaderHtml.soy
rename to resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index 4710d8c..ea12455 100644
--- a/resources/com/google/gerrit/server/mail/HeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -1,5 +1,5 @@
 /**
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,20 @@
  * limitations under the License.
 */
 
+
 {namespace com.google.gerrit.server.mail.template}
 
-{template .HeaderHtml}
+{template .ChangeHeaderHtml}
+  {@param attentionSet: ?}
+  {if $attentionSet}
+    <p> Attention is currently required from:{sp}
+    {for $attentionSetUser in $attentionSet}
+      {$attentionSetUser}
+      //add commas or dot.
+      {if isLast($attentionSetUser)}.
+      {else},{sp}
+      {/if}
+    {/for} </p>
+    {\n}
+  {/if}
 {/template}