Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(<NotificationFooter {...props} />);

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(<NotificationFooter {...props} />);

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());

Expand All @@ -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(<NotificationFooter {...props} />);

await userEvent.click(screen.getByTestId('view-author-profile'));

expect(openExternalLinkSpy).toHaveBeenCalledTimes(1);
expect(openExternalLinkSpy).toHaveBeenCalledWith(props.notification.subject.author!.htmlUrl);
});
});
69 changes: 62 additions & 7 deletions src/renderer/components/notifications/NotificationFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<NotificationTimestampProps> = ({
notification,
}: NotificationTimestampProps) => (
<Stack direction="horizontal" gap="none">
<Text className="pr-1" title={notification.reason.description}>
{notification.reason.title}
</Text>
<RelativeTime datetime={notification.updatedAt} />
</Stack>
);

interface NotificationAuthorProps {
author: GitifyNotificationUser;
}

const NotificationAuthor: FC<NotificationAuthorProps> = ({ author }: NotificationAuthorProps) => (
<button
className="hidden font-medium hover:underline group-hover:inline"
data-testid="view-author-profile"
onClick={(event: MouseEvent<HTMLElement>) => {
// Don't trigger onClick of parent element.
event.stopPropagation();
openUserProfile(author);
}}
title={`Open ${author.login}'s profile ↗`}
type="button"
>
{`by ${author.login}`}
</button>
);

export const NotificationFooter: FC<NotificationFooterProps> = ({
notification,
}: NotificationFooterProps) => {
const author = getVisibleAuthor(notification);

return (
<Stack
align="center"
Expand Down Expand Up @@ -46,12 +99,14 @@ export const NotificationFooter: FC<NotificationFooterProps> = ({
<AvatarWithFallback size={Size.SMALL} userType={notification.display.defaultUserType} />
)}

<Stack direction="horizontal" gap="none">
<Text className="pr-1" title={notification.reason.description}>
{notification.reason.title}
</Text>
<RelativeTime datetime={notification.updatedAt} />
</Stack>
{author ? (
<Stack direction="horizontal" gap="condensed">
<NotificationTimestamp notification={notification} />
<NotificationAuthor author={author} />
</Stack>
) : (
<NotificationTimestamp notification={notification} />
)}

<MetricGroup notification={notification} />
</Stack>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading