Merge "Implement a detailed check run hovercard"
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index 65f90ae..89ee399 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -27,12 +27,10 @@
 import com.google.gerrit.server.restapi.change.CommentJson.HumanCommentFormatter;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import java.util.List;
 import java.util.Map;
 import org.kohsuke.args4j.Option;
 
-@Singleton
 public class ListChangeDrafts implements RestReadView<ChangeResource> {
   private final ChangeData.Factory changeDataFactory;
   private final Provider<CommentJson> commentJson;
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index fdb785c..658a97e 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -82,6 +82,13 @@
    * Will be shown as buttons in the header of the Checks tab.
    */
   actions?: Action[];
+
+  /**
+   * Top-level links that are not associated with a specific run or result.
+   * Will be shown as icons in the header of the Checks tab.
+   */
+  links?: Link[];
+
   runs?: CheckRun[];
 }
 
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index da7f4e8..b2d5f3b 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -275,7 +275,8 @@
   | {type: 'magnifier-dragged'}
   | {type: 'version-switcher-clicked'; button: 'base' | 'revision'}
   | {type: 'zoom-level-changed'; scale: number | 'fit'}
-  | {type: 'follow-mouse-changed'; value: boolean};
+  | {type: 'follow-mouse-changed'; value: boolean}
+  | {type: 'background-color-changed'; value: string};
 
 export enum GrDiffLineType {
   ADD = 'add',
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 363f6e5..689347a 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -58,6 +58,13 @@
 }
 
 /**
+ * @desc Templates that can be used in change log messages.
+ */
+export enum ChangeMessageTemplate {
+  ACCOUNT_TEMPLATE = '<GERRIT_ACCOUNT_(\\d+)>',
+}
+
+/**
  * @desc Modes for gr-diff-cursor
  * The scroll behavior for the cursor. Values are 'never' and
  * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index aa28f04..c41fe57 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -189,10 +189,6 @@
     );
   }
 
-  _computeHideEditClass(section: PermissionAccessSection) {
-    return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
-  }
-
   _handleAddedPermissionRemoved(e: PolymerDomRepeatEvent) {
     if (!this._permissions) {
       return;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
index ba089f6..98d21f9 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
@@ -25,9 +25,6 @@
       color: var(--deemphasized-text-color);
       content: ' *';
     }
-    .inputUpdateBtn {
-      margin-top: var(--spacing-s);
-    }
   </style>
   <style include="gr-form-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index db33344..b1cd6bc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -35,7 +35,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {appContext} from '../../../services/app-context';
-import {fetchChangeUpdates, CURRENT} from '../../../utils/patch-set-util';
+import {CURRENT} from '../../../utils/patch-set-util';
 import {
   changeIsOpen,
   isOwner,
@@ -383,6 +383,8 @@
 
   private readonly jsAPI = appContext.jsApiService;
 
+  private readonly changeService = appContext.changeService;
+
   @property({type: Object})
   change?: ChangeViewChangeInfo;
 
@@ -1687,10 +1689,6 @@
     });
   }
 
-  _handleShowRevertSubmissionChangesConfirm() {
-    this._hideAllDialogs();
-  }
-
   _handleResponseError(
     action: UIActionInfo,
     response: Response | undefined | null,
@@ -1749,7 +1747,7 @@
         new Error('Properties change and changeNum must be set.')
       );
     }
-    return fetchChangeUpdates(change, this.restApiService).then(result => {
+    return this.changeService.fetchChangeUpdates(change).then(result => {
       if (!result.isLatest) {
         this.dispatchEvent(
           new CustomEvent<ShowAlertEventDetail>('show-alert', {
@@ -1796,10 +1794,6 @@
     });
   }
 
-  _handleAbandonTap() {
-    this._showActionDialog(this.$.confirmAbandonDialog);
-  }
-
   _handleCherrypickTap() {
     if (!this.change) {
       throw new Error('The change property must be set');
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 69f2954..3d55097 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -140,7 +140,7 @@
   change?: ParsedChangeInfo;
 
   @property({type: Object})
-  revertSubmittedChange?: ChangeInfo;
+  revertedChange?: ChangeInfo;
 
   @property({type: Object, notify: true})
   labels?: LabelNameToInfoMap;
@@ -584,9 +584,11 @@
 
   _getRevertSectionTitle(
     _change?: ParsedChangeInfo,
-    revertSubmittedChange?: ChangeInfo
+    revertedChange?: ChangeInfo
   ) {
-    return revertSubmittedChange ? 'Revert Submitted As' : 'Revert Created As';
+    return revertedChange?.status === ChangeStatus.MERGED
+      ? 'Revert Submitted As'
+      : 'Revert Created As';
   }
 
   _showRevertCreatedAs(change?: ParsedChangeInfo) {
@@ -594,18 +596,12 @@
     return getRevertCreatedChangeIds(change.messages).length > 0;
   }
 
-  _computeRevertCommit(
-    change?: ParsedChangeInfo,
-    revertSubmittedChange?: ChangeInfo
-  ) {
-    if (
-      revertSubmittedChange?.current_revision &&
-      revertSubmittedChange?.revisions
-    ) {
+  _computeRevertCommit(change?: ParsedChangeInfo, revertedChange?: ChangeInfo) {
+    if (revertedChange?.current_revision && revertedChange?.revisions) {
       return {
         commit: this._computeMergedCommitInfo(
-          revertSubmittedChange.current_revision,
-          revertSubmittedChange.revisions
+          revertedChange.current_revision,
+          revertedChange.revisions
         ),
       };
     }
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 50bb5ac..9f0c780 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -356,12 +356,12 @@
         class$="[[_computeDisplayState(_showAllSections, change, _SECTION.REVERT_CREATED_AS)]]"
       >
         <span class="title"
-          >[[_getRevertSectionTitle(change, revertSubmittedChange)]]</span
+          >[[_getRevertSectionTitle(change, revertedChange)]]</span
         >
         <span class="value">
           <gr-commit-info
             change="[[change]]"
-            commit-info="[[_computeRevertCommit(change, revertSubmittedChange)]]"
+            commit-info="[[_computeRevertCommit(change, revertedChange)]]"
             server-config="[[serverConfig]]"
           ></gr-commit-info>
         </span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 696b9e4..89f7b89 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -19,7 +19,6 @@
 import {GrLitElement} from '../../lit/gr-lit-element';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {appContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {
   CheckResult,
   CheckRun,
@@ -264,10 +263,6 @@
 
 @customElement('gr-change-summary')
 export class GrChangeSummary extends GrLitElement {
-  private readonly newChangeSummaryUiEnabled = appContext.flagsService.isEnabled(
-    KnownExperimentId.NEW_CHANGE_SUMMARY_UI
-  );
-
   @property({type: Object})
   changeComments?: ChangeComments;
 
@@ -312,9 +307,6 @@
           display: block;
           color: var(--deemphasized-text-color);
           max-width: 650px;
-          /* temporary for old checks status */
-        }
-        :host.new-change-summary-true {
           margin-bottom: var(--spacing-m);
         }
         .zeroState {
@@ -500,7 +492,7 @@
               )}${this.renderChecksChipForStatus(RunStatus.RUNNING, isRunning)}
             </td>
           </tr>
-          <tr ?hidden=${!this.newChangeSummaryUiEnabled}>
+          <tr>
             <td class="key">Comments</td>
             <td class="value">
               <span
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index b753cf9..f56209b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -38,7 +38,6 @@
 import '../gr-included-in-dialog/gr-included-in-dialog';
 import '../gr-messages-list/gr-messages-list';
 import '../gr-related-changes-list/gr-related-changes-list';
-import '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
 import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
 import '../gr-reply-dialog/gr-reply-dialog';
 import '../gr-thread-list/gr-thread-list';
@@ -52,10 +51,7 @@
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {pluralize} from '../../../utils/string-util';
-import {
-  getComputedStyleValue,
-  windowLocationReload,
-} from '../../../utils/dom-util';
+import {windowLocationReload} from '../../../utils/dom-util';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -69,7 +65,6 @@
 import {
   computeAllPatchSets,
   computeLatestPatchNum,
-  fetchChangeUpdates,
   hasEditBasedOnCurrentPatchSet,
   hasEditPatchsetLoaded,
   PatchSet,
@@ -167,39 +162,20 @@
   fireDialogChange,
   fireTitleChange,
 } from '../../../utils/event-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {GerritView} from '../../../services/router/router-model';
 import {takeUntil} from 'rxjs/operators';
 import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
 import {Subject} from 'rxjs';
-import {GrRelatedChangesListExperimental} from '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {Timing} from '../../../constants/reporting';
 import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
 import {getRevertCreatedChangeIds} from '../../../utils/message-util';
 
-const CHANGE_ID_ERROR = {
-  MISMATCH: 'mismatch',
-  MISSING: 'missing',
-};
-const CHANGE_ID_REGEX_PATTERN = /^(Change-Id:\s|Link:.*\/id\/)(I[0-9a-f]{8,40})/gm;
-
-const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
+const MIN_LINES_FOR_COMMIT_COLLAPSE = 17;
 
 const REVIEWERS_REGEX = /^(R|CC)=/gm;
 const MIN_CHECK_INTERVAL_SECS = 0;
 
-// These are the same as the breakpoint set in CSS. Make sure both are changed
-// together.
-const BREAKPOINT_RELATED_SMALL = '50em';
-const BREAKPOINT_RELATED_MED = '75em';
-
-// In the event that the related changes medium width calculation is too close
-// to zero, provide some height.
-const MINIMUM_RELATED_MAX_HEIGHT = 100;
-
-const SMALL_RELATED_HEIGHT = 400;
-
 const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
 
 const ACCIDENTAL_STARRING_LIMIT_MS = 10 * 1000;
@@ -271,8 +247,6 @@
 
   private readonly reporting = appContext.reportingService;
 
-  private readonly flagsService = appContext.flagsService;
-
   private readonly jsAPI = appContext.jsApiService;
 
   private readonly changeService = appContext.changeService;
@@ -376,8 +350,7 @@
     type: Boolean,
     computed:
       '_computeHideEditCommitMessage(_loggedIn, ' +
-      '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' +
-      '_commitCollapsible)',
+      '_editingCommitMessage, _change, _editMode)',
   })
   _hideEditCommitMessage?: boolean;
 
@@ -399,13 +372,6 @@
   @property({type: Number})
   _lineHeight?: number;
 
-  @property({
-    type: String,
-    computed:
-      '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
-  })
-  _changeIdCommitMessageError?: string;
-
   @property({type: Object})
   _patchRange?: ChangeViewPatchRange;
 
@@ -471,9 +437,6 @@
   })
   _commitCollapsible?: boolean;
 
-  @property({type: Boolean})
-  _relatedChangesCollapsed = true;
-
   @property({type: Number})
   _updateCheckTimerHandle?: number | null;
 
@@ -483,9 +446,6 @@
   })
   _editMode?: boolean;
 
-  @property({type: Boolean, observer: '_updateToggleContainerClass'})
-  _showRelatedToggle = false;
-
   @property({
     type: Boolean,
     computed: '_isParentCurrent(_currentRevisionActions)',
@@ -551,14 +511,11 @@
   @property({type: Boolean})
   _showChecksTab = false;
 
-  @property({type: Boolean})
-  _isNewChangeSummaryUiEnabled = false;
-
   @property({type: String})
   _tabState?: TabState;
 
   @property({type: Object})
-  revertSubmittedChange?: ChangeInfo;
+  revertedChange?: ChangeInfo;
 
   restApiService = appContext.restApiService;
 
@@ -600,9 +557,6 @@
     aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
       this._showChecksTab = b;
     });
-    this._isNewChangeSummaryUiEnabled = this.flagsService.isEnabled(
-      KnownExperimentId.NEW_CHANGE_SUMMARY_UI
-    );
   }
 
   constructor() {
@@ -892,11 +846,6 @@
     });
   }
 
-  _handleEditCommitMessage() {
-    this._editingCommitMessage = true;
-    this.$.commitMessageEditor.focusTextarea();
-  }
-
   _handleCommitMessageSave(e: EditableContentSaveEvent) {
     assertIsDefined(this._change, '_change');
     if (!this._changeNum)
@@ -958,19 +907,13 @@
     loggedIn: boolean,
     editing: boolean,
     change: ChangeInfo,
-    editMode?: boolean,
-    collapsed?: boolean,
-    collapsible?: boolean
+    editMode?: boolean
   ) {
-    const hideWhenCollapsed = this._isNewChangeSummaryUiEnabled
-      ? false
-      : collapsed && collapsible;
     if (
       !loggedIn ||
       editing ||
       (change && change.status === ChangeStatus.MERGED) ||
-      editMode ||
-      hideWhenCollapsed
+      editMode
     ) {
       return true;
     }
@@ -1331,7 +1274,6 @@
 
     this._initialLoadComplete = false;
     this._changeNum = value.changeNum;
-    this.getRelatedChangesList()?.clear();
     this._reload(true).then(() => {
       this._performPostLoadTasks();
     });
@@ -1581,53 +1523,6 @@
     return GerritNav.getUrlForChange(change);
   }
 
-  _computeChangeIdClass(displayChangeId: string) {
-    return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
-  }
-
-  _computeTitleAttributeWarning(displayChangeId: string) {
-    if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
-      return 'Change-Id mismatch';
-    } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
-      return 'No Change-Id in commit message';
-    }
-    return undefined;
-  }
-
-  _computeChangeIdCommitMessageError(
-    commitMessage?: string,
-    change?: ChangeInfo
-  ) {
-    if (change === undefined) {
-      return undefined;
-    }
-
-    if (!commitMessage) {
-      return CHANGE_ID_ERROR.MISSING;
-    }
-
-    // Find the last match in the commit message:
-    let changeId;
-    let changeIdArr;
-
-    while ((changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage))) {
-      changeId = changeIdArr[2];
-    }
-
-    if (changeId) {
-      // A change-id is detected in the commit message.
-
-      if (changeId === change.change_id) {
-        // The change-id found matches the real change-id.
-        return null;
-      }
-      // The change-id found does not match the change-id.
-      return CHANGE_ID_ERROR.MISMATCH;
-    }
-    // There is no change-id in the commit message.
-    return CHANGE_ID_ERROR.MISSING;
-  }
-
   _computeReplyButtonLabel(
     changeRecord?: ElementPropertyDeepChange<
       GrChangeView,
@@ -1965,15 +1860,19 @@
         this.restApiService.getChange(changeId)
       )
     ).then(changes => {
+      changes = changes.filter(
+        change => change?.status !== ChangeStatus.ABANDONED
+      );
       if (!changes.length) return;
-      const change = changes.find(
+      const submittedRevert = changes.find(
         change => change?.status === ChangeStatus.MERGED
       );
       if (!this._changeStatuses) return;
-      if (change) {
-        this.revertSubmittedChange = change;
+      if (submittedRevert) {
+        this.revertedChange = submittedRevert;
         this.push('_changeStatuses', ChangeStates.REVERT_SUBMITTED);
       } else {
+        if (changes[0]) this.revertedChange = changes[0];
         this.push('_changeStatuses', ChangeStates.REVERT_CREATED);
       }
     });
@@ -2201,7 +2100,6 @@
       return Promise.resolve([]);
     }
     this._loading = true;
-    this._relatedChangesCollapsed = true;
     this.reporting.time(Timing.CHANGE_RELOAD);
     this.reporting.time(Timing.CHANGE_DATA);
 
@@ -2299,30 +2197,25 @@
     if (isLocationChange) {
       this._editingCommitMessage = false;
       const relatedChangesLoaded = coreDataPromise.then(() => {
-        this.getRelatedChangesList()?.reload();
-        if (this._isNewChangeSummaryUiEnabled) {
-          let relatedChangesPromise:
-            | Promise<RelatedChangesInfo | undefined>
-            | undefined;
-          const patchNum = this._computeLatestPatchNum(this._allPatchSets);
-          if (this._change && patchNum) {
-            relatedChangesPromise = this.restApiService
-              .getRelatedChanges(this._change._number, patchNum)
-              .then(response => {
-                if (this._change && response) {
-                  this.hasParent = this._calculateHasParent(
-                    this._change.change_id,
-                    response.changes
-                  );
-                }
-                return response;
-              });
-          }
-          // TODO: use returned Promise
-          this.getRelatedChangesListExperimental()?.reload(
-            relatedChangesPromise
-          );
+        let relatedChangesPromise:
+          | Promise<RelatedChangesInfo | undefined>
+          | undefined;
+        const patchNum = this._computeLatestPatchNum(this._allPatchSets);
+        if (this._change && patchNum) {
+          relatedChangesPromise = this.restApiService
+            .getRelatedChanges(this._change._number, patchNum)
+            .then(response => {
+              if (this._change && response) {
+                this.hasParent = this._calculateHasParent(
+                  this._change.change_id,
+                  response.changes
+                );
+              }
+              return response;
+            });
         }
+        // TODO: use returned Promise
+        this.getRelatedChangesList()?.reload(relatedChangesPromise);
       });
       allDataPromises.push(relatedChangesLoaded);
     }
@@ -2408,30 +2301,10 @@
     );
   }
 
-  _computeReplyDisabled() {
-    return false;
-  }
-
   _computeChangePermalinkAriaLabel(changeNum: NumericChangeId) {
     return `Change ${changeNum}`;
   }
 
-  _computeCommitMessageCollapsed(collapsed?: boolean, collapsible?: boolean) {
-    if (this._isNewChangeSummaryUiEnabled) {
-      return false;
-    }
-    return collapsible && collapsed;
-  }
-
-  _computeRelatedChangesClass(collapsed: boolean) {
-    return collapsed ? 'collapsed' : '';
-  }
-
-  _computeCollapseText(collapsed: boolean) {
-    // Symbols are up and down triangles.
-    return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
-  }
-
   /**
    * Returns the text to be copied when
    * click the copy icon next to change subject
@@ -2444,146 +2317,11 @@
     );
   }
 
-  _toggleCommitCollapsed() {
-    this._commitCollapsed = !this._commitCollapsed;
-    if (this._commitCollapsed) {
-      window.scrollTo(0, 0);
-    }
-  }
-
-  _toggleRelatedChangesCollapsed() {
-    this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
-    if (this._relatedChangesCollapsed) {
-      window.scrollTo(0, 0);
-    }
-  }
-
   _computeCommitCollapsible(commitMessage?: string) {
     if (!commitMessage) {
       return false;
     }
-    const MIN_LINES = this._isNewChangeSummaryUiEnabled
-      ? 17
-      : MIN_LINES_FOR_COMMIT_COLLAPSE;
-    return commitMessage.split('\n').length >= MIN_LINES;
-  }
-
-  _getOffsetHeight(element: HTMLElement) {
-    return element.offsetHeight;
-  }
-
-  _getScrollHeight(element: HTMLElement) {
-    return element.scrollHeight;
-  }
-
-  /**
-   * Get the line height of an element to the nearest integer.
-   */
-  _getLineHeight(element: Element) {
-    const lineHeightStr = getComputedStyle(element).lineHeight;
-    return Math.round(Number(lineHeightStr.slice(0, lineHeightStr.length - 2)));
-  }
-
-  /**
-   * New max height for the related changes section, shorter than the existing
-   * change info height.
-   */
-  _updateRelatedChangeMaxHeight() {
-    // Takes into account approximate height for the expand button and
-    // bottom margin.
-    const EXTRA_HEIGHT = 30;
-    let newHeight;
-
-    if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`).matches) {
-      // In a small (mobile) view, give the relation chain some space.
-      newHeight = SMALL_RELATED_HEIGHT;
-    } else if (
-      window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`).matches
-    ) {
-      // Since related changes are below the commit message, but still next to
-      // metadata, the height should be the height of the metadata minus the
-      // height of the commit message to reduce jank. However, if that doesn't
-      // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
-      // Note: extraHeight is to take into account margin/padding.
-      const medRelatedHeight = Math.max(
-        this._getOffsetHeight(this.$.mainChangeInfo) -
-          this._getOffsetHeight(this.$.commitMessage) -
-          2 * EXTRA_HEIGHT,
-        MINIMUM_RELATED_MAX_HEIGHT
-      );
-      newHeight = medRelatedHeight;
-    } else {
-      if (this._commitCollapsible) {
-        // Make sure the content is lined up if both areas have buttons. If
-        // the commit message is not collapsed, instead use the change info
-        // height.
-        newHeight = this._getOffsetHeight(this.$.commitMessage);
-      } else {
-        newHeight =
-          this._getOffsetHeight(this.$.commitAndRelated) - EXTRA_HEIGHT;
-      }
-    }
-    const stylesToUpdate: {[key: string]: string} = {};
-
-    const relatedChanges = this.getRelatedChangesList();
-    // Get the line height of related changes, and convert it to the nearest
-    // integer.
-    const DEFAULT_LINE_HEIGHT = 20;
-    const lineHeight = relatedChanges
-      ? this._getLineHeight(relatedChanges)
-      : DEFAULT_LINE_HEIGHT;
-
-    // Figure out a new height that is divisible by the rounded line height.
-    const remainder = newHeight % lineHeight;
-    newHeight = newHeight - remainder;
-
-    stylesToUpdate['--relation-chain-max-height'] = `${newHeight}px`;
-
-    // Update the max-height of the relation chain to this new height.
-    if (this._commitCollapsible) {
-      stylesToUpdate['--related-change-btn-top-padding'] = `${remainder}px`;
-    }
-
-    this.updateStyles(stylesToUpdate);
-  }
-
-  _computeShowRelatedToggle() {
-    // Make sure the max height has been applied, since there is now content
-    // to populate.
-    if (!getComputedStyleValue('--relation-chain-max-height', this)) {
-      this._updateRelatedChangeMaxHeight();
-    }
-    // Prevents showMore from showing when click on related change, since the
-    // line height would be positive, but related changes height is 0.
-    const relatedChanges = this.getRelatedChangesList();
-    if (relatedChanges) {
-      if (!this._getScrollHeight(relatedChanges)) {
-        return (this._showRelatedToggle = false);
-      }
-
-      if (
-        this._getScrollHeight(relatedChanges) >
-        this._getOffsetHeight(relatedChanges) +
-          this._getLineHeight(relatedChanges)
-      ) {
-        return (this._showRelatedToggle = true);
-      }
-    }
-    return (this._showRelatedToggle = false);
-  }
-
-  _updateToggleContainerClass(showRelatedToggle: boolean) {
-    const relatedChangesToggle = this.shadowRoot!.querySelector<HTMLDivElement>(
-      '#relatedChangesToggle'
-    );
-    if (!relatedChangesToggle) {
-      return;
-    }
-    if (showRelatedToggle) {
-      relatedChangesToggle.classList.add('showToggle');
-    } else {
-      relatedChangesToggle.classList.remove('showToggle');
-    }
+    return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE;
   }
 
   _startUpdateCheckTimer() {
@@ -2599,7 +2337,7 @@
     this._updateCheckTimerHandle = window.setTimeout(() => {
       assertIsDefined(this._change, '_change');
       const change = this._change;
-      fetchChangeUpdates(change, this.restApiService).then(result => {
+      this.changeService.fetchChangeUpdates(change).then(result => {
         let toastMessage = null;
         if (!result.isLatest) {
           toastMessage = ReloadToastMessage.NEWER_REVISION;
@@ -2872,12 +2610,6 @@
       '#relatedChanges'
     );
   }
-
-  getRelatedChangesListExperimental() {
-    return this.shadowRoot!.querySelector<GrRelatedChangesListExperimental>(
-      '#relatedChangesExperimental'
-    );
-  }
 }
 
 declare global {
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 de08318..132833f 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
@@ -91,11 +91,6 @@
       background-color: var(--view-background-color);
       box-shadow: var(--elevation-level-1);
     }
-    .changeId {
-      color: var(--deemphasized-text-color);
-      font-family: var(--font-family);
-      margin-top: var(--spacing-l);
-    }
     .changeMetadata {
       /* Limit meta section to half of the screen at max */
       max-width: 50%;
@@ -115,21 +110,10 @@
     #commitMessageEditor {
       /* Account for border and padding and rounding errors. */
       min-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
-      --collapsed-max-height: 36em;
-    }
-    .new-change-summary-true #commitMessageEditor {
       --collapsed-max-height: 300px;
     }
-    .editCommitMessage {
-      margin-top: var(--spacing-l);
-
-      --gr-button: {
-        padding: 5px 0px;
-      }
-    }
     .changeStatuses,
-    .commitActions,
-    .statusText {
+    .commitActions {
       align-items: center;
       display: flex;
     }
@@ -156,21 +140,12 @@
     .mobile {
       display: none;
     }
-    .warning {
-      color: var(--error-text-color);
-    }
     hr {
       border: 0;
       border-top: 1px solid var(--border-color);
       height: 0;
       margin-bottom: var(--spacing-l);
     }
-    #relatedChanges.collapsed {
-      margin-bottom: var(--spacing-l);
-      max-height: var(--relation-chain-max-height, 2em);
-      overflow: hidden;
-      position: relative; /* for arrowToCurrentChange to have position:absolute and be hidden */
-    }
     .emptySpace {
       flex-grow: 1;
     }
@@ -181,23 +156,6 @@
       margin: var(--spacing-l) 0;
       padding: 0 var(--spacing-l);
     }
-    .collapseToggleContainer {
-      display: flex;
-      margin-bottom: 8px;
-    }
-    #relatedChangesToggle {
-      display: none;
-    }
-    #relatedChangesToggle.showToggle {
-      display: flex;
-    }
-    .collapseToggleContainer gr-button {
-      display: block;
-    }
-    #relatedChangesToggle {
-      margin-left: var(--spacing-l);
-      padding-top: var(--related-change-btn-top-padding, 0);
-    }
     .showOnEdit {
       display: none;
     }
