Merge "Swap remove/add delta counts"
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index eef2a44..04887ad 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -74,8 +74,6 @@
 
 @customElement('gr-admin-view')
 export class GrAdminView extends LitElement {
-  private account?: AccountDetailInfo;
-
   @state()
   view?: GerritView;
 
@@ -459,13 +457,18 @@
   async reload() {
     try {
       this.reloading = true;
+      // There is async barrier inside reload function, we need to clear
+      // subsectionLinks now, because the element might render while waiting for
+      // RestApi responses breaking the invariant that this.view is part of
+      // subsectionLinks if non-empty.
+      this.subsectionLinks = [];
       const promises: [Promise<AccountDetailInfo | undefined>, Promise<void>] =
         [
           this.restApiService.getAccount(),
           this.getPluginLoader().awaitPluginsLoaded(),
         ];
       const result = await Promise.all(promises);
-      this.account = result[0];
+      const account = result[0];
       let options: AdminNavLinksOption | undefined = undefined;
       if (this.repoName) {
         options = {repoName: this.repoName};
@@ -484,7 +487,7 @@
       }
 
       const res = await getAdminLinks(
-        this.account,
+        account,
         () =>
           this.restApiService.getAccountCapabilities().then(capabilities => {
             if (!capabilities) {
@@ -501,7 +504,6 @@
         : '';
 
       if (!res.expandedSection) {
-        this.subsectionLinks = [];
         return;
       }
       this.subsectionLinks = [res.expandedSection]
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index ee4a179..8cf4e05 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -111,10 +111,10 @@
     assert.isNotOk(element.filteredLinks![0].subsection);
 
     // Groups
-    assert.isNotOk(element.filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![1].subsection);
 
     // Plugins
-    assert.isNotOk(element.filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![2].subsection);
   });
 
   test('filteredLinks non admin authenticated', async () => {
@@ -123,7 +123,7 @@
     // Repos
     assert.isNotOk(element.filteredLinks![0].subsection);
     // Groups
-    assert.isNotOk(element.filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![1].subsection);
   });
 
   test('filteredLinks non admin unathenticated', async () => {
@@ -262,6 +262,7 @@
 
   test('Needs reload when changing from repo to group', async () => {
     element.repoName = 'Test Repo' as RepoName;
+    element.view = GerritView.REPO;
     stubRestApi('getAccount').returns(
       Promise.resolve({
         name: 'test-user',
@@ -278,9 +279,12 @@
     const groupId = '1' as GroupId;
     element.view = GerritView.GROUP;
     element.groupViewState = {groupId, view: GerritView.GROUP};
+    // Check for reload before update. This would normally be done as part of
+    // subscribe method that updates the view/viewState.
+    assert.isTrue(element.needsReload());
+    element.reload();
     await element.updateComplete;
 
-    assert.isTrue(element.needsReload());
     assert.equal(element.groupId, groupId);
   });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 57f9ee6..3f99416 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -117,10 +117,12 @@
       return;
     }
 
-    this.restApiService.getAccountDetails(userId).then(details => {
-      this._accountDetails = details ?? undefined;
-      this._status = details?.status ?? '';
-    });
+    this.restApiService
+      .getAccountDetails(userId, () => {})
+      .then(details => {
+        this._accountDetails = details ?? undefined;
+        this._status = details?.status ?? '';
+      });
   }
 
   _computeDetail(
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 fa44e18..80b762f 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
@@ -330,6 +330,12 @@
   newAttentionSet: Set<UserId> = new Set();
 
   @state()
+  manuallyAddedAttentionSet: Set<UserId> = new Set();
+
+  @state()
+  manuallyDeletedAttentionSet: Set<UserId> = new Set();
+
+  @state()
   patchsetLevelDraftIsResolved = true;
 
   @state()
@@ -477,12 +483,14 @@
         #pluginMessage:empty {
           display: none;
         }
-        .attention .edit-attention-button {
+        .edit-attention-button {
           vertical-align: top;
           --gr-button-padding: 0px 4px;
         }
-        .attention .edit-attention-button gr-icon {
+        .edit-attention-button gr-icon {
           color: inherit;
+          
+        .attentionSummary .edit-attention-button gr-icon {
           /* The line-height:26px hack (see below) requires us to do this.
            Normally the gr-icon would account for a proper positioning
            within the standard line-height:20px context. */
@@ -999,30 +1007,9 @@
                 )}
               `
             )}
-            <gr-tooltip-content
-              has-tooltip
-              title=${this.computeAttentionButtonTitle()}
-            >
-              <gr-button
-                class="edit-attention-button"
-                @click=${this.handleAttentionModify}
-                ?disabled=${this.isSendDisabled()}
-                link
-                position-below
-                data-label="Edit"
-                data-action-type="change"
-                data-action-key="edit"
-                role="button"
-                tabindex="0"
-              >
-                <div>
-                  <gr-icon icon="edit" filled small></gr-icon>
-                  <span>Modify</span>
-                </div>
-              </gr-button>
-            </gr-tooltip-content>
           </div>
           <div>
+            ${this.renderModifyAttentionSetButton()}
             <a
               href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
               target="_blank"
@@ -1036,6 +1023,30 @@
     `;
   }
 
+  private renderModifyAttentionSetButton() {
+    return html` <gr-button
+      class="edit-attention-button"
+      @click=${this.toggleAttentionModify}
+      ?disabled=${this.isSendDisabled()}
+      link
+      position-below
+      data-label="Edit"
+      data-action-type="change"
+      data-action-key="edit"
+      role="button"
+      tabindex="0"
+    >
+      <div>
+        <gr-icon
+          icon=${this.attentionExpanded ? 'expand_circle_up' : 'edit'}
+          filled
+          small
+        ></gr-icon>
+        <span>${this.attentionExpanded ? 'Collapse' : 'Modify'}</span>
+      </div>
+    </gr-button>`;
+  }
+
   private renderAttentionDetailsSection() {
     if (!this.attentionExpanded) return;
     return html`
@@ -1044,8 +1055,10 @@
           <div>
             <span>Modify attention to</span>
           </div>
+
           <div></div>
           <div>
+            ${this.renderModifyAttentionSetButton()}
             <a
               href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
               target="_blank"
@@ -1137,7 +1150,7 @@
           `
         )}
         ${when(
-          this.computeShowAttentionTip(),
+          this.computeShowAttentionTip(3),
           () => html`
             <div class="attentionTip">
               <gr-icon icon="lightbulb"></gr-icon>
@@ -1533,8 +1546,8 @@
     this.reviewers = getAccounts(ReviewerState.REVIEWER);
   }
 
-  handleAttentionModify() {
-    this.attentionExpanded = true;
+  toggleAttentionModify() {
+    this.attentionExpanded = !this.attentionExpanded;
   }
 
   onAttentionExpandedChange() {
@@ -1543,13 +1556,6 @@
     fire(this, 'iron-resize', {});
   }
 
-  computeAttentionButtonTitle() {
-    return this.isSendDisabled()
-      ? 'Modify the attention set by adding a comment or use the account ' +
-          'hovercard in the change page.'
-      : 'Edit attention set changes';
-  }
-
   handleAttentionClick(e: Event) {
     const targetAccount = (e.target as GrAccountChip)?.account;
     if (!targetAccount) return;
@@ -1561,11 +1567,15 @@
 
     if (this.newAttentionSet.has(id)) {
       this.newAttentionSet.delete(id);
+      this.manuallyAddedAttentionSet.delete(id);
+      this.manuallyDeletedAttentionSet.add(id);
       this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
         action: `REMOVE${self}${role}`,
       });
     } else {
       this.newAttentionSet.add(id);
+      this.manuallyAddedAttentionSet.add(id);
+      this.manuallyDeletedAttentionSet.delete(id);
       this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
         action: `ADD${self}${role}`,
       });
@@ -1655,18 +1665,24 @@
       .map(a => getUserId(a))
       .filter(id => !!id);
     this.newAttentionSet = new Set([
-      ...[...newAttention].filter(id => allAccountIds.includes(id)),
+      ...this.manuallyAddedAttentionSet,
+      ...[...newAttention].filter(
+        id =>
+          allAccountIds.includes(id) &&
+          !this.manuallyDeletedAttentionSet.has(id)
+      ),
     ]);
-
-    this.attentionExpanded = this.computeShowAttentionTip();
+    // Possibly expand if need be, never collapse as this is jarring to the user.
+    this.attentionExpanded =
+      this.attentionExpanded || this.computeShowAttentionTip(1);
   }
 
-  computeShowAttentionTip() {
+  computeShowAttentionTip(minimum: number) {
     if (!this.currentAttentionSet || !this.newAttentionSet) return false;
     const addedIds = [...this.newAttentionSet].filter(
       id => !this.currentAttentionSet.has(id)
     );
-    return this.isOwner && addedIds.length > 2;
+    return this.isOwner && addedIds.length >= minimum;
   }
 
   computeCommentAccounts(threads: CommentThread[]) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 3c3c246..5766d80 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -255,30 +255,25 @@
                 <div class="attentionSummary">
                   <div>
                     <span> No changes to the attention set. </span>
-                    <gr-tooltip-content
-                      has-tooltip=""
-                      title="Modify the attention set by adding a comment or use the account hovercard in the change page."
-                    >
-                      <gr-button
-                        aria-disabled="true"
-                        disabled=""
-                        class="edit-attention-button"
-                        data-action-key="edit"
-                        data-action-type="change"
-                        data-label="Edit"
-                        link=""
-                        position-below=""
-                        role="button"
-                        tabindex="-1"
-                      >
-                        <div>
-                          <gr-icon icon="edit" filled small></gr-icon>
-                          <span>Modify</span>
-                        </div>
-                      </gr-button>
-                    </gr-tooltip-content>
                   </div>
                   <div>
+                    <gr-button
+                      aria-disabled="true"
+                      disabled=""
+                      class="edit-attention-button"
+                      data-action-key="edit"
+                      data-action-type="change"
+                      data-label="Edit"
+                      link=""
+                      position-below=""
+                      role="button"
+                      tabindex="-1"
+                    >
+                      <div>
+                        <gr-icon icon="edit" filled small></gr-icon>
+                        <span>Modify</span>
+                      </div>
+                    </gr-button>
                     <a
                       href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
                       target="_blank"
@@ -1732,19 +1727,17 @@
     element._ccs = [...element.ccs, makeAccount()];
     await element.updateComplete;
 
-    // The 'attention modified' section collapses and resets when reviewers or
-    // ccs change.
-    assert.isFalse(element.attentionExpanded);
-
-    queryAndAssert<GrButton>(element, '.edit-attention-button').click();
-    await element.updateComplete;
-
     assert.isTrue(element.attentionExpanded);
     accountLabels = Array.from(
       queryAll(element, '.attention-detail gr-account-label')
     );
     assert.equal(accountLabels.length, 7);
 
+    // Verify that toggling the attention-set-button collapses.
+    queryAndAssert<GrButton>(element, '.edit-attention-button').click();
+    await element.updateComplete;
+    assert.isFalse(element.attentionExpanded);
+
     element.reviewers.pop();
     element.reviewers.pop();
     element._ccs.pop();
@@ -2556,6 +2549,59 @@
     });
   });
 
+  test('manually added users are not lost when view updates.', async () => {
+    assert.sameMembers([...element.newAttentionSet], []);
+
+    element.reviewers = [
+      createAccountWithId(1),
+      createAccountWithId(2),
+      createAccountWithId(3),
+    ];
+    element.patchsetLevelDraftMessage = 'abc';
+
+    await element.updateComplete;
+    assert.sameMembers(
+      [...element.newAttentionSet],
+      [2 as AccountId, 3 as AccountId, 999 as AccountId]
+    );
+
+    const modifyButton = queryAndAssert<GrButton>(
+      element,
+      '.edit-attention-button'
+    );
+
+    modifyButton.click();
+    assert.isTrue(element.attentionExpanded);
+    await element.updateComplete;
+
+    const accountsChips = Array.from(
+      queryAll<GrAccountLabel>(element, '.attention-detail gr-account-label')
+    );
+    assert.equal(accountsChips.length, 4);
+    for (let i = 0; i < 4; ++i) {
+      if (accountsChips[i].account?._account_id === 1) {
+        accountsChips[i].click();
+        break;
+      }
+    }
+
+    await element.updateComplete;
+
+    assert.sameMembers(
+      [...element.newAttentionSet],
+      [1 as AccountId, 2 as AccountId, 3 as AccountId, 999 as AccountId]
+    );
+
+    // Doesn't get reset when message changes.
+    element.patchsetLevelDraftMessage = 'def';
+    await element.updateComplete;
+
+    assert.sameMembers(
+      [...element.newAttentionSet],
+      [1 as AccountId, 2 as AccountId, 3 as AccountId, 999 as AccountId]
+    );
+  });
+
   suite('mention users', () => {
     setup(async () => {
       element.account = createAccountWithId(1);
@@ -2692,6 +2738,13 @@
       await element.updateComplete;
 
       assert.sameMembers([...element.newAttentionSet], [999 as AccountId]);
+
+      // Random update
+      element.patchsetLevelDraftMessage = 'abc';
+      await element.updateComplete;
+
+      assert.sameMembers([...element.newAttentionSet], [999 as AccountId]);
+      element.patchsetLevelDraftMessage = 'abc';
     });
 
     test('mention user who is already CCed', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index b6af123..3dcba9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -187,8 +187,11 @@
   }
 
   protected override willUpdate(changedProperties: PropertyValues): void {
+    if (changedProperties.has('value') || changedProperties.has('items')) {
+      this.updateText();
+    }
     if (changedProperties.has('value')) {
-      this.handleValueChange();
+      fireNoBubble(this, 'value-change', {value: this.value});
     }
   }
 
@@ -307,7 +310,7 @@
     }, 1);
   }
 
-  private handleValueChange() {
+  private updateText() {
     if (this.value === undefined || this.items === undefined) {
       return;
     }
@@ -318,7 +321,6 @@
     this.text = selectedObj.triggerText
       ? selectedObj.triggerText
       : selectedObj.text;
-    fireNoBubble(this, 'value-change', {value: this.value});
   }
 
   /**