Repository: gitify-app/gitify
Pull request #1274 (2024-06-19 11:29:44)

feat: avatar component


Patch:
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 5c7c86f1..2ce62415 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -31,6 +31,7 @@ import {
 import { openNotification, openUserProfile } from '../utils/links';
 import { formatReason } from '../utils/reason';
 import { PillButton } from './buttons/PillButton';
+import { AvatarIcon } from './icons/AvatarIcon';
 
 interface INotificationRow {
   notification: Notification;
@@ -141,11 +142,11 @@ export const NotificationRow: FC<INotificationRow> = ({
               }}
               className="flex-shrink-0"
             >
-              <img
-                className="size-4 cursor-pointer rounded-full object-cover"
-                src={notification.subject.user.avatar_url}
+              <AvatarIcon
                 title={notification.subject.user.login}
-                alt={`${notification.subject.user.login}'s avatar`}
+                url={notification.subject.user.avatar_url}
+                size="small"
+                defaultIcon={FeedPersonIcon}
               />
             </button>
           ) : (
diff --git a/src/components/RepositoryNotifications.tsx b/src/components/RepositoryNotifications.tsx
index 1aadf16f..31909999 100644
--- a/src/components/RepositoryNotifications.tsx
+++ b/src/components/RepositoryNotifications.tsx
@@ -10,6 +10,7 @@ import { AppContext } from '../context/App';
 import type { Notification } from '../typesGitHub';
 import { openRepository } from '../utils/links';
 import { NotificationRow } from './NotificationRow';
+import { AvatarIcon } from './icons/AvatarIcon';
 
 interface IRepositoryNotifications {
   repoNotifications: Notification[];
@@ -55,15 +56,12 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
         onClick={toggleRepositoryNotifications}
       >
         <div className="mt-0 flex flex-1 space-x-3 overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-medium">
-          {avatarUrl ? (
-            <img
-              className="size-5 rounded"
-              src={avatarUrl}
-              alt={`${repoName}'s avatar`}
-            />
-          ) : (
-            <MarkGithubIcon size={18} />
-          )}
+          <AvatarIcon
+            title={repoName}
+            url={avatarUrl}
+            size="medium"
+            defaultIcon={MarkGithubIcon}
+          />
           <span
             className="cursor-pointer truncate opacity-90"
             onClick={() => openRepository(repoNotifications[0].repository)}
diff --git a/src/components/icons/AvatarIcon.tsx b/src/components/icons/AvatarIcon.tsx
new file mode 100644
index 00000000..08881ec7
--- /dev/null
+++ b/src/components/icons/AvatarIcon.tsx
@@ -0,0 +1,32 @@
+import type { Icon } from '@primer/octicons-react';
+import type { FC } from 'react';
+import { cn } from '../../utils/cn';
+
+export interface IAvatarIcon {
+  title: string;
+  url: string | null;
+  size: 'small' | 'medium';
+  defaultIcon: Icon;
+}
+
+export const AvatarIcon: FC<IAvatarIcon> = (props: IAvatarIcon) => {
+  if (props.url) {
+    return (
+      <img
+        className={cn(
+          'cursor-pointer rounded-full object-cover',
+          props.size === 'small' ? 'size-4' : 'size-5',
+        )}
+        src={props.url}
+        alt={`${props.title}'s avatar`}
+      />
+    );
+  }
+
+  return (
+    <props.defaultIcon
+      size={props.size === 'small' ? 14 : 18}
+      className="text-gray-500 dark:text-gray-300"
+    />
+  );
+};


Test patch:
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 1ade8bac..fa2df90e 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -4683,9 +4683,8 @@ exports[`components/NotificationRow.tsx should render itself & its children 1`]
             >
               <img
                 alt="gitify-app's avatar"
-                class="size-4 cursor-pointer rounded-full object-cover"
+                class="cursor-pointer rounded-full object-cover size-4"
                 src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
-                title="gitify-app"
               />
             </button>
             <div
@@ -4866,9 +4865,8 @@ exports[`components/NotificationRow.tsx should render itself & its children 1`]
           >
             <img
               alt="gitify-app's avatar"
-              class="size-4 cursor-pointer rounded-full object-cover"
+              class="cursor-pointer rounded-full object-cover size-4"
               src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
-              title="gitify-app"
             />
           </button>
           <div
@@ -5106,9 +5104,8 @@ exports[`components/NotificationRow.tsx should render itself & its children when
             >
               <img
                 alt="gitify-app's avatar"
-                class="size-4 cursor-pointer rounded-full object-cover"
+                class="cursor-pointer rounded-full object-cover size-4"
                 src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
-                title="gitify-app"
               />
             </button>
             <div
@@ -5289,9 +5286,8 @@ exports[`components/NotificationRow.tsx should render itself & its children when
           >
             <img
               alt="gitify-app's avatar"
-              class="size-4 cursor-pointer rounded-full object-cover"
+              class="cursor-pointer rounded-full object-cover size-4"
               src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
-              title="gitify-app"
             />
           </button>
           <div
diff --git a/src/components/__snapshots__/RepositoryNotifications.test.tsx.snap b/src/components/__snapshots__/RepositoryNotifications.test.tsx.snap
index 729d3066..5d08ac19 100644
--- a/src/components/__snapshots__/RepositoryNotifications.test.tsx.snap
+++ b/src/components/__snapshots__/RepositoryNotifications.test.tsx.snap
@@ -13,7 +13,7 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
         >
           <img
             alt="gitify-app/notifications-test's avatar"
-            class="size-5 rounded"
+            class="cursor-pointer rounded-full object-cover size-5"
             src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
           />
           <span
@@ -106,7 +106,7 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
       >
         <img
           alt="gitify-app/notifications-test's avatar"
-          class="size-5 rounded"
+          class="cursor-pointer rounded-full object-cover size-5"
           src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
         />
         <span
@@ -256,7 +256,7 @@ exports[`components/Repository.tsx should toggle account notifications visibilit
         >
           <svg
             aria-hidden="true"
-            class="octicon octicon-mark-github"
+            class="text-gray-500 dark:text-gray-300"
             fill="currentColor"
             focusable="false"
             height="18"
@@ -351,7 +351,7 @@ exports[`components/Repository.tsx should toggle account notifications visibilit
         >
           <svg
             aria-hidden="true"
-            class="octicon octicon-mark-github"
+            class="text-gray-500 dark:text-gray-300"
             fill="currentColor"
             focusable="false"
             height="18"
@@ -453,7 +453,7 @@ exports[`components/Repository.tsx should toggle account notifications visibilit
       >
         <svg
           aria-hidden="true"
-          class="octicon octicon-mark-github"
+          class="text-gray-500 dark:text-gray-300"
           fill="currentColor"
           focusable="false"
           height="18"
@@ -612,7 +612,7 @@ exports[`components/Repository.tsx should use default repository icon when avata
         >
           <svg
             aria-hidden="true"
-            class="octicon octicon-mark-github"
+            class="text-gray-500 dark:text-gray-300"
             fill="currentColor"
             focusable="false"
             height="18"
@@ -714,7 +714,7 @@ exports[`components/Repository.tsx should use default repository icon when avata
       >
         <svg
           aria-hidden="true"
-          class="octicon octicon-mark-github"
+          class="text-gray-500 dark:text-gray-300"
           fill="currentColor"
           focusable="false"
           height="18"
diff --git a/src/components/icons/AvatarIcon.test.tsx b/src/components/icons/AvatarIcon.test.tsx
new file mode 100644
index 00000000..4bb3c6c3
--- /dev/null
+++ b/src/components/icons/AvatarIcon.test.tsx
@@ -0,0 +1,38 @@
+import { MarkGithubIcon } from '@primer/octicons-react';
+import { render } from '@testing-library/react';
+import { AvatarIcon, type IAvatarIcon } from './AvatarIcon';
+
+describe('components/icons/AvatarIcon.tsx', () => {
+  it('should render small avatar', () => {
+    const props: IAvatarIcon = {
+      defaultIcon: MarkGithubIcon,
+      title: 'test',
+      url: 'https://avatars.githubusercontent.com/u/583231?v=4',
+      size: 'small',
+    };
+    const tree = render(<AvatarIcon {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should render medium avatar', () => {
+    const props: IAvatarIcon = {
+      defaultIcon: MarkGithubIcon,
+      title: 'test',
+      url: 'https://avatars.githubusercontent.com/u/583231?v=4',
+      size: 'medium',
+    };
+    const tree = render(<AvatarIcon {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should render default icon when no url', () => {
+    const props: IAvatarIcon = {
+      defaultIcon: MarkGithubIcon,
+      title: 'test',
+      url: null,
+      size: 'small',
+    };
+    const tree = render(<AvatarIcon {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/src/components/icons/__snapshots__/AvatarIcon.test.tsx.snap b/src/components/icons/__snapshots__/AvatarIcon.test.tsx.snap
new file mode 100644
index 00000000..61358d06
--- /dev/null
+++ b/src/components/icons/__snapshots__/AvatarIcon.test.tsx.snap
@@ -0,0 +1,238 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/icons/AvatarIcon.tsx should render default icon when no url 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <svg
+        aria-hidden="true"
+        class="text-gray-500 dark:text-gray-300"
+        fill="currentColor"
+        focusable="false"
+        height="14"
+        style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+        viewBox="0 0 16 16"
+        width="14"
+      >
+        <path
+          d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+        />
+      </svg>
+    </div>
+  </body>,
+  "container": <div>
+    <svg
+      aria-hidden="true"
+      class="text-gray-500 dark:text-gray-300"
+      fill="currentColor"
+      focusable="false"
+      height="14"
+      style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+      viewBox="0 0 16 16"
+      width="14"
+    >
+      <path
+        d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+      />
+    </svg>
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;
+
+exports[`components/icons/AvatarIcon.tsx should render medium avatar 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <img
+        alt="test's avatar"
+        class="cursor-pointer rounded-full object-cover size-5"
+        src="https://avatars.githubusercontent.com/u/583231?v=4"
+      />
+    </div>
+  </body>,
+  "container": <div>
+    <img
+      alt="test's avatar"
+      class="cursor-pointer rounded-full object-cover size-5"
+      src="https://avatars.githubusercontent.com/u/583231?v=4"
+    />
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;
+
+exports[`components/icons/AvatarIcon.tsx should render small avatar 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <img
+        alt="test's avatar"
+        class="cursor-pointer rounded-full object-cover size-4"
+        src="https://avatars.githubusercontent.com/u/583231?v=4"
+      />
+    </div>
+  </body>,
+  "container": <div>
+    <img
+      alt="test's avatar"
+      class="cursor-pointer rounded-full object-cover size-4"
+      src="https://avatars.githubusercontent.com/u/583231?v=4"
+    />
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;