Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,150 @@ import { isLinux, isMacOS, isWindows } from '../shared/platform';

import { invokeMainEvent, onRendererEvent, sendMainEvent } from './utils';

/**
* The Gitify Bridge API exposed to the renderer via `contextBridge`.
*
* All renderer↔main IPC communication must go through this object.
* It is available on `window.gitify` inside the renderer process.
*/
export const api = {
/**
* Open a URL in the user's default browser.
*
* @param url - The URL to open.
* @param openInForeground - When `true`, brings the browser to the foreground.
*/
openExternalLink: (url: string, openInForeground: boolean) => {
sendMainEvent(EVENTS.OPEN_EXTERNAL, {
url: url,
activate: openInForeground,
});
},

/**
* Encrypt a plaintext string using Electron's safe storage.
*
* @param value - The plaintext string to encrypt.
* @returns A promise resolving to the encrypted string.
*/
encryptValue: (value: string) =>
invokeMainEvent(EVENTS.SAFE_STORAGE_ENCRYPT, value),

/**
* Decrypt an encrypted string using Electron's safe storage.
*
* @param value - The encrypted string to decrypt.
* @returns A promise resolving to the plaintext string.
*/
decryptValue: (value: string) =>
invokeMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, value),

/**
* Enable or disable launching the application at system login.
*
* @param value - `true` to enable auto-launch, `false` to disable.
*/
setAutoLaunch: (value: boolean) =>
sendMainEvent(EVENTS.UPDATE_AUTO_LAUNCH, {
openAtLogin: value,
openAsHidden: value,
}),

/**
* Register or unregister the global keyboard shortcut for toggling the app window.
*
* @param keyboardShortcut - `true` to register the shortcut, `false` to unregister.
*/
setKeyboardShortcut: (keyboardShortcut: boolean) => {
sendMainEvent(EVENTS.UPDATE_KEYBOARD_SHORTCUT, {
enabled: keyboardShortcut,
keyboardShortcut: APPLICATION.DEFAULT_KEYBOARD_SHORTCUT,
});
},

/** Tray icon controls. */
tray: {
/**
* Update the tray icon color based on unread notification count.
*
* Pass a negative number to set the error state color.
*
* @param notificationsCount - Number of unread notifications.
*/
updateColor: (notificationsCount = 0) => {
sendMainEvent(EVENTS.UPDATE_ICON_COLOR, notificationsCount);
},

/**
* Update the tray icon title (the text shown next to the icon).
*
* @param title - The title string to display. Pass an empty string to clear it.
*/
updateTitle: (title = '') => sendMainEvent(EVENTS.UPDATE_ICON_TITLE, title),

/**
* Switch the tray icon to an alternate idle icon variant.
*
* @param value - `true` to use the alternate idle icon, `false` for the default.
*/
useAlternateIdleIcon: (value: boolean) =>
sendMainEvent(EVENTS.USE_ALTERNATE_IDLE_ICON, value),

/**
* Switch the tray icon to an "active" variant when there are unread notifications.
*
* @param value - `true` to use the unread-active icon, `false` for the default.
*/
useUnreadActiveIcon: (value: boolean) =>
sendMainEvent(EVENTS.USE_UNREAD_ACTIVE_ICON, value),
},

/**
* Resolve the absolute file path of the notification sound asset.
*
* @returns A promise resolving to the sound file path.
*/
notificationSoundPath: () => invokeMainEvent(EVENTS.NOTIFICATION_SOUND_PATH),

/**
* Resolve the absolute directory path of the bundled Twemoji assets.
*
* @returns A promise resolving to the Twemoji directory path.
*/
twemojiDirectory: () => invokeMainEvent(EVENTS.TWEMOJI_DIRECTORY),

/** Platform detection helpers. */
/** Platform detection helpers. */
platform: {
/** Returns `true` when running on Linux. */
isLinux: () => isLinux(),

/** Returns `true` when running on macOS. */
isMacOS: () => isMacOS(),

/** Returns `true` when running on Windows. */
isWindows: () => isWindows(),
},

