Merge "Add filter for mentioned threads in Comments Tab"
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 2b35d6b..f4375a6 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
@@ -582,7 +582,7 @@
     return html` <gr-summary-chip
       class="mentionSummary"
       styleType=${SummaryChipStyles.WARNING}
-      category=${CommentTabState.UNRESOLVED}
+      category=${CommentTabState.MENTIONS}
       icon="alternate_email"
     >
       ${pluralize(this.mentionCount, 'mention')}</gr-summary-chip
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 9aa2117..dc8135e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -134,7 +134,7 @@
     // Resolved threads are ignored hence mention chip count is 2
     expect(mentionSummary).dom.to.equal(/* HTML */ `
       <gr-summary-chip
-        category="unresolved"
+        category="mentions"
         class="mentionSummary"
         icon="alternate_email"
         styletype="warning"
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index a56d87d..fec995c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -17,9 +17,11 @@
 import {
   CommentThread,
   getCommentAuthors,
+  getMentionedThreads,
   hasHumanReply,
   isDraft,
   isDraftThread,
+  isMentionedThread,
   isRobotThread,
   isUnresolved,
   lastUpdated,
@@ -40,6 +42,7 @@
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {Interaction} from '../../../constants/reporting';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 enum SortDropdownState {
   TIMESTAMP = 'Latest timestamp',
@@ -192,10 +195,15 @@
   @state()
   draftsOnly = false;
 
+  @state()
+  mentionsOnly = false;
+
   private readonly getChangeModel = resolve(this, changeModelToken);
 
   private readonly reporting = getAppContext().reportingService;
 
+  private readonly flagsService = getAppContext().flagsService;
+
   private readonly userModel = getAppContext().userModel;
 
   constructor() {
@@ -228,6 +236,9 @@
 
   private onCommentTabStateUpdate() {
     switch (this.commentTabState?.commentTab) {
+      case CommentTabState.MENTIONS:
+        this.handleOnlyMentions();
+        break;
       case CommentTabState.UNRESOLVED:
         this.handleOnlyUnresolved();
         break;
@@ -452,6 +463,7 @@
   }
 
   private getCommentsDropdownValue() {
+    if (this.mentionsOnly) return CommentTabState.MENTIONS;
     if (this.draftsOnly) return CommentTabState.DRAFTS;
     if (this.unresolvedOnly) return CommentTabState.UNRESOLVED;
     return CommentTabState.SHOW_ALL;
@@ -473,6 +485,14 @@
       value: CommentTabState.UNRESOLVED,
     });
     if (this.account) {
+      if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
+        items.push({
+          text: `Mentions (${
+            getMentionedThreads(threads, this.account).length
+          })`,
+          value: CommentTabState.MENTIONS,
+        });
+      }
       items.push({
         text: `Drafts (${threads.filter(isDraftThread).length})`,
         value: CommentTabState.DRAFTS,
@@ -504,6 +524,9 @@
       case CommentTabState.UNRESOLVED:
         this.handleOnlyUnresolved();
         break;
+      case CommentTabState.MENTIONS:
+        this.handleOnlyMentions();
+        break;
       case CommentTabState.DRAFTS:
         this.handleOnlyDrafts();
         break;
@@ -566,6 +589,9 @@
       if (isRobotThread(thread) && !hasHumanReply(thread)) return false;
     }
 
+    if (this.mentionsOnly && !isMentionedThread(thread, this.account))
+      return false;
+
     if (this.draftsOnly && !isDraftThread(thread)) return false;
     if (this.unresolvedOnly && !isUnresolved(thread)) return false;
 
@@ -575,16 +601,25 @@
   private handleOnlyUnresolved() {
     this.unresolvedOnly = true;
     this.draftsOnly = false;
+    this.mentionsOnly = false;
+  }
+
+  private handleOnlyMentions() {
+    this.mentionsOnly = true;
+    this.unresolvedOnly = true;
+    this.draftsOnly = false;
   }
 
   private handleOnlyDrafts() {
     this.draftsOnly = true;
     this.unresolvedOnly = false;
+    this.mentionsOnly = false;
   }
 
   private handleAllComments() {
     this.draftsOnly = false;
     this.unresolvedOnly = false;
+    this.mentionsOnly = false;
   }
 
   private queryThreadElement(rootId: string): GrCommentThread | undefined {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
index ed06325..f6de258 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
@@ -12,16 +12,23 @@
   GrThreadList,
   __testOnly_SortDropdownState,
 } from './gr-thread-list';
-import {queryAll} from '../../../test/test-utils';
+import {queryAll, stubFlags} from '../../../test/test-utils';
 import {accountOrGroupKey} from '../../../utils/account-util';
 import {tap} from '@polymer/iron-test-helpers/mock-interactions';
 import {
   createAccountDetailWithId,
+  createComment,
+  createCommentThread,
   createDraft,
   createParsedChange,
   createThread,
 } from '../../../test/test-data-generators';
-import {AccountId, NumericChangeId, Timestamp} from '../../../api/rest-api';
+import {
+  AccountId,
+  EmailAddress,
+  NumericChangeId,
+  Timestamp,
+} from '../../../api/rest-api';
 import {
   RobotId,
   UrlEncodedCommentId,
@@ -498,6 +505,54 @@
     assert.equal(element.getDisplayedThreads().length, 2);
   });
 
+  suite('mention threads', () => {
+    let mentionedThreads: CommentThread[];
+    setup(async () => {
+      stubFlags('isEnabled').returns(true);
+      mentionedThreads = [
+        createCommentThread([
+          {
+            ...createComment(),
+            message: 'random text with no emails',
+          },
+        ]),
+        // Resolved thread does not contribute to the count
+        createCommentThread([
+          {
+            ...createComment(),
+            message: '@abcd@def.com please take a look',
+          },
+          {
+            ...createComment(),
+            message: '@abcd@def.com please take a look again at this',
+          },
+        ]),
+        createCommentThread([
+          {
+            ...createComment(),
+            message: '@abcd@def.com this is important',
+            unresolved: true,
+          },
+        ]),
+      ];
+      element.account!.email = 'abcd@def.com' as EmailAddress;
+      element.threads.push(...mentionedThreads);
+      element.requestUpdate();
+      await element.updateComplete;
+    });
+
+    test('mentions filter', async () => {
+      const filterDropdown = queryAndAssert<GrDropdownList>(
+        element,
+        '#filterDropdown'
+      );
+      filterDropdown.value = CommentTabState.MENTIONS;
+      await filterDropdown.updateComplete;
+      await element.updateComplete;
+      assert.deepEqual(element.getDisplayedThreads(), [mentionedThreads[2]]);
+    });
+  });
+
   suite('hideDropdown', () => {
     test('header hidden for hideDropdown=true', async () => {
       element.hideDropdown = true;
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index b732295..2a568dc 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -225,6 +225,7 @@
   UNRESOLVED = 'unresolved',
   DRAFTS = 'drafts',
   SHOW_ALL = 'show all',
+  MENTIONS = 'mentions',
 }
 export interface ChecksTabState {
   statusOrCategory?: RunStatus | Category;
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 911c821..f531698 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -289,6 +289,16 @@
   return isDraft(getLastComment(thread));
 }
 
+export function isMentionedThread(
+  thread: CommentThread,
+  account?: AccountInfo
+) {
+  if (!account?.email) return false;
+  return getMentionedUsers(thread)
+    .map(v => v.email)
+    .includes(account.email);
+}
+
 export function isRobotThread(thread: CommentThread): boolean {
   return isRobot(getFirstComment(thread));
 }