From 67121b90394aa8d657d84115b520879080b0ef20 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 11 Jul 2024 23:56:15 -0400 Subject: [PATCH 1/2] feat: support repo-level transition and delayed notifications --- src/components/NotificationRow.tsx | 4 ++ .../RepositoryNotifications.test.tsx | 10 ++-- src/components/RepositoryNotifications.tsx | 51 ++++++++++--------- src/hooks/useNotifications.ts | 5 +- src/utils/remove-notifications.test.ts | 15 ++++++ src/utils/remove-notifications.ts | 7 ++- 6 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index 11f6749e5..e0ae9cc99 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -23,10 +23,12 @@ import { NotificationHeader } from './notification/NotificationHeader'; interface INotificationRow { notification: Notification; + isRead?: boolean; } export const NotificationRow: FC = ({ notification, + isRead = false, }: INotificationRow) => { const { settings, @@ -56,6 +58,7 @@ export const NotificationRow: FC = ({ removeNotificationFromState, settings, ]); + const unsubscribeFromThread = (event: MouseEvent) => { // Don't trigger onClick of parent element. event.stopPropagation(); @@ -88,6 +91,7 @@ export const NotificationRow: FC = ({ animateExit && 'translate-x-full opacity-0 transition duration-[350ms] ease-in-out', showAsRead && Opacity.READ, + isRead && Opacity.READ, )} >
{ it('should mark a repo as read', () => { render( - + , ); @@ -69,7 +71,9 @@ describe('components/Repository.tsx', () => { it('should mark a repo as done', () => { render( - + , ); diff --git a/src/components/RepositoryNotifications.tsx b/src/components/RepositoryNotifications.tsx index 7ea78c7e2..41d3d579c 100644 --- a/src/components/RepositoryNotifications.tsx +++ b/src/components/RepositoryNotifications.tsx @@ -5,13 +5,7 @@ import { MarkGithubIcon, ReadIcon, } from '@primer/octicons-react'; -import { - type FC, - type MouseEvent, - useCallback, - useContext, - useState, -} from 'react'; +import { type FC, type MouseEvent, useContext, useState } from 'react'; import { AppContext } from '../context/App'; import { Opacity, Size } from '../types'; import type { Notification } from '../typesGitHub'; @@ -31,22 +25,15 @@ export const RepositoryNotifications: FC = ({ repoName, repoNotifications, }) => { - const { markRepoNotificationsRead, markRepoNotificationsDone } = + const { settings, markRepoNotificationsRead, markRepoNotificationsDone } = useContext(AppContext); - - const markRepoAsRead = useCallback(() => { - markRepoNotificationsRead(repoNotifications[0]); - }, [repoNotifications, markRepoNotificationsRead]); - - const markRepoAsDone = useCallback(() => { - markRepoNotificationsDone(repoNotifications[0]); - }, [repoNotifications, markRepoNotificationsDone]); - - const avatarUrl = repoNotifications[0].repository.owner.avatar_url; - + const [animateExit, setAnimateExit] = useState(false); + const [showAsRead, setShowAsRead] = useState(false); const [showRepositoryNotifications, setShowRepositoryNotifications] = useState(true); + const avatarUrl = repoNotifications[0].repository.owner.avatar_url; + const toggleRepositoryNotifications = () => { setShowRepositoryNotifications(!showRepositoryNotifications); }; @@ -68,7 +55,9 @@ export const RepositoryNotifications: FC = ({
= ({ title="Mark Repository as Done" icon={CheckIcon} size={Size.MEDIUM} - onClick={markRepoAsDone} + onClick={(event: MouseEvent) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + setAnimateExit(!settings.delayNotificationState); + setShowAsRead(settings.delayNotificationState); + markRepoNotificationsDone(repoNotifications[0]); + }} /> ) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + setAnimateExit(!settings.delayNotificationState); + setShowAsRead(settings.delayNotificationState); + markRepoNotificationsRead(repoNotifications[0]); + }} /> = ({ {showRepositoryNotifications && repoNotifications.map((notification) => ( - + ))} ); diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 2b4b604c4..ece638730 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -154,7 +154,7 @@ export const useNotifications = (): NotificationsState => { ); const markRepoNotificationsRead = useCallback( - async (_state: GitifyState, notification: Notification) => { + async (state: GitifyState, notification: Notification) => { setStatus('loading'); const repoSlug = notification.repository.full_name; @@ -166,7 +166,9 @@ export const useNotifications = (): NotificationsState => { hostname, notification.account.token, ); + const updatedNotifications = removeNotifications( + state.settings, notification, notifications, ); @@ -209,6 +211,7 @@ export const useNotifications = (): NotificationsState => { } const updatedNotifications = removeNotifications( + state.settings, notification, notifications, ); diff --git a/src/utils/remove-notifications.test.ts b/src/utils/remove-notifications.test.ts index 9ed87a90c..98d476de8 100644 --- a/src/utils/remove-notifications.test.ts +++ b/src/utils/remove-notifications.test.ts @@ -2,6 +2,7 @@ import { mockAccountNotifications, mockSingleAccountNotifications, } from '../__mocks__/notifications-mocks'; +import { mockSettings } from '../__mocks__/state-mocks'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import { removeNotifications } from './remove-notifications'; @@ -10,6 +11,7 @@ describe('utils/remove-notifications.ts', () => { expect(mockSingleAccountNotifications[0].notifications.length).toBe(1); const result = removeNotifications( + mockSettings, mockSingleNotification, mockSingleAccountNotifications, ); @@ -22,6 +24,7 @@ describe('utils/remove-notifications.ts', () => { expect(mockAccountNotifications[1].notifications.length).toBe(2); const result = removeNotifications( + mockSettings, mockSingleNotification, mockAccountNotifications, ); @@ -29,4 +32,16 @@ describe('utils/remove-notifications.ts', () => { expect(result[0].notifications.length).toBe(0); expect(result[1].notifications.length).toBe(2); }); + + it('should skip notification removal if delay state enabled', () => { + expect(mockSingleAccountNotifications[0].notifications.length).toBe(1); + + const result = removeNotifications( + { ...mockSettings, delayNotificationState: true }, + mockSingleNotification, + mockSingleAccountNotifications, + ); + + expect(result[0].notifications.length).toBe(1); + }); }); diff --git a/src/utils/remove-notifications.ts b/src/utils/remove-notifications.ts index ee819ebf3..507d46fa0 100644 --- a/src/utils/remove-notifications.ts +++ b/src/utils/remove-notifications.ts @@ -1,11 +1,16 @@ -import type { AccountNotifications } from '../types'; +import type { AccountNotifications, SettingsState } from '../types'; import type { Notification } from '../typesGitHub'; import { getAccountUUID } from './auth/utils'; export function removeNotifications( + settings: SettingsState, notification: Notification, notifications: AccountNotifications[], ): AccountNotifications[] { + if (settings.delayNotificationState) { + return notifications; + } + const repoSlug = notification.repository.full_name; const accountIndex = notifications.findIndex( From 593c83cffce88b544903193e7df18a18eb6b40ce Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 15 Jul 2024 10:10:23 -0400 Subject: [PATCH 2/2] Merge branch 'main' into feat/repo-transition-and-delay --- src/components/RepositoryNotifications.tsx | 220 ++++++++++----------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/src/components/RepositoryNotifications.tsx b/src/components/RepositoryNotifications.tsx index 74dc90bc9..a7f1e9973 100644 --- a/src/components/RepositoryNotifications.tsx +++ b/src/components/RepositoryNotifications.tsx @@ -1,125 +1,125 @@ import { - CheckIcon, - ChevronDownIcon, - ChevronUpIcon, - MarkGithubIcon, - ReadIcon, -} from "@primer/octicons-react"; -import { type FC, type MouseEvent, useContext, useState } from "react"; -import { AppContext } from "../context/App"; -import { Opacity, Size } from "../types"; -import type { Notification } from "../typesGitHub"; -import { cn } from "../utils/cn"; -import { openRepository } from "../utils/links"; -import { HoverGroup } from "./HoverGroup"; -import { NotificationRow } from "./NotificationRow"; -import { InteractionButton } from "./buttons/InteractionButton"; -import { AvatarIcon } from "./icons/AvatarIcon"; + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, + MarkGithubIcon, + ReadIcon, +} from '@primer/octicons-react'; +import { type FC, type MouseEvent, useContext, useState } from 'react'; +import { AppContext } from '../context/App'; +import { Opacity, Size } from '../types'; +import type { Notification } from '../typesGitHub'; +import { cn } from '../utils/cn'; +import { openRepository } from '../utils/links'; +import { HoverGroup } from './HoverGroup'; +import { NotificationRow } from './NotificationRow'; +import { InteractionButton } from './buttons/InteractionButton'; +import { AvatarIcon } from './icons/AvatarIcon'; interface IRepositoryNotifications { - repoNotifications: Notification[]; - repoName: string; + repoNotifications: Notification[]; + repoName: string; } export const RepositoryNotifications: FC = ({ - repoName, - repoNotifications, + repoName, + repoNotifications, }) => { - const { settings, markRepoNotificationsRead, markRepoNotificationsDone } = - useContext(AppContext); - const [animateExit, setAnimateExit] = useState(false); - const [showAsRead, setShowAsRead] = useState(false); - const [showRepositoryNotifications, setShowRepositoryNotifications] = - useState(true); + const { settings, markRepoNotificationsRead, markRepoNotificationsDone } = + useContext(AppContext); + const [animateExit, setAnimateExit] = useState(false); + const [showAsRead, setShowAsRead] = useState(false); + const [showRepositoryNotifications, setShowRepositoryNotifications] = + useState(true); - const avatarUrl = repoNotifications[0].repository.owner.avatar_url; + const avatarUrl = repoNotifications[0].repository.owner.avatar_url; - const toggleRepositoryNotifications = () => { - setShowRepositoryNotifications(!showRepositoryNotifications); - }; + const toggleRepositoryNotifications = () => { + setShowRepositoryNotifications(!showRepositoryNotifications); + }; - const ChevronIcon = showRepositoryNotifications - ? ChevronDownIcon - : ChevronUpIcon; + const ChevronIcon = showRepositoryNotifications + ? ChevronDownIcon + : ChevronUpIcon; - const toggleRepositoryNotificationsLabel = showRepositoryNotifications - ? "Hide repository notifications" - : "Show repository notifications"; + const toggleRepositoryNotificationsLabel = showRepositoryNotifications + ? 'Hide repository notifications' + : 'Show repository notifications'; - return ( - <> -
-
- - ) => { - // Don't trigger onClick of parent element. - event.stopPropagation(); - openRepository(repoNotifications[0].repository); - }} - > - {repoName} - -
+ return ( + <> +
+
+ + ) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + openRepository(repoNotifications[0].repository); + }} + > + {repoName} + +
- - ) => { - // Don't trigger onClick of parent element. - event.stopPropagation(); - setAnimateExit(!settings.delayNotificationState); - setShowAsRead(settings.delayNotificationState); - markRepoNotificationsDone(repoNotifications[0]); - }} - /> - ) => { - // Don't trigger onClick of parent element. - event.stopPropagation(); - setAnimateExit(!settings.delayNotificationState); - setShowAsRead(settings.delayNotificationState); - markRepoNotificationsRead(repoNotifications[0]); - }} - /> - - -
+ + ) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + setAnimateExit(!settings.delayNotificationState); + setShowAsRead(settings.delayNotificationState); + markRepoNotificationsDone(repoNotifications[0]); + }} + /> + ) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + setAnimateExit(!settings.delayNotificationState); + setShowAsRead(settings.delayNotificationState); + markRepoNotificationsRead(repoNotifications[0]); + }} + /> + + +
- {showRepositoryNotifications && - repoNotifications.map((notification) => ( - - ))} - - ); + {showRepositoryNotifications && + repoNotifications.map((notification) => ( + + ))} + + ); };