• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gitify-app / gitify / 12586431667

02 Jan 2025 05:40PM UTC coverage: 87.209%. Remained the same
12586431667

Pull #1700

github

web-flow
Merge 35a8d97a0 into 0e8d27442
Pull Request #1700: build(deps): bump cross-spawn from 7.0.3 to 7.0.6

591 of 656 branches covered (90.09%)

Branch coverage included in aggregate %.

1584 of 1838 relevant lines covered (86.18%)

24.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.23
/src/renderer/context/App.tsx
1
import { ipcRenderer, webFrame } from 'electron';
72✔
2
import {
72✔
3
  type ReactNode,
4
  createContext,
5
  useCallback,
6
  useEffect,
7
  useMemo,
8
  useState,
9
} from 'react';
10
import { useInterval } from '../hooks/useInterval';
72✔
11
import { useNotifications } from '../hooks/useNotifications';
72✔
12
import {
72✔
13
  type Account,
14
  type AccountNotifications,
15
  type AuthState,
16
  type GitifyError,
17
  GroupBy,
18
  OpenPreference,
19
  type SettingsState,
20
  type SettingsValue,
21
  type Status,
22
  Theme,
23
} from '../types';
24
import type { Notification } from '../typesGitHub';
25
import { headNotifications } from '../utils/api/client';
72✔
26
import { migrateAuthenticatedAccounts } from '../utils/auth/migration';
72✔
27
import type {
28
  LoginOAuthAppOptions,
29
  LoginPersonalAccessTokenOptions,
30
} from '../utils/auth/types';
31
import {
72✔
32
  addAccount,
33
  authGitHub,
34
  getToken,
35
  hasAccounts,
36
  refreshAccount,
37
  removeAccount,
38
} from '../utils/auth/utils';
39
import {
72✔
40
  setAlternateIdleIcon,
41
  setAutoLaunch,
42
  setKeyboardShortcut,
43
  updateTrayTitle,
44
} from '../utils/comms';
45
import { Constants } from '../utils/constants';
72✔
46
import { getNotificationCount } from '../utils/notifications';
72✔
47
import { clearState, loadState, saveState } from '../utils/storage';
72✔
48
import { setTheme } from '../utils/theme';
72✔
49
import { zoomPercentageToLevel } from '../utils/zoom';
72✔
50

51
export const defaultAuth: AuthState = {
72✔
52
  accounts: [],
53
  token: null,
54
  enterpriseAccounts: [],
55
  user: null,
56
};
57

58
const defaultAppearanceSettings = {
72✔
59
  theme: Theme.SYSTEM,
60
  zoomPercentage: 100,
61
  detailedNotifications: true,
62
  showPills: true,
63
  showNumber: true,
64
  showAccountHeader: false,
65
};
66

67
const defaultNotificationSettings = {
72✔
68
  groupBy: GroupBy.REPOSITORY,
69
  fetchAllNotifications: true,
70
  participating: false,
71
  markAsDoneOnOpen: false,
72
  markAsDoneOnUnsubscribe: false,
73
  delayNotificationState: false,
74
};
75

76
const defaultSystemSettings = {
72✔
77
  openLinks: OpenPreference.FOREGROUND,
78
  keyboardShortcut: true,
79
  showNotificationsCountInTray: false,
80
  showNotifications: true,
81
  playSound: true,
82
  useAlternateIdleIcon: false,
83
  openAtStartup: false,
84
};
85

86
export const defaultFilters = {
72✔
87
  hideBots: false,
88
  filterReasons: [],
89
};
90

91
export const defaultSettings: SettingsState = {
72✔
92
  ...defaultAppearanceSettings,
93
  ...defaultNotificationSettings,
94
  ...defaultSystemSettings,
95
  ...defaultFilters,
96
};
97

98
interface AppContextState {
99
  auth: AuthState;
100
  isLoggedIn: boolean;
101
  loginWithGitHubApp: () => void;
102
  loginWithOAuthApp: (data: LoginOAuthAppOptions) => void;
103
  loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => void;
104
  logoutFromAccount: (account: Account) => void;
105

106
  notifications: AccountNotifications[];
107
  status: Status;
108
  globalError: GitifyError;
109
  removeAccountNotifications: (account: Account) => Promise<void>;
110
  fetchNotifications: () => Promise<void>;
111
  markNotificationsAsRead: (notifications: Notification[]) => Promise<void>;
112
  markNotificationsAsDone: (notifications: Notification[]) => Promise<void>;
113
  unsubscribeNotification: (notification: Notification) => Promise<void>;
114

115
  settings: SettingsState;
116
  clearFilters: () => void;
117
  resetSettings: () => void;
118
  updateSetting: (name: keyof SettingsState, value: SettingsValue) => void;
119
}
120

