Merge "Remove gitweb providing edit weblink"
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index ea9495c..4c26f89 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -37,7 +37,7 @@
 
 declare global {
   interface HTMLElementEventMap {
-    'text-changed': CustomEvent;
+    'text-changed': CustomEvent<string>;
     'value-changed': CustomEvent;
   }
   interface HTMLElementTagNameMap {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 8faa5e7..6054d9c 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -57,7 +57,7 @@
 
 declare global {
   interface HTMLElementEventMap {
-    'text-changed': CustomEvent;
+    'text-changed': CustomEvent<string>;
     'value-changed': CustomEvent;
   }
   interface HTMLElementTagNameMap {
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
new file mode 100644
index 0000000..9492fa9
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -0,0 +1,169 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {
+  ChangeMessage,
+  LabelExtreme,
+  PATCH_SET_PREFIX_PATTERN,
+} from '../../../utils/comment-util';
+import {hasOwnProperty} from '../../../utils/common-util';
+
+const VOTE_RESET_TEXT = '0 (vote reset)';
+
+interface Score {
+  label?: string;
+  value?: string;
+}
+
+export const LABEL_TITLE_SCORE_PATTERN =
+  /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.:]?$/;
+
+@customElement('gr-message-scores')
+export class GrMessageScores extends LitElement {
+  @property()
+  labelExtremes?: LabelExtreme;
+
+  @property({type: Object})
+  message?: ChangeMessage;
+
+  static override get styles() {
+    return css`
+      .score {
+        box-sizing: border-box;
+        border-radius: var(--border-radius);
+        color: var(--vote-text-color);
+        display: inline-block;
+        padding: 0 var(--spacing-s);
+        text-align: center;
+        margin-right: var(--spacing-s);
+        min-width: 115px;
+      }
+      .score.removed {
+        background-color: var(--vote-color-neutral);
+      }
+      .score.negative {
+        background-color: var(--vote-color-disliked);
+        border: 1px solid var(--vote-outline-disliked);
+        line-height: calc(var(--line-height-normal) - 2px);
+        color: var(--chip-color);
+      }
+      .score.negative.min {
+        background-color: var(--vote-color-rejected);
+        border: none;
+        padding-top: 1px;
+        padding-bottom: 1px;
+        color: var(--vote-text-color);
+      }
+      .score.positive {
+        background-color: var(--vote-color-recommended);
+        border: 1px solid var(--vote-outline-recommended);
+        line-height: calc(var(--line-height-normal) - 2px);
+        color: var(--chip-color);
+      }
+      .score.positive.max {
+        background-color: var(--vote-color-approved);
+        border: none;
+        padding-top: 1px;
+        padding-bottom: 1px;
+        color: var(--vote-text-color);
+      }
+
+      @media screen and (max-width: 50em) {
+        .score {
+          min-width: 0px;
+        }
+      }
+    `;
+  }
+
+  override render() {
+    const scores = this._getScores(this.message, this.labelExtremes);
+    return scores.map(score => this.renderScore(score));
+  }
+
+  private renderScore(score: Score) {
+    return html`<span
+      class="score ${this._computeScoreClass(score, this.labelExtremes)}"
+    >
+      ${score.label} ${score.value}
+    </span>`;
+  }
+
+  _computeScoreClass(score?: Score, labelExtremes?: LabelExtreme) {
+    // Polymer 2: check for undefined
+    if (score === undefined || labelExtremes === undefined) {
+      return '';
+    }
+    if (!score.value) {
+      return '';
+    }
+    if (score.value.includes(VOTE_RESET_TEXT)) {
+      return 'removed';
+    }
+    const classes = [];
+    if (Number(score.value) > 0) {
+      classes.push('positive');
+    } else if (Number(score.value) < 0) {
+      classes.push('negative');
+    }
+    if (score.label) {
+      const extremes = labelExtremes[score.label];
+      if (extremes) {
+        const intScore = Number(score.value);
+        if (intScore === extremes.max) {
+          classes.push('max');
+        } else if (intScore === extremes.min) {
+          classes.push('min');
+        }
+      }
+    }
+    return classes.join(' ');
+  }
+
+  _getScores(message?: ChangeMessage, labelExtremes?: LabelExtreme): Score[] {
+    if (!message || !message.message || !labelExtremes) {
+      return [];
+    }
+    const line = message.message.split('\n', 1)[0];
+    const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+    if (!line.match(patchSetPrefix)) {
+      return [];
+    }
+    const scoresRaw = line.split(patchSetPrefix)[1];
+    if (!scoresRaw) {
+      return [];
+    }
+    return scoresRaw
+      .split(' ')
+      .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+      .filter(
+        ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2])
+      )
+      .map(ms => {
+        const label = ms?.[2];
+        const value = ms?.[1] === '-' ? VOTE_RESET_TEXT : ms?.[3];
+        return {label, value};
+      });
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-message-scores': GrMessageScores;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
new file mode 100644
index 0000000..7398909
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
@@ -0,0 +1,136 @@
+/**
+ * @license
+ * Copyright (C) 2016 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-message-scores';
+import {createChangeMessage} from '../../../test/test-data-generators';
+import {queryAll} from '../../../test/test-utils';
+import {GrMessageScores} from './gr-message-scores';
+
+const basicFixture = fixtureFromElement('gr-message-scores');
+
+suite('gr-message-score tests', () => {
+  let element: GrMessageScores;
+
+  setup(async () => {
+    element = basicFixture.instantiate();
+    await element.updateComplete;
+  });
+
+  test('votes', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+    };
+    element.labelExtremes = {
+      Verified: {max: 1, min: -1},
+      'Code-Review': {max: 2, min: -2},
+      'Trybot-Label3': {max: 3, min: 0},
+    };
+    await element.updateComplete;
+    const scoreChips = queryAll(element, '.score');
+    assert.equal(scoreChips.length, 3);
+
+    assert.isTrue(scoreChips[0].classList.contains('positive'));
+    assert.isTrue(scoreChips[0].classList.contains('max'));
+
+    assert.isTrue(scoreChips[1].classList.contains('negative'));
+    assert.isTrue(scoreChips[1].classList.contains('min'));
+
+    assert.isTrue(scoreChips[2].classList.contains('positive'));
+    assert.isFalse(scoreChips[2].classList.contains('min'));
+  });
+
+  test('Uploaded patch set X', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message:
+        'Uploaded patch set 1:' +
+        'Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+    };
+    element.labelExtremes = {
+      Verified: {max: 1, min: -1},
+      'Code-Review': {max: 2, min: -2},
+      'Trybot-Label3': {max: 3, min: 0},
+    };
+    await element.updateComplete;
+    const scoreChips = queryAll(element, '.score');
+    assert.equal(scoreChips.length, 3);
+
+    assert.isTrue(scoreChips[0].classList.contains('positive'));
+    assert.isTrue(scoreChips[0].classList.contains('max'));
+
+    assert.isTrue(scoreChips[1].classList.contains('negative'));
+    assert.isTrue(scoreChips[1].classList.contains('min'));
+
+    assert.isTrue(scoreChips[2].classList.contains('positive'));
+    assert.isFalse(scoreChips[2].classList.contains('min'));
+  });
+
+  test('Uploaded and rebased', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message: 'Uploaded patch set 4: Commit-Queue+1: Patch Set 3 was rebased.',
+    };
+    element.labelExtremes = {
+      'Commit-Queue': {max: 2, min: -2},
+    };
+    await element.updateComplete;
+    const scoreChips = queryAll(element, '.score');
+    assert.equal(scoreChips.length, 1);
+    assert.isTrue(scoreChips[0].classList.contains('positive'));
+  });
+
+  test('removed votes', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
+    };
+    element.labelExtremes = {
+      Verified: {max: 1, min: -1},
+      'Code-Review': {max: 2, min: -2},
+      'Commit-Queue': {max: 3, min: 0},
+    };
+    await element.updateComplete;
+    const scoreChips = queryAll(element, '.score');
+    assert.equal(scoreChips.length, 3);
+
+    assert.isTrue(scoreChips[1].classList.contains('removed'));
+    assert.isTrue(scoreChips[2].classList.contains('removed'));
+  });
+
+  test('false negative vote', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
+    };
+    element.labelExtremes = {};
+    await element.updateComplete;
+    const scoreChips = element.shadowRoot?.querySelectorAll('.score');
+    assert.equal(scoreChips?.length, 0);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index cc7cf88..8664de3 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -22,26 +22,29 @@
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-formatted-text/gr-formatted-text';
 import '../../../styles/shared-styles';
+import '../gr-message-scores/gr-message-scores';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-message_html';
 import {MessageTag, SpecialFilePath} from '../../../constants/constants';
 import {customElement, property, computed, observe} from '@polymer/decorators';
 import {
   ChangeInfo,
-  ChangeMessageInfo,
   ServerInfo,
   ConfigInfo,
   RepoName,
   ReviewInputTag,
-  VotingRangeInfo,
   NumericChangeId,
   ChangeMessageId,
   PatchSetNum,
   AccountInfo,
   BasePatchSetNum,
 } from '../../../types/common';
-import {CommentThread} from '../../../utils/comment-util';
-import {hasOwnProperty} from '../../../utils/common-util';
+import {
+  ChangeMessage,
+  CommentThread,
+  LabelExtreme,
+  PATCH_SET_PREFIX_PATTERN,
+} from '../../../utils/comment-util';
 import {getAppContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -52,12 +55,8 @@
 } from '../../../utils/patch-set-util';
 import {isServiceUser, replaceTemplates} from '../../../utils/account-util';
 
-const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
-const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.:]?$/;
 const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
 const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
-const VOTE_RESET_TEXT = '0 (vote reset)';
-
 declare global {
   interface HTMLElementTagNameMap {
     'gr-message': GrMessage;
@@ -68,20 +67,6 @@
   id: ChangeMessageId;
 }
 
-export interface ChangeMessage extends ChangeMessageInfo {
-  // TODO(TS): maybe should be an enum instead
-  type: string;
-  expanded: boolean;
-  commentThreads: CommentThread[];
-}
-
-export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
-
-interface Score {
-  label?: string;
-  value?: string;
-}
-
 @customElement('gr-message')
 export class GrMessage extends PolymerElement {
   static get template() {
@@ -444,63 +429,6 @@
     return message.type === 'REVIEWER_UPDATE';
   }
 
-  _getScores(message?: ChangeMessage, labelExtremes?: LabelExtreme): Score[] {
-    if (!message || !message.message || !labelExtremes) {
-      return [];
-    }
-    const line = message.message.split('\n', 1)[0];
-    const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
-    if (!line.match(patchSetPrefix)) {
-      return [];
-    }
-    const scoresRaw = line.split(patchSetPrefix)[1];
-    if (!scoresRaw) {
-      return [];
-    }
-    return scoresRaw
-      .split(' ')
-      .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
-      .filter(
-        ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2])
-      )
-      .map(ms => {
-        const label = ms?.[2];
-        const value = ms?.[1] === '-' ? VOTE_RESET_TEXT : ms?.[3];
-        return {label, value};
-      });
-  }
-
-  _computeScoreClass(score?: Score, labelExtremes?: LabelExtreme) {
-    // Polymer 2: check for undefined
-    if (score === undefined || labelExtremes === undefined) {
-      return '';
-    }
-    if (!score.value) {
-      return '';
-    }
-    if (score.value.includes(VOTE_RESET_TEXT)) {
-      return 'removed';
-    }
-    const classes = [];
-    if (Number(score.value) > 0) {
-      classes.push('positive');
-    } else if (Number(score.value) < 0) {
-      classes.push('negative');
-    }
-    if (score.label) {
-      const extremes = labelExtremes[score.label];
-      if (extremes) {
-        const intScore = Number(score.value);
-        if (intScore === extremes.max) {
-          classes.push('max');
-        } else if (intScore === extremes.min) {
-          classes.push('min');
-        }
-      }
-    }
-    return classes.join(' ');
-  }
-
   _computeClass(expanded?: boolean, author?: AccountInfo) {
     const classes = [];
     classes.push(expanded ? 'expanded' : 'collapsed');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 8def279..628af83 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -144,15 +144,6 @@
       cursor: pointer;
       vertical-align: top;
     }
-    .score {
-      box-sizing: border-box;
-      border-radius: var(--border-radius);
-      color: var(--vote-text-color);
-      display: inline-block;
-      padding: 0 var(--spacing-s);
-      text-align: center;
-    }
-    .score,
     .commentsSummary {
       margin-right: var(--spacing-s);
       min-width: 115px;
@@ -163,35 +154,6 @@
     .commentsIcon {
       vertical-align: top;
     }
-    .score.removed {
-      background-color: var(--vote-color-neutral);
-    }
-    .score.negative {
-      background-color: var(--vote-color-disliked);
-      border: 1px solid var(--vote-outline-disliked);
-      line-height: calc(var(--line-height-normal) - 2px);
-      color: var(--chip-color);
-    }
-    .score.negative.min {
-      background-color: var(--vote-color-rejected);
-      border: none;
-      padding-top: 1px;
-      padding-bottom: 1px;
-      color: var(--vote-text-color);
-    }
-    .score.positive {
-      background-color: var(--vote-color-recommended);
-      border: 1px solid var(--vote-outline-recommended);
-      line-height: calc(var(--line-height-normal) - 2px);
-      color: var(--chip-color);
-    }
-    .score.positive.max {
-      background-color: var(--vote-color-approved);
-      border: none;
-      padding-top: 1px;
-      padding-bottom: 1px;
-      color: var(--vote-text-color);
-    }
     gr-account-label::part(gr-account-label-text) {
       font-weight: var(--font-weight-bold);
     }
@@ -203,7 +165,6 @@
       .expanded .content {
         padding-left: 0;
       }
-      .score,
       .commentsSummary {
         min-width: 0px;
       }
@@ -226,15 +187,10 @@
           account="[[author]]"
           class="authorLabel"
         ></gr-account-label>
-        <template
-          is="dom-repeat"
-          items="[[_getScores(message, labelExtremes)]]"
-          as="score"
-        >
-          <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
-            [[score.label]] [[score.value]]
-          </span>
-        </template>
+        <gr-message-scores
+          label-extremes="[[labelExtremes]]"
+          message="[[message]]"
+        ></gr-message-scores>
       </div>
       <template is="dom-if" if="[[_commentCountText]]">
         <div class="commentsSummary">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index 0fd39d2..2acc2a8 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -28,7 +28,6 @@
 import {
   mockPromise,
   query,
-  queryAll,
   queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
@@ -463,109 +462,6 @@
         assert.equal(actual, expected);
       });
     });
-
-    test('votes', () => {
-      element.message = {
-        ...createChangeMessage(),
-        author: {},
-        expanded: false,
-        message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
-      };
-      element.labelExtremes = {
-        Verified: {max: 1, min: -1},
-        'Code-Review': {max: 2, min: -2},
-        'Trybot-Label3': {max: 3, min: 0},
-      };
-      flush();
-      const scoreChips = queryAll(element, '.score');
-      assert.equal(scoreChips.length, 3);
-
-      assert.isTrue(scoreChips[0].classList.contains('positive'));
-      assert.isTrue(scoreChips[0].classList.contains('max'));
-
-      assert.isTrue(scoreChips[1].classList.contains('negative'));
-      assert.isTrue(scoreChips[1].classList.contains('min'));
-
-      assert.isTrue(scoreChips[2].classList.contains('positive'));
-      assert.isFalse(scoreChips[2].classList.contains('min'));
-    });
-
-    test('Uploaded patch set X', () => {
-      element.message = {
-        ...createChangeMessage(),
-        author: {},
-        expanded: false,
-        message:
-          'Uploaded patch set 1:' +
-          'Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
-      };
-      element.labelExtremes = {
-        Verified: {max: 1, min: -1},
-        'Code-Review': {max: 2, min: -2},
-        'Trybot-Label3': {max: 3, min: 0},
-      };
-      flush();
-      const scoreChips = queryAll(element, '.score');
-      assert.equal(scoreChips.length, 3);
-
-      assert.isTrue(scoreChips[0].classList.contains('positive'));
-      assert.isTrue(scoreChips[0].classList.contains('max'));
-
-      assert.isTrue(scoreChips[1].classList.contains('negative'));
-      assert.isTrue(scoreChips[1].classList.contains('min'));
-
-      assert.isTrue(scoreChips[2].classList.contains('positive'));
-      assert.isFalse(scoreChips[2].classList.contains('min'));
-    });
-
-    test('Uploaded and rebased', () => {
-      element.message = {
-        ...createChangeMessage(),
-        author: {},
-        expanded: false,
-        message:
-          'Uploaded patch set 4: Commit-Queue+1: Patch Set 3 was rebased.',
-      };
-      element.labelExtremes = {
-        'Commit-Queue': {max: 2, min: -2},
-      };
-      flush();
-      const scoreChips = queryAll(element, '.score');
-      assert.equal(scoreChips.length, 1);
-      assert.isTrue(scoreChips[0].classList.contains('positive'));
-    });
-
-    test('removed votes', () => {
-      element.message = {
-        ...createChangeMessage(),
-        author: {},
-        expanded: false,
-        message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
-      };
-      element.labelExtremes = {
-        Verified: {max: 1, min: -1},
-        'Code-Review': {max: 2, min: -2},
-        'Commit-Queue': {max: 3, min: 0},
-      };
-      flush();
-      const scoreChips = queryAll(element, '.score');
-      assert.equal(scoreChips.length, 3);
-
-      assert.isTrue(scoreChips[1].classList.contains('removed'));
-      assert.isTrue(scoreChips[2].classList.contains('removed'));
-    });
-
-    test('false negative vote', () => {
-      element.message = {
-        ...createChangeMessage(),
-        author: {},
-        expanded: false,
-        message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
-      };
-      element.labelExtremes = {};
-      const scoreChips = element.root!.querySelectorAll('.score');
-      assert.equal(scoreChips.length, 0);
-    });
   });
 
   suite('when not logged in', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 3c1baa6..851cf9a 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -41,7 +41,11 @@
   ReviewerUpdateInfo,
   VotingRangeInfo,
 } from '../../../types/common';
-import {CommentThread, isRobot} from '../../../utils/comment-util';
+import {
+  CommentThread,
+  isRobot,
+  LabelExtreme,
+} from '../../../utils/comment-util';
 import {GrMessage, MessageAnchorTapDetail} from '../gr-message/gr-message';
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {DomRepeat} from '@polymer/polymer/lib/elements/dom-repeat';
@@ -249,7 +253,7 @@
   _combinedMessages: CombinedMessage[] = [];
 
   @property({type: Object, computed: '_computeLabelExtremes(labels.*)'})
-  _labelExtremes: {[labelName: string]: VotingRangeInfo} = {};
+  _labelExtremes: LabelExtreme = {};
 
   private readonly userModel = getAppContext().userModel;
 
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 15a971a1..8fa2fa4 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -16,9 +16,10 @@
  */
 import '../../shared/gr-label-info/gr-label-info';
 import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
-import '../gr-trigger-vote-hovercard/gr-trigger-vote-hovercard';
+import '../gr-trigger-vote/gr-trigger-vote';
 import '../gr-change-summary/gr-change-summary';
 import '../../shared/gr-limited-text/gr-limited-text';
+import '../../shared/gr-vote-chip/gr-vote-chip';
 import {LitElement, css, html, TemplateResult} from 'lit';
 import {customElement, property, state} from 'lit/decorators';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -26,7 +27,6 @@
   AccountInfo,
   isDetailedLabelInfo,
   isQuickLabelInfo,
-  LabelInfo,
   LabelNameToInfoMap,
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
@@ -47,7 +47,6 @@
 import {CheckRun} from '../../../models/checks/checks-model';
 import {getResultsOf, hasResultsOf} from '../../../models/checks/checks-util';
 import {Category} from '../../../api/checks';
-import '../../shared/gr-vote-chip/gr-vote-chip';
 import {fireShowPrimaryTab} from '../../../utils/event-util';
 import {PrimaryTab} from '../../../constants/constants';
 import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
@@ -371,103 +370,8 @@
   }
 }
 
-@customElement('gr-trigger-vote')
-export class GrTriggerVote extends LitElement {
-  @property()
-  label?: string;
-
-  @property({type: Object})
-  labelInfo?: LabelInfo;
-
-  @property({type: Object})
-  change?: ParsedChangeInfo;
-
-  @property({type: Object})
-  account?: AccountInfo;
-
-  @property({type: Boolean})
-  mutable?: boolean;
-
-  static override get styles() {
-    return css`
-      :host {
-        display: block;
-      }
-      .container {
-        box-sizing: border-box;
-        border: 1px solid var(--border-color);
-        border-radius: calc(var(--border-radius) + 2px);
-        background-color: var(--background-color-primary);
-        display: flex;
-        padding: 0;
-        padding-left: var(--spacing-s);
-        padding-right: var(--spacing-xxs);
-        align-items: center;
-      }
-      .label {
-        padding-right: var(--spacing-s);
-        font-weight: var(--font-weight-bold);
-      }
-      gr-vote-chip {
-        --gr-vote-chip-width: 14px;
-        --gr-vote-chip-height: 14px;
-        margin-right: 0px;
-        margin-left: var(--spacing-xs);
-      }
-      gr-vote-chip:first-of-type {
-        margin-left: 0px;
-      }
-    `;
-  }
-
-  override render() {
-    if (!this.labelInfo) return;
-    return html`
-      <div class="container">
-        <gr-trigger-vote-hovercard
-          .labelName=${this.label}
-          .labelInfo=${this.labelInfo}
-        >
-          <gr-label-info
-            slot="label-info"
-            .change=${this.change}
-            .account=${this.account}
-            .mutable=${this.mutable}
-            .label=${this.label}
-            .labelInfo=${this.labelInfo}
-            .showAllReviewers=${false}
-          ></gr-label-info>
-        </gr-trigger-vote-hovercard>
-        <span class="label">${this.label}</span>
-        ${this.renderVotes()}
-      </div>
-    `;
-  }
-
-  private renderVotes() {
-    const {labelInfo} = this;
-    if (!labelInfo) return;
-    if (isDetailedLabelInfo(labelInfo)) {
-      const approvals = getAllUniqueApprovals(labelInfo).filter(
-        approval => !hasNeutralStatus(labelInfo, approval)
-      );
-      return approvals.map(
-        approvalInfo => html`<gr-vote-chip
-          .vote="${approvalInfo}"
-          .label="${labelInfo}"
-        ></gr-vote-chip>`
-      );
-    } else if (isQuickLabelInfo(labelInfo)) {
-      return [html`<gr-vote-chip .label="${this.labelInfo}"></gr-vote-chip>`];
-    } else {
-      return html``;
-    }
-  }
-}
-
 declare global {
   interface HTMLElementTagNameMap {
     'gr-submit-requirements': GrSubmitRequirements;
-    'gr-trigger-vote': GrTriggerVote;
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
new file mode 100644
index 0000000..d920b46
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
@@ -0,0 +1,132 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../shared/gr-label-info/gr-label-info';
+import '../../shared/gr-vote-chip/gr-vote-chip';
+import '../gr-trigger-vote-hovercard/gr-trigger-vote-hovercard';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {ParsedChangeInfo} from '../../../types/types';
+import {
+  AccountInfo,
+  isDetailedLabelInfo,
+  isQuickLabelInfo,
+  LabelInfo,
+} from '../../../api/rest-api';
+import {
+  getAllUniqueApprovals,
+  hasNeutralStatus,
+} from '../../../utils/label-util';
+
+@customElement('gr-trigger-vote')
+export class GrTriggerVote extends LitElement {
+  @property()
+  label?: string;
+
+  @property({type: Object})
+  labelInfo?: LabelInfo;
+
+  @property({type: Object})
+  change?: ParsedChangeInfo;
+
+  @property({type: Object})
+  account?: AccountInfo;
+
+  @property({type: Boolean})
+  mutable?: boolean;
+
+  static override get styles() {
+    return css`
+      :host {
+        display: block;
+      }
+      .container {
+        box-sizing: border-box;
+        border: 1px solid var(--border-color);
+        border-radius: calc(var(--border-radius) + 2px);
+        background-color: var(--background-color-primary);
+        display: flex;
+        padding: 0;
+        padding-left: var(--spacing-s);
+        padding-right: var(--spacing-xxs);
+        align-items: center;
+      }
+      .label {
+        padding-right: var(--spacing-s);
+        font-weight: var(--font-weight-bold);
+      }
+      gr-vote-chip {
+        --gr-vote-chip-width: 14px;
+        --gr-vote-chip-height: 14px;
+        margin-right: 0px;
+        margin-left: var(--spacing-xs);
+      }
+      gr-vote-chip:first-of-type {
+        margin-left: 0px;
+      }
+    `;
+  }
+
+  override render() {
+    if (!this.labelInfo) return;
+    return html`
+      <div class="container">
+        <gr-trigger-vote-hovercard
+          .labelName=${this.label}
+          .labelInfo=${this.labelInfo}
+        >
+          <gr-label-info
+            slot="label-info"
+            .change=${this.change}
+            .account=${this.account}
+            .mutable=${this.mutable}
+            .label=${this.label}
+            .labelInfo=${this.labelInfo}
+            .showAllReviewers=${false}
+          ></gr-label-info>
+        </gr-trigger-vote-hovercard>
+        <span class="label">${this.label}</span>
+        ${this.renderVotes()}
+      </div>
+    `;
+  }
+
+  private renderVotes() {
+    const {labelInfo} = this;
+    if (!labelInfo) return;
+    if (isDetailedLabelInfo(labelInfo)) {
+      const approvals = getAllUniqueApprovals(labelInfo).filter(
+        approval => !hasNeutralStatus(labelInfo, approval)
+      );
+      return approvals.map(
+        approvalInfo => html`<gr-vote-chip
+          .vote="${approvalInfo}"
+          .label="${labelInfo}"
+        ></gr-vote-chip>`
+      );
+    } else if (isQuickLabelInfo(labelInfo)) {
+      return [html`<gr-vote-chip .label="${this.labelInfo}"></gr-vote-chip>`];
+    } else {
+      return html``;
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-trigger-vote': GrTriggerVote;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
new file mode 100644
index 0000000..cd25da0
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
@@ -0,0 +1,90 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import './gr-trigger-vote';
+import {GrTriggerVote} from './gr-trigger-vote';
+import {
+  createAccountWithIdNameAndEmail,
+  createApproval,
+  createDetailedLabelInfo,
+  createParsedChange,
+  createSubmitRequirementExpressionInfo,
+  createSubmitRequirementResultInfo,
+  createNonApplicableSubmitRequirementResultInfo,
+} from '../../../test/test-data-generators';
+import {SubmitRequirementResultInfo} from '../../../api/rest-api';
+import {ParsedChangeInfo} from '../../../types/types';
+
+suite('gr-trigger-vote tests', () => {
+  let element: GrTriggerVote;
+  setup(async () => {
+    const submitRequirement: SubmitRequirementResultInfo = {
+      ...createSubmitRequirementResultInfo(),
+      description: 'Test Description',
+      submittability_expression_result: {
+        ...createSubmitRequirementExpressionInfo(),
+        expression: 'label:Verified=MAX -label:Verified=MIN',
+      },
+    };
+    const change: ParsedChangeInfo = {
+      ...createParsedChange(),
+      submit_requirements: [
+        submitRequirement,
+        createNonApplicableSubmitRequirementResultInfo(),
+      ],
+      labels: {
+        Verified: {
+          ...createDetailedLabelInfo(),
+          all: [
+            {
+              ...createApproval(),
+              value: 2,
+            },
+          ],
+        },
+      },
+    };
+    const account = createAccountWithIdNameAndEmail();
+    const label = 'Verified';
+    const labelInfo = change?.labels?.[label];
+    element = await fixture<GrTriggerVote>(
+      html`<gr-trigger-vote
+        .label="${label}"
+        .labelInfo="${labelInfo}"
+        .change="${change}"
+        .account="${account}"
+        .mutable="${false}"
+      ></gr-trigger-vote>`
+    );
+  });
+
+  test('renders', () => {
+    expect(element).shadowDom.to.equal(`<div class="container">
+      <gr-trigger-vote-hovercard>
+        <gr-label-info slot="label-info"></gr-label-info>
+      </gr-trigger-vote-hovercard>
+      <span class="label">
+        Verified
+      </span>
+      <gr-vote-chip>
+      </gr-vote-chip>
+    </div>`);
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
index 85f29cb..753835d 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
@@ -16,24 +16,19 @@
  */
 import '../gr-autocomplete/gr-autocomplete';
 import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-labeled-autocomplete_html';
-import {customElement, property} from '@polymer/decorators';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property, query} from 'lit/decorators';
 import {
   GrAutocomplete,
   AutocompleteQuery,
 } from '../gr-autocomplete/gr-autocomplete';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
 
-export interface GrLabeledAutocomplete {
-  $: {
-    autocomplete: GrAutocomplete;
-  };
-}
 @customElement('gr-labeled-autocomplete')
-export class GrLabeledAutocomplete extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrLabeledAutocomplete extends LitElement {
+  @query('#autocomplete')
+  autocomplete?: GrAutocomplete;
 
   /**
    * Fired when a value is chosen.
@@ -44,7 +39,7 @@
   @property({type: Object})
   query: AutocompleteQuery = () => Promise.resolve([]);
 
-  @property({type: String, notify: true})
+  @property({type: String})
   text = '';
 
   @property({type: String})
@@ -56,15 +51,73 @@
   @property({type: Boolean})
   disabled = false;
 
-  _handleTriggerClick(e: Event) {
+  static override get styles() {
+    return css`
+      :host {
+        display: block;
+        width: 12em;
+      }
+      #container {
+        background: var(--chip-background-color);
+        border-radius: 1em;
+        padding: var(--spacing-m);
+      }
+      #header {
+        color: var(--deemphasized-text-color);
+        font-weight: var(--font-weight-bold);
+        font-size: var(--font-size-small);
+      }
+      #body {
+        display: flex;
+      }
+      #trigger {
+        color: var(--deemphasized-text-color);
+        cursor: pointer;
+        padding-left: var(--spacing-s);
+      }
+      #trigger:hover {
+        color: var(--primary-text-color);
+      }
+    `;
+  }
+
+  override render() {
+    return html`
+      <div id="container">
+        <div id="header">${this.label}</div>
+        <div id="body">
+          <gr-autocomplete
+            id="autocomplete"
+            threshold="0"
+            .query="${this.query}"
+            ?disabled="${this.disabled}"
+            .placeholder="${this.placeholder}"
+            borderless=""
+          ></gr-autocomplete>
+          <div id="trigger" @click="${this._handleTriggerClick}">▼</div>
+        </div>
+      </div>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('text')) {
+      fire(this, 'text-changed', this.text);
+    }
+  }
+
+  // Private but used in tests.
+  _handleTriggerClick = (e: Event) => {
     // Stop propagation here so we don't confuse gr-autocomplete, which
     // listens for taps on body to try to determine when it's blurred.
     e.stopPropagation();
-    this.$.autocomplete.focus();
-  }
+    assertIsDefined(this.autocomplete);
+    this.autocomplete.focus();
+  };
 
   setText(text: string) {
-    this.$.autocomplete.setText(text);
+    assertIsDefined(this.autocomplete);
+    this.autocomplete.setText(text);
   }
 
   clear() {
@@ -73,6 +126,9 @@
 }
 
 declare global {
+  interface HTMLElementEventMap {
+    'text-changed': CustomEvent<string>;
+  }
   interface HTMLElementTagNameMap {
     'gr-labeled-autocomplete': GrLabeledAutocomplete;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts
deleted file mode 100644
index 934ab84..0000000
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * @license
- * 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.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    :host {
-      display: block;
-      width: 12em;
-    }
-    #container {
-      background: var(--chip-background-color);
-      border-radius: 1em;
-      padding: var(--spacing-m);
-    }
-    #header {
-      color: var(--deemphasized-text-color);
-      font-weight: var(--font-weight-bold);
-      font-size: var(--font-size-small);
-    }
-    #body {
-      display: flex;
-    }
-    #trigger {
-      color: var(--deemphasized-text-color);
-      cursor: pointer;
-      padding-left: var(--spacing-s);
-    }
-    #trigger:hover {
-      color: var(--primary-text-color);
-    }
-  </style>
-  <div id="container">
-    <div id="header">[[label]]</div>
-    <div id="body">
-      <gr-autocomplete
-        id="autocomplete"
-        threshold="0"
-        query="[[query]]"
-        disabled="[[disabled]]"
-        placeholder="[[placeholder]]"
-        borderless=""
-      ></gr-autocomplete>
-      <div id="trigger" on-click="_handleTriggerClick">▼</div>
-    </div>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
index d6fc45f..49b14b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
@@ -18,28 +18,51 @@
 import '../../../test/common-test-setup-karma';
 import './gr-labeled-autocomplete';
 import {GrLabeledAutocomplete} from './gr-labeled-autocomplete';
+import {assertIsDefined} from '../../../utils/common-util';
 
 const basicFixture = fixtureFromElement('gr-labeled-autocomplete');
 
 suite('gr-labeled-autocomplete tests', () => {
   let element: GrLabeledAutocomplete;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
   test('tapping trigger focuses autocomplete', () => {
     const e = {stopPropagation: () => undefined};
     const stopPropagationStub = sinon.stub(e, 'stopPropagation');
-    const autocompleteStub = sinon.stub(element.$.autocomplete, 'focus');
+    assertIsDefined(element.autocomplete);
+    const autocompleteStub = sinon.stub(element.autocomplete, 'focus');
     element._handleTriggerClick(e as Event);
     assert.isTrue(stopPropagationStub.calledOnce);
     assert.isTrue(autocompleteStub.calledOnce);
   });
 
   test('setText', () => {
-    const setTextStub = sinon.stub(element.$.autocomplete, 'setText');
+    assertIsDefined(element.autocomplete);
+    const setTextStub = sinon.stub(element.autocomplete, 'setText');
     element.setText('foo-bar');
     assert.isTrue(setTextStub.calledWith('foo-bar'));
   });
+
+  test('shadowDom', async () => {
+    element.label = 'Some label';
+    await element.updateComplete;
+
+    expect(element).shadowDom.to.equal(`
+      <div id="container">
+        <div id="header">Some label</div>
+        <div id="body">
+          <gr-autocomplete
+            id="autocomplete"
+            threshold="0"
+            borderless=""
+          ></gr-autocomplete>
+          <div id="trigger">▼</div>
+        </div>
+      </div>
+    `);
+  });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
index 31efa73..99aebb1 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
@@ -26,8 +26,9 @@
 suite('gr-repo-branch-picker tests', () => {
   let element: GrRepoBranchPicker;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await flush();
   });
 
   suite('_getRepoSuggestions', () => {
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 0fa2673..1abb0b9 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -103,6 +103,7 @@
 import {CommitInfoWithRequiredCommit} from '../elements/change/gr-change-metadata/gr-change-metadata';
 import {WebLinkInfo} from '../types/diff';
 import {
+  ChangeMessage,
   CommentThread,
   createCommentThreads,
   DraftInfo,
@@ -111,7 +112,6 @@
 import {GerritView} from '../services/router/router-model';
 import {ChangeComments} from '../elements/diff/gr-comment-api/gr-comment-api';
 import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
-import {ChangeMessage} from '../elements/change/gr-message/gr-message';
 import {
   GenerateUrlEditViewParameters,
   GenerateUrlTopicViewParams,
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 3a46e60..6ad9e71 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -15,11 +15,10 @@
  * limitations under the License.
  */
 import {PatchSetNum} from './common';
-import {Comment} from '../utils/comment-util';
+import {ChangeMessage, Comment} from '../utils/comment-util';
 import {FetchRequest} from './types';
 import {LineNumberEventDetail, MovedLinkClickedEventDetail} from '../api/diff';
 import {Category, RunStatus} from '../api/checks';
-import {ChangeMessage} from '../elements/change/gr-message/gr-message';
 
 export enum EventType {
   BIND_VALUE_CHANGED = 'bind-value-changed',
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index ee26915..501da7d 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -29,6 +29,8 @@
   RevisionPatchSetNum,
   AccountInfo,
   AccountDetailInfo,
+  ChangeMessageInfo,
+  VotingRangeInfo,
 } from '../types/common';
 import {CommentSide, SpecialFilePath} from '../constants/constants';
 import {parseDate} from './date-util';
@@ -89,6 +91,18 @@
   id: UrlEncodedCommentId;
 }
 
+export interface ChangeMessage extends ChangeMessageInfo {
+  // TODO(TS): maybe should be an enum instead
+  type: string;
+  expanded: boolean;
+  commentThreads: CommentThread[];
+}
+
+export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
+
+export const PATCH_SET_PREFIX_PATTERN =
+  /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
+
 export function sortComments<T extends SortableComment>(comments: T[]): T[] {
   return comments.slice(0).sort((c1, c2) => {
     const d1 = isDraft(c1);
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 3dff9ce9..86eb872 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -17,7 +17,7 @@
 GITILES_REPO = GERRIT
 
 # When updating Bouncy Castle, also update it in bazlets.
-BC_VERS = "1.61"
+BC_VERS = "1.64"
 HTTPCOMP_VERS = "4.5.2"
 JETTY_VERS = "9.4.36.v20210114"
 BYTE_BUDDY_VERSION = "1.10.7"
@@ -575,19 +575,19 @@
     maven_jar(
         name = "bcprov",
         artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
-        sha1 = "00df4b474e71be02c1349c3292d98886f888d1f7",
+        sha1 = "1467dac1b787b5ad2a18201c0c281df69882259e",
     )
 
     maven_jar(
         name = "bcpg",
         artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
-        sha1 = "422656435514ab8a28752b117d5d2646660a0ace",
+        sha1 = "56956a8c63ccadf62e7c678571cf86f30bd84441",
     )
 
     maven_jar(
         name = "bcpkix",
         artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
-        sha1 = "89bb3aa5b98b48e584eee2a7401b7682a46779b4",
+        sha1 = "3dac163e20110817d850d17e0444852a6d7d0bd7",
     )
 
     maven_jar(