From e32f2f592cf9da6667f6879fce62c9d2f240d170 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Mon, 29 Dec 2025 01:05:10 +0000 Subject: [PATCH 1/5] refactor: use GitifyNotification type with transformation layer Introduce a clean separation between raw GitHub API types and internal Gitify notification types: - Add transform.ts at API boundary to convert raw GitHub responses - Define GitifyNotification, GitifySubject, GitifyRepository, GitifyOwner types with camelCase properties - Update all components, handlers, filters, and utilities to use new types - Update GraphQL schema aliases for consistent property naming - Remove Notification type alias from typesGitHub.ts Closes #828 --- src/renderer/__mocks__/notifications-mocks.ts | 41 +- src/renderer/__mocks__/user-mocks.ts | 11 + .../components/metrics/MetricGroup.tsx | 5 +- .../notifications/AccountNotifications.tsx | 10 +- .../notifications/NotificationFooter.test.tsx | 19 +- .../notifications/NotificationFooter.tsx | 9 +- .../notifications/NotificationHeader.test.tsx | 2 +- .../notifications/NotificationHeader.tsx | 9 +- .../notifications/NotificationRow.tsx | 5 +- .../RepositoryNotifications.test.tsx | 2 +- .../notifications/RepositoryNotifications.tsx | 7 +- .../NotificationFooter.test.tsx.snap | 467 +----------------- src/renderer/context/App.tsx | 18 +- src/renderer/hooks/useNotifications.test.ts | 112 ++++- src/renderer/hooks/useNotifications.ts | 14 +- src/renderer/types.ts | 79 ++- src/renderer/typesGitHub.ts | 31 +- .../utils/api/__mocks__/response-mocks.ts | 116 ++--- src/renderer/utils/api/client.ts | 14 +- src/renderer/utils/api/graphql/common.graphql | 4 +- .../utils/api/graphql/generated/gql.ts | 6 +- .../utils/api/graphql/generated/graphql.ts | 216 ++++---- src/renderer/utils/api/transform.ts | 70 +++ src/renderer/utils/helpers.test.ts | 16 +- src/renderer/utils/helpers.ts | 11 +- src/renderer/utils/links.test.ts | 20 +- src/renderer/utils/links.ts | 18 +- src/renderer/utils/logger.ts | 12 +- .../notifications/filters/filter.test.ts | 28 +- .../utils/notifications/filters/filter.ts | 20 +- .../utils/notifications/filters/reason.ts | 8 +- .../notifications/filters/search.test.ts | 13 +- .../utils/notifications/filters/search.ts | 11 +- .../utils/notifications/filters/state.test.ts | 9 +- .../utils/notifications/filters/state.ts | 4 +- .../notifications/filters/subjectType.ts | 5 +- .../utils/notifications/filters/types.ts | 4 +- .../notifications/filters/userType.test.ts | 4 +- .../utils/notifications/filters/userType.ts | 8 +- .../utils/notifications/group.test.ts | 24 +- src/renderer/utils/notifications/group.ts | 19 +- .../notifications/handlers/checkSuite.test.ts | 12 +- .../notifications/handlers/checkSuite.ts | 20 +- .../notifications/handlers/commit.test.ts | 24 +- .../utils/notifications/handlers/commit.ts | 27 +- .../notifications/handlers/default.test.ts | 9 +- .../utils/notifications/handlers/default.ts | 29 +- .../notifications/handlers/discussion.test.ts | 54 +- .../notifications/handlers/discussion.ts | 18 +- .../utils/notifications/handlers/index.ts | 4 +- .../notifications/handlers/issue.test.ts | 46 +- .../utils/notifications/handlers/issue.ts | 14 +- .../handlers/pullRequest.test.ts | 64 +-- .../notifications/handlers/pullRequest.ts | 14 +- .../notifications/handlers/release.test.ts | 18 +- .../utils/notifications/handlers/release.ts | 26 +- .../repositoryDependabotAlertsThread.test.ts | 7 +- .../repositoryDependabotAlertsThread.ts | 9 +- .../handlers/repositoryInvitation.test.ts | 7 +- .../handlers/repositoryInvitation.ts | 9 +- .../repositoryVulnerabilityAlert.test.ts | 7 +- .../handlers/repositoryVulnerabilityAlert.ts | 4 +- .../utils/notifications/handlers/types.ts | 25 +- .../notifications/handlers/utils.test.ts | 12 +- .../utils/notifications/handlers/utils.ts | 4 +- .../handlers/workflowRun.test.ts | 13 +- .../notifications/handlers/workflowRun.ts | 18 +- .../utils/notifications/native.test.ts | 4 +- src/renderer/utils/notifications/native.ts | 8 +- .../utils/notifications/notifications.test.ts | 11 +- .../utils/notifications/notifications.ts | 26 +- src/renderer/utils/notifications/remove.ts | 10 +- src/renderer/utils/notifications/utils.ts | 5 +- 73 files changed, 928 insertions(+), 1131 deletions(-) create mode 100644 src/renderer/utils/api/transform.ts diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index 5f8fbff1c..1063abb43 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -1,15 +1,14 @@ import { Constants } from '../constants'; import type { AccountNotifications, + GitifyNotification, GitifyNotificationState, + GitifyRepository, + GitifySubject, Hostname, + Link, } from '../types'; -import type { - Notification, - Repository, - Subject, - SubjectType, -} from '../typesGitHub'; +import type { SubjectType } from '../typesGitHub'; import { mockEnterpriseNotifications, mockGitHubNotifications, @@ -47,21 +46,21 @@ export function createMockSubject(mocks: { title?: string; type?: SubjectType; state?: GitifyNotificationState; -}): Subject { +}): GitifySubject { return { title: mocks.title ?? 'Mock Subject', type: mocks.type ?? ('Unknown' as SubjectType), state: mocks.state ?? ('Unknown' as GitifyNotificationState), url: null, - latest_comment_url: null, + latestCommentUrl: null, }; } export function createPartialMockNotification( - subject: Partial, - repository?: Partial, -): Notification { - const mockNotification: Partial = { + subject: Partial, + repository?: Partial, +): GitifyNotification { + const mockNotification: Partial = { account: { method: 'Personal Access Token', platform: 'GitHub Cloud', @@ -70,28 +69,30 @@ export function createPartialMockNotification( user: mockGitifyUser, hasRequiredScopes: true, }, - subject: subject as Subject, + subject: subject as GitifySubject, repository: { name: 'notifications-test', - full_name: 'gitify-app/notifications-test', - html_url: 'https://github.com/gitify-app/notifications-test', + fullName: 'gitify-app/notifications-test', + htmlUrl: 'https://github.com/gitify-app/notifications-test' as Link, owner: { login: 'gitify-app', + avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link, + type: 'Organization', }, ...repository, - } as Repository, + } as GitifyRepository, }; - return mockNotification as Notification; + return mockNotification as GitifyNotification; } export function createMockNotificationForRepoName( id: string, repoFullName: string | null, -): Notification { +): GitifyNotification { return { id, - repository: repoFullName ? { full_name: repoFullName } : null, + repository: repoFullName ? { fullName: repoFullName } : null, account: mockGitHubCloudAccount, - } as Notification; + } as GitifyNotification; } diff --git a/src/renderer/__mocks__/user-mocks.ts b/src/renderer/__mocks__/user-mocks.ts index 28af4f4ba..eae029fa8 100644 --- a/src/renderer/__mocks__/user-mocks.ts +++ b/src/renderer/__mocks__/user-mocks.ts @@ -1,5 +1,6 @@ import type { GitifyUser, Link } from '../types'; import type { User } from '../typesGitHub'; +import type { AuthorFieldsFragment } from '../utils/api/graphql/generated/graphql'; export const mockGitifyUser: GitifyUser = { login: 'octocat', @@ -18,3 +19,13 @@ export function createPartialMockUser(login: string): User { return mockUser as User; } + +export function createMockAuthorFragment(login: string): AuthorFieldsFragment { + return { + __typename: 'User', + login: login, + htmlUrl: `https://github.com/${login}`, + avatarUrl: 'https://avatars.githubusercontent.com/u/583231?v=4', + type: 'User', + }; +} diff --git a/src/renderer/components/metrics/MetricGroup.tsx b/src/renderer/components/metrics/MetricGroup.tsx index 203bcc795..dd325e026 100644 --- a/src/renderer/components/metrics/MetricGroup.tsx +++ b/src/renderer/components/metrics/MetricGroup.tsx @@ -8,13 +8,12 @@ import { } from '@primer/octicons-react'; import { useAppContext } from '../../context/App'; -import { IconColor } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { type GitifyNotification, IconColor } from '../../types'; import { getPullRequestReviewIcon } from '../../utils/icons'; import { MetricPill } from './MetricPill'; interface MetricGroupProps { - notification: Notification; + notification: GitifyNotification; } export const MetricGroup: FC = ({ diff --git a/src/renderer/components/notifications/AccountNotifications.tsx b/src/renderer/components/notifications/AccountNotifications.tsx index 599283f10..12f427f93 100644 --- a/src/renderer/components/notifications/AccountNotifications.tsx +++ b/src/renderer/components/notifications/AccountNotifications.tsx @@ -4,8 +4,12 @@ import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react'; import { Button, Stack } from '@primer/react'; import { useAppContext } from '../../context/App'; -import { type Account, type GitifyError, Size } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { + type Account, + type GitifyError, + type GitifyNotification, + Size, +} from '../../types'; import { hasMultipleAccounts } from '../../utils/auth/utils'; import { cn } from '../../utils/cn'; import { getChevronDetails } from '../../utils/helpers'; @@ -28,7 +32,7 @@ import { RepositoryNotifications } from './RepositoryNotifications'; interface AccountNotificationsProps { account: Account; - notifications: Notification[]; + notifications: GitifyNotification[]; error: GitifyError | null; showAccountHeader: boolean; } diff --git a/src/renderer/components/notifications/NotificationFooter.test.tsx b/src/renderer/components/notifications/NotificationFooter.test.tsx index f05b5f1f4..d2e32678f 100644 --- a/src/renderer/components/notifications/NotificationFooter.test.tsx +++ b/src/renderer/components/notifications/NotificationFooter.test.tsx @@ -28,19 +28,6 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { expect(tree).toMatchSnapshot(); }); - it('should render itself & its children when last_read_at is null', async () => { - const mockNotification = mockSingleNotification; - mockNotification.last_read_at = null; - - const props = { - notification: mockNotification, - }; - - const tree = renderWithAppContext(); - - expect(tree).toMatchSnapshot(); - }); - describe('security alerts should use github icon for avatar', () => { it('Repository Dependabot Alerts Thread', async () => { const mockNotification = mockSingleNotification; @@ -94,8 +81,8 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { ...mockSingleNotification.subject, user: { login: 'some-user', - html_url: 'https://github.com/some-user' as Link, - avatar_url: + htmlUrl: 'https://github.com/some-user' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User' as UserType, }, @@ -111,7 +98,7 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); expect(openExternalLinkSpy).toHaveBeenCalledWith( - props.notification.subject.user.html_url, + props.notification.subject.user.htmlUrl, ); }); }); diff --git a/src/renderer/components/notifications/NotificationFooter.tsx b/src/renderer/components/notifications/NotificationFooter.tsx index dd9e59dc9..9bc01181a 100644 --- a/src/renderer/components/notifications/NotificationFooter.tsx +++ b/src/renderer/components/notifications/NotificationFooter.tsx @@ -2,8 +2,7 @@ import type { FC, MouseEvent } from 'react'; import { RelativeTime, Stack, Text } from '@primer/react'; -import { Opacity, Size } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { type GitifyNotification, Opacity, Size } from '../../types'; import { cn } from '../../utils/cn'; import { openUserProfile } from '../../utils/links'; import { getReasonDetails } from '../../utils/reason'; @@ -11,7 +10,7 @@ import { AvatarWithFallback } from '../avatars/AvatarWithFallback'; import { MetricGroup } from '../metrics/MetricGroup'; interface NotificationFooterProps { - notification: Notification; + notification: GitifyNotification; } export const NotificationFooter: FC = ({ @@ -41,7 +40,7 @@ export const NotificationFooter: FC = ({ @@ -61,7 +60,7 @@ export const NotificationFooter: FC = ({ {reason.title} - + diff --git a/src/renderer/components/notifications/NotificationHeader.test.tsx b/src/renderer/components/notifications/NotificationHeader.test.tsx index a05b39cc2..98cfe0e71 100644 --- a/src/renderer/components/notifications/NotificationHeader.test.tsx +++ b/src/renderer/components/notifications/NotificationHeader.test.tsx @@ -87,7 +87,7 @@ describe('renderer/components/notifications/NotificationHeader.tsx', () => { expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); expect(openExternalLinkSpy).toHaveBeenCalledWith( - props.notification.repository.html_url, + props.notification.repository.htmlUrl, ); }); }); diff --git a/src/renderer/components/notifications/NotificationHeader.tsx b/src/renderer/components/notifications/NotificationHeader.tsx index 1753843a7..55c6178a1 100644 --- a/src/renderer/components/notifications/NotificationHeader.tsx +++ b/src/renderer/components/notifications/NotificationHeader.tsx @@ -3,14 +3,13 @@ import type { FC, MouseEvent } from 'react'; import { Stack } from '@primer/react'; import { useAppContext } from '../../context/App'; -import { GroupBy, Opacity, Size } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { type GitifyNotification, GroupBy, Opacity, Size } from '../../types'; import { cn } from '../../utils/cn'; import { openRepository } from '../../utils/links'; import { AvatarWithFallback } from '../avatars/AvatarWithFallback'; interface NotificationHeaderProps { - notification: Notification; + notification: GitifyNotification; } export const NotificationHeader: FC = ({ @@ -18,7 +17,7 @@ export const NotificationHeader: FC = ({ }: NotificationHeaderProps) => { const { settings } = useAppContext(); - const repoSlug = notification.repository.full_name; + const repoSlug = notification.repository.fullName; const notificationNumber = notification.subject?.number ? `#${notification.subject.number}` @@ -45,7 +44,7 @@ export const NotificationHeader: FC = ({ alt={repoSlug} name={repoSlug} size={Size.SMALL} - src={notification.repository.owner.avatar_url} + src={notification.repository.owner.avatarUrl} userType={notification.repository.owner.type} /> diff --git a/src/renderer/components/notifications/NotificationRow.tsx b/src/renderer/components/notifications/NotificationRow.tsx index 2a77f129f..c158689c6 100644 --- a/src/renderer/components/notifications/NotificationRow.tsx +++ b/src/renderer/components/notifications/NotificationRow.tsx @@ -4,8 +4,7 @@ import { BellSlashIcon, CheckIcon, ReadIcon } from '@primer/octicons-react'; import { Stack, Text, Tooltip } from '@primer/react'; import { useAppContext } from '../../context/App'; -import { GroupBy, Opacity, Size } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { type GitifyNotification, GroupBy, Opacity, Size } from '../../types'; import { cn } from '../../utils/cn'; import { isMarkAsDoneFeatureSupported } from '../../utils/features'; import { openNotification } from '../../utils/links'; @@ -16,7 +15,7 @@ import { NotificationFooter } from './NotificationFooter'; import { NotificationHeader } from './NotificationHeader'; interface NotificationRowProps { - notification: Notification; + notification: GitifyNotification; isAnimated?: boolean; } diff --git a/src/renderer/components/notifications/RepositoryNotifications.test.tsx b/src/renderer/components/notifications/RepositoryNotifications.test.tsx index 61bc44cfa..fc0a2220b 100644 --- a/src/renderer/components/notifications/RepositoryNotifications.test.tsx +++ b/src/renderer/components/notifications/RepositoryNotifications.test.tsx @@ -85,7 +85,7 @@ describe('renderer/components/notifications/RepositoryNotifications.tsx', () => }); it('should use default repository icon when avatar is not available', () => { - props.repoNotifications[0].repository.owner.avatar_url = '' as Link; + props.repoNotifications[0].repository.owner.avatarUrl = '' as Link; const tree = renderWithAppContext(); diff --git a/src/renderer/components/notifications/RepositoryNotifications.tsx b/src/renderer/components/notifications/RepositoryNotifications.tsx index cedf73ece..7f2f3ce0d 100644 --- a/src/renderer/components/notifications/RepositoryNotifications.tsx +++ b/src/renderer/components/notifications/RepositoryNotifications.tsx @@ -4,8 +4,7 @@ import { CheckIcon, ReadIcon } from '@primer/octicons-react'; import { Button, Stack } from '@primer/react'; import { useAppContext } from '../../context/App'; -import { Opacity, Size } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import { type GitifyNotification, Opacity, Size } from '../../types'; import { cn } from '../../utils/cn'; import { isMarkAsDoneFeatureSupported } from '../../utils/features'; import { getChevronDetails } from '../../utils/helpers'; @@ -16,7 +15,7 @@ import { HoverGroup } from '../primitives/HoverGroup'; import { NotificationRow } from './NotificationRow'; interface RepositoryNotificationsProps { - repoNotifications: Notification[]; + repoNotifications: GitifyNotification[]; repoName: string; } @@ -30,7 +29,7 @@ export const RepositoryNotifications: FC = ({ const [showRepositoryNotifications, setShowRepositoryNotifications] = useState(true); - const avatarUrl = repoNotifications[0].repository.owner.avatar_url; + const avatarUrl = repoNotifications[0].repository.owner.avatarUrl; const actionMarkAsDone = () => { setAnimateExit(!settings.delayNotificationState); diff --git a/src/renderer/components/notifications/__snapshots__/NotificationFooter.test.tsx.snap b/src/renderer/components/notifications/__snapshots__/NotificationFooter.test.tsx.snap index 9b06e32f9..c4782ce2f 100644 --- a/src/renderer/components/notifications/__snapshots__/NotificationFooter.test.tsx.snap +++ b/src/renderer/components/notifications/__snapshots__/NotificationFooter.test.tsx.snap @@ -74,7 +74,7 @@ exports[`renderer/components/notifications/NotificationFooter.tsx security alert class="flex gap-1" > -
- - Updated - - - May 20, 2017 - -
-
- - - - -
- - - - - , - "container":
-
-
-
- -
- - Updated - - - May 20, 2017 - -
-
- - - - -
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index e0b7ab0c0..4ca7a5f40 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -23,13 +23,13 @@ import type { FilterSettingsState, FilterSettingsValue, GitifyError, + GitifyNotification, SettingsState, SettingsValue, Status, Token, } from '../types'; import { FetchType } from '../types'; -import type { Notification } from '../typesGitHub'; import { headNotifications } from '../utils/api/client'; import type { LoginOAuthAppOptions, @@ -89,9 +89,13 @@ export interface AppContextState { fetchNotifications: () => Promise; removeAccountNotifications: (account: Account) => Promise; - markNotificationsAsRead: (notifications: Notification[]) => Promise; - markNotificationsAsDone: (notifications: Notification[]) => Promise; - unsubscribeNotification: (notification: Notification) => Promise; + markNotificationsAsRead: ( + notifications: GitifyNotification[], + ) => Promise; + markNotificationsAsDone: ( + notifications: GitifyNotification[], + ) => Promise; + unsubscribeNotification: (notification: GitifyNotification) => Promise; settings: SettingsState; clearFilters: () => void; @@ -444,19 +448,19 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { ); const markNotificationsAsReadWithAccounts = useCallback( - async (notifications: Notification[]) => + async (notifications: GitifyNotification[]) => await markNotificationsAsRead({ auth, settings }, notifications), [auth, settings, markNotificationsAsRead], ); const markNotificationsAsDoneWithAccounts = useCallback( - async (notifications: Notification[]) => + async (notifications: GitifyNotification[]) => await markNotificationsAsDone({ auth, settings }, notifications), [auth, settings, markNotificationsAsDone], ); const unsubscribeNotificationWithAccounts = useCallback( - async (notification: Notification) => + async (notification: GitifyNotification) => await unsubscribeNotification({ auth, settings }, notification), [auth, settings, unsubscribeNotification], ); diff --git a/src/renderer/hooks/useNotifications.test.ts b/src/renderer/hooks/useNotifications.test.ts index 9fc7b46de..e99f84bdc 100644 --- a/src/renderer/hooks/useNotifications.test.ts +++ b/src/renderer/hooks/useNotifications.test.ts @@ -41,8 +41,50 @@ describe('renderer/hooks/useNotifications.ts', () => { }; const notifications = [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, + { + id: '1', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', + subject: { + title: 'This is a notification.', + type: 'Issue', + url: null, + latest_comment_url: null, + }, + repository: { + name: 'test-repo', + full_name: 'org/test-repo', + html_url: 'https://github.com/org/test-repo', + owner: { + login: 'org', + avatar_url: 'https://avatar.url', + type: 'Organization', + }, + }, + }, + { + id: '2', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', + subject: { + title: 'This is another one.', + type: 'Issue', + url: null, + latest_comment_url: null, + }, + repository: { + name: 'test-repo', + full_name: 'org/test-repo', + html_url: 'https://github.com/org/test-repo', + owner: { + login: 'org', + avatar_url: 'https://avatar.url', + type: 'Organization', + }, + }, + }, ]; nock('https://api.github.com') @@ -77,34 +119,49 @@ describe('renderer/hooks/useNotifications.ts', () => { }); it('should fetch detailed notifications with success', async () => { + const mockRepository = { + name: 'notifications-test', + full_name: 'gitify-app/notifications-test', + html_url: 'https://github.com/gitify-app/notifications-test', + owner: { + login: 'gitify-app', + avatar_url: 'https://avatar.url', + type: 'Organization', + }, + }; + const mockNotifications = [ { - id: 1, + id: '1', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is a check suite workflow.', type: 'CheckSuite', url: null, latest_comment_url: null, }, - repository: { - full_name: 'gitify-app/notifications-test', - }, + repository: mockRepository, }, { - id: 2, + id: '2', + unread: true, + updated_at: '2024-02-26T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is a Discussion.', type: 'Discussion', url: null, latest_comment_url: null, }, - repository: { - full_name: 'gitify-app/notifications-test', - }, - updated_at: '2024-02-26T00:00:00Z', + repository: mockRepository, }, { - id: 3, + id: '3', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is an Issue.', type: 'Issue', @@ -112,12 +169,13 @@ describe('renderer/hooks/useNotifications.ts', () => { latest_comment_url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/3/comments', }, - repository: { - full_name: 'gitify-app/notifications-test', - }, + repository: mockRepository, }, { - id: 4, + id: '4', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is a Pull Request.', type: 'PullRequest', @@ -125,33 +183,33 @@ describe('renderer/hooks/useNotifications.ts', () => { latest_comment_url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/4/comments', }, - repository: { - full_name: 'gitify-app/notifications-test', - }, + repository: mockRepository, }, { - id: 5, + id: '5', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is an invitation.', type: 'RepositoryInvitation', url: null, latest_comment_url: null, }, - repository: { - full_name: 'gitify-app/notifications-test', - }, + repository: mockRepository, }, { - id: 6, + id: '6', + unread: true, + updated_at: '2024-01-01T00:00:00Z', + reason: 'subscribed', subject: { title: 'This is a workflow run.', type: 'WorkflowRun', url: null, latest_comment_url: null, }, - repository: { - full_name: 'gitify-app/notifications-test', - }, + repository: mockRepository, }, ]; diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index c636d1b25..a13d287d5 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -4,10 +4,10 @@ import type { Account, AccountNotifications, GitifyError, + GitifyNotification, GitifyState, Status, } from '../types'; -import type { Notification } from '../typesGitHub'; import { ignoreNotificationThreadSubscription, markNotificationThreadAsDone, @@ -44,15 +44,15 @@ interface NotificationsState { markNotificationsAsRead: ( state: GitifyState, - notifications: Notification[], + notifications: GitifyNotification[], ) => Promise; markNotificationsAsDone: ( state: GitifyState, - notifications: Notification[], + notifications: GitifyNotification[], ) => Promise; unsubscribeNotification: ( state: GitifyState, - notification: Notification, + notification: GitifyNotification, ) => Promise; } @@ -136,7 +136,7 @@ export const useNotifications = (): NotificationsState => { ); const markNotificationsAsRead = useCallback( - async (state: GitifyState, readNotifications: Notification[]) => { + async (state: GitifyState, readNotifications: GitifyNotification[]) => { setStatus('loading'); try { @@ -172,7 +172,7 @@ export const useNotifications = (): NotificationsState => { ); const markNotificationsAsDone = useCallback( - async (state: GitifyState, doneNotifications: Notification[]) => { + async (state: GitifyState, doneNotifications: GitifyNotification[]) => { if (!isMarkAsDoneFeatureSupported(doneNotifications[0].account)) { return; } @@ -212,7 +212,7 @@ export const useNotifications = (): NotificationsState => { ); const unsubscribeNotification = useCallback( - async (state: GitifyState, notification: Notification) => { + async (state: GitifyState, notification: GitifyNotification) => { setStatus('loading'); try { diff --git a/src/renderer/types.ts b/src/renderer/types.ts index e5c37d0c5..4dd565273 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -2,12 +2,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; -import type { - Notification, - Reason, - SubjectType, - UserType, -} from './typesGitHub'; +import type { Reason, SubjectType, UserType } from './typesGitHub'; import type { AuthorFieldsFragment, DiscussionStateReason, @@ -196,7 +191,7 @@ export interface RadioGroupItem { export interface AccountNotifications { account: Account; - notifications: Notification[]; + notifications: GitifyNotification[]; error: GitifyError | null; } @@ -265,25 +260,95 @@ export type FilterStateType = 'open' | 'closed' | 'merged' | 'draft' | 'other'; * * Gitify Notification Types * + * These types represent the clean, UI-focused notification structure + * used throughout the application. Raw GitHub API responses are + * transformed into these types at the API boundary. + * **/ +/** + * Complete notification type for UI consumption. + * Contains only fields actually used by components. + */ export interface GitifyNotification { + /** Unique notification ID from GitHub */ + id: string; + /** Whether the notification is unread */ + unread: boolean; + /** When the notification was last updated */ + updatedAt: string; + /** Reason for receiving the notification */ + reason: Reason; + /** Subject details (what the notification is about) */ + subject: GitifySubject; + /** Repository context */ + repository: GitifyRepository; + /** Account context (for API operations) */ account: Account; + /** UI ordering index */ order: number; } +/** + * Subject information combining REST and GraphQL enriched data. + */ export interface GitifySubject { + /** Subject title */ + title: string; + /** Subject type (Issue, PullRequest, etc.) */ + type: SubjectType; + /** API URL for the subject (used for GraphQL fetching) */ + url: Link | null; + /** API URL for the latest comment */ + latestCommentUrl: Link | null; + + // Enriched fields (from GraphQL - all optional) + /** Issue/PR/Discussion number */ number?: number; + /** Parsed state from GraphQL */ state?: GitifyNotificationState; + /** Latest comment/PR author */ user?: GitifyNotificationUser; + /** PR review states & reviewers */ reviews?: GitifyPullRequestReview[]; + /** PRs closing issues */ linkedIssues?: string[]; + /** Total comment count */ comments?: number; + /** Label names */ labels?: string[]; + /** Milestone state/title */ milestone?: MilestoneFieldsFragment; + /** Deep link to latest comment */ htmlUrl?: Link; } +/** + * Minimal repository information needed for UI. + */ +export interface GitifyRepository { + /** Repository name */ + name: string; + /** Full repository name (owner/repo) */ + fullName: string; + /** Repository web URL */ + htmlUrl: Link; + /** Repository owner */ + owner: GitifyOwner; +} + +/** + * Minimal owner information for avatar and navigation. + */ +export interface GitifyOwner { + /** Owner login name */ + login: string; + /** Owner avatar URL */ + avatarUrl: Link; + /** Owner type (User, Organization, Bot, etc.) */ + type: UserType; +} + export type GitifyNotificationUser = AuthorFieldsFragment; export interface GitifyPullRequestReview { diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index 559a3ebf9..ccf4579db 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -1,8 +1,6 @@ import type { components } from '@octokit/openapi-types'; -import type { GitifyNotification, GitifySubject, Link } from './types'; - -// TODO: #828 Add explicit types for GitHub API response vs Gitify Notifications object +import type { Link } from './types'; export interface GitHubRESTError { message: string; @@ -50,41 +48,18 @@ export type UserType = export type NotificationThreadSubscription = components['schemas']['thread-subscription']; -type BaseNotification = components['schemas']['thread']; type BaseUser = components['schemas']['simple-user']; type BaseRepository = components['schemas']['repository']; type BaseCommit = components['schemas']['commit']; type BaseCommitComment = components['schemas']['commit-comment']; type BaseRelease = components['schemas']['release']; -type BaseSubject = components['schemas']['thread']['subject']; - -// Strengthen user-related types with explicit property overrides -type GitHubNotification = Omit< - BaseNotification, - 'reason' | 'subject' | 'repository' -> & { - reason: Reason; - subject: Subject; - repository: Repository; -}; - -type GitHubSubject = Omit< - BaseSubject, - 'url' | 'latest_comment_url' | 'type' -> & { - url: Link | null; - latest_comment_url: Link | null; - type: SubjectType; -}; type StrengthenNullable = Omit & { [P in K]: T[P] extends null ? null : NonNullable & Extra; }; -// Exported strengthened types -export type Notification = GitHubNotification & GitifyNotification; - -export type Subject = GitHubSubject & GitifySubject; +// Exported strengthened types for REST API responses +// These are used only at the API boundary before transforming to GitifyNotification export type Repository = Omit & { html_url: Link; diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 4a514d7ed..5aff06858 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -2,13 +2,14 @@ import { mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount, } from '../../../__mocks__/account-mocks'; -import type { Link } from '../../../types'; import type { - Notification, - Owner, - Repository, - User, -} from '../../../typesGitHub'; + GitifyNotification, + GitifyNotificationUser, + GitifyOwner, + GitifyRepository, + Link, +} from '../../../types'; +import type { User } from '../../../typesGitHub'; export const mockNotificationUser = { id: 123456789, @@ -22,47 +23,44 @@ export const mockNotificationUser = { // 2 Notifications // Hostname : 'github.com' // Repository : 'gitify-app/notifications-test' -const mockGitHubOwner = { - id: 6333409, +const mockGitHubOwner: GitifyOwner = { login: 'gitify-app', - avatar_url: + avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, - url: 'https://api.github.com/users/gitify-app' as Link, - html_url: 'https://github.com/gitify-app' as Link, type: 'User', -} satisfies Partial; +}; -const mockGitHubRepository = { - id: 57216596, +const mockGitHubRepository: GitifyRepository = { name: 'notifications-test', - full_name: 'gitify-app/notifications-test', - owner: mockGitHubOwner as unknown as Owner, - html_url: 'https://github.com/gitify-app/notifications-test' as Link, -} satisfies Partial; + fullName: 'gitify-app/notifications-test', + owner: mockGitHubOwner, + htmlUrl: 'https://github.com/gitify-app/notifications-test' as Link, +}; -export const mockGitHubNotifications: Notification[] = [ +const mockSubjectUser: GitifyNotificationUser = { + login: 'gitify-app', + htmlUrl: 'https://github.com/gitify-app' as Link, + avatarUrl: + 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, + type: 'User', +}; + +export const mockGitHubNotifications: GitifyNotification[] = [ { account: mockGitHubCloudAccount, order: 0, id: '138661096', unread: true, reason: 'subscribed', - updated_at: '2017-05-20T17:51:57Z', - last_read_at: '2017-05-20T17:06:51Z', + updatedAt: '2017-05-20T17:51:57Z', subject: { title: 'I am a robot and this is a test!', url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, type: 'Issue', state: 'OPEN', - user: { - login: 'gitify-app', - html_url: 'https://github.com/gitify-app' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, - type: 'User', - }, + user: mockSubjectUser, reviews: [ { state: 'APPROVED', @@ -78,10 +76,7 @@ export const mockGitHubNotifications: Notification[] = [ }, ], }, - repository: mockGitHubRepository as unknown as Repository, - url: 'https://api.github.com/notifications/threads/138661096' as Link, - subscription_url: - 'https://api.github.com/notifications/threads/138661096/subscription' as Link, + repository: mockGitHubRepository, }, { account: mockGitHubCloudAccount, @@ -89,64 +84,52 @@ export const mockGitHubNotifications: Notification[] = [ id: '148827438', unread: true, reason: 'author', - updated_at: '2017-05-20T17:06:34Z', - last_read_at: '2017-05-20T16:59:03Z', + updatedAt: '2017-05-20T17:06:34Z', subject: { title: 'Improve the UI', url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/4' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302885965' as Link, type: 'Issue', reviews: null, }, - repository: mockGitHubRepository as unknown as Repository, - url: 'https://api.github.com/notifications/threads/148827438' as Link, - subscription_url: - 'https://api.github.com/notifications/threads/148827438/subscription' as Link, + repository: mockGitHubRepository, }, ]; // 2 Notifications // Hostname : 'github.gitify.io' // Repository : 'myorg/notifications-test' -const mockEnterpriseOwner = { +const mockEnterpriseOwner: GitifyOwner = { login: 'myorg', - id: 4, - avatar_url: 'https://github.gitify.io/avatars/u/4?' as Link, - url: 'https://github.gitify.io/api/v3/users/myorg', - html_url: 'https://github.gitify.io/myorg' as Link, + avatarUrl: 'https://github.gitify.io/avatars/u/4?' as Link, type: 'Organization', -} satisfies Partial; +}; -const mockEnterpriseRepository = { - id: 1, +const mockEnterpriseRepository: GitifyRepository = { name: 'notifications-test', - full_name: 'myorg/notifications-test', - owner: mockEnterpriseOwner as unknown as Owner, - html_url: 'https://github.gitify.io/myorg/notifications-test' as Link, -} satisfies Partial; + fullName: 'myorg/notifications-test', + owner: mockEnterpriseOwner, + htmlUrl: 'https://github.gitify.io/myorg/notifications-test' as Link, +}; -export const mockEnterpriseNotifications: Notification[] = [ +export const mockEnterpriseNotifications: GitifyNotification[] = [ { account: mockGitHubEnterpriseServerAccount, order: 0, id: '3', unread: true, reason: 'subscribed', - updated_at: '2017-05-20T13:02:48Z', - last_read_at: null, + updatedAt: '2017-05-20T13:02:48Z', subject: { title: 'Release 0.0.1', url: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/3' as Link, - latest_comment_url: + latestCommentUrl: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/3' as Link, type: 'Release', reviews: null, }, - repository: mockEnterpriseRepository as unknown as Repository, - url: 'https://github.gitify.io/api/v3/notifications/threads/4' as Link, - subscription_url: - 'https://github.gitify.io/api/v3/notifications/threads/4/subscription' as Link, + repository: mockEnterpriseRepository, }, { account: mockGitHubEnterpriseServerAccount, @@ -154,21 +137,18 @@ export const mockEnterpriseNotifications: Notification[] = [ id: '4', unread: true, reason: 'subscribed', - updated_at: '2017-05-20T15:52:20Z', - last_read_at: '2017-05-20T14:20:55Z', + updatedAt: '2017-05-20T15:52:20Z', subject: { title: 'Bump Version', url: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/4' as Link, - latest_comment_url: + latestCommentUrl: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/comments/21' as Link, type: 'PullRequest', reviews: null, }, - repository: mockEnterpriseRepository as unknown as Repository, - url: 'https://github.gitify.io/api/v3/notifications/threads/3' as Link, - subscription_url: - 'https://github.gitify.io/api/v3/notifications/threads/3/subscription' as Link, + repository: mockEnterpriseRepository, }, ]; -export const mockSingleNotification: Notification = mockGitHubNotifications[0]; +export const mockSingleNotification: GitifyNotification = + mockGitHubNotifications[0]; diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index 5187108cf..8a5bcb3f9 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -1,17 +1,21 @@ +import type { components } from '@octokit/openapi-types'; import type { AxiosPromise } from 'axios'; import type { ExecutionResult } from 'graphql'; import type { Account, + GitifyNotification, Hostname, Link, SettingsState, Token, } from '../../types'; + +type RawGitHubNotification = components['schemas']['thread']; + import type { Commit, CommitComment, - Notification, NotificationThreadSubscription, Release, } from '../../typesGitHub'; @@ -61,7 +65,7 @@ export function headNotifications( export function listNotificationsForAuthenticatedUser( account: Account, settings: SettingsState, -): AxiosPromise { +): AxiosPromise { const url = getGitHubAPIBaseUrl(account.hostname); url.pathname += 'notifications'; url.searchParams.append('participating', String(settings.participating)); @@ -197,7 +201,7 @@ export async function fetchAuthenticatedUserDetails( * Fetch GitHub Issue by Issue Number. */ export async function fetchIssueByNumber( - notification: Notification, + notification: GitifyNotification, ): Promise> { const url = getGitHubGraphQLUrl(notification.account.hostname); const number = getNumberFromUrl(notification.subject.url); @@ -220,7 +224,7 @@ export async function fetchIssueByNumber( * Fetch GitHub Pull Request by PR Number. */ export async function fetchPullByNumber( - notification: Notification, + notification: GitifyNotification, ): Promise> { const url = getGitHubGraphQLUrl(notification.account.hostname); const number = getNumberFromUrl(notification.subject.url); @@ -245,7 +249,7 @@ export async function fetchPullByNumber( * Fetch GitHub Discussion by Discussion Number. */ export async function fetchDiscussionByNumber( - notification: Notification, + notification: GitifyNotification, ): Promise> { const url = getGitHubGraphQLUrl(notification.account.hostname); const number = getNumberFromUrl(notification.subject.url); diff --git a/src/renderer/utils/api/graphql/common.graphql b/src/renderer/utils/api/graphql/common.graphql index 97219483b..6acf59c00 100644 --- a/src/renderer/utils/api/graphql/common.graphql +++ b/src/renderer/utils/api/graphql/common.graphql @@ -1,7 +1,7 @@ fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index bc49c280c..806d82aeb 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -15,14 +15,14 @@ import * as types from './graphql'; * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.AuthorFieldsFragmentDoc, + "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": typeof types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": typeof types.FetchIssueByNumberDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": typeof types.FetchPullRequestByNumberDocument, "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument, }; const documents: Documents = { - "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc, + "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": types.FetchIssueByNumberDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": types.FetchPullRequestByNumberDocument, @@ -32,7 +32,7 @@ const documents: Documents = { /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; +export function graphql(source: "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index 8ea49cf77..82d3cf2ae 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -35894,15 +35894,15 @@ export type WorkflowsParametersInput = { export type _Entity = Issue; -type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' }; +type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' }; -type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' }; +type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' }; -type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' }; +type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' }; -type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' }; +type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' }; -type AuthorFields_User_Fragment = { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' }; +type AuthorFields_User_Fragment = { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' }; export type AuthorFieldsFragment = | AuthorFields_Bot_Fragment @@ -35926,65 +35926,65 @@ export type FetchDiscussionByNumberQueryVariables = Exact<{ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; export type DiscussionDetailsFragment = { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null }; export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null }; export type DiscussionCommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null }; export type FetchIssueByNumberQueryVariables = Exact<{ @@ -35997,31 +35997,31 @@ export type FetchIssueByNumberQueryVariables = Exact<{ export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; export type IssueDetailsFragment = { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null }; export type FetchPullRequestByNumberQueryVariables = Exact<{ @@ -36036,17 +36036,17 @@ export type FetchPullRequestByNumberQueryVariables = Exact<{ export type FetchPullRequestByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } @@ -36056,17 +36056,17 @@ export type FetchPullRequestByNumberQuery = { __typename?: 'Query', repository?: | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; export type PullRequestDetailsFragment = { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } @@ -36109,8 +36109,8 @@ export class TypedDocumentString export const AuthorFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } `, {"fragmentName":"AuthorFields"}) as unknown as TypedDocumentString; @@ -36125,8 +36125,8 @@ export const CommentFieldsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename }`, {"fragmentName":"CommentFields"}) as unknown as TypedDocumentString; export const DiscussionCommentFieldsFragmentDoc = new TypedDocumentString(` @@ -36141,8 +36141,8 @@ export const DiscussionCommentFieldsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment CommentFields on DiscussionComment { @@ -36178,8 +36178,8 @@ export const DiscussionDetailsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment CommentFields on DiscussionComment { @@ -36236,8 +36236,8 @@ export const IssueDetailsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36296,8 +36296,8 @@ export const PullRequestDetailsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36320,8 +36320,8 @@ export const FetchDiscussionByNumberDocument = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment DiscussionDetails on Discussion { @@ -36373,8 +36373,8 @@ export const FetchIssueByNumberDocument = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36419,8 +36419,8 @@ export const FetchPullRequestByNumberDocument = new TypedDocumentString(` } fragment AuthorFields on Actor { login - html_url: url - avatar_url: avatarUrl + htmlUrl: url + avatarUrl type: __typename } fragment MilestoneFields on Milestone { diff --git a/src/renderer/utils/api/transform.ts b/src/renderer/utils/api/transform.ts new file mode 100644 index 000000000..1550343a1 --- /dev/null +++ b/src/renderer/utils/api/transform.ts @@ -0,0 +1,70 @@ +import type { components } from '@octokit/openapi-types'; + +import type { + Account, + GitifyNotification, + GitifyOwner, + GitifyRepository, + GitifySubject, + Link, +} from '../../types'; +import type { Reason, SubjectType, UserType } from '../../typesGitHub'; + +type RawGitHubNotification = components['schemas']['thread']; + +/** + * Transform a raw GitHub notification to GitifyNotification. + * Called immediately after REST API response is received. + * + * This is the ONLY place where raw GitHub types should be converted + * to Gitify's internal notification type. + */ +export function transformNotification( + raw: RawGitHubNotification, + account: Account, + order = 0, +): GitifyNotification { + return { + id: raw.id, + unread: raw.unread, + updatedAt: raw.updated_at, + reason: raw.reason as Reason, + subject: transformSubject(raw.subject), + repository: transformRepository(raw.repository), + account, + order, + }; +} + +function transformSubject( + raw: RawGitHubNotification['subject'], +): GitifySubject { + return { + title: raw.title, + type: raw.type as SubjectType, + url: raw.url as Link | null, + latestCommentUrl: raw.latest_comment_url as Link | null, + // Enriched fields start as undefined, populated by handlers + }; +} + +function transformRepository( + raw: RawGitHubNotification['repository'], +): GitifyRepository { + return { + name: raw.name, + fullName: raw.full_name, + htmlUrl: raw.html_url as Link, + owner: transformOwner(raw.owner), + }; +} + +function transformOwner( + raw: NonNullable, +): GitifyOwner { + return { + login: raw.login, + avatarUrl: raw.avatar_url as Link, + type: raw.type as UserType, + }; +} diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index 0bd6c7f3f..f85d9b3b8 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -5,8 +5,8 @@ import { } from '@primer/octicons-react'; import { mockToken } from '../__mocks__/state-mocks'; -import type { Hostname, Link } from '../types'; -import type { Subject, SubjectType } from '../typesGitHub'; +import type { GitifySubject, Hostname, Link } from '../types'; +import type { SubjectType } from '../typesGitHub'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as apiClient from './api/client'; import { @@ -85,10 +85,10 @@ describe('renderer/utils/helpers.ts', () => { const subject = { title: 'generate github web url unit tests', url: mockSubjectUrl, - latest_comment_url: mockLatestCommentUrl, + latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as Subject; + } as GitifySubject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -109,10 +109,10 @@ describe('renderer/utils/helpers.ts', () => { const subject = { title: 'generate github web url unit tests', url: mockSubjectUrl, - latest_comment_url: mockLatestCommentUrl, + latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as Subject; + } as GitifySubject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); @@ -138,10 +138,10 @@ describe('renderer/utils/helpers.ts', () => { const subject = { title: 'generate github web url unit tests', url: mockSubjectUrl, - latest_comment_url: mockLatestCommentUrl, + latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as Subject; + } as GitifySubject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 9580fc116..2b532462f 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -5,8 +5,7 @@ import { } from '@primer/octicons-react'; import { Constants } from '../constants'; -import type { Chevron, Hostname, Link } from '../types'; -import type { Notification } from '../typesGitHub'; +import type { Chevron, GitifyNotification, Hostname, Link } from '../types'; import { getHtmlUrl } from './api/client'; import type { PlatformType } from './auth/types'; import { rendererLogError } from './logger'; @@ -23,7 +22,7 @@ export function isEnterpriseServerHost(hostname: Hostname): boolean { } export function generateNotificationReferrerId( - notification: Notification, + notification: GitifyNotification, ): string { const raw = `018:NotificationThread${notification.id}:${notification.account.user.id}`; return btoa(raw); @@ -45,7 +44,7 @@ export function actionsURL(repositoryURL: string, filters: string[]): Link { } export async function generateGitHubWebUrl( - notification: Notification, + notification: GitifyNotification, ): Promise { const handler = createNotificationHandler(notification); const url = new URL(handler.defaultUrl(notification)); @@ -54,9 +53,9 @@ export async function generateGitHubWebUrl( url.href = notification.subject.htmlUrl; } else { try { - if (notification.subject.latest_comment_url) { + if (notification.subject.latestCommentUrl) { url.href = await getHtmlUrl( - notification.subject.latest_comment_url, + notification.subject.latestCommentUrl, notification.account.token, ); } else if (notification.subject.url) { diff --git a/src/renderer/utils/links.test.ts b/src/renderer/utils/links.test.ts index d311d648b..c5f169949 100644 --- a/src/renderer/utils/links.test.ts +++ b/src/renderer/utils/links.test.ts @@ -1,8 +1,12 @@ import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; -import { createPartialMockUser } from '../__mocks__/user-mocks'; +import { createMockAuthorFragment } from '../__mocks__/user-mocks'; import { Constants } from '../constants'; -import type { Hostname, Link } from '../types'; -import type { Repository } from '../typesGitHub'; +import type { + GitifyNotificationUser, + GitifyRepository, + Hostname, + Link, +} from '../types'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as authUtils from './auth/utils'; import * as comms from './comms'; @@ -71,7 +75,9 @@ describe('renderer/utils/links.ts', () => { }); it('openUserProfile', () => { - const mockUser = createPartialMockUser('mock-user'); + const mockUser = createMockAuthorFragment( + 'mock-user', + ) as GitifyNotificationUser; openUserProfile(mockUser); @@ -100,8 +106,8 @@ describe('renderer/utils/links.ts', () => { it('openRepository', () => { const mockHtmlUrl = 'https://github.com/gitify-app/gitify'; const repo = { - html_url: mockHtmlUrl, - } as Repository; + htmlUrl: mockHtmlUrl, + } as GitifyRepository; openRepository(repo); @@ -109,7 +115,7 @@ describe('renderer/utils/links.ts', () => { }); it('openNotification', async () => { - const mockNotificationUrl = mockSingleNotification.repository.html_url; + const mockNotificationUrl = mockSingleNotification.repository.htmlUrl; jest .spyOn(helpers, 'generateGitHubWebUrl') .mockResolvedValue(mockNotificationUrl); diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 4b0beb4f5..74535aae5 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -1,8 +1,14 @@ import { APPLICATION } from '../../shared/constants'; import { Constants } from '../constants'; -import type { Account, GitifyNotificationUser, Hostname, Link } from '../types'; -import type { Notification, Repository } from '../typesGitHub'; +import type { + Account, + GitifyNotification, + GitifyNotificationUser, + GitifyRepository, + Hostname, + Link, +} from '../types'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; import { generateGitHubWebUrl } from './helpers'; @@ -38,7 +44,7 @@ export function openAccountProfile(account: Account) { } export function openUserProfile(user: GitifyNotificationUser) { - openExternalLink(user.html_url); + openExternalLink(user.htmlUrl); } export function openHost(hostname: Hostname) { @@ -50,11 +56,11 @@ export function openDeveloperSettings(account: Account) { openExternalLink(url); } -export function openRepository(repository: Repository) { - openExternalLink(repository.html_url); +export function openRepository(repository: GitifyRepository) { + openExternalLink(repository.htmlUrl); } -export async function openNotification(notification: Notification) { +export async function openNotification(notification: GitifyNotification) { const url = await generateGitHubWebUrl(notification); openExternalLink(url); } diff --git a/src/renderer/utils/logger.ts b/src/renderer/utils/logger.ts index 02aab5bf2..4275c7e12 100644 --- a/src/renderer/utils/logger.ts +++ b/src/renderer/utils/logger.ts @@ -1,12 +1,12 @@ import { logError, logInfo, logWarn } from '../../shared/logger'; -import type { Notification } from '../typesGitHub'; +import type { GitifyNotification } from '../types'; // Renderer logger augments log entries with notification context formatting. export function rendererLogInfo( type: string, message: string, - notification?: Notification, + notification?: GitifyNotification, ) { logInfo(type, message, buildContexts(notification)); } @@ -14,7 +14,7 @@ export function rendererLogInfo( export function rendererLogWarn( type: string, message: string, - notification?: Notification, + notification?: GitifyNotification, ) { logWarn(type, message, buildContexts(notification)); } @@ -23,19 +23,19 @@ export function rendererLogError( type: string, message: string, err: Error, - notification?: Notification, + notification?: GitifyNotification, ) { logError(type, message, err, buildContexts(notification)); } -function buildContexts(notification?: Notification): string[] { +function buildContexts(notification?: GitifyNotification): string[] { if (!notification) { return []; } return [ notification.subject.type, - notification.repository.full_name, + notification.repository.fullName, notification.subject.title, ]; } diff --git a/src/renderer/utils/notifications/filters/filter.test.ts b/src/renderer/utils/notifications/filters/filter.test.ts index d0610f162..3d53bcef6 100644 --- a/src/renderer/utils/notifications/filters/filter.test.ts +++ b/src/renderer/utils/notifications/filters/filter.test.ts @@ -1,8 +1,12 @@ import { createPartialMockNotification } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { defaultSettings } from '../../../context/defaults'; -import type { Link, SearchToken, SettingsState } from '../../../types'; -import type { Owner } from '../../../typesGitHub'; +import type { + GitifyOwner, + Link, + SearchToken, + SettingsState, +} from '../../../types'; import { filterBaseNotifications, filterDetailedNotifications, @@ -21,8 +25,8 @@ describe('renderer/utils/notifications/filters/filter.ts', () => { title: 'User authored notification', user: { login: 'github-user', - html_url: 'https://github.com/user' as Link, - avatar_url: + htmlUrl: 'https://github.com/user' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, type: 'User', }, @@ -30,8 +34,10 @@ describe('renderer/utils/notifications/filters/filter.ts', () => { { owner: { login: 'gitify-app', - } as Owner, - full_name: 'gitify-app/gitify', + avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link, + type: 'Organization', + } as GitifyOwner, + fullName: 'gitify-app/gitify', }, ), createPartialMockNotification( @@ -39,8 +45,8 @@ describe('renderer/utils/notifications/filters/filter.ts', () => { title: 'Bot authored notification', user: { login: 'github-bot', - html_url: 'https://github.com/bot' as Link, - avatar_url: + htmlUrl: 'https://github.com/bot' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, type: 'Bot', }, @@ -48,8 +54,10 @@ describe('renderer/utils/notifications/filters/filter.ts', () => { { owner: { login: 'github', - } as Owner, - full_name: 'github/github', + avatarUrl: 'https://avatars.githubusercontent.com/u/2' as Link, + type: 'Organization', + } as GitifyOwner, + fullName: 'github/github', }, ), ]; diff --git a/src/renderer/utils/notifications/filters/filter.ts b/src/renderer/utils/notifications/filters/filter.ts index 9ebb2812f..a48619093 100644 --- a/src/renderer/utils/notifications/filters/filter.ts +++ b/src/renderer/utils/notifications/filters/filter.ts @@ -1,9 +1,9 @@ import type { + GitifyNotification, GitifyNotificationState, GitifyNotificationUser, SettingsState, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import { BASE_SEARCH_QUALIFIERS, DETAILED_ONLY_SEARCH_QUALIFIERS, @@ -18,9 +18,9 @@ import { } from '.'; export function filterBaseNotifications( - notifications: Notification[], + notifications: GitifyNotification[], settings: SettingsState, -): Notification[] { +): GitifyNotification[] { return notifications.filter((notification) => { let passesFilters = true; @@ -56,9 +56,9 @@ export function filterBaseNotifications( } export function filterDetailedNotifications( - notifications: Notification[], + notifications: GitifyNotification[], settings: SettingsState, -): Notification[] { +): GitifyNotification[] { return notifications.filter((notification) => { let passesFilters = true; @@ -89,7 +89,7 @@ export function hasActiveFilters(settings: SettingsState): boolean { * Apply include/exclude search token logic for a specific search qualifier prefix. */ function passesSearchTokenFiltersForQualifier( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, qualifier: SearchQualifier, ): boolean { @@ -126,7 +126,7 @@ function passesSearchTokenFiltersForQualifier( } function passesUserFilters( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, ): boolean { let passesFilters = true; @@ -154,7 +154,7 @@ function passesUserFilters( } function passesStateFilter( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, ): boolean { if (stateFilter.hasFilters(settings)) { @@ -170,7 +170,7 @@ export function isStateFilteredOut( state: GitifyNotificationState, settings: SettingsState, ): boolean { - const notification = { subject: { state: state } } as Notification; + const notification = { subject: { state: state } } as GitifyNotification; return !passesStateFilter(notification, settings); } @@ -179,7 +179,7 @@ export function isUserFilteredOut( user: GitifyNotificationUser, settings: SettingsState, ): boolean { - const notification = { subject: { user: user } } as Notification; + const notification = { subject: { user: user } } as GitifyNotification; return !passesUserFilters(notification, settings); } diff --git a/src/renderer/utils/notifications/filters/reason.ts b/src/renderer/utils/notifications/filters/reason.ts index 7cf825acc..f9b2e8390 100644 --- a/src/renderer/utils/notifications/filters/reason.ts +++ b/src/renderer/utils/notifications/filters/reason.ts @@ -1,9 +1,10 @@ import type { AccountNotifications, + GitifyNotification, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification, Reason } from '../../../typesGitHub'; +import type { Reason } from '../../../typesGitHub'; import { REASON_TYPE_DETAILS } from '../../reason'; import type { Filter } from './types'; @@ -37,7 +38,10 @@ export const reasonFilter: Filter = { ); }, - filterNotification(notification: Notification, reason: Reason): boolean { + filterNotification( + notification: GitifyNotification, + reason: Reason, + ): boolean { return notification.reason === reason; }, }; diff --git a/src/renderer/utils/notifications/filters/search.test.ts b/src/renderer/utils/notifications/filters/search.test.ts index de3d9d830..5bbfe941c 100644 --- a/src/renderer/utils/notifications/filters/search.test.ts +++ b/src/renderer/utils/notifications/filters/search.test.ts @@ -1,6 +1,5 @@ import { createPartialMockNotification } from '../../../__mocks__/notifications-mocks'; -import type { Link } from '../../../types'; -import type { Owner } from '../../../typesGitHub'; +import type { GitifyOwner, Link } from '../../../types'; import { ALL_SEARCH_QUALIFIERS, filterNotificationBySearchTerm, @@ -41,8 +40,8 @@ describe('renderer/utils/notifications/filters/search.ts', () => { title: 'User authored notification', user: { login: 'github-user', - html_url: 'https://github.com/user' as Link, - avatar_url: + htmlUrl: 'https://github.com/user' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, type: 'User', }, @@ -50,8 +49,10 @@ describe('renderer/utils/notifications/filters/search.ts', () => { { owner: { login: 'gitify-app', - } as Owner, - full_name: 'gitify-app/gitify', + avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link, + type: 'Organization', + } as GitifyOwner, + fullName: 'gitify-app/gitify', }, ); diff --git a/src/renderer/utils/notifications/filters/search.ts b/src/renderer/utils/notifications/filters/search.ts index 16e8ce87a..fba34c7e1 100644 --- a/src/renderer/utils/notifications/filters/search.ts +++ b/src/renderer/utils/notifications/filters/search.ts @@ -1,5 +1,4 @@ -import type { SettingsState } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, SettingsState } from '../../../types'; export const SEARCH_DELIMITER = ':'; @@ -8,19 +7,19 @@ const SEARCH_QUALIFIERS = { prefix: 'author:', description: 'filter by notification author', requiresDetailsNotifications: true, - extract: (n: Notification) => n.subject?.user?.login, + extract: (n: GitifyNotification) => n.subject?.user?.login, }, org: { prefix: 'org:', description: 'filter by organization owner', requiresDetailsNotifications: false, - extract: (n: Notification) => n.repository?.owner?.login, + extract: (n: GitifyNotification) => n.repository?.owner?.login, }, repo: { prefix: 'repo:', description: 'filter by repository full name', requiresDetailsNotifications: false, - extract: (n: Notification) => n.repository?.full_name, + extract: (n: GitifyNotification) => n.repository?.fullName, }, } as const; @@ -81,7 +80,7 @@ export function parseSearchInput(raw: string): ParsedSearchToken | null { } export function filterNotificationBySearchTerm( - notification: Notification, + notification: GitifyNotification, token: string, ): boolean { const parsed = parseSearchInput(token); diff --git a/src/renderer/utils/notifications/filters/state.test.ts b/src/renderer/utils/notifications/filters/state.test.ts index b2bd2df45..36b3df99a 100644 --- a/src/renderer/utils/notifications/filters/state.test.ts +++ b/src/renderer/utils/notifications/filters/state.test.ts @@ -1,5 +1,8 @@ -import type { FilterStateType, GitifyNotificationState } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { + FilterStateType, + GitifyNotification, + GitifyNotificationState, +} from '../../../types'; import { stateFilter } from './state'; describe('renderer/utils/notifications/filters/state.ts', () => { @@ -10,7 +13,7 @@ describe('renderer/utils/notifications/filters/state.ts', () => { describe('can filter by notification states', () => { const mockNotification = { subject: { state: 'OPEN' }, - } as Partial as Notification; + } as Partial as GitifyNotification; const cases = { OPEN: 'open', diff --git a/src/renderer/utils/notifications/filters/state.ts b/src/renderer/utils/notifications/filters/state.ts index 79fab7e36..027efdcd9 100644 --- a/src/renderer/utils/notifications/filters/state.ts +++ b/src/renderer/utils/notifications/filters/state.ts @@ -1,11 +1,11 @@ import type { AccountNotifications, FilterStateType, + GitifyNotification, GitifyNotificationState, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import type { Filter } from './types'; const STATE_TYPE_DETAILS: Record = { @@ -61,7 +61,7 @@ export const stateFilter: Filter = { }, filterNotification( - notification: Notification, + notification: GitifyNotification, stateType: FilterStateType, ): boolean { const mapped = mapStateToFilter(notification.subject?.state); diff --git a/src/renderer/utils/notifications/filters/subjectType.ts b/src/renderer/utils/notifications/filters/subjectType.ts index 4e444c078..486ffdf3d 100644 --- a/src/renderer/utils/notifications/filters/subjectType.ts +++ b/src/renderer/utils/notifications/filters/subjectType.ts @@ -1,9 +1,10 @@ import type { AccountNotifications, + GitifyNotification, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification, SubjectType } from '../../../typesGitHub'; +import type { SubjectType } from '../../../typesGitHub'; import type { Filter } from './types'; const SUBJECT_TYPE_DETAILS: Record = { @@ -71,7 +72,7 @@ export const subjectTypeFilter: Filter = { }, filterNotification( - notification: Notification, + notification: GitifyNotification, subjectType: SubjectType, ): boolean { return notification.subject.type === subjectType; diff --git a/src/renderer/utils/notifications/filters/types.ts b/src/renderer/utils/notifications/filters/types.ts index 438fda917..a0e6cdb54 100644 --- a/src/renderer/utils/notifications/filters/types.ts +++ b/src/renderer/utils/notifications/filters/types.ts @@ -1,9 +1,9 @@ import type { AccountNotifications, + GitifyNotification, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; export interface Filter { FILTER_TYPES: Record; @@ -18,5 +18,5 @@ export interface Filter { getFilterCount(accountNotifications: AccountNotifications[], type: T): number; - filterNotification(notification: Notification, type: T): boolean; + filterNotification(notification: GitifyNotification, type: T): boolean; } diff --git a/src/renderer/utils/notifications/filters/userType.test.ts b/src/renderer/utils/notifications/filters/userType.test.ts index 1055b6299..062a64203 100644 --- a/src/renderer/utils/notifications/filters/userType.test.ts +++ b/src/renderer/utils/notifications/filters/userType.test.ts @@ -1,4 +1,4 @@ -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification } from '../../../types'; import { isNonHumanUser, userTypeFilter } from './userType'; describe('renderer/utils/notifications/filters/userType.ts', () => { @@ -21,7 +21,7 @@ describe('renderer/utils/notifications/filters/userType.ts', () => { type: 'User', }, }, - } as Partial as Notification; + } as Partial as GitifyNotification; mockPartialNotification.subject.user.type = 'User'; expect( diff --git a/src/renderer/utils/notifications/filters/userType.ts b/src/renderer/utils/notifications/filters/userType.ts index 8c712127d..5d5b1e6a6 100644 --- a/src/renderer/utils/notifications/filters/userType.ts +++ b/src/renderer/utils/notifications/filters/userType.ts @@ -1,9 +1,10 @@ import type { AccountNotifications, + GitifyNotification, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification, UserType } from '../../../typesGitHub'; +import type { UserType } from '../../../typesGitHub'; import type { Filter } from './types'; const USER_TYPE_DETAILS: Record = { @@ -50,7 +51,10 @@ export const userTypeFilter: Filter = { ); }, - filterNotification(notification: Notification, userType: UserType): boolean { + filterNotification( + notification: GitifyNotification, + userType: UserType, + ): boolean { const allUserTypes = ['User', 'EnterpriseUserAccount']; if (userType === 'User') { diff --git a/src/renderer/utils/notifications/group.test.ts b/src/renderer/utils/notifications/group.test.ts index ba9d39642..33d3e0c09 100644 --- a/src/renderer/utils/notifications/group.test.ts +++ b/src/renderer/utils/notifications/group.test.ts @@ -1,7 +1,7 @@ import { createMockNotificationForRepoName } from '../../__mocks__/notifications-mocks'; import { mockSettings } from '../../__mocks__/state-mocks'; +import type { GitifyNotification } from '../../types'; import { GroupBy } from '../../types'; -import type { Notification } from '../../typesGitHub'; import { getFlattenedNotificationsByRepo, groupNotificationsByRepository, @@ -22,8 +22,8 @@ describe('renderer/utils/notifications/group.ts', () => { }); describe('groupNotificationsByRepository', () => { - it('groups notifications by repository full_name', () => { - const notifications: Notification[] = [ + it('groups notifications by repository fullName', () => { + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-a'), createMockNotificationForRepoName('2', 'owner/repo-b'), createMockNotificationForRepoName('3', 'owner/repo-a'), @@ -37,7 +37,7 @@ describe('renderer/utils/notifications/group.ts', () => { }); it('preserves first-seen repository order', () => { - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-c'), createMockNotificationForRepoName('2', 'owner/repo-a'), createMockNotificationForRepoName('3', 'owner/repo-b'), @@ -51,7 +51,7 @@ describe('renderer/utils/notifications/group.ts', () => { }); it('skips notifications without repository data', () => { - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-a'), createMockNotificationForRepoName('2', null), createMockNotificationForRepoName('3', 'owner/repo-a'), @@ -64,7 +64,7 @@ describe('renderer/utils/notifications/group.ts', () => { }); it('returns empty map when no notifications', () => { - const notifications: Notification[] = []; + const notifications: GitifyNotification[] = []; const result = groupNotificationsByRepository(notifications); @@ -72,7 +72,7 @@ describe('renderer/utils/notifications/group.ts', () => { }); it('returns empty map when all notifications lack repository data', () => { - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', null), createMockNotificationForRepoName('2', null), ]; @@ -86,7 +86,7 @@ describe('renderer/utils/notifications/group.ts', () => { describe('getFlattenedNotificationsByRepo', () => { it('returns repository-grouped order when groupBy is REPOSITORY', () => { const settings = { ...mockSettings, groupBy: GroupBy.REPOSITORY }; - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-b'), createMockNotificationForRepoName('2', 'owner/repo-a'), createMockNotificationForRepoName('3', 'owner/repo-b'), @@ -101,7 +101,7 @@ describe('renderer/utils/notifications/group.ts', () => { it('returns natural account order when groupBy is DATE', () => { const settings = { ...mockSettings, groupBy: GroupBy.DATE }; - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-b'), createMockNotificationForRepoName('2', 'owner/repo-a'), createMockNotificationForRepoName('3', 'owner/repo-b'), @@ -115,7 +115,7 @@ describe('renderer/utils/notifications/group.ts', () => { it('returns empty array when no notifications', () => { const settings = { ...mockSettings, groupBy: GroupBy.REPOSITORY }; - const notifications: Notification[] = []; + const notifications: GitifyNotification[] = []; const result = getFlattenedNotificationsByRepo(notifications, settings); @@ -124,7 +124,7 @@ describe('renderer/utils/notifications/group.ts', () => { it('handles notifications without repository data when grouped', () => { const settings = { ...mockSettings, groupBy: GroupBy.REPOSITORY }; - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-a'), createMockNotificationForRepoName('2', null), createMockNotificationForRepoName('3', 'owner/repo-a'), @@ -138,7 +138,7 @@ describe('renderer/utils/notifications/group.ts', () => { it('preserves notifications without repository data when not grouped', () => { const settings = { ...mockSettings, groupBy: GroupBy.DATE }; - const notifications: Notification[] = [ + const notifications: GitifyNotification[] = [ createMockNotificationForRepoName('1', 'owner/repo-a'), createMockNotificationForRepoName('2', null), createMockNotificationForRepoName('3', 'owner/repo-a'), diff --git a/src/renderer/utils/notifications/group.ts b/src/renderer/utils/notifications/group.ts index 38cc6b6ea..951de0b5c 100644 --- a/src/renderer/utils/notifications/group.ts +++ b/src/renderer/utils/notifications/group.ts @@ -1,5 +1,4 @@ -import type { SettingsState } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import type { GitifyNotification, SettingsState } from '../../types'; /** * Returns true when settings say to group by repository. @@ -9,17 +8,17 @@ export function isGroupByRepository(settings: SettingsState) { } /** - * Group notifications by repository.full_name preserving first-seen repository order. - * Returns a Map where keys are repo full_names and values are arrays of notifications. + * Group notifications by repository.fullName preserving first-seen repository order. + * Returns a Map where keys are repo fullNames and values are arrays of notifications. * Skips notifications without valid repository data. */ export function groupNotificationsByRepository( - notifications: Notification[], -): Map { - const repoGroups = new Map(); + notifications: GitifyNotification[], +): Map { + const repoGroups = new Map(); for (const notification of notifications) { - const repo = notification.repository?.full_name; + const repo = notification.repository?.fullName; // Skip notifications without valid repository data if (!repo) { @@ -42,9 +41,9 @@ export function groupNotificationsByRepository( * - natural notifications order otherwise */ export function getFlattenedNotificationsByRepo( - notifications: Notification[], + notifications: GitifyNotification[], settings: SettingsState, -): Notification[] { +): GitifyNotification[] { if (isGroupByRepository(settings)) { const groupedNotifications = groupNotificationsByRepository(notifications); diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index 1a992c7c6..5d44f7d4e 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -3,12 +3,12 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; +import type { GitifyNotification } from '../../../types'; import { type GitifyCheckSuiteStatus, IconColor, type Link, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import { checkSuiteHandler, getCheckSuiteAttributes } from './checkSuite'; describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { @@ -138,7 +138,8 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { mockSettings, ); - expect(result).toBeNull(); + // Returns empty object when state cannot be determined + expect(result).toEqual({}); }); it('unhandled check suite title', async () => { @@ -152,7 +153,8 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { mockSettings, ); - expect(result).toBeNull(); + // Returns empty object when title cannot be parsed + expect(result).toEqual({}); }); }); @@ -222,9 +224,9 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { title: 'Some notification', }, repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/actions`); }); diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index 1985c50d8..a8a6257e2 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -11,12 +11,12 @@ import { import { type GitifyCheckSuiteStatus, + type GitifyNotification, type GitifySubject, IconColor, type Link, type SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; import { DefaultHandler, defaultHandler } from './default'; @@ -32,9 +32,9 @@ class CheckSuiteHandler extends DefaultHandler { readonly type = 'CheckSuite'; async enrich( - notification: Notification, + notification: GitifyNotification, _settings: SettingsState, - ): Promise { + ): Promise> { const state = getCheckSuiteAttributes(notification)?.status; if (state) { @@ -45,10 +45,10 @@ class CheckSuiteHandler extends DefaultHandler { }; } - return null; + return {}; } - iconType(subject: Subject): FC | null { + iconType(subject: GitifySubject): FC | null { switch (subject.state as GitifyCheckSuiteStatus) { case 'CANCELLED': return StopIcon; @@ -63,7 +63,7 @@ class CheckSuiteHandler extends DefaultHandler { } } - iconColor(subject: Subject): IconColor { + iconColor(subject: GitifySubject): IconColor { switch (subject.state as GitifyCheckSuiteStatus) { case 'SUCCESS': return IconColor.GREEN; @@ -74,7 +74,7 @@ class CheckSuiteHandler extends DefaultHandler { } } - defaultUrl(notification: Notification): Link { + defaultUrl(notification: GitifyNotification): Link { return getCheckSuiteUrl(notification); } } @@ -86,7 +86,7 @@ export const checkSuiteHandler = new CheckSuiteHandler(); * but there isn't an obvious/clean way to do this currently. */ export function getCheckSuiteAttributes( - notification: Notification, + notification: GitifyNotification, ): CheckSuiteAttributes | null { const regex = /^(?.*?) workflow run(, Attempt #(?\d+))? (?.*?) for (?.*?) branch$/; @@ -127,7 +127,7 @@ function getCheckSuiteStatus( } } -export function getCheckSuiteUrl(notification: Notification): Link { +export function getCheckSuiteUrl(notification: GitifyNotification): Link { const filters = []; const checkSuiteAttributes = getCheckSuiteAttributes(notification); @@ -146,5 +146,5 @@ export function getCheckSuiteUrl(notification: Notification): Link { filters.push(`branch:${checkSuiteAttributes.branchName}`); } - return actionsURL(notification.repository.html_url, filters); + return actionsURL(notification.repository.htmlUrl, filters); } diff --git a/src/renderer/utils/notifications/handlers/commit.test.ts b/src/renderer/utils/notifications/handlers/commit.test.ts index a846bb150..66978d7d8 100644 --- a/src/renderer/utils/notifications/handlers/commit.test.ts +++ b/src/renderer/utils/notifications/handlers/commit.test.ts @@ -7,8 +7,7 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { commitHandler } from './commit'; describe('renderer/utils/notifications/handlers/commit.ts', () => { @@ -27,7 +26,7 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { title: 'This is a commit with comments', type: 'Commit', url: 'https://api.github.com/repos/gitify-app/notifications-test/commits/d2a86d80e3d24ea9510d5de6c147e53c30f313a8' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/comments/141012658' as Link, }); @@ -47,8 +46,8 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { state: null, user: { login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, + htmlUrl: mockCommenter.html_url, + avatarUrl: mockCommenter.avatar_url, type: mockCommenter.type, }, }); @@ -59,7 +58,7 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { title: 'This is a commit with comments', type: 'Commit', url: 'https://api.github.com/repos/gitify-app/notifications-test/commits/d2a86d80e3d24ea9510d5de6c147e53c30f313a8' as Link, - latest_comment_url: null, + latestCommentUrl: null, }); nock('https://api.github.com') @@ -74,8 +73,8 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { state: null, user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.html_url, + avatarUrl: mockAuthor.avatar_url, type: mockAuthor.type, }, }); @@ -86,7 +85,7 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { title: 'This is a commit with comments', type: 'Commit', url: 'https://api.github.com/repos/gitify-app/notifications-test/commits/d2a86d80e3d24ea9510d5de6c147e53c30f313a8' as Link, - latest_comment_url: null, + latestCommentUrl: null, }); const result = await commitHandler.enrich(mockNotification, { @@ -94,7 +93,8 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { filterStates: ['closed'], }); - expect(result).toEqual(null); + // Returns empty object when filtered (no API call made) + expect(result).toEqual({}); }); }); @@ -111,9 +111,9 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { expect( commitHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(mockHtmlUrl); }); }); diff --git a/src/renderer/utils/notifications/handlers/commit.ts b/src/renderer/utils/notifications/handlers/commit.ts index 4187c5056..38141a352 100644 --- a/src/renderer/utils/notifications/handlers/commit.ts +++ b/src/renderer/utils/notifications/handlers/commit.ts @@ -4,12 +4,12 @@ import type { OcticonProps } from '@primer/octicons-react'; import { GitCommitIcon } from '@primer/octicons-react'; import type { + GitifyNotification, GitifyNotificationState, GitifyNotificationUser, GitifySubject, SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { getCommit, getCommitComment } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; @@ -19,27 +19,32 @@ class CommitHandler extends DefaultHandler { readonly type = 'Commit'; async enrich( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, - ): Promise { + ): Promise> { const commitState: GitifyNotificationState = null; // Commit notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(commitState, settings)) { - return null; + return {}; } let user: GitifyNotificationUser; - if (notification.subject.latest_comment_url) { + if (notification.subject.latestCommentUrl) { const commitComment = ( await getCommitComment( - notification.subject.latest_comment_url, + notification.subject.latestCommentUrl, notification.account.token, ) ).data; - user = commitComment.user; + user = { + login: commitComment.user.login, + avatarUrl: commitComment.user.avatar_url, + htmlUrl: commitComment.user.html_url, + type: commitComment.user.type as GitifyNotificationUser['type'], + }; } else { const commit = ( await getCommit(notification.subject.url, notification.account.token) @@ -47,9 +52,9 @@ class CommitHandler extends DefaultHandler { user = { login: commit.author.login, - avatar_url: commit.author.avatar_url, - html_url: commit.author.html_url, - type: commit.author.type, + avatarUrl: commit.author.avatar_url, + htmlUrl: commit.author.html_url, + type: commit.author.type as GitifyNotificationUser['type'], }; } @@ -59,7 +64,7 @@ class CommitHandler extends DefaultHandler { }; } - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): 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 bc7371727..346f63199 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -3,12 +3,12 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; +import type { GitifyNotification } from '../../../types'; import { type GitifyNotificationState, IconColor, type Link, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import { defaultHandler } from './default'; describe('renderer/utils/notifications/handlers/default.ts', () => { @@ -25,7 +25,8 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { mockSettings, ); - expect(result).toBeNull(); + // Default handler returns empty object (no enrichment) + expect(result).toEqual({}); }); }); @@ -131,9 +132,9 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { expect( defaultHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(mockHtmlUrl); }); }); diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 8c674b6a3..e27e7a1ea 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -3,9 +3,14 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { QuestionIcon } from '@primer/octicons-react'; -import type { GitifySubject, Link, SettingsState } from '../../../types'; +import type { + GitifyNotification, + GitifySubject, + Link, + SettingsState, +} from '../../../types'; import { IconColor } from '../../../types'; -import type { Notification, Subject, SubjectType } from '../../../typesGitHub'; +import type { SubjectType } from '../../../typesGitHub'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; @@ -13,34 +18,34 @@ export class DefaultHandler implements NotificationTypeHandler { type?: SubjectType; async enrich( - _notification: Notification, + _notification: GitifyNotification, _settings: SettingsState, - ): Promise { - return null; + ): Promise> { + return {}; } - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return QuestionIcon; } - iconColor(_subject: Subject): IconColor { + iconColor(_subject: GitifySubject): IconColor { return IconColor.GRAY; } - formattedNotificationType(notification: Notification): string { + formattedNotificationType(notification: GitifyNotification): string { return formatForDisplay([ notification.subject.state, notification.subject.type, ]); } - formattedNotificationNumber(notification: Notification): string { + formattedNotificationNumber(notification: GitifyNotification): string { return notification.subject?.number ? `#${notification.subject.number}` : ''; } - formattedNotificationTitle(notification: Notification): string { + formattedNotificationTitle(notification: GitifyNotification): string { let title = notification.subject.title; if (notification.subject?.number) { @@ -49,8 +54,8 @@ export class DefaultHandler implements NotificationTypeHandler { return title; } - defaultUrl(notification: Notification): Link { - return notification.repository.html_url; + defaultUrl(notification: GitifyNotification): Link { + return notification.repository.htmlUrl; } } diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 743f5373a..27dfed08f 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -6,23 +6,23 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createPartialMockUser } from '../../../__mocks__/user-mocks'; +import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import type { GitifyNotification } from '../../../types'; import { type GitifyDiscussionState, type GitifySubject, IconColor, type Link, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import type { DiscussionDetailsFragment, DiscussionStateReason, } from '../../api/graphql/generated/graphql'; import { discussionHandler } from './discussion'; -const mockAuthor = createPartialMockUser('discussion-author'); -const mockCommenter = createPartialMockUser('discussion-commenter'); -const mockReplier = createPartialMockUser('discussion-replier'); +const mockAuthor = createMockAuthorFragment('discussion-author'); +const mockCommenter = createMockAuthorFragment('discussion-commenter'); +const mockReplier = createMockAuthorFragment('discussion-replier'); describe('renderer/utils/notifications/handlers/discussion.ts', () => { describe('enrich', () => { @@ -30,9 +30,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { title: 'This is a mock discussion', type: 'Discussion', url: 'https://api.github.com/repos/gitify-app/notifications-test/discussions/123' as Link, - latest_comment_url: null, + latestCommentUrl: null, }); - mockNotification.updated_at = '2024-01-01T00:00:00Z'; + mockNotification.updatedAt = '2024-01-01T00:00:00Z'; beforeEach(() => { // axios will default to using the XHR adapter which can't be intercepted @@ -63,15 +63,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123', - } as GitifySubject); + } as Partial); }); it('open / unanswered discussion - no stateReason', async () => { @@ -97,15 +97,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123', - } as GitifySubject); + } as Partial); }); it('discussion with stateReason - stateReason always takes precedence', async () => { @@ -134,15 +134,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'DUPLICATE', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123', - } as GitifySubject); + } as Partial); }); it('discussion with labels', async () => { @@ -175,15 +175,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, labels: ['enhancement'], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123', - } as GitifySubject); + } as Partial); }); it('discussion with comments', async () => { @@ -223,15 +223,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, + htmlUrl: mockCommenter.htmlUrl, + avatarUrl: mockCommenter.avatarUrl, type: mockCommenter.type, }, comments: 1, labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-1234', - } as GitifySubject); + } as Partial); }); it('discussion with comments and replies', async () => { @@ -277,15 +277,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockReplier.login, - html_url: mockReplier.html_url, - avatar_url: mockReplier.avatar_url, + htmlUrl: mockReplier.htmlUrl, + avatarUrl: mockReplier.avatarUrl, type: mockReplier.type, }, comments: 1, labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-6789', - } as GitifySubject); + } as Partial); }); }); @@ -338,9 +338,9 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { expect( discussionHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/discussions`); }); }); diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index 57ca0e651..f7290dc56 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -12,12 +12,12 @@ import { differenceInMilliseconds } from 'date-fns'; import { type GitifyDiscussionState, + type GitifyNotification, type GitifySubject, IconColor, type Link, type SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { fetchDiscussionByNumber } from '../../api/client'; import type { CommentFieldsFragment, @@ -30,9 +30,9 @@ class DiscussionHandler extends DefaultHandler { readonly type = 'Discussion'; async enrich( - notification: Notification, + notification: GitifyNotification, _settings: SettingsState, - ): Promise { + ): Promise> { const response = await fetchDiscussionByNumber(notification); const discussion = response.data.repository?.discussion; @@ -64,7 +64,7 @@ class DiscussionHandler extends DefaultHandler { }; } - iconType(subject: Subject): FC | null { + iconType(subject: GitifySubject): FC | null { switch (subject.state as GitifyDiscussionState) { case 'DUPLICATE': return DiscussionDuplicateIcon; @@ -77,7 +77,7 @@ class DiscussionHandler extends DefaultHandler { } } - iconColor(subject: Subject): IconColor { + iconColor(subject: GitifySubject): IconColor { switch (subject.state) { case 'ANSWERED': return IconColor.GREEN; @@ -88,8 +88,8 @@ class DiscussionHandler extends DefaultHandler { } } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/discussions'; return url.href as Link; } @@ -98,14 +98,14 @@ class DiscussionHandler extends DefaultHandler { export const discussionHandler = new DiscussionHandler(); export function getClosestDiscussionCommentOrReply( - notification: Notification, + notification: GitifyNotification, comments: DiscussionCommentFieldsFragment[], ): CommentFieldsFragment | null { if (!comments || comments.length === 0) { return null; } - const targetTimestamp = notification.updated_at; + const targetTimestamp = notification.updatedAt; const allCommentsAndReplies = comments.flatMap((comment) => [ ...comment.replies.nodes, diff --git a/src/renderer/utils/notifications/handlers/index.ts b/src/renderer/utils/notifications/handlers/index.ts index b6e5d539f..449f616b4 100644 --- a/src/renderer/utils/notifications/handlers/index.ts +++ b/src/renderer/utils/notifications/handlers/index.ts @@ -1,4 +1,4 @@ -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification } from '../../../types'; import { checkSuiteHandler } from './checkSuite'; import { commitHandler } from './commit'; import { defaultHandler } from './default'; @@ -13,7 +13,7 @@ import type { NotificationTypeHandler } from './types'; import { workflowRunHandler } from './workflowRun'; export function createNotificationHandler( - notification: Notification, + notification: GitifyNotification, ): NotificationTypeHandler { switch (notification.subject.type) { case 'CheckSuite': diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index af700653f..a151fa610 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -6,14 +6,14 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createPartialMockUser } from '../../../__mocks__/user-mocks'; +import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import type { GitifyNotification } from '../../../types'; import { type GitifyIssueState, type GitifySubject, IconColor, type Link, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import type { IssueDetailsFragment, IssueState, @@ -21,19 +21,19 @@ import type { } from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; -const mockAuthor = createPartialMockUser('issue-author'); -const mockCommenter = createPartialMockUser('issue-commenter'); +const mockAuthor = createMockAuthorFragment('issue-author'); +const mockCommenter = createMockAuthorFragment('issue-commenter'); describe('renderer/utils/notifications/handlers/issue.ts', () => { describe('enrich', () => { - let mockNotification: Notification; + let mockNotification: GitifyNotification; beforeEach(() => { mockNotification = createPartialMockNotification({ title: 'This is a mock issue', type: 'Issue', url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, }); @@ -64,15 +64,15 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: null, - } as GitifySubject); + } as Partial); }); it('issue with stateReason - prefer stateReason over state when available', async () => { @@ -98,15 +98,15 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'COMPLETED', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: null, - } as GitifySubject); + } as Partial); }); it('issue with comments', async () => { @@ -140,8 +140,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, + htmlUrl: mockCommenter.htmlUrl, + avatarUrl: mockCommenter.avatarUrl, type: mockCommenter.type, }, comments: 1, @@ -149,7 +149,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', labels: [], milestone: null, - } as GitifySubject); + } as Partial); }); it('with labels', async () => { @@ -177,15 +177,15 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: ['enhancement'], milestone: null, - } as GitifySubject); + } as Partial); }); it('with milestone', async () => { @@ -214,8 +214,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, @@ -225,7 +225,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', title: 'Open Milestone', }, - } as GitifySubject); + } as Partial); }); }); @@ -278,9 +278,9 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { expect( issueHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/issues`); }); }); diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 2a1fb9206..13de90dd2 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -10,12 +10,12 @@ import { import type { GitifyIssueState, + GitifyNotification, GitifySubject, Link, SettingsState, } from '../../../types'; import { IconColor } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; import { DefaultHandler, defaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -24,9 +24,9 @@ class IssueHandler extends DefaultHandler { readonly type = 'Issue'; async enrich( - notification: Notification, + notification: GitifyNotification, _settings: SettingsState, - ): Promise { + ): Promise> { const response = await fetchIssueByNumber(notification); const issue = response.data.repository?.issue; @@ -50,7 +50,7 @@ class IssueHandler extends DefaultHandler { }; } - iconType(subject: Subject): FC | null { + iconType(subject: GitifySubject): FC | null { switch (subject.state as GitifyIssueState) { case 'CLOSED': case 'COMPLETED': @@ -65,7 +65,7 @@ class IssueHandler extends DefaultHandler { } } - iconColor(subject: Subject): IconColor { + iconColor(subject: GitifySubject): IconColor { switch (subject.state as GitifyIssueState) { case 'OPEN': case 'REOPENED': @@ -79,8 +79,8 @@ class IssueHandler extends DefaultHandler { } } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/issues'; return url.href as Link; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index ce1485be3..0a646156f 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -6,14 +6,14 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createPartialMockUser } from '../../../__mocks__/user-mocks'; +import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import type { GitifyNotification } from '../../../types'; import { type GitifyPullRequestState, type GitifySubject, IconColor, type Link, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; import type { PullRequestDetailsFragment, PullRequestReviewState, @@ -21,18 +21,18 @@ import type { } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; -const mockAuthor = createPartialMockUser('some-author'); -const mockCommenter = createPartialMockUser('some-commenter'); +const mockAuthor = createMockAuthorFragment('some-author'); +const mockCommenter = createMockAuthorFragment('some-commenter'); describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { - let mockNotification: Notification; + let mockNotification: GitifyNotification; beforeEach(() => { mockNotification = createPartialMockNotification({ title: 'This is a mock pull request', type: 'PullRequest', url: 'https://api.github.com/repos/gitify-app/notifications-test/pulls/1' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, }); }); @@ -67,8 +67,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'CLOSED', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -77,7 +77,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('draft pull request state', async () => { @@ -106,8 +106,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'DRAFT', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -116,7 +116,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('merge queue pull request state', async () => { @@ -145,8 +145,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'MERGE_QUEUE', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -155,7 +155,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('merged pull request state', async () => { @@ -184,8 +184,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'MERGED', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -194,7 +194,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('with comments', async () => { @@ -231,8 +231,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, + htmlUrl: mockCommenter.htmlUrl, + avatarUrl: mockCommenter.avatarUrl, type: mockCommenter.type, }, reviews: null, @@ -242,7 +242,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234', - } as GitifySubject); + } as Partial); }); it('with labels', async () => { @@ -277,8 +277,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -287,7 +287,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('with linked issues', async () => { @@ -322,8 +322,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -332,7 +332,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); it('with milestone', async () => { @@ -364,8 +364,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, reviews: null, @@ -377,7 +377,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { title: 'Open Milestone', }, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); + } as Partial); }); }); @@ -428,9 +428,9 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect( pullRequestHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/pulls`); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 55610e761..5fc237f16 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -10,6 +10,7 @@ import { } from '@primer/octicons-react'; import { + type GitifyNotification, type GitifyPullRequestReview, type GitifyPullRequestState, type GitifySubject, @@ -17,7 +18,6 @@ import { type Link, type SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; import type { PullRequestReviewFieldsFragment } from '../../api/graphql/generated/graphql'; import { DefaultHandler, defaultHandler } from './default'; @@ -27,9 +27,9 @@ class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; async enrich( - notification: Notification, + notification: GitifyNotification, _settings: SettingsState, - ): Promise { + ): Promise> { const response = await fetchPullByNumber(notification); const pr = response.data.repository.pullRequest; @@ -61,7 +61,7 @@ class PullRequestHandler extends DefaultHandler { }; } - iconType(subject: Subject): FC | null { + iconType(subject: GitifySubject): FC | null { switch (subject.state as GitifyPullRequestState) { case 'DRAFT': return GitPullRequestDraftIcon; @@ -76,7 +76,7 @@ class PullRequestHandler extends DefaultHandler { } } - iconColor(subject: Subject): IconColor { + iconColor(subject: GitifySubject): IconColor { switch (subject.state as GitifyPullRequestState) { case 'OPEN': return IconColor.GREEN; @@ -91,8 +91,8 @@ class PullRequestHandler extends DefaultHandler { } } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/pulls'; return url.href as Link; } diff --git a/src/renderer/utils/notifications/handlers/release.test.ts b/src/renderer/utils/notifications/handlers/release.test.ts index 27490a9cb..26e1905e7 100644 --- a/src/renderer/utils/notifications/handlers/release.test.ts +++ b/src/renderer/utils/notifications/handlers/release.test.ts @@ -7,8 +7,7 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { releaseHandler } from './release'; describe('renderer/utils/notifications/handlers/release.ts', () => { @@ -26,7 +25,7 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { title: 'This is a mock release', type: 'Release', url: 'https://api.github.com/repos/gitify-app/notifications-test/releases/1' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/releases/1' as Link, }); @@ -43,8 +42,8 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { state: null, user: { login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.html_url, + avatarUrl: mockAuthor.avatar_url, type: mockAuthor.type, }, }); @@ -55,7 +54,7 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { title: 'This is a mock release', type: 'Release', url: 'https://api.github.com/repos/gitify-app/notifications-test/releases/1' as Link, - latest_comment_url: + latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/releases/1' as Link, }); @@ -64,7 +63,8 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { filterStates: ['closed'], }); - expect(result).toEqual(null); + // Returns empty object when filtered (no API call made) + expect(result).toEqual({}); }); }); @@ -85,9 +85,9 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { expect( releaseHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/releases`); }); }); diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index 9b2fe3149..c66df4b87 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -4,12 +4,13 @@ import type { OcticonProps } from '@primer/octicons-react'; import { TagIcon } from '@primer/octicons-react'; import type { + GitifyNotification, GitifyNotificationState, + GitifyNotificationUser, GitifySubject, Link, SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { getRelease } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; @@ -19,32 +20,41 @@ class ReleaseHandler extends DefaultHandler { readonly type = 'Release'; async enrich( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, - ): Promise { + ): Promise> { const releaseState: GitifyNotificationState = null; // Release notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(releaseState, settings)) { - return null; + return {}; } const release = ( await getRelease(notification.subject.url, notification.account.token) ).data; + const user: GitifyNotificationUser = release.author + ? { + login: release.author.login, + avatarUrl: release.author.avatar_url, + htmlUrl: release.author.html_url, + type: release.author.type as GitifyNotificationUser['type'], + } + : null; + return { state: releaseState, - user: getNotificationAuthor([release.author]), + user: getNotificationAuthor([user]), }; } - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return TagIcon; } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/releases'; return url.href as Link; } diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts index c754eba5a..985d8aed1 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts @@ -1,6 +1,5 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { repositoryDependabotAlertsThreadHandler } from './repositoryDependabotAlertsThread'; describe('renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts', () => { @@ -21,9 +20,9 @@ describe('renderer/utils/notifications/handlers/repositoryDependabotAlertsThread expect( repositoryDependabotAlertsThreadHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/security/dependabot`); }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts index c71747d48..5616399c7 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts @@ -3,19 +3,18 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { AlertIcon } from '@primer/octicons-react'; -import type { Link } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; +import type { GitifyNotification, GitifySubject, Link } from '../../../types'; import { DefaultHandler } from './default'; class RepositoryDependabotAlertsThreadHandler extends DefaultHandler { readonly type = 'RepositoryDependabotAlertsThread'; - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return AlertIcon; } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/security/dependabot'; return url.href as Link; } diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts index a9b08ed5d..1d685cebd 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts @@ -1,6 +1,5 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { repositoryInvitationHandler } from './repositoryInvitation'; describe('renderer/utils/notifications/handlers/repositoryInvitation.ts', () => { @@ -21,9 +20,9 @@ describe('renderer/utils/notifications/handlers/repositoryInvitation.ts', () => expect( repositoryInvitationHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/invitations`); }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts index a5beaea28..82d01c044 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts @@ -2,19 +2,18 @@ import type { FC } from 'react'; import { MailIcon, type OcticonProps } from '@primer/octicons-react'; -import type { Link } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; +import type { GitifyNotification, GitifySubject, Link } from '../../../types'; import { DefaultHandler } from './default'; class RepositoryInvitationHandler extends DefaultHandler { readonly type = 'RepositoryInvitation'; - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return MailIcon; } - defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); + defaultUrl(notification: GitifyNotification): Link { + const url = new URL(notification.repository.htmlUrl); url.pathname += '/invitations'; return url.href as Link; } diff --git a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts index 73ae231ca..12070b027 100644 --- a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts @@ -1,6 +1,5 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { repositoryVulnerabilityAlertHandler } from './repositoryVulnerabilityAlert'; describe('renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts', () => { @@ -21,9 +20,9 @@ describe('renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts' expect( repositoryVulnerabilityAlertHandler.defaultUrl({ repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(mockHtmlUrl); }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts index f3b8e77e7..6d1011a09 100644 --- a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts +++ b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts @@ -2,13 +2,13 @@ import type { FC } from 'react'; import { AlertIcon, type OcticonProps } from '@primer/octicons-react'; -import type { Subject } from '../../../typesGitHub'; +import type { GitifySubject } from '../../../types'; import { DefaultHandler } from './default'; class RepositoryVulnerabilityAlertHandler extends DefaultHandler { readonly type = 'RepositoryVulnerabilityAlert'; - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return AlertIcon; } } diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index ced167101..bc69ee1b8 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -2,8 +2,13 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; -import type { GitifySubject, Link, SettingsState } from '../../../types'; -import type { Notification, Subject, SubjectType } from '../../../typesGitHub'; +import type { + GitifyNotification, + GitifySubject, + Link, + SettingsState, +} from '../../../types'; +import type { SubjectType } from '../../../typesGitHub'; export interface NotificationTypeHandler { readonly type?: SubjectType; @@ -12,37 +17,37 @@ export interface NotificationTypeHandler { * Enrich a notification. Settings may be unused for some handlers. */ enrich( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, - ): Promise; + ): Promise>; /** * Return the icon component for this notification type. */ - iconType(subject: Subject): FC | null; + iconType(subject: GitifySubject): FC | null; /** * Return the icon color for this notification type. */ - iconColor(subject: Subject): string | undefined; + iconColor(subject: GitifySubject): string | undefined; /** * Return the formatted notification type for this notification. */ - formattedNotificationType(notification: Notification): string; + formattedNotificationType(notification: GitifyNotification): string; /** * Return the formatted notification number for this notification. */ - formattedNotificationNumber(notification: Notification): string; + formattedNotificationNumber(notification: GitifyNotification): string; /** * Return the formatted notification title for this notification. */ - formattedNotificationTitle(notification: Notification): string; + formattedNotificationTitle(notification: GitifyNotification): string; /** * Default url for notification type. */ - defaultUrl(notification: Notification): Link; + defaultUrl(notification: GitifyNotification): Link; } diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index 3b18b6a94..90b677682 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -1,9 +1,9 @@ -import { createPartialMockUser } from '../../../__mocks__/user-mocks'; +import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; import { formatForDisplay, getNotificationAuthor } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { describe('getNotificationAuthor', () => { - const mockAuthor = createPartialMockUser('some-author'); + const mockAuthor = createMockAuthorFragment('some-author'); it('returns null when all users are null', () => { const result = getNotificationAuthor([null, null]); @@ -16,8 +16,8 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { expect(result).toEqual({ login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }); }); @@ -27,8 +27,8 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { expect(result).toEqual({ login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, + htmlUrl: mockAuthor.htmlUrl, + avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }); }); diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index 4f9187d36..e46da63e2 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -14,8 +14,8 @@ export function getNotificationAuthor( if (user) { subjectUser = { login: user.login, - html_url: user.html_url, - avatar_url: user.avatar_url, + htmlUrl: user.htmlUrl, + avatarUrl: user.avatarUrl, type: user.type, }; diff --git a/src/renderer/utils/notifications/handlers/workflowRun.test.ts b/src/renderer/utils/notifications/handlers/workflowRun.test.ts index b5c156a9d..77871cb21 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.test.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.test.ts @@ -3,8 +3,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import type { Link } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; +import type { GitifyNotification, Link } from '../../../types'; import { getWorkflowRunAttributes, workflowRunHandler } from './workflowRun'; describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { @@ -40,7 +39,8 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { mockSettings, ); - expect(result).toBeNull(); + // Returns empty object when state cannot be determined + expect(result).toEqual({}); }); it('unhandled workflow run title', async () => { @@ -54,7 +54,8 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { mockSettings, ); - expect(result).toBeNull(); + // Returns empty object when title cannot be parsed + expect(result).toEqual({}); }); }); @@ -78,9 +79,9 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { title: 'Some notification', }, repository: { - html_url: mockHtmlUrl, + htmlUrl: mockHtmlUrl, }, - } as Notification), + } as GitifyNotification), ).toEqual(`${mockHtmlUrl}/actions`); }); diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 2eaa8f1b0..1fff46963 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -5,11 +5,11 @@ import { RocketIcon } from '@primer/octicons-react'; import type { GitifyCheckSuiteStatus, + GitifyNotification, GitifySubject, Link, SettingsState, } from '../../../types'; -import type { Notification, Subject } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; @@ -23,9 +23,9 @@ class WorkflowRunHandler extends DefaultHandler { readonly type = 'WorkflowRun'; async enrich( - notification: Notification, + notification: GitifyNotification, _settings: SettingsState, - ): Promise { + ): Promise> { const state = getWorkflowRunAttributes(notification)?.status; if (state) { @@ -36,14 +36,14 @@ class WorkflowRunHandler extends DefaultHandler { }; } - return null; + return {}; } - iconType(_subject: Subject): FC | null { + iconType(_subject: GitifySubject): FC | null { return RocketIcon; } - defaultUrl(notification: Notification): Link { + defaultUrl(notification: GitifyNotification): Link { return getWorkflowRunUrl(notification); } } @@ -55,7 +55,7 @@ export const workflowRunHandler = new WorkflowRunHandler(); * but there isn't an obvious/clean way to do this currently. */ export function getWorkflowRunAttributes( - notification: Notification, + notification: GitifyNotification, ): WorkflowRunAttributes | null { const regex = /^(?.*?) requested your (?.*?) to deploy to an environment$/; @@ -86,7 +86,7 @@ function getWorkflowRunStatus( } } -export function getWorkflowRunUrl(notification: Notification): Link { +export function getWorkflowRunUrl(notification: GitifyNotification): Link { const filters = []; const workflowRunAttributes = getWorkflowRunAttributes(notification); @@ -95,5 +95,5 @@ export function getWorkflowRunUrl(notification: Notification): Link { filters.push(`is:${workflowRunAttributes.status}`); } - return actionsURL(notification.repository.html_url, filters); + return actionsURL(notification.repository.htmlUrl, filters); } diff --git a/src/renderer/utils/notifications/native.test.ts b/src/renderer/utils/notifications/native.test.ts index 97890e520..8770fc2e5 100644 --- a/src/renderer/utils/notifications/native.test.ts +++ b/src/renderer/utils/notifications/native.test.ts @@ -9,7 +9,7 @@ import * as native from './native'; describe('renderer/utils/notifications/native.ts', () => { const mockHtmlUrl = - mockSingleAccountNotifications[0].notifications[0].repository.html_url; + mockSingleAccountNotifications[0].notifications[0].repository.htmlUrl; jest .spyOn(helpers, 'generateGitHubWebUrl') @@ -31,7 +31,7 @@ describe('renderer/utils/notifications/native.ts', () => { expect(window.gitify.raiseNativeNotification).toHaveBeenCalledWith( expect.stringContaining( - mockSingleAccountNotifications[0].notifications[0].repository.full_name, + mockSingleAccountNotifications[0].notifications[0].repository.fullName, ), expect.stringContaining( mockSingleAccountNotifications[0].notifications[0].subject.title, diff --git a/src/renderer/utils/notifications/native.ts b/src/renderer/utils/notifications/native.ts index 9d70dd4d7..679a5d4a7 100644 --- a/src/renderer/utils/notifications/native.ts +++ b/src/renderer/utils/notifications/native.ts @@ -1,9 +1,11 @@ import { APPLICATION } from '../../../shared/constants'; -import type { Notification } from '../../typesGitHub'; +import type { GitifyNotification } from '../../types'; import { generateGitHubWebUrl } from '../helpers'; -export async function raiseNativeNotification(notifications: Notification[]) { +export async function raiseNativeNotification( + notifications: GitifyNotification[], +) { let title: string; let body: string; let url: string = null; @@ -12,7 +14,7 @@ export async function raiseNativeNotification(notifications: Notification[]) { const notification = notifications[0]; title = window.gitify.platform.isWindows() ? '' - : notification.repository.full_name; + : notification.repository.fullName; body = notification.subject.title; url = await generateGitHubWebUrl(notification); } else { diff --git a/src/renderer/utils/notifications/notifications.test.ts b/src/renderer/utils/notifications/notifications.test.ts index 7ee1e2c8c..6fd71e952 100644 --- a/src/renderer/utils/notifications/notifications.test.ts +++ b/src/renderer/utils/notifications/notifications.test.ts @@ -13,11 +13,11 @@ import { import { mockSettings } from '../../__mocks__/state-mocks'; import { type AccountNotifications, + type GitifyRepository, GroupBy, type Link, type SettingsState, } from '../../types'; -import type { Repository } from '../../typesGitHub'; import * as logger from '../../utils/logger'; import { enrichNotification, @@ -63,13 +63,16 @@ describe('renderer/utils/notifications/notifications.ts', () => { type: 'Issue', url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, }); - const mockRepository = { + const mockRepository: GitifyRepository = { name: 'notifications-test', - full_name: 'gitify-app/notifications-test', + fullName: 'gitify-app/notifications-test', + htmlUrl: 'https://github.com/gitify-app/notifications-test' as Link, owner: { login: 'gitify-app', + avatarUrl: 'https://avatars.githubusercontent.com/u/1' as Link, + type: 'Organization', }, - } as Repository; + }; mockNotification.repository = mockRepository; nock('https://api.github.com').post('/graphql').replyWithError(mockError); diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index ff822c872..e242157cd 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -1,12 +1,13 @@ import type { AccountNotifications, + GitifyNotification, GitifyState, GitifySubject, SettingsState, } from '../../types'; -import type { Notification } from '../../typesGitHub'; import { listNotificationsForAuthenticatedUser } from '../api/client'; import { determineFailureType } from '../api/errors'; +import { transformNotification } from '../api/transform'; import { rendererLogError, rendererLogWarn } from '../logger'; import { filterBaseNotifications, @@ -72,12 +73,11 @@ export async function getAllNotifications( .filter((response) => !!response) .map(async (accountNotifications) => { try { - let notifications = ( - await accountNotifications.notifications - ).data.map((notification: Notification) => ({ - ...notification, - account: accountNotifications.account, - })); + const rawNotifications = (await accountNotifications.notifications) + .data; + let notifications = rawNotifications.map((raw) => + transformNotification(raw, accountNotifications.account), + ); notifications = filterBaseNotifications( notifications, @@ -122,15 +122,15 @@ export async function getAllNotifications( } export async function enrichNotifications( - notifications: Notification[], + notifications: GitifyNotification[], settings: SettingsState, -): Promise { +): Promise { if (!settings.detailedNotifications) { return notifications; } const enrichedNotifications = await Promise.all( - notifications.map(async (notification: Notification) => { + notifications.map(async (notification: GitifyNotification) => { return enrichNotification(notification, settings); }), ); @@ -146,10 +146,10 @@ export async function enrichNotifications( * @returns The enriched notification. */ export async function enrichNotification( - notification: Notification, + notification: GitifyNotification, settings: SettingsState, -) { - let additionalSubjectDetails: GitifySubject = {}; +): Promise { + let additionalSubjectDetails: Partial = {}; try { const handler = createNotificationHandler(notification); diff --git a/src/renderer/utils/notifications/remove.ts b/src/renderer/utils/notifications/remove.ts index ad430fcdd..160d479a5 100644 --- a/src/renderer/utils/notifications/remove.ts +++ b/src/renderer/utils/notifications/remove.ts @@ -1,5 +1,9 @@ -import type { Account, AccountNotifications, SettingsState } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import type { + Account, + AccountNotifications, + GitifyNotification, + SettingsState, +} from '../../types'; import { getAccountUUID } from '../auth/utils'; /** @@ -10,7 +14,7 @@ import { getAccountUUID } from '../auth/utils'; export function removeNotificationsForAccount( account: Account, settings: SettingsState, - notificationsToRemove: Notification[], + notificationsToRemove: GitifyNotification[], accountNotifications: AccountNotifications[], ): AccountNotifications[] { if (notificationsToRemove.length === 0) { diff --git a/src/renderer/utils/notifications/utils.ts b/src/renderer/utils/notifications/utils.ts index dea4705f7..1d98e36e3 100644 --- a/src/renderer/utils/notifications/utils.ts +++ b/src/renderer/utils/notifications/utils.ts @@ -1,5 +1,4 @@ -import type { AccountNotifications } from '../../types'; -import type { Notification } from '../../typesGitHub'; +import type { AccountNotifications, GitifyNotification } from '../../types'; import { getAccountUUID } from '../auth/utils'; /** @@ -8,7 +7,7 @@ import { getAccountUUID } from '../auth/utils'; export function getNewNotifications( previousAccountNotifications: AccountNotifications[], newAccountNotifications: AccountNotifications[], -): Notification[] { +): GitifyNotification[] { return newAccountNotifications.flatMap((accountNotifications) => { const accountPreviousNotifications = previousAccountNotifications.find( (item) => From c11ec23e1766571e29e9cd3a648c297f30c1ff26 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 29 Dec 2025 16:06:58 +1100 Subject: [PATCH 2/5] further type refactoring Signed-off-by: Adam Setch --- src/renderer/__mocks__/notifications-mocks.ts | 2 +- src/renderer/__mocks__/user-mocks.ts | 15 +-- .../components/avatars/AvatarWithFallback.tsx | 3 +- .../components/metrics/MetricGroup.test.tsx | 6 +- .../notifications/NotificationFooter.test.tsx | 3 +- src/renderer/types.ts | 61 ++++++++++-- src/renderer/typesGitHub.ts | 93 ------------------- .../utils/api/__mocks__/response-mocks.ts | 4 +- src/renderer/utils/api/client.ts | 23 ++--- src/renderer/utils/api/errors.test.ts | 2 +- src/renderer/utils/api/errors.ts | 2 +- src/renderer/utils/api/transform.ts | 9 +- src/renderer/utils/api/types.ts | 21 +++++ src/renderer/utils/helpers.test.ts | 3 +- src/renderer/utils/icons.ts | 2 +- .../utils/notifications/filters/reason.ts | 2 +- .../notifications/filters/subjectType.ts | 2 +- .../utils/notifications/filters/userType.ts | 2 +- .../utils/notifications/handlers/default.ts | 3 +- .../notifications/handlers/index.test.ts | 2 +- .../utils/notifications/handlers/types.ts | 2 +- src/renderer/utils/reason.test.ts | 2 +- src/renderer/utils/reason.ts | 3 +- 23 files changed, 118 insertions(+), 149 deletions(-) delete mode 100644 src/renderer/typesGitHub.ts create mode 100644 src/renderer/utils/api/types.ts diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index 1063abb43..ff054f98a 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -7,8 +7,8 @@ import type { GitifySubject, Hostname, Link, + SubjectType, } from '../types'; -import type { SubjectType } from '../typesGitHub'; import { mockEnterpriseNotifications, mockGitHubNotifications, diff --git a/src/renderer/__mocks__/user-mocks.ts b/src/renderer/__mocks__/user-mocks.ts index eae029fa8..c53b9df1f 100644 --- a/src/renderer/__mocks__/user-mocks.ts +++ b/src/renderer/__mocks__/user-mocks.ts @@ -1,6 +1,5 @@ -import type { GitifyUser, Link } from '../types'; -import type { User } from '../typesGitHub'; -import type { AuthorFieldsFragment } from '../utils/api/graphql/generated/graphql'; +import type { GitifyNotificationUser, GitifyUser, Link } from '../types'; +import type { RawUser } from '../utils/api/types'; export const mockGitifyUser: GitifyUser = { login: 'octocat', @@ -9,18 +8,20 @@ export const mockGitifyUser: GitifyUser = { avatar: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link, }; -export function createPartialMockUser(login: string): User { - const mockUser: Partial = { +export function createPartialMockUser(login: string): RawUser { + const mockUser: Partial = { login: login, html_url: `https://github.com/${login}` as Link, avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link, type: 'User', }; - return mockUser as User; + return mockUser as RawUser; } -export function createMockAuthorFragment(login: string): AuthorFieldsFragment { +export function createMockAuthorFragment( + login: string, +): GitifyNotificationUser { return { __typename: 'User', login: login, diff --git a/src/renderer/components/avatars/AvatarWithFallback.tsx b/src/renderer/components/avatars/AvatarWithFallback.tsx index 59700e9ec..fee537794 100644 --- a/src/renderer/components/avatars/AvatarWithFallback.tsx +++ b/src/renderer/components/avatars/AvatarWithFallback.tsx @@ -3,8 +3,7 @@ import { useState } from 'react'; import { Avatar, Stack, Truncate } from '@primer/react'; -import { type Link, Size } from '../../types'; -import type { UserType } from '../../typesGitHub'; +import { type Link, Size, type UserType } from '../../types'; import { getDefaultUserIcon } from '../../utils/icons'; import { isNonHumanUser } from '../../utils/notifications/filters/userType'; diff --git a/src/renderer/components/metrics/MetricGroup.test.tsx b/src/renderer/components/metrics/MetricGroup.test.tsx index 51089ca65..8c82925f9 100644 --- a/src/renderer/components/metrics/MetricGroup.test.tsx +++ b/src/renderer/components/metrics/MetricGroup.test.tsx @@ -1,7 +1,7 @@ import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockSettings } from '../../__mocks__/state-mocks'; +import type { GitifyMilestone } from '../../types'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; -import type { MilestoneFieldsFragment } from '../../utils/api/graphql/generated/graphql'; import { MetricGroup } from './MetricGroup'; describe('renderer/components/metrics/MetricGroup.tsx', () => { @@ -104,7 +104,7 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { mockNotification.subject.milestone = { title: 'Milestone 1', state: 'OPEN', - } as MilestoneFieldsFragment; + } as GitifyMilestone; const props = { notification: mockNotification, @@ -119,7 +119,7 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { mockNotification.subject.milestone = { title: 'Milestone 1', state: 'CLOSED', - } as MilestoneFieldsFragment; + } as GitifyMilestone; const props = { notification: mockNotification, diff --git a/src/renderer/components/notifications/NotificationFooter.test.tsx b/src/renderer/components/notifications/NotificationFooter.test.tsx index d2e32678f..5899b378b 100644 --- a/src/renderer/components/notifications/NotificationFooter.test.tsx +++ b/src/renderer/components/notifications/NotificationFooter.test.tsx @@ -3,8 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockGitHubCloudAccount } from '../../__mocks__/account-mocks'; -import type { Link } from '../../types'; -import type { UserType } from '../../typesGitHub'; +import type { Link, UserType } from '../../types'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; import * as comms from '../../utils/comms'; import { NotificationFooter } from './NotificationFooter'; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 4dd565273..b7b69eb17 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -2,7 +2,6 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; -import type { Reason, SubjectType, UserType } from './typesGitHub'; import type { AuthorFieldsFragment, DiscussionStateReason, @@ -261,8 +260,9 @@ export type FilterStateType = 'open' | 'closed' | 'merged' | 'draft' | 'other'; * Gitify Notification Types * * These types represent the clean, UI-focused notification structure - * used throughout the application. Raw GitHub API responses are - * transformed into these types at the API boundary. + * used throughout the application. + * + * Raw GitHub API responses are transformed into these types at the API boundary. * **/ @@ -297,12 +297,12 @@ export interface GitifySubject { title: string; /** Subject type (Issue, PullRequest, etc.) */ type: SubjectType; - /** API URL for the subject (used for GraphQL fetching) */ + /** API URL for the subject */ url: Link | null; /** API URL for the latest comment */ latestCommentUrl: Link | null; - // Enriched fields (from GraphQL - all optional) + // Enriched fields (from additional GraphQL or REST API calls) /** Issue/PR/Discussion number */ number?: number; /** Parsed state from GraphQL */ @@ -318,7 +318,7 @@ export interface GitifySubject { /** Label names */ labels?: string[]; /** Milestone state/title */ - milestone?: MilestoneFieldsFragment; + milestone?: GitifyMilestone; /** Deep link to latest comment */ htmlUrl?: Link; } @@ -351,6 +351,8 @@ export interface GitifyOwner { export type GitifyNotificationUser = AuthorFieldsFragment; +export type GitifyMilestone = MilestoneFieldsFragment; + export interface GitifyPullRequestReview { state: PullRequestReviewState; users: string[]; @@ -382,3 +384,50 @@ export type GitifyCheckSuiteStatus = | 'SUCCESS' | 'TIMED_OUT' | 'WAITING'; + +/** + * + * Gitify Type Enhancements + * + * These types represent the clean, UI-focused notification structure + * used throughout the application. Raw GitHub API responses are + * transformed into these types at the API boundary. + * + **/ + +// Stronger typings for string literal attributes +export type Reason = + | 'approval_requested' + | 'assign' + | 'author' + | 'ci_activity' + | 'comment' + | 'invitation' + | 'manual' + | 'member_feature_requested' + | 'mention' + | 'review_requested' + | 'security_advisory_credit' + | 'security_alert' + | 'state_change' + | 'subscribed' + | 'team_mention'; + +export type SubjectType = + | 'CheckSuite' + | 'Commit' + | 'Discussion' + | 'Issue' + | 'PullRequest' + | 'Release' + | 'RepositoryDependabotAlertsThread' + | 'RepositoryInvitation' + | 'RepositoryVulnerabilityAlert' + | 'WorkflowRun'; + +export type UserType = + | 'Bot' + | 'EnterpriseUserAccount' + | 'Mannequin' + | 'Organization' + | 'User'; diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts deleted file mode 100644 index ccf4579db..000000000 --- a/src/renderer/typesGitHub.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { components } from '@octokit/openapi-types'; - -import type { Link } from './types'; - -export interface GitHubRESTError { - message: string; - documentation_url: Link; -} - -// Stronger typings for string literal attributes -export type Reason = - | 'approval_requested' - | 'assign' - | 'author' - | 'ci_activity' - | 'comment' - | 'invitation' - | 'manual' - | 'member_feature_requested' - | 'mention' - | 'review_requested' - | 'security_advisory_credit' - | 'security_alert' - | 'state_change' - | 'subscribed' - | 'team_mention'; - -export type SubjectType = - | 'CheckSuite' - | 'Commit' - | 'Discussion' - | 'Issue' - | 'PullRequest' - | 'Release' - | 'RepositoryDependabotAlertsThread' - | 'RepositoryInvitation' - | 'RepositoryVulnerabilityAlert' - | 'WorkflowRun'; - -export type UserType = - | 'Bot' - | 'EnterpriseUserAccount' - | 'Mannequin' - | 'Organization' - | 'User'; - -// Base types from Octokit -export type NotificationThreadSubscription = - components['schemas']['thread-subscription']; - -type BaseUser = components['schemas']['simple-user']; -type BaseRepository = components['schemas']['repository']; -type BaseCommit = components['schemas']['commit']; -type BaseCommitComment = components['schemas']['commit-comment']; -type BaseRelease = components['schemas']['release']; - -type StrengthenNullable = Omit & { - [P in K]: T[P] extends null ? null : NonNullable & Extra; -}; - -// Exported strengthened types for REST API responses -// These are used only at the API boundary before transforming to GitifyNotification - -export type Repository = Omit & { - html_url: Link; - owner: Owner; -}; - -export type User = Omit & { type: UserType }; - -export type Owner = Omit< - NonNullable, - 'type' | 'avatar_url' -> & { - type: UserType; - avatar_url: Link; -}; - -export type Commit = StrengthenNullable< - BaseCommit, - 'author', - { type: UserType } ->; -export type CommitComment = StrengthenNullable< - BaseCommitComment, - 'user', - { type: UserType } ->; -export type Release = StrengthenNullable< - BaseRelease, - 'author', - { type: UserType } ->; diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 5aff06858..575872792 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -9,7 +9,7 @@ import type { GitifyRepository, Link, } from '../../../types'; -import type { User } from '../../../typesGitHub'; +import type { RawUser } from '../types'; export const mockNotificationUser = { id: 123456789, @@ -18,7 +18,7 @@ export const mockNotificationUser = { url: 'https://api.github.com/users/octocat' as Link, html_url: 'https://github.com/octocat' as Link, type: 'User', -} satisfies Partial; +} satisfies Partial; // 2 Notifications // Hostname : 'github.com' diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index 8a5bcb3f9..7806dc75b 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -1,4 +1,3 @@ -import type { components } from '@octokit/openapi-types'; import type { AxiosPromise } from 'axios'; import type { ExecutionResult } from 'graphql'; @@ -10,15 +9,6 @@ import type { SettingsState, Token, } from '../../types'; - -type RawGitHubNotification = components['schemas']['thread']; - -import type { - Commit, - CommitComment, - NotificationThreadSubscription, - Release, -} from '../../typesGitHub'; import { isAnsweredDiscussionFeatureSupported } from '../features'; import { rendererLogError } from '../logger'; import { @@ -36,6 +26,13 @@ import { type ExecutionResultWithHeaders, performGraphQLRequest, } from './request'; +import type { + NotificationThreadSubscription, + RawCommit, + RawCommitComment, + RawGitHubNotification, + RawRelease, +} from './types'; import { getGitHubAPIBaseUrl, getGitHubGraphQLUrl, @@ -138,7 +135,7 @@ export function ignoreNotificationThreadSubscription( * * Endpoint documentation: https://docs.github.com/en/rest/commits/commits#get-a-commit */ -export function getCommit(url: Link, token: Token): AxiosPromise { +export function getCommit(url: Link, token: Token): AxiosPromise { return apiRequestAuth(url, 'GET', token); } @@ -151,7 +148,7 @@ export function getCommit(url: Link, token: Token): AxiosPromise { export function getCommitComment( url: Link, token: Token, -): AxiosPromise { +): AxiosPromise { return apiRequestAuth(url, 'GET', token); } @@ -160,7 +157,7 @@ export function getCommitComment( * * Endpoint documentation: https://docs.github.com/en/rest/releases/releases#get-a-release */ -export function getRelease(url: Link, token: Token): AxiosPromise { +export function getRelease(url: Link, token: Token): AxiosPromise { return apiRequestAuth(url, 'GET', token); } diff --git a/src/renderer/utils/api/errors.test.ts b/src/renderer/utils/api/errors.test.ts index 1f0ca9d97..268e40719 100644 --- a/src/renderer/utils/api/errors.test.ts +++ b/src/renderer/utils/api/errors.test.ts @@ -3,9 +3,9 @@ import { AxiosError, type AxiosResponse } from 'axios'; import { EVENTS } from '../../../shared/events'; import type { Link } from '../../types'; -import type { GitHubRESTError } from '../../typesGitHub'; import { Errors } from '../errors'; import { determineFailureType } from './errors'; +import type { GitHubRESTError } from './types'; describe('renderer/utils/api/errors.ts', () => { it('network error', async () => { diff --git a/src/renderer/utils/api/errors.ts b/src/renderer/utils/api/errors.ts index 468a63041..649af6439 100644 --- a/src/renderer/utils/api/errors.ts +++ b/src/renderer/utils/api/errors.ts @@ -1,8 +1,8 @@ import { AxiosError } from 'axios'; import type { GitifyError } from '../../types'; -import type { GitHubRESTError } from '../../typesGitHub'; import { Errors } from '../errors'; +import type { GitHubRESTError } from './types'; export function determineFailureType( err: AxiosError, diff --git a/src/renderer/utils/api/transform.ts b/src/renderer/utils/api/transform.ts index 1550343a1..63b747394 100644 --- a/src/renderer/utils/api/transform.ts +++ b/src/renderer/utils/api/transform.ts @@ -1,5 +1,3 @@ -import type { components } from '@octokit/openapi-types'; - import type { Account, GitifyNotification, @@ -7,10 +5,11 @@ import type { GitifyRepository, GitifySubject, Link, + Reason, + SubjectType, + UserType, } from '../../types'; -import type { Reason, SubjectType, UserType } from '../../typesGitHub'; - -type RawGitHubNotification = components['schemas']['thread']; +import type { RawGitHubNotification } from './types'; /** * Transform a raw GitHub notification to GitifyNotification. diff --git a/src/renderer/utils/api/types.ts b/src/renderer/utils/api/types.ts new file mode 100644 index 000000000..2ada18d0b --- /dev/null +++ b/src/renderer/utils/api/types.ts @@ -0,0 +1,21 @@ +import type { components } from '@octokit/openapi-types'; + +import type { Link } from '../../types'; + +export interface GitHubRESTError { + message: string; + documentation_url: Link; +} + +export type NotificationThreadSubscription = + components['schemas']['thread-subscription']; + +export type RawCommit = components['schemas']['commit']; + +export type RawCommitComment = components['schemas']['commit-comment']; + +export type RawGitHubNotification = components['schemas']['thread']; + +export type RawRelease = components['schemas']['release']; + +export type RawUser = components['schemas']['simple-user']; diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index f85d9b3b8..c1ac53729 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -5,8 +5,7 @@ import { } from '@primer/octicons-react'; import { mockToken } from '../__mocks__/state-mocks'; -import type { GitifySubject, Hostname, Link } from '../types'; -import type { SubjectType } from '../typesGitHub'; +import type { GitifySubject, Hostname, Link, SubjectType } from '../types'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as apiClient from './api/client'; import { diff --git a/src/renderer/utils/icons.ts b/src/renderer/utils/icons.ts index c682e8b72..e8ae5043b 100644 --- a/src/renderer/utils/icons.ts +++ b/src/renderer/utils/icons.ts @@ -18,8 +18,8 @@ import { type GitifyPullRequestReview, IconColor, type PullRequestApprovalIcon, + type UserType, } from '../types'; -import type { UserType } from '../typesGitHub'; import type { AuthMethod, PlatformType } from './auth/types'; export function getPullRequestReviewIcon( diff --git a/src/renderer/utils/notifications/filters/reason.ts b/src/renderer/utils/notifications/filters/reason.ts index f9b2e8390..eddf66c7e 100644 --- a/src/renderer/utils/notifications/filters/reason.ts +++ b/src/renderer/utils/notifications/filters/reason.ts @@ -1,10 +1,10 @@ import type { AccountNotifications, GitifyNotification, + Reason, SettingsState, TypeDetails, } from '../../../types'; -import type { Reason } from '../../../typesGitHub'; import { REASON_TYPE_DETAILS } from '../../reason'; import type { Filter } from './types'; diff --git a/src/renderer/utils/notifications/filters/subjectType.ts b/src/renderer/utils/notifications/filters/subjectType.ts index 486ffdf3d..156c1d666 100644 --- a/src/renderer/utils/notifications/filters/subjectType.ts +++ b/src/renderer/utils/notifications/filters/subjectType.ts @@ -2,9 +2,9 @@ import type { AccountNotifications, GitifyNotification, SettingsState, + SubjectType, TypeDetails, } from '../../../types'; -import type { SubjectType } from '../../../typesGitHub'; import type { Filter } from './types'; const SUBJECT_TYPE_DETAILS: Record = { diff --git a/src/renderer/utils/notifications/filters/userType.ts b/src/renderer/utils/notifications/filters/userType.ts index 5d5b1e6a6..f20c21ed1 100644 --- a/src/renderer/utils/notifications/filters/userType.ts +++ b/src/renderer/utils/notifications/filters/userType.ts @@ -3,8 +3,8 @@ import type { GitifyNotification, SettingsState, TypeDetails, + UserType, } from '../../../types'; -import type { UserType } from '../../../typesGitHub'; import type { Filter } from './types'; const USER_TYPE_DETAILS: Record = { diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index e27e7a1ea..1fa0756aa 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -9,8 +9,7 @@ import type { Link, SettingsState, } from '../../../types'; -import { IconColor } from '../../../types'; -import type { SubjectType } from '../../../typesGitHub'; +import { IconColor, type SubjectType } from '../../../types'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; diff --git a/src/renderer/utils/notifications/handlers/index.test.ts b/src/renderer/utils/notifications/handlers/index.test.ts index c30155a9c..5e1191efb 100644 --- a/src/renderer/utils/notifications/handlers/index.test.ts +++ b/src/renderer/utils/notifications/handlers/index.test.ts @@ -1,5 +1,5 @@ import { createPartialMockNotification } from '../../../__mocks__/notifications-mocks'; -import type { SubjectType } from '../../../typesGitHub'; +import type { SubjectType } from '../../../types'; import { checkSuiteHandler } from './checkSuite'; import { commitHandler } from './commit'; import { defaultHandler } from './default'; diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index bc69ee1b8..dd8477085 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -7,8 +7,8 @@ import type { GitifySubject, Link, SettingsState, + SubjectType, } from '../../../types'; -import type { SubjectType } from '../../../typesGitHub'; export interface NotificationTypeHandler { readonly type?: SubjectType; diff --git a/src/renderer/utils/reason.test.ts b/src/renderer/utils/reason.test.ts index 794cd4d73..7eba0fcc5 100644 --- a/src/renderer/utils/reason.test.ts +++ b/src/renderer/utils/reason.test.ts @@ -1,4 +1,4 @@ -import type { Reason } from '../typesGitHub'; +import type { Reason } from '../types'; import { getReasonDetails } from './reason'; describe('renderer/utils/reason.ts', () => { diff --git a/src/renderer/utils/reason.ts b/src/renderer/utils/reason.ts index 90f1cf241..5362f6cdf 100644 --- a/src/renderer/utils/reason.ts +++ b/src/renderer/utils/reason.ts @@ -1,5 +1,4 @@ -import type { TypeDetails } from '../types'; -import type { Reason } from '../typesGitHub'; +import type { Reason, TypeDetails } from '../types'; export const REASON_TYPE_DETAILS: Record = { approval_requested: { From cd64fe0675a7b64436f4fa97882057c5dd013f39 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 29 Dec 2025 16:09:10 +1100 Subject: [PATCH 3/5] further type refactoring Signed-off-by: Adam Setch --- src/renderer/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/types.ts b/src/renderer/types.ts index b7b69eb17..e23125679 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -305,7 +305,7 @@ export interface GitifySubject { // Enriched fields (from additional GraphQL or REST API calls) /** Issue/PR/Discussion number */ number?: number; - /** Parsed state from GraphQL */ + /** Parsed state */ state?: GitifyNotificationState; /** Latest comment/PR author */ user?: GitifyNotificationUser; From 87470d5a31cbd495bd171950a3a9bbd6381bde38 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 29 Dec 2025 16:22:59 +1100 Subject: [PATCH 4/5] further type refactoring Signed-off-by: Adam Setch --- src/renderer/__mocks__/user-mocks.ts | 7 +++---- .../notifications/NotificationFooter.test.tsx | 4 ++-- src/renderer/types.ts | 15 +++++++++++++-- src/renderer/utils/links.test.ts | 4 ++-- .../utils/notifications/handlers/commit.ts | 9 +++++---- .../notifications/handlers/discussion.test.ts | 8 ++++---- .../utils/notifications/handlers/issue.test.ts | 6 +++--- .../notifications/handlers/pullRequest.test.ts | 6 +++--- .../utils/notifications/handlers/release.ts | 4 ++-- .../utils/notifications/handlers/utils.test.ts | 4 ++-- 10 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/renderer/__mocks__/user-mocks.ts b/src/renderer/__mocks__/user-mocks.ts index c53b9df1f..fe3c6af51 100644 --- a/src/renderer/__mocks__/user-mocks.ts +++ b/src/renderer/__mocks__/user-mocks.ts @@ -19,14 +19,13 @@ export function createPartialMockUser(login: string): RawUser { return mockUser as RawUser; } -export function createMockAuthorFragment( +export function createMockNotificationUser( login: string, ): GitifyNotificationUser { return { - __typename: 'User', login: login, - htmlUrl: `https://github.com/${login}`, - avatarUrl: 'https://avatars.githubusercontent.com/u/583231?v=4', + htmlUrl: `https://github.com/${login}` as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link, type: 'User', }; } diff --git a/src/renderer/components/notifications/NotificationFooter.test.tsx b/src/renderer/components/notifications/NotificationFooter.test.tsx index 5899b378b..afe5512bd 100644 --- a/src/renderer/components/notifications/NotificationFooter.test.tsx +++ b/src/renderer/components/notifications/NotificationFooter.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockGitHubCloudAccount } from '../../__mocks__/account-mocks'; -import type { Link, UserType } from '../../types'; +import type { GitifyNotificationUser, Link } from '../../types'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; import * as comms from '../../utils/comms'; import { NotificationFooter } from './NotificationFooter'; @@ -83,7 +83,7 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { htmlUrl: 'https://github.com/some-user' as Link, avatarUrl: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, - type: 'User' as UserType, + type: 'User' as GitifyNotificationUser['type'], }, reviews: null, }, diff --git a/src/renderer/types.ts b/src/renderer/types.ts index e23125679..7939504a5 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -3,7 +3,6 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import type { - AuthorFieldsFragment, DiscussionStateReason, IssueState, IssueStateReason, @@ -349,7 +348,19 @@ export interface GitifyOwner { type: UserType; } -export type GitifyNotificationUser = AuthorFieldsFragment; +/** + * Minimal notification user information. + */ +export interface GitifyNotificationUser { + /** Notification user login name */ + login: string; + /** Notification user avatar URL */ + avatarUrl: Link; + /** Notification user html URL */ + htmlUrl: Link; + /** Notification user type (User, Organization, Bot, etc.) */ + type: UserType; +} export type GitifyMilestone = MilestoneFieldsFragment; diff --git a/src/renderer/utils/links.test.ts b/src/renderer/utils/links.test.ts index c5f169949..fba2a9d60 100644 --- a/src/renderer/utils/links.test.ts +++ b/src/renderer/utils/links.test.ts @@ -1,5 +1,5 @@ import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; -import { createMockAuthorFragment } from '../__mocks__/user-mocks'; +import { createMockNotificationUser } from '../__mocks__/user-mocks'; import { Constants } from '../constants'; import type { GitifyNotificationUser, @@ -75,7 +75,7 @@ describe('renderer/utils/links.ts', () => { }); it('openUserProfile', () => { - const mockUser = createMockAuthorFragment( + const mockUser = createMockNotificationUser( 'mock-user', ) as GitifyNotificationUser; diff --git a/src/renderer/utils/notifications/handlers/commit.ts b/src/renderer/utils/notifications/handlers/commit.ts index 38141a352..b60432171 100644 --- a/src/renderer/utils/notifications/handlers/commit.ts +++ b/src/renderer/utils/notifications/handlers/commit.ts @@ -8,6 +8,7 @@ import type { GitifyNotificationState, GitifyNotificationUser, GitifySubject, + Link, SettingsState, } from '../../../types'; import { getCommit, getCommitComment } from '../../api/client'; @@ -41,8 +42,8 @@ class CommitHandler extends DefaultHandler { user = { login: commitComment.user.login, - avatarUrl: commitComment.user.avatar_url, - htmlUrl: commitComment.user.html_url, + avatarUrl: commitComment.user.avatar_url as Link, + htmlUrl: commitComment.user.html_url as Link, type: commitComment.user.type as GitifyNotificationUser['type'], }; } else { @@ -52,8 +53,8 @@ class CommitHandler extends DefaultHandler { user = { login: commit.author.login, - avatarUrl: commit.author.avatar_url, - htmlUrl: commit.author.html_url, + avatarUrl: commit.author.avatar_url as Link, + htmlUrl: commit.author.html_url as Link, type: commit.author.type as GitifyNotificationUser['type'], }; } diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 27dfed08f..e220612b6 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyDiscussionState, @@ -20,9 +20,9 @@ import type { } from '../../api/graphql/generated/graphql'; import { discussionHandler } from './discussion'; -const mockAuthor = createMockAuthorFragment('discussion-author'); -const mockCommenter = createMockAuthorFragment('discussion-commenter'); -const mockReplier = createMockAuthorFragment('discussion-replier'); +const mockAuthor = createMockNotificationUser('discussion-author'); +const mockCommenter = createMockNotificationUser('discussion-commenter'); +const mockReplier = createMockNotificationUser('discussion-replier'); describe('renderer/utils/notifications/handlers/discussion.ts', () => { describe('enrich', () => { diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index a151fa610..24c5d7883 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyIssueState, @@ -21,8 +21,8 @@ import type { } from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; -const mockAuthor = createMockAuthorFragment('issue-author'); -const mockCommenter = createMockAuthorFragment('issue-commenter'); +const mockAuthor = createMockNotificationUser('issue-author'); +const mockCommenter = createMockNotificationUser('issue-commenter'); describe('renderer/utils/notifications/handlers/issue.ts', () => { describe('enrich', () => { diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 0a646156f..d34d1740e 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyPullRequestState, @@ -21,8 +21,8 @@ import type { } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; -const mockAuthor = createMockAuthorFragment('some-author'); -const mockCommenter = createMockAuthorFragment('some-commenter'); +const mockAuthor = createMockNotificationUser('some-author'); +const mockCommenter = createMockNotificationUser('some-commenter'); describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: GitifyNotification; diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index c66df4b87..5c70c50d0 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -37,8 +37,8 @@ class ReleaseHandler extends DefaultHandler { const user: GitifyNotificationUser = release.author ? { login: release.author.login, - avatarUrl: release.author.avatar_url, - htmlUrl: release.author.html_url, + avatarUrl: release.author.avatar_url as Link, + htmlUrl: release.author.html_url as Link, type: release.author.type as GitifyNotificationUser['type'], } : null; diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index 90b677682..478d0cba0 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -1,9 +1,9 @@ -import { createMockAuthorFragment } from '../../../__mocks__/user-mocks'; +import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; import { formatForDisplay, getNotificationAuthor } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { describe('getNotificationAuthor', () => { - const mockAuthor = createMockAuthorFragment('some-author'); + const mockAuthor = createMockNotificationUser('some-author'); it('returns null when all users are null', () => { const result = getNotificationAuthor([null, null]); From 9bd7a3f0d3acc3ab3c0b8417eb396ada3df38c66 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 29 Dec 2025 16:24:33 +1100 Subject: [PATCH 5/5] further type refactoring Signed-off-by: Adam Setch --- .../utils/notifications/handlers/default.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 1fa0756aa..df542ffd8 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -3,13 +3,14 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { QuestionIcon } from '@primer/octicons-react'; -import type { - GitifyNotification, - GitifySubject, - Link, - SettingsState, +import { + type GitifyNotification, + type GitifySubject, + IconColor, + type Link, + type SettingsState, + type SubjectType, } from '../../../types'; -import { IconColor, type SubjectType } from '../../../types'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils';