Repository: gitify-app/gitify
Pull request #1230 (2024-06-13 13:43:42)

fix: revert updated_at formatting
revert the enhancement from https://github.com/gitify-app/gitify/pull/1103 as it was causing some odd behavior for long-running issues/prs getting out of sync and showing the last read date rather than the new updated date


Patch:
diff --git a/package.json b/package.json
index fa9f1ee9..a0c5aee9 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
     "@electron/remote": "2.1.2",
     "@primer/octicons-react": "19.9.0",
     "axios": "1.7.2",
+    "clsx": "2.1.1",
     "date-fns": "3.6.0",
     "electron-updater": "6.2.1",
     "final-form": "4.20.10",
@@ -119,7 +120,6 @@
     "react-dom": "18.3.1",
     "react-final-form": "6.5.9",
     "react-router-dom": "6.23.1",
-    "react-transition-group": "4.4.5",
     "ts-loader": "9.5.1",
     "typescript": "5.4.5"
   },
@@ -132,7 +132,6 @@
     "@types/nprogress": "0.2.3",
     "@types/react": "18.3.3",
     "@types/react-router-dom": "5.3.3",
-    "@types/react-transition-group": "4.4.10",
     "autoprefixer": "10.4.19",
     "css-loader": "7.1.2",
     "electron": "31.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 227ae2df..4a84b637 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
       axios:
         specifier: 1.7.2
         version: 1.7.2
+      clsx:
+        specifier: 2.1.1
+        version: 2.1.1
       date-fns:
         specifier: 3.6.0
         version: 3.6.0
@@ -44,9 +47,6 @@ importers:
       react-router-dom:
         specifier: 6.23.1
         version: 6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      react-transition-group:
-        specifier: 4.4.5
-        version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       ts-loader:
         specifier: 9.5.1
         version: 9.5.1(typescript@5.4.5)(webpack@5.92.0(webpack-cli@5.1.4))
@@ -78,9 +78,6 @@ importers:
       '@types/react-router-dom':
         specifier: 5.3.3
         version: 5.3.3
-      '@types/react-transition-group':
-        specifier: 4.4.10
-        version: 4.4.10
       autoprefixer:
         specifier: 10.4.19
         version: 10.4.19(postcss@8.4.38)
@@ -694,9 +691,6 @@ packages:
   '@types/react-router@5.1.20':
     resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
 
-  '@types/react-transition-group@4.4.10':
-    resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
-
   '@types/react@18.3.3':
     resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
 
@@ -1103,6 +1097,10 @@ packages:
   clone-response@1.0.3:
     resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
 
+  clsx@2.1.1:
+    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+    engines: {node: '>=6'}
+
   co@4.6.0:
     resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
     engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -1322,9 +1320,6 @@ packages:
   dom-accessibility-api@0.5.16:
     resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
 
-  dom-helpers@5.2.1:
-    resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
-
   domexception@4.0.0:
     resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
     engines: {node: '>=12'}
@@ -2482,9 +2477,6 @@ packages:
     resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
     engines: {node: '>= 6'}
 
-  prop-types@15.8.1:
-    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
-
   propagate@2.0.1:
     resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==}
     engines: {node: '>= 8'}
@@ -2529,9 +2521,6 @@ packages:
       final-form: ^4.20.4
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
 
-  react-is@16.13.1:
-    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
-
   react-is@17.0.2:
     resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
 
@@ -2551,12 +2540,6 @@ packages:
     peerDependencies:
       react: '>=16.8'
 
-  react-transition-group@4.4.5:
-    resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
-    peerDependencies:
-      react: '>=16.6.0'
-      react-dom: '>=16.6.0'
-
   react@18.3.1:
     resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
     engines: {node: '>=0.10.0'}
@@ -3901,10 +3884,6 @@ snapshots:
       '@types/history': 4.7.11
       '@types/react': 18.3.3
 
