Skip to content

Commit

Permalink
feat(@captn/react): add theme and language hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
pixelass committed May 7, 2024
1 parent 0c08194 commit a24446d
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 0 deletions.
60 changes: 60 additions & 0 deletions packages/react/src/__tests__/use-language.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { USER_LANGUAGE_KEY } from "@captn/utils/constants";
import { IPCHandlers } from "@captn/utils/types";
import { renderHook, act } from "@testing-library/react";

import { useLanguage } from "../use-language";

// Mock the IPC event system
const mockOn = jest.fn();
const mockUnsubscribe = jest.fn();
window.ipc = {
on: jest.fn((key, callback) => {
mockOn(key, callback);
return mockUnsubscribe;
}),
} as unknown as IPCHandlers;

describe("useLanguage", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("should register and unregister the language change listener", () => {
const { unmount } = renderHook(() => useLanguage(jest.fn()));

// Check that the ipc.on was called correctly
expect(window.ipc.on).toHaveBeenCalledWith(USER_LANGUAGE_KEY, expect.any(Function));
expect(mockOn).toHaveBeenCalledWith(USER_LANGUAGE_KEY, expect.any(Function));

// Unmount the hook and check if cleanup is performed correctly
unmount();
expect(mockUnsubscribe).toHaveBeenCalled();
});

it("should handle callback updates correctly", () => {
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();

const { rerender } = renderHook(({ callback }) => useLanguage(callback), {
initialProps: { callback: mockCallback1 },
});

// Simulate a language change
const language = "en";
act(() => {
const callback = mockOn.mock.calls[0][1];
callback(language);
});
expect(mockCallback1).toHaveBeenCalledWith(language);

// Update the callback and simulate another language change
rerender({ callback: mockCallback2 });
const newLanguage = "de";
act(() => {
const callback = mockOn.mock.calls[0][1]; // The callback should remain the same reference
callback(newLanguage);
});
expect(mockCallback2).toHaveBeenCalledWith(newLanguage);
expect(mockCallback1).not.toHaveBeenCalledWith(newLanguage);
});
});
64 changes: 64 additions & 0 deletions packages/react/src/__tests__/use-theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { USER_THEME_KEY } from "@captn/utils/constants";
import { IPCHandlers } from "@captn/utils/types";
import { renderHook, act } from "@testing-library/react";

import { useTheme } from "../use-theme";

// Mock the IPC event system
const mockOn = jest.fn();
const mockUnsubscribe = jest.fn();
window.ipc = {
on: jest.fn((key, callback) => {
mockOn(key, callback);
return mockUnsubscribe;
}),
} as unknown as IPCHandlers;

describe("useTheme", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("registers and unregisters the theme change listener", () => {
const { unmount } = renderHook(() => useTheme(jest.fn()));

// Expect that ipc.on was called with USER_THEME_KEY
expect(window.ipc.on).toHaveBeenCalledWith(USER_THEME_KEY, expect.any(Function));

// Unmount the hook to simulate the component unmounting
unmount();

// Expect that the unsubscribe function was called
expect(mockUnsubscribe).toHaveBeenCalled();
});

it("handles theme updates correctly", () => {
const callback = jest.fn();
const { rerender } = renderHook(({ callback }) => useTheme(callback), {
initialProps: { callback },
});

// Simulate an IPC event that changes the theme
act(() => {
const themeCallback = mockOn.mock.calls[0][1];
themeCallback("dark");
});

// Check that the callback was called with the right argument
expect(callback).toHaveBeenCalledWith("dark");

// Change the callback function
const newCallback = jest.fn();
rerender({ callback: newCallback });

// Simulate another IPC event
act(() => {
const themeCallback = mockOn.mock.calls[0][1];
themeCallback("light");
});

// Verify the new callback is called
expect(newCallback).toHaveBeenCalledWith("light");
expect(callback).not.toHaveBeenCalledWith("light");
});
});
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export * from "./types";
export * from "./use-sdk";
export * from "./use-captain-action";
export * from "./use-language";
export * from "./use-object";
export * from "./use-required-downloads";
export * from "./use-resettable-state";
export * from "./use-save-image";
export * from "./use-sdk";
export * from "./use-text-to-image";
export * from "./use-theme";
export * from "./use-unload";
export * from "./use-vector-store";
40 changes: 40 additions & 0 deletions packages/react/src/use-language/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { USER_LANGUAGE_KEY } from "@captn/utils/constants";
import { useEffect, useRef } from "react";

