Repository: gitify-app/gitify
Pull request #866 (2024-03-07 19:47:10)

feat: add hover title text to icons
This PR adds text which shows when mousing over the notification icons.

The text is formatted based on the notification subject type and reason (if applicable)

*Examples*
![Screenshot 2024-03-11 at 11 41 52 AM](https://github.com/gitify-app/gitify/assets/386277/7d6189d0-e2d0-4391-8958-b96d3f0a6f00)
![Screenshot 2024-03-11 at 11 42 08 AM](https://github.com/gitify-app/gitify/assets/386277/e62665e1-fe48-4d54-bc05-c5522e29f742)
![Screenshot 2024-03-11 at 11 42 16 AM](https://github.com/gitify-app/gitify/assets/386277/17e3e0f4-1e36-4d23-881c-29f8d678d87e)



Patch:
diff --git a/package.json b/package.json
index e273b0ef..91ac089a 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
     "@electron/remote": "2.1.2",
     "@primer/octicons-react": "19.8.0",
     "axios": "1.6.7",
-    "date-fns": "3.3.1",
+    "date-fns": "3.4.0",
     "electron-updater": "6.1.8",
     "final-form": "4.20.10",
     "menubar": "9.4.0",
@@ -131,7 +131,7 @@
     "@types/react-transition-group": "4.4.10",
     "autoprefixer": "10.4.18",
     "css-loader": "6.10.0",
-    "electron": "29.1.0",
+    "electron": "29.1.1",
     "electron-builder": "24.13.3",
     "husky": "9.0.11",
     "jest": "29.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b86a9e88..a570271b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,7 +7,7 @@ settings:
 dependencies:
   '@electron/remote':
     specifier: 2.1.2
-    version: 2.1.2(electron@29.1.0)
+    version: 2.1.2(electron@29.1.1)
   '@primer/octicons-react':
     specifier: 19.8.0
     version: 19.8.0(react@18.2.0)
@@ -15,8 +15,8 @@ dependencies:
     specifier: 1.6.7
     version: 1.6.7
   date-fns:
-    specifier: 3.3.1
-    version: 3.3.1
+    specifier: 3.4.0
+    version: 3.4.0
   electron-updater:
     specifier: 6.1.8
     version: 6.1.8
@@ -25,7 +25,7 @@ dependencies:
     version: 4.20.10
   menubar:
     specifier: 9.4.0
-    version: 9.4.0(electron@29.1.0)
+    version: 9.4.0(electron@29.1.1)
   nprogress:
     specifier: 0.2.0
     version: 0.2.0
@@ -89,8 +89,8 @@ devDependencies:
     specifier: 6.10.0
     version: 6.10.0(webpack@5.90.3)
   electron:
-    specifier: 29.1.0
-    version: 29.1.0
+    specifier: 29.1.1
+    version: 29.1.1
   electron-builder:
     specifier: 24.13.3
     version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
@@ -573,12 +573,12 @@ packages:
       - supports-color
     dev: true
 
-  /@electron/remote@2.1.2(electron@29.1.0):
+  /@electron/remote@2.1.2(electron@29.1.1):
     resolution: {integrity: sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==}
     peerDependencies:
       electron: '>= 13.0.0'
     dependencies:
-      electron: 29.1.0
+      electron: 29.1.1
     dev: false
 
   /@electron/universal@1.5.1:
@@ -2219,8 +2219,8 @@ packages:
       whatwg-url: 11.0.0
     dev: true
 
-  /date-fns@3.3.1:
-    resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==}
+  /date-fns@3.4.0:
+    resolution: {integrity: sha512-Akz4R8J9MXBsOgF1QeWeCsbv6pntT5KCPjU0Q9prBxVmWJYPLhwAIsNg3b0QAdr0ttiozYLD3L/af7Ra0jqYXw==}
     dev: false
 
   /debug@4.3.4:
@@ -2487,8 +2487,8 @@ packages:
       - supports-color
     dev: false
 
-  /electron@29.1.0:
-    resolution: {integrity: sha512-giJVIm0sWVp+8V1GXrKqKTb+h7no0P3ooYqEd34AD9wMJzGnAeL+usj+R0155/0pdvvP1mgydnA7lcaFA2M9lw==}
+  /electron@29.1.1:
+    resolution: {integrity: sha512-cXN15NgCi7MkzGo5/23ZQbii+0UfhmUiDjACunmzcUofYCjF42XhFbL7JZnwgI0qtBCCeJU8qZNZt9lU91gUFw==}
     engines: {node: '>= 12.20.55'}
     hasBin: true
     requiresBuild: true
@@ -4141,12 +4141,12 @@ packages:
       escape-string-regexp: 4.0.0
     optional: true
 
-  /menubar@9.4.0(electron@29.1.0):
+  /menubar@9.4.0(electron@29.1.1):
     resolution: {integrity: sha512-Zn2si5UXiWbdBZwFSsCyQ8xfaBKkeFYceNvjWATyMUreqv6gDMUvDkvZY/ZFf89DCfo9F0SrvygDOdpZuwoWgw==}
     peerDependencies:
       electron: ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0 ||  ^23.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0
     dependencies:
-      electron: 29.1.0
+      electron: 29.1.1
       electron-positioner: 4.1.0
     dev: false
 
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 44c049c0..58c79021 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -7,7 +7,7 @@ import {
   getNotificationTypeIcon,
   getNotificationTypeIconColor,
 } from '../utils/github-api';
-import { openInBrowser } from '../utils/helpers';
+import { formatForDisplay, openInBrowser } from '../utils/helpers';
 import { Notification } from '../typesGithub';
 import { AppContext } from '../context/App';
 
@@ -61,10 +61,17 @@ export const NotificationRow: React.FC<IProps> = ({
   const updatedAt = formatDistanceToNow(parseISO(notification.updated_at), {
     addSuffix: true,
   });
+  const notificationTitle = formatForDisplay([
+    notification.subject.state,
+    notification.subject.type,
+  ]);
 
   return (
     <div className="flex space-x-3 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">
-      <div className={`flex justify-center items-center w-5 ${realIconColor}`}>
+      <div
+        className={`flex justify-center items-center w-5 ${realIconColor}`}
+        title={notificationTitle}
+      >
         <NotificationIcon size={18} aria-label={notification.subject.type} />
       </div>
 
diff --git a/src/typesGithub.ts b/src/typesGithub.ts
index 2f46d015..a6029391 100644
--- a/src/typesGithub.ts
+++ b/src/typesGithub.ts
@@ -165,6 +165,79 @@ export interface Subject {
   type: SubjectType;
 }
 
+export interface PullRequest {
+  url: string;
+  id: number;
+  node_id: string;
+  html_url: string;
+  diff_url: string;
+  patch_url: string;
+  issue_url: string;
+  number: number;
+  state: PullRequestStateType;
+  locked: boolean;
+  title: string;
+  user: User;
+  body: string;
+  created_at: string;
+  updated_at: string;
+  closed_at: string | null;
+  merged_at: string | null;
+  merge_commit_sha: string | null;
+  draft: boolean;
+  commits_url: string;
+  review_comments_url: string;
+  review_comment_url: string;
+  comments_url: string;
+  statuses_url: string;
+  author_association: string;
+  merged: boolean;
+  mergeable: boolean;
+  rebaseable: boolean;
+  comments: number;
+  review_comments: number;
+  maintainer_can_modify: boolean;
+  commits: number;
+  additions: number;
+  deletions: number;
+  changed_files: number;
+}
+
+export interface Issue {
+  url: string;
+  repository_url: string;
+  labels_url: string;
+  comments_url: string;
+  events_url: string;
+  html_url: string;
+  id: number;
+  node_id: string;
+  number: number;
+  title: string;
+  user: User;
+  state: IssueStateType;
+  locked: boolean;
+  comments: number;
+  created_at: string;
+  updated_at: string;
+  closed_at: string | null;
+  author_association: string;
+  body: string;
+  state_reason: IssueStateReasonType | null;
+}
+
+export interface IssueComments {
+  url: string;
+  html_url: string;
+  issue_url: string;
+  id: number;
+  node_id: string;
+  user: User;
+  created_at: string;
+  updated_at: string;
+  body: string;
+}
+
 export interface GraphQLSearch<T> {
   data: {
     data: {
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 322fb628..fdb9109e 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -4,6 +4,9 @@ import {
   GraphQLSearch,
   DiscussionCommentNode,
   DiscussionSearchResultNode,
+  PullRequest,
+  Issue,
+  IssueComments,
 } from '../typesGithub';
 import { apiRequestAuth } from '../utils/api-requests';
 import { openExternalLink } from '../utils/comms';
@@ -66,9 +69,11 @@ export function formatSearchQueryString(
 }
 
 export async function getHtmlUrl(url: string, token: string): Promise<string> {
-  const response = await apiRequestAuth(url, 'GET', token);
+  const response: Issue | IssueComments | PullRequest = (
+    await apiRequestAuth(url, 'GET', token)
+  ).data;
 
-  return response.data.html_url;
+  return response.html_url;
 }
 
 export function getCheckSuiteUrl(notification: Notification) {
@@ -225,6 +230,21 @@ export async function generateGitHubWebUrl(
   return url;
 }
 
+export function formatForDisplay(text: string[]) {
+  if (!text) {
+    return '';
+  }
+
+  return text
+    .join(' ')
+    .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase character followed by an uppercase character
+    .replace(/_/g, ' ') // Replace underscores with spaces
+    .replace(/\w+/g, (word) => {
+      // Convert to proper case (capitalize first letter of each word)
+      return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+    });
+}
+
 export async function openInBrowser(
   notification: Notification,
   accounts: AuthState,
diff --git a/src/utils/state.ts b/src/utils/state.ts
index ad783e89..9680f133 100644
--- a/src/utils/state.ts
+++ b/src/utils/state.ts
@@ -5,8 +5,11 @@ import {
   DiscussionStateSearchResultNode,
   DiscussionStateType,
   GraphQLSearch,
+  Issue,
+  IssueStateReasonType,
   IssueStateType,
   Notification,
+  PullRequest,
   PullRequestStateType,
   StateType,
   WorkflowRunAttributes,
@@ -129,9 +132,10 @@ export async function getDiscussionState(
 export async function getIssueState(
   notification: Notification,
   token: string,
-): Promise<IssueStateType> {
-  const issue = (await apiRequestAuth(notification.subject.url, 'GET', token))
-    .data;
+): Promise<IssueStateType | IssueStateReasonType> {
+  const issue: Issue = (
+    await apiRequestAuth(notification.subject.url, 'GET', token)
+  ).data;
 
   return issue.state_reason ?? issue.state;
 }
@@ -140,8 +144,9 @@ export async function getPullRequestState(
   notification: Notification,
   token: string,
 ): Promise<PullRequestStateType> {
-  const pr = (await apiRequestAuth(notification.subject.url, 'GET', token))
-    .data;
+  const pr: PullRequest = (
+    await apiRequestAuth(notification.subject.url, 'GET', token)
+  ).data;
 
   if (pr.merged) {
     return 'merged';


Test patch:
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 643122d3..13da2c7f 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -6,6 +6,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
 >
   <div
     className="flex justify-center items-center w-5 text-green-500"
+    title="Open Issue"
   >
     <svg
       aria-hidden="false"
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index 3f5e3625..ce5cd519 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -7,6 +7,7 @@ import {
   addNotificationReferrerIdToUrl,
   getHtmlUrl,
   generateGitHubWebUrl,
+  formatForDisplay,
 } from './helpers';
 import {
   mockedGraphQLResponse,
@@ -558,5 +559,19 @@ describe('utils/helpers.ts', () => {
         `${mockedSingleNotification.repository.html_url}?${mockedNotificationReferrer}`,
       );
     });
+
+    it('formatForDisplay', () => {
+      expect(formatForDisplay(null)).toBe('');
+      expect(formatForDisplay([])).toBe('');
+      expect(formatForDisplay(['open', 'PullRequest'])).toBe(
+        'Open Pull Request',
+      );
+      expect(formatForDisplay(['OUTDATED', 'Discussion'])).toBe(
+        'Outdated Discussion',
+      );
+      expect(formatForDisplay(['not_planned', 'Issue'])).toBe(
+        'Not Planned Issue',
+      );
+    });
   });
 });