diff --git a/src/renderer/components/notifications/NotificationRow.tsx b/src/renderer/components/notifications/NotificationRow.tsx index 9ff012f82..dda3f6547 100644 --- a/src/renderer/components/notifications/NotificationRow.tsx +++ b/src/renderer/components/notifications/NotificationRow.tsx @@ -8,8 +8,6 @@ import { GroupBy, Opacity, Size } from '../../types'; import type { Notification } from '../../typesGitHub'; import { cn } from '../../utils/cn'; import { isMarkAsDoneFeatureSupported } from '../../utils/features'; -import { formatForDisplay } from '../../utils/helpers'; -import { getNotificationTypeIconColor } from '../../utils/icons'; import { openNotification } from '../../utils/links'; import { createNotificationHandler } from '../../utils/notifications/handlers'; import { HoverButton } from '../primitives/HoverButton'; @@ -72,22 +70,11 @@ export const NotificationRow: FC = ({ }; const handler = createNotificationHandler(notification); - - const NotificationIcon = handler.getIcon(notification.subject); - const iconColor = getNotificationTypeIconColor(notification.subject); - - const notificationType = formatForDisplay([ - notification.subject.state, - notification.subject.type, - ]); - - const notificationNumber = notification.subject?.number - ? `#${notification.subject.number}` - : ''; - - const notificationTitle = notificationNumber - ? `${notification.subject.title} [${notificationNumber}]` - : notification.subject.title; + const NotificationIcon = handler.iconType(notification.subject); + const iconColor = handler.iconColor(notification.subject); + const notificationType = handler.formattedNotificationType(notification); + const notificationNumber = handler.formattedNotificationNumber(notification); + const notificationTitle = handler.formattedNotificationTitle(notification); const groupByDate = settings.groupBy === GroupBy.DATE; diff --git a/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap b/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap index e1e7e0ca6..c2dbf27b6 100644 --- a/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap +++ b/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap @@ -1100,7 +1100,7 @@ exports[`renderer/components/notifications/AccountNotifications.tsx should rende data-wrap="nowrap" > { }); }); - describe('formatting', () => { - it('formatForDisplay', () => { - expect(formatForDisplay(null)).toBe(''); - expect(formatForDisplay([])).toBe(''); - expect(formatForDisplay(['open', 'PullRequest'])).toBe( - 'Open Pull Request', - ); - expect(formatForDisplay(['OUTDATED', 'Discussion'])).toBe( - 'Outdated Discussion', - ); - expect(formatForDisplay(['not_planned', 'Issue'])).toBe( - 'Not Planned Issue', - ); - }); - }); - describe('getChevronDetails', () => { it('should return correct chevron details', () => { expect(getChevronDetails(true, true, 'account')).toEqual({ diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 27e932ab5..24d47c7dc 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -171,21 +171,6 @@ export async function generateGitHubWebUrl( return url.toString() as Link; } -export function formatForDisplay(text: string[]): string { - if (!text) { - return ''; - } - - return text - .join(' ') - .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase character followed by an uppercase character - .replace(/_/g, ' ') // Replace underscores with spaces - .replace(/\w+/g, (word) => { - // Convert to proper case (capitalize first letter of each word) - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); - }); -} - export function getChevronDetails( hasNotifications: boolean, isVisible: boolean, diff --git a/src/renderer/utils/icons.test.ts b/src/renderer/utils/icons.test.ts index 4ecf91c4a..1ca83f48e 100644 --- a/src/renderer/utils/icons.test.ts +++ b/src/renderer/utils/icons.test.ts @@ -8,118 +8,15 @@ import { } from '@primer/octicons-react'; import { IconColor } from '../types'; -import type { - GitifyPullRequestReview, - StateType, - Subject, - SubjectType, -} from '../typesGitHub'; +import type { GitifyPullRequestReview } from '../typesGitHub'; import { getAuthMethodIcon, getDefaultUserIcon, - getNotificationTypeIconColor, getPlatformIcon, getPullRequestReviewIcon, } from './icons'; describe('renderer/utils/icons.ts', () => { - describe('getNotificationTypeIconColor', () => { - it('should format the notification color for check suite', () => { - expect( - getNotificationTypeIconColor( - createSubjectMock({ - type: 'CheckSuite', - state: 'cancelled', - }), - ), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ - type: 'CheckSuite', - state: 'failure', - }), - ), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ - type: 'CheckSuite', - state: 'skipped', - }), - ), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ - type: 'CheckSuite', - state: 'success', - }), - ), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ - type: 'CheckSuite', - state: null, - }), - ), - ).toMatchSnapshot(); - }); - - it('should format the notification color for state', () => { - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'ANSWERED' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'closed' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'completed' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'draft' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'merged' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ state: 'not_planned' }), - ), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'open' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'reopened' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor(createSubjectMock({ state: 'RESOLVED' })), - ).toMatchSnapshot(); - - expect( - getNotificationTypeIconColor( - createSubjectMock({ - state: 'something_else_unknown' as StateType, - }), - ), - ).toMatchSnapshot(); - }); - }); - describe('getPullRequestReviewIcon', () => { let mockReviewSingleReviewer: GitifyPullRequestReview; let mockReviewMultipleReviewer: GitifyPullRequestReview; @@ -235,17 +132,3 @@ describe('renderer/utils/icons.ts', () => { expect(getDefaultUserIcon('User')).toBe(FeedPersonIcon); }); }); - -function createSubjectMock(mocks: { - title?: string; - type?: SubjectType; - state?: StateType; -}): Subject { - return { - title: mocks.title ?? 'Mock Subject', - type: mocks.type ?? ('Unknown' as SubjectType), - state: mocks.state ?? ('Unknown' as StateType), - url: null, - latest_comment_url: null, - }; -} diff --git a/src/renderer/utils/icons.ts b/src/renderer/utils/icons.ts index c9048aca8..1f472ff5d 100644 --- a/src/renderer/utils/icons.ts +++ b/src/renderer/utils/icons.ts @@ -15,32 +15,9 @@ import { } from '@primer/octicons-react'; import { IconColor, type PullRequestApprovalIcon } from '../types'; -import type { - GitifyPullRequestReview, - Subject, - UserType, -} from '../typesGitHub'; +import type { GitifyPullRequestReview, UserType } from '../typesGitHub'; import type { AuthMethod, PlatformType } from './auth/types'; -export function getNotificationTypeIconColor(subject: Subject): IconColor { - switch (subject.state) { - case 'open': - case 'reopened': - case 'ANSWERED': - case 'success': - return IconColor.GREEN; - case 'closed': - case 'failure': - return IconColor.RED; - case 'completed': - case 'RESOLVED': - case 'merged': - return IconColor.PURPLE; - default: - return IconColor.GRAY; - } -} - export function getPullRequestReviewIcon( review: GitifyPullRequestReview, ): PullRequestApprovalIcon | null { diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index caaa96639..8f50f8af1 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -136,15 +136,15 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - checkSuiteHandler.getIcon( + checkSuiteHandler.iconType( createSubjectMock({ type: 'CheckSuite', state: null }), ).displayName, ).toBe('RocketIcon'); expect( - checkSuiteHandler.getIcon( + checkSuiteHandler.iconType( createSubjectMock({ type: 'CheckSuite', state: 'cancelled', @@ -153,7 +153,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ).toBe('StopIcon'); expect( - checkSuiteHandler.getIcon( + checkSuiteHandler.iconType( createSubjectMock({ type: 'CheckSuite', state: 'failure', @@ -162,7 +162,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ).toBe('XIcon'); expect( - checkSuiteHandler.getIcon( + checkSuiteHandler.iconType( createSubjectMock({ type: 'CheckSuite', state: 'skipped', @@ -171,7 +171,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ).toBe('SkipIcon'); expect( - checkSuiteHandler.getIcon( + checkSuiteHandler.iconType( createSubjectMock({ type: 'CheckSuite', state: 'success', diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index 1f129b480..df38346b5 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -17,9 +17,9 @@ import type { Notification, Subject, } from '../../../typesGitHub'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; -class CheckSuiteHandler implements NotificationTypeHandler { +class CheckSuiteHandler extends DefaultHandler { readonly type = 'CheckSuite'; async enrich( @@ -38,7 +38,7 @@ class CheckSuiteHandler implements NotificationTypeHandler { return null; } - getIcon(subject: Subject): FC | null { + iconType(subject: Subject): FC | null { switch (subject.state) { case 'cancelled': return StopIcon; diff --git a/src/renderer/utils/notifications/handlers/commit.test.ts b/src/renderer/utils/notifications/handlers/commit.test.ts index 854cb579c..bf9d26ba9 100644 --- a/src/renderer/utils/notifications/handlers/commit.test.ts +++ b/src/renderer/utils/notifications/handlers/commit.test.ts @@ -97,9 +97,9 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - commitHandler.getIcon(createSubjectMock({ type: 'Commit' })).displayName, + commitHandler.iconType(createSubjectMock({ type: 'Commit' })).displayName, ).toBe('GitCommitIcon'); }); }); diff --git a/src/renderer/utils/notifications/handlers/commit.ts b/src/renderer/utils/notifications/handlers/commit.ts index a2f8c47e1..882134f03 100644 --- a/src/renderer/utils/notifications/handlers/commit.ts +++ b/src/renderer/utils/notifications/handlers/commit.ts @@ -13,10 +13,10 @@ import type { } from '../../../typesGitHub'; import { getCommit, getCommitComment } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; import { getSubjectUser } from './utils'; -class CommitHandler implements NotificationTypeHandler { +class CommitHandler extends DefaultHandler { readonly type = 'Commit'; async enrich( @@ -55,7 +55,7 @@ class CommitHandler implements NotificationTypeHandler { }; } - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return GitCommitIcon; } } diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index 8f1db8780..d1abc5f7c 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -1,6 +1,8 @@ import { createSubjectMock } from '../../../__mocks__/notifications-mocks'; import { partialMockNotification } from '../../../__mocks__/partial-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; +import { IconColor } from '../../../types'; +import type { StateType } from '../../../typesGitHub'; import { defaultHandler } from './default'; describe('renderer/utils/notifications/handlers/default.ts', () => { @@ -21,9 +23,109 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { }); }); - it('getIcon', () => { - expect(defaultHandler.getIcon(createSubjectMock({})).displayName).toBe( + it('iconType', () => { + expect(defaultHandler.iconType(createSubjectMock({})).displayName).toBe( 'QuestionIcon', ); }); + + describe('iconColor', () => { + const cases: Array<[StateType | null, IconColor]> = [ + ['open' as StateType, IconColor.GREEN], + ['reopened' as StateType, IconColor.GREEN], + ['ANSWERED' as StateType, IconColor.GREEN], + ['success' as StateType, IconColor.GREEN], + ['closed' as StateType, IconColor.RED], + ['failure' as StateType, IconColor.RED], + ['completed' as StateType, IconColor.PURPLE], + ['RESOLVED' as StateType, IconColor.PURPLE], + ['merged' as StateType, IconColor.PURPLE], + ['not_planned' as StateType, IconColor.GRAY], + ['draft' as StateType, IconColor.GRAY], + ['skipped' as StateType, IconColor.GRAY], + ['cancelled' as StateType, IconColor.GRAY], + ['unknown' as StateType, IconColor.GRAY], + [null, IconColor.GRAY], + [undefined, IconColor.GRAY], + ]; + + it.each(cases)('returns correct color for state %s', (state, expected) => { + const subject = createSubjectMock({ state }); + expect(defaultHandler.iconColor(subject)).toBe(expected); + }); + }); + + describe('formattedNotificationType', () => { + it('formats state and type with proper casing and spacing', () => { + const notification = partialMockNotification({ + title: 'Sample', + type: 'PullRequest', + state: 'open', + }); + + expect(defaultHandler.formattedNotificationType(notification)).toBe( + 'Open Pull Request', + ); + }); + + it('handles missing state (null) gracefully', () => { + const notification = partialMockNotification({ + title: 'Sample', + type: 'Issue', + state: null, + }); + + expect(defaultHandler.formattedNotificationType(notification)).toBe( + 'Issue', + ); + }); + }); + + describe('formattedNotificationNumber', () => { + it('returns formatted number when present', () => { + const notification = partialMockNotification({ + title: 'Sample', + type: 'Issue', + state: 'open', + }); + notification.subject.number = 42; + expect(defaultHandler.formattedNotificationNumber(notification)).toBe( + '#42', + ); + }); + + it('returns empty string when number absent', () => { + const notification = partialMockNotification({ + title: 'Sample', + type: 'Issue', + state: 'open', + }); + expect(defaultHandler.formattedNotificationNumber(notification)).toBe(''); + }); + }); + + describe('formattedNotificationTitle', () => { + it('appends number in brackets when present', () => { + const notification = partialMockNotification({ + title: 'Fix bug', + type: 'Issue', + state: 'open', + }); + notification.subject.number = 101; + expect(defaultHandler.formattedNotificationTitle(notification)).toBe( + 'Fix bug [#101]', + ); + }); + + it('returns title unchanged when number missing', () => { + const notification = partialMockNotification({ + title: 'Improve docs', + type: 'Issue', + state: 'open', + }); + expect(defaultHandler.formattedNotificationTitle(notification)).toBe( + 'Improve docs', + ); + }); + }); }); diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 8c43c407f..99f66b474 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -4,14 +4,19 @@ import type { OcticonProps } from '@primer/octicons-react'; import { QuestionIcon } from '@primer/octicons-react'; import type { SettingsState } from '../../../types'; +import { IconColor } from '../../../types'; import type { GitifySubject, Notification, Subject, + SubjectType, } from '../../../typesGitHub'; import type { NotificationTypeHandler } from './types'; +import { formatForDisplay } from './utils'; + +export class DefaultHandler implements NotificationTypeHandler { + type?: SubjectType; -class DefaultHandler implements NotificationTypeHandler { async enrich( _notification: Notification, _settings: SettingsState, @@ -19,9 +24,48 @@ class DefaultHandler implements NotificationTypeHandler { return null; } - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return QuestionIcon; } + + iconColor(subject: Subject): IconColor { + switch (subject.state) { + case 'open': + case 'reopened': + case 'ANSWERED': + case 'success': + return IconColor.GREEN; + case 'closed': + case 'failure': + return IconColor.RED; + case 'completed': + case 'RESOLVED': + case 'merged': + return IconColor.PURPLE; + default: + return IconColor.GRAY; + } + } + + formattedNotificationType(notification: Notification): string { + return formatForDisplay([ + notification.subject.state, + notification.subject.type, + ]); + } + formattedNotificationNumber(notification: Notification): string { + return notification.subject?.number + ? `#${notification.subject.number}` + : ''; + } + formattedNotificationTitle(notification: Notification): string { + let title = notification.subject.title; + + if (notification.subject?.number) { + title = `${title} [${this.formattedNotificationNumber(notification)}]`; + } + return title; + } } export const defaultHandler = new DefaultHandler(); diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 5f03ddb93..e81878886 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -279,26 +279,26 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - discussionHandler.getIcon(createSubjectMock({ type: 'Discussion' })) + discussionHandler.iconType(createSubjectMock({ type: 'Discussion' })) .displayName, ).toBe('CommentDiscussionIcon'); expect( - discussionHandler.getIcon( + discussionHandler.iconType( createSubjectMock({ type: 'Discussion', state: 'DUPLICATE' }), ).displayName, ).toBe('DiscussionDuplicateIcon'); expect( - discussionHandler.getIcon( + discussionHandler.iconType( createSubjectMock({ type: 'Discussion', state: 'OUTDATED' }), ).displayName, ).toBe('DiscussionOutdatedIcon'); expect( - discussionHandler.getIcon( + discussionHandler.iconType( createSubjectMock({ type: 'Discussion', state: 'RESOLVED' }), ).displayName, ).toBe('DiscussionClosedIcon'); diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index 755e69176..255cd68b1 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -21,9 +21,9 @@ import type { } from '../../../typesGitHub'; import { getLatestDiscussion } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; -class DiscussionHandler implements NotificationTypeHandler { +class DiscussionHandler extends DefaultHandler { readonly type = 'Discussion'; async enrich( @@ -77,7 +77,7 @@ class DiscussionHandler implements NotificationTypeHandler { }; } - getIcon(subject: Subject): FC | null { + iconType(subject: Subject): FC | null { switch (subject.state) { case 'DUPLICATE': return DiscussionDuplicateIcon; diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index cf437b490..511aaeee6 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -294,18 +294,19 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - issueHandler.getIcon(createSubjectMock({ type: 'Issue' })).displayName, + issueHandler.iconType(createSubjectMock({ type: 'Issue' })).displayName, ).toBe('IssueOpenedIcon'); expect( - issueHandler.getIcon(createSubjectMock({ type: 'Issue', state: 'draft' })) - .displayName, + issueHandler.iconType( + createSubjectMock({ type: 'Issue', state: 'draft' }), + ).displayName, ).toBe('IssueDraftIcon'); expect( - issueHandler.getIcon( + issueHandler.iconType( createSubjectMock({ type: 'Issue', state: 'closed', @@ -314,7 +315,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).toBe('IssueClosedIcon'); expect( - issueHandler.getIcon( + issueHandler.iconType( createSubjectMock({ type: 'Issue', state: 'completed', @@ -323,7 +324,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).toBe('IssueClosedIcon'); expect( - issueHandler.getIcon( + issueHandler.iconType( createSubjectMock({ type: 'Issue', state: 'not_planned', @@ -332,7 +333,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).toBe('SkipIcon'); expect( - issueHandler.getIcon( + issueHandler.iconType( createSubjectMock({ type: 'Issue', state: 'reopened', diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 46f1dbffd..74b637aad 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -18,10 +18,10 @@ import type { } from '../../../typesGitHub'; import { getIssue, getIssueOrPullRequestComment } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; import { getSubjectUser } from './utils'; -class IssueHandler implements NotificationTypeHandler { +class IssueHandler extends DefaultHandler { readonly type = 'Issue'; async enrich( @@ -61,7 +61,7 @@ class IssueHandler implements NotificationTypeHandler { }; } - getIcon(subject: Subject): FC | null { + iconType(subject: Subject): FC | null { switch (subject.state) { case 'draft': return IssueDraftIcon; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 6c0a440e2..c70f264bd 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -438,14 +438,14 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - pullRequestHandler.getIcon(createSubjectMock({ type: 'PullRequest' })) + pullRequestHandler.iconType(createSubjectMock({ type: 'PullRequest' })) .displayName, ).toBe('GitPullRequestIcon'); expect( - pullRequestHandler.getIcon( + pullRequestHandler.iconType( createSubjectMock({ type: 'PullRequest', state: 'draft', @@ -454,7 +454,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { ).toBe('GitPullRequestDraftIcon'); expect( - pullRequestHandler.getIcon( + pullRequestHandler.iconType( createSubjectMock({ type: 'PullRequest', state: 'closed', @@ -463,7 +463,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { ).toBe('GitPullRequestClosedIcon'); expect( - pullRequestHandler.getIcon( + pullRequestHandler.iconType( createSubjectMock({ type: 'PullRequest', state: 'merged', diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index baf53b3ee..2d0617c5c 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -25,10 +25,10 @@ import { getPullRequestReviews, } from '../../api/client'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; import { getSubjectUser } from './utils'; -class PullRequestHandler implements NotificationTypeHandler { +class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; async enrich( @@ -87,7 +87,7 @@ class PullRequestHandler implements NotificationTypeHandler { }; } - getIcon(subject: Subject): FC | null { + iconType(subject: Subject): FC | null { switch (subject.state) { case 'draft': return GitPullRequestDraftIcon; diff --git a/src/renderer/utils/notifications/handlers/release.test.ts b/src/renderer/utils/notifications/handlers/release.test.ts index fb34d840c..49e6f87a4 100644 --- a/src/renderer/utils/notifications/handlers/release.test.ts +++ b/src/renderer/utils/notifications/handlers/release.test.ts @@ -67,9 +67,9 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - releaseHandler.getIcon( + releaseHandler.iconType( createSubjectMock({ type: 'Release', }), diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index 6400ef362..9caadc1a7 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -12,10 +12,10 @@ import type { } from '../../../typesGitHub'; import { getRelease } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; import { getSubjectUser } from './utils'; -class ReleaseHandler implements NotificationTypeHandler { +class ReleaseHandler extends DefaultHandler { readonly type = 'Release'; async enrich( @@ -39,7 +39,7 @@ class ReleaseHandler implements NotificationTypeHandler { }; } - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return TagIcon; } } diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts index 7d1937f56..b6fd92706 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts @@ -2,9 +2,9 @@ import { createSubjectMock } from '../../../__mocks__/notifications-mocks'; import { repositoryDependabotAlertsThreadHandler } from './repositoryDependabotAlertsThread'; describe('renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts', () => { - it('getIcon', () => { + it('iconType', () => { expect( - repositoryDependabotAlertsThreadHandler.getIcon( + repositoryDependabotAlertsThreadHandler.iconType( createSubjectMock({ type: 'RepositoryDependabotAlertsThread', }), diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts index 6fd5a49f5..cd06771be 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts @@ -3,27 +3,13 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { AlertIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; -import type { - GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; -import type { NotificationTypeHandler } from './types'; +import type { Subject } from '../../../typesGitHub'; +import { DefaultHandler } from './default'; -class RepositoryDependabotAlertsThreadHandler - implements NotificationTypeHandler -{ +class RepositoryDependabotAlertsThreadHandler extends DefaultHandler { readonly type = 'RepositoryDependabotAlertsThread'; - async enrich( - _notification: Notification, - _settings: SettingsState, - ): Promise { - return; - } - - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return AlertIcon; } } diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts index 30d52ee2b..69062a277 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts @@ -2,9 +2,9 @@ import { createSubjectMock } from '../../../__mocks__/notifications-mocks'; import { repositoryInvitationHandler } from './repositoryInvitation'; describe('renderer/utils/notifications/handlers/repositoryInvitation.ts', () => { - it('getIcon', () => { + it('iconType', () => { expect( - repositoryInvitationHandler.getIcon( + repositoryInvitationHandler.iconType( createSubjectMock({ type: 'RepositoryInvitation', }), diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts index 6eac000fc..38bf30470 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts @@ -2,25 +2,13 @@ import type { FC } from 'react'; import { MailIcon, type OcticonProps } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; -import type { - GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; -import type { NotificationTypeHandler } from './types'; +import type { Subject } from '../../../typesGitHub'; +import { DefaultHandler } from './default'; -class RepositoryInvitationHandler implements NotificationTypeHandler { +class RepositoryInvitationHandler extends DefaultHandler { readonly type = 'RepositoryInvitation'; - async enrich( - _notification: Notification, - _settings: SettingsState, - ): Promise { - return; - } - - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return MailIcon; } } diff --git a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts index d1a88b395..f6055ed3d 100644 --- a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts @@ -2,9 +2,9 @@ import { createSubjectMock } from '../../../__mocks__/notifications-mocks'; import { repositoryVulnerabilityAlertHandler } from './repositoryVulnerabilityAlert'; describe('renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts', () => { - it('getIcon', () => { + it('iconType', () => { expect( - repositoryVulnerabilityAlertHandler.getIcon( + repositoryVulnerabilityAlertHandler.iconType( createSubjectMock({ type: 'RepositoryVulnerabilityAlert', }), diff --git a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts index 5647d6ebe..f3b8e77e7 100644 --- a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts +++ b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts @@ -2,25 +2,13 @@ import type { FC } from 'react'; import { AlertIcon, type OcticonProps } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; -import type { - GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; -import type { NotificationTypeHandler } from './types'; +import type { Subject } from '../../../typesGitHub'; +import { DefaultHandler } from './default'; -class RepositoryVulnerabilityAlertHandler implements NotificationTypeHandler { +class RepositoryVulnerabilityAlertHandler extends DefaultHandler { readonly type = 'RepositoryVulnerabilityAlert'; - async enrich( - _notification: Notification, - _settings: SettingsState, - ): Promise { - return; - } - - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return AlertIcon; } } diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index 7f377298b..686e3ef50 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -21,6 +21,28 @@ export interface NotificationTypeHandler { settings: SettingsState, ): Promise; - /** Return an icon component for this notification type. */ - getIcon(subject: Subject): FC | null; + /** + * Return the icon component for this notification type. + */ + iconType(subject: Subject): FC | null; + + /** + * Return the icon color for this notification type. + */ + iconColor(subject: Subject): string | undefined; + + /** + * Return the formatted notification type for this notification. + */ + formattedNotificationType(notification: Notification): string; + + /** + * Return the formatted notification number for this notification. + */ + formattedNotificationNumber(notification: Notification): string; + + /** + * Return the formatted notification title for this notification. + */ + formattedNotificationTitle(notification: Notification): string; } diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index 9863cc4a5..9437f40fa 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -1,5 +1,5 @@ import { partialMockUser } from '../../../__mocks__/partial-mocks'; -import { getSubjectUser } from './utils'; +import { formatForDisplay, getSubjectUser } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { describe('getSubjectUser', () => { @@ -33,4 +33,16 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { }); }); }); + + it('formatForDisplay', () => { + expect(formatForDisplay(null)).toBe(''); + expect(formatForDisplay([])).toBe(''); + expect(formatForDisplay(['open', 'PullRequest'])).toBe('Open Pull Request'); + expect(formatForDisplay(['OUTDATED', 'Discussion'])).toBe( + 'Outdated Discussion', + ); + expect(formatForDisplay(['not_planned', 'Issue'])).toBe( + 'Not Planned Issue', + ); + }); }); diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index 5bee31fe5..cb27e3b77 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -23,3 +23,19 @@ export function getSubjectUser(users: User[]): SubjectUser { return subjectUser; } + +export function formatForDisplay(text: string[]): string { + if (!text) { + return ''; + } + + return text + .join(' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase character followed by an uppercase character + .replace(/_/g, ' ') // Replace underscores with spaces + .replace(/\w+/g, (word) => { + // Convert to proper case (capitalize first letter of each word) + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .trim(); +} diff --git a/src/renderer/utils/notifications/handlers/workflowRun.test.ts b/src/renderer/utils/notifications/handlers/workflowRun.test.ts index 000d4ccb1..5d843cf1d 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.test.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.test.ts @@ -52,9 +52,9 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { }); }); - it('getIcon', () => { + it('iconType', () => { expect( - workflowRunHandler.getIcon( + workflowRunHandler.iconType( createSubjectMock({ type: 'WorkflowRun', }), diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 9dbd4a493..9dfd7df2e 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -11,9 +11,9 @@ import type { Subject, WorkflowRunAttributes, } from '../../../typesGitHub'; -import type { NotificationTypeHandler } from './types'; +import { DefaultHandler } from './default'; -class WorkflowRunHandler implements NotificationTypeHandler { +class WorkflowRunHandler extends DefaultHandler { readonly type = 'WorkflowRun'; async enrich( @@ -32,7 +32,7 @@ class WorkflowRunHandler implements NotificationTypeHandler { return null; } - getIcon(_subject: Subject): FC | null { + iconType(_subject: Subject): FC | null { return RocketIcon; } }