@@ -239,12 +197,10 @@
       padding-top: var(--spacing-l);
       width: 100%;
     }
-    gr-change-summary.new-change-summary-true {
+    gr-change-summary {
       /* temporary for old checks status */
       margin-bottom: var(--spacing-m);
     }
-    /* NOTE: If you update this breakpoint, also update the
-      BREAKPOINT_RELATED_MED in the JS */
     @media screen and (max-width: 75em) {
       .relatedChanges {
         padding: 0;
@@ -266,8 +222,6 @@
         padding-right: 0;
       }
     }
-    /* NOTE: If you update this breakpoint, also update the
-      BREAKPOINT_RELATED_SMALL in the JS */
     @media screen and (max-width: 50em) {
       .mobile {
         display: block;
@@ -369,7 +323,7 @@
             <template is="dom-repeat" items="[[_changeStatuses]]" as="status">
               <gr-change-status
                 change="[[_change]]"
-                revert-submitted-change="[[revertSubmittedChange]]"
+                reverted-change="[[revertedChange]]"
                 max-width="100"
                 status="[[status]]"
               ></gr-change-status>
@@ -431,7 +385,7 @@
           <gr-change-metadata
             id="metadata"
             change="{{_change}}"
-            revert-submitted-change="[[revertSubmittedChange]]"
+            reverted-change="[[revertedChange]]"
             account="[[_account]]"
             revision="[[_selectedRevision]]"
             commit-info="[[_commitInfo]]"
@@ -458,10 +412,7 @@
                   >[[_replyButtonLabel]]</gr-button
                 >
               </div>
-              <div
-                id="commitMessage"
-                class$="commitMessage new-change-summary-[[_isNewChangeSummaryUiEnabled]]"
-              >
+              <div id="commitMessage" class="commitMessage">
                 <gr-editable-content
                   id="commitMessageEditor"
                   editing="{{_editingCommitMessage}}"
@@ -470,7 +421,6 @@
                   hide-edit-commit-message="[[_hideEditCommitMessage]]"
                   commit-collapsible="[[_commitCollapsible]]"
                   remove-zero-width-space=""
-                  collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]"
                 >
                   <gr-linked-text
                     pre=""
@@ -479,48 +429,8 @@
                     remove-zero-width-space=""
                   ></gr-linked-text>
                 </gr-editable-content>
-                <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
-                  <gr-button
-                    link=""
-                    class="editCommitMessage"
-                    title="Edit commit message"
-                    on-click="_handleEditCommitMessage"
-                    hidden$="[[_hideEditCommitMessage]]"
-                    >Edit</gr-button
-                  >
-                  <div
-                    class="changeId"
-                    hidden$="[[!_changeIdCommitMessageError]]"
-                  >
-                    <hr />
-                    Change-Id:
-                    <span
-                      class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]"
-                      title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]"
-                    >
-                      [[_change.change_id]]
-                    </span>
-                  </div>
-                </template>
               </div>
-              <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
-                <div
-                  id="commitCollapseToggle"
-                  class="collapseToggleContainer"
-                  hidden$="[[!_commitCollapsible]]"
-                >
-                  <gr-button
-                    link=""
-                    id="commitCollapseToggleButton"
-                    class="collapseToggleButton"
-                    on-click="_toggleCommitCollapsed"
-                  >
-                    [[_computeCollapseText(_commitCollapsed)]]
-                  </gr-button>
-                </div>
-              </template>
               <gr-change-summary
-                class$="new-change-summary-[[_isNewChangeSummaryUiEnabled]]"
                 change-comments="[[_changeComments]]"
                 comment-threads="[[_commentThreads]]"
                 self-account="[[_account]]"
@@ -537,36 +447,11 @@
               </gr-endpoint-decorator>
             </div>
             <div class="relatedChanges">
-              <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
-                <gr-related-changes-list-experimental
-                  change="[[_change]]"
-                  id="relatedChangesExperimental"
-                  patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
-                ></gr-related-changes-list-experimental>
-              </template>
-              <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
-                <gr-related-changes-list
-                  id="relatedChanges"
-                  class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]"
-                  change="[[_change]]"
-                  mergeable="[[_mergeable]]"
-                  has-parent="{{hasParent}}"
-                  on-update="_updateRelatedChangeMaxHeight"
-                  patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
-                  on-new-section-loaded="_computeShowRelatedToggle"
-                >
-                </gr-related-changes-list>
-                <div id="relatedChangesToggle" class="collapseToggleContainer">
-                  <gr-button
-                    link=""
-                    id="relatedChangesToggleButton"
-                    class="collapseToggleButton"
-                    on-click="_toggleRelatedChangesCollapsed"
-                  >
-                    [[_computeCollapseText(_relatedChangesCollapsed)]]
-                  </gr-button>
-                </div>
-              </template>
+              <gr-related-changes-list
+                change="[[_change]]"
+                id="relatedChanges"
+                patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+              ></gr-related-changes-list>
             </div>
             <div class="emptySpace"></div>
           </div>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index fc0d289..3650d01 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -30,7 +30,6 @@
 } from '../../../constants/constants';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getComputedStyleValue} from '../../../utils/dom-util';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
@@ -39,7 +38,6 @@
 import 'lodash/lodash';
 import {
   stubRestApi,
-  SinonSpyMember,
   TestKeyboardShortcutBinder,
 } from '../../../test/test-utils';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -47,7 +45,6 @@
   createAppElementChangeViewParams,
   createApproval,
   createChange,
-  createChangeConfig,
   createChangeMessages,
   createCommit,
   createMergeable,
@@ -58,7 +55,6 @@
   createUserConfig,
   TEST_NUMERIC_CHANGE_ID,
   TEST_PROJECT_NAME,
-  getCurrentRevision,
   createEditRevision,
   createAccountWithIdNameAndEmail,
   createChangeViewChange,
@@ -409,9 +405,6 @@
     });
   });
 
-  const getCustomCssValue = (cssParam: string) =>
-    getComputedStyleValue(cssParam, element);
-
   test('_handleMessageAnchorTap', () => {
     element._changeNum = 1 as NumericChangeId;
     element._patchRange = {
@@ -1308,6 +1301,46 @@
       });
     });
 
+    test('do not show any chip if all reverts are abandoned', done => {
+      const change = {
+        ...createChange(),
+        messages: createChangeMessages(2),
+      };
+      change.messages[0].message = 'Created a revert of this change as 12345';
+      change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+      change.messages[1].message = 'Created a revert of this change as 23456';
+      change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+      const getChangeStub = stubRestApi('getChange');
+      getChangeStub.onFirstCall().returns(
+        Promise.resolve({
+          ...createChange(),
+          status: ChangeStatus.ABANDONED,
+        })
+      );
+      getChangeStub.onSecondCall().returns(
+        Promise.resolve({
+          ...createChange(),
+          status: ChangeStatus.ABANDONED,
+        })
+      );
+      element._change = change;
+      element._mergeable = true;
+      element._submitEnabled = true;
+      flush();
+      element.computeRevertSubmitted(element._change);
+      flush(() => {
+        assert.isFalse(
+          element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+        );
+        assert.isFalse(
+          element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+        );
+        done();
+      });
+    });
+
     test('show revert created if no revert is merged', done => {
       const change = {
         ...createChange(),
@@ -1315,6 +1348,10 @@
       };
       change.messages[0].message = 'Created a revert of this change as 12345';
       change.messages[0].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
+      change.messages[1].message = 'Created a revert of this change as 23456';
+      change.messages[1].tag = MessageTag.TAG_REVERT as ReviewInputTag;
+
       const getChangeStub = stubRestApi('getChange');
       getChangeStub.onFirstCall().returns(
         Promise.resolve({
@@ -1342,7 +1379,7 @@
       });
     });
 
-    test('show revert created if no revert is merged', done => {
+    test('show revert submitted if revert is merged', done => {
       const change = {
         ...createChange(),
         messages: createChangeMessages(2),
@@ -1622,10 +1659,6 @@
       .stub(element, '_reloadPatchNumDependentResources')
       .callsFake(() => Promise.resolve([undefined, undefined, undefined]));
     flush();
-    const relatedChanges = element.shadowRoot!.querySelector(
-      '#relatedChanges'
-    ) as GrRelatedChangesList;
-    const relatedClearSpy = sinon.spy(relatedChanges, 'clear');
     const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
 
     const value: AppElementChangeViewParams = {
@@ -1635,7 +1668,6 @@
     };
     element._paramsChanged(value);
     assert.isTrue(reloadStub.calledOnce);
-    assert.isTrue(relatedClearSpy.calledOnce);
 
     element._initialLoadComplete = true;
 
@@ -1644,7 +1676,6 @@
     element._paramsChanged(value);
     assert.isFalse(reloadStub.calledTwice);
     assert.isTrue(reloadPatchDependentStub.calledOnce);
-    assert.isTrue(relatedClearSpy.calledOnce);
     assert.isTrue(collapseStub.calledTwice);
   });
 
@@ -1657,10 +1688,6 @@
       element.$.commentAPI,
       'reloadPortedComments'
     );
-    const relatedChanges = element.shadowRoot!.querySelector(
-      '#relatedChanges'
-    ) as GrRelatedChangesList;
-    sinon.spy(relatedChanges, 'clear');
     sinon.stub(element.$.fileList, 'collapseAllDiffs');
 
     const value: AppElementChangeViewParams = {
@@ -1784,104 +1811,6 @@
     assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
   });
 
-  test('_computeChangeIdCommitMessageError', () => {
-    let commitMessage = 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
-    let change: ChangeInfo = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      null
-    );
-
-    change = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      'mismatch'
-    );
-
-    commitMessage = 'This is the greatest change.';
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      'missing'
-    );
-  });
-
-  test('multiple change Ids in commit message picks last', () => {
-    const commitMessage = [
-      'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
-      'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
-    ].join('\n');
-    let change: ChangeInfo = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      null
-    );
-    change = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      'mismatch'
-    );
-  });
-
-  test('does not count change Id that starts mid line', () => {
-    const commitMessage = [
-      'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
-      'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
-    ].join(' and ');
-    let change: ChangeInfo = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      null
-    );
-    change = {
-      ...createChangeViewChange(),
-      change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
-    };
-    assert.equal(
-      element._computeChangeIdCommitMessageError(commitMessage, change),
-      'mismatch'
-    );
-  });
-
-  test('_computeTitleAttributeWarning', () => {
-    let changeIdCommitMessageError = 'missing';
-    assert.equal(
-      element._computeTitleAttributeWarning(changeIdCommitMessageError),
-      'No Change-Id in commit message'
-    );
-
-    changeIdCommitMessageError = 'mismatch';
-    assert.equal(
-      element._computeTitleAttributeWarning(changeIdCommitMessageError),
-      'Change-Id mismatch'
-    );
-  });
-
-  test('_computeChangeIdClass', () => {
-    let changeIdCommitMessageError = 'missing';
-    assert.equal(element._computeChangeIdClass(changeIdCommitMessageError), '');
-
-    changeIdCommitMessageError = 'mismatch';
-    assert.equal(
-      element._computeChangeIdClass(changeIdCommitMessageError),
-      'warning'
-    );
-  });
-
   test('topic is coalesced to null', done => {
     sinon.stub(element, '_changeChanged');
     stubRestApi('getChangeDetail').returns(
@@ -2235,373 +2164,6 @@
     });
   });
 
-  suite('commit message expand/collapse', () => {
-    setup(() => {
-      element._change = {
-        ...createChangeViewChange(),
-        revisions: createRevisions(1),
-        messages: createChangeMessages(1),
-      };
-      element._change.labels = {};
-      stubRestApi('getChangeDetail').callsFake(() =>
-        Promise.resolve({
-          ...createChangeViewChange(),
-          // new patchset was uploaded
-          revisions: createRevisions(2),
-          current_revision: getCurrentRevision(2),
-          messages: createChangeMessages(1),
-        })
-      );
-    });
-
-    test('commitCollapseToggle hidden for short commit message', () => {
-      element._latestCommitMessage = '';
-      flush();
-      const commitCollapseToggle = element.shadowRoot!.querySelector(
-        '#commitCollapseToggle'
-      );
-      assert.isTrue(commitCollapseToggle?.hasAttribute('hidden'));
-    });
-
-    test('commitCollapseToggle shown for long commit message', () => {
-      element._latestCommitMessage = _.times(31, String).join('\n');
-      const commitCollapseToggle = element.shadowRoot!.querySelector(
-        '#commitCollapseToggle'
-      );
-      assert.isFalse(commitCollapseToggle?.hasAttribute('hidden'));
-    });
-
-    test('commitCollapseToggle functions', () => {
-      element._latestCommitMessage = _.times(35, String).join('\n');
-      assert.isTrue(element._commitCollapsed);
-      assert.isTrue(element._commitCollapsible);
-      assert.isTrue(element.$.commitMessageEditor.hasAttribute('collapsed'));
-      const commitCollapseToggleButton = element.shadowRoot!.querySelector(
-        '#commitCollapseToggleButton'
-      )!;
-      tap(commitCollapseToggleButton);
-      assert.isFalse(element._commitCollapsed);
-      assert.isTrue(element._commitCollapsible);
-      assert.isFalse(element.$.commitMessageEditor.hasAttribute('collapsed'));
-    });
-  });
-
-  suite('related changes expand/collapse', () => {
-    let updateHeightSpy: SinonSpyMember<
-      typeof element._updateRelatedChangeMaxHeight
-    >;
-    setup(() => {
-      updateHeightSpy = sinon.spy(element, '_updateRelatedChangeMaxHeight');
-    });
-
-    test('relatedChangesToggle shown height greater than changeInfo height', () => {
-      const relatedChangesToggle = element.shadowRoot!.querySelector(
-        '#relatedChangesToggle'
-      );
-      assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getScrollHeight').callsFake(() => 60);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 5);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: true} as MediaQueryList));
-      const relatedChanges = element.shadowRoot!.querySelector(
-        '#relatedChanges'
-      ) as GrRelatedChangesList;
-      relatedChanges.dispatchEvent(new CustomEvent('new-section-loaded'));
-      assert.isTrue(relatedChangesToggle!.classList.contains('showToggle'));
-      assert.equal(updateHeightSpy.callCount, 1);
-    });
-
-    test('relatedChangesToggle hidden height less than changeInfo height', () => {
-      const relatedChangesToggle = element.shadowRoot!.querySelector(
-        '#relatedChangesToggle'
-      );
-      assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getScrollHeight').callsFake(() => 40);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 5);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: true} as MediaQueryList));
-      const relatedChanges = element.shadowRoot!.querySelector(
-        '#relatedChanges'
-      ) as GrRelatedChangesList;
-      relatedChanges.dispatchEvent(new CustomEvent('new-section-loaded'));
-      assert.isFalse(relatedChangesToggle!.classList.contains('showToggle'));
-      assert.equal(updateHeightSpy.callCount, 1);
-    });
-
-    test('relatedChangesToggle functions', () => {
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: false} as MediaQueryList));
-      assert.isTrue(element._relatedChangesCollapsed);
-      const relatedChangesToggleButton = element.shadowRoot!.querySelector(
-        '#relatedChangesToggleButton'
-      );
-      const relatedChanges = element.shadowRoot!.querySelector(
-        '#relatedChanges'
-      ) as GrRelatedChangesList;
-      assert.isTrue(relatedChanges.classList.contains('collapsed'));
-      tap(relatedChangesToggleButton!);
-      assert.isFalse(element._relatedChangesCollapsed);
-      assert.isFalse(relatedChanges.classList.contains('collapsed'));
-    });
-
-    test('_updateRelatedChangeMaxHeight without commit toggle', () => {
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: false} as MediaQueryList));
-
-      // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
-      // 20 (max existing height)  % 12 (line height) = 6 (remainder).
-      // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
-
-      element._updateRelatedChangeMaxHeight();
-      assert.equal(getCustomCssValue('--relation-chain-max-height'), '12px');
-      assert.equal(getCustomCssValue('--related-change-btn-top-padding'), '');
-    });
-
-    test('_updateRelatedChangeMaxHeight with commit toggle', () => {
-      element._latestCommitMessage = _.times(31, String).join('\n');
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: false} as MediaQueryList));
-
-      // 50 (existing height) % 12 (line height) = 2 (remainder).
-      // 50 (existing height)  - 2 (remainder) = 48 (max height to set).
-
-      element._updateRelatedChangeMaxHeight();
-      assert.equal(getCustomCssValue('--relation-chain-max-height'), '48px');
-      assert.equal(
-        getCustomCssValue('--related-change-btn-top-padding'),
-        '2px'
-      );
-    });
-
-    test('_updateRelatedChangeMaxHeight in small screen mode', () => {
-      element._latestCommitMessage = _.times(31, String).join('\n');
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
-      sinon
-        .stub(window, 'matchMedia')
-        .callsFake(() => ({matches: true} as MediaQueryList));
-
-      element._updateRelatedChangeMaxHeight();
-
-      // 400 (new height) % 12 (line height) = 4 (remainder).
-      // 400 (new height) - 4 (remainder) = 396.
-
-      assert.equal(getCustomCssValue('--relation-chain-max-height'), '396px');
-    });
-
-    test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
-      element._latestCommitMessage = _.times(31, String).join('\n');
-      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
-      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
-      const matchMediaStub = sinon.stub(window, 'matchMedia').callsFake(() => {
-        if (matchMediaStub.lastCall.args[0] === '(max-width: 75em)') {
-          return {matches: true} as MediaQueryList;
-        } else {
-          return {matches: false} as MediaQueryList;
-        }
-      });
-
-      // 100 (new height) % 12 (line height) = 4 (remainder).
-      // 100 (new height) - 4 (remainder) = 96.
-      element._updateRelatedChangeMaxHeight();
-      assert.equal(getCustomCssValue('--relation-chain-max-height'), '96px');
-    });
-
-    suite('update checks', () => {
-      let clock: SinonFakeTimers;
-      let startUpdateCheckTimerSpy: SinonSpyMember<
-        typeof element._startUpdateCheckTimer
-      >;
-      setup(() => {
-        clock = sinon.useFakeTimers();
-        startUpdateCheckTimerSpy = sinon.spy(element, '_startUpdateCheckTimer');
-        element._change = {
-          ...createChangeViewChange(),
-          revisions: createRevisions(1),
-          messages: createChangeMessages(1),
-        };
-      });
-
-      test('_startUpdateCheckTimer negative delay', () => {
-        const getChangeDetailStub = stubRestApi('getChangeDetail').returns(
-          Promise.resolve({
-            ...createChangeViewChange(),
-            // element has latest info
-            revisions: {rev1: createRevision()},
-            messages: createChangeMessages(1),
-            current_revision: 'rev1' as CommitId,
-          })
-        );
-
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: -1},
-        };
-
-        assert.isTrue(startUpdateCheckTimerSpy.called);
-        assert.isFalse(getChangeDetailStub.called);
-      });
-
-      test('_startUpdateCheckTimer up-to-date', async () => {
-        const getChangeDetailStub = stubRestApi('getChangeDetail').callsFake(
-          () =>
-            Promise.resolve({
-              ...createChangeViewChange(),
-              // element has latest info
-              revisions: {rev1: createRevision()},
-              messages: createChangeMessages(1),
-              current_revision: 'rev1' as CommitId,
-            })
-        );
-
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: 12345},
-        };
-        clock.tick(12345 * 1000);
-        await flush();
-
-        assert.equal(startUpdateCheckTimerSpy.callCount, 2);
-        assert.isTrue(getChangeDetailStub.called);
-      });
-
-      test('_startUpdateCheckTimer out-of-date shows an alert', async () => {
-        stubRestApi('getChangeDetail').callsFake(() =>
-          Promise.resolve({
-            ...createChange(),
-            // new patchset was uploaded
-            revisions: createRevisions(2),
-            current_revision: getCurrentRevision(2),
-            messages: createChangeMessages(1),
-          })
-        );
-
-        let alertMessage = 'alert not fired';
-        element.addEventListener('show-alert', e => {
-          alertMessage = e.detail.message;
-        });
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: 12345},
-        };
-        clock.tick(12345 * 1000);
-        await flush();
-
-        assert.equal(alertMessage, 'A newer patch set has been uploaded');
-        assert.equal(startUpdateCheckTimerSpy.callCount, 1);
-      });
-
-      test('_startUpdateCheckTimer respects _loading', async () => {
-        stubRestApi('getChangeDetail').callsFake(() =>
-          Promise.resolve({
-            ...createChangeViewChange(),
-            // new patchset was uploaded
-            revisions: createRevisions(2),
-            current_revision: getCurrentRevision(2),
-            messages: createChangeMessages(1),
-          })
-        );
-
-        element._loading = true;
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: 12345},
-        };
-        clock.tick(12345 * 1000 * 2);
-        await flush();
-
-        // No toast, instead a second call to _startUpdateCheckTimer().
-        assert.equal(startUpdateCheckTimerSpy.callCount, 2);
-      });
-
-      test('_startUpdateCheckTimer new status shows an alert', async () => {
-        stubRestApi('getChangeDetail').callsFake(() =>
-          Promise.resolve({
-            ...createChangeViewChange(),
-            // element has latest info
-            revisions: {rev1: createRevision()},
-            messages: createChangeMessages(1),
-            current_revision: 'rev1' as CommitId,
-            status: ChangeStatus.MERGED,
-          })
-        );
-
-        let alertMessage = 'alert not fired';
-        element.addEventListener('show-alert', e => {
-          alertMessage = e.detail.message;
-        });
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: 12345},
-        };
-        clock.tick(12345 * 1000);
-        await flush();
-
-        assert.equal(alertMessage, 'This change has been merged');
-      });
-
-      test('_startUpdateCheckTimer new messages shows an alert', async () => {
-        stubRestApi('getChangeDetail').callsFake(() =>
-          Promise.resolve({
-            ...createChangeViewChange(),
-            revisions: {rev1: createRevision()},
-            // element has new message
-            messages: createChangeMessages(2),
-            current_revision: 'rev1' as CommitId,
-          })
-        );
-
-        let alertMessage = 'alert not fired';
-        element.addEventListener('show-alert', e => {
-          alertMessage = e.detail.message;
-        });
-        element._serverConfig = {
-          ...createServerInfo(),
-          change: {...createChangeConfig(), update_delay: 12345},
-        };
-        clock.tick(12345 * 1000);
-        await flush();
-
-        assert.equal(alertMessage, 'There are new messages on this change');
-      });
-    });
-
-    test('canStartReview computation', () => {
-      const change1: ChangeInfo = createChange();
-      const change2: ChangeInfo = {
-        ...createChangeViewChange(),
-        actions: {
-          ready: {
-            enabled: true,
-          },
-        },
-      };
-      const change3: ChangeInfo = {
-        ...createChangeViewChange(),
-        actions: {
-          ready: {
-            label: 'Ready for Review',
-          },
-        },
-      };
-      assert.isFalse(element._computeCanStartReview(change1));
-      assert.isTrue(element._computeCanStartReview(change2));
-      assert.isFalse(element._computeCanStartReview(change3));
-    });
-  });
-
   test('header class computation', () => {
     assert.equal(element._computeHeaderClass(), 'header');
     assert.equal(element._computeHeaderClass(true), 'header editMode');
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index e22253b..aece283 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -41,7 +41,7 @@
 import {DiffPreferencesInfo} from '../../../types/diff';
 import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
 import {GrDiffModeSelector} from '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
-import {ChangeStatus, DiffViewMode} from '../../../constants/constants';
+import {DiffViewMode} from '../../../constants/constants';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {fireEvent} from '../../../utils/event-util';
 
@@ -184,13 +184,6 @@
     return shownFileCount <= maxFilesForBulkActions;
   }
 
-  _showAddPatchsetDescription(
-    patchsetDescription: string,
-    change?: ChangeInfo
-  ) {
-    return !patchsetDescription && change?.status === ChangeStatus.NEW;
-  }
-
   _handlePatchChange(e: CustomEvent) {
     const {basePatchNum, patchNum} = e.detail;
     if (
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index 8ebb029..878caea 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -21,9 +21,6 @@
     .prefsButton {
       float: right;
     }
-    .collapseToggleButton {
-      text-decoration: none;
-    }
     .patchInfoOldPatchSet.patchInfo-header {
       background-color: var(--emphasis-color);
     }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index fc48679..bb3c975 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -1096,14 +1096,6 @@
     );
   }
 
-  _addDraftAtTarget() {
-    const diff = this.$.diffCursor.getTargetDiffElement();
-    const target = this.$.diffCursor.getTargetLineElement();
-    if (diff && target) {
-      diff.addDraftAtLine(target);
-    }
-  }
-
   _shouldHideChangeTotals(_patchChange: PatchChange): boolean {
     return _patchChange.inserted === 0 && _patchChange.deleted === 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 3ea9f68..fed02a7 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -25,7 +25,11 @@
 import '../../../styles/gr-voting-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-message_html';
-import {MessageTag, SpecialFilePath} from '../../../constants/constants';
+import {
+  ChangeMessageTemplate,
+  MessageTag,
+  SpecialFilePath,
+} from '../../../constants/constants';
 import {customElement, property, computed, observe} from '@polymer/decorators';
 import {
   ChangeInfo,
@@ -40,6 +44,7 @@
   PatchSetNum,
   AccountInfo,
   BasePatchSetNum,
+  AccountId,
 } from '../../../types/common';
 import {CommentThread} from '../../../utils/comment-util';
 import {hasOwnProperty} from '../../../utils/common-util';
@@ -176,14 +181,19 @@
 
   @property({
     type: String,
-    computed: '_computeMessageContentExpanded(message.message, message.tag)',
+    computed:
+      '_computeMessageContentExpanded(message.message,' +
+      ' message.accountsInMessage,' +
+      ' message.tag)',
   })
   _messageContentExpanded = '';
 
   @property({
     type: String,
     computed:
-      '_computeMessageContentCollapsed(message.message, message.tag,' +
+      '_computeMessageContentCollapsed(message.message,' +
+      ' message.accountsInMessage,' +
+      ' message.tag,' +
       ' message.commentThreads)',
   })
   _messageContentCollapsed = '';
@@ -231,8 +241,12 @@
     return pluralize(threadsLength, 'comment');
   }
 
-  _computeMessageContentExpanded(content?: string, tag?: ReviewInputTag) {
-    return this._computeMessageContent(true, content, tag);
+  _computeMessageContentExpanded(
+    content?: string,
+    accountsInMessage?: AccountInfo[],
+    tag?: ReviewInputTag
+  ) {
+    return this._computeMessageContent(true, content, accountsInMessage, tag);
   }
 
   _patchsetCommentSummary(commentThreads: CommentThread[] = []) {
@@ -261,10 +275,16 @@
 
   _computeMessageContentCollapsed(
     content?: string,
+    accountsInMessage?: AccountInfo[],
     tag?: ReviewInputTag,
     commentThreads?: CommentThread[]
   ) {
-    const summary = this._computeMessageContent(false, content, tag);
+    const summary = this._computeMessageContent(
+      false,
+      content,
+      accountsInMessage,
+      tag
+    );
     if (summary || !commentThreads) return summary;
     return this._patchsetCommentSummary(commentThreads);
   }
@@ -319,11 +339,22 @@
   _computeMessageContent(
     isExpanded: boolean,
     content?: string,
+    accountsInMessage?: AccountInfo[],
     tag?: ReviewInputTag
   ) {
     if (!content) return '';
     const isNewPatchSet = this._isNewPatchsetTag(tag);
 
+    if (accountsInMessage) {
+      content = content.replace(
+        new RegExp(ChangeMessageTemplate.ACCOUNT_TEMPLATE, 'g'),
+        (_accountIdTemplate, accountId) =>
+          accountsInMessage.find(
+            account => account._account_id === (Number(accountId) as AccountId)
+          )?.name || `Gerrit Account ${accountId}`
+      );
+    }
+
     const lines = content.split('\n');
     const filteredLines = lines.filter(line => {
       if (!isExpanded && line.startsWith('>')) {
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 b8f3c73..97568dc 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
@@ -19,6 +19,7 @@
 import './gr-message';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {
+  createAccountWithIdNameAndEmail,
   createChange,
   createChangeMessage,
   createComment,
@@ -349,11 +350,21 @@
     suite('compute messages', () => {
       test('empty', () => {
         assert.equal(
-          element._computeMessageContent(true, '', '' as ReviewInputTag),
+          element._computeMessageContent(
+            true,
+            '',
+            undefined,
+            '' as ReviewInputTag
+          ),
           ''
         );
         assert.equal(
-          element._computeMessageContent(false, '', '' as ReviewInputTag),
+          element._computeMessageContent(
+            false,
+            '',
+            undefined,
+            '' as ReviewInputTag
+          ),
           ''
         );
       });
@@ -361,13 +372,13 @@
       test('new patchset', () => {
         const original = 'Uploaded patch set 1.';
         const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
-        let actual = element._computeMessageContent(true, original, tag);
+        let actual = element._computeMessageContent(true, original, [], tag);
         assert.equal(
           actual,
-          element._computeMessageContentCollapsed(original, tag, [])
+          element._computeMessageContentCollapsed(original, [], tag, [])
         );
         assert.equal(actual, original);
-        actual = element._computeMessageContent(false, original, tag);
+        actual = element._computeMessageContent(false, original, [], tag);
         assert.equal(actual, original);
       });
 
@@ -375,13 +386,13 @@
         const original = 'Patch Set 27: Patch Set 26 was rebased';
         const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
         const expected = 'Patch Set 26 was rebased';
-        let actual = element._computeMessageContent(true, original, tag);
+        let actual = element._computeMessageContent(true, original, [], tag);
         assert.equal(actual, expected);
         assert.equal(
           actual,
-          element._computeMessageContentCollapsed(original, tag, [])
+          element._computeMessageContentCollapsed(original, [], tag, [])
         );
-        actual = element._computeMessageContent(false, original, tag);
+        actual = element._computeMessageContent(false, original, [], tag);
         assert.equal(actual, expected);
       });
 
@@ -389,13 +400,13 @@
         const original = 'Patch Set 1:\n\nThis change is ready for review.';
         const tag = undefined;
         const expected = 'This change is ready for review.';
-        let actual = element._computeMessageContent(true, original, tag);
+        let actual = element._computeMessageContent(true, original, [], tag);
         assert.equal(actual, expected);
         assert.equal(
           actual,
-          element._computeMessageContentCollapsed(original, tag, [])
+          element._computeMessageContentCollapsed(original, [], tag, [])
         );
-        actual = element._computeMessageContent(false, original, tag);
+        actual = element._computeMessageContent(false, original, [], tag);
         assert.equal(actual, expected);
       });
 
@@ -403,9 +414,9 @@
         const original = 'Patch Set 1: Code-Style+1';
         const tag = undefined;
         const expected = '';
-        let actual = element._computeMessageContent(true, original, tag);
+        let actual = element._computeMessageContent(true, original, [], tag);
         assert.equal(actual, expected);
-        actual = element._computeMessageContent(false, original, tag);
+        actual = element._computeMessageContent(false, original, [], tag);
         assert.equal(actual, expected);
       });
 
@@ -413,9 +424,47 @@
         const original = 'Patch Set 1:\n\n(3 comments)';
         const tag = undefined;
         const expected = '';
-        let actual = element._computeMessageContent(true, original, tag);
+        let actual = element._computeMessageContent(true, original, [], tag);
         assert.equal(actual, expected);
-        actual = element._computeMessageContent(false, original, tag);
+        actual = element._computeMessageContent(false, original, [], tag);
+        assert.equal(actual, expected);
+      });
+
+      test('message template', () => {
+        const original =
+          'Removed vote: \n\n * Code-Style+1 by <GERRIT_ACCOUNT_0000001>\n * Code-Style-1 by <GERRIT_ACCOUNT_0000002>';
+        const tag = undefined;
+        const expected =
+          'Removed vote: \n\n * Code-Style+1 by User-1\n * Code-Style-1 by User-2';
+        const accountsInMessage = [
+          createAccountWithIdNameAndEmail(1),
+          createAccountWithIdNameAndEmail(2),
+        ];
+        let actual = element._computeMessageContent(
+          true,
+          original,
+          accountsInMessage,
+          tag
+        );
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(
+          false,
+          original,
+          accountsInMessage,
+          tag
+        );
+        assert.equal(actual, expected);
+      });
+
+      test('message template missing accounts', () => {
+        const original =
+          'Removed vote: \n\n * Code-Style+1 by <GERRIT_ACCOUNT_0000001>\n * Code-Style-1 by <GERRIT_ACCOUNT_0000002>';
+        const tag = undefined;
+        const expected =
+          'Removed vote: \n\n * Code-Style+1 by Gerrit Account 0000001\n * Code-Style-1 by Gerrit Account 0000002';
+        let actual = element._computeMessageContent(true, original, [], tag);
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(false, original, [], tag);
         assert.equal(actual, expected);
       });
     });