121
export const AppContext = createContext<Partial<AppContextState>>({});
72✔
122

123
export const AppProvider = ({ children }: { children: ReactNode }) => {
72✔
124
  const [auth, setAuth] = useState<AuthState>(defaultAuth);
26✔
125
  const [settings, setSettings] = useState<SettingsState>(defaultSettings);
26✔
126
  const {
127
    removeAccountNotifications,
128
    fetchNotifications,
129
    notifications,
130
    globalError,
131
    status,
132
    markNotificationsAsRead,
133
    markNotificationsAsDone,
134
    unsubscribeNotification,
135
  } = useNotifications();
26✔
136

137
  useEffect(() => {
26✔
138
    restoreSettings();
20✔
139
  }, []);
140

141
  useEffect(() => {
26✔
142
    setTheme(settings.theme);
20✔
143
  }, [settings.theme]);
144

145
  // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for account changes
146
  useEffect(() => {
26✔
147
    fetchNotifications({ auth, settings });
20✔
148
  }, [auth.accounts]);
149

150
  useInterval(() => {
26✔
151
    fetchNotifications({ auth, settings });
6✔
152
  }, Constants.FETCH_NOTIFICATIONS_INTERVAL);
153

154
  useInterval(() => {
26✔
155
    for (const account of auth.accounts) {
×
156
      refreshAccount(account);
×
157
    }
158
  }, Constants.REFRESH_ACCOUNTS_INTERVAL);
159

160
  useEffect(() => {
26✔
161
    const count = getNotificationCount(notifications);
20✔
162

163
    if (settings.showNotificationsCountInTray && count > 0) {
20!
164
      updateTrayTitle(count.toString());
×
165
    } else {
166
      updateTrayTitle();
20✔
167
    }
168
  }, [settings.showNotificationsCountInTray, notifications]);
169

170
  useEffect(() => {
26✔
171
    setKeyboardShortcut(settings.keyboardShortcut);
20✔
172
  }, [settings.keyboardShortcut]);
173

174
  useEffect(() => {
26✔
175
    ipcRenderer.on('gitify:reset-app', () => {
20✔
176
      clearState();
×
177
      setAuth(defaultAuth);
×
178
      setSettings(defaultSettings);
×
179
    });
180
  }, []);
181

182
  const clearFilters = useCallback(() => {
26✔
183
    const newSettings = { ...settings, ...defaultFilters };
2✔
184
    setSettings(newSettings);
2✔
185
    saveState({ auth, settings: newSettings });
2✔
186
  }, [auth, settings]);
187

188
  const resetSettings = useCallback(() => {
26✔
189
    setSettings(defaultSettings);
2✔
190
    saveState({ auth, settings: defaultSettings });
2✔
191
  }, [auth]);
192

193
  const updateSetting = useCallback(
26✔
194
    (name: keyof SettingsState, value: SettingsValue) => {
195
      if (name === 'openAtStartup') {
4✔
196
        setAutoLaunch(value as boolean);
2✔
197
      }
198
      if (name === 'useAlternateIdleIcon') {
4!
199
        setAlternateIdleIcon(value as boolean);
×
200
      }
201

202
      const newSettings = { ...settings, [name]: value };
4✔
203
      setSettings(newSettings);
4✔
204
      saveState({ auth, settings: newSettings });
4✔
205
    },
206
    [auth, settings],
207
  );
208

209
  const isLoggedIn = useMemo(() => {
26✔
210
    return hasAccounts(auth);
20✔
211
  }, [auth]);
212

213
  const loginWithGitHubApp = useCallback(async () => {
26✔
214
    const { authCode } = await authGitHub();
×
215
    const { token } = await getToken(authCode);
×
216
    const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;
×
217
    const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname);
×
218
    setAuth(updatedAuth);
×
219
    saveState({ auth: updatedAuth, settings });
×
220
  }, [auth, settings]);
221

