Merge "Ensure existing reviewers are removed as part of ReviewInput"
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 e303dad..5504877 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
@@ -40,8 +40,9 @@
 } from '../../../constants/constants';
 import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {
-  accountKey,
   accountOrGroupKey,
+  isReviewerOrCC,
+  mapReviewer,
   removeServiceUsers,
 } from '../../../utils/account-util';
 import {getDisplayName} from '../../../utils/display-name-util';
@@ -74,7 +75,6 @@
   ParsedJSON,
   PatchSetNum,
   ProjectInfo,
-  ReviewerInput,
   Reviewers,
   ReviewInput,
   ReviewResult,
@@ -542,18 +542,6 @@
     }
   }
 
-  _mapReviewer(addition: AccountAddition): ReviewerInput {
-    if (addition.account) {
-      return {reviewer: accountKey(addition.account)};
-    }
-    if (addition.group) {
-      const reviewer = decodeURIComponent(addition.group.id) as GroupId;
-      const confirmed = addition.group.confirmed;
-      return {reviewer, confirmed};
-    }
-    throw new Error('Reviewer must be either an account or a group.');
-  }
-
   send(includeComments: boolean, startReview: boolean) {
     this.reporting.time(Timing.SEND_REPLY);
     const labels = this.$.labelScores.getLabelValues();
@@ -606,16 +594,24 @@
       state?: ReviewerState
     ) => {
       additions.forEach(addition => {
-        const reviewer = this._mapReviewer(addition);
+        const reviewer = mapReviewer(addition);
         if (state) reviewer.state = state;
         reviewInput.reviewers?.push(reviewer);
       });
     };
     reviewInput.reviewers = [];
+    assertIsDefined(this.change, 'change');
+    const change = this.change;
     addToReviewInput(this.$.reviewers.additions(), ReviewerState.REVIEWER);
     addToReviewInput(this.$.ccs.additions(), ReviewerState.CC);
-    addToReviewInput(this.$.reviewers.removals(), ReviewerState.REMOVED);
-    addToReviewInput(this.$.ccs.removals(), ReviewerState.REMOVED);
+    addToReviewInput(
+      this.$.reviewers.removals().filter(r => isReviewerOrCC(change, r)),
+      ReviewerState.REMOVED
+    );
+    addToReviewInput(
+      this.$.ccs.removals().filter(r => isReviewerOrCC(change, r)),
+      ReviewerState.REMOVED
+    );
 
     this.disabled = true;
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index b51eb5f..f5474d8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -1075,6 +1075,11 @@
     element._reviewers = [reviewer1, reviewer2];
     element._ccs = [cc1, cc2, cc3];
 
+    element.change.reviewers = {
+      [ReviewerState.CC]: [],
+      [ReviewerState.REVIEWER]: [{_account_id: 33}],
+    };
+
     const mutations = [];
 
     stubSaveReview(review => mutations.push(...review.reviewers));
@@ -1128,7 +1133,7 @@
 
     // Send and purge and verify moves, delete cc3.
     await element.send();
-    expect(mutations).to.have.lengthOf(7);
+    expect(mutations).to.have.lengthOf(5);
     expect(mutations[0]).to.deep.equal(mapReviewer(cc1,
         ReviewerState.REVIEWER));
     expect(mutations[1]).to.deep.equal(mapReviewer(cc2,
@@ -1138,13 +1143,9 @@
     expect(mutations[3]).to.deep.equal(mapReviewer(reviewer2,
         ReviewerState.CC));
 
-    // 3 remove events stored
+    // Only 1 account was initially part of the change
     expect(mutations[4]).to.deep.equal({reviewer: 33, state:
         ReviewerState.REMOVED});
-    expect(mutations[5]).to.deep.equal({reviewer: 35, state:
-        ReviewerState.REMOVED});
-    expect(mutations[6]).to.deep.equal({reviewer: 37, state:
-        ReviewerState.REMOVED});
   });
 
   test('emits cancel on esc key', () => {
diff --git a/polygerrit-ui/app/utils/account-util.ts b/polygerrit-ui/app/utils/account-util.ts
index ff37608..2a509d3 100644
--- a/polygerrit-ui/app/utils/account-util.ts
+++ b/polygerrit-ui/app/utils/account-util.ts
@@ -18,13 +18,17 @@
 import {
   AccountId,
   AccountInfo,
+  ChangeInfo,
   EmailAddress,
+  GroupId,
   GroupInfo,
   isAccount,
   isGroup,
+  ReviewerInput,
 } from '../types/common';
-import {AccountTag} from '../constants/constants';
+import {AccountTag, ReviewerState} from '../constants/constants';
 import {assertNever} from './common-util';
+import {AccountAddition} from '../elements/shared/gr-account-list/gr-account-list';
 
 export function accountKey(account: AccountInfo): AccountId | EmailAddress {
   if (account._account_id) return account._account_id;
@@ -32,6 +36,30 @@
   throw new Error('Account has neither _account_id nor email.');
 }
 
+export function mapReviewer(addition: AccountAddition): ReviewerInput {
+  if (addition.account) {
+    return {reviewer: accountKey(addition.account)};
+  }
+  if (addition.group) {
+    const reviewer = decodeURIComponent(addition.group.id) as GroupId;
+    const confirmed = addition.group.confirmed;
+    return {reviewer, confirmed};
+  }
+  throw new Error('Reviewer must be either an account or a group.');
+}
+
+export function isReviewerOrCC(
+  change: ChangeInfo,
+  reviewerAddition: AccountAddition
+): boolean {
+  const reviewers = [
+    ...(change.reviewers[ReviewerState.CC] ?? []),
+    ...(change.reviewers[ReviewerState.REVIEWER] ?? []),
+  ];
+  const reviewer = mapReviewer(reviewerAddition);
+  return reviewers.some(r => accountOrGroupKey(r) === reviewer.reviewer);
+}
+
 export function isServiceUser(account?: AccountInfo): boolean {
   return !!account?.tags?.includes(AccountTag.SERVICE_USER);
 }