@@ -570,10 +619,18 @@
         },
       ];
       assert.equal(
-        element._computeMessageContentCollapsed('', undefined, threads),
+        element._computeMessageContentCollapsed(
+          '',
+          undefined,
+          undefined,
+          threads
+        ),
         'testing the load'
       );
-      assert.equal(element._computeMessageContent(false, '', undefined), '');
+      assert.equal(
+        element._computeMessageContent(false, '', undefined, undefined),
+        ''
+      );
     });
 
     test('single patchset comment with reply', () => {
@@ -610,10 +667,18 @@
         },
       ];
       assert.equal(
-        element._computeMessageContentCollapsed('', undefined, threads),
+        element._computeMessageContentCollapsed(
+          '',
+          undefined,
+          undefined,
+          threads
+        ),
         'n'
       );
-      assert.equal(element._computeMessageContent(false, '', undefined), '');
+      assert.equal(
+        element._computeMessageContent(false, '', undefined, undefined),
+        ''
+      );
     });
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
deleted file mode 100644
index 8c83b58..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ /dev/null
@@ -1,754 +0,0 @@
-/**
- * @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 {html, nothing} from 'lit-html';
-import './gr-related-change';
-import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
-import {classMap} from 'lit-html/directives/class-map';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {
-  customElement,
-  property,
-  css,
-  internalProperty,
-  TemplateResult,
-} from 'lit-element';
-import {sharedStyles} from '../../../styles/shared-styles';
-import {
-  SubmittedTogetherInfo,
-  ChangeInfo,
-  RelatedChangeAndCommitInfo,
-  RelatedChangesInfo,
-  PatchSetNum,
-  CommitId,
-} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
-import {ParsedChangeInfo} from '../../../types/types';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {pluralize} from '../../../utils/string-util';
-import {
-  changeIsOpen,
-  getRevisionKey,
-  isChangeInfo,
-} from '../../../utils/change-util';
-
-/** What is the maximum number of shown changes in collapsed list? */
-const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
-
-export interface ChangeMarkersInList {
-  showCurrentChangeArrow: boolean;
-  showWhenCollapsed: boolean;
-  showTopArrow: boolean;
-  showBottomArrow: boolean;
-}
-
-export enum Section {
-  RELATED_CHANGES = 'related changes',
-  SUBMITTED_TOGETHER = 'submitted together',
-  SAME_TOPIC = 'same topic',
-  MERGE_CONFLICTS = 'merge conflicts',
-  CHERRY_PICKS = 'cherry picks',
-}
-
-@customElement('gr-related-changes-list-experimental')
-export class GrRelatedChangesListExperimental extends GrLitElement {
-  @property()
-  change?: ParsedChangeInfo;
-
-  @property({type: String})
-  patchNum?: PatchSetNum;
-
-  @property()
-  mergeable?: boolean;
-
-  @internalProperty()
-  submittedTogether?: SubmittedTogetherInfo = {
-    changes: [],
-    non_visible_changes: 0,
-  };
-
-  @internalProperty()
-  relatedChanges: RelatedChangeAndCommitInfo[] = [];
-
-  @internalProperty()
-  conflictingChanges: ChangeInfo[] = [];
-
-  @internalProperty()
-  cherryPickChanges: ChangeInfo[] = [];
-
-  @internalProperty()
-  sameTopicChanges: ChangeInfo[] = [];
-
-  private readonly restApiService = appContext.restApiService;
-
-  static get styles() {
-    return [
-      sharedStyles,
-      css`
-        .note {
-          color: var(--error-text-color);
-          margin-left: 1.2em;
-        }
-        section {
-          margin-bottom: var(--spacing-l);
-        }
-        gr-related-change {
-          display: flex;
-        }
-        .marker {
-          position: absolute;
-          margin-left: calc(-1 * var(--spacing-s));
-        }
-        .arrowToCurrentChange {
-          position: absolute;
-        }
-      `,
-    ];
-  }
-
-  render() {
-    const sectionSize = this.sectionSizeFactory(
-      this.relatedChanges.length,
-      this.submittedTogether?.changes.length || 0,
-      this.sameTopicChanges.length,
-      this.conflictingChanges.length,
-      this.cherryPickChanges.length
-    );
-    const relatedChangesMarkersPredicate = this.markersPredicateFactory(
-      this.relatedChanges.length,
-      this.relatedChanges.findIndex(relatedChange =>
-        this._changesEqual(relatedChange, this.change)
-      ),
-      sectionSize(Section.RELATED_CHANGES)
-    );
-    const connectedRevisions = this._computeConnectedRevisions(
-      this.change,
-      this.patchNum,
-      this.relatedChanges
-    );
-    let firstNonEmptySectionFound = false;
-    let isFirstNonEmpty =
-      !firstNonEmptySectionFound && !!this.relatedChanges.length;
-    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
-    const relatedChangeSection = html` <section
-      id="relatedChanges"
-      ?hidden=${!this.relatedChanges.length}
-    >
-      <gr-related-collapse
-        title="Relation chain"
-        class="${classMap({first: isFirstNonEmpty})}"
-        .length=${this.relatedChanges.length}
-        .numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
-      >
-        ${this.relatedChanges.map(
-          (change, index) =>
-            html`${this.renderMarkers(
-                relatedChangesMarkersPredicate(index)
-              )}<gr-related-change
-                class="${classMap({
-                  ['show-when-collapsed']: relatedChangesMarkersPredicate(index)
-                    .showWhenCollapsed,
-                })}"
-                .change="${change}"
-                .connectedRevisions="${connectedRevisions}"
-                .href="${change?._change_number
-                  ? GerritNav.getUrlForChangeById(
-                      change._change_number,
-                      change.project,
-                      change._revision_number as PatchSetNum
-                    )
-                  : ''}"
-                .showChangeStatus=${true}
-                >${change.commit.subject}</gr-related-change
-              >`
-        )}
-      </gr-related-collapse>
-    </section>`;
-
-    const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
-    const countNonVisibleChanges =
-      this.submittedTogether?.non_visible_changes ?? 0;
-    const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
-      submittedTogetherChanges.length,
-      submittedTogetherChanges.findIndex(relatedChange =>
-        this._changesEqual(relatedChange, this.change)
-      ),
-      sectionSize(Section.SUBMITTED_TOGETHER)
-    );
-    isFirstNonEmpty =
-      !firstNonEmptySectionFound &&
-      (!!submittedTogetherChanges?.length ||
-        !!this.submittedTogether?.non_visible_changes);
-    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
-    const submittedTogetherSection = html`<section
-      id="submittedTogether"
-      ?hidden=${!submittedTogetherChanges?.length &&
-      !this.submittedTogether?.non_visible_changes}
-    >
-      <gr-related-collapse
-        title="Submitted together"
-        class="${classMap({first: isFirstNonEmpty})}"
-        .length=${submittedTogetherChanges.length}
-        .numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
-      >
-        ${submittedTogetherChanges.map(
-          (change, index) =>
-            html`${this.renderMarkers(
-                submittedTogetherMarkersPredicate(index)
-              )}<gr-related-change
-                class="${classMap({
-                  ['show-when-collapsed']: submittedTogetherMarkersPredicate(
-                    index
-                  ).showWhenCollapsed,
-                })}"
-                .change="${change}"
-                .href="${GerritNav.getUrlForChangeById(
-                  change._number,
-                  change.project
-                )}"
-                .showSubmittableCheck=${true}
-                >${change.project}: ${change.branch}:
-                ${change.subject}</gr-related-change
-              >`
-        )}
-      </gr-related-collapse>
-      <div class="note" ?hidden=${!countNonVisibleChanges}>
-        (+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
-      </div>
-    </section>`;
-
-    const sameTopicMarkersPredicate = this.markersPredicateFactory(
-      this.sameTopicChanges.length,
-      -1,
-      sectionSize(Section.SAME_TOPIC)
-    );
-    isFirstNonEmpty =
-      !firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
-    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
-    const sameTopicSection = html`<section
-      id="sameTopic"
-      ?hidden=${!this.sameTopicChanges?.length}
-    >
-      <gr-related-collapse
-        title="Same topic"
-        class="${classMap({first: isFirstNonEmpty})}"
-        .length=${this.sameTopicChanges.length}
-        .numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
-      >
-        ${this.sameTopicChanges.map(
-          (change, index) =>
-            html`${this.renderMarkers(
-                sameTopicMarkersPredicate(index)
-              )}<gr-related-change
-                class="${classMap({
-                  ['show-when-collapsed']: sameTopicMarkersPredicate(index)
-                    .showWhenCollapsed,
-                })}"
-                .change="${change}"
-                .href="${GerritNav.getUrlForChangeById(
-                  change._number,
-                  change.project
-                )}"
-                >${change.project}: ${change.branch}:
-                ${change.subject}</gr-related-change
-              >`
-        )}
-      </gr-related-collapse>
-    </section>`;
-
-    const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
-      this.conflictingChanges.length,
-      -1,
-      sectionSize(Section.MERGE_CONFLICTS)
-    );
-    isFirstNonEmpty =
-      !firstNonEmptySectionFound && !!this.conflictingChanges?.length;
-    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
-    const mergeConflictsSection = html`<section
-      id="mergeConflicts"
-      ?hidden=${!this.conflictingChanges?.length}
-    >
-      <gr-related-collapse
-        title="Merge conflicts"
-        class="${classMap({first: isFirstNonEmpty})}"
-        .length=${this.conflictingChanges.length}
-        .numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
-      >
-        ${this.conflictingChanges.map(
-          (change, index) =>
-            html`${this.renderMarkers(
-                mergeConflictsMarkersPredicate(index)
-              )}<gr-related-change
-                class="${classMap({
-                  ['show-when-collapsed']: mergeConflictsMarkersPredicate(index)
-                    .showWhenCollapsed,
-                })}"
-                .change="${change}"
-                .href="${GerritNav.getUrlForChangeById(
-                  change._number,
-                  change.project
-                )}"
-                >${change.subject}</gr-related-change
-              >`
-        )}
-      </gr-related-collapse>
-    </section>`;
-
-    const cherryPicksMarkersPredicate = this.markersPredicateFactory(
-      this.cherryPickChanges.length,
-      -1,
-      sectionSize(Section.CHERRY_PICKS)
-    );
-    isFirstNonEmpty =
-      !firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
-    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
-    const cherryPicksSection = html`<section
-      id="cherryPicks"
-      ?hidden=${!this.cherryPickChanges?.length}
-    >
-      <gr-related-collapse
-        title="Cherry picks"
-        class="${classMap({first: isFirstNonEmpty})}"
-        .length=${this.cherryPickChanges.length}
-        .numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
-      >
-        ${this.cherryPickChanges.map(
-          (change, index) =>
-            html`${this.renderMarkers(
-                cherryPicksMarkersPredicate(index)
-              )}<gr-related-change
-                class="${classMap({
-                  ['show-when-collapsed']: cherryPicksMarkersPredicate(index)
-                    .showWhenCollapsed,
-                })}"
-                .change="${change}"
-                .href="${GerritNav.getUrlForChangeById(
-                  change._number,
-                  change.project
-                )}"
-                >${change.branch}: ${change.subject}</gr-related-change
-              >`
-        )}
-      </gr-related-collapse>
-    </section>`;
-
-    return html`<gr-endpoint-decorator name="related-changes-section">
-      <gr-endpoint-param
-        name="change"
-        .value=${this.change}
-      ></gr-endpoint-param>
-      <gr-endpoint-slot name="top"></gr-endpoint-slot>
-      ${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
-      ${mergeConflictsSection} ${cherryPicksSection}
-      <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
-    </gr-endpoint-decorator>`;
-  }
-
-  sectionSizeFactory(
-    relatedChangesLen: number,
-    submittedTogetherLen: number,
-    sameTopicLen: number,
-    mergeConflictsLen: number,
-    cherryPicksLen: number
-  ) {
-    const calcDefaultSize = (length: number) =>
-      Math.min(length, DEFALT_NUM_CHANGES_WHEN_COLLAPSED);
-
-    const sectionSizes = [
-      {
-        section: Section.RELATED_CHANGES,
-        size: calcDefaultSize(relatedChangesLen),
-        len: relatedChangesLen,
-      },
-      {
-        section: Section.SUBMITTED_TOGETHER,
-        size: calcDefaultSize(submittedTogetherLen),
-        len: submittedTogetherLen,
-      },
-      {
-        section: Section.SAME_TOPIC,
-        size: calcDefaultSize(sameTopicLen),
-        len: sameTopicLen,
-      },
-      {
-        section: Section.MERGE_CONFLICTS,
-        size: calcDefaultSize(mergeConflictsLen),
-        len: mergeConflictsLen,
-      },
-      {
-        section: Section.CHERRY_PICKS,
-        size: calcDefaultSize(cherryPicksLen),
-        len: cherryPicksLen,
-      },
-    ];
-
-    const FILLER = 1; // space for header
-    let totalSize = sectionSizes.reduce(
-      (acc, val) => acc + val.size + (val.size !== 0 ? FILLER : 0),
-      0
-    );
-
-    const MAX_SIZE = 16;
-    for (let i = 0; i < sectionSizes.length; i++) {
-      if (totalSize >= MAX_SIZE) break;
-      const sizeObj = sectionSizes[i];
-      if (sizeObj.size === sizeObj.len) continue;
-      const newSize = Math.min(
-        MAX_SIZE - totalSize + sizeObj.size,
-        sizeObj.len
-      );
-      totalSize += newSize - sizeObj.size;
-      sizeObj.size = newSize;
-    }
-
-    return (section: Section) => {
-      const sizeObj = sectionSizes.find(sizeObj => sizeObj.section === section);
-      if (sizeObj) return sizeObj.size;
-      return DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
-    };
-  }
-
-  markersPredicateFactory(
-    length: number,
-    highlightIndex: number,
-    numChangesShownWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED
-  ): (index: number) => ChangeMarkersInList {
-    const showWhenCollapsedPredicate = (index: number) => {
-      if (highlightIndex === -1) return index < numChangesShownWhenCollapsed;
-      if (highlightIndex === 0)
-        return index <= numChangesShownWhenCollapsed - 1;
-      if (highlightIndex === length - 1)
-        return index >= length - numChangesShownWhenCollapsed;
-      let numBeforeHighlight = Math.floor(numChangesShownWhenCollapsed / 2);
-      let numAfterHighlight =
-        Math.floor(numChangesShownWhenCollapsed / 2) -
-        (numChangesShownWhenCollapsed % 2 ? 0 : 1);
-      numBeforeHighlight += Math.max(
-        highlightIndex + numAfterHighlight - length + 1,
-        0
-      );
-      numAfterHighlight -= Math.min(0, highlightIndex - numBeforeHighlight);
-      return (
-        highlightIndex - numBeforeHighlight <= index &&
-        index <= highlightIndex + numAfterHighlight
-      );
-    };
-    return (index: number) => {
-      return {
-        showCurrentChangeArrow:
-          highlightIndex !== -1 && index === highlightIndex,
-        showWhenCollapsed: showWhenCollapsedPredicate(index),
-        showTopArrow:
-          index >= 1 &&
-          index !== highlightIndex &&
-          showWhenCollapsedPredicate(index) &&
-          !showWhenCollapsedPredicate(index - 1),
-        showBottomArrow:
-          index <= length - 2 &&
-          index !== highlightIndex &&
-          showWhenCollapsedPredicate(index) &&
-          !showWhenCollapsedPredicate(index + 1),
-      };
-    };
-  }
-
-  renderMarkers(changeMarkers: ChangeMarkersInList) {
-    if (changeMarkers.showCurrentChangeArrow) {
-      return html`<span
-        role="img"
-        class="arrowToCurrentChange"
-        aria-label="Arrow marking current change"
-        >âž”</span
-      >`;
-    }
-    if (changeMarkers.showTopArrow) {
-      return html`<span
-        role="img"
-        class="marker"
-        aria-label="Arrow marking change has collapsed ancestors"
-        ><iron-icon icon="gr-icons:arrowDropUp"></iron-icon
-      ></span> `;
-    }
-    if (changeMarkers.showBottomArrow) {
-      return html`<span
-        role="img"
-        class="marker"
-        aria-label="Arrow marking change has collapsed descendants"
-        ><iron-icon icon="gr-icons:arrowDropDown"></iron-icon
-      ></span> `;
-    }
-    return nothing;
-  }
-
-  reload(getRelatedChanges?: Promise<RelatedChangesInfo | undefined>) {
-    const change = this.change;
-    if (!change) return Promise.reject(new Error('change missing'));
-    if (!this.patchNum) return Promise.reject(new Error('patchNum missing'));
-    if (!getRelatedChanges) {
-      getRelatedChanges = this.restApiService.getRelatedChanges(
-        change._number,
-        this.patchNum
-      );
-    }
-    const promises: Array<Promise<void>> = [
-      getRelatedChanges.then(response => {
-        if (!response) {
-          throw new Error('getRelatedChanges returned undefined response');
-        }
-        this.relatedChanges = response?.changes ?? [];
-      }),
-      this.restApiService
-        .getChangesSubmittedTogether(change._number)
-        .then(response => {
-          this.submittedTogether = response;
-        }),
-      this.restApiService
-        .getChangeCherryPicks(change.project, change.change_id, change._number)
-        .then(response => {
-          this.cherryPickChanges = response || [];
-        }),
-    ];
-
-    // Get conflicts if change is open and is mergeable.
-    // Mergeable is output of restApiServict.getMergeable from gr-change-view
-    if (changeIsOpen(change) && this.mergeable) {
-      promises.push(
-        this.restApiService
-          .getChangeConflicts(change._number)
-          .then(response => {
-            this.conflictingChanges = response ?? [];
-          })
-      );
-    }
-    if (change.topic) {
-      const changeTopic = change.topic;
-      promises.push(
-        this.restApiService.getConfig().then(config => {
-          if (config && !config.change.submit_whole_topic) {
-            return this.restApiService
-              .getChangesWithSameTopic(changeTopic, change._number)
-              .then(response => {
-                if (changeTopic === this.change?.topic) {
-                  this.sameTopicChanges = response ?? [];
-                }
-              });
-          }
-          this.sameTopicChanges = [];
-          return Promise.resolve();
-        })
-      );
-    }
-
-    return Promise.all(promises);
-  }
-
-  /**
-   * Do the given objects describe the same change? Compares the changes by
-   * their numbers.
-   */
-  _changesEqual(
-    a?: ChangeInfo | RelatedChangeAndCommitInfo,
-    b?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
-  ) {
-    const aNum = this._getChangeNumber(a);
-    const bNum = this._getChangeNumber(b);
-    return aNum === bNum;
-  }
-
-  /**
-   * Get the change number from either a ChangeInfo (such as those included in
-   * SubmittedTogetherInfo responses) or get the change number from a
-   * RelatedChangeAndCommitInfo (such as those included in a
-   * RelatedChangesInfo response).
-   */
-  _getChangeNumber(
-    change?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
-  ) {
-    // Default to 0 if change property is not defined.
-    if (!change) return 0;
-
-    if (isChangeInfo(change)) {
-      return change._number;
-    }
-    return change._change_number;
-  }
-
-  /*
-   * A list of commit ids connected to change to understand if other change
-   * is direct or indirect ancestor / descendant.
-   */
-  _computeConnectedRevisions(
-    change?: ParsedChangeInfo,
-    patchNum?: PatchSetNum,
-    relatedChanges?: RelatedChangeAndCommitInfo[]
-  ) {
-    if (!patchNum || !relatedChanges || !change) {
-      return [];
-    }
-
-    const connected: CommitId[] = [];
-    const changeRevision = getRevisionKey(change, patchNum);
-    const commits = relatedChanges.map(c => c.commit);
-    let pos = commits.length - 1;
-
-    while (pos >= 0) {
-      const commit: CommitId = commits[pos].commit;
-      connected.push(commit);
-      // TODO(TS): Ensure that both (commit and changeRevision) are string and use === instead
-      // eslint-disable-next-line eqeqeq
-      if (commit == changeRevision) {
-        break;
-      }
-      pos--;
-    }
-    while (pos >= 0) {
-      for (let i = 0; i < commits[pos].parents.length; i++) {
-        if (connected.includes(commits[pos].parents[i].commit)) {
-          connected.push(commits[pos].commit);
-          break;
-        }
-      }
-      --pos;
-    }
-    return connected;
-  }
-}
-
-@customElement('gr-related-collapse')
-export class GrRelatedCollapse extends GrLitElement {
-  @property()
-  title = '';
-
-  @property()
-  showAll = false;
-
-  @property()
-  length = 0;
-
-  @property()
-  numChangesWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
-
-  private readonly reporting = appContext.reportingService;
-
-  static get styles() {
-    return [
-      sharedStyles,
-      css`
-        .title {
-          font-weight: var(--font-weight-bold);
-          color: var(--deemphasized-text-color);
-          padding-left: var(--metadata-horizontal-padding);
-        }
-        h4 {
-          display: flex;
-          align-self: flex-end;
-        }
-        gr-button {
-          display: flex;
-        }
-        /* This is a hacky solution from old gr-related-change-list
-         * TODO(milutin): find layout without needing it
-         */
-        h4:before,
-        gr-button:before,
-        ::slotted(gr-related-change):before {
-          content: ' ';
-          flex-shrink: 0;
-          width: 1.2em;
-        }
-        .collapsed ::slotted(gr-related-change.show-when-collapsed) {
-          visibility: visible;
-          height: auto;
-        }
-        .collapsed ::slotted(.marker) {
-          display: block;
-        }
-        .show-all ::slotted(.marker) {
-          display: none;
-        }
-        /* keep width, so width of section and position of show all button
-         * are set according to width of all (even hidden) elements
-         */
-        .collapsed ::slotted(gr-related-change) {
-          visibility: hidden;
-          height: 0px;
-        }
-        ::slotted(gr-related-change) {
-          visibility: visible;
-          height: auto;
-        }
-        gr-button iron-icon {
-          color: inherit;
-          --iron-icon-height: 18px;
-          --iron-icon-width: 18px;
-        }
-        .container {
-          justify-content: space-between;
-          display: flex;
-          margin-bottom: var(--spacing-s);
-        }
-        :host(.first) .container {
-          margin-bottom: var(--spacing-m);
-        }
-      `,
-    ];
-  }
-
-  render() {
-    const title = html`<h4 class="title">${this.title}</h4>`;
-
-    const collapsible = this.length > this.numChangesWhenCollapsed;
-    const items = html` <div
-      class="${!this.showAll && collapsible ? 'collapsed' : 'show-all'}"
-    >
-      <slot></slot>
-    </div>`;
-
-    let button: TemplateResult | typeof nothing = nothing;
-    if (collapsible) {
-      let buttonText = 'Show less';
-      let buttonIcon = 'expand-less';
-      if (!this.showAll) {
-        buttonText = `Show all (${this.length})`;
-        buttonIcon = 'expand-more';
-      }
-      button = html`<gr-button link="" @click="${this.toggle}"
-        >${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
-      ></gr-button>`;
-    }
-
-    return html`<div class="container">${title}${button}</div>
-      ${items}`;
-  }
-
-  private toggle(e: MouseEvent) {
-    e.stopPropagation();
-    this.showAll = !this.showAll;
-    this.reporting.reportInteraction('toggle show all button', {
-      sectionName: this.title,
-      toState: this.showAll ? 'Show all' : 'Show less',
-    });
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-related-changes-list-experimental': GrRelatedChangesListExperimental;
-    'gr-related-collapse': GrRelatedCollapse;
-  }
-}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts
deleted file mode 100644
index 971da40..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental_test.ts
+++ /dev/null
@@ -1,630 +0,0 @@
-/**
- * @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 {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
-import {PluginApi} from '../../../api/plugin';
-import {ChangeStatus} from '../../../constants/constants';
-import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import '../../../test/common-test-setup-karma';
-import {
-  createChange,
-  createCommitInfoWithRequiredCommit,
-  createParsedChange,
-  createRelatedChangeAndCommitInfo,
-  createRelatedChangesInfo,
-  createRevision,
-  createSubmittedTogetherInfo,
-} from '../../../test/test-data-generators';
-import {
-  queryAndAssert,
-  resetPlugins,
-  stubRestApi,
-} from '../../../test/test-utils';
-import {
-  ChangeId,
-  ChangeInfo,
-  CommitId,
-  NumericChangeId,
-  PatchSetNum,
-  RelatedChangeAndCommitInfo,
-  RelatedChangesInfo,
-  SubmittedTogetherInfo,
-} from '../../../types/common';
-import {ParsedChangeInfo} from '../../../types/types';
-import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import './gr-related-changes-list-experimental';
-import {
-  ChangeMarkersInList,
-  GrRelatedChangesListExperimental,
-  GrRelatedCollapse,
-  Section,
-} from './gr-related-changes-list-experimental';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-const basicFixture = fixtureFromElement('gr-related-changes-list-experimental');
-
-suite('gr-related-changes-list-experimental', () => {
-  let element: GrRelatedChangesListExperimental;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  suite('show when collapsed', () => {
-    function genBoolArray(
-      instructions: Array<{
-        len: number;
-        v: boolean;
-      }>
-    ) {
-      return instructions
-        .map(inst => Array.from({length: inst.len}, () => inst.v))
-        .reduce((acc, val) => acc.concat(val), []);
-    }
-
-    function checkShowWhenCollapsed(
-      expected: boolean[],
-      markersPredicate: (index: number) => ChangeMarkersInList,
-      msg: string
-    ) {
-      for (let i = 0; i < expected.length; i++) {
-        assert.equal(
-          markersPredicate(i).showWhenCollapsed,
-          expected[i],
-          `change on pos (${i}) ${msg}`
-        );
-      }
-    }
-
-    test('size 5', () => {
-      const markersPredicate = element.markersPredicateFactory(10, 4, 5);
-      const expectedCollapsing = genBoolArray([
-        {len: 2, v: false},
-        {len: 5, v: true},
-        {len: 3, v: false},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing,
-        markersPredicate,
-        'highlight 4, size 10, size 5'
-      );
-
-      const markersPredicate2 = element.markersPredicateFactory(10, 8, 5);
-      const expectedCollapsing2 = genBoolArray([
-        {len: 5, v: false},
-        {len: 5, v: true},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing2,
-        markersPredicate2,
-        'highlight 8, size 10, size 5'
-      );
-
-      const markersPredicate3 = element.markersPredicateFactory(10, 1, 5);
-      const expectedCollapsing3 = genBoolArray([
-        {len: 5, v: true},
-        {len: 5, v: false},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing3,
-        markersPredicate3,
-        'highlight 1, size 10, size 5'
-      );
-    });
-
-    test('size 4', () => {
-      const markersPredicate = element.markersPredicateFactory(10, 4, 4);
-      const expectedCollapsing = genBoolArray([
-        {len: 2, v: false},
-        {len: 4, v: true},
-        {len: 4, v: false},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing,
-        markersPredicate,
-        'highlight 4, len 10, size 4'
-      );
-
-      const markersPredicate2 = element.markersPredicateFactory(10, 8, 4);
-      const expectedCollapsing2 = genBoolArray([
-        {len: 6, v: false},
-        {len: 4, v: true},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing2,
-        markersPredicate2,
-        'highlight 8, len 10, size 4'
-      );
-
-      const markersPredicate3 = element.markersPredicateFactory(10, 1, 4);
-      const expectedCollapsing3 = genBoolArray([
-        {len: 4, v: true},
-        {len: 6, v: false},
-      ]);
-      checkShowWhenCollapsed(
-        expectedCollapsing3,
-        markersPredicate3,
-        'highlight 1, len 10, size 4'
-      );
-    });
-  });
-
-  suite('section size', () => {
-    test('1 section', () => {
-      const sectionSize = element.sectionSizeFactory(20, 0, 0, 0, 0);
-      assert.equal(sectionSize(Section.RELATED_CHANGES), 15);
-      const sectionSize2 = element.sectionSizeFactory(0, 0, 10, 0, 0);
-      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
-    });
-    test('2 sections', () => {
-      const sectionSize = element.sectionSizeFactory(20, 20, 0, 0, 0);
-      assert.equal(sectionSize(Section.RELATED_CHANGES), 11);
-      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
-      const sectionSize2 = element.sectionSizeFactory(4, 0, 10, 0, 0);
-      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
-      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
-    });
-    test('many sections', () => {
-      const sectionSize = element.sectionSizeFactory(20, 20, 3, 3, 3);
-      assert.equal(sectionSize(Section.RELATED_CHANGES), 3);
-      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
-      const sectionSize2 = element.sectionSizeFactory(4, 1, 10, 1, 1);
-      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
-      assert.equal(sectionSize2(Section.SAME_TOPIC), 4);
-    });
-  });
-
-  suite('test first non-empty list', () => {
-    const relatedChangeInfo: RelatedChangesInfo = {
-      ...createRelatedChangesInfo(),
-      changes: [createRelatedChangeAndCommitInfo()],
-    };
-    const submittedTogether: SubmittedTogetherInfo = {
-      ...createSubmittedTogetherInfo(),
-      changes: [createChange()],
-    };
-
-    setup(() => {
-      element.change = createParsedChange();
-      element.patchNum = 1 as PatchSetNum;
-    });
-
-    test('first list', async () => {
-      stubRestApi('getRelatedChanges').returns(
-        Promise.resolve(relatedChangeInfo)
-      );
-      await element.reload();
-      const section = queryAndAssert<HTMLElement>(element, '#relatedChanges');
-      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
-        section,
-        'gr-related-collapse'
-      );
-      assert.isTrue(relatedChanges!.classList.contains('first'));
-    });
-
-    test('first empty second non-empty', async () => {
-      stubRestApi('getRelatedChanges').returns(
-        Promise.resolve(createRelatedChangesInfo())
-      );
-      stubRestApi('getChangesSubmittedTogether').returns(
-        Promise.resolve(submittedTogether)
-      );
-      await element.reload();
-      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
-        queryAndAssert<HTMLElement>(element, '#relatedChanges'),
-        'gr-related-collapse'
-      );
-      assert.isFalse(relatedChanges!.classList.contains('first'));
-      const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
-        queryAndAssert<HTMLElement>(element, '#submittedTogether'),
-        'gr-related-collapse'
-      );
-      assert.isTrue(submittedTogetherSection!.classList.contains('first'));
-    });
-
-    test('first non-empty second empty third non-empty', async () => {
-      stubRestApi('getRelatedChanges').returns(
-        Promise.resolve(relatedChangeInfo)
-      );
-      stubRestApi('getChangesSubmittedTogether').returns(
-        Promise.resolve(createSubmittedTogetherInfo())
-      );
-      stubRestApi('getChangeCherryPicks').returns(
-        Promise.resolve([createChange()])
-      );
-      await element.reload();
-      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
-        queryAndAssert<HTMLElement>(element, '#relatedChanges'),
-        'gr-related-collapse'
-      );
-      assert.isTrue(relatedChanges!.classList.contains('first'));
-      const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
-        queryAndAssert<HTMLElement>(element, '#submittedTogether'),
-        'gr-related-collapse'
-      );
-      assert.isFalse(submittedTogetherSection!.classList.contains('first'));
-      const cherryPicks = queryAndAssert<GrRelatedCollapse>(
-        queryAndAssert<HTMLElement>(element, '#cherryPicks'),
-        'gr-related-collapse'
-      );
-      assert.isFalse(cherryPicks!.classList.contains('first'));
-    });
-  });
-
-  test('_changesEqual', () => {
-    const change1: ChangeInfo = {
-      ...createChange(),
-      change_id: '123' as ChangeId,
-      _number: 0 as NumericChangeId,
-    };
-    const change2: ChangeInfo = {
-      ...createChange(),
-      change_id: '456' as ChangeId,
-      _number: 1 as NumericChangeId,
-    };
-    const change3: ChangeInfo = {
-      ...createChange(),
-      change_id: '123' as ChangeId,
-      _number: 2 as NumericChangeId,
-    };
-    const change4: RelatedChangeAndCommitInfo = {
-      ...createRelatedChangeAndCommitInfo(),
-      change_id: '123' as ChangeId,
-      _change_number: 1 as NumericChangeId,
-    };
-
-    assert.isTrue(element._changesEqual(change1, change1));
-    assert.isFalse(element._changesEqual(change1, change2));
-    assert.isFalse(element._changesEqual(change1, change3));
-    assert.isTrue(element._changesEqual(change2, change4));
-  });
-
-  test('_getChangeNumber', () => {
-    const change1: ChangeInfo = {
-      ...createChange(),
-      change_id: '123' as ChangeId,
-      _number: 0 as NumericChangeId,
-    };
-    const change2: ChangeInfo = {
-      ...createChange(),
-      change_id: '456' as ChangeId,
-      _number: 1 as NumericChangeId,
-    };
-    assert.equal(element._getChangeNumber(change1), 0);
-    assert.equal(element._getChangeNumber(change2), 1);
-  });
-
-  suite('get conflicts tests', () => {
-    let element: GrRelatedChangesListExperimental;
-    let conflictsStub: SinonStubbedMember<RestApiService['getChangeConflicts']>;
-
-    setup(() => {
-      element = basicFixture.instantiate();
-      conflictsStub = stubRestApi('getChangeConflicts').returns(
-        Promise.resolve(undefined)
-      );
-    });
-
-    test('request conflicts if open and mergeable', () => {
-      element.patchNum = 7 as PatchSetNum;
-      element.change = {
-        ...createParsedChange(),
-        change_id: '123' as ChangeId,
-        status: ChangeStatus.NEW,
-      };
-      element.mergeable = true;
-      element.reload();
-      assert.isTrue(conflictsStub.called);
-    });
-
-    test('does not request conflicts if closed and mergeable', () => {
-      element.patchNum = 7 as PatchSetNum;
-      element.change = {
-        ...createParsedChange(),
-        change_id: '123' as ChangeId,
-        status: ChangeStatus.NEW,
-      };
-      element.reload();
-      assert.isFalse(conflictsStub.called);
-    });
-
-    test('does not request conflicts if open and not mergeable', () => {
-      element.patchNum = 7 as PatchSetNum;
-      element.change = {
-        ...createParsedChange(),
-        change_id: '123' as ChangeId,
-        status: ChangeStatus.NEW,
-      };
-      element.mergeable = false;
-      element.reload();
-      assert.isFalse(conflictsStub.called);
-    });
-
-    test('doesnt request conflicts if closed and not mergeable', () => {
-      element.patchNum = 7 as PatchSetNum;
-      element.change = {
-        ...createParsedChange(),
-        change_id: '123' as ChangeId,
-        status: ChangeStatus.NEW,
-      };
-      element.mergeable = false;
-      element.reload();
-      assert.isFalse(conflictsStub.called);
-    });
-  });
-
-  test('connected revisions', () => {
-    const change: ParsedChangeInfo = {
-      ...createParsedChange(),
-      revisions: {
-        e3c6d60783bfdec9ebae7dcfec4662360433449e: createRevision(1),
-        '26e5e4c9c7ae31cbd876271cca281ce22b413997': createRevision(2),
-        bf7884d695296ca0c91702ba3e2bc8df0f69a907: createRevision(7),
-        b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3: createRevision(5),
-        d6bcee67570859ccb684873a85cf50b1f0e96fda: createRevision(6),
-        cc960918a7f90388f4a9e05753d0f7b90ad44546: createRevision(3),
-        '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': createRevision(4),
-      },
-    };
-    let patchNum = 7 as PatchSetNum;
-    let relatedChanges: RelatedChangeAndCommitInfo[] = [
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
-          ),
-          parents: [
-            {
-              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
-              subject: 'subject1',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
-          ),
-          parents: [
-            {
-              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
-              subject: 'subject2',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
-          ),
-          parents: [
-            {
-              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
-              subject: 'subject3',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            'b0ccb183494a8e340b8725a2dc553967d61e6dae'
-          ),
-          parents: [
-            {
-              commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' as CommitId,
-              subject: 'subject4',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
-          ),
-          parents: [
-            {
-              commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' as CommitId,
-              subject: 'subject5',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '613bc4f81741a559c6667ac08d71dcc3348f73ce'
-          ),
-          parents: [
-            {
-              commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' as CommitId,
-              subject: 'subject6',
-            },
-          ],
-        },
-      },
-    ];
-
-    let connectedChanges = element._computeConnectedRevisions(
-      change,
-      patchNum,
-      relatedChanges
-    );
-    assert.deepEqual(connectedChanges, [
-      '613bc4f81741a559c6667ac08d71dcc3348f73ce',
-      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
-      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
-      'b0ccb183494a8e340b8725a2dc553967d61e6dae',
-      '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
-      '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
-      '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
-    ]);
-
-    patchNum = 4 as PatchSetNum;
-    relatedChanges = [
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
-          ),
-          parents: [
-            {
-              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
-          ),
-          parents: [
-            {
-              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
-          ),
-          parents: [
-            {
-              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b'
-          ),
-          parents: [
-            {
-              commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
-          ),
-          parents: [
-            {
-              commit: 'af815dac54318826b7f1fa468acc76349ffc588e' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-      {
-        ...createRelatedChangeAndCommitInfo(),
-        commit: {
-          ...createCommitInfoWithRequiredCommit(
-            'af815dac54318826b7f1fa468acc76349ffc588e'
-          ),
-          parents: [
-            {
-              commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' as CommitId,
-              subject: 'My parent commit',
-            },
-          ],
-        },
-      },
-    ];
-
-    connectedChanges = element._computeConnectedRevisions(
-      change,
-      patchNum,
-      relatedChanges
-    );
-    assert.deepEqual(connectedChanges, [
-      'af815dac54318826b7f1fa468acc76349ffc588e',
-      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
-      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
-      'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
-    ]);
-  });
-
-  suite('gr-related-changes-list plugin tests', () => {
-    let element: GrRelatedChangesListExperimental;
-
-    setup(() => {
-      resetPlugins();
-      element = basicFixture.instantiate();
-    });
-
-    teardown(() => {
-      resetPlugins();
-    });
-
-    test('endpoint params', done => {
-      element.change = {...createParsedChange(), labels: {}};
-      interface RelatedChangesListGrEndpointDecorator
-        extends GrEndpointDecorator {
-        plugin: PluginApi;
-        change: ParsedChangeInfo;
-      }
-      let hookEl: RelatedChangesListGrEndpointDecorator;
-      let plugin: PluginApi;
-      pluginApi.install(
-        p => {
-          plugin = p;
-          plugin
-            .hook('related-changes-section')
-            .getLastAttached()
-            .then(el => (hookEl = el as RelatedChangesListGrEndpointDecorator));
-        },
-        '0.1',
-        'http://some/plugins/url1.js'
-      );
-      getPluginLoader().loadPlugins([]);
-      flush(() => {
-        assert.strictEqual(hookEl.plugin, plugin);
-        assert.strictEqual(hookEl.change, element.change);
-        done();
-      });
-    });
-  });
-});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
similarity index 100%
rename from polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 927f3c9..8d5bc33 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,217 +14,547 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/shared-styles';
+import {html, nothing} from 'lit-html';
+import './gr-related-change';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-related-changes-list_html';
+import {classMap} from 'lit-html/directives/class-map';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {
+  customElement,
+  property,
+  css,
+  internalProperty,
+  TemplateResult,
+} from 'lit-element';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {
+  SubmittedTogetherInfo,
+  ChangeInfo,
+  RelatedChangeAndCommitInfo,
+  RelatedChangesInfo,
+  PatchSetNum,
+  CommitId,
+} from '../../../types/common';
+import {appContext} from '../../../services/app-context';
+import {ParsedChangeInfo} from '../../../types/types';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {ChangeStatus} from '../../../constants/constants';
-
+import {pluralize} from '../../../utils/string-util';
 import {
   changeIsOpen,
   getRevisionKey,
   isChangeInfo,
 } from '../../../utils/change-util';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {customElement, observe, property} from '@polymer/decorators';
-import {
-  ChangeId,
-  ChangeInfo,
-  CommitId,
-  NumericChangeId,
-  PatchSetNum,
-  RelatedChangeAndCommitInfo,
-  RelatedChangesInfo,
-  RepoName,
-  SubmittedTogetherInfo,
-} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
-import {pluralize} from '../../../utils/string-util';
-import {ParsedChangeInfo} from '../../../types/types';
 
-function getEmptySubmitTogetherInfo(): SubmittedTogetherInfo {
-  return {changes: [], non_visible_changes: 0};
+/** What is the maximum number of shown changes in collapsed list? */
+const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
+
+export interface ChangeMarkersInList {
+  showCurrentChangeArrow: boolean;
+  showWhenCollapsed: boolean;
+  showTopArrow: boolean;
+  showBottomArrow: boolean;
+}
+
+export enum Section {
+  RELATED_CHANGES = 'related changes',
+  SUBMITTED_TOGETHER = 'submitted together',
+  SAME_TOPIC = 'same topic',
+  MERGE_CONFLICTS = 'merge conflicts',
+  CHERRY_PICKS = 'cherry picks',
 }
 
 @customElement('gr-related-changes-list')