-  '@types/react-transition-group@4.4.10':
-    dependencies:
-      '@types/react': 18.3.3
-
   '@types/react@18.3.3':
     dependencies:
       '@types/prop-types': 15.7.12
@@ -4425,6 +4404,8 @@ snapshots:
     dependencies:
       mimic-response: 1.0.1
 
+  clsx@2.1.1: {}
+
   co@4.6.0: {}
 
   collect-v8-coverage@1.0.2: {}
@@ -4635,11 +4616,6 @@ snapshots:
 
   dom-accessibility-api@0.5.16: {}
 
-  dom-helpers@5.2.1:
-    dependencies:
-      '@babel/runtime': 7.24.1
-      csstype: 3.1.3
-
   domexception@4.0.0:
     dependencies:
       webidl-conversions: 7.0.0
@@ -5961,12 +5937,6 @@ snapshots:
       kleur: 3.0.3
       sisteransi: 1.0.5
 
-  prop-types@15.8.1:
-    dependencies:
-      loose-envify: 1.4.0
-      object-assign: 4.1.1
-      react-is: 16.13.1
-
   propagate@2.0.1: {}
 
   proxy-from-env@1.1.0: {}
@@ -6004,8 +5974,6 @@ snapshots:
       final-form: 4.20.10
       react: 18.3.1
 
-  react-is@16.13.1: {}
-
   react-is@17.0.2: {}
 
   react-is@18.2.0: {}
@@ -6022,15 +5990,6 @@ snapshots:
       '@remix-run/router': 1.16.1
       react: 18.3.1
 
-  react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
-    dependencies:
-      '@babel/runtime': 7.24.1
-      dom-helpers: 5.2.1
-      loose-envify: 1.4.0
-      prop-types: 15.8.1
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-
   react@18.3.1:
     dependencies:
       loose-envify: 1.4.0
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index b4ad71a0..0ce9e8af 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -8,11 +8,17 @@ import {
   ReadIcon,
   TagIcon,
 } from '@primer/octicons-react';
-import { type FC, type MouseEvent, useCallback, useContext } from 'react';
-
+import {
+  type FC,
+  type MouseEvent,
+  useCallback,
+  useContext,
+  useState,
+} from 'react';
 import { AppContext } from '../context/App';
 import { type Account, IconColor } from '../types';
 import type { Notification } from '../typesGitHub';
