From da02c1e912fadecd14e4f4b736aebcbc57e7b1fc Mon Sep 17 00:00:00 2001 From: Yuri Teixeira Date: Wed, 8 Jul 2026 15:56:42 +0200 Subject: [PATCH] feat(notifications): display who's the author MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Display the author in the notification footers for issues and pull requests. - Link author names to their GitHub profiles. Made with ♥️ with the help of spotify-hendrix/gpt-5.5 & https://pi.dev --- .../notifications/NotificationFooter.test.tsx | 68 ++++++++ .../notifications/NotificationFooter.tsx | 69 +++++++- .../AccountNotifications.test.tsx.snap | 38 +++-- .../NotificationFooter.test.tsx.snap | 38 +++-- .../NotificationRow.test.tsx.snap | 152 +++++++++++++----- 5 files changed, 298 insertions(+), 67 deletions(-) diff --git a/src/renderer/components/notifications/NotificationFooter.test.tsx b/src/renderer/components/notifications/NotificationFooter.test.tsx index a61df87e4..6c9382192 100644 --- a/src/renderer/components/notifications/NotificationFooter.test.tsx +++ b/src/renderer/components/notifications/NotificationFooter.test.tsx @@ -63,6 +63,46 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { expect(tree.container).toMatchSnapshot(); }); + it.each(['Issue', 'PullRequest'] as const)('should show %s authors on hover', (type) => { + const props: NotificationFooterProps = { + notification: { + ...mockGitifyNotification, + subject: { + ...mockGitifyNotification.subject, + author: { + login: 'thread-author', + htmlUrl: 'https://github.com/thread-author' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/123?v=4' as Link, + type: 'User' as GitifyNotificationUser['type'], + }, + type, + }, + }, + }; + + renderWithProviders(); + + expect(screen.getByText('by thread-author')).toHaveClass('hidden', 'group-hover:inline'); + }); + + it('should not show authors for other notification types', () => { + const props: NotificationFooterProps = { + notification: { + ...mockGitifyNotification, + subject: { + ...mockGitifyNotification.subject, + type: 'Release', + }, + }, + }; + + renderWithProviders(); + + expect( + screen.queryByText(`by ${mockGitifyNotification.subject.author!.login}`), + ).not.toBeInTheDocument(); + }); + it('should open notification user profile', async () => { const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); @@ -89,4 +129,32 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); expect(openExternalLinkSpy).toHaveBeenCalledWith(props.notification.subject.user!.htmlUrl); }); + + it('should open notification author profile', async () => { + const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); + + const props: NotificationFooterProps = { + notification: { + ...mockGitifyNotification, + subject: { + ...mockGitifyNotification.subject, + author: { + login: 'issue-author', + htmlUrl: 'https://github.com/issue-author' as Link, + avatarUrl: 'https://avatars.githubusercontent.com/u/123?v=4' as Link, + type: 'User' as GitifyNotificationUser['type'], + }, + reviews: undefined, + type: 'Issue', + }, + }, + }; + + renderWithProviders(); + + await userEvent.click(screen.getByTestId('view-author-profile')); + + expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); + expect(openExternalLinkSpy).toHaveBeenCalledWith(props.notification.subject.author!.htmlUrl); + }); }); diff --git a/src/renderer/components/notifications/NotificationFooter.tsx b/src/renderer/components/notifications/NotificationFooter.tsx index cacdc51b9..cd4329f7b 100644 --- a/src/renderer/components/notifications/NotificationFooter.tsx +++ b/src/renderer/components/notifications/NotificationFooter.tsx @@ -2,7 +2,13 @@ import type { FC, MouseEvent } from 'react'; import { RelativeTime, Stack, Text } from '@primer/react'; -import { type GitifyNotification, Opacity, Size } from '../../types'; +import { + type GitifyNotification, + type GitifyNotificationUser, + type SubjectType, + Opacity, + Size, +} from '../../types'; import { openUserProfile } from '../../utils/system/links'; import { cn } from '../../utils/ui/cn'; @@ -13,9 +19,56 @@ export interface NotificationFooterProps { notification: GitifyNotification; } +const subjectTypesWithVisibleAuthor: SubjectType[] = ['Issue', 'PullRequest']; + +function getVisibleAuthor(notification: GitifyNotification): GitifyNotificationUser | undefined { + if (!subjectTypesWithVisibleAuthor.includes(notification.subject.type)) { + return undefined; + } + + return notification.subject.author; +} + +interface NotificationTimestampProps { + notification: GitifyNotification; +} + +const NotificationTimestamp: FC = ({ + notification, +}: NotificationTimestampProps) => ( + + + {notification.reason.title} + + + +); + +interface NotificationAuthorProps { + author: GitifyNotificationUser; +} + +const NotificationAuthor: FC = ({ author }: NotificationAuthorProps) => ( + +); + export const NotificationFooter: FC = ({ notification, }: NotificationFooterProps) => { + const author = getVisibleAuthor(notification); + return ( = ({ )} - - - {notification.reason.title} - - - + {author ? ( + + + + + ) : ( + + )} diff --git a/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap b/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap index 75bded530..d8e3ce5b9 100644 --- a/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap +++ b/src/renderer/components/notifications/__snapshots__/AccountNotifications.test.tsx.snap @@ -840,22 +840,40 @@ exports[`renderer/components/notifications/AccountNotifications.tsx > should ren class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + + +
should rende class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + +
+
should render i class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + +
+
should render i class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + +
+
should render i class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + +
+
should render i class="prc-Stack-Stack-UQ9k6" data-align="stretch" data-direction="horizontal" - data-gap="none" + data-gap="condensed" data-justify="start" data-padding="none" data-wrap="nowrap" > - - Updated - - + Updated + + + May 20, 2017 + +
+