-export class GrRelatedChangesList extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  /**
-   * Fired when a new section is loaded so that the change view can determine
-   * a show more button is needed, sometimes before all the sections finish
-   * loading.
-   *
-   * @event new-section-loaded
-   */
-
-  @property({type: Object})
+export class GrRelatedChangesList extends GrLitElement {
+  @property()
   change?: ParsedChangeInfo;
 
-  @property({type: Boolean, notify: true})
-  hasParent = false;
-
   @property({type: String})
   patchNum?: PatchSetNum;
 
-  @property({type: Boolean, reflectToAttribute: true})
-  hidden = false;
-
-  @property({type: Boolean, notify: true})
-  loading?: boolean;
-
-  @property({type: Boolean})
+  @property()
   mergeable?: boolean;
 
-  @property({
-    type: Array,
-    computed:
-      '_computeConnectedRevisions(change, patchNum, ' +
-      '_relatedResponse.changes)',
-  })
-  _connectedRevisions?: CommitId[];
+  @internalProperty()
+  submittedTogether?: SubmittedTogetherInfo = {
+    changes: [],
+    non_visible_changes: 0,
+  };
 
-  @property({type: Object})
-  _relatedResponse: RelatedChangesInfo = {changes: []};
+  @internalProperty()
+  relatedChanges: RelatedChangeAndCommitInfo[] = [];
 
-  @property({type: Object})
-  _submittedTogether?: SubmittedTogetherInfo = getEmptySubmitTogetherInfo();
+  @internalProperty()
+  conflictingChanges: ChangeInfo[] = [];
 
-  @property({type: Array})
-  _conflicts: ChangeInfo[] = [];
+  @internalProperty()
+  cherryPickChanges: ChangeInfo[] = [];
 
-  @property({type: Array})
-  _cherryPicks: ChangeInfo[] = [];
-
-  @property({type: Array})
-  _sameTopic?: ChangeInfo[] = [];
+  @internalProperty()
+  sameTopicChanges: ChangeInfo[] = [];
 
   private readonly restApiService = appContext.restApiService;
 
-  private readonly reportingService = appContext.reportingService;
-
-  clear() {
-    this.loading = true;
-    this.hidden = true;
-
-    this._relatedResponse = {changes: []};
-    this._submittedTogether = getEmptySubmitTogetherInfo();
-    this._conflicts = [];
-    this._cherryPicks = [];
-    this._sameTopic = [];
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        .note {
+          color: var(--error-text-color);
+          margin-left: 1.2em;
+        }
+        section {
+          margin-bottom: var(--spacing-l);
+        }
+        gr-related-change {
+          display: flex;
+        }
+        .marker {
+          position: absolute;
+          margin-left: calc(-1 * var(--spacing-s));
+        }
+        .arrowToCurrentChange {
+          position: absolute;
+        }
+      `,
+    ];
   }
 
-  reload() {
-    if (!this.change || !this.patchNum) {
-      return Promise.resolve();
+  render() {
+    const sectionSize = this.sectionSizeFactory(
+      this.relatedChanges.length,
+      this.submittedTogether?.changes.length || 0,
+      this.sameTopicChanges.length,
+      this.conflictingChanges.length,
+      this.cherryPickChanges.length
+    );
+    const relatedChangesMarkersPredicate = this.markersPredicateFactory(
+      this.relatedChanges.length,
+      this.relatedChanges.findIndex(relatedChange =>
+        this._changesEqual(relatedChange, this.change)
+      ),
+      sectionSize(Section.RELATED_CHANGES)
+    );
+    const connectedRevisions = this._computeConnectedRevisions(
+      this.change,
+      this.patchNum,
+      this.relatedChanges
+    );
+    let firstNonEmptySectionFound = false;
+    let isFirstNonEmpty =
+      !firstNonEmptySectionFound && !!this.relatedChanges.length;
+    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
+    const relatedChangeSection = html` <section
+      id="relatedChanges"
+      ?hidden=${!this.relatedChanges.length}
+    >
+      <gr-related-collapse
+        title="Relation chain"
+        class="${classMap({first: isFirstNonEmpty})}"
+        .length=${this.relatedChanges.length}
+        .numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
+      >
+        ${this.relatedChanges.map(
+          (change, index) =>
+            html`${this.renderMarkers(
+                relatedChangesMarkersPredicate(index)
+              )}<gr-related-change
+                class="${classMap({
+                  ['show-when-collapsed']: relatedChangesMarkersPredicate(index)
+                    .showWhenCollapsed,
+                })}"
+                .change="${change}"
+                .connectedRevisions="${connectedRevisions}"
+                .href="${change?._change_number
+                  ? GerritNav.getUrlForChangeById(
+                      change._change_number,
+                      change.project,
+                      change._revision_number as PatchSetNum
+                    )
+                  : ''}"
+                .showChangeStatus=${true}
+                >${change.commit.subject}</gr-related-change
+              >`
+        )}
+      </gr-related-collapse>
+    </section>`;
+
+    const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
+    const countNonVisibleChanges =
+      this.submittedTogether?.non_visible_changes ?? 0;
+    const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
+      submittedTogetherChanges.length,
+      submittedTogetherChanges.findIndex(relatedChange =>
+        this._changesEqual(relatedChange, this.change)
+      ),
+      sectionSize(Section.SUBMITTED_TOGETHER)
+    );
+    isFirstNonEmpty =
+      !firstNonEmptySectionFound &&
+      (!!submittedTogetherChanges?.length ||
+        !!this.submittedTogether?.non_visible_changes);
+    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
+    const submittedTogetherSection = html`<section
+      id="submittedTogether"
+      ?hidden=${!submittedTogetherChanges?.length &&
+      !this.submittedTogether?.non_visible_changes}
+    >
+      <gr-related-collapse
+        title="Submitted together"
+        class="${classMap({first: isFirstNonEmpty})}"
+        .length=${submittedTogetherChanges.length}
+        .numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
+      >
+        ${submittedTogetherChanges.map(
+          (change, index) =>
+            html`${this.renderMarkers(
+                submittedTogetherMarkersPredicate(index)
+              )}<gr-related-change
+                class="${classMap({
+                  ['show-when-collapsed']: submittedTogetherMarkersPredicate(
+                    index
+                  ).showWhenCollapsed,
+                })}"
+                .change="${change}"
+                .href="${GerritNav.getUrlForChangeById(
+                  change._number,
+                  change.project
+                )}"
+                .showSubmittableCheck=${true}
+                >${change.project}: ${change.branch}:
+                ${change.subject}</gr-related-change
+              >`
+        )}
+      </gr-related-collapse>
+      <div class="note" ?hidden=${!countNonVisibleChanges}>
+        (+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
+      </div>
+    </section>`;
+
+    const sameTopicMarkersPredicate = this.markersPredicateFactory(
+      this.sameTopicChanges.length,
+      -1,
+      sectionSize(Section.SAME_TOPIC)
+    );
+    isFirstNonEmpty =
+      !firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
+    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
+    const sameTopicSection = html`<section
+      id="sameTopic"
+      ?hidden=${!this.sameTopicChanges?.length}
+    >
+      <gr-related-collapse
+        title="Same topic"
+        class="${classMap({first: isFirstNonEmpty})}"
+        .length=${this.sameTopicChanges.length}
+        .numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
+      >
+        ${this.sameTopicChanges.map(
+          (change, index) =>
+            html`${this.renderMarkers(
+                sameTopicMarkersPredicate(index)
+              )}<gr-related-change
+                class="${classMap({
+                  ['show-when-collapsed']: sameTopicMarkersPredicate(index)
+                    .showWhenCollapsed,
+                })}"
+                .change="${change}"
+                .href="${GerritNav.getUrlForChangeById(
+                  change._number,
+                  change.project
+                )}"
+                >${change.project}: ${change.branch}:
+                ${change.subject}</gr-related-change
+              >`
+        )}
+      </gr-related-collapse>
+    </section>`;
+
+    const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
+      this.conflictingChanges.length,
+      -1,
+      sectionSize(Section.MERGE_CONFLICTS)
+    );
+    isFirstNonEmpty =
+      !firstNonEmptySectionFound && !!this.conflictingChanges?.length;
+    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
+    const mergeConflictsSection = html`<section
+      id="mergeConflicts"
+      ?hidden=${!this.conflictingChanges?.length}
+    >
+      <gr-related-collapse
+        title="Merge conflicts"
+        class="${classMap({first: isFirstNonEmpty})}"
+        .length=${this.conflictingChanges.length}
+        .numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
+      >
+        ${this.conflictingChanges.map(
+          (change, index) =>
+            html`${this.renderMarkers(
+                mergeConflictsMarkersPredicate(index)
+              )}<gr-related-change
+                class="${classMap({
+                  ['show-when-collapsed']: mergeConflictsMarkersPredicate(index)
+                    .showWhenCollapsed,
+                })}"
+                .change="${change}"
+                .href="${GerritNav.getUrlForChangeById(
+                  change._number,
+                  change.project
+                )}"
+                >${change.subject}</gr-related-change
+              >`
+        )}
+      </gr-related-collapse>
+    </section>`;
+
+    const cherryPicksMarkersPredicate = this.markersPredicateFactory(
+      this.cherryPickChanges.length,
+      -1,
+      sectionSize(Section.CHERRY_PICKS)
+    );
+    isFirstNonEmpty =
+      !firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
+    firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
+    const cherryPicksSection = html`<section
+      id="cherryPicks"
+      ?hidden=${!this.cherryPickChanges?.length}
+    >
+      <gr-related-collapse
+        title="Cherry picks"
+        class="${classMap({first: isFirstNonEmpty})}"
+        .length=${this.cherryPickChanges.length}
+        .numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
+      >
+        ${this.cherryPickChanges.map(
+          (change, index) =>
+            html`${this.renderMarkers(
+                cherryPicksMarkersPredicate(index)
+              )}<gr-related-change
+                class="${classMap({
+                  ['show-when-collapsed']: cherryPicksMarkersPredicate(index)
+                    .showWhenCollapsed,
+                })}"
+                .change="${change}"
+                .href="${GerritNav.getUrlForChangeById(
+                  change._number,
+                  change.project
+                )}"
+                >${change.branch}: ${change.subject}</gr-related-change
+              >`
+        )}
+      </gr-related-collapse>
+    </section>`;
+
+    return html`<gr-endpoint-decorator name="related-changes-section">
+      <gr-endpoint-param
+        name="change"
+        .value=${this.change}
+      ></gr-endpoint-param>
+      <gr-endpoint-slot name="top"></gr-endpoint-slot>
+      ${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
+      ${mergeConflictsSection} ${cherryPicksSection}
+      <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
+    </gr-endpoint-decorator>`;
+  }
+
+  sectionSizeFactory(
+    relatedChangesLen: number,
+    submittedTogetherLen: number,
+    sameTopicLen: number,
+    mergeConflictsLen: number,
+    cherryPicksLen: number
+  ) {
+    const calcDefaultSize = (length: number) =>
+      Math.min(length, DEFALT_NUM_CHANGES_WHEN_COLLAPSED);
+
+    const sectionSizes = [
+      {
+        section: Section.RELATED_CHANGES,
+        size: calcDefaultSize(relatedChangesLen),
+        len: relatedChangesLen,
+      },
+      {
+        section: Section.SUBMITTED_TOGETHER,
+        size: calcDefaultSize(submittedTogetherLen),
+        len: submittedTogetherLen,
+      },
+      {
+        section: Section.SAME_TOPIC,
+        size: calcDefaultSize(sameTopicLen),
+        len: sameTopicLen,
+      },
+      {
+        section: Section.MERGE_CONFLICTS,
+        size: calcDefaultSize(mergeConflictsLen),
+        len: mergeConflictsLen,
+      },
+      {
+        section: Section.CHERRY_PICKS,
+        size: calcDefaultSize(cherryPicksLen),
+        len: cherryPicksLen,
+      },
+    ];
+
+    const FILLER = 1; // space for header
+    let totalSize = sectionSizes.reduce(
+      (acc, val) => acc + val.size + (val.size !== 0 ? FILLER : 0),
+      0
+    );
+
+    const MAX_SIZE = 16;
+    for (let i = 0; i < sectionSizes.length; i++) {
+      if (totalSize >= MAX_SIZE) break;
+      const sizeObj = sectionSizes[i];
+      if (sizeObj.size === sizeObj.len) continue;
+      const newSize = Math.min(
+        MAX_SIZE - totalSize + sizeObj.size,
+        sizeObj.len
+      );
+      totalSize += newSize - sizeObj.size;
+      sizeObj.size = newSize;
     }
+
+    return (section: Section) => {
+      const sizeObj = sectionSizes.find(sizeObj => sizeObj.section === section);
+      if (sizeObj) return sizeObj.size;
+      return DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
+    };
+  }
+
+  markersPredicateFactory(
+    length: number,
+    highlightIndex: number,
+    numChangesShownWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED
+  ): (index: number) => ChangeMarkersInList {
+    const showWhenCollapsedPredicate = (index: number) => {
+      if (highlightIndex === -1) return index < numChangesShownWhenCollapsed;
+      if (highlightIndex === 0)
+        return index <= numChangesShownWhenCollapsed - 1;
+      if (highlightIndex === length - 1)
+        return index >= length - numChangesShownWhenCollapsed;
+      let numBeforeHighlight = Math.floor(numChangesShownWhenCollapsed / 2);
+      let numAfterHighlight =
+        Math.floor(numChangesShownWhenCollapsed / 2) -
+        (numChangesShownWhenCollapsed % 2 ? 0 : 1);
+      numBeforeHighlight += Math.max(
+        highlightIndex + numAfterHighlight - length + 1,
+        0
+      );
+      numAfterHighlight -= Math.min(0, highlightIndex - numBeforeHighlight);
+      return (
+        highlightIndex - numBeforeHighlight <= index &&
+        index <= highlightIndex + numAfterHighlight
+      );
+    };
+    return (index: number) => {
+      return {
+        showCurrentChangeArrow:
+          highlightIndex !== -1 && index === highlightIndex,
+        showWhenCollapsed: showWhenCollapsedPredicate(index),
+        showTopArrow:
+          index >= 1 &&
+          index !== highlightIndex &&
+          showWhenCollapsedPredicate(index) &&
+          !showWhenCollapsedPredicate(index - 1),
+        showBottomArrow:
+          index <= length - 2 &&
+          index !== highlightIndex &&
+          showWhenCollapsedPredicate(index) &&
+          !showWhenCollapsedPredicate(index + 1),
+      };
+    };
+  }
+
+  renderMarkers(changeMarkers: ChangeMarkersInList) {
+    if (changeMarkers.showCurrentChangeArrow) {
+      return html`<span
+        role="img"
+        class="arrowToCurrentChange"
+        aria-label="Arrow marking current change"
+        >âž”</span
+      >`;
+    }
+    if (changeMarkers.showTopArrow) {
+      return html`<span
+        role="img"
+        class="marker"
+        aria-label="Arrow marking change has collapsed ancestors"
+        ><iron-icon icon="gr-icons:arrowDropUp"></iron-icon
+      ></span> `;
+    }
+    if (changeMarkers.showBottomArrow) {
+      return html`<span
+        role="img"
+        class="marker"
+        aria-label="Arrow marking change has collapsed descendants"
+        ><iron-icon icon="gr-icons:arrowDropDown"></iron-icon
+      ></span> `;
+    }
+    return nothing;
+  }
+
+  reload(getRelatedChanges?: Promise<RelatedChangesInfo | undefined>) {
     const change = this.change;
-    this.loading = true;
+    if (!change) return Promise.reject(new Error('change missing'));
+    if (!this.patchNum) return Promise.reject(new Error('patchNum missing'));
+    if (!getRelatedChanges) {
+      getRelatedChanges = this.restApiService.getRelatedChanges(
+        change._number,
+        this.patchNum
+      );
+    }
     const promises: Array<Promise<void>> = [
-      this.restApiService
-        .getRelatedChanges(change._number, this.patchNum)
-        .then(response => {
-          if (!response) {
-            throw new Error('getRelatedChanges returned undefined response');
-          }
-          this._relatedResponse = response;
-          this._fireReloadEvent();
-          this.hasParent = this._calculateHasParent(
-            change.change_id,
-            response.changes
-          );
-        }),
+      getRelatedChanges.then(response => {
+        if (!response) {
+          throw new Error('getRelatedChanges returned undefined response');
+        }
+        this.relatedChanges = response?.changes ?? [];
+      }),
       this.restApiService
         .getChangesSubmittedTogether(change._number)
         .then(response => {
-          this._submittedTogether = response;
-          this._fireReloadEvent();
+          this.submittedTogether = response;
         }),
       this.restApiService
         .getChangeCherryPicks(change.project, change.change_id, change._number)
         .then(response => {
-          this._cherryPicks = response || [];
-          this._fireReloadEvent();
+          this.cherryPickChanges = response || [];
         }),
     ];
 
     // Get conflicts if change is open and is mergeable.
+    // Mergeable is output of restApiServict.getMergeable from gr-change-view
     if (changeIsOpen(change) && this.mergeable) {
       promises.push(
         this.restApiService
           .getChangeConflicts(change._number)
           .then(response => {
-            // Because the server doesn't always return a response and the
-            // template expects an array, always return an array.
-            this._conflicts = response ? response : [];
-            this._fireReloadEvent();
+            this.conflictingChanges = response ?? [];
           })
       );
     }
-
-    promises.push(
-      this._getServerConfig().then(config => {
-        if (change.topic) {
-          if (!config) {
-            throw new Error('_getServerConfig returned undefined ');
-          }
-          if (!config.change.submit_whole_topic) {
+    if (change.topic) {
+      const changeTopic = change.topic;
+      promises.push(
+        this.restApiService.getConfig().then(config => {
+          if (config && !config.change.submit_whole_topic) {
             return this.restApiService
-              .getChangesWithSameTopic(change.topic, change._number)
+              .getChangesWithSameTopic(changeTopic, change._number)
               .then(response => {
-                this._sameTopic = response;
+                if (changeTopic === this.change?.topic) {
+                  this.sameTopicChanges = response ?? [];
+                }
               });
           }
-        }
-        this._sameTopic = [];
-        return Promise.resolve();
-      })
-    );
+          this.sameTopicChanges = [];
+          return Promise.resolve();
+        })
+      );
+    }
 
-    return Promise.all(promises).then(() => {
-      this.loading = false;
-    });
-  }
-
-  _fireReloadEvent() {
-    // The listener on the change computes height of the related changes
-    // section, so they have to be rendered first, and inside a dom-repeat,
-    // that requires a flush.
-    flush();
-    this.dispatchEvent(new CustomEvent('new-section-loaded'));
-  }
-
-  /**
-   * Determines whether or not the given change has a parent change. If there
-   * is a relation chain, and the change id is not the last item of the
-   * relation chain, there is a parent.
-   */
-  _calculateHasParent(
-    currentChangeId: ChangeId,
-    relatedChanges: RelatedChangeAndCommitInfo[]
-  ) {
-    return (
-      relatedChanges.length > 0 &&
-      relatedChanges[relatedChanges.length - 1].change_id !== currentChangeId
-    );
-  }
-
-  _getServerConfig() {
-    return this.restApiService.getConfig();
-  }
-
-  _computeChangeURL(
-    changeNum: NumericChangeId,
-    project: RepoName,
-    patchNum?: PatchSetNum
-  ) {
-    return GerritNav.getUrlForChangeById(changeNum, project, patchNum);
+    return Promise.all(promises);
   }
 
   /**
@@ -232,8 +562,8 @@
    * their numbers.
    */
   _changesEqual(
-    a: ChangeInfo | RelatedChangeAndCommitInfo,
-    b: ChangeInfo | RelatedChangeAndCommitInfo
+    a?: ChangeInfo | RelatedChangeAndCommitInfo,
+    b?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
   ) {
     const aNum = this._getChangeNumber(a);
     const bNum = this._getChangeNumber(b);
@@ -246,7 +576,9 @@
    * RelatedChangeAndCommitInfo (such as those included in a
    * RelatedChangesInfo response).
    */
-  _getChangeNumber(change?: ChangeInfo | RelatedChangeAndCommitInfo) {
+  _getChangeNumber(
+    change?: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
+  ) {
     // Default to 0 if change property is not defined.
     if (!change) return 0;
 
@@ -256,136 +588,10 @@
     return change._change_number;
   }
 
-  _computeLinkClass(change: ParsedChangeInfo) {
-    const statuses = [];
-    if (change.status === ChangeStatus.ABANDONED) {
-      statuses.push('strikethrough');
-    }
-    if (change.submittable) {
-      statuses.push('submittable');
-    }
-    return statuses.join(' ');
-  }
-
-  _computeChangeStatusClass(change: RelatedChangeAndCommitInfo) {
-    const classes = ['status'];
-    if (change._revision_number !== change._current_revision_number) {
-      classes.push('notCurrent');
-    } else if (this._isIndirectAncestor(change)) {
-      classes.push('indirectAncestor');
-    } else if (change.submittable) {
-      classes.push('submittable');
-    } else if (change.status === ChangeStatus.NEW) {
-      classes.push('hidden');
-    }
-    return classes.join(' ');
-  }
-
-  _computeChangeStatus(change: RelatedChangeAndCommitInfo) {
-    switch (change.status) {
-      case ChangeStatus.MERGED:
-        return 'Merged';
-      case ChangeStatus.ABANDONED:
-        return 'Abandoned';
-    }
-    if (change._revision_number !== change._current_revision_number) {
-      return 'Not current';
-    } else if (this._isIndirectAncestor(change)) {
-      return 'Indirect ancestor';
-    } else if (change.submittable) {
-      return 'Submittable';
-    }
-    return '';
-  }
-
-  /** @override */
-  connectedCallback() {
-    super.connectedCallback();
-    // We listen to `new-section-loaded` events to allow plugins to trigger
-    // visibility computations, if their content or visibility changed.
-    this.addEventListener('new-section-loaded', () =>
-      this._handleNewSectionLoaded()
-    );
-  }
-
-  _handleNewSectionLoaded() {
-    // A plugin sent a `new-section-loaded` event, so its visibility likely
-    // changed. Hence, we update our visibility if needed.
-    this._resultsChanged(
-      this._relatedResponse,
-      this._submittedTogether,
-      this._conflicts,
-      this._cherryPicks,
-      this._sameTopic
-    );
-  }
-
-  @observe(
-    '_relatedResponse',
-    '_submittedTogether',
-    '_conflicts',
-    '_cherryPicks',
-    '_sameTopic'
-  )
-  _resultsChanged(
-    related: RelatedChangesInfo,
-    submittedTogether: SubmittedTogetherInfo | undefined,
-    conflicts: ChangeInfo[],
-    cherryPicks: ChangeInfo[],
-    sameTopic?: ChangeInfo[]
-  ) {
-    if (!submittedTogether || !sameTopic) {
-      return;
-    }
-    const submittedTogetherChangesCount =
-      (submittedTogether.changes || []).length +
-      (submittedTogether.non_visible_changes || 0);
-    const results = [
-      related && related.changes,
-      // If there are either visible or non-visible changes, we need a
-      // non-empty list to fire the event and set visibility.
-      submittedTogetherChangesCount ? [{}] : [],
-      conflicts,
-      cherryPicks,
-      sameTopic,
-    ];
-    for (let i = 0; i < results.length; i++) {
-      if (results[i] && results[i].length > 0) {
-        this.hidden = false;
-        this.dispatchEvent(
-          new CustomEvent('update', {
-            composed: true,
-            bubbles: false,
-          })
-        );
-        return;
-      }
-    }
-
-    this._computeHidden();
-  }
-
-  _computeHidden() {
-    // None of the built-in change lists had elements. So all of them are
-    // hidden. But since plugins might have injected visible content, we need
-    // to check for that and stay visible if we find any such visible content.
-    // (We consider plugins visible except if it's main element has the hidden
-    // attribute set to true.)
-    const plugins = getPluginEndpoints().getDetails('related-changes-section');
-    this.hidden = !plugins.some(
-      plugin =>
-        !plugin.domHook ||
-        plugin.domHook.getAllAttached().some(instance => !instance.hidden)
-    );
-  }
-
-  _isIndirectAncestor(change: RelatedChangeAndCommitInfo) {
-    return (
-      this._connectedRevisions &&
-      !this._connectedRevisions.includes(change.commit.commit)
-    );
-  }
-
+  /*
+   * A list of commit ids connected to change to understand if other change
+   * is direct or indirect ancestor / descendant.
+   */
   _computeConnectedRevisions(
     change?: ParsedChangeInfo,
     patchNum?: PatchSetNum,
@@ -421,40 +627,121 @@
     }
     return connected;
   }
+}
 
-  _computeSubmittedTogetherClass(submittedTogether?: SubmittedTogetherInfo) {
-    if (
-      !submittedTogether ||
-      (submittedTogether.changes.length === 0 &&
-        !submittedTogether.non_visible_changes)
-    ) {
-      return 'hidden';
+@customElement('gr-related-collapse')
+export class GrRelatedCollapse extends GrLitElement {
+  @property()
+  title = '';
+
+  @property()
+  showAll = false;
+
+  @property()
+  length = 0;
+
+  @property()
+  numChangesWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
+
+  private readonly reporting = appContext.reportingService;
+
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        .title {
+          font-weight: var(--font-weight-bold);
+          color: var(--deemphasized-text-color);
+          padding-left: var(--metadata-horizontal-padding);
+        }
+        h4 {
+          display: flex;
+          align-self: flex-end;
+        }
+        gr-button {
+          display: flex;
+        }
+        /* This is a hacky solution from old gr-related-change-list
+         * TODO(milutin): find layout without needing it
+         */
+        h4:before,
+        gr-button:before,
+        ::slotted(gr-related-change):before {
+          content: ' ';
+          flex-shrink: 0;
+          width: 1.2em;
+        }
+        .collapsed ::slotted(gr-related-change.show-when-collapsed) {
+          visibility: visible;
+          height: auto;
+        }
+        .collapsed ::slotted(.marker) {
+          display: block;
+        }
+        .show-all ::slotted(.marker) {
+          display: none;
+        }
+        /* keep width, so width of section and position of show all button
+         * are set according to width of all (even hidden) elements
+         */
+        .collapsed ::slotted(gr-related-change) {
+          visibility: hidden;
+          height: 0px;
+        }
+        ::slotted(gr-related-change) {
+          visibility: visible;
+          height: auto;
+        }
+        gr-button iron-icon {
+          color: inherit;
+          --iron-icon-height: 18px;
+          --iron-icon-width: 18px;
+        }
+        .container {
+          justify-content: space-between;
+          display: flex;
+          margin-bottom: var(--spacing-s);
+        }
+        :host(.first) .container {
+          margin-bottom: var(--spacing-m);
+        }
+      `,
+    ];
+  }
+
+  render() {
+    const title = html`<h4 class="title">${this.title}</h4>`;
+
+    const collapsible = this.length > this.numChangesWhenCollapsed;
+    const items = html` <div
+      class="${!this.showAll && collapsible ? 'collapsed' : 'show-all'}"
+    >
+      <slot></slot>
+    </div>`;
+
+    let button: TemplateResult | typeof nothing = nothing;
+    if (collapsible) {
+      let buttonText = 'Show less';
+      let buttonIcon = 'expand-less';
+      if (!this.showAll) {
+        buttonText = `Show all (${this.length})`;
+        buttonIcon = 'expand-more';
+      }
+      button = html`<gr-button link="" @click="${this.toggle}"
+        >${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
+      ></gr-button>`;
     }
-    return '';
+
+    return html`<div class="container">${title}${button}</div>
+      ${items}`;
   }
 
-  _computeNonVisibleChangesNote(n: number) {
-    return `(+ ${pluralize(n, 'non-visible change')})`;
-  }
-
-  // TODO(milutin): Temporary for data collection, remove when data collected
-  _reportClick(e: Event) {
-    const target = e.target as HTMLAnchorElement | undefined;
-    const section = target?.parentElement?.parentElement;
-    const sectionName = section?.getElementsByTagName('h4')[0]?.innerText;
-    const sectionLinks = [...(section?.getElementsByTagName('a') ?? [])];
-    const currentChange = section
-      ?.getElementsByClassName('arrowToCurrentChange')[0]
-      ?.nextElementSibling?.nextElementSibling?.getElementsByTagName('a')[0];
-
-    if (!target) return;
-    this.reportingService.reportInteraction('related-change-click', {
-      sectionName,
-      index: sectionLinks.indexOf(target) + 1,
-      countChanges: sectionLinks.length,
-      currentChangeIndex: !currentChange
-        ? undefined
-        : sectionLinks.indexOf(currentChange) + 1,
+  private toggle(e: MouseEvent) {
+    e.stopPropagation();
+    this.showAll = !this.showAll;
+    this.reporting.reportInteraction('toggle show all button', {
+      sectionName: this.title,
+      toState: this.showAll ? 'Show all' : 'Show less',
     });
   }
 }
@@ -462,5 +749,6 @@
 declare global {
   interface HTMLElementTagNameMap {
     'gr-related-changes-list': GrRelatedChangesList;
+    'gr-related-collapse': GrRelatedCollapse;
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
deleted file mode 100644
index 2f53319..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
+++ /dev/null
@@ -1,222 +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;
-    }
-    section {
-      margin-bottom: 1.4em; /* Same as line height for collapse purposes */
-    }
-    a {
-      display: block;
-    }
-    .changeContainer,
-    a {
-      max-width: 100%;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-    .changeContainer {
-      display: flex;
-    }
-    .arrowToCurrentChange {
-      position: absolute;
-    }
-    h4,
-    section div {
-      display: flex;
-    }
-    h4:before,
-    section div:before {
-      content: ' ';
-      flex-shrink: 0;
-      width: 1.2em;
-    }
-    .note {
-      color: var(--error-text-color);
-    }
-    .relatedChanges a {
-      display: inline-block;
-    }
-    .strikethrough {
-      color: var(--deemphasized-text-color);
-      text-decoration: line-through;
-    }
-    .status {
-      color: var(--deemphasized-text-color);
-      font-weight: var(--font-weight-bold);
-      margin-left: var(--spacing-xs);
-    }
-    .notCurrent {
-      color: var(--warning-foreground);
-    }
-    .indirectAncestor {
-      color: var(--indirect-ancestor-text-color);
-    }
-    .submittableCheck {
-      padding-left: var(--spacing-s);
-      color: var(--positive-green-text-color);
-      display: none;
-    }
-    .submittableCheck.submittable {
-      display: inline;
-    }
-    .hidden,
-    .mobile {
-      display: none;
-    }
-    @media screen and (max-width: 60em) {
-      .mobile {
-        display: block;
-      }
-    }
-  </style>
-  <div>
-    <gr-endpoint-decorator name="related-changes-section">
-      <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
-      <gr-endpoint-slot name="top"></gr-endpoint-slot>
-      <section
-        class="relatedChanges"
-        hidden$="[[!_relatedResponse.changes.length]]"
-        hidden=""
-      >
-        <h4>Relation chain</h4>
-        <template
-          is="dom-repeat"
-          items="[[_relatedResponse.changes]]"
-          as="related"
-        >
-          <template is="dom-if" if="[[_changesEqual(related, change)]]">
-            <span
-              role="img"
-              class="arrowToCurrentChange"
-              aria-label="Arrow marking current change"
-              >âž”</span
-            >
-          </template>
-          <div class="rightIndent changeContainer">
-            <a
-              href$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]"
-              class$="[[_computeLinkClass(related)]]"
-              title$="[[related.commit.subject]]"
-              on-click="_reportClick"
-            >
-              [[related.commit.subject]]
-            </a>
-            <span class$="[[_computeChangeStatusClass(related)]]">
-              ([[_computeChangeStatus(related)]])
-            </span>
-          </div>
-        </template>
-      </section>
-      <section
-        id="submittedTogether"
-        class$="[[_computeSubmittedTogetherClass(_submittedTogether)]]"
-      >
-        <h4>Submitted together</h4>
-        <template
-          is="dom-repeat"
-          items="[[_submittedTogether.changes]]"
-          as="related"
-        >
-          <template is="dom-if" if="[[_changesEqual(related, change)]]">
-            <span
-              role="img"
-              class="arrowToCurrentChange"
-              aria-label="Arrow marking current change"
-              >âž”</span
-            >
-          </template>
-          <div class="changeContainer">
-            <a
-              href$="[[_computeChangeURL(related._number, related.project)]]"
-              class$="[[_computeLinkClass(related)]]"
-              title$="[[related.project]]: [[related.branch]]: [[related.subject]]"
-              on-click="_reportClick"
-            >
-              [[related.project]]: [[related.branch]]: [[related.subject]]
-            </a>
-            <span
-              tabindex="-1"
-              title="Submittable"
-              class$="submittableCheck [[_computeLinkClass(related)]]"
-              role="img"
-              aria-label="Submittable"
-              >✓</span
-            >
-          </div>
-        </template>
-        <template is="dom-if" if="[[_submittedTogether.non_visible_changes]]">
-          <div class="note">
-            [[_computeNonVisibleChangesNote(_submittedTogether.non_visible_changes)]]
-          </div>
-        </template>
-      </section>
-      <section hidden$="[[!_sameTopic.length]]" hidden="">
-        <h4>Same topic</h4>
-        <template is="dom-repeat" items="[[_sameTopic]]" as="change">
-          <div>
-            <a
-              href$="[[_computeChangeURL(change._number, change.project)]]"
-              class$="[[_computeLinkClass(change)]]"
-              title$="[[change.project]]: [[change.branch]]: [[change.subject]]"
-              on-click="_reportClick"
-            >
-              [[change.project]]: [[change.branch]]: [[change.subject]]
-            </a>
-          </div>
-        </template>
-      </section>
-      <section hidden$="[[!_conflicts.length]]" hidden="">
-        <h4>Merge conflicts</h4>
-        <template is="dom-repeat" items="[[_conflicts]]" as="change">
-          <div>
-            <a
-              href$="[[_computeChangeURL(change._number, change.project)]]"
-              class$="[[_computeLinkClass(change)]]"
-              title$="[[change.subject]]"
-              on-click="_reportClick"
-            >
-              [[change.subject]]
-            </a>
-          </div>
-        </template>
-      </section>
-      <section hidden$="[[!_cherryPicks.length]]" hidden="">
-        <h4>Cherry picks</h4>
-        <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
-          <div>
-            <a
-              href$="[[_computeChangeURL(change._number, change.project)]]"
-              class$="[[_computeLinkClass(change)]]"
-              title$="[[change.branch]]: [[change.subject]]"
-              on-click="_reportClick"
-            >
-              [[change.branch]]: [[change.subject]]
-            </a>
-          </div>
-        </template>
-      </section>
-      <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
-    </gr-endpoint-decorator>
-  </div>
-  <div hidden$="[[!loading]]">Loading...</div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index 631c077..ae9af4a 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -15,70 +15,314 @@
  * limitations under the License.
  */
 
+import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {PluginApi} from '../../../api/plugin';
 import {ChangeStatus} from '../../../constants/constants';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import '../../../test/common-test-setup-karma';
 import {
   createChange,
-  createCommit,
   createCommitInfoWithRequiredCommit,
   createParsedChange,
+  createRelatedChangeAndCommitInfo,
+  createRelatedChangesInfo,
+  createRevision,
+  createSubmittedTogetherInfo,
 } from '../../../test/test-data-generators';
 import {
+  queryAndAssert,
+  resetPlugins,
+  stubRestApi,
+} from '../../../test/test-utils';
+import {
   ChangeId,
   ChangeInfo,
   CommitId,
   NumericChangeId,
   PatchSetNum,
   RelatedChangeAndCommitInfo,
-  RepoName,
+  RelatedChangesInfo,
+  SubmittedTogetherInfo,
 } from '../../../types/common';
-import './gr-related-changes-list';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {ParsedChangeInfo} from '../../../types/types';
+import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
-import {query, queryAndAssert} from '../../../test/test-utils';
-import {GrRelatedChangesList} from './gr-related-changes-list';
-import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import './gr-related-changes-list';
+import {
+  ChangeMarkersInList,
+  GrRelatedChangesList,
+  GrRelatedCollapse,
+  Section,
+} from './gr-related-changes-list';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
 const basicFixture = fixtureFromElement('gr-related-changes-list');
 
-suite('gr-related-changes-list tests', () => {
+suite('gr-related-changes-list', () => {
   let element: GrRelatedChangesList;
 
   setup(() => {
-    // Since pluginEndpoints are global, must reset state.
-    _testOnly_resetEndpoints();
     element = basicFixture.instantiate();
   });
 
-  // obsolete
-  test('event for section loaded fires for each section ', () => {
-    const loadedStub = sinon.stub();
-    element.patchNum = 7 as PatchSetNum;
-    element.change = {
-      ...createParsedChange(),
-      change_id: '123' as ChangeId,
-      status: ChangeStatus.NEW,
-    };
-    element.mergeable = true;
-    element.addEventListener('new-section-loaded', loadedStub);
+  suite('show when collapsed', () => {
+    function genBoolArray(
+      instructions: Array<{
+        len: number;
+        v: boolean;
+      }>
+    ) {
+      return instructions
+        .map(inst => Array.from({length: inst.len}, () => inst.v))
+        .reduce((acc, val) => acc.concat(val), []);
+    }
 
-    return element.reload().then(() => {
-      assert.equal(loadedStub.callCount, 4);
+    function checkShowWhenCollapsed(
+      expected: boolean[],
+      markersPredicate: (index: number) => ChangeMarkersInList,
+      msg: string
+    ) {
+      for (let i = 0; i < expected.length; i++) {
+        assert.equal(
+          markersPredicate(i).showWhenCollapsed,
+          expected[i],
+          `change on pos (${i}) ${msg}`
+        );
+      }
+    }
+
+    test('size 5', () => {
+      const markersPredicate = element.markersPredicateFactory(10, 4, 5);
+      const expectedCollapsing = genBoolArray([
+        {len: 2, v: false},
+        {len: 5, v: true},
+        {len: 3, v: false},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing,
+        markersPredicate,
+        'highlight 4, size 10, size 5'
+      );
+
+      const markersPredicate2 = element.markersPredicateFactory(10, 8, 5);
+      const expectedCollapsing2 = genBoolArray([
+        {len: 5, v: false},
+        {len: 5, v: true},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing2,
+        markersPredicate2,
+        'highlight 8, size 10, size 5'
+      );
+
+      const markersPredicate3 = element.markersPredicateFactory(10, 1, 5);
+      const expectedCollapsing3 = genBoolArray([
+        {len: 5, v: true},
+        {len: 5, v: false},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing3,
+        markersPredicate3,
+        'highlight 1, size 10, size 5'
+      );
+    });
+
+    test('size 4', () => {
+      const markersPredicate = element.markersPredicateFactory(10, 4, 4);
+      const expectedCollapsing = genBoolArray([
+        {len: 2, v: false},
+        {len: 4, v: true},
+        {len: 4, v: false},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing,
+        markersPredicate,
+        'highlight 4, len 10, size 4'
+      );
+
+      const markersPredicate2 = element.markersPredicateFactory(10, 8, 4);
+      const expectedCollapsing2 = genBoolArray([
+        {len: 6, v: false},
+        {len: 4, v: true},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing2,
+        markersPredicate2,
+        'highlight 8, len 10, size 4'
+      );
+
+      const markersPredicate3 = element.markersPredicateFactory(10, 1, 4);
+      const expectedCollapsing3 = genBoolArray([
+        {len: 4, v: true},
+        {len: 6, v: false},
+      ]);
+      checkShowWhenCollapsed(
+        expectedCollapsing3,
+        markersPredicate3,
+        'highlight 1, len 10, size 4'
+      );
     });
   });
 
-  // trivial
-  suite('getChangeConflicts resolves undefined', () => {
+  suite('section size', () => {
+    test('1 section', () => {
+      const sectionSize = element.sectionSizeFactory(20, 0, 0, 0, 0);
+      assert.equal(sectionSize(Section.RELATED_CHANGES), 15);
+      const sectionSize2 = element.sectionSizeFactory(0, 0, 10, 0, 0);
+      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
+    });
+    test('2 sections', () => {
+      const sectionSize = element.sectionSizeFactory(20, 20, 0, 0, 0);
+      assert.equal(sectionSize(Section.RELATED_CHANGES), 11);
+      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
+      const sectionSize2 = element.sectionSizeFactory(4, 0, 10, 0, 0);
+      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
+      assert.equal(sectionSize2(Section.SAME_TOPIC), 10);
+    });
+    test('many sections', () => {
+      const sectionSize = element.sectionSizeFactory(20, 20, 3, 3, 3);
+      assert.equal(sectionSize(Section.RELATED_CHANGES), 3);
+      assert.equal(sectionSize(Section.SUBMITTED_TOGETHER), 3);
+      const sectionSize2 = element.sectionSizeFactory(4, 1, 10, 1, 1);
+      assert.equal(sectionSize2(Section.RELATED_CHANGES), 4);
+      assert.equal(sectionSize2(Section.SAME_TOPIC), 4);
+    });
+  });
+
+  suite('test first non-empty list', () => {
+    const relatedChangeInfo: RelatedChangesInfo = {
+      ...createRelatedChangesInfo(),
+      changes: [createRelatedChangeAndCommitInfo()],
+    };
+    const submittedTogether: SubmittedTogetherInfo = {
+      ...createSubmittedTogetherInfo(),
+      changes: [createChange()],
+    };
+
+    setup(() => {
+      element.change = createParsedChange();
+      element.patchNum = 1 as PatchSetNum;
+    });
+
+    test('first list', async () => {
+      stubRestApi('getRelatedChanges').returns(
+        Promise.resolve(relatedChangeInfo)
+      );
+      await element.reload();
+      const section = queryAndAssert<HTMLElement>(element, '#relatedChanges');
+      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
+        section,
+        'gr-related-collapse'
+      );
+      assert.isTrue(relatedChanges!.classList.contains('first'));
+    });
+
+    test('first empty second non-empty', async () => {
+      stubRestApi('getRelatedChanges').returns(
+        Promise.resolve(createRelatedChangesInfo())
+      );
+      stubRestApi('getChangesSubmittedTogether').returns(
+        Promise.resolve(submittedTogether)
+      );
+      await element.reload();
+      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
+        queryAndAssert<HTMLElement>(element, '#relatedChanges'),
+        'gr-related-collapse'
+      );
+      assert.isFalse(relatedChanges!.classList.contains('first'));
+      const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
+        queryAndAssert<HTMLElement>(element, '#submittedTogether'),
+        'gr-related-collapse'
+      );
+      assert.isTrue(submittedTogetherSection!.classList.contains('first'));
+    });
+
+    test('first non-empty second empty third non-empty', async () => {
+      stubRestApi('getRelatedChanges').returns(
+        Promise.resolve(relatedChangeInfo)
+      );
+      stubRestApi('getChangesSubmittedTogether').returns(
+        Promise.resolve(createSubmittedTogetherInfo())
+      );
+      stubRestApi('getChangeCherryPicks').returns(
+        Promise.resolve([createChange()])
+      );
+      await element.reload();
+      const relatedChanges = queryAndAssert<GrRelatedCollapse>(
+        queryAndAssert<HTMLElement>(element, '#relatedChanges'),
+        'gr-related-collapse'
+      );
+      assert.isTrue(relatedChanges!.classList.contains('first'));
+      const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
+        queryAndAssert<HTMLElement>(element, '#submittedTogether'),
+        'gr-related-collapse'
+      );
+      assert.isFalse(submittedTogetherSection!.classList.contains('first'));
+      const cherryPicks = queryAndAssert<GrRelatedCollapse>(
+        queryAndAssert<HTMLElement>(element, '#cherryPicks'),
+        'gr-related-collapse'
+      );
+      assert.isFalse(cherryPicks!.classList.contains('first'));
+    });
+  });
+
+  test('_changesEqual', () => {
+    const change1: ChangeInfo = {
+      ...createChange(),
+      change_id: '123' as ChangeId,
+      _number: 0 as NumericChangeId,
+    };
+    const change2: ChangeInfo = {
+      ...createChange(),
+      change_id: '456' as ChangeId,
+      _number: 1 as NumericChangeId,
+    };
+    const change3: ChangeInfo = {
+      ...createChange(),
+      change_id: '123' as ChangeId,
+      _number: 2 as NumericChangeId,
+    };
+    const change4: RelatedChangeAndCommitInfo = {
+      ...createRelatedChangeAndCommitInfo(),
+      change_id: '123' as ChangeId,
+      _change_number: 1 as NumericChangeId,
+    };
+
+    assert.isTrue(element._changesEqual(change1, change1));
+    assert.isFalse(element._changesEqual(change1, change2));
+    assert.isFalse(element._changesEqual(change1, change3));
+    assert.isTrue(element._changesEqual(change2, change4));
+  });
+
+  test('_getChangeNumber', () => {
+    const change1: ChangeInfo = {
+      ...createChange(),
+      change_id: '123' as ChangeId,
+      _number: 0 as NumericChangeId,
+    };
+    const change2: ChangeInfo = {
+      ...createChange(),
+      change_id: '456' as ChangeId,
+      _number: 1 as NumericChangeId,
+    };
+    assert.equal(element._getChangeNumber(change1), 0);
+    assert.equal(element._getChangeNumber(change2), 1);
+  });
+
+  suite('get conflicts tests', () => {
     let element: GrRelatedChangesList;
+    let conflictsStub: SinonStubbedMember<RestApiService['getChangeConflicts']>;
 
     setup(() => {
       element = basicFixture.instantiate();
+      conflictsStub = stubRestApi('getChangeConflicts').returns(
+        Promise.resolve(undefined)
+      );
     });
 
-    test('_conflicts are an empty array', () => {
+    test('request conflicts if open and mergeable', () => {
       element.patchNum = 7 as PatchSetNum;
       element.change = {
         ...createParsedChange(),
@@ -87,383 +331,300 @@
       };
       element.mergeable = true;
       element.reload();
-      assert.equal(element._conflicts.length, 0);
+      assert.isTrue(conflictsStub.called);
+    });
+
+    test('does not request conflicts if closed and mergeable', () => {
+      element.patchNum = 7 as PatchSetNum;
+      element.change = {
+        ...createParsedChange(),
+        change_id: '123' as ChangeId,
+        status: ChangeStatus.NEW,
+      };
+      element.reload();
+      assert.isFalse(conflictsStub.called);
+    });
+
+    test('does not request conflicts if open and not mergeable', () => {
+      element.patchNum = 7 as PatchSetNum;
+      element.change = {
+        ...createParsedChange(),
+        change_id: '123' as ChangeId,
+        status: ChangeStatus.NEW,
+      };
+      element.mergeable = false;
+      element.reload();
+      assert.isFalse(conflictsStub.called);
+    });
+
+    test('doesnt request conflicts if closed and not mergeable', () => {
+      element.patchNum = 7 as PatchSetNum;
+      element.change = {
+        ...createParsedChange(),
+        change_id: '123' as ChangeId,
+        status: ChangeStatus.NEW,
+      };
+      element.mergeable = false;
+      element.reload();
+      assert.isFalse(conflictsStub.called);
     });
   });
 
-  suite('hidden attribute and update event', () => {
-    const changes: ChangeInfo[] = [
-      {
-        ...createChange(),
-        project: 'foo/bar' as RepoName,
-        change_id: 'Ideadbeef' as ChangeId,
-        status: ChangeStatus.NEW,
+  test('connected revisions', () => {
+    const change: ParsedChangeInfo = {
+      ...createParsedChange(),
+      revisions: {
+        e3c6d60783bfdec9ebae7dcfec4662360433449e: createRevision(1),
+        '26e5e4c9c7ae31cbd876271cca281ce22b413997': createRevision(2),
+        bf7884d695296ca0c91702ba3e2bc8df0f69a907: createRevision(7),
+        b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3: createRevision(5),
+        d6bcee67570859ccb684873a85cf50b1f0e96fda: createRevision(6),
+        cc960918a7f90388f4a9e05753d0f7b90ad44546: createRevision(3),
+        '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': createRevision(4),
       },
-    ];
-    const relatedChanges: RelatedChangeAndCommitInfo[] = [
+    };
+    let patchNum = 7 as PatchSetNum;
+    let relatedChanges: RelatedChangeAndCommitInfo[] = [
       {
-        ...createCommitInfoWithRequiredCommit(),
-        project: 'foo/bar' as RepoName,
-        change_id: 'Ideadbeef' as ChangeId,
+        ...createRelatedChangeAndCommitInfo(),
         commit: {
-          ...createCommit(),
-          commit: 'deadbeef' as CommitId,
+          ...createCommitInfoWithRequiredCommit(
+            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
+          ),
           parents: [
             {
-              commit: 'abc123' as CommitId,
-              subject: 'abc123',
+              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
+              subject: 'subject1',
             },
           ],
-          subject: 'do that thing',
         },
-        _change_number: 12345 as NumericChangeId,
-        _revision_number: 1,
-        _current_revision_number: 1,
-        status: ChangeStatus.NEW,
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
+          ),
+          parents: [
+            {
+              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
+              subject: 'subject2',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
+          ),
+          parents: [
+            {
+              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
+              subject: 'subject3',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            'b0ccb183494a8e340b8725a2dc553967d61e6dae'
+          ),
+          parents: [
+            {
+              commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' as CommitId,
+              subject: 'subject4',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
+          ),
+          parents: [
+            {
+              commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' as CommitId,
+              subject: 'subject5',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '613bc4f81741a559c6667ac08d71dcc3348f73ce'
+          ),
+          parents: [
+            {
+              commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' as CommitId,
+              subject: 'subject6',
+            },
+          ],
+        },
       },
     ];
 
-    // obsolete
-    test('clear and empties', () => {
-      element._relatedResponse = {changes: relatedChanges};
-      element._submittedTogether = {
-        changes,
-        non_visible_changes: 0,
-      };
-      element._conflicts = changes;
-      element._cherryPicks = changes;
-      element._sameTopic = changes;
-
-      element.hidden = false;
-      element.clear();
-      assert.isTrue(element.hidden);
-      assert.equal(element._relatedResponse.changes.length, 0);
-      assert.equal(element._submittedTogether?.changes.length, 0);
-      assert.equal(element._conflicts.length, 0);
-      assert.equal(element._cherryPicks.length, 0);
-      assert.equal(element._sameTopic?.length, 0);
-    });
-
-    // obsolete
-    test('update fires', () => {
-      const updateHandler = sinon.stub();
-      element.addEventListener('update', updateHandler);
-
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isTrue(element.hidden);
-      assert.isFalse(updateHandler.called);
-
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        changes
-      );
-      assert.isFalse(element.hidden);
-      assert.isTrue(updateHandler.called);
-      updateHandler.reset();
-
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isTrue(element.hidden);
-      assert.isFalse(updateHandler.called);
-
-      element._resultsChanged(
-        {changes: []},
-        {changes, non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isFalse(element.hidden);
-      assert.isTrue(updateHandler.called);
-      updateHandler.reset();
-
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 1},
-        [],
-        [],
-        []
-      );
-      assert.isFalse(element.hidden);
-      assert.isTrue(updateHandler.called);
-    });
-
-    suite('hiding and unhiding', () => {
-      test('related response', () => {
-        assert.isTrue(element.hidden);
-        element._resultsChanged(
-          {changes: relatedChanges},
-          {changes: [], non_visible_changes: 0},
-          [],
-          [],
-          []
-        );
-        assert.isFalse(element.hidden);
-      });
-
-      test('submitted together', () => {
-        assert.isTrue(element.hidden);
-        element._resultsChanged(
-          {changes: []},
-          {changes, non_visible_changes: 0},
-          [],
-          [],
-          []
-        );
-        assert.isFalse(element.hidden);
-      });
-
-      test('conflicts', () => {
-        assert.isTrue(element.hidden);
-        element._resultsChanged(
-          {changes: []},
-          {changes: [], non_visible_changes: 0},
-          changes,
-          [],
-          []
-        );
-        assert.isFalse(element.hidden);
-      });
-
-      test('cherrypicks', () => {
-        assert.isTrue(element.hidden);
-        element._resultsChanged(
-          {changes: []},
-          {changes: [], non_visible_changes: 0},
-          [],
-          changes,
-          []
-        );
-        assert.isFalse(element.hidden);
-      });
-
-      test('same topic', () => {
-        assert.isTrue(element.hidden);
-        element._resultsChanged(
-          {changes: []},
-          {changes: [], non_visible_changes: 0},
-          [],
-          [],
-          changes
-        );
-        assert.isFalse(element.hidden);
-      });
-    });
-  });
-
-  // trivial
-  test('_computeChangeURL uses GerritNav', () => {
-    const getUrlStub = sinon.stub(GerritNav, 'getUrlForChangeById');
-    element._computeChangeURL(
-      123 as NumericChangeId,
-      'abc/def' as RepoName,
-      12 as PatchSetNum
+    let connectedChanges = element._computeConnectedRevisions(
+      change,
+      patchNum,
+      relatedChanges
     );
-    assert.isTrue(getUrlStub.called);
-  });
+    assert.deepEqual(connectedChanges, [
+      '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+      'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+      'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+      '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+      '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+      '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+    ]);
 
-  // trivial
-  suite('submitted together changes', () => {
-    const change: ChangeInfo = {
-      ...createChange(),
-      project: 'foo/bar' as RepoName,
-      change_id: 'Ideadbeef' as ChangeId,
-      status: ChangeStatus.NEW,
-    };
-
-    test('_computeSubmittedTogetherClass', () => {
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass(undefined),
-        'hidden'
-      );
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass({
-          changes: [],
-          non_visible_changes: 0,
-        }),
-        'hidden'
-      );
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass({
-          changes: [change],
-          non_visible_changes: 0,
-        }),
-        ''
-      );
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass({
-          changes: [],
-          non_visible_changes: 0,
-        }),
-        'hidden'
-      );
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass({
-          changes: [],
-          non_visible_changes: 1,
-        }),
-        ''
-      );
-      assert.strictEqual(
-        element._computeSubmittedTogetherClass({
-          changes: [],
-          non_visible_changes: 1,
-        }),
-        ''
-      );
-    });
-
-    test('no submitted together changes', () => {
-      flush();
-      assert.include(element.$.submittedTogether.className, 'hidden');
-    });
-
-    test('no non-visible submitted together changes', () => {
-      element._submittedTogether = {changes: [change], non_visible_changes: 0};
-      flush();
-      assert.notInclude(element.$.submittedTogether.className, 'hidden');
-      assert.isUndefined(query(element, '.note'));
-    });
-
-    test('no visible submitted together changes', () => {
-      // Technically this should never happen, but worth asserting the logic.
-      element._submittedTogether = {changes: [], non_visible_changes: 1};
-      flush();
-      assert.notInclude(element.$.submittedTogether.className, 'hidden');
-      assert.strictEqual(
-        queryAndAssert<HTMLDivElement>(element, '.note').innerText.trim(),
-        '(+ 1 non-visible change)'
-      );
-    });
-
-    test('visible and non-visible submitted together changes', () => {
-      element._submittedTogether = {changes: [change], non_visible_changes: 2};
-      flush();
-      assert.notInclude(element.$.submittedTogether.className, 'hidden');
-      assert.strictEqual(
-        queryAndAssert<HTMLDivElement>(element, '.note').innerText.trim(),
-        '(+ 2 non-visible changes)'
-      );
-    });
-  });
-
-  // obsolete
-  test('hiding and unhiding', done => {
-    element.change = {...createParsedChange(), labels: {}};
-    let hookEl: HTMLElement;
-    let plugin;
-
-    // No changes, and no plugin. The element is still hidden.
-    element._resultsChanged(
-      {changes: []},
-      {changes: [], non_visible_changes: 0},
-      [],
-      [],
-      []
-    );
-    assert.isTrue(element.hidden);
-    pluginApi.install(
-      p => {
-        plugin = p;
-        plugin
-          .hook('related-changes-section')
-          .getLastAttached()
-          .then(el => (hookEl = el));
+    patchNum = 4 as PatchSetNum;
+    relatedChanges = [
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '2cebeedfb1e80f4b872d0a13ade529e70652c0c8'
+          ),
+          parents: [
+            {
+              commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
       },
-      '0.1',
-      'http://some/plugins/url2.js'
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
+          ),
+          parents: [
+            {
+              commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
+          ),
+          parents: [
+            {
+              commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b'
+          ),
+          parents: [
+            {
+              commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
+          ),
+          parents: [
+            {
+              commit: 'af815dac54318826b7f1fa468acc76349ffc588e' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
+      },
+      {
+        ...createRelatedChangeAndCommitInfo(),
+        commit: {
+          ...createCommitInfoWithRequiredCommit(
+            'af815dac54318826b7f1fa468acc76349ffc588e'
+          ),
+          parents: [
+            {
+              commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' as CommitId,
+              subject: 'My parent commit',
+            },
+          ],
+        },
+      },
+    ];
+
+    connectedChanges = element._computeConnectedRevisions(
+      change,
+      patchNum,
+      relatedChanges
     );
-    getPluginLoader().loadPlugins([]);
-    flush(() => {
-      // No changes, and plugin without hidden attribute. So it's visible.
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isFalse(element.hidden);
+    assert.deepEqual(connectedChanges, [
+      'af815dac54318826b7f1fa468acc76349ffc588e',
+      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+      '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+      'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+    ]);
+  });
 
-      // No changes, but plugin with true hidden attribute. So it's invisible.
-      hookEl.hidden = true;
+  suite('gr-related-changes-list plugin tests', () => {
+    let element: GrRelatedChangesList;
 
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isTrue(element.hidden);
+    setup(() => {
+      resetPlugins();
+      element = basicFixture.instantiate();
+    });
 
-      // No changes, and plugin with false hidden attribute. So it's visible.
-      hookEl.hidden = false;
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        []
-      );
-      assert.isFalse(element.hidden);
+    teardown(() => {
+      resetPlugins();
+    });
 
-      // Hiding triggered by plugin itself
-      hookEl.hidden = true;
-      hookEl.dispatchEvent(
-        new CustomEvent('new-section-loaded', {
-          composed: true,
-          bubbles: true,
-        })
+    test('endpoint params', done => {
+      element.change = {...createParsedChange(), labels: {}};
+      interface RelatedChangesListGrEndpointDecorator
+        extends GrEndpointDecorator {
+        plugin: PluginApi;
+        change: ParsedChangeInfo;
+      }
+      let hookEl: RelatedChangesListGrEndpointDecorator;
+      let plugin: PluginApi;
+      pluginApi.install(
+        p => {
+          plugin = p;
+          plugin
+            .hook('related-changes-section')
+            .getLastAttached()
+            .then(el => (hookEl = el as RelatedChangesListGrEndpointDecorator));
+        },
+        '0.1',
+        'http://some/plugins/url1.js'
       );
-      assert.isTrue(element.hidden);
-
-      // Unhiding triggered by plugin itself
-      hookEl.hidden = false;
-      hookEl.dispatchEvent(
-        new CustomEvent('new-section-loaded', {
-          composed: true,
-          bubbles: true,
-        })
-      );
-      assert.isFalse(element.hidden);
-
-      // Hiding plugin keeps list visible, if there are changes
-      hookEl.hidden = false;
-      const change = createChange();
-      element._sameTopic = [change];
-      element._resultsChanged(
-        {changes: []},
-        {changes: [], non_visible_changes: 0},
-        [],
-        [],
-        [change]
-      );
-      assert.isFalse(element.hidden);
-      hookEl.hidden = true;
-      hookEl.dispatchEvent(
-        new CustomEvent('new-section-loaded', {
-          composed: true,
-          bubbles: true,
-        })
-      );
-      assert.isFalse(element.hidden);
-
-      done();
+      getPluginLoader().loadPlugins([]);
+      flush(() => {
+        assert.strictEqual(hookEl.plugin, plugin);
+        assert.strictEqual(hookEl.change, element.change);
+        done();
+      });
     });
   });
 });
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 2cabf90..bf61aa5 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
@@ -38,7 +38,6 @@
   ReviewerState,
   SpecialFilePath,
 } from '../../../constants/constants';
-import {fetchChangeUpdates} from '../../../utils/patch-set-util';
 import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {accountKey, removeServiceUsers} from '../../../utils/account-util';
 import {getDisplayName} from '../../../utils/display-name-util';
@@ -226,6 +225,8 @@
 
   flagsService = appContext.flagsService;
 
+  changeService = appContext.changeService;
+
   @property({type: Object})
   change?: ChangeInfo;
 
@@ -436,7 +437,7 @@
   open(focusTarget?: FocusTarget) {
     assertIsDefined(this.change, 'change');
     this.knownLatestState = LatestPatchState.CHECKING;
-    fetchChangeUpdates(this.change, this.restApiService).then(result => {
+    this.changeService.fetchChangeUpdates(this.change).then(result => {
       this.knownLatestState = result.isLatest
         ? LatestPatchState.LATEST
         : LatestPatchState.NOT_LATEST;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 86934c2..adf4f71 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -41,6 +41,7 @@
 import {isRemovableReviewer} from '../../../utils/change-util';
 import {ReviewerState} from '../../../constants/constants';
 import {appContext} from '../../../services/app-context';
+import {fireAlert} from '../../../utils/event-util';
 
 @customElement('gr-reviewer-list')
 export class GrReviewerList extends PolymerElement {
@@ -261,32 +262,37 @@
   _handleRemove(e: Event) {
     e.preventDefault();
     const target = (dom(e) as EventApi).rootTarget as GrAccountChip;
-    if (!target.account || !this.change) {
-      return;
-    }
+    if (!target.account || !this.change?.reviewers) return;
     const accountID = target.account._account_id || target.account.email;
-    this.disabled = true;
     if (!accountID) return;
-    this._xhrPromise = this._removeReviewer(accountID)
-      .then((response: Response | undefined) => {
-        this.disabled = false;
-        if (!response || !response.ok) {
-          return response;
+    const reviewers = this.change.reviewers;
+    let removedAccount: AccountInfo | undefined;
+    let removedType: ReviewerState | undefined;
+    for (const type of [ReviewerState.REVIEWER, ReviewerState.CC]) {
+      const reviewerStateByType = reviewers[type] || [];
+      reviewers[type] = reviewerStateByType;
+      for (let i = 0; i < reviewerStateByType.length; i++) {
+        if (
+          reviewerStateByType[i]._account_id === accountID ||
+          reviewerStateByType[i].email === accountID
+        ) {
+          removedAccount = reviewerStateByType[i];
+          removedType = type;
+          this.splice(`change.reviewers.${type}`, i, 1);
+          break;
         }
-        if (!this.change || !this.change.reviewers) return;
-        const reviewers = this.change.reviewers;
-        for (const type of [ReviewerState.REVIEWER, ReviewerState.CC]) {
-          const reviewerStateByType = reviewers[type] || [];
-          reviewers[type] = reviewerStateByType;
-          for (let i = 0; i < reviewerStateByType.length; i++) {
-            if (
-              reviewerStateByType[i]._account_id === accountID ||
-              reviewerStateByType[i].email === accountID
-            ) {
-              this.splice('change.reviewers.' + type, i, 1);
-              break;
-            }
-          }
+      }
+    }
+    const curChange = this.change;
+    this.disabled = true;
+    this._xhrPromise = this._removeReviewer(accountID)
+      .then(response => {
+        this.disabled = false;
+        if (!this.change?.reviewers || this.change !== curChange) return;
+        if (!response?.ok) {
+          this.push(`change.reviewers.${removedType}`, removedAccount);
+          fireAlert(this, `Cannot remove a ${removedType}`);
+          return response;
         }
         return;
       })
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 794e9c9..da91095 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
@@ -44,10 +44,6 @@
       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);
@@ -84,8 +80,9 @@
           type="radio"
           on-click="_handleOnlyDrafts"
           checked="[[_draftsOnly]]"
+          hidden$="[[!loggedIn]]"
         />
-        <label for="draftsRadio">
+        <label for="draftsRadio" hidden$="[[!loggedIn]]">
           Drafts ([[_countDrafts(threads)]])
         </label>
         <input
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 7b47bd7..1fe00ff 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
@@ -270,6 +270,17 @@
     });
   });
 
+  test('draft toggle only appears when logged in', () => {
+    element.loggedIn = false;
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('#draftsRadio')).display,
+    'none');
+    element.loggedIn = true;
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('#draftsRadio')).display,
+    'none');
+  });
+
   test('show all threads by default', () => {
     assert.equal(dom(element.root)
         .querySelectorAll('gr-comment-thread').length, element.threads.length);
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index feecd66..57bed1c 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -24,6 +24,7 @@
   property,
   PropertyValues,
   query,
+  TemplateResult,
 } from 'lit-element';
 import {GrLitElement} from '../lit/gr-lit-element';
 import '@polymer/paper-tooltip/paper-tooltip';
@@ -42,6 +43,7 @@
   someProvidersAreLoading$,
   RunResult,
   CheckRun,
+  allLinks$,
 } from '../../services/checks/checks-model';
 import {
   allResults,
@@ -253,14 +255,11 @@
       `;
     }
     return html`
-      <tr
-        class="${classMap({container: true, collapsed: !this.isExpanded})}"
-        @click="${this.toggleExpanded}"
-      >
-        <td class="iconCol">
+      <tr class="${classMap({container: true, collapsed: !this.isExpanded})}">
+        <td class="iconCol" @click="${this.toggleExpanded}">
           <div>${this.renderIcon()}</div>
         </td>
-        <td class="nameCol">
+        <td class="nameCol" @click="${this.toggleExpanded}">
           <div>
             <span>${this.result.checkName}</span>
             <span class="attempt" ?hidden="${this.result.isSingleAttempt}"
@@ -272,7 +271,7 @@
           <div class="summary-cell">
             ${(this.result.links?.slice(0, 1) ?? []).map(this.renderLink)}
             ${this.renderSummary(this.result.summary)}
-            <div class="message">
+            <div class="message" @click="${this.toggleExpanded}">
               ${this.isExpanded ? '' : this.result.message}
             </div>
             <div class="tags">
@@ -282,7 +281,7 @@
           </div>
           ${this.renderExpanded()}
         </td>
-        <td class="expanderCol">
+        <td class="expanderCol" @click="${this.toggleExpanded}">
           <div
             class="show-hide"
             role="switch"
@@ -309,7 +308,6 @@
     if (!this.isExpanded) return;
     return html`<gr-result-expanded
       .result="${this.result}"
-      @click="${this.avoidToggleExpanded}"
     ></gr-result-expanded>`;
   }
 
@@ -318,16 +316,12 @@
     this.isExpanded = !this.isExpanded;
   }
 
-  private avoidToggleExpanded(e: Event) {
-    e.stopPropagation();
-  }
-
   renderSummary(text?: string) {
     if (!text) return;
     return html`
       <!-- The &nbsp; is for being able to shrink a tiny amount without
        the text itself getting shrunk with an ellipsis. -->
-      <div class="summary">${text}&nbsp;</div>
+      <div class="summary" @click="${this.toggleExpanded}">${text}&nbsp;</div>
     `;
   }
 
@@ -465,6 +459,12 @@
   }
 }
 
+const SHOW_ALL_THRESHOLDS: Map<Category | 'SUCCESS', number> = new Map();
+SHOW_ALL_THRESHOLDS.set(Category.ERROR, 20);
+SHOW_ALL_THRESHOLDS.set(Category.WARNING, 20);
+SHOW_ALL_THRESHOLDS.set(Category.INFO, 5);
+SHOW_ALL_THRESHOLDS.set('SUCCESS', 5);
+
 @customElement('gr-checks-results')
 export class GrChecksResults extends GrLitElement {
   @query('#filterInput')
@@ -488,6 +488,9 @@
   actions: Action[] = [];
 
   @property()
+  links: Link[] = [];
+
+  @property()
   tabState?: ChecksTabState;
 
   @property()
@@ -506,6 +509,10 @@
     number | undefined
   >();
 
+  /** Maintains the state of which result sections should show all results. */
+  @internalProperty()
+  isShowAll: Map<Category | 'SUCCESS', boolean> = new Map();
+
   /**
    * This is the current state of whether a section is expanded or not. As long
    * as isSectionExpandedByUser is false this will be computed by a default rule
@@ -525,6 +532,7 @@
   constructor() {
     super();
     this.subscribe('actions', allActions$);
+    this.subscribe('links', allLinks$);
     this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
     this.subscribe('latestPatchsetNumber', latestPatchNum$);
     this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
@@ -563,6 +571,10 @@
           display: flex;
           align-items: center;
         }
+        .headerBottomRow .links iron-icon {
+          color: var(--link-color);
+          margin-right: var(--spacing-l);
+        }
         #moreActions iron-icon {
           color: var(--link-color);
         }
@@ -653,6 +665,12 @@
           font-weight: var(--font-weight-bold);
           padding: var(--spacing-s);
         }
+        gr-button.showAll {
+          margin: var(--spacing-m);
+        }
+        tr {
+          border-top: 1px solid var(--border-color);
+        }
       `,
     ];
   }
@@ -705,7 +723,7 @@
         </div>
         <div class="headerBottomRow">
           <div class="left">${this.renderFilter()}</div>
-          <div class="right">${this.renderActions()}</div>
+          <div class="right">${this.renderLinks()}${this.renderActions()}</div>
         </div>
       </div>
       <div class="body">
@@ -716,6 +734,24 @@
     `;
   }
 
+  private renderLinks() {
+    const links = (this.links ?? []).slice(0, 4);
+    if (links.length === 0) return;
+    return html`<div class="links">${links.map(this.renderLink)}</div>`;
+  }
+
+  private renderLink(link: Link) {
+    const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
+    return html`<a href="${link.url}" target="_blank"
+      ><iron-icon
+        aria-label="${tooltipText}"
+        class="link"
+        icon="gr-icons:${iconForLink(link.icon)}"
+      ></iron-icon
+      ><paper-tooltip offset="5">${tooltipText}</paper-tooltip></a
+    >`;
+  }
+
   private renderActions() {
     const overflowItems = this.actions.slice(2).map(action => {
       return {...action, id: action.name};
@@ -851,6 +887,16 @@
     }
     const expandedClass = expanded ? 'expanded' : 'collapsed';
     const icon = expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+    const isShowAll = this.isShowAll.get(category) ?? false;
+    const showAllThreshold = SHOW_ALL_THRESHOLDS.get(category) ?? 5;
+    const resultCount = filtered.length;
+    const resultLimit = isShowAll ? 1000 : showAllThreshold;
+    const showAllButton = this.renderShowAllButton(
+      category,
+      isShowAll,
+      showAllThreshold,
+      resultCount
+    );
     return html`
       <div class="${expandedClass}">
         <h3
@@ -867,15 +913,49 @@
             >${this.renderCount(all, selected, filtered)}</span
           >
         </h3>
-        ${this.renderResults(all, selected, filtered)}
+        ${this.renderResults(
+          all,
+          selected,
+          filtered,
+          resultLimit,
+          showAllButton
+        )}
       </div>
     `;
   }
 
+  renderShowAllButton(
+    category: Category | 'SUCCESS',
+    isShowAll: boolean,
+    showAllThreshold: number,
+    resultCount: number
+  ) {
+    if (resultCount <= showAllThreshold) return;
+    const message = isShowAll ? 'Show Less' : `Show All (${resultCount})`;
+    const handler = () => this.toggleShowAll(category);
+    return html`
+      <tr class="showAllRow">
+        <td colspan="4">
+          <gr-button class="showAll" link @click="${handler}"
+            >${message}</gr-button
+          >
+        </td>
+      </tr>
+    `;
+  }
+
+  toggleShowAll(category: Category | 'SUCCESS') {
+    const current = this.isShowAll.get(category) ?? false;
+    this.isShowAll.set(category, !current);
+    this.requestUpdate();
+  }
+
   renderResults(
     all: RunResult[],
     selected: RunResult[],
-    filtered: RunResult[]
+    filtered: RunResult[],
+    limit: number,
+    showAll: TemplateResult | undefined
   ) {
     if (all.length === 0) {
       return html`<div class="noResultsMessage">No results</div>`;
@@ -890,6 +970,7 @@
         No results match the regular expression
       </div>`;
     }
+    filtered = filtered.slice(0, limit);
     return html`
       <table class="resultsTable">
         <thead>
@@ -911,6 +992,7 @@
               ></gr-result-row>
             `
           )}
+          ${showAll}
         </tbody>
       </table>
     `;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index dc09479..de9c5fb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -26,7 +26,7 @@
   query,
 } from 'lit-element';
 import {GrLitElement} from '../lit/gr-lit-element';
-import {Action, RunStatus} from '../../api/checks';
+import {Action, Link, RunStatus} from '../../api/checks';
 import {sharedStyles} from '../../styles/shared-styles';
 import {
   AttemptDetail,
@@ -50,6 +50,7 @@
   fakeRun4_3,
   fakeRun4_4,
   updateStateSetResults,
+  fakeLinks,
 } from '../../services/checks/checks-model';
 import {assertIsDefined} from '../../utils/common-util';
 import {whenVisible} from '../../utils/dom-util';
@@ -436,7 +437,8 @@
         <gr-button link @click="${this.none}">none</gr-button>
         <gr-button
           link
-          @click="${() => this.toggle('f0', [fakeRun0], fakeActions)}"
+          @click="${() =>
+            this.toggle('f0', [fakeRun0], fakeActions, fakeLinks)}"
           >0</gr-button
         >
         <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
@@ -474,7 +476,7 @@
   }
 
   all() {
-    updateStateSetResults('f0', [fakeRun0], fakeActions);
+    updateStateSetResults('f0', [fakeRun0], fakeActions, fakeLinks);
     updateStateSetResults('f1', [fakeRun1]);
     updateStateSetResults('f2', [fakeRun2]);
     updateStateSetResults('f3', [fakeRun3]);
@@ -486,9 +488,14 @@
     ]);
   }
 
-  toggle(plugin: string, runs: CheckRun[], actions: Action[] = []) {
+  toggle(
+    plugin: string,
+    runs: CheckRun[],
+    actions: Action[] = [],
+    links: Link[] = []
+  ) {
     const newRuns = this.runs.includes(runs[0]) ? [] : runs;
-    updateStateSetResults(plugin, newRuns, actions);
+    updateStateSetResults(plugin, newRuns, actions, links);
   }
 
   renderSection(status: RunStatus) {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 0bf9f1c..5cd7bfc 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -40,6 +40,8 @@
 import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
 import {ParsedChangeInfo} from '../../../types/types';
 import {GrButton} from '../../shared/gr-button/gr-button';
+import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 export interface GrApplyFixDialog {
   $: {
@@ -97,6 +99,12 @@
   })
   _disableApplyFixButton?: boolean;
 
+  layers = appContext.flagsService.isEnabled(
+    KnownExperimentId.TOKEN_HIGHLIGHTING
+  )
+    ? [new TokenHighlightLayer()]
+    : [];
+
   private refitOverlay?: () => void;
 
   private readonly restApiService = appContext.restApiService;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
index 52fa9841..b0716dd 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
@@ -31,10 +31,6 @@
       background-color: var(--background-color-secondary);
       border-bottom: 1px solid var(--border-color);
     }
-    .fixActions {
-      display: flex;
-      justify-content: flex-end;
-    }
     gr-button {
       margin-left: var(--spacing-m);
     }
@@ -65,6 +61,7 @@
               change-num="[[changeNum]]"
               path="[[item.filepath]]"
               diff="[[item.preview]]"
+              layers="[[layers]]"
             ></gr-diff>
           </div>
         </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index 96206f2..d65deb6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -48,7 +48,6 @@
 import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
 import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
 import {fireAlert, fireEvent} from '../../../utils/event-util';
-import {TokenHighlightLayer} from './token-highlight-layer';
 
 const TRAILING_WHITESPACE_PATTERN = /\s+$/;
 
@@ -250,7 +249,6 @@
       this.$.rangeLayer,
       this.$.coverageLayerLeft,
       this.$.coverageLayerRight,
-      new TokenHighlightLayer(),
     ];
 
     if (this.layers) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index d0b3d3c..1d9a44b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -22,8 +22,6 @@
   getLineNumberByChild,
   lineNumberToNumber,
 } from '../gr-diff/gr-diff-utils';
-import {appContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
 
 const tokenMatcher = new RegExp(/[a-zA-Z0-9_-]+/g);
 
@@ -96,17 +94,12 @@
 
   private updateTokenTask?: DelayedTask;
 
-  private readonly enabled = appContext.flagsService.isEnabled(
-    KnownExperimentId.TOKEN_HIGHLIGHTING
-  );
-
   annotate(
     el: HTMLElement,
     _: HTMLElement,
     line: GrDiffLine,
     side: Side
   ): void {
-    if (!this.enabled) return;
     const text = el.textContent;
     if (!text) return;
     // Binary files encoded as text for example can have super long lines
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 1f4b778..6f34067 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
@@ -80,6 +80,7 @@
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {assertIsDefined} from '../../../utils/common-util';
 import {DiffContextExpandedEventDetail} from '../gr-diff-builder/gr-diff-builder';
+import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
 import {Timing} from '../../../constants/reporting';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
@@ -400,8 +401,16 @@
   }
 
   private _getLayers(path: string): DiffLayer[] {
+    const layers = [];
+    if (
+      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
+    ) {
+      layers.push(new TokenHighlightLayer());
+    }
+    layers.push(this.syntaxLayer);
     // Get layers from plugins (if any).
-    return [this.syntaxLayer, ...this.jsAPI.getDiffLayers(path)];
+    layers.push(...this.jsAPI.getDiffLayers(path));
+    return layers;
   }
 
   clear() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index bf4805c..cac28e9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -19,6 +19,7 @@
 import '@polymer/paper-checkbox/paper-checkbox';
 import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
 import '@polymer/paper-fab/paper-fab';
+import '@polymer/paper-icon-button/paper-icon-button';
 import '@polymer/paper-item/paper-item';
 import '@polymer/paper-listbox/paper-listbox';
 import './gr-overview-image';
@@ -71,6 +72,8 @@
 
   @internalProperty() protected checkerboardSelected = true;
 
+  @internalProperty() protected backgroundColor = '';
+
   @internalProperty() protected zoomedImageStyle: StyleInfo = {};
 
   @query('.imageArea') protected imageArea!: HTMLDivElement;
@@ -128,12 +131,25 @@
     }
   );
 
+  // Ensure constant function references, so that render() does not bind a new
+  // event listener on every call, as it would with lambdas.
+  private createColorPickerCallback(color: string) {
+    return {color, callback: () => this.pickColor(color)};
+  }
+
+  private readonly colorPickerCallbacks = [
+    this.createColorPickerCallback('#fff'),
+    this.createColorPickerCallback('#000'),
+    this.createColorPickerCallback('#aaa'),
+  ];
+
   static styles = css`
     :host {
       display: flex;
       width: 100%;
       height: 100%;
       box-sizing: border-box;
+      text-align: initial !important;
       font-size: var(--font-size-normal);
       --image-border-width: 2px;
     }
@@ -240,10 +256,80 @@
       margin: 0 var(--spacing-xl);
     }
     #follow-mouse {
+      margin: var(--spacing-m) var(--spacing-xl);
+    }
+    .color-picker {
       margin: var(--spacing-m) var(--spacing-xl) 0;
     }
+    .color-picker .label {
+      margin-bottom: var(--spacing-s);
+    }
+    .color-picker .options {
+      display: flex;
+      /* Ignore selection border for alignment, for visual balance. */
+      margin-left: -3px;
+    }
+    .color-picker-button {
+      border-width: 2px;
+      border-style: solid;
+      border-color: transparent;
+      border-radius: 50%;
+      width: 24px;
+      height: 24px;
+      padding: 1px;
+    }
+    .color-picker-button.selected {
+      border-color: var(--primary-button-background-color);
+    }
+    .color-picker-button:focus-within:not(.selected) {
+      /* Not an actual outline, as those do not follow border-radius. */
+      border-color: var(--outline-color-focus);
+    }
+    .color-picker-button .color {
+      border: 1px solid var(--border-color);
+      border-radius: 50%;
+      width: 100%;
+      height: 100%;
+      box-sizing: border-box;
+    }
   `;
 
+  private renderColorPickerButton(color: string, colorPicked: () => void) {
+    const selected =
+      color === this.backgroundColor && !this.checkerboardSelected;
+    return html`
+      <div
+        class="${classMap({
+          'color-picker-button': true,
+          selected,
+        })}"
+      >
+        <paper-icon-button
+          class="color"
+          style="${styleMap({backgroundColor: color})}"
+          @click="${colorPicked}"
+        ></paper-icon-button>
+      </div>
+    `;
+  }
+
+  private renderCheckerboardButton() {
+    return html`
+      <div
+        class="${classMap({
+          'color-picker-button': true,
+          selected: this.checkerboardSelected,
+        })}"
+      >
+        <paper-icon-button
+          class="color checkerboard"
+          @click="${this.pickCheckerboard}"
+        >
+        </paper-icon-button>
+      </div>
+    `;
+  }
+
   render() {
     const src = this.baseSelected ? this.baseUrl : this.revisionUrl;
 
@@ -251,8 +337,11 @@
       <img
         id="source-image"
         src="${src}"
-        class="${classMap({
-          checkerboard: this.checkerboardSelected,
+        class="${classMap({checkerboard: this.checkerboardSelected})}"
+        style="${styleMap({
+          backgroundColor: this.checkerboardSelected
+            ? ''
+            : this.backgroundColor,
         })}"
         @load="${this.updateSizes}"
       />
@@ -298,7 +387,15 @@
         .frameRect="${this.overviewFrame}"
         @center-updated="${this.onOverviewCenterUpdated}"
       >
-        <img src="${src}" class="checkerboard" />
+        <img
+          src="${src}"
+          class="${classMap({checkerboard: this.checkerboardSelected})}"
+          style="${styleMap({
+            backgroundColor: this.checkerboardSelected
+              ? ''
+              : this.backgroundColor,
+          })}"
+        />
       </gr-overview-image>
     `;
 
@@ -331,6 +428,18 @@
       </paper-checkbox>
     `;
 
+    const backgroundPicker = html`
+      <div class="color-picker">
+        <div class="label">Background</div>
+        <div class="options">
+          ${this.renderCheckerboardButton()}
+          ${this.colorPickerCallbacks.map(({color, callback}) =>
+            this.renderColorPickerButton(color, callback)
+          )}
+        </div>
+      </div>
+    `;
+
     /*
      * We want the content to fill the available space until it can display
      * without being cropped, the maximum of which will be determined by
@@ -438,7 +547,7 @@
 
       <paper-card class="controls">
         ${versionSwitcher} ${overviewImage} ${zoomControl}
-        ${!this.scaledSelected ? followMouse : ''}
+        ${!this.scaledSelected ? followMouse : ''} ${backgroundPicker}
       </paper-card>
     `;
   }
@@ -513,6 +622,19 @@
     );
   }
 
+  pickColor(value: string) {
+    this.checkerboardSelected = false;
+    this.backgroundColor = value;
+    this.dispatchEvent(createEvent({type: 'background-color-changed', value}));
+  }
+
+  pickCheckerboard() {
+    this.checkerboardSelected = true;
+    this.dispatchEvent(
+      createEvent({type: 'background-color-changed', value: 'checkerboard'})
+    );
+  }
+
   mousedownMagnifier(event: MouseEvent) {
     if (event.buttons === 1) {
       this.ownsMouseDown = true;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index ad2e9c0..8ead181 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -429,10 +429,6 @@
     return this.restApiService.getPreferences();
   }
 
-  _getWindowWidth() {
-    return window.innerWidth;
-  }
-
   _handleReviewedChange(e: Event) {
     this._setReviewed(
       ((dom(e) as EventApi).rootTarget as HTMLInputElement).checked
@@ -1215,17 +1211,6 @@
     );
   }
 
-  _patchRangeStr(patchRange: PatchRange) {
-    let patchStr = `${patchRange.patchNum}`;
-    if (
-      patchRange.basePatchNum &&
-      patchRange.basePatchNum !== ParentPatchSetNum
-    ) {
-      patchStr = `${patchRange.basePatchNum}..${patchRange.patchNum}`;
-    }
-    return patchStr;
-  }
-
   /**
    * When the latest patch of the change is selected (and there is no base
    * patch) then the patch range need not appear in the URL. Return a patch
@@ -1530,12 +1515,6 @@
     return this._changeComments.getPaths(patchRange);
   }
 
-  _getDiffDrafts() {
-    assertIsDefined(this._changeNum, '_changeNum');
-
-    return this.restApiService.getDiffDrafts(this._changeNum);
-  }
-
   _computeCommentSkips(
     commentMap?: CommentMap,
     fileList?: string[],
@@ -1812,10 +1791,6 @@
     return loggedIn && changeIsOpen(changeChangeRecord.base);
   }
 
-  _computeIsLoggedIn(loggedIn: boolean) {
-    return loggedIn ? true : false;
-  }
-
   /**
    * Wrapper for using in the element template and computed properties
    */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 30c6f49..0412779 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -112,10 +112,6 @@
     .prefsButton {
       text-align: right;
     }
-    .noOverflow {
-      display: block;
-      overflow: auto;
-    }
     .editMode .hideOnEdit {
       display: none;
     }
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
index 69ac702..ab24168 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
@@ -31,14 +31,6 @@
       padding: var(--spacing-xxl);
       width: 50em;
     }
-    .publicKey {
-      font-family: var(--monospace-font-family);
-      font-size: var(--font-size-mono);
-      line-height: var(--line-height-mono);
-      overflow-x: scroll;
-      overflow-wrap: break-word;
-      width: 30em;
-    }
     .closeButton {
       bottom: 2em;
       position: absolute;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 89cab8a..71c1add 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -20,7 +20,6 @@
 import {htmlTemplate} from './gr-change-status_html';
 import {customElement, property} from '@polymer/decorators';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {getRevertCreatedChangeIds} from '../../../utils/message-util';
 import {ChangeInfo} from '../../../types/common';
 import {ParsedChangeInfo} from '../../../types/types';
 
@@ -67,7 +66,7 @@
   tooltipText = '';
 
   @property({type: Object})
-  revertSubmittedChange?: ChangeInfo;
+  revertedChange?: ChangeInfo;
 
   _computeStatusString(status: ChangeStates) {
     if (status === ChangeStates.WIP && !this.flat) {
@@ -80,26 +79,15 @@
     return str ? str.toLowerCase().replace(/\s/g, '-') : '';
   }
 
-  hasStatusLink(status: ChangeStates) {
-    return (
-      status === ChangeStates.REVERT_CREATED ||
-      status === ChangeStates.REVERT_SUBMITTED
-    );
+  hasStatusLink(revertedChange?: ChangeInfo) {
+    return revertedChange !== undefined;
   }
 
-  getStatusLink(change?: ParsedChangeInfo, status?: ChangeStates) {
-    if (!change?.messages) return;
-    if (status === ChangeStates.REVERT_CREATED) {
-      const revertChangeId = getRevertCreatedChangeIds(change.messages)?.[0];
-      if (!revertChangeId) return;
-      return GerritNav.getUrlForSearchQuery(revertChangeId);
-    }
-    if (this.revertSubmittedChange) {
-      return GerritNav.getUrlForSearchQuery(
-        `${this.revertSubmittedChange._number}`
-      );
-    }
-    return;
+  getStatusLink(revertedChange?: ChangeInfo) {
+    return (
+      revertedChange &&
+      GerritNav.getUrlForSearchQuery(`${revertedChange._number}`)
+    );
   }
 
   _updateChipDetails(status?: ChangeStates, previousStatus?: ChangeStates) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 2a96cdf..3d227a9 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -81,14 +81,14 @@
     title="[[tooltipText]]"
     max-width="40em"
   >
-    <template is="dom-if" if="[[hasStatusLink(status)]]">
-      <a class="status-link" href="[[getStatusLink(change, status)]]">
+    <template is="dom-if" if="[[!!hasStatusLink(revertedChange)]]">
+      <a class="status-link" href="[[getStatusLink(revertedChange)]]">
         <div class="chip" aria-label$="Label: [[status]]">
           [[_computeStatusString(status)]]
         </div>
       </a>
     </template>
-    <template is="dom-if" if="[[!hasStatusLink(status)]]">
+    <template is="dom-if" if="[[!hasStatusLink(revertedChange)]]">
       <div class="chip" aria-label$="Label: [[status]]">
         [[_computeStatusString(status)]]
       </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 5b70cb4..86c1b8c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -60,6 +60,7 @@
 import {waitForEventOnce} from '../../../utils/event-util';
 import {GrSyntaxLayer} from '../../diff/gr-syntax-layer/gr-syntax-layer';
 import {StorageLocation} from '../../../services/storage/gr-storage';
+import {TokenHighlightLayer} from '../../diff/gr-diff-builder/token-highlight-layer';
 
 const UNRESOLVED_EXPAND_COUNT = 5;
 const NEWLINE_PATTERN = /\n/g;
@@ -344,7 +345,14 @@
 
   _getLayers(diff?: DiffInfo) {
     if (!diff) return [];
-    return [this.syntaxLayer];
+    const layers = [];
+    if (
+      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
+    ) {
+      layers.push(new TokenHighlightLayer());
+    }
+    layers.push(this.syntaxLayer);
+    return layers;
   }
 
   _getUrlForViewDiff(comments: UIComment[]) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index a1644e7..1f0ce3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -80,10 +80,6 @@
       justify-content: space-between;
       padding: 0 var(--spacing-s) var(--spacing-s);
     }
-    .descriptionText {
-      margin-left: var(--spacing-m);
-      font-style: italic;
-    }
     .fileName {
       padding: var(--spacing-m) var(--spacing-s) var(--spacing-m);
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index bf10121..6125d33 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -271,15 +271,6 @@
     this._openEmojiDropdown();
   }
 
-  _getFontSize() {
-    const fontSizePx = getComputedStyle(this).fontSize || '12px';
-    return Number(fontSizePx.substr(0, fontSizePx.length - 2));
-  }
-
-  _getScrollTop() {
-    return document.body.scrollTop;
-  }
-
   /**
    * _handleKeydown used for key handling in the this.$.textarea AND all child
    * autocomplete options.
diff --git a/polygerrit-ui/app/services/change/change-service.ts b/polygerrit-ui/app/services/change/change-service.ts
index c292fb5..1c6fc4c 100644
--- a/polygerrit-ui/app/services/change/change-service.ts
+++ b/polygerrit-ui/app/services/change/change-service.ts
@@ -17,8 +17,16 @@
 import {routerChangeNum$} from '../router/router-model';
 import {updateState} from './change-model';
 import {ParsedChangeInfo} from '../../types/types';
+import {appContext} from '../app-context';
+import {ChangeInfo} from '../../types/common';
+import {
+  computeAllPatchSets,
+  computeLatestPatchNum,
+} from '../../utils/patch-set-util';
 
 export class ChangeService {
+  private readonly restApiService = appContext.restApiService;
+
   constructor() {
     // TODO: In the future we will want to make restApiService.getChangeDetail()
     // calls from a switchMap() here. For now just make sure to invalidate the
@@ -38,4 +46,35 @@
   updateChange(change: ParsedChangeInfo) {
     updateState(change);
   }
+
+  /**
+   * Check whether there is no newer patch than the latest patch that was
+   * available when this change was loaded.
+   *
+   * @return A promise that yields true if the latest patch
+   *     has been loaded, and false if a newer patch has been uploaded in the
+   *     meantime. The promise is rejected on network error.
+   */
+  fetchChangeUpdates(change: ChangeInfo | ParsedChangeInfo) {
+    const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
+    return this.restApiService.getChangeDetail(change._number).then(detail => {
+      if (!detail) {
+        const error = new Error('Change detail not found.');
+        return Promise.reject(error);
+      }
+      const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
+      if (!actualLatest || !knownLatest) {
+        const error = new Error('Unable to check for latest patchset.');
+        return Promise.reject(error);
+      }
+      return {
+        isLatest: actualLatest <= knownLatest,
+        newStatus: change.status !== detail.status ? detail.status : null,
+        newMessages:
+          (change.messages || []).length < (detail.messages || []).length
+            ? detail.messages![detail.messages!.length - 1]
+            : undefined,
+      };
+    });
+  }
 }
diff --git a/polygerrit-ui/app/services/change/change-services_test.ts b/polygerrit-ui/app/services/change/change-services_test.ts
new file mode 100644
index 0000000..3e427ff
--- /dev/null
+++ b/polygerrit-ui/app/services/change/change-services_test.ts
@@ -0,0 +1,108 @@
+/**
+ * @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 {ChangeStatus} from '../../constants/constants';
+import '../../test/common-test-setup-karma';
+import {
+  createChange,
+  createChangeMessageInfo,
+  createRevision,
+} from '../../test/test-data-generators';
+import {stubRestApi} from '../../test/test-utils';
+import {CommitId, PatchSetNum} from '../../types/common';
+import {ParsedChangeInfo} from '../../types/types';
+import {ChangeService} from './change-service';
+
+suite('change service tests', () => {
+  let changeService: ChangeService;
+  let knownChange: ParsedChangeInfo;
+  setup(() => {
+    changeService = new ChangeService();
+    knownChange = {
+      ...createChange(),
+      revisions: {
+        sha1: {
+          ...createRevision(1),
+          description: 'patch 1',
+          _number: 1 as PatchSetNum,
+        },
+        sha2: {
+          ...createRevision(2),
+          description: 'patch 2',
+          _number: 2 as PatchSetNum,
+        },
+      },
+      status: ChangeStatus.NEW,
+      current_revision: 'abc' as CommitId,
+      messages: [],
+    };
+  });
+
+  test('changeService.fetchChangeUpdates on latest', async () => {
+    stubRestApi('getChangeDetail').returns(Promise.resolve(knownChange));
+    const result = await changeService.fetchChangeUpdates(knownChange);
+    assert.isTrue(result.isLatest);
+    assert.isNotOk(result.newStatus);
+    assert.isNotOk(result.newMessages);
+  });
+
+  test('changeService.fetchChangeUpdates not on latest', async () => {
+    const actualChange = {
+      ...knownChange,
+      revisions: {
+        ...knownChange.revisions,
+        sha3: {
+          ...createRevision(3),
+          description: 'patch 3',
+          _number: 3 as PatchSetNum,
+        },
+      },
+    };
+    stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+    const result = await changeService.fetchChangeUpdates(knownChange);
+    assert.isFalse(result.isLatest);
+    assert.isNotOk(result.newStatus);
+    assert.isNotOk(result.newMessages);
+  });
+
+  test('changeService.fetchChangeUpdates new status', async () => {
+    const actualChange = {
+      ...knownChange,
+      status: ChangeStatus.MERGED,
+    };
+    stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+    const result = await changeService.fetchChangeUpdates(knownChange);
+    assert.isTrue(result.isLatest);
+    assert.equal(result.newStatus, ChangeStatus.MERGED);
+    assert.isNotOk(result.newMessages);
+  });
+
+  test('changeService.fetchChangeUpdates new messages', async () => {
+    const actualChange = {
+      ...knownChange,
+      messages: [{...createChangeMessageInfo(), message: 'blah blah'}],
+    };
+    stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+    const result = await changeService.fetchChangeUpdates(knownChange);
+    assert.isTrue(result.isLatest);
+    assert.isNotOk(result.newStatus);
+    assert.deepEqual(result.newMessages, {
+      ...createChangeMessageInfo(),
+      message: 'blah blah',
+    });
+  });
+});
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index f2d8757..ecd56cb 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -22,6 +22,7 @@
   CheckResult as CheckResultApi,
   CheckRun as CheckRunApi,
   ChecksApiConfig,
+  Link,
   LinkIcon,
   RunStatus,
 } from '../../api/checks';
@@ -76,6 +77,7 @@
   config?: ChecksApiConfig;
   runs: CheckRun[];
   actions: Action[];
+  links: Link[];
 }
 
 interface ChecksState {
@@ -148,6 +150,18 @@
   )
 );
 
+export const allLinks$ = checksProviderState$.pipe(
+  map(state =>
+    Object.values(state).reduce(
+      (allActions: Link[], providerState: ChecksProviderState) => [
+        ...allActions,
+        ...providerState.links,
+      ],
+      []
+    )
+  )
+);
+
 export const allRuns$ = checksProviderState$.pipe(
   map(state =>
     Object.values(state).reduce(
@@ -208,6 +222,7 @@
     config,
     runs: [],
     actions: [],
+    links: [],
   };
   privateState$.next(nextState);
 }
@@ -457,6 +472,21 @@
   },
 ];
 
+export const fakeLinks: Link[] = [
+  {
+    url: 'https://www.google.com',
+    primary: false,
+    tooltip: 'Tooltip for Bug Report Fake Link',
+    icon: LinkIcon.REPORT_BUG,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: false,
+    tooltip: 'Tooltip for External Fake Link',
+    icon: LinkIcon.EXTERNAL,
+  },
+];
+
 export function updateStateSetLoading(pluginName: string) {
   const nextState = {...privateState$.getValue()};
   nextState.providerNameToState = {...nextState.providerNameToState};
@@ -501,7 +531,8 @@
 export function updateStateSetResults(
   pluginName: string,
   runs: CheckRunApi[],
-  actions: Action[] = []
+  actions: Action[] = [],
+  links: Link[] = []
 ) {
   const attemptMap = createAttemptMap(runs);
   for (const attemptInfo of attemptMap.values()) {
@@ -535,6 +566,7 @@
       };
     }),
     actions: [...actions],
+    links: [...links],
   };
   privateState$.next(nextState);
 }
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
index c7e42ba..250cea5 100644
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ b/polygerrit-ui/app/services/checks/checks-service.ts
@@ -176,7 +176,8 @@
             updateStateSetResults(
               pluginName,
               response.runs ?? [],
-              response.actions
+              response.actions ?? [],
+              response.links ?? []
             );
             break;
         }
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 6f94b8d..92a5d55 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -28,7 +28,6 @@
   // be used by plugins. The new Checks UI will show up, if a plugin registers
   // with the new Checks plugin API.
   CI_REBOOT_CHECKS = 'UiFeature__ci_reboot_checks',
-  NEW_CHANGE_SUMMARY_UI = 'UiFeature__new_change_summary_ui',
   NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
   COMMENT_CONTEXT = 'UiFeature__comment_context',
   TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 0f229df..324a912 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -528,6 +528,7 @@
   real_author?: AccountInfo;
   date: Timestamp;
   message: string;
+  accountsInMessage?: AccountInfo[];
   tag?: ReviewInputTag;
   _revision_number?: PatchSetNum;
 }
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index 5034028..24662fd 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -8,7 +8,6 @@
   BasePatchSetNum,
   RevisionPatchSetNum,
 } from '../types/common';
-import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
 import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
 import {check} from './common-util';
 
@@ -302,41 +301,6 @@
 }
 
 /**
- * Check whether there is no newer patch than the latest patch that was
- * available when this change was loaded.
- *
- * @return A promise that yields true if the latest patch
- *     has been loaded, and false if a newer patch has been uploaded in the
- *     meantime. The promise is rejected on network error.
- */
-// TODO: remove usage of RestApiService inside util
-export function fetchChangeUpdates(
-  change: ChangeInfo | ParsedChangeInfo,
-  restAPI: RestApiService
-) {
-  const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
-  return restAPI.getChangeDetail(change._number).then(detail => {
-    if (!detail) {
-      const error = new Error('Change detail not found.');
-      return Promise.reject(error);
-    }
-    const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
-    if (!actualLatest || !knownLatest) {
-      const error = new Error('Unable to check for latest patchset.');
-      return Promise.reject(error);
-    }
-    return {
-      isLatest: actualLatest <= knownLatest,
-      newStatus: change.status !== detail.status ? detail.status : null,
-      newMessages:
-        (change.messages || []).length < (detail.messages || []).length
-          ? detail.messages![detail.messages!.length - 1]
-          : undefined,
-    };
-  });
-}
-
-/**
  * @param revisions A sorted array of revisions.
  *
  * @return the index of the revision with the given patchNum.
diff --git a/polygerrit-ui/app/utils/patch-set-util_test.js b/polygerrit-ui/app/utils/patch-set-util_test.js
index eefdcda..93073fa 100644
--- a/polygerrit-ui/app/utils/patch-set-util_test.js
+++ b/polygerrit-ui/app/utils/patch-set-util_test.js
@@ -18,7 +18,7 @@
 import '../test/common-test-setup-karma.js';
 import {
   _testOnly_computeWipForPatchSets, computeAllPatchSets,
-  fetchChangeUpdates, findEditParentPatchNum, findEditParentRevision,
+  findEditParentPatchNum, findEditParentRevision,
   getParentIndex, getRevisionByPatchNum,
   isMergeParent,
   sortRevisions,
@@ -36,123 +36,6 @@
     assert.equal(getRevisionByPatchNum(revisions, 3), undefined);
   });
 
-  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);
-      },
-    };
-    fetchChangeUpdates(knownChange, mockRestApi)
-        .then(result => {
-          assert.isTrue(result.isLatest);
-          assert.isNotOk(result.newStatus);
-          assert.isNotOk(result.newMessages);
-          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: {
-        sha1: {description: 'patch 1', _number: 1},
-        sha2: {description: 'patch 2', _number: 2},
-        sha3: {description: 'patch 3', _number: 3},
-      },
-      status: 'NEW',
-      messages: [],
-    };
-    const mockRestApi = {
-      getChangeDetail() {
-        return Promise.resolve(actualChange);
-      },
-    };
-    fetchChangeUpdates(knownChange, mockRestApi)
-        .then(result => {
-          assert.isFalse(result.isLatest);
-          assert.isNotOk(result.newStatus);
-          assert.isNotOk(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);
-      },
-    };
-    fetchChangeUpdates(knownChange, mockRestApi)
-        .then(result => {
-          assert.isTrue(result.isLatest);
-          assert.equal(result.newStatus, 'MERGED');
-          assert.isNotOk(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);
-      },
-    };
-    fetchChangeUpdates(knownChange, mockRestApi)
-        .then(result => {
-          assert.isTrue(result.isLatest);
-          assert.isNotOk(result.newStatus);
-          assert.deepEqual(result.newMessages, {message: 'blah blah'});
-          done();
-        });
-  });
-
   test('_computeWipForPatchSets', () => {
     // Compute patch sets for a given timeline on a change. The initial WIP
     // property of the change can be true or false. The map of tags by