Clean up and fixes
This commit is contained in:
@@ -4,3 +4,10 @@
|
||||
|
||||
export { NavigationProvider, useNavigation } from './useNavigation.js';
|
||||
export { AppProvider, useAppContext, useDialog, useStatus } from './useAppContext.js';
|
||||
export {
|
||||
useInvitations,
|
||||
useInvitation,
|
||||
useInvitationData,
|
||||
useCreateInvitation,
|
||||
useInvitationIds,
|
||||
} from './useInvitations.js';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* App context hook for accessing controllers and app-level functions.
|
||||
* App context hook for accessing AppService and app-level functions.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||||
import type { WalletController } from '../../controllers/wallet-controller.js';
|
||||
import type { InvitationController } from '../../controllers/invitation-controller.js';
|
||||
import { AppService } from '../../services/app.js';
|
||||
import type { AppConfig } from '../../app.js';
|
||||
import type { AppContextType, DialogState } from '../types.js';
|
||||
|
||||
/**
|
||||
@@ -37,27 +37,50 @@ const StatusContext = createContext<StatusContextType | null>(null);
|
||||
*/
|
||||
interface AppProviderProps {
|
||||
children: ReactNode;
|
||||
walletController: WalletController;
|
||||
invitationController: InvitationController;
|
||||
config: AppConfig;
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* App provider component.
|
||||
* Provides controllers, dialog management, and app-level functions to children.
|
||||
* Provides AppService, dialog management, and app-level functions to children.
|
||||
*/
|
||||
export function AppProvider({
|
||||
children,
|
||||
walletController,
|
||||
invitationController,
|
||||
config,
|
||||
onExit,
|
||||
}: AppProviderProps): React.ReactElement {
|
||||
const [appService, setAppService] = useState<AppService | null>(null);
|
||||
const [dialog, setDialog] = useState<DialogState | null>(null);
|
||||
const [status, setStatusState] = useState<string>('Ready');
|
||||
const [isWalletInitialized, setWalletInitialized] = useState(false);
|
||||
|
||||
// Promise resolver for confirm dialogs
|
||||
const [confirmResolver, setConfirmResolver] = useState<((value: boolean) => void) | null>(null);
|
||||
/**
|
||||
* Initialize wallet with seed phrase and create AppService.
|
||||
*/
|
||||
const initializeWallet = useCallback(async (seed: string) => {
|
||||
try {
|
||||
// Create the AppService with the seed
|
||||
const service = await AppService.create(seed, {
|
||||
syncServerUrl: config.syncServerUrl,
|
||||
engineConfig: {
|
||||
databasePath: config.databasePath,
|
||||
databaseFilename: config.databaseFilename,
|
||||
},
|
||||
invitationStoragePath: config.invitationStoragePath,
|
||||
});
|
||||
|
||||
// Start the AppService (loads existing invitations)
|
||||
await service.start();
|
||||
|
||||
// Set the service and mark as initialized
|
||||
setAppService(service);
|
||||
setWalletInitialized(true);
|
||||
} catch (error) {
|
||||
// Re-throw the error so the caller can handle it
|
||||
throw error;
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
/**
|
||||
* Show an error dialog.
|
||||
@@ -88,7 +111,6 @@ export function AppProvider({
|
||||
*/
|
||||
const confirm = useCallback((message: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
setConfirmResolver(() => resolve);
|
||||
setDialog({
|
||||
visible: true,
|
||||
type: 'confirm',
|
||||
@@ -113,15 +135,15 @@ export function AppProvider({
|
||||
}, []);
|
||||
|
||||
const appValue: AppContextType = {
|
||||
walletController,
|
||||
invitationController,
|
||||
appService,
|
||||
initializeWallet,
|
||||
isWalletInitialized,
|
||||
config,
|
||||
showError,
|
||||
showInfo,
|
||||
confirm,
|
||||
exit: onExit,
|
||||
setStatus,
|
||||
isWalletInitialized,
|
||||
setWalletInitialized,
|
||||
};
|
||||
|
||||
const dialogValue: DialogContextType = {
|
||||
|
||||
144
src/tui/hooks/useInvitations.tsx
Normal file
144
src/tui/hooks/useInvitations.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Performance-optimized invitation hooks.
|
||||
* Uses useSyncExternalStore for fine-grained reactivity.
|
||||
*/
|
||||
|
||||
import { useSyncExternalStore, useMemo, useCallback } from 'react';
|
||||
import type { Invitation } from '../../services/invitation.js';
|
||||
import type { XOInvitation } from '@xo-cash/types';
|
||||
import { useAppContext } from './useAppContext.js';
|
||||
|
||||
/**
|
||||
* Get all invitations reactively.
|
||||
* Re-renders when invitations are added or removed.
|
||||
*/
|
||||
export function useInvitations(): Invitation[] {
|
||||
const { appService } = useAppContext();
|
||||
|
||||
const subscribe = useCallback(
|
||||
(callback: () => void) => {
|
||||
if (!appService) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Subscribe to invitation list changes
|
||||
const onAdded = () => callback();
|
||||
const onRemoved = () => callback();
|
||||
|
||||
appService.on('invitation-added', onAdded);
|
||||
appService.on('invitation-removed', onRemoved);
|
||||
|
||||
return () => {
|
||||
appService.off('invitation-added', onAdded);
|
||||
appService.off('invitation-removed', onRemoved);
|
||||
};
|
||||
},
|
||||
[appService]
|
||||
);
|
||||
|
||||
const getSnapshot = useCallback(() => {
|
||||
return appService?.invitations ?? [];
|
||||
}, [appService]);
|
||||
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single invitation by ID with selective re-rendering.
|
||||
* Only re-renders when the specific invitation is updated.
|
||||
*/
|
||||
export function useInvitation(invitationId: string | null): Invitation | null {
|
||||
const { appService } = useAppContext();
|
||||
|
||||
const subscribe = useCallback(
|
||||
(callback: () => void) => {
|
||||
if (!appService || !invitationId) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Find the invitation instance
|
||||
const invitation = appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invitationId
|
||||
);
|
||||
|
||||
if (!invitation) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Subscribe to this specific invitation's updates
|
||||
const onUpdated = () => callback();
|
||||
const onStatusChanged = () => callback();
|
||||
|
||||
invitation.on('invitation-updated', onUpdated);
|
||||
invitation.on('invitation-status-changed', onStatusChanged);
|
||||
|
||||
// Also subscribe to list changes in case the invitation is removed
|
||||
const onRemoved = () => callback();
|
||||
appService.on('invitation-removed', onRemoved);
|
||||
|
||||
return () => {
|
||||
invitation.off('invitation-updated', onUpdated);
|
||||
invitation.off('invitation-status-changed', onStatusChanged);
|
||||
appService.off('invitation-removed', onRemoved);
|
||||
};
|
||||
},
|
||||
[appService, invitationId]
|
||||
);
|
||||
|
||||
const getSnapshot = useCallback(() => {
|
||||
if (!appService || !invitationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invitationId
|
||||
) ?? null
|
||||
);
|
||||
}, [appService, invitationId]);
|
||||
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invitation data with memoization.
|
||||
* Returns stable references to prevent unnecessary re-renders.
|
||||
*/
|
||||
export function useInvitationData(invitationId: string | null): XOInvitation | null {
|
||||
const invitation = useInvitation(invitationId);
|
||||
|
||||
return useMemo(() => {
|
||||
return invitation?.data ?? null;
|
||||
}, [invitation?.data.invitationIdentifier, invitation?.data.commits?.length]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to create invitations.
|
||||
* Returns a memoized function to create invitations.
|
||||
*/
|
||||
export function useCreateInvitation() {
|
||||
const { appService } = useAppContext();
|
||||
|
||||
return useCallback(
|
||||
async (invitation: XOInvitation | string): Promise<Invitation> => {
|
||||
if (!appService) {
|
||||
throw new Error('AppService not initialized');
|
||||
}
|
||||
|
||||
return await appService.createInvitation(invitation);
|
||||
},
|
||||
[appService]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get all invitations with their IDs.
|
||||
* Useful for lists where you only need IDs (prevents re-renders on data changes).
|
||||
*/
|
||||
export function useInvitationIds(): string[] {
|
||||
const invitations = useInvitations();
|
||||
|
||||
return useMemo(() => {
|
||||
return invitations.map((inv) => inv.data.invitationIdentifier);
|
||||
}, [invitations]);
|
||||
}
|
||||
Reference in New Issue
Block a user