/**
* A custom React hook that subscribes to language changes within the application and executes a callback
* function when the language changes. This hook ensures that the callback function has the most current
* reference through the use of a ref, and it handles cleanup by unsubscribing from the event listener
* when the component unmounts.
*
* The hook listens for changes to the user's language setting through an IPC event identified by
* `USER_LANGUAGE_KEY`. It is designed for use in environments where language settings can change
* dynamically and need to be responded to in real-time, such as in desktop applications built with Electron.
*
* @param {Function} callback - A callback function that is called with the new language string as its
* argument whenever the language setting changes. This function can perform any actions needed in
* response to a language change, such as updating state or UI elements.
*
* Usage:
* This hook is used in components that need to react to changes in the user's language setting. It
* abstracts the complexity of subscribing to and handling IPC events, making it easier to implement
* responsive, i18n-aware components.
*/
export function useLanguage(callback: (language: string) => void) {
const callbackReference = useRef(callback);

useEffect(() => {
callbackReference.current = callback;
}, [callback]);

useEffect(() => {
const unsubscribe = window.ipc?.on(USER_LANGUAGE_KEY, (language: string) => {
callbackReference.current(language);
});
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, []);
}
20 changes: 20 additions & 0 deletions packages/react/src/use-object/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import isEqual from "lodash.isequal";
import { useDebounce } from "use-debounce";

/**
* Custom React hook that debounces an object value. This hook is primarily used to limit the rate at
* which a function that depends on the object's value gets invoked, thereby enhancing performance when
* tracking object changes in a React component's state or props. The debouncing is handled with both
* leading and trailing calls, ensuring the value is updated immediately and also after the specified
* delay if changes continue to occur.
*
* The function utilizes lodash's `isEqual` method to perform a deep comparison between the previous
* and current values, preventing unnecessary updates when the object has not changed meaningfully.
* This is especially useful when dealing with complex objects that may undergo frequent updates.
*
* @template T - A type extending a record of string keys to unknown values, which defines the shape
* of the object being debounced.
* @param {T | null} value_ - The object value to debounce. If `null`, the hook behaves as if it were
* an empty object without properties.
* @param {number} [delay=300] - The number of milliseconds to delay the debounced value. Defaults to 300ms.
* @returns {T | null} - Returns the debounced version of the object, adhering to the same structure
* and types as the input. The returned value updates only when changes are detected post the specified
* delay, or immediately if specified by the debouncing settings.
*/
export function useObject<T extends Record<string, unknown>>(value_?: T | null, delay = 300) {
const [debouncedValue] = useDebounce(value_, delay, {
equalityFn: isEqual,
Expand Down
47 changes: 47 additions & 0 deletions packages/react/src/use-theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { USER_THEME_KEY } from "@captn/utils/constants";
import { useEffect, useRef } from "react";

/**
* A custom React hook that subscribes to theme changes within the application and executes a callback
* function when the theme changes. This hook manages the lifecycle of theme-related event listeners,
* ensuring that the most current callback is used and properly cleaned up when the component unmounts
* or the callback changes.
*
* The hook listens for theme changes through an IPC event defined by `USER_THEME_KEY`. It supports
* themes specified as "light", "dark", or "system", providing flexibility in responding to changes
* in the user's preferred theme settings in a dynamic environment, such as an Electron application.
*
* @param {Function} callback - A callback function that is called with the new theme setting ("light",
* "dark", or "system") as its argument whenever the theme setting changes. This function can perform
* any actions needed in response to a theme change, such as updating state or UI elements to reflect
* the new theme.
*
* Usage:
* This hook is typically used in components that need to react to changes in the user's theme
* settings, ensuring that the application's UI is always in sync with the user's preferences. It
* abstracts the complexity of subscribing to and handling IPC events, making it easier to implement
* theme-aware components.
*/
export function useTheme(callback: (theme: "light" | "dark" | "system") => void) {
const callbackReference = useRef(callback);

useEffect(() => {
callbackReference.current = callback;
}, [callback]);

useEffect(() => {
const unsubscribe = window.ipc?.on(
USER_THEME_KEY,
(theme?: "light" | "dark" | "system") => {
if (theme) {
callbackReference.current(theme);
}
}
);
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, []);
}
22 changes: 22 additions & 0 deletions packages/react/src/use-vector-store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import { useObject } from "../use-object";
* @returns {VectorStoreResponse[]} The search results as an array of VectorStoreResponse objects.
*
* @example
* ```ts
* // In a React component
* const searchResults = useVectorStore('search term', { score_threshold: 0.5 });
* // searchResults will contain an array of search responses where each item has a score above 0.5
* ```
*/
export function useVectorStore(
query: string,
Expand Down Expand Up @@ -65,6 +67,26 @@ export function useVectorStore(
return { data, error };
}

/**
* Custom React hook to interact with the vector store for scrolling through large datasets.
*
* This hook manages the state of scrolling operations within a vector store, sending scroll
* parameters to the vector store and managing the state of the scroll results. It provides a
* continuous way to retrieve large sets of results where the dataset is too large to feasibly
* return at once, using debounced filter settings to minimize unnecessary queries.
*
* @param {ScrollOptions} [{ order_by, with_payload, limit, filter }={}] The scrolling options
* including ordering, payload inclusion, limit, and filtering criteria to tailor the scroll results.
* @returns {{ data: VectorStoreResponse[], error: Error | null }} An object containing an array of
* VectorStoreResponse objects representing the scroll results, and an error object if an error occurs.
*
* @example
* ```ts
* // In a React component
* const { data, error } = useVectorScroll({ order_by: 'date', limit: 10 });
* // data will contain an array of VectorStoreResponses, each representing a chunk of the vector store data
* ```
*/
export function useVectorScroll({ order_by, with_payload, limit, filter }: ScrollOptions = {}) {
const [data, setData] = useState<VectorStoreResponse[]>([]); // State to store the search results
const [error, setError] = useState<Error | null>(null);
Expand Down

0 comments on commit a24446d

Please sign in to comment.