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:',