+import { cn } from '../utils/cn';
 import {
   formatForDisplay,
   formatNotificationUpdatedAt,
@@ -41,8 +47,10 @@ export const NotificationRow: FC<IProps> = ({ notification, account }) => {
     unsubscribeNotification,
     notifications,
   } = useContext(AppContext);
+  const [animateExit, setAnimateExit] = useState(false);
 
   const handleNotification = useCallback(() => {
+    setAnimateExit(true);
     openNotification(notification);
 
     if (settings.markAsDoneOnOpen) {
@@ -89,10 +97,14 @@ export const NotificationRow: FC<IProps> = ({ notification, account }) => {
   return (
     <div
       id={notification.id}
-      className="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      className={cn(
+        'group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker',
+        animateExit &&
+          'translate-x-full opacity-0 transition duration-[350ms] ease-in-out',
+      )}
     >
       <div
-        className={`flex justify-center items-center mr-3 w-5 ${iconColor}`}
+        className={cn('flex justify-center items-center mr-3 w-5', iconColor)}
         title={notificationTitle}
       >
         <NotificationIcon size={18} aria-label={notification.subject.type} />
@@ -204,7 +216,10 @@ export const NotificationRow: FC<IProps> = ({ notification, account }) => {
           type="button"
           className="focus:outline-none h-full hover:text-green-500"
           title="Mark as Done"
-          onClick={() => markNotificationDone(notification)}
+          onClick={() => {
+            setAnimateExit(true);
+            markNotificationDone(notification);
+          }}
         >
           <CheckIcon size={16} aria-label="Mark as Done" />
         </button>
@@ -222,7 +237,10 @@ export const NotificationRow: FC<IProps> = ({ notification, account }) => {
           type="button"
           className="focus:outline-none h-full hover:text-green-500"
           title="Mark as Read"
-          onClick={() => markNotificationRead(notification)}
+          onClick={() => {
+            setAnimateExit(true);
+            markNotificationRead(notification);
+          }}
         >
           <ReadIcon size={14} aria-label="Mark as Read" />
         </button>
diff --git a/src/components/Repository.tsx b/src/components/Repository.tsx
index 4021ac27..04186a61 100644
--- a/src/components/Repository.tsx
+++ b/src/components/Repository.tsx
@@ -1,6 +1,5 @@
 import { CheckIcon, MarkGithubIcon, ReadIcon } from '@primer/octicons-react';
 import { type FC, useCallback, useContext } from 'react';
-import { CSSTransition, TransitionGroup } from 'react-transition-group';
 import { AppContext } from '../context/App';
 import type { Account } from '../types';
 import type { Notification } from '../typesGitHub';
@@ -77,17 +76,9 @@ export const RepositoryNotifications: FC<IProps> = ({
         </div>
       </div>
 
-      <TransitionGroup>
-        {repoNotifications.map((obj) => (
-          <CSSTransition key={obj.id} timeout={250} classNames="notification">
-            <NotificationRow
-              key={obj.id}
-              account={account}
-              notification={obj}
-            />
-          </CSSTransition>
-        ))}
-      </TransitionGroup>
+      {repoNotifications.map((obj) => (
+        <NotificationRow key={obj.id} account={account} notification={obj} />
+      ))}
     </>
   );
 };
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 0d153ac1..74632eb2 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -9,6 +9,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
 import { Logo } from '../components/Logo';
 import { AppContext } from '../context/App';
 import { BUTTON_SIDEBAR_CLASS_NAME } from '../styles/gitify';
+import { cn } from '../utils/cn';
 import { quitApp } from '../utils/comms';
 import { openGitHubNotifications, openGitifyRepository } from '../utils/links';
 import { getNotificationCount } from '../utils/notifications';
@@ -47,9 +48,10 @@ export const Sidebar: FC = () => {
 
         <button
           type="button"
-          className={`flex justify-around self-stretch items-center my-1 py-1 px-2 text-xs font-extrabold cursor-pointer ${
-            notificationsCount > 0 ? 'text-green-500' : 'text-white'
-          }`}
+          className={cn(
+            'flex justify-around self-stretch items-center my-1 py-1 px-2 text-xs font-extrabold cursor-pointer',
+            notificationsCount > 0 ? 'text-green-500' : 'text-white',
+          )}
           onClick={() => openGitHubNotifications()}
           title={`${notificationsCount} Unread Notifications`}
         >
diff --git a/src/components/buttons/PillButton.tsx b/src/components/buttons/PillButton.tsx
index 5f92b3d1..a9207a85 100644
--- a/src/components/buttons/PillButton.tsx
+++ b/src/components/buttons/PillButton.tsx
@@ -1,6 +1,7 @@
 import type { Icon } from '@primer/octicons-react';
 import type { FC } from 'react';
 import type { IconColor } from '../../types';
+import { cn } from '../../utils/cn';
 
 export interface IPillButton {
   key?: string;
@@ -19,7 +20,7 @@ export const PillButton: FC<IPillButton> = (props: IPillButton) => {
       >
         <props.icon
           size={12}
-          className={`mr-1 ${props.color}`}
+          className={cn('mr-1', props.color)}
           aria-label={props.title}
         />
         {props.metric}
diff --git a/src/components/fields/Button.tsx b/src/components/fields/Button.tsx
index 4b54cafb..3655b553 100644
--- a/src/components/fields/Button.tsx
+++ b/src/components/fields/Button.tsx
@@ -1,11 +1,12 @@
 import type { Icon } from '@primer/octicons-react';
 import type { FC } from 'react';
+import { cn } from '../../utils/cn';
 import { openExternalLink } from '../../utils/comms';
 
 export interface IButton {
   name: string;
   label: string;
-  class?: string;
+  className?: string;
   icon?: Icon;
   size?: number;
   url?: string;
@@ -22,7 +23,7 @@ export const Button: FC<IButton> = (props: IButton) => {
       type={props.type ?? 'button'}
       title={props.label}
       aria-label={props.label}
-      className={`${props.class} ${baseClass}`}
+      className={cn(props.className, baseClass)}
       disabled={props.disabled}
       onClick={() => {
         if (props.url) {
diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx
index f33acab4..a1f80c23 100644
--- a/src/routes/Login.tsx
+++ b/src/routes/Login.tsx
@@ -52,14 +52,14 @@ export const LoginRoute: FC = () => {
         name="Personal Access Token"
         icon={KeyIcon}
         label="Login with Personal Access Token"
-        class="py-2 mt-2"
+        className="py-2 mt-2"
         onClick={() => navigate('/login-personal-access-token')}
       />
       <Button
         name="OAuth App"
         icon={PersonIcon}
         label="Login with OAuth App"
-        class="py-2 mt-2"
+        className="py-2 mt-2"
         onClick={() => navigate('/login-oauth-app')}
       />
     </div>
diff --git a/src/routes/LoginWithOAuthApp.tsx b/src/routes/LoginWithOAuthApp.tsx
index f95c5415..1bf4282d 100644
--- a/src/routes/LoginWithOAuthApp.tsx
+++ b/src/routes/LoginWithOAuthApp.tsx
@@ -96,7 +96,7 @@ export const LoginWithOAuthApp: FC = () => {
           <Button
             name="Docs"
             label="GitHub Docs"
-            class="mt-2"
+            className="mt-2"
             icon={BookIcon}
             size={12}
             url={Constants.GITHUB_DOCS.OAUTH_URL}
@@ -105,7 +105,7 @@ export const LoginWithOAuthApp: FC = () => {
           <Button
             name="Login"
             label="Login"
-            class="px-4 py-2 mt-2 !text-sm"
+            className="px-4 py-2 mt-2 !text-sm"
             icon={SignInIcon}
             size={16}
             disabled={submitting || pristine}
diff --git a/src/routes/LoginWithPersonalAccessToken.tsx b/src/routes/LoginWithPersonalAccessToken.tsx
index 26b79cea..1ce3e446 100644
--- a/src/routes/LoginWithPersonalAccessToken.tsx
+++ b/src/routes/LoginWithPersonalAccessToken.tsx
@@ -101,7 +101,7 @@ export const LoginWithPersonalAccessToken: FC = () => {
           <Button
             name="Docs"
             label="GitHub Docs"
-            class="mt-2"
+            className="mt-2"
             icon={BookIcon}
             size={12}
             url={Constants.GITHUB_DOCS.PAT_URL}
@@ -109,7 +109,7 @@ export const LoginWithPersonalAccessToken: FC = () => {
           <Button
             name="Login"
             label="Login"
-            class="px-4 py-2 mt-2 !text-sm"
+            className="px-4 py-2 mt-2 !text-sm"
             icon={SignInIcon}
             size={16}
             disabled={submitting || pristine}
diff --git a/src/utils/cn.ts b/src/utils/cn.ts
new file mode 100644
index 00000000..73c46e8f
--- /dev/null
+++ b/src/utils/cn.ts
@@ -0,0 +1,5 @@
+import { type ClassValue, clsx } from 'clsx/lite';
+
+export function cn(...inputs: ClassValue[]) {
+  return clsx(...inputs);
+}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 422238c8..2032e84e 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -157,10 +157,8 @@ export function formatForDisplay(text: string[]): string {
 export function formatNotificationUpdatedAt(
   notification: Notification,
 ): string {
-  const date = notification.last_read_at ?? notification.updated_at;
-
   try {
-    return formatDistanceToNowStrict(parseISO(date), {
+    return formatDistanceToNowStrict(parseISO(notification.updated_at), {
       addSuffix: true,
     });
   } catch (e) {}
diff --git a/tsconfig.json b/tsconfig.json
index bf9e08b9..99a01a63 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
     "module": "es6",
-    "moduleResolution": "node",
+    "moduleResolution": "Bundler",
     "target": "es2022",
     "outDir": "./build/",
     "sourceMap": true,


Test patch:
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 270ecb25..3efb3ce4 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -6,7 +6,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -250,7 +250,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -551,7 +551,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -795,7 +795,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -1096,7 +1096,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -1315,7 +1315,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics comment pil
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -1591,7 +1591,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics label pills
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -1862,7 +1862,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics label pills
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -2190,7 +2190,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics linked issu
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -2409,7 +2409,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics linked issu
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -2685,7 +2685,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics linked issu
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -2904,7 +2904,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics linked issu
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -3180,7 +3180,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics milestone p
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -3475,7 +3475,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics milestone p
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -3827,7 +3827,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics milestone p
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -4122,7 +4122,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics milestone p
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -4474,7 +4474,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics showPills d
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -4613,7 +4613,7 @@ exports[`components/NotificationRow.tsx notification pills / metrics showPills d
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -4809,7 +4809,7 @@ exports[`components/NotificationRow.tsx should render itself & its children 1`]
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -4996,7 +4996,7 @@ exports[`components/NotificationRow.tsx should render itself & its children 1`]
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -5240,7 +5240,7 @@ exports[`components/NotificationRow.tsx should render itself & its children when
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -5427,7 +5427,7 @@ exports[`components/NotificationRow.tsx should render itself & its children when
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
@@ -5671,7 +5671,7 @@ exports[`components/NotificationRow.tsx should render itself & its children with
   "baseElement": <body>
     <div>
       <div
-        class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+        class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
         id="138661096"
       >
         <div
@@ -5862,7 +5862,7 @@ exports[`components/NotificationRow.tsx should render itself & its children with
   </body>,
   "container": <div>
     <div
-      class="flex py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+      class="group flex border-b border-gray-100 bg-white px-3 py-2 hover:bg-gray-100 dark:border-gray-darker dark:bg-gray-dark dark:text-white dark:hover:bg-gray-darker"
       id="138661096"
     >
       <div
diff --git a/src/components/__snapshots__/Repository.test.tsx.snap b/src/components/__snapshots__/Repository.test.tsx.snap
index dba53036..74b3cd8f 100644
--- a/src/components/__snapshots__/Repository.test.tsx.snap
+++ b/src/components/__snapshots__/Repository.test.tsx.snap
@@ -73,12 +73,10 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
         </div>
       </div>
       <div>
-        <div>
-          NotificationRow
-        </div>
-        <div>
-          NotificationRow
-        </div>
+        NotificationRow
+      </div>
+      <div>
+        NotificationRow
       </div>
     </div>
   </body>,
@@ -151,12 +149,10 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
       </div>
     </div>
     <div>
-      <div>
-        NotificationRow
-      </div>
-      <div>
-        NotificationRow
-      </div>
+      NotificationRow
+    </div>
+    <div>
+      NotificationRow
     </div>
   </div>,
   "debug": [Function],
@@ -295,12 +291,10 @@ exports[`components/Repository.tsx should use default repository icon when avata
         </div>
       </div>
       <div>
-        <div>
-          NotificationRow
-        </div>
-        <div>
-          NotificationRow
-        </div>
+        NotificationRow
+      </div>
+      <div>
+        NotificationRow
       </div>
     </div>
   </body>,
@@ -382,12 +376,10 @@ exports[`components/Repository.tsx should use default repository icon when avata
       </div>
     </div>
     <div>
-      <div>
-        NotificationRow
-      </div>
-      <div>
-        NotificationRow
-      </div>
+      NotificationRow
+    </div>
+    <div>
+      NotificationRow
     </div>
   </div>,
   "debug": [Function],
diff --git a/src/components/fields/__snapshots__/Button.test.tsx.snap b/src/components/fields/__snapshots__/Button.test.tsx.snap
index 9e8117b3..48f860f0 100644
--- a/src/components/fields/__snapshots__/Button.test.tsx.snap
+++ b/src/components/fields/__snapshots__/Button.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`components/fields/Button.tsx should render with icon 1`] = `
     <div>
       <button
         aria-label="button"
-        class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+        class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
         title="button"
         type="button"
       >
@@ -32,7 +32,7 @@ exports[`components/fields/Button.tsx should render with icon 1`] = `
   "container": <div>
     <button
       aria-label="button"
-      class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+      class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
       title="button"
       type="button"
     >
@@ -114,7 +114,7 @@ exports[`components/fields/Button.tsx should render without icon 1`] = `
     <div>
       <button
         aria-label="button"
-        class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+        class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
         title="button"
         type="button"
       >
@@ -125,7 +125,7 @@ exports[`components/fields/Button.tsx should render without icon 1`] = `
   "container": <div>
     <button
       aria-label="button"
-      class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+      class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
       title="button"
       type="button"
     >
diff --git a/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap b/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
index e1933982..2cca26a8 100644
--- a/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
+++ b/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
@@ -81,7 +81,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
                 >
                   <button
                     aria-label="Create new OAuth App"
-                    class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+                    class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
                     disabled=""
                     title="Create new OAuth App"
                     type="button"
@@ -283,7 +283,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
               >
                 <button
                   aria-label="Create new OAuth App"
-                  class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+                  class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
                   disabled=""
                   title="Create new OAuth App"
                   type="button"
diff --git a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
index c64aeb84..c0ca85e8 100644
--- a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
+++ b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
@@ -107,7 +107,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
                   <div>
                     <button
                       aria-label="Generate a PAT"
-                      class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+                      class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
                       title="Generate a PAT"
                       type="button"
                     >
@@ -298,7 +298,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
                 <div>
                   <button
                     aria-label="Generate a PAT"
-                    class="undefined rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
+                    class="rounded bg-gray-300 font-semibold text-xs text-center hover:bg-gray-500 hover:text-white dark:text-black focus:outline-none cursor-pointer px-2 py-1"
                     title="Generate a PAT"
                     type="button"
                   >
diff --git a/src/utils/cn.test.ts b/src/utils/cn.test.ts
new file mode 100644
index 00000000..31dd4b0e
--- /dev/null
+++ b/src/utils/cn.test.ts
@@ -0,0 +1,13 @@
+import { cn } from './cn';
+
+describe('utils/cn.ts', () => {
+  it('should return a string', () => {
+    expect(cn('foo', true && 'bar', false && 'baz')).toBe('foo bar');
+  });
+
+  it('should ignore anything that is not a string', () => {
+    expect(cn('foo', null, undefined, 1, '', ['bar'], { baz: 'qux' })).toBe(
+      'foo',
+    );
+  });
+});
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index 49faffa6..25b15991 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -464,16 +464,6 @@ describe('utils/helpers.ts', () => {
     });
 
     describe('formatNotificationUpdatedAt', () => {
-      it('should use last_read_at if available', () => {
-        const notification = {
-          ...mockSingleNotification,
-          last_read_at: '2021-06-23T16:00:00Z',
-          updated_at: '2021-06-23T17:00:00Z',
-        };
-
-        expect(formatNotificationUpdatedAt(notification)).toContain('ago');
-      });
-
       it('should use updated_at if last_read_at is null', () => {
         const notification = {
           ...mockSingleNotification,