/** Application window and lifecycle controls. */
app: {
/** Hide the application window. */
hide: () => sendMainEvent(EVENTS.WINDOW_HIDE),

/** Show and focus the application window. */
show: () => sendMainEvent(EVENTS.WINDOW_SHOW),

/** Quit the application. */
quit: () => sendMainEvent(EVENTS.QUIT),

/**
* Return the application version string.
*
* Returns `"dev"` in development mode; otherwise returns `"v<semver>"`
* retrieved from the main process.
*
* @returns A promise resolving to the version string.
*/
version: async () => {
if (process.env.NODE_ENV === 'development') {
return 'dev';
Expand All @@ -77,24 +161,61 @@ export const api = {
},
},

/** Electron web frame zoom controls. */
zoom: {
/**
* Return the current Electron zoom level.
*
* @returns The current zoom level (0 = 100%).
*/
getLevel: () => webFrame.getZoomLevel(),

/**
* Set the Electron zoom level.
*
* @param zoomLevel - The zoom level to apply (0 = 100%).
*/
setLevel: (zoomLevel: number) => webFrame.setZoomLevel(zoomLevel),
},

/**
* Register a callback invoked when the main process requests an app reset.
*
* @param callback - Called when the reset event is received.
*/
onResetApp: (callback: () => void) => {
onRendererEvent(EVENTS.RESET_APP, () => callback());
},

/**
* Register a callback invoked when the main process delivers an OAuth callback URL.
*
* @param callback - Called with the full callback URL (e.g. `gitify://oauth?code=...`).
*/
onAuthCallback: (callback: (url: string) => void) => {
onRendererEvent(EVENTS.AUTH_CALLBACK, (_, url) => callback(url));
},

/**
* Register a callback invoked when the OS system theme changes.
*
* @param callback - Called with the new theme string (`"light"` or `"dark"`).
*/
onSystemThemeUpdate: (callback: (theme: string) => void) => {
onRendererEvent(EVENTS.UPDATE_THEME, (_, theme) => callback(theme));
},

/**
* Display a native OS notification.
*
* Clicking the notification opens `url` in the browser (hiding the app window),
* or shows the app window if no URL is provided.
*
* @param title - The notification title.
* @param body - The notification body text.
* @param url - Optional URL to open when the notification is clicked.
* @returns The created `Notification` instance.
*/
raiseNativeNotification: (title: string, body: string, url?: string) => {
const notification = new Notification(title, { body: body, silent: true });
notification.onclick = () => {
Expand Down
5 changes: 5 additions & 0 deletions src/preload/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type { api } from '.';

/**
* The type of the Gitify Bridge API exposed to the renderer via `contextBridge`.
*
* Mirrors the shape of `window.gitify` as declared in `preload.d.ts`.
*/
export type GitifyAPI = typeof api;
21 changes: 13 additions & 8 deletions src/preload/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { ipcRenderer } from 'electron';
import type { EventData, EventType } from '../shared/events';

/**
* Send renderer event without expecting a response
* @param event the type of event to send
* @param data the data to send with the event
* Send a fire-and-forget IPC message from the renderer to the main process.
*
* @param event - The IPC event type to send.
* @param data - Optional payload to include with the event.
*/
export function sendMainEvent(event: EventType, data?: EventData): void {
ipcRenderer.send(event, data);
}

/**
* Send renderer event and expect a response
* @param event the type of event to send
* @param data the data to send with the event
* @returns
* Send an IPC message from the renderer to the main process and await a response.
*
* @param event - The IPC event type to invoke.
* @param data - Optional string payload to include with the event.
* @returns A promise that resolves to the string response from the main process.
*/
export function invokeMainEvent(
event: EventType,
Expand All @@ -25,7 +27,10 @@ export function invokeMainEvent(
}

/**
* Handle renderer event without expecting a response
* Register a listener for an IPC event sent from the main process to the renderer.
*
* @param event - The IPC event type to listen for.
* @param listener - The callback invoked when the event is received.
*/
export function onRendererEvent(
event: EventType,
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/hooks/timers/useInactivityTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const events = ['mousedown', 'keypress', 'click'];

/**
* Hook that triggers a callback after a specified period of user inactivity.
* User activity as defined in `events` will reset the timer.
* User activity (mousedown, keypress, click) resets the timer.
*
* @param callback - The function to call once inactivity exceeds `delay`.
* @param delay - Inactivity timeout in milliseconds.
*/
export const useInactivityTimer = (callback: () => void, delay: number) => {
const savedCallback = useRef<(() => void) | null>(null);
Expand Down
11 changes: 5 additions & 6 deletions src/renderer/hooks/timers/useIntervalTimer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useRef } from 'react';

import { isOnline } from '../../utils/system/network';

/**
* Hook that triggers a callback after a specified period of time.
* Hook that triggers a callback on a recurring interval.
*
* Thanks to https://overreacted.io/making-setinterval-declarative-with-react-hooks/
*
* @param callback - Function to call on each interval tick. Always uses the latest reference.
* @param delay - Interval duration in milliseconds. Pass `null` to disable.
*/
export const useIntervalTimer = (callback: () => void, delay: number) => {
const savedCallback = useRef<(() => void) | null>(null);
Expand All @@ -18,9 +19,7 @@ export const useIntervalTimer = (callback: () => void, delay: number) => {
// Set up the interval.
useEffect(() => {
function tick() {
if (savedCallback.current && isOnline()) {
savedCallback.current();
}
savedCallback.current();
}

if (delay !== null) {
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ interface NotificationsState {
) => Promise<void>;
}

/**
* Hook that manages all notification state and actions for the application.
*
* Handles fetching, filtering, sound/native notification triggering,
* mark-as-read, mark-as-done, and unsubscribe operations across all accounts.
*
* @returns The current notifications state and action callbacks.
*/
export const useNotifications = (): NotificationsState => {
const [status, setStatus] = useState<Status>('success');
const [globalError, setGlobalError] = useState<GitifyError>();
Expand Down
21 changes: 10 additions & 11 deletions src/renderer/utils/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import type { TypedDocumentString } from './graphql/generated/graphql';
import { createOctokitClient } from './octokit';

/**
* Perform a GraphQL API request with typed operation document
* Perform a GraphQL API request with typed operation document.
*
* @param url The API url
* @param query The GraphQL operation/query statement
* @param variables The GraphQL operation variables
* @returns Resolves to a GitHub GraphQL response
* @param account - The authenticated account to make the request with.
* @param query - The typed GraphQL operation document.
* @param variables - The GraphQL operation variables.
* @returns Resolves to a typed GitHub GraphQL response.
*/
export async function performGraphQLRequest<TResult, TVariables>(
account: Account,
Expand All @@ -35,13 +35,12 @@ export async function performGraphQLRequest<TResult, TVariables>(
/**
* Perform a GraphQL API request using a raw query string instead of a TypedDocumentString.
*
* Useful for dynamically composed queries (e.g: merged queries built at runtime).
* Useful for dynamically composed queries (e.g. merged queries built at runtime).
*
* @param url The API url
* @param token The GitHub token (decrypted)
* @param query The GraphQL operation/query statement
* @param variables The GraphQL operation variables
* @returns Resolves to a GitHub GraphQL response
* @param account - The authenticated account to make the request with.
* @param query - The raw GraphQL operation/query string.
* @param variables - The GraphQL operation variables.
* @returns Resolves to a typed GitHub GraphQL response.
*/
export async function performGraphQLRequestString<TResult>(
account: Account,
Expand Down
Loading