Skip to content
Draft
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
5 changes: 3 additions & 2 deletions src/main/lifecycle/first-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ const getConfigPath = () => {
};

/**
* Determine if this is the first run of the application by checking for the existence of a specific file.
* Determine whether this is the first run by checking for the marker file. When
* it is the first run, create the marker so later launches skip this check.
*
* @returns true if this is the first run, false otherwise
* @returns `true` if this is the first run, `false` otherwise.
*/
function checkAndMarkFirstRun(): boolean {
const configPath = getConfigPath();
Expand Down
6 changes: 3 additions & 3 deletions src/main/lifecycle/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function initializeAppLifecycle(
}

/**
* Handle a gitify:// protocol URL by forwarding it to the renderer process
* as an AUTH_CALLBACK event.
* Handle a custom-protocol URL by forwarding it to the renderer process as an
* AUTH_CALLBACK event.
*
* @param mb - The menubar instance to forward the event through.
* @param url - The protocol URL to handle.
Expand All @@ -53,7 +53,7 @@ export function handleProtocolURL(mb: Menubar, url: string, protocol: string): v
* Enforce a single application instance. If a second instance is launched,
* any protocol URL in the command line is forwarded to the existing instance.
*
* @param mb - The menubar instance to show when a second instance is detected.
* @param mb - The menubar instance used to forward a detected protocol URL.
* @param protocol - The custom protocol string to extract from command line args.
*/
function preventSecondInstance(mb: Menubar, protocol: string): void {
Expand Down
11 changes: 11 additions & 0 deletions src/preload/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type { EventArgs, EventRequest, EventResponse, EventType } from '../share
/**
* Send a fire-and-forget IPC message from the renderer to the main process.
* Variadic so events without a payload can be called as `sendMainEvent(event)`.
*
* @param event - The IPC channel/event name to send.
* @param args - Payload arguments for the event; omitted for events with no
* request payload.
*/
export function sendMainEvent<E extends EventType>(event: E, ...args: EventArgs<E>): void {
ipcRenderer.send(event, ...args);
Expand All @@ -13,6 +17,11 @@ export function sendMainEvent<E extends EventType>(event: E, ...args: EventArgs<
/**
* Send an IPC message from the renderer to the main process and await a response.
* The resolved value type is enforced by the event's contract.
*
* @param event - The IPC channel/event name to invoke.
* @param args - Payload arguments for the event; omitted for events with no
* request payload.
* @returns The response value defined by the event's contract.
*/
export async function invokeMainEvent<E extends EventType>(
event: E,
Expand All @@ -30,6 +39,8 @@ export async function invokeMainEvent<E extends EventType>(
/**
* Register a listener for an IPC event sent from the main process to the renderer.
*
* @param event - The IPC channel/event name to listen for.
* @param listener - Callback invoked with the event and its typed request payload.
* @returns A function that removes the listener. Callers that register
* listeners from React effects must return it as the effect cleanup,
* otherwise every remount leaks a permanent ipcRenderer listener.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export interface LoginWithPersonalAccessTokenFormProps {
/**
* Shared personal-access-token login form.
*
* Forge-specific route components (`routes/github`, `routes/gitea`) supply
* their own copy via props so this form stays free of forge conditionals.
* Forge-specific route components under `routes/` supply their own copy via
* props so this form stays free of forge conditionals.
*/
export function LoginWithPersonalAccessTokenForm({
forge,
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/stores/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ export interface NotificationActionFailure {
*/
export interface NotificationActionFailuresState {
/**
* Map of notification ID to the details of its most recent failed action attempt.
* Map of notification failure key (`<accountUUID>:<notificationId>`) to the
* details of its most recent failed action attempt.
*/
failures: Record<string, NotificationActionFailure>;
}
Expand All @@ -258,8 +259,9 @@ export interface NotificationActionFailuresActions {
clearFailures: (notificationKeys: string[]) => void;

/**
* Clears any recorded failures for notification IDs not present in `notificationIds`
* (e.g. once a notification no longer appears in the notifications list).
* Clears any recorded failures for notification keys not present in
* `notificationKeys` (e.g. once a notification no longer appears in the
* notifications list).
*/
pruneFailures: (notificationKeys: string[]) => void;

Expand Down
45 changes: 21 additions & 24 deletions src/renderer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,23 +305,20 @@ export interface Chevron {

export type FilterStateType = 'open' | 'closed' | 'merged' | 'draft' | 'other';

/**
*
* Gitify Notification Types
*
* These types represent the clean, UI-focused notification structure
* used throughout the application.
*
* Raw GitHub API responses are transformed into these types at the API boundary.
*
**/
// ---------------------------------------------------------------------------
// Gitify notification types
//
// These types represent the clean, UI-focused notification structure used
// throughout the application. Raw forge responses are transformed into these
// types at the API boundary.
// ---------------------------------------------------------------------------

/**
* Complete notification type for UI consumption.
* Contains only fields actually used by components.
*/
export interface GitifyNotification {
/** Unique notification ID from GitHub */
/** Unique notification ID from the account's forge */
id: string;
/** Whether the notification is unread */
unread: boolean;
Expand All @@ -343,9 +340,11 @@ export interface GitifyNotification {

/**
* A notification that has been transformed from a forge response but not yet
* formatted for display. The orchestrator pipeline (transform → filter →
* enrich → filter) operates on this type; `formatNotification` is the only
* place that widens it to `GitifyNotification` by populating `display`.
* formatted for display. The orchestrator pipeline (transform → base filter →
* enrich → format → order) operates on this type; `formatNotification` is the
* only place that widens it to `GitifyNotification` by populating `display`.
* Detailed filtering runs later on the formatted notification in the query
* selection layer.
*/
export type RawGitifyNotification = Omit<GitifyNotification, 'display'>;

Expand Down Expand Up @@ -562,16 +561,14 @@ export type GitifyCheckSuiteStatus =
| 'TIMED_OUT'
| 'WAITING';

/**
* Gitify Type Enhancements
*
* These types represent the clean, UI-focused notification structure
* used throughout the application.
*
* Raw GitHub API responses are transformed into these types at the API boundary.
**/

// Stronger typings for string literal attributes
// ---------------------------------------------------------------------------
// Gitify type enhancements
//
// Stronger typings for the string literal attributes used by the notification
// types above, so filters, display logic, and reason labels share one set of
// named values.
// ---------------------------------------------------------------------------

export type Reason =
| 'agent_session_finished'
| 'approval_requested'
Expand Down
11 changes: 7 additions & 4 deletions src/renderer/utils/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ export function determineFailureType(
}

/**
* Handle GraphQL response errors.
* Logs and throws if `errors` array is present and non-empty.
* Log a GraphQL response error and throw.
*
* @param context The context of the GraphQL request
* @param payload The GraphQL response payload
* The caller must pass a GraphQL error payload with a non-empty `errors` array;
* this function does not guard against a missing array.
*
* @param context - The context of the GraphQL request.
* @param payload - The GraphQL error response.
* @throws An `Error` containing the joined GraphQL error messages.
*/
export function handleGraphQLResponseError<TResult>(
context: string,
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/utils/api/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ export const notificationsKeys = {
* accounts always resolves to a distinct cache entry.
*
* @param accountUUIDs - The UUIDs of all accounts being fetched
* @param fetchReadNotifications - Whether to fetch only unread notifications
* @param participating - Whether to fetch only participating notifications
* @param fetchReadNotifications - Whether to also fetch read notifications
* (maps to GitHub's `all` parameter)
* @param participating - Whether to restrict results to participating
* notifications (maps to GitHub's `participating` parameter)
*/
list: (accountUUIDs: AccountUUID[], fetchReadNotifications: boolean, participating: boolean) =>
[...notificationsKeys.all, accountUUIDs, fetchReadNotifications, participating] as const,
Expand Down
14 changes: 8 additions & 6 deletions src/renderer/utils/forges/github/handlers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import type {
type AuthorInput = AuthorFieldsFragment | GitifyNotificationUser | null | undefined;

/**
* Construct the notification subject user based on an order prioritized list of users
* @param users array of users in order or priority
* @returns the subject user
* Construct the notification subject user from users in priority order.
*
* @param users - Users in priority order.
* @returns The first user with a value, or `undefined` when none is present.
*/
export function getNotificationAuthor(users: AuthorInput[]): GitifyNotificationUser | undefined {
for (const user of users) {
Expand All @@ -32,15 +33,16 @@ export function getNotificationAuthor(users: AuthorInput[]): GitifyNotificationU
}

/**
* Construct a GitHub Actions URL for a repository with optional workflow filters.
* Construct a GitHub Actions URL for a repository with workflow filters.
*
* Appends the provided filter strings as a `+`-joined `query` search parameter.
* Note: `%2B` in the URL is un-encoded back to `+` because the GitHub Actions
* UI does not handle encoded plus signs correctly.
*
* @param repositoryURL - The base HTML URL of the repository.
* @param filters - Optional workflow filter strings to append as a query.
* @returns The GitHub Actions URL, with filters applied if provided.
* @param filters - Workflow filter strings to append as a query; pass an empty
* array for none.
* @returns The GitHub Actions URL, with filters applied.
*/
export function actionsURL(repositoryURL: string, filters: string[]): Link {
const url = new URL(repositoryURL);
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/utils/notifications/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export function formatForDisplay(text: string[]): string {
}

/**
* Formats a string to proper case (capitalize the first letter of each word).
* Formats a string to proper case: capitalize the first letter of each word and
* lower-case the remaining characters.
*
* @param text - The string to format.
* @returns The proper-cased string.
Expand Down
13 changes: 7 additions & 6 deletions src/renderer/utils/notifications/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@ import useSettingsStore from '../../stores/useSettingsStore';
import type { GitifyNotification } from '../../types';

/**
* Returns true when settings say to group by date.
* Returns true when the settings store says to group by date.
*
* @param settings - The application settings.
* @returns `true` when `groupBy` is `"DATE"`.
*/
export function isGroupByDate() {
return useSettingsStore.getState().groupBy === 'DATE';
}

/**
* Returns true when settings say to group by repository.
* Returns true when the settings store says to group by repository.
*
* @param settings - The application settings.
* @returns `true` when `groupBy` is `"REPOSITORY"`.
*/
export function isGroupByRepository() {
Expand Down Expand Up @@ -53,13 +51,16 @@ export function groupNotificationsByRepository(
}

return repoGroups;
} /**
}

/**
* Returns a flattened, ordered notifications list according to:
* - repository-first-seen order (when grouped by repository)
* - natural notifications order otherwise
*
* Uses the settings store's `groupBy` value to choose the strategy.
*
* @param notifications - The notifications to flatten and order.
* @param settings - Application settings controlling the groupBy strategy.
* @returns A flat, ordered array of notifications.
*/
export function getFlattenedNotificationsByRepo(
Expand Down
7 changes: 5 additions & 2 deletions src/renderer/utils/notifications/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ function getNotifications(accounts: Account[]) {
* - Transform
* - Base filtering
* - Enrichment
* - Detailed filtering
* - Formatting
* - Ordering
*
* Detailed filtering runs later in the notifications query's `select` step, so
* the cached data stays unfiltered and filter changes apply without a refetch.
*
* @returns A promise that resolves to an array of account notifications.
*/
export async function getAllNotifications(): Promise<AccountNotifications[]> {
Expand Down Expand Up @@ -264,7 +266,8 @@ export function clearEnrichmentCache(): void {
* Assign an order property to each notification to stabilize how they are displayed
* during notification interaction events (mark as read, mark as done, etc.)
*
* @param accountNotifications
* @param accountNotifications - The account notifications whose notifications
* should be ordered.
*/
export function stabilizeNotificationsOrder(accountNotifications: AccountNotifications[]) {
let orderIndex = 0;
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/utils/system/keyboardShortcut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ const MODIFIER_KEYS = new Set(['Control', 'Shift', 'Alt', 'Meta', 'Dead']);

/**
* Convert a keydown event to an Electron accelerator string, or `null` if invalid.
* Requires Command (macOS) or Control (Windows/Linux) plus a non-modifier key.
* Requires a primary modifier (Command or Control) plus a non-modifier key. The
* result always uses the platform-neutral `CommandOrControl` accelerator.
*
* @param event - The keydown event to convert.
* @returns The accelerator string, or `null` when the event is not a valid shortcut.
*/
export function keyboardEventToAccelerator(event: KeyboardEvent): string | null {
if (event.repeat) {
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/utils/system/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { generateNotificationWebUrl } from '../notifications/url';
/**
* Raises a native OS notification.
*
* For a single notification, the message is used as the title (non-Windows) and the
* formatted footer text is used as the body. For multiple notifications, a generic count
* summary is shown instead.
* For a single notification, the repository full name is the title, the subject
* title is the body, and the generated web URL is the click target. For
* multiple notifications, the app name is the title and the body summarizes the
* notification count.
*
* @param notifications - The notifications to surface as a native OS notification.
*/
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/utils/ui/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const ZOOM_STEP = 10 as Percentage;
* Zoom percentage to level. 100% is the recommended zoom level (0).
* If somehow the percentage is not set, it will return 0, the default zoom level.
*
* @param percentage - Zoom percentage (0–150).
* @returns Electron zoom level (-2 to 0.5).
* @param percentage - Zoom percentage (0–120).
* @returns Electron zoom level (-2 to 0.4).
*/
export function zoomPercentageToLevel(percentage: Percentage): number {
if (percentage === undefined) {
Expand All @@ -27,8 +27,8 @@ export function zoomPercentageToLevel(percentage: Percentage): number {
* Zoom level to percentage. 0 is the recommended zoom level (100%).
* If somehow the zoom level is not set, it will return 100, the default zoom percentage.
*
* @param zoom - Electron zoom level (-2 to 0.5).
* @returns Zoom percentage (0–150).
* @param zoom - Electron zoom level (-2 to 0.4).
* @returns Zoom percentage (0–120).
*/
export function zoomLevelToPercentage(zoom: number): Percentage {
if (zoom === undefined) {
Expand Down
3 changes: 3 additions & 0 deletions src/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ type LogLevel = 'info' | 'warn' | 'error';

/**
* Safely coerce an unknown caught value into an Error instance.
*
* @param err - The caught value to coerce.
* @returns An `Error` carrying the value's message.
*/
export function toError(err: unknown): Error {
return err instanceof Error ? err : new Error(String(err));
Expand Down
Loading
Loading