222
  const loginWithOAuthApp = useCallback(
26✔
223
    async (data: LoginOAuthAppOptions) => {
224
      const { authOptions, authCode } = await authGitHub(data);
×
225
      const { token, hostname } = await getToken(authCode, authOptions);
×
226
      const updatedAuth = await addAccount(auth, 'OAuth App', token, hostname);
×
227
      setAuth(updatedAuth);
×
228
      saveState({ auth: updatedAuth, settings });
×
229
    },
230
    [auth, settings],
231
  );
232

233
  const loginWithPersonalAccessToken = useCallback(
26✔
234
    async ({ token, hostname }: LoginPersonalAccessTokenOptions) => {
235
      await headNotifications(hostname, token);
2✔
236
      const updatedAuth = await addAccount(
2✔
237
        auth,
238
        'Personal Access Token',
239
        token,
240
        hostname,
241
      );
242
      setAuth(updatedAuth);
×
243
      saveState({ auth: updatedAuth, settings });
×
244
    },
245
    [auth, settings],
246
  );
247

248
  const logoutFromAccount = useCallback(
26✔
249
    async (account: Account) => {
250
      // Remove notifications for account
251
      removeAccountNotifications(account);
×
252

253
      // Remove from auth state
254
      const updatedAuth = removeAccount(auth, account);
×
255
      setAuth(updatedAuth);
×
256
      saveState({ auth: updatedAuth, settings });
×
257
    },
258
    [auth, settings],
259
  );
260

261
  const restoreSettings = useCallback(async () => {
26✔
262
    await migrateAuthenticatedAccounts();
20✔
263
    const existing = loadState();
20✔
264

265
    // Restore settings before accounts to ensure filters are available before fetching notifications
266
    if (existing.settings) {
20!
267
      setKeyboardShortcut(existing.settings.keyboardShortcut);
×
268
      setAlternateIdleIcon(existing.settings.useAlternateIdleIcon);
×
269
      setSettings({ ...defaultSettings, ...existing.settings });
×
270
      webFrame.setZoomLevel(
×
271
        zoomPercentageToLevel(existing.settings.zoomPercentage),
272
      );
273
    }
274

275
    if (existing.auth) {
20!
276
      setAuth({ ...defaultAuth, ...existing.auth });
×
277

278
      // Refresh account data on app start
279
      for (const account of existing.auth.accounts) {
×
280
        await refreshAccount(account);
×
281
      }
282
    }
283
  }, []);
284

285
  const fetchNotificationsWithAccounts = useCallback(
26✔
286
    async () => await fetchNotifications({ auth, settings }),
2✔
287
    [auth, settings, fetchNotifications],
288
  );
289

290
  const markNotificationsAsReadWithAccounts = useCallback(
26✔
291
    async (notifications: Notification[]) =>
292
      await markNotificationsAsRead({ auth, settings }, notifications),
2✔
293
    [auth, settings, markNotificationsAsRead],
294
  );
295

296
  const markNotificationsAsDoneWithAccounts = useCallback(
26✔
297
    async (notifications: Notification[]) =>
298
      await markNotificationsAsDone({ auth, settings }, notifications),
2✔
299
    [auth, settings, markNotificationsAsDone],
300
  );
301

302
  const unsubscribeNotificationWithAccounts = useCallback(
26✔
303
    async (notification: Notification) =>
304
      await unsubscribeNotification({ auth, settings }, notification),
2✔
305
    [auth, settings, unsubscribeNotification],
306
  );
307

308
  return (
26✔
309
    <AppContext.Provider
310
      value={{
311
        auth,
312
        isLoggedIn,
313
        loginWithGitHubApp,
314
        loginWithOAuthApp,
315
        loginWithPersonalAccessToken,
316
        logoutFromAccount,
317

318
        notifications,
319
        status,
320
        globalError,
321
        fetchNotifications: fetchNotificationsWithAccounts,
322
        markNotificationsAsRead: markNotificationsAsReadWithAccounts,
323
        markNotificationsAsDone: markNotificationsAsDoneWithAccounts,
324
        unsubscribeNotification: unsubscribeNotificationWithAccounts,
325

326
        settings,
327
        clearFilters,
328
        resetSettings,
329
        updateSetting,
330
      }}
331
    >
332
      {children}
333
    </AppContext.Provider>
334
  );
335
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc