Merge "Update docs of Checks API types"
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 54d9176..89038e2 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -1425,7 +1425,7 @@
           if (group.getUUID() != null) {
             keepGroups.add(group.getUUID());
           }
-          rules.add(rule.asString(needRange));
+          rules.add(rule.toBuilder().setGroup(group).build().asString(needRange));
         }
         rc.setStringList(CAPABILITY, null, permission.getName(), rules);
       }
@@ -1470,7 +1470,7 @@
           if (group.getUUID() != null) {
             keepGroups.add(group.getUUID());
           }
-          rules.add(rule.asString(needRange));
+          rules.add(rule.toBuilder().setGroup(group).build().asString(needRange));
         }
         rc.setStringList(ACCESS, refName, permission.getName(), rules);
       }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 0d94ce6..436ad7c 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -113,6 +113,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -704,6 +705,36 @@
   }
 
   @Test
+  public void renamingGroupChangesProjectConfigs() throws Exception {
+    String name = name("Name1");
+    GroupInfo group = gApi.groups().create(name).get();
+
+    // Use group in a permission
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(AccountGroup.uuid(group.id)))
+        .update();
+    Optional<String> beforeRename =
+        projectCache.get(project).get().getLocalGroups().stream()
+            .filter(g -> g.getUUID().get().equals(group.id))
+            .map(GroupReference::getName)
+            .findAny();
+    // Groups created with ProjectOperations always have their UUID as local name
+    assertThat(beforeRename).hasValue(group.id);
+
+    String newName = name("Name2");
+    gApi.groups().id(name).name(newName);
+
+    Optional<String> afterRename =
+        projectCache.get(project).get().getLocalGroups().stream()
+            .filter(g -> g.getUUID().get().equals(group.id))
+            .map(GroupReference::getName)
+            .findAny();
+    assertThat(afterRename).hasValue(newName);
+  }
+
+  @Test
   public void groupDescription() throws Exception {
     String name = name("group");
     gApi.groups().create(name);
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 6029dc1..061c5cd 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -21,10 +21,11 @@
 export enum PrimaryTab {
   FILES = 'files',
   /**
-   * When renaming this, the links in UrlFormatter must be updated.
+   * When renaming 'comments' or 'findings', UrlFormatter.java must be updated.
    */
   COMMENT_THREADS = 'comments',
   FINDINGS = 'findings',
+  CHECKS = 'checks',
 }
 
 /**
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 f3ea363..26b553e 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
@@ -43,6 +43,7 @@
 import '../gr-reply-dialog/gr-reply-dialog';
 import '../gr-thread-list/gr-thread-list';
 import '../gr-upload-help-dialog/gr-upload-help-dialog';
+import '../../checks/gr-checks-tab';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
@@ -153,6 +154,7 @@
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
 import {PORTING_COMMENTS_CHANGE_LATENCY_LABEL} from '../../../services/gr-reporting/gr-reporting';
 import {fire, EventType} from '../../../utils/event-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -263,6 +265,8 @@
 
   reporting = appContext.reportingService;
 
+  flagsService = appContext.flagsService;
+
   /**
    * URL params passed from the router.
    */
@@ -532,6 +536,8 @@
 
   _throttledToggleChangeStar?: EventListener;
 
+  _isChecksEnabled = false;
+
   keyboardShortcuts() {
     return {
       [Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
@@ -556,6 +562,14 @@
   }
 
   /** @override */
+  ready() {
+    super.ready();
+    this._isChecksEnabled = this.flagsService.isEnabled(
+      KnownExperimentId.CI_REBOOT_CHECKS
+    );
+  }
+
+  /** @override */
   connectedCallback() {
     super.connectedCallback();
     this._throttledToggleChangeStar = this._throttleWrap(e =>
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 efc86bc..fbf01dc 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
@@ -558,6 +558,11 @@
           <span>Comments</span></gr-tooltip-content
         >
       </paper-tab>
+      <template is="dom-if" if="[[_isChecksEnabled]]">
+        <paper-tab data-name$="[[_constants.PrimaryTab.CHECKS]]"
+          >Checks</paper-tab
+        >
+      </template>
       <template
         is="dom-repeat"
         items="[[_dynamicTabHeaderEndpoints]]"
@@ -648,6 +653,12 @@
       </template>
       <template
         is="dom-if"
+        if="[[_isTabActive(_constants.PrimaryTab.CHECKS, _activeTabs)]]"
+      >
+        <gr-checks-tab id="checksTab"></gr-checks-tab>
+      </template>
+      <template
+        is="dom-if"
         if="[[_isTabActive(_constants.PrimaryTab.FINDINGS, _activeTabs)]]"
       >
         <gr-dropdown-list
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 8d06a03..6262300 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
@@ -190,6 +190,7 @@
       path: '/COMMIT_MSG',
       line: 5,
       rootId: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
     {
       comments: [
@@ -249,6 +250,7 @@
       path: '/COMMIT_MSG',
       line: 4,
       rootId: '8caddf38_44770ec1' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
     {
       comments: [
@@ -271,6 +273,7 @@
       path: '/COMMIT_MSG',
       line: 4,
       rootId: 'scaddf38_44770ec1' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
     {
       comments: [
@@ -291,6 +294,7 @@
       path: '/COMMIT_MSG',
       line: 6,
       rootId: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
     {
       comments: [
@@ -314,6 +318,7 @@
       path: '/COMMIT_MSG',
       line: 5,
       rootId: 'rc1' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
     {
       comments: [
@@ -351,6 +356,7 @@
       path: '/COMMIT_MSG',
       line: 5,
       rootId: 'rc2' as UrlEncodedCommentId,
+      commentSide: CommentSide.REVISION,
     },
   ];
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index d85ae4d..c750bd2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1498,6 +1498,7 @@
     const commitMsgComments = [
       {
         patch_set: 2,
+        path: '/p',
         id: 'ecf0b9fa_fe1a5f62',
         line: 20,
         updated: '2018-02-08 18:49:18.000000000',
@@ -1506,6 +1507,7 @@
       },
       {
         patch_set: 2,
+        path: '/p',
         id: '503008e2_0ab203ee',
         line: 10,
         updated: '2018-02-14 22:07:43.000000000',
@@ -1514,6 +1516,7 @@
       },
       {
         patch_set: 2,
+        path: '/p',
         id: 'cc788d2c_cb1d728c',
         line: 20,
         in_reply_to: 'ecf0b9fa_fe1a5f62',
@@ -1888,6 +1891,7 @@
       const commentStubRes1 = [
         {
           patch_set: 2,
+          path: '/p',
           id: '503008e2_0ab203ee',
           line: 20,
           updated: '2018-02-08 18:49:18.000000000',
@@ -1898,6 +1902,7 @@
       const commentStubRes2 = [
         {
           patch_set: 2,
+          path: '/p',
           id: 'ecf0b9fa_fe1a5f62',
           line: 20,
           updated: '2018-02-08 18:49:18.000000000',
@@ -1906,6 +1911,7 @@
         },
         {
           patch_set: 2,
+          path: '/p',
           id: '503008e2_0ab203ee',
           line: 10,
           in_reply_to: 'ecf0b9fa_fe1a5f62',
@@ -1915,6 +1921,7 @@
         },
         {
           patch_set: 2,
+          path: '/p',
           id: '503008e2_0ab203ef',
           line: 20,
           in_reply_to: '503008e2_0ab203ee',
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
new file mode 100644
index 0000000..ca73f4f
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -0,0 +1,36 @@
+/**
+ * @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 'lit-html';
+import {customElement} from 'lit-element';
+import {GrLitElement} from '../lit/gr-lit-element';
+
+/**
+ * The "Checks" tab on the Gerrit change page. Gets its data from plugins that
+ * have registered with the Checks Plugin API.
+ */
+@customElement('gr-checks-tab')
+export class GrChecksTab extends GrLitElement {
+  render() {
+    return html`<span>Hello Checks!</span>`;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-checks-tab': GrChecksTab;
+  }
+}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
new file mode 100644
index 0000000..85183ed
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
@@ -0,0 +1,26 @@
+/**
+ * @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 '../../test/common-test-setup-karma';
+import {GrChecksTab} from './gr-checks-tab';
+
+suite('gr-checks-tab test', () => {
+  test('is defined', () => {
+    const el = document.createElement('gr-checks-tab');
+    assert.instanceOf(el, GrChecksTab);
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index bbe99b9..b01a110 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -46,11 +46,11 @@
   CommentThread,
   DraftInfo,
   isUnresolved,
-  sortComments,
   UIComment,
   UIDraft,
   UIHuman,
   UIRobot,
+  createCommentThreads,
 } from '../../../utils/comment-util';
 import {PatchSetFile, PatchNumOnly, isPatchSetFile} from '../../../types/types';
 
@@ -448,7 +448,7 @@
       );
     }
 
-    return this.getCommentThreads(comments).length;
+    return createCommentThreads(comments).length;
   }
 
   /**
@@ -480,60 +480,14 @@
     }
 
     comments = comments.concat(drafts);
-    const threads = this.getCommentThreads(sortComments(comments));
+    const threads = createCommentThreads(comments);
     const unresolvedThreads = threads.filter(isUnresolved);
     return unresolvedThreads.length;
   }
 
   getAllThreadsForChange() {
     const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
-    const sortedComments = sortComments(comments);
-    return this.getCommentThreads(sortedComments);
-  }
-
-  /**
-   * Computes all of the comments in thread format.
-   *
-   * @param comments sorted by updated timestamp.
-   */
-  getCommentThreads(comments: UIComment[]) {
-    const threads: CommentThread[] = [];
-    const idThreadMap: CommentIdToCommentThreadMap = {};
-    for (const comment of comments) {
-      if (!comment.id) continue;
-      // If the comment is in reply to another comment, find that comment's
-      // thread and append to it.
-      if (comment.in_reply_to) {
-        const thread = idThreadMap[comment.in_reply_to];
-        if (thread) {
-          thread.comments.push(comment);
-          idThreadMap[comment.id] = thread;
-          continue;
-        }
-      }
-
-      // Otherwise, this comment starts its own thread.
-      if (!comment.__path && !comment.path) {
-        throw new Error('Comment missing required "path".');
-      }
-      const newThread: CommentThread = {
-        comments: [comment],
-        patchNum: comment.patch_set,
-        path: comment.__path || comment.path!,
-        rootId: comment.id,
-        line: comment.line,
-        range: comment.range,
-      };
-      if (comment.side) {
-        newThread.commentSide = comment.side;
-      }
-      if (!comment.line && !comment.range) {
-        newThread.line = 'FILE';
-      }
-      threads.push(newThread);
-      idThreadMap[comment.id] = newThread;
-    }
-    return threads;
+    return createCommentThreads(comments);
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index d9b6b53..d21af73 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-comment-api.js';
 import {ChangeComments} from './gr-comment-api.js';
+import {CommentSide} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-comment-api');
 
@@ -538,6 +539,7 @@
                 __path: 'file/one',
               },
             ],
+            diffSide: undefined,
             commentSide: 'PARENT',
             patchNum: 2,
             path: 'file/one',
@@ -561,11 +563,12 @@
                 updated: '2013-02-26 15:01:43.986000000',
               },
             ],
-            commentSide: 'PARENT',
             patchNum: 2,
             path: 'file/one',
             line: 2,
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.PARENT,
             rootId: '03',
           }, {
             comments: [
@@ -603,6 +606,8 @@
             line: 1,
             rootId: '04',
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -619,6 +624,8 @@
             line: 2,
             rootId: '05',
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -635,6 +642,8 @@
             line: 2,
             rootId: '06',
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -665,6 +674,7 @@
             line: 1,
             rootId: '07',
             range: undefined,
+            diffSide: undefined,
           }, {
             comments: [
               {
@@ -681,6 +691,8 @@
             line: 1,
             rootId: '09',
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -699,6 +711,7 @@
             line: 1,
             rootId: '10',
             range: undefined,
+            diffSide: undefined,
           }, {
             comments: [
               {
@@ -715,6 +728,8 @@
             path: 'file/four',
             line: 1,
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -732,6 +747,8 @@
             path: 'file/two',
             line: 1,
             range: undefined,
+            diffSide: undefined,
+            commentSide: CommentSide.REVISION,
           }, {
             comments: [
               {
@@ -751,6 +768,7 @@
             path: 'file/one',
             line: 1,
             range: undefined,
+            diffSide: undefined,
           },
         ];
         const threads = element._changeComments.getAllThreadsForChange();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index efac2b1..1ae302a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -105,7 +105,6 @@
     row.classList.add('diff-row', 'side-by-side');
     row.setAttribute('left-type', leftLine.type);
     row.setAttribute('right-type', rightLine.type);
-    row.tabIndex = -1;
 
     row.appendChild(this._createBlameCell(leftLine.beforeNumber));
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index c4ea267..04ac472 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -104,7 +104,6 @@
   _createRow(line: GrDiffLine) {
     const row = this._createElement('tr', line.type);
     row.classList.add('diff-row', 'unified');
-    row.tabIndex = -1;
     row.appendChild(this._createBlameCell(line.beforeNumber));
     let lineNumberEl = this._createLineEl(
       line,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index f4aa660..75f95f3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -563,6 +563,7 @@
     if (type === GrDiffBuilder.ContextButtonType.ALL) {
       if (this.useNewContextControls) {
         text = `+${numLines} common line`;
+        button.setAttribute('aria-label', `Show ${numLines} common lines`);
       } else {
         text = `Show ${numLines} common line`;
         const icon = this._createElement('iron-icon', 'showContext');
@@ -583,6 +584,7 @@
       if (this.useNewContextControls) {
         text = `+${context}`;
         button.classList.add('aboveButton');
+        button.setAttribute('aria-label', `Show ${context} lines above`);
       } else {
         text = `+${context} above`;
       }
@@ -591,6 +593,7 @@
       if (this.useNewContextControls) {
         text = `+${context}`;
         button.classList.add('belowButton');
+        button.setAttribute('aria-label', `Show ${context} lines below`);
       } else {
         text = `+${context} below`;
       }
@@ -659,6 +662,9 @@
       button.classList.add(side);
       button.dataset['value'] = number.toString();
       button.textContent = number === 'FILE' ? 'File' : number.toString();
+      if (number === 'FILE') {
+        button.setAttribute('aria-label', 'Add file comment');
+      }
 
       // Add aria-labels for valid line numbers.
       // For unified diff, this method will be called with number set to 0 for
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 cb75956..4b0208a 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
@@ -34,8 +34,9 @@
 import {
   Comment,
   isDraft,
-  sortComments,
   UIComment,
+  CommentThread,
+  createCommentThreads,
 } from '../../../utils/comment-util';
 import {TwoSidesComments} from '../gr-comment-api/gr-comment-api';
 import {customElement, observe, property} from '@polymer/decorators';
@@ -65,7 +66,7 @@
 import {JsApiService} from '../../shared/gr-js-api-interface/gr-js-api-types';
 import {GrDiff, LineOfInterest} from '../gr-diff/gr-diff';
 import {GrSyntaxLayer} from '../gr-syntax-layer/gr-syntax-layer';
-import {DiffViewMode, Side} from '../../../constants/constants';
+import {DiffViewMode, Side, CommentSide} from '../../../constants/constants';
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
 import {LineNumber, FILE} from '../gr-diff/gr-diff-line';
@@ -109,20 +110,6 @@
   afterNumber?: LineNumber;
 }
 
-// TODO(TS): Consolidate this with the CommentThread interface of comment-api.
-// What is being used here is just a local object for collecting all the data
-// that is needed to create a GrCommentThread component, see
-// _createThreadElement().
-interface CommentThread {
-  comments: UIComment[];
-  // In the context of a diff each thread must have a side!
-  commentSide: Side;
-  patchNum?: PatchSetNum;
-  lineNum?: LineNumber;
-  isOnParent?: boolean;
-  range?: CommentRange;
-}
-
 export interface GrDiffHost {
   $: {
     restAPI: RestApiService & Element;
@@ -712,46 +699,13 @@
     // and recreate them. If this changes in future, we might want to reuse
     // some DOM nodes here.
     this._clearThreads();
-    const threads = this._createThreads(allComments);
+    const threads = createCommentThreads(allComments);
     for (const thread of threads) {
       const threadEl = this._createThreadElement(thread);
       this._attachThreadElement(threadEl);
     }
   }
 
-  _createThreads(comments: UIComment[]): CommentThread[] {
-    const sortedComments = sortComments(comments);
-    const threads = [];
-    for (const comment of sortedComments) {
-      // If the comment is in reply to another comment, find that comment's
-      // thread and append to it.
-      if (comment.in_reply_to) {
-        const thread = threads.find(thread =>
-          thread.comments.some(c => c.id === comment.in_reply_to)
-        );
-        if (thread) {
-          thread.comments.push(comment);
-          continue;
-        }
-      }
-
-      // Otherwise, this comment starts its own thread.
-      if (!comment.__commentSide) throw new Error('Missing "__commentSide".');
-      const newThread: CommentThread = {
-        comments: [comment],
-        commentSide: comment.__commentSide,
-        patchNum: comment.patch_set,
-        lineNum: comment.line,
-        isOnParent: comment.side === 'PARENT',
-      };
-      if (comment.range) {
-        newThread.range = {...comment.range};
-      }
-      threads.push(newThread);
-    }
-    return threads;
-  }
-
   _computeIsBlameLoaded(blame: BlameInfo[] | null) {
     return !!blame;
   }
@@ -767,13 +721,14 @@
   }
 
   _handleCreateComment(e: CustomEvent) {
-    const {lineNum, side, patchNum, isOnParent, range} = e.detail;
+    const {lineNum, side, patchNum, range, path, commentSide} = e.detail;
     const threadEl = this._getOrCreateThread(
       patchNum,
       lineNum,
       side,
-      range,
-      isOnParent
+      commentSide,
+      path,
+      range
     );
     threadEl.addOrEditDraft(lineNum, range);
 
@@ -787,19 +742,21 @@
   _getOrCreateThread(
     patchNum: PatchSetNum,
     lineNum: LineNumber | undefined,
-    commentSide: Side,
-    range?: CommentRange,
-    isOnParent?: boolean
+    diffSide: Side,
+    commentSide: CommentSide,
+    path: string,
+    range?: CommentRange
   ): GrCommentThread {
-    let threadEl = this._getThreadEl(lineNum, commentSide, range);
+    let threadEl = this._getThreadEl(lineNum, diffSide, range);
     if (!threadEl) {
       threadEl = this._createThreadElement({
         comments: [],
+        path,
+        diffSide,
         commentSide,
         patchNum,
-        lineNum,
+        line: lineNum,
         range,
-        isOnParent,
       });
       this._attachThreadElement(threadEl);
     }
@@ -820,18 +777,18 @@
   _createThreadElement(thread: CommentThread) {
     const threadEl = document.createElement('gr-comment-thread');
     threadEl.className = 'comment-thread';
-    threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
+    threadEl.setAttribute('slot', `${thread.diffSide}-${thread.line}`);
     threadEl.comments = thread.comments;
-    threadEl.commentSide = thread.commentSide;
-    threadEl.isOnParent = !!thread.isOnParent;
+    threadEl.commentSide = thread.diffSide;
+    threadEl.isOnParent = thread.commentSide === CommentSide.PARENT;
     threadEl.parentIndex = this._parentIndex;
     // Use path before renmaing when comment added on the left when comparing
     // two patch sets (not against base)
     if (
       this.file &&
       this.file.basePath &&
-      thread.commentSide === Side.LEFT &&
-      !thread.isOnParent
+      thread.diffSide === Side.LEFT &&
+      !threadEl.isOnParent
     ) {
       threadEl.path = this.file.basePath;
     } else {
@@ -841,7 +798,7 @@
     threadEl.patchNum = thread.patchNum;
     threadEl.showPatchset = false;
     // GrCommentThread does not understand 'FILE', but requires undefined.
-    threadEl.lineNum = thread.lineNum !== 'FILE' ? thread.lineNum : undefined;
+    threadEl.lineNum = thread.line !== 'FILE' ? thread.line : undefined;
     threadEl.projectName = this.projectName;
     threadEl.range = thread.range;
     const threadDiscardListener = (e: Event) => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 1dd2737..bf4d912 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -20,9 +20,10 @@
 import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {sortComments} from '../../../utils/comment-util.js';
-import {Side} from '../../../constants/constants.js';
+import {sortComments, createCommentThreads} from '../../../utils/comment-util.js';
+import {Side, CommentSide} from '../../../constants/constants.js';
 import {createChange} from '../../../test/test-data-generators.js';
+import {FILE} from '../gr-diff/gr-diff-line.js';
 
 const basicFixture = fixtureFromElement('gr-diff-host');
 
@@ -254,16 +255,20 @@
   });
 
   test('thread-discard handling', () => {
-    const threads = element._createThreads([
+    const threads = createCommentThreads([
       {
         id: 4711,
         __commentSide: 'left',
         updated: '2015-12-20 15:01:20.396000000',
+        patch_set: 1,
+        path: 'some/path',
       },
       {
         id: 42,
         __commentSide: 'left',
         updated: '2017-12-20 15:01:20.396000000',
+        patch_set: 1,
+        path: 'some/path',
       },
     ]);
     element._parentIndex = 1;
@@ -1161,6 +1166,8 @@
         updated: '2015-12-23 15:00:20.396000000',
         line: 1,
         __commentSide: 'left',
+        patch_set: 1,
+        path: 'some/path',
       }, {
         id: 'jacks_reply',
         message: 'i like you, too',
@@ -1168,6 +1175,8 @@
         __commentSide: 'left',
         line: 1,
         in_reply_to: 'sallys_confession',
+        patch_set: 1,
+        path: 'some/path',
       },
       {
         id: 'new_draft',
@@ -1175,25 +1184,27 @@
         __commentSide: 'left',
         __draft: true,
         updated: '2015-12-20 15:01:20.396000000',
+        patch_set: 1,
+        path: 'some/path',
       },
     ];
 
-    const actualThreads = element._createThreads(comments);
+    const actualThreads = createCommentThreads(comments);
 
     assert.equal(actualThreads.length, 2);
 
-    assert.equal(actualThreads[0].commentSide, 'left');
+    assert.equal(actualThreads[0].diffSide, 'left');
     assert.equal(actualThreads[0].comments.length, 2);
     assert.deepEqual(actualThreads[0].comments[0], comments[0]);
     assert.deepEqual(actualThreads[0].comments[1], comments[1]);
-    assert.equal(actualThreads[0].patchNum, undefined);
-    assert.equal(actualThreads[0].lineNum, 1);
+    assert.equal(actualThreads[0].patchNum, 1);
+    assert.equal(actualThreads[0].line, 1);
 
-    assert.equal(actualThreads[1].commentSide, 'left');
+    assert.equal(actualThreads[1].diffSide, 'left');
     assert.equal(actualThreads[1].comments.length, 1);
     assert.deepEqual(actualThreads[1].comments[0], comments[2]);
-    assert.equal(actualThreads[1].patchNum, undefined);
-    assert.equal(actualThreads[1].lineNum, undefined);
+    assert.equal(actualThreads[1].patchNum, 1);
+    assert.equal(actualThreads[1].line, FILE);
   });
 
   test('_createThreads inherits patchNum and range', () => {
@@ -1208,15 +1219,20 @@
         end_character: 2,
       },
       patch_set: 5,
+      path: '/p',
       __commentSide: 'left',
       line: 1,
     }];
 
     const expectedThreads = [
       {
-        commentSide: 'left',
+        diffSide: 'left',
+        commentSide: CommentSide.REVISION,
+        path: '/p',
+        rootId: 'betsys_confession',
         comments: [{
           id: 'betsys_confession',
+          path: '/p',
           message: 'i like you, jack',
           updated: '2015-12-24 15:00:10.396000000',
           range: {
@@ -1236,13 +1252,12 @@
           end_line: 1,
           end_character: 2,
         },
-        lineNum: 1,
-        isOnParent: false,
+        line: 1,
       },
     ];
 
     assert.deepEqual(
-        element._createThreads(comments),
+        createCommentThreads(comments),
         expectedThreads);
   });
 
@@ -1254,55 +1269,31 @@
             message: 'i like you, jack',
             updated: '2015-12-23 15:00:20.396000000',
             __commentSide: 'left',
+            path: '/p',
           }, {
             id: 'jacks_reply',
             message: 'i like you, too',
             updated: '2015-12-24 15:01:20.396000000',
             __commentSide: 'left',
+            path: '/p',
           },
         ];
-        assert.equal(element._createThreads(comments).length, 2);
-      });
-
-  test('_createThreads derives isOnParent using  side from first comment',
-      () => {
-        const comments = [
-          {
-            id: 'sallys_confession',
-            message: 'i like you, jack',
-            updated: '2015-12-23 15:00:20.396000000',
-            __commentSide: 'left',
-          }, {
-            id: 'jacks_reply',
-            message: 'i like you, too',
-            updated: '2015-12-24 15:01:20.396000000',
-            __commentSide: 'left',
-            in_reply_to: 'sallys_confession',
-          },
-        ];
-
-        assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
-        comments[0].side = 'REVISION';
-        assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
-        comments[0].side = 'PARENT';
-        assert.equal(element._createThreads(comments)[0].isOnParent, true);
+        assert.equal(createCommentThreads(comments).length, 2);
       });
 
   test('_getOrCreateThread', () => {
-    const commentSide = 'left';
+    const diffSide = 'left';
+    const commentSide = CommentSide.PARENT;
 
     assert.isOk(element._getOrCreateThread('2', 3,
-        commentSide, undefined, false));
+        diffSide, commentSide, '/p'));
 
     let threads = dom(element.$.diff)
         .queryDistributedElements('gr-comment-thread');
 
     assert.equal(threads.length, 1);
-    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].commentSide, diffSide);
     assert.equal(threads[0].range, undefined);
-    assert.equal(threads[0].isOnParent, false);
     assert.equal(threads[0].patchNum, 2);
 
     // Try to fetch a thread with a different range.
@@ -1314,63 +1305,62 @@
     };
 
     assert.isOk(element._getOrCreateThread(
-        '3', 1, commentSide, range, true));
+        '3', 1, diffSide, commentSide, '/p', range));
 
     threads = dom(element.$.diff)
         .queryDistributedElements('gr-comment-thread');
 
     assert.equal(threads.length, 2);
-    assert.equal(threads[1].commentSide, commentSide);
+    assert.equal(threads[1].commentSide, diffSide);
     assert.equal(threads[1].range, range);
-    assert.equal(threads[1].isOnParent, true);
     assert.equal(threads[1].patchNum, 3);
   });
 
-  test('thread should use old file path if first created' +
+  test('thread should use old file path if first created ' +
    'on patch set (left) before renaming', () => {
-    const commentSide = 'left';
+    const diffSide = 'left';
     element.file = {basePath: 'file_renamed.txt', path: element.path};
 
     assert.isOk(element._getOrCreateThread('2', 3,
-        commentSide, undefined, /* isOnParent= */ false));
+        diffSide, CommentSide.REVISION, '/p'));
 
     const threads = dom(element.$.diff)
         .queryDistributedElements('gr-comment-thread');
 
     assert.equal(threads.length, 1);
-    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].commentSide, diffSide);
     assert.equal(threads[0].path, element.file.basePath);
   });
 
   test('thread should use new file path if first created' +
    'on patch set (right) after renaming', () => {
-    const commentSide = 'right';
+    const diffSide = 'right';
     element.file = {basePath: 'file_renamed.txt', path: element.path};
 
     assert.isOk(element._getOrCreateThread('2', 3,
-        commentSide, undefined, /* isOnParent= */ false));
+        diffSide, CommentSide.REVISION, '/p'));
 
     const threads = dom(element.$.diff)
         .queryDistributedElements('gr-comment-thread');
 
     assert.equal(threads.length, 1);
-    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].commentSide, diffSide);
     assert.equal(threads[0].path, element.file.path);
   });
 
   test('thread should use new file path if first created' +
    'on patch set (left) but is base', () => {
-    const commentSide = 'left';
+    const diffSide = 'left';
     element.file = {basePath: 'file_renamed.txt', path: element.path};
 
     assert.isOk(element._getOrCreateThread('2', 3,
-        commentSide, undefined, /* isOnParent= */ true));
+        diffSide, CommentSide.PARENT, '/p', undefined));
 
     const threads = dom(element.$.diff)
         .queryDistributedElements('gr-comment-thread');
 
     assert.equal(threads.length, 1);
-    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].commentSide, diffSide);
     assert.equal(threads[0].path, element.file.path);
   });
 
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 9d24b3e..cbee7c5 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
@@ -941,6 +941,23 @@
     this._commitRange = commit && baseCommit ? {commit, baseCommit} : undefined;
   }
 
+  _updateUrlToDiffUrl(lineNum?: number, leftSide?: boolean) {
+    if (!this._change) return;
+    if (!this._patchRange) return;
+    if (!this._changeNum) return;
+    if (!this._path) return;
+    const url = GerritNav.getUrlForDiffById(
+      this._changeNum,
+      this._change.project,
+      this._path,
+      this._patchRange.patchNum,
+      this._patchRange.basePatchNum,
+      lineNum,
+      leftSide
+    );
+    history.replaceState(null, '', url);
+  }
+
   _initPatchRange() {
     let leftSide = false;
     if (!this._change) return;
@@ -991,6 +1008,12 @@
     }
     if (!this._patchRange) throw new Error('Failed to initialize patchRange.');
     this._initLineOfInterestAndCursor(leftSide);
+
+    if (this.params?.commentId) {
+      // url is of type /comment/{commentId} which isn't meaningful
+      this._updateUrlToDiffUrl(this._focusLineNum, leftSide);
+    }
+
     this._commentMap = this._getPaths(this._patchRange);
 
     this._commentsForDiff = this._getCommentsForPath(
@@ -1437,26 +1460,12 @@
     _: Event,
     detail: {side: Side | CommentSide; number: number}
   ) {
-    if (!this._change) return;
-    if (!this._path) return;
-    if (!this._changeNum) return;
-    if (!this._patchRange) return;
-
-    const number = detail.number;
     // for on-comment-anchor-tap side can be PARENT/REVISIONS
     // for on-line-selected side can be left/right
-    const leftSide =
-      detail.side === Side.LEFT || detail.side === CommentSide.PARENT;
-    const url = GerritNav.getUrlForDiffById(
-      this._changeNum,
-      this._change.project,
-      this._path,
-      this._patchRange.patchNum,
-      this._patchRange.basePatchNum,
-      number,
-      leftSide
+    this._updateUrlToDiffUrl(
+      detail.number,
+      detail.side === Side.LEFT || detail.side === CommentSide.PARENT
     );
-    history.replaceState(null, '', url);
   }
 
   _computeDownloadDropdownLinks(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 9a08723..7ef75ed 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -184,6 +184,8 @@
     test('comment route', () => {
       const initLineOfInterestAndCursorStub =
         sinon.stub(element, '_initLineOfInterestAndCursor');
+      const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
+      const replaceStateStub = sinon.stub(history, 'replaceState');
       sinon.stub(element, '_getFiles');
       sinon.stub(element.reporting, 'diffViewDisplayed');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
@@ -213,6 +215,10 @@
         assert.equal(element._focusLineNum, 10);
         assert.equal(element._patchRange.patchNum, 11);
         assert.equal(element._patchRange.basePatchNum, 2);
+
+        assert.isTrue(replaceStateStub.called);
+        assert.isTrue(getUrlStub.calledWithExactly('42', 'test-project',
+            '/COMMIT_MSG', 11, 2, 10, true));
       });
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index d036f27..a63b468 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -52,7 +52,7 @@
   PolymerDomWrapper,
 } from '../../../types/types';
 import {CommentRangeLayer} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
-import {DiffViewMode, Side} from '../../../constants/constants';
+import {DiffViewMode, Side, CommentSide} from '../../../constants/constants';
 import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
 import {FlattenedNodesObserver} from '@polymer/polymer/lib/utils/flattened-nodes-observer';
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
@@ -661,6 +661,7 @@
       lineEl,
       contentEl
     );
+    const commentSide = isOnParent ? CommentSide.PARENT : CommentSide.REVISION;
     this.dispatchEvent(
       new CustomEvent('create-comment', {
         bubbles: true,
@@ -668,9 +669,11 @@
         detail: {
           lineNum,
           side,
+          commentSide,
           patchNum: patchForNewThreads,
-          isOnParent,
           range,
+          path: this.path,
+          isOnParent,
         },
       })
     );
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index 957496c..cb25b81 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -22,6 +22,7 @@
 import {IronOverlayMixin} from '../../../mixins/iron-overlay-mixin/iron-overlay-mixin';
 import {customElement, property} from '@polymer/decorators';
 import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior';
+import {findActiveElement} from '../../../utils/dom-util';
 
 const AWAIT_MAX_ITERS = 10;
 const AWAIT_STEP = 5;
@@ -59,7 +60,9 @@
 
   private _boundHandleClose: () => void = () => super.close();
 
-  private focusableNodes: Node[] | undefined;
+  private focusableNodes?: Node[];
+
+  private returnFocusTo?: HTMLElement;
 
   get _focusableNodes() {
     if (this.focusableNodes) {
@@ -89,6 +92,7 @@
   }
 
   open() {
+    this.returnFocusTo = findActiveElement(document, true) ?? undefined;
     window.addEventListener('popstate', this._boundHandleClose);
     return new Promise((resolve, reject) => {
       super.open.apply(this);
@@ -121,6 +125,10 @@
       );
       this._fullScreenOpen = false;
     }
+    if (this.returnFocusTo) {
+      this.returnFocusTo.focus();
+      this.returnFocusTo = undefined;
+    }
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 0629457..3f3ded2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -253,7 +253,6 @@
 
 interface GetDiffParams {
   [paramName: string]: string | undefined | null | number | boolean;
-  context?: number | 'ALL';
   intraline?: boolean | null;
   whitespace?: IgnoreWhitespaceType;
   parent?: number;
@@ -2525,7 +2524,6 @@
     errFn?: ErrorCallback
   ) {
     const params: GetDiffParams = {
-      context: 'ALL',
       intraline: null,
       whitespace: whitespace || 'IGNORE_NONE',
     };
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index d6b2237..e40412d 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -26,4 +26,5 @@
 export enum KnownExperimentId {
   PATCHSET_COMMENTS = 'UiFeature__patchset_comments',
   NEW_CONTEXT_CONTROLS = 'UiFeature__new_context_controls',
+  CI_REBOOT_CHECKS = 'UiFeature__ci_reboot_checks',
 }
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index e1e002a..5af9bb7 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -26,6 +26,7 @@
 import {CommentSide, Side} from '../constants/constants';
 import {parseDate} from './date-util';
 import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
+import {CommentIdToCommentThreadMap} from '../elements/diff/gr-comment-api/gr-comment-api';
 
 export interface DraftCommentProps {
   __draft?: boolean;
@@ -99,13 +100,56 @@
   });
 }
 
+export function createCommentThreads(comments: UIComment[]) {
+  const sortedComments = sortComments(comments);
+  const threads: CommentThread[] = [];
+  const idThreadMap: CommentIdToCommentThreadMap = {};
+  for (const comment of sortedComments) {
+    if (!comment.id) continue;
+    // If the comment is in reply to another comment, find that comment's
+    // thread and append to it.
+    if (comment.in_reply_to) {
+      const thread = idThreadMap[comment.in_reply_to];
+      if (thread) {
+        thread.comments.push(comment);
+        idThreadMap[comment.id] = thread;
+        continue;
+      }
+    }
+
+    // Otherwise, this comment starts its own thread.
+    if (!comment.__path && !comment.path) {
+      throw new Error('Comment missing required "path".');
+    }
+    const newThread: CommentThread = {
+      comments: [comment],
+      patchNum: comment.patch_set,
+      commentSide: comment.side ?? CommentSide.REVISION,
+      path: comment.__path || comment.path!,
+      line: comment.line,
+      range: comment.range,
+      rootId: comment.id,
+      diffSide: comment.__commentSide,
+    };
+    if (!comment.line && !comment.range) {
+      newThread.line = 'FILE';
+    }
+    threads.push(newThread);
+    idThreadMap[comment.id] = newThread;
+  }
+  return threads;
+}
+
 export interface CommentThread {
   comments: UIComment[];
-  patchNum?: PatchSetNum;
   path: string;
+  commentSide: CommentSide;
+  patchNum?: PatchSetNum;
   line?: LineNumber;
-  rootId: UrlEncodedCommentId;
-  commentSide?: CommentSide;
+  /* rootId is optional since we create a empty comment thread element for
+     drafts and then create the draft which becomes the root */
+  rootId?: UrlEncodedCommentId;
+  diffSide?: Side;
   range?: CommentRange;
 }
 
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 364112b..8d02119 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -234,3 +234,32 @@
   }
   return _sharedApiEl;
 }
+
+// document.activeElement is not enough, because it's not getting activeElement
+// without looking inside of shadow roots. This will find best activeElement.
+export function findActiveElement(
+  root: DocumentOrShadowRoot | null,
+  ignoreDialogs?: boolean
+): HTMLElement | null {
+  if (root === null) {
+    return null;
+  }
+  if (
+    ignoreDialogs &&
+    root.activeElement &&
+    root.activeElement.nodeName.toUpperCase().includes('DIALOG')
+  ) {
+    return null;
+  }
+  if (root.activeElement?.shadowRoot?.activeElement) {
+    return findActiveElement(root.activeElement.shadowRoot);
+  }
+  if (!root.activeElement) {
+    return null;
+  }
+  // We block some elements
+  if ('BODY' === root.activeElement.nodeName.toUpperCase()) {
+    return null;
+  }
+  return root.activeElement as HTMLElement;
+}
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index eaf2017..e6487a7 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -200,8 +200,8 @@
 		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}tslib/tslib.es6.js';"))
 
 		// 'lit-element' imports and exports have to be resolved to 'lit-element/lit-element.js'.
-		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)lit-element.js';$")
-		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}lit-element/lit-element.js';"))
+		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)lit-(element|html).js';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}lit-${3}/lit-${3}.js';"))
 
 		if strings.HasSuffix(normalizedContentPath, "/node_modules/page/page.js") {
 			// Can't import page.js directly, because this is undefined.