diff --git a/src/renderer/components/filters/__snapshots__/SearchFilterSuggestions.test.tsx.snap b/src/renderer/components/filters/__snapshots__/SearchFilterSuggestions.test.tsx.snap
index bda3a4ad3..154831e99 100644
--- a/src/renderer/components/filters/__snapshots__/SearchFilterSuggestions.test.tsx.snap
+++ b/src/renderer/components/filters/__snapshots__/SearchFilterSuggestions.test.tsx.snap
@@ -126,7 +126,29 @@ exports[`renderer/components/filters/SearchFilterSuggestions.tsx > should render
- filter by notification author
+ filter by thread author
+
+
+
+
+
+
+ commenter:
+
+
+ filter by latest comment author
diff --git a/src/renderer/types.ts b/src/renderer/types.ts
index e004a5c04..933b5f495 100644
--- a/src/renderer/types.ts
+++ b/src/renderer/types.ts
@@ -385,8 +385,16 @@ export interface GitifySubject {
number?: number;
/** Parsed state */
state?: GitifyNotificationState;
- /** Latest comment/PR author */
+ /**
+ * Identity shown for this notification in the UI (avatar, user-type filter).
+ * Resolves to the most recent actor: the latest commenter, falling back to
+ * the author.
+ */
user?: GitifyNotificationUser;
+ /** Author who created the thread (pull request/issue/discussion/release/commit) */
+ author?: GitifyNotificationUser;
+ /** Author of the latest comment, when the subject has comments */
+ commenter?: GitifyNotificationUser;
/** PR review states & reviewers */
reviews?: GitifyPullRequestReview[];
/** PRs closing issues */
diff --git a/src/renderer/utils/forges/github/handlers/commit.test.ts b/src/renderer/utils/forges/github/handlers/commit.test.ts
index 4ec23c114..4eff25f2c 100644
--- a/src/renderer/utils/forges/github/handlers/commit.test.ts
+++ b/src/renderer/utils/forges/github/handlers/commit.test.ts
@@ -45,6 +45,18 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => {
avatarUrl: mockCommenter.avatar_url,
type: mockCommenter.type,
},
+ author: {
+ login: mockAuthor.login,
+ htmlUrl: mockAuthor.html_url,
+ avatarUrl: mockAuthor.avatar_url,
+ type: mockAuthor.type,
+ },
+ commenter: {
+ login: mockCommenter.login,
+ htmlUrl: mockCommenter.html_url,
+ avatarUrl: mockCommenter.avatar_url,
+ type: mockCommenter.type,
+ },
});
});
@@ -70,9 +82,34 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => {
avatarUrl: mockAuthor.avatar_url,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ htmlUrl: mockAuthor.html_url,
+ avatarUrl: mockAuthor.avatar_url,
+ type: mockAuthor.type,
+ },
});
});
+ it('leaves roles undefined when the commit has no linked GitHub user', async () => {
+ const mockNotification = mockPartialGitifyNotification({
+ title: 'This is a commit with comments',
+ type: 'Commit',
+ url: 'https://api.github.com/repos/gitify-app/notifications-test/commits/d2a86d80e3d24ea9510d5de6c147e53c30f313a8' as Link,
+ latestCommentUrl: null,
+ });
+
+ getCommitSpy.mockResolvedValue({
+ author: null,
+ } as GetCommitResponse);
+
+ const result = await commitHandler.enrich(mockNotification, mockSettings);
+
+ expect(result.user).toBeUndefined();
+ expect(result.author).toBeUndefined();
+ expect(result.commenter).toBeUndefined();
+ });
+
it('return early if commit state filtered', async () => {
useFiltersStore.setState({ states: ['closed'] });
diff --git a/src/renderer/utils/forges/github/handlers/commit.ts b/src/renderer/utils/forges/github/handlers/commit.ts
index ece27a041..d1fe77b9f 100644
--- a/src/renderer/utils/forges/github/handlers/commit.ts
+++ b/src/renderer/utils/forges/github/handlers/commit.ts
@@ -11,11 +11,26 @@ import type {
Link,
SettingsState,
} from '../../../../types';
+import type { RawUser } from '../types';
import { isStateFilteredOut } from '../../../notifications/filters/filter';
import { getCommit, getCommitComment } from '../client';
import { DefaultHandler } from './default';
-import { getNotificationAuthor } from './utils';
+
+function toNotificationUser(
+ user: RawUser | Record | null | undefined,
+): GitifyNotificationUser | undefined {
+ if (!user || !('login' in user)) {
+ return undefined;
+ }
+
+ return {
+ login: user.login,
+ avatarUrl: user.avatar_url as Link,
+ htmlUrl: user.html_url as Link,
+ type: user.type as GitifyNotificationUser['type'],
+ };
+}
class CommitHandler extends DefaultHandler {
override async enrich(
@@ -29,34 +44,31 @@ class CommitHandler extends DefaultHandler {
return {};
}
- let user: GitifyNotificationUser;
+ // Always resolve the commit author; additionally resolve the latest
+ // comment author when the notification points at a comment. Both calls run
+ // in parallel so populating both roles costs no extra latency.
+ let author: GitifyNotificationUser | undefined;
+ let commenter: GitifyNotificationUser | undefined;
if (notification.subject.latestCommentUrl) {
- const commitComment = await getCommitComment(
- notification.account,
- notification.subject.latestCommentUrl,
- );
+ const [commit, commitComment] = await Promise.all([
+ getCommit(notification.account, notification.subject.url!),
+ getCommitComment(notification.account, notification.subject.latestCommentUrl),
+ ]);
- user = {
- login: commitComment.user!.login,
- avatarUrl: commitComment.user!.avatar_url as Link,
- htmlUrl: commitComment.user!.html_url as Link,
- type: commitComment.user!.type as GitifyNotificationUser['type'],
- };
+ author = toNotificationUser(commit.author);
+ commenter = toNotificationUser(commitComment.user);
} else {
const commit = await getCommit(notification.account, notification.subject.url!);
- user = {
- login: commit.author!.login,
- avatarUrl: commit.author!.avatar_url as Link,
- htmlUrl: commit.author!.html_url as Link,
- type: commit.author!.type as GitifyNotificationUser['type'],
- };
+ author = toNotificationUser(commit.author);
}
return {
state: commitState,
- user: getNotificationAuthor([user]),
+ user: commenter ?? author,
+ author: author,
+ commenter: commenter,
};
}
diff --git a/src/renderer/utils/forges/github/handlers/discussion.test.ts b/src/renderer/utils/forges/github/handlers/discussion.test.ts
index 6e8f0fe41..3267280e5 100644
--- a/src/renderer/utils/forges/github/handlers/discussion.test.ts
+++ b/src/renderer/utils/forges/github/handlers/discussion.test.ts
@@ -58,6 +58,12 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
labels: [],
htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123' as Link,
@@ -86,6 +92,12 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
labels: [],
htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123' as Link,
@@ -117,6 +129,12 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
labels: [],
htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123' as Link,
@@ -153,6 +171,12 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
labels: [{ name: 'enhancement', color: '0e8a16' }],
htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123' as Link,
@@ -199,6 +223,18 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockCommenter.htmlUrl,
type: mockCommenter.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
+ commenter: {
+ login: mockCommenter.login,
+ avatarUrl: mockCommenter.avatarUrl,
+ htmlUrl: mockCommenter.htmlUrl,
+ type: mockCommenter.type,
+ },
commentCount: 1,
labels: [],
htmlUrl:
@@ -256,6 +292,18 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => {
htmlUrl: mockReplier.htmlUrl,
type: mockReplier.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
+ commenter: {
+ login: mockReplier.login,
+ avatarUrl: mockReplier.avatarUrl,
+ htmlUrl: mockReplier.htmlUrl,
+ type: mockReplier.type,
+ },
commentCount: 1,
labels: [],
htmlUrl:
diff --git a/src/renderer/utils/forges/github/handlers/discussion.ts b/src/renderer/utils/forges/github/handlers/discussion.ts
index 10c8979a3..b1ac8209b 100644
--- a/src/renderer/utils/forges/github/handlers/discussion.ts
+++ b/src/renderer/utils/forges/github/handlers/discussion.ts
@@ -63,10 +63,15 @@ class DiscussionHandler extends DefaultHandler {
const discussionReactionGroup =
latestDiscussionComment?.reactionGroups ?? discussion.reactionGroups;
+ const author = getNotificationAuthor([discussion.author]);
+ const commenter = getNotificationAuthor([latestDiscussionComment?.author]);
+
return {
number: discussion.number,
state: discussionState,
- user: getNotificationAuthor([latestDiscussionComment?.author, discussion.author]),
+ user: commenter ?? author,
+ author: author,
+ commenter: commenter,
commentCount: discussion.comments.totalCount,
labels:
discussion.labels?.nodes?.filter(Boolean).map((label) => ({
diff --git a/src/renderer/utils/forges/github/handlers/issue.test.ts b/src/renderer/utils/forges/github/handlers/issue.test.ts
index ae6c1dc97..e141f5fef 100644
--- a/src/renderer/utils/forges/github/handlers/issue.test.ts
+++ b/src/renderer/utils/forges/github/handlers/issue.test.ts
@@ -54,6 +54,12 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123' as Link,
labels: [],
@@ -86,6 +92,12 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123' as Link,
labels: [],
@@ -130,6 +142,18 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => {
htmlUrl: mockCommenter.htmlUrl,
type: mockCommenter.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
+ commenter: {
+ login: mockCommenter.login,
+ avatarUrl: mockCommenter.avatarUrl,
+ htmlUrl: mockCommenter.htmlUrl,
+ type: mockCommenter.type,
+ },
commentCount: 1,
htmlUrl:
'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234' as Link,
@@ -165,6 +189,12 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123' as Link,
labels: [{ name: 'enhancement', color: '0e8a16' }],
@@ -200,6 +230,12 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
commentCount: 0,
htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123' as Link,
labels: [],
diff --git a/src/renderer/utils/forges/github/handlers/issue.ts b/src/renderer/utils/forges/github/handlers/issue.ts
index fc14416ef..2e17ed3e1 100644
--- a/src/renderer/utils/forges/github/handlers/issue.ts
+++ b/src/renderer/utils/forges/github/handlers/issue.ts
@@ -40,7 +40,9 @@ class IssueHandler extends DefaultHandler {
const issueComment = issue.comments?.nodes?.[0];
- const issueUser = getNotificationAuthor([issueComment?.author, issue.author]);
+ const author = getNotificationAuthor([issue.author]);
+ const commenter = getNotificationAuthor([issueComment?.author]);
+ const issueUser = commenter ?? author;
const issueReactionCount = issueComment?.reactions.totalCount ?? issue.reactions.totalCount;
const issueReactionGroup = issueComment?.reactionGroups ?? issue.reactionGroups;
@@ -49,6 +51,8 @@ class IssueHandler extends DefaultHandler {
number: issue.number,
state: issueState,
user: issueUser,
+ author: author,
+ commenter: commenter,
commentCount: issue.comments.totalCount,
labels:
issue.labels?.nodes?.filter(Boolean).map((label) => ({
diff --git a/src/renderer/utils/forges/github/handlers/pullRequest.test.ts b/src/renderer/utils/forges/github/handlers/pullRequest.test.ts
index 908507fb0..cd90ab0c4 100644
--- a/src/renderer/utils/forges/github/handlers/pullRequest.test.ts
+++ b/src/renderer/utils/forges/github/handlers/pullRequest.test.ts
@@ -62,6 +62,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
@@ -96,6 +102,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
@@ -130,6 +142,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
@@ -164,6 +182,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
@@ -210,6 +234,18 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockCommenter.htmlUrl,
type: mockCommenter.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
+ commenter: {
+ login: mockCommenter.login,
+ avatarUrl: mockCommenter.avatarUrl,
+ htmlUrl: mockCommenter.htmlUrl,
+ type: mockCommenter.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
@@ -252,6 +288,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [{ name: 'enhancement', color: '0e8a16' }],
linkedIssues: [],
@@ -292,6 +334,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: ['#789'],
@@ -329,6 +377,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => {
htmlUrl: mockAuthor.htmlUrl,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ avatarUrl: mockAuthor.avatarUrl,
+ htmlUrl: mockAuthor.htmlUrl,
+ type: mockAuthor.type,
+ },
reviews: [],
labels: [],
linkedIssues: [],
diff --git a/src/renderer/utils/forges/github/handlers/pullRequest.ts b/src/renderer/utils/forges/github/handlers/pullRequest.ts
index dfc946a66..2a63432f5 100644
--- a/src/renderer/utils/forges/github/handlers/pullRequest.ts
+++ b/src/renderer/utils/forges/github/handlers/pullRequest.ts
@@ -51,7 +51,9 @@ class PullRequestHandler extends DefaultHandler {
const prComment = pr.comments?.nodes?.[0];
- const prUser = getNotificationAuthor([prComment?.author, pr.author]);
+ const author = getNotificationAuthor([pr.author]);
+ const commenter = getNotificationAuthor([prComment?.author]);
+ const prUser = commenter ?? author;
const reviews = getLatestReviewForReviewers(
(pr.reviews?.nodes?.filter(Boolean) ?? []) as PullRequestReviewFieldsFragment[],
@@ -64,6 +66,8 @@ class PullRequestHandler extends DefaultHandler {
number: pr.number,
state: prState,
user: prUser,
+ author: author,
+ commenter: commenter,
reviews: reviews,
commentCount: pr.comments.totalCount,
labels:
diff --git a/src/renderer/utils/forges/github/handlers/release.test.ts b/src/renderer/utils/forges/github/handlers/release.test.ts
index 03f8eeef9..61c8e7733 100644
--- a/src/renderer/utils/forges/github/handlers/release.test.ts
+++ b/src/renderer/utils/forges/github/handlers/release.test.ts
@@ -39,6 +39,12 @@ describe('renderer/utils/notifications/handlers/release.ts', () => {
avatarUrl: mockAuthor.avatar_url,
type: mockAuthor.type,
},
+ author: {
+ login: mockAuthor.login,
+ htmlUrl: mockAuthor.html_url,
+ avatarUrl: mockAuthor.avatar_url,
+ type: mockAuthor.type,
+ },
});
});
diff --git a/src/renderer/utils/forges/github/handlers/release.ts b/src/renderer/utils/forges/github/handlers/release.ts
index 296bccf85..5227d2c19 100644
--- a/src/renderer/utils/forges/github/handlers/release.ts
+++ b/src/renderer/utils/forges/github/handlers/release.ts
@@ -16,7 +16,6 @@ import type {
import { isStateFilteredOut } from '../../../notifications/filters/filter';
import { getRelease } from '../client';
import { DefaultHandler, defaultHandler } from './default';
-import { getNotificationAuthor } from './utils';
class ReleaseHandler extends DefaultHandler {
override async enrich(
@@ -32,7 +31,7 @@ class ReleaseHandler extends DefaultHandler {
const release = await getRelease(notification.account, notification.subject.url!);
- const user: GitifyNotificationUser | undefined = release.author
+ const author: GitifyNotificationUser | undefined = release.author
? {
login: release.author.login,
avatarUrl: release.author.avatar_url as Link,
@@ -43,7 +42,8 @@ class ReleaseHandler extends DefaultHandler {
return {
state: releaseState,
- user: getNotificationAuthor([user]),
+ user: author,
+ author: author,
};
}
diff --git a/src/renderer/utils/notifications/filters/filter.test.ts b/src/renderer/utils/notifications/filters/filter.test.ts
index ebf44cecd..bb32cfd32 100644
--- a/src/renderer/utils/notifications/filters/filter.test.ts
+++ b/src/renderer/utils/notifications/filters/filter.test.ts
@@ -19,6 +19,18 @@ describe('renderer/utils/notifications/filters/filter.ts', () => {
avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link,
type: 'User',
},
+ author: {
+ login: 'github-user',
+ htmlUrl: 'https://github.com/user' as Link,
+ avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link,
+ type: 'User',
+ },
+ commenter: {
+ login: 'coderabbitai',
+ htmlUrl: 'https://github.com/coderabbitai' as Link,
+ avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link,
+ type: 'Bot',
+ },
},
{
owner: {
@@ -38,6 +50,12 @@ describe('renderer/utils/notifications/filters/filter.ts', () => {
avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link,
type: 'Bot',
},
+ author: {
+ login: 'github-bot',
+ htmlUrl: 'https://github.com/bot' as Link,
+ avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link,
+ type: 'Bot',
+ },
},
{
owner: {
@@ -182,6 +200,50 @@ describe('renderer/utils/notifications/filters/filter.ts', () => {
expect(result).toEqual([mockNotifications[0]]);
});
+ it('should not match author handle against the latest commenter', async () => {
+ // The bot is the latest commenter on the human-authored notification,
+ // but it is not the author, so an author exclude must not hide it.
+ useFiltersStore.setState({
+ excludeSearchTokens: ['author:coderabbitai' as SearchToken],
+ });
+
+ const result = filterDetailedNotifications(mockNotifications, {
+ ...mockSettings,
+ detailedNotifications: true,
+ });
+
+ expect(result.length).toBe(2);
+ expect(result).toEqual(mockNotifications);
+ });
+
+ it('should filter notifications that match include commenter handle', async () => {
+ useFiltersStore.setState({
+ includeSearchTokens: ['commenter:coderabbitai' as SearchToken],
+ });
+
+ const result = filterDetailedNotifications(mockNotifications, {
+ ...mockSettings,
+ detailedNotifications: true,
+ });
+
+ expect(result.length).toBe(1);
+ expect(result).toEqual([mockNotifications[0]]);
+ });
+
+ it('should filter notifications that match exclude commenter handle', async () => {
+ useFiltersStore.setState({
+ excludeSearchTokens: ['commenter:coderabbitai' as SearchToken],
+ });
+
+ const result = filterDetailedNotifications(mockNotifications, {
+ ...mockSettings,
+ detailedNotifications: true,
+ });
+
+ expect(result.length).toBe(1);
+ expect(result).toEqual([mockNotifications[1]]);
+ });
+
it('should filter notifications by state when provided', async () => {
useFiltersStore.setState({ states: ['closed'] });
diff --git a/src/renderer/utils/notifications/filters/filter.ts b/src/renderer/utils/notifications/filters/filter.ts
index eb9ebdda6..6fe4a837f 100644
--- a/src/renderer/utils/notifications/filters/filter.ts
+++ b/src/renderer/utils/notifications/filters/filter.ts
@@ -1,11 +1,6 @@
import useFiltersStore from '../../../stores/useFiltersStore';
-import type {
- GitifyNotificationState,
- GitifyNotificationUser,
- RawGitifyNotification,
- SettingsState,
-} from '../../../types';
+import type { GitifyNotificationState, RawGitifyNotification, SettingsState } from '../../../types';
import {
BASE_SEARCH_QUALIFIERS,
@@ -175,18 +170,3 @@ export function isStateFilteredOut(state: GitifyNotificationState | undefined):
return !passesStateFilter(notification);
}
-
-/**
- * Return true if a notification with the given user would be filtered out
- * by the current user-type filter settings.
- *
- * Convenience helper used by UI components to indicate filtered-out users.
- *
- * @param user - The notification user to check.
- * @returns `true` if the user is currently filtered out.
- */
-export function isUserFilteredOut(user: GitifyNotificationUser): boolean {
- const notification = { subject: { user: user } } as RawGitifyNotification;
-
- return !passesUserFilters(notification);
-}
diff --git a/src/renderer/utils/notifications/filters/search.test.ts b/src/renderer/utils/notifications/filters/search.test.ts
index b3778f8a7..8f9c5af72 100644
--- a/src/renderer/utils/notifications/filters/search.test.ts
+++ b/src/renderer/utils/notifications/filters/search.test.ts
@@ -4,8 +4,6 @@ import type { GitifyOwner, Link } from '../../../types';
import { ALL_SEARCH_QUALIFIERS, filterNotificationBySearchTerm, parseSearchInput } from './search';
-// (helper removed – no longer used)
-
describe('renderer/utils/notifications/filters/search.ts', () => {
describe('parseSearchInput (prefix matching behavior)', () => {
it('returns null for empty string', () => {
@@ -36,12 +34,18 @@ describe('renderer/utils/notifications/filters/search.ts', () => {
const mockNotification = mockPartialGitifyNotification(
{
title: 'User authored notification',
- user: {
+ author: {
login: 'github-user',
htmlUrl: 'https://github.com/user' as Link,
avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link,
type: 'User',
},
+ commenter: {
+ login: 'coderabbitai',
+ htmlUrl: 'https://github.com/coderabbitai' as Link,
+ avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link,
+ type: 'Bot',
+ },
},
{
owner: {
@@ -53,14 +57,26 @@ describe('renderer/utils/notifications/filters/search.ts', () => {
},
);
- it('matches author qualifier (case-insensitive)', () => {
+ it('matches author qualifier against the thread author (case-insensitive)', () => {
expect(filterNotificationBySearchTerm(mockNotification, 'author:github-user')).toBe(true);
expect(filterNotificationBySearchTerm(mockNotification, 'author:GITHUB-USER')).toBe(true);
+ // The latest commenter is not the author.
+ expect(filterNotificationBySearchTerm(mockNotification, 'author:coderabbitai')).toBe(false);
+
expect(filterNotificationBySearchTerm(mockNotification, 'author:some-bot')).toBe(false);
});
+ it('matches commenter qualifier against the latest comment author (case-insensitive)', () => {
+ expect(filterNotificationBySearchTerm(mockNotification, 'commenter:coderabbitai')).toBe(true);
+
+ expect(filterNotificationBySearchTerm(mockNotification, 'commenter:CODERABBITAI')).toBe(true);
+
+ // The thread author is not the latest commenter.
+ expect(filterNotificationBySearchTerm(mockNotification, 'commenter:github-user')).toBe(false);
+ });
+
it('matches org qualifier (case-insensitive)', () => {
expect(filterNotificationBySearchTerm(mockNotification, 'org:gitify-app')).toBe(true);
diff --git a/src/renderer/utils/notifications/filters/search.ts b/src/renderer/utils/notifications/filters/search.ts
index e23df7018..2362b210f 100644
--- a/src/renderer/utils/notifications/filters/search.ts
+++ b/src/renderer/utils/notifications/filters/search.ts
@@ -7,9 +7,15 @@ export const SEARCH_DELIMITER = ':';
const SEARCH_QUALIFIERS = {
author: {
prefix: 'author:',
- description: 'filter by notification author',
+ description: 'filter by thread author',
requiresDetailsNotifications: true,
- extract: (n: RawGitifyNotification) => n.subject?.user?.login,
+ extract: (n: RawGitifyNotification) => n.subject?.author?.login,
+ },
+ commenter: {
+ prefix: 'commenter:',
+ description: 'filter by latest comment author',
+ requiresDetailsNotifications: true,
+ extract: (n: RawGitifyNotification) => n.subject?.commenter?.login,
},
org: {
prefix: 'org:',