Skip to content

Commit 3d4a957

Browse files
authored
feat: repo-level transition and delayed notifications (#1365)
1 parent e9c7a64 commit 3d4a957

File tree

6 files changed

+64
-28
lines changed

6 files changed

+64
-28
lines changed

src/components/NotificationRow.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import { NotificationHeader } from './notification/NotificationHeader';
2323

2424
interface INotificationRow {
2525
notification: Notification;
26+
isRead?: boolean;
2627
}
2728

2829
export const NotificationRow: FC<INotificationRow> = ({
2930
notification,
31+
isRead = false,
3032
}: INotificationRow) => {
3133
const {
3234
settings,
@@ -56,6 +58,7 @@ export const NotificationRow: FC<INotificationRow> = ({
5658
removeNotificationFromState,
5759
settings,
5860
]);
61+
5962
const unsubscribeFromThread = (event: MouseEvent<HTMLElement>) => {
6063
// Don't trigger onClick of parent element.
6164
event.stopPropagation();
@@ -88,6 +91,7 @@ export const NotificationRow: FC<INotificationRow> = ({
8891
animateExit &&
8992
'translate-x-full opacity-0 transition duration-[350ms] ease-in-out',
9093
showAsRead && Opacity.READ,
94+
isRead && Opacity.READ,
9195
)}
9296
>
9397
<div

src/components/RepositoryNotifications.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { act, fireEvent, render, screen } from '@testing-library/react';
2-
import { mockGitHubCloudAccount } from '../__mocks__/state-mocks';
2+
import { mockGitHubCloudAccount, mockSettings } from '../__mocks__/state-mocks';
33
import { AppContext } from '../context/App';
44
import type { Link } from '../types';
55
import {
@@ -57,7 +57,9 @@ describe('components/Repository.tsx', () => {
5757

5858
it('should mark a repo as read', () => {
5959
render(
60-
<AppContext.Provider value={{ markRepoNotificationsRead }}>
60+
<AppContext.Provider
61+
value={{ settings: { ...mockSettings }, markRepoNotificationsRead }}
62+
>
6163
<RepositoryNotifications {...props} />
6264
</AppContext.Provider>,
6365
);
@@ -71,7 +73,9 @@ describe('components/Repository.tsx', () => {
7173

7274
it('should mark a repo as done', () => {
7375
render(
74-
<AppContext.Provider value={{ markRepoNotificationsDone }}>
76+
<AppContext.Provider
77+
value={{ settings: { ...mockSettings }, markRepoNotificationsDone }}
78+
>
7579
<RepositoryNotifications {...props} />
7680
</AppContext.Provider>,
7781
);

src/components/RepositoryNotifications.tsx

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import {
55
MarkGithubIcon,
66
ReadIcon,
77
} from '@primer/octicons-react';
8-
import {
9-
type FC,
10-
type MouseEvent,
11-
useCallback,
12-
useContext,
13-
useState,
14-
} from 'react';
8+
import { type FC, type MouseEvent, useContext, useState } from 'react';
159
import { AppContext } from '../context/App';
1610
import { Opacity, Size } from '../types';
1711
import type { Notification } from '../typesGitHub';
@@ -31,22 +25,15 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
3125
repoName,
3226
repoNotifications,
3327
}) => {
34-
const { markRepoNotificationsRead, markRepoNotificationsDone } =
28+
const { settings, markRepoNotificationsRead, markRepoNotificationsDone } =
3529
useContext(AppContext);
36-
37-
const markRepoAsRead = useCallback(() => {
38-
markRepoNotificationsRead(repoNotifications[0]);
39-
}, [repoNotifications, markRepoNotificationsRead]);
40-
41-
const markRepoAsDone = useCallback(() => {
42-
markRepoNotificationsDone(repoNotifications[0]);
43-
}, [repoNotifications, markRepoNotificationsDone]);
44-
45-
const avatarUrl = repoNotifications[0].repository.owner.avatar_url;
46-
30+
const [animateExit, setAnimateExit] = useState(false);
31+
const [showAsRead, setShowAsRead] = useState(false);
4732
const [showRepositoryNotifications, setShowRepositoryNotifications] =
4833
useState(true);
4934

35+
const avatarUrl = repoNotifications[0].repository.owner.avatar_url;
36+
5037
const toggleRepositoryNotifications = () => {
5138
setShowRepositoryNotifications(!showRepositoryNotifications);
5239
};
@@ -68,7 +55,9 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
6855
<div
6956
className={cn(
7057
'flex flex-1 gap-4 items-center truncate text-sm font-medium',
71-
Opacity.MEDIUM,
58+
animateExit &&
59+
'translate-x-full opacity-0 transition duration-[350ms] ease-in-out',
60+
showAsRead ? Opacity.READ : Opacity.MEDIUM,
7261
)}
7362
>
7463
<AvatarIcon
@@ -94,13 +83,25 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
9483
title="Mark Repository as Done"
9584
icon={CheckIcon}
9685
size={Size.MEDIUM}
97-
onClick={markRepoAsDone}
86+
onClick={(event: MouseEvent<HTMLElement>) => {
87+
// Don't trigger onClick of parent element.
88+
event.stopPropagation();
89+
setAnimateExit(!settings.delayNotificationState);
90+
setShowAsRead(settings.delayNotificationState);
91+
markRepoNotificationsDone(repoNotifications[0]);
92+
}}
9893
/>
9994
<InteractionButton
10095
title="Mark Repository as Read"
10196
icon={ReadIcon}
10297
size={Size.SMALL}
103-
onClick={markRepoAsRead}
98+
onClick={(event: MouseEvent<HTMLElement>) => {
99+
// Don't trigger onClick of parent element.
100+
event.stopPropagation();
101+
setAnimateExit(!settings.delayNotificationState);
102+
setShowAsRead(settings.delayNotificationState);
103+
markRepoNotificationsRead(repoNotifications[0]);
104+
}}
104105
/>
105106
<InteractionButton
106107
title={toggleRepositoryNotificationsLabel}
@@ -113,7 +114,11 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
113114

114115
{showRepositoryNotifications &&
115116
repoNotifications.map((notification) => (
116-
<NotificationRow key={notification.id} notification={notification} />
117+
<NotificationRow
118+
key={notification.id}
119+
notification={notification}
120+
isRead={showAsRead}
121+
/>
117122
))}
118123
</>
119124
);

src/hooks/useNotifications.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export const useNotifications = (): NotificationsState => {
154154
);
155155

156156
const markRepoNotificationsRead = useCallback(
157-
async (_state: GitifyState, notification: Notification) => {
157+
async (state: GitifyState, notification: Notification) => {
158158
setStatus('loading');
159159

160160
const repoSlug = notification.repository.full_name;
@@ -166,7 +166,9 @@ export const useNotifications = (): NotificationsState => {
166166
hostname,
167167
notification.account.token,
168168
);
169+
169170
const updatedNotifications = removeNotifications(
171+
state.settings,
170172
notification,
171173
notifications,
172174
);
@@ -209,6 +211,7 @@ export const useNotifications = (): NotificationsState => {
209211
}
210212

211213
const updatedNotifications = removeNotifications(
214+
state.settings,
212215
notification,
213216
notifications,
214217
);

src/utils/remove-notifications.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
mockAccountNotifications,
33
mockSingleAccountNotifications,
44
} from '../__mocks__/notifications-mocks';
5+
import { mockSettings } from '../__mocks__/state-mocks';
56
import { mockSingleNotification } from './api/__mocks__/response-mocks';
67
import { removeNotifications } from './remove-notifications';
78

@@ -10,6 +11,7 @@ describe('utils/remove-notifications.ts', () => {
1011
expect(mockSingleAccountNotifications[0].notifications.length).toBe(1);
1112

1213
const result = removeNotifications(
14+
mockSettings,
1315
mockSingleNotification,
1416
mockSingleAccountNotifications,
1517
);
@@ -22,11 +24,24 @@ describe('utils/remove-notifications.ts', () => {
2224
expect(mockAccountNotifications[1].notifications.length).toBe(2);
2325

2426
const result = removeNotifications(
27+
mockSettings,
2528
mockSingleNotification,
2629
mockAccountNotifications,
2730
);
2831

2932
expect(result[0].notifications.length).toBe(0);
3033
expect(result[1].notifications.length).toBe(2);
3134
});
35+
36+
it('should skip notification removal if delay state enabled', () => {
37+
expect(mockSingleAccountNotifications[0].notifications.length).toBe(1);
38+
39+
const result = removeNotifications(
40+
{ ...mockSettings, delayNotificationState: true },
41+
mockSingleNotification,
42+
mockSingleAccountNotifications,
43+
);
44+
45+
expect(result[0].notifications.length).toBe(1);
46+
});
3247
});

src/utils/remove-notifications.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import type { AccountNotifications } from '../types';
1+
import type { AccountNotifications, SettingsState } from '../types';
22
import type { Notification } from '../typesGitHub';
33
import { getAccountUUID } from './auth/utils';
44

55
export function removeNotifications(
6+
settings: SettingsState,
67
notification: Notification,
78
notifications: AccountNotifications[],
89
): AccountNotifications[] {
10+
if (settings.delayNotificationState) {
11+
return notifications;
12+
}
13+
914
const repoSlug = notification.repository.full_name;
1015

1116
const accountIndex = notifications.findIndex(

0 commit comments

Comments
 (0)