From 9911c9c55e9b518fad0a51c34263a1246c296173 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 18 Sep 2025 22:04:40 +0530 Subject: [PATCH] fix: build and memory leak --- app/src/components/CellList.tsx | 6 +- app/src/components/CreateCellDialog.tsx | 18 +- app/src/components/Header.tsx | 80 ++-- app/src/components/PostList.tsx | 37 +- .../components/ui/call-sign-setup-dialog.tsx | 10 +- app/src/components/ui/delegation-step.tsx | 4 +- app/src/components/ui/verification-step.tsx | 7 +- .../components/ui/waku-health-indicator.tsx | 13 +- app/src/components/ui/wallet-wizard.tsx | 13 +- app/src/hooks/index.ts | 30 +- app/src/pages/FeedPage.tsx | 7 +- app/src/pages/Index.tsx | 6 +- app/src/pages/ProfilePage.tsx | 41 +- packages/react/src/contexts/AuthContext.tsx | 68 +--- packages/react/src/contexts/ClientContext.tsx | 29 ++ packages/react/src/contexts/ForumContext.tsx | 65 ++-- .../react/src/hooks/actions/useAuthActions.ts | 160 -------- .../src/hooks/actions/useForumActions.ts | 368 ------------------ .../react/src/hooks/actions/useUserActions.ts | 171 -------- packages/react/src/hooks/core/useBookmarks.ts | 240 ------------ .../react/src/hooks/core/useUserDisplay.ts | 156 +++++--- packages/react/src/hooks/index.ts | 34 +- packages/react/src/hooks/useForum.ts | 340 ++++++++++++++-- .../react/src/hooks/utilities/useBookmarks.ts | 130 +++++++ .../src/hooks/utilities/useDelegation.ts | 82 ---- .../src/hooks/utilities/useMessageSigning.ts | 44 --- .../src/hooks/utilities/useNetworkStatus.ts | 85 +++- .../react/src/hooks/utilities/usePending.ts | 46 --- .../src/hooks/utilities/useWakuHealth.ts | 112 ------ packages/react/src/index.ts | 1 + .../react/src/provider/OpChanProvider.tsx | 13 +- 31 files changed, 817 insertions(+), 1599 deletions(-) create mode 100644 packages/react/src/contexts/ClientContext.tsx delete mode 100644 packages/react/src/hooks/actions/useAuthActions.ts delete mode 100644 packages/react/src/hooks/actions/useForumActions.ts delete mode 100644 packages/react/src/hooks/actions/useUserActions.ts delete mode 100644 packages/react/src/hooks/core/useBookmarks.ts create mode 100644 packages/react/src/hooks/utilities/useBookmarks.ts delete mode 100644 packages/react/src/hooks/utilities/useDelegation.ts delete mode 100644 packages/react/src/hooks/utilities/useMessageSigning.ts delete mode 100644 packages/react/src/hooks/utilities/usePending.ts delete mode 100644 packages/react/src/hooks/utilities/useWakuHealth.ts diff --git a/app/src/components/CellList.tsx b/app/src/components/CellList.tsx index 02379dc..7e53737 100644 --- a/app/src/components/CellList.tsx +++ b/app/src/components/CellList.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { useForumData, useForumActions, usePermissions } from '@/hooks'; +import { useForumData, usePermissions } from '@/hooks'; import { Layout, MessageSquare, @@ -141,7 +141,7 @@ const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => { const CellList = () => { const { cellsWithStats, isInitialLoading } = useForumData(); - const { refreshData } = useForumActions(); + const { content } = useForum(); const { canCreateCell } = usePermissions(); const [sortOption, setSortOption] = useState('relevance'); @@ -221,7 +221,7 @@ const CellList = () => { @@ -519,24 +518,24 @@ export default function ProfilePage() { - {delegationStatus.isValid || delegationInfo?.isValid + {forum.user.delegation.isValid || delegationInfo?.isValid ? 'Active' : 'Inactive'} {/* Expiry Date */} - {(delegationStatus.expiresAt || + {(forum.user.delegation.expiresAt || currentUser.delegationExpiry) && (
@@ -544,7 +543,7 @@ export default function ProfilePage() {
{( - delegationStatus.expiresAt || + forum.user.delegation.expiresAt || new Date(currentUser.delegationExpiry!) ).toLocaleDateString()}
@@ -559,13 +558,13 @@ export default function ProfilePage() { - {delegationStatus.isValid || + {forum.user.delegation.isValid || currentUser.delegationSignature === 'valid' ? 'Valid' : 'Not signed'} @@ -580,18 +579,18 @@ export default function ProfilePage() {
- {delegationStatus.publicKey || currentUser.browserPubKey - ? `${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(0, 12)}...${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(-8)}` + {forum.user.delegation.publicKey || currentUser.browserPubKey + ? `${(forum.user.delegation.publicKey || currentUser.browserPubKey!).slice(0, 12)}...${(forum.user.delegation.publicKey || currentUser.browserPubKey!).slice(-8)}` : 'Not delegated'}
- {(delegationStatus.publicKey || + {(forum.user.delegation.publicKey || currentUser.browserPubKey) && (
{/* Warning for expired delegation */} - {(!delegationStatus.isValid && - delegationStatus.hasDelegation) || + {(!forum.user.delegation.isValid && + forum.user.delegation.hasDelegation) || (!delegationInfo?.isValid && delegationInfo?.hasDelegation && (
diff --git a/packages/react/src/contexts/AuthContext.tsx b/packages/react/src/contexts/AuthContext.tsx index 86c1a0a..05d4733 100644 --- a/packages/react/src/contexts/AuthContext.tsx +++ b/packages/react/src/contexts/AuthContext.tsx @@ -43,82 +43,42 @@ export const AuthProvider: React.FC<{ // Define verifyOwnership function early so it can be used in useEffect dependencies const verifyOwnership = useCallback(async (): Promise => { - console.log('🔍 verifyOwnership called, currentUser:', currentUser); if (!currentUser) { - console.log('❌ No currentUser, returning false'); return false; } - + try { - console.log('🚀 Starting verification for', currentUser.walletType, 'wallet:', currentUser.address); - - // Actually check for ENS/Ordinal ownership using core services - const { WalletManager } = await import('@opchan/core'); - - let hasOwnership = false; - let ensName: string | undefined; - let ordinalDetails: { ordinalId: string; ordinalDetails: string } | undefined; + // Centralize identity resolution in core service + const identity = await client.userIdentityService.getUserIdentityFresh(currentUser.address); - if (currentUser.walletType === 'ethereum') { - console.log('🔗 Checking ENS ownership for Ethereum address:', currentUser.address); - // Check ENS ownership - const resolvedEns = await WalletManager.resolveENS(currentUser.address); - console.log('📝 ENS resolution result:', resolvedEns); - ensName = resolvedEns || undefined; - hasOwnership = !!ensName; - console.log('✅ ENS hasOwnership:', hasOwnership); - } else if (currentUser.walletType === 'bitcoin') { - console.log('🪙 Checking Ordinal ownership for Bitcoin address:', currentUser.address); - // Check Ordinal ownership - const ordinals = await WalletManager.resolveOperatorOrdinals(currentUser.address); - console.log('📝 Ordinals resolution result:', ordinals); - hasOwnership = !!ordinals && ordinals.length > 0; - if (hasOwnership && ordinals) { - const inscription = ordinals[0]; - const detail = inscription.parent_inscription_id || 'Operator badge present'; - ordinalDetails = { - ordinalId: inscription.inscription_id, - ordinalDetails: String(detail), - }; - } - console.log('✅ Ordinals hasOwnership:', hasOwnership); - } - - const newVerificationStatus = hasOwnership - ? EVerificationStatus.ENS_ORDINAL_VERIFIED - : EVerificationStatus.WALLET_CONNECTED; - - console.log('📊 Setting verification status to:', newVerificationStatus); + const newVerificationStatus = identity?.verificationStatus ?? EVerificationStatus.WALLET_CONNECTED; const updatedUser = { ...currentUser, verificationStatus: newVerificationStatus, - ensDetails: ensName ? { ensName } : undefined, - ordinalDetails, - }; + ensDetails: identity?.ensName ? { ensName: identity.ensName } : undefined, + ordinalDetails: identity?.ordinalDetails, + } as User; setCurrentUser(updatedUser); await localDatabase.storeUser(updatedUser); - - // Also update the user identities cache so UserIdentityService can access ENS details + await localDatabase.upsertUserIdentity(currentUser.address, { - ensName: ensName || undefined, - ordinalDetails, + ensName: identity?.ensName || undefined, + ordinalDetails: identity?.ordinalDetails, verificationStatus: newVerificationStatus, lastUpdated: Date.now(), }); - - console.log('✅ Verification completed successfully, hasOwnership:', hasOwnership); - return hasOwnership; + + return newVerificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED; } catch (error) { console.error('❌ Verification failed:', error); - // Fall back to wallet connected status - const updatedUser = { ...currentUser, verificationStatus: EVerificationStatus.WALLET_CONNECTED }; + const updatedUser = { ...currentUser, verificationStatus: EVerificationStatus.WALLET_CONNECTED } as User; setCurrentUser(updatedUser); await localDatabase.storeUser(updatedUser); return false; } - }, [currentUser]); + }, [client.userIdentityService, currentUser]); // Hydrate user from LocalDatabase on mount useEffect(() => { diff --git a/packages/react/src/contexts/ClientContext.tsx b/packages/react/src/contexts/ClientContext.tsx new file mode 100644 index 0000000..3af191a --- /dev/null +++ b/packages/react/src/contexts/ClientContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext } from 'react'; +import { OpChanClient } from '@opchan/core'; + +export interface ClientContextValue { + client: OpChanClient; +} + +const ClientContext = createContext(null); + +export const ClientProvider: React.FC<{ + client: OpChanClient; + children: React.ReactNode; +}> = ({ client, children }) => { + return ( + + {children} + + ); +}; + +export function useClient(): OpChanClient { + const context = useContext(ClientContext); + if (!context) { + throw new Error('useClient must be used within OpChanProvider'); + } + return context.client; +} + +export { ClientContext }; diff --git a/packages/react/src/contexts/ForumContext.tsx b/packages/react/src/contexts/ForumContext.tsx index 294cdb2..811b47a 100644 --- a/packages/react/src/contexts/ForumContext.tsx +++ b/packages/react/src/contexts/ForumContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { localDatabase, messageManager, ForumActions, OpChanClient, getDataFromCache } from '@opchan/core'; +import { localDatabase, ForumActions, OpChanClient, getDataFromCache } from '@opchan/core'; import { transformCell, transformPost, transformComment } from '@opchan/core'; import { useAuth } from './AuthContext'; import { Cell, Post, Comment, UserVerificationStatus } from '@opchan/core'; @@ -63,47 +63,58 @@ export const ForumProvider: React.FC<{ } }, [updateFromCache]); + // 1) Initial cache hydrate only – decoupled from network subscriptions useEffect(() => { - let unsubHealth: (() => void) | null = null; - let unsubMsg: (() => void) | null = null; - const init = async () => { try { - // Ensure LocalDatabase is opened before hydrating - if (!localDatabase.getSyncState) { - console.log('📥 Opening LocalDatabase for ForumProvider...'); - await localDatabase.open(); - } - await updateFromCache(); setIsInitialLoading(false); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to initialize'); setIsInitialLoading(false); } - - // Check initial health status - const initialHealth = messageManager.currentHealth; - const initialReady = messageManager.isReady; - console.log('🔌 ForumContext initial state:', { initialReady, initialHealth }); - setIsNetworkConnected(!!initialReady); - - unsubHealth = messageManager.onHealthChange((ready: boolean, health: any) => { - console.log('🔌 ForumContext health change:', { ready, health }); - setIsNetworkConnected(!!ready); - }); - - unsubMsg = messageManager.onMessageReceived(async () => { - await updateFromCache(); - }); }; - init(); + }, [updateFromCache]); + + // 2) Network wiring – subscribe once to the client's message manager + useEffect(() => { + let unsubHealth: (() => void) | null = null; + let unsubMsg: (() => void) | null = null; + + // Check initial health status from the provided client to ensure a single core instance + const initialHealth = client.messageManager.currentHealth; + const initialReady = client.messageManager.isReady; + console.log('🔌 ForumContext initial state:', { initialReady, initialHealth }); + setIsNetworkConnected(!!initialReady); + + unsubHealth = client.messageManager.onHealthChange((ready: boolean, health: any) => { + console.log('🔌 ForumContext health change:', { ready, health }); + setIsNetworkConnected(!!ready); + }); + + unsubMsg = client.messageManager.onMessageReceived(async () => { + await updateFromCache(); + }); + return () => { try { unsubHealth && unsubHealth(); } catch {} try { unsubMsg && unsubMsg(); } catch {} }; - }, [updateFromCache]); + }, [client, updateFromCache]); + + // 3) Visibility change: re-check connection immediately when tab becomes active + useEffect(() => { + const handleVisibility = () => { + if (document.visibilityState === 'visible') { + const ready = client.messageManager.isReady; + setIsNetworkConnected(!!ready); + console.debug('🔌 ForumContext visibility check, ready:', ready); + } + }; + document.addEventListener('visibilitychange', handleVisibility); + return () => document.removeEventListener('visibilitychange', handleVisibility); + }, [client]); const ctx: ForumContextValue = useMemo(() => ({ cells, diff --git a/packages/react/src/hooks/actions/useAuthActions.ts b/packages/react/src/hooks/actions/useAuthActions.ts deleted file mode 100644 index 6c0d0c0..0000000 --- a/packages/react/src/hooks/actions/useAuthActions.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { useCallback, useState } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; -import { DelegationDuration, EVerificationStatus } from '@opchan/core'; - -export interface AuthActionStates { - isConnecting: boolean; - isVerifying: boolean; - isDelegating: boolean; - isDisconnecting: boolean; -} - -export interface AuthActions extends AuthActionStates { - connectWallet: () => Promise; - disconnectWallet: () => Promise; - verifyWallet: () => Promise; - delegateKey: (duration: DelegationDuration) => Promise; - clearDelegation: () => Promise; - renewDelegation: (duration: DelegationDuration) => Promise; - checkVerificationStatus: () => Promise; -} - -export function useAuthActions(): AuthActions { - const { - isAuthenticated, - isAuthenticating, - verificationStatus, - connectWallet: baseConnectWallet, - disconnectWallet: baseDisconnectWallet, - verifyOwnership, - delegateKey: baseDelegateKey, - getDelegationStatus, - clearDelegation: baseClearDelegation, - } = useAuth(); - - const [isConnecting, setIsConnecting] = useState(false); - const [isVerifying, setIsVerifying] = useState(false); - const [isDelegating, setIsDelegating] = useState(false); - const [isDisconnecting, setIsDisconnecting] = useState(false); - - const connectWallet = useCallback(async (): Promise => { - if (isAuthenticated) return true; - - setIsConnecting(true); - try { - const result = await baseConnectWallet(); - return result; - } catch (error) { - console.error('Failed to connect wallet:', error); - return false; - } finally { - setIsConnecting(false); - } - }, [isAuthenticated, baseConnectWallet]); - - const disconnectWallet = useCallback(async (): Promise => { - if (!isAuthenticated) return true; - - setIsDisconnecting(true); - try { - baseDisconnectWallet(); - return true; - } catch (error) { - console.error('Failed to disconnect wallet:', error); - return false; - } finally { - setIsDisconnecting(false); - } - }, [isAuthenticated, baseDisconnectWallet]); - - const verifyWallet = useCallback(async (): Promise => { - console.log('🎯 verifyWallet called, isAuthenticated:', isAuthenticated, 'verificationStatus:', verificationStatus); - - if (!isAuthenticated) { - console.log('❌ Not authenticated, returning false'); - return false; - } - - if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { - console.log('✅ Already verified, returning true'); - return true; - } - - console.log('🔄 Setting isVerifying to true and calling verifyOwnership...'); - setIsVerifying(true); - try { - const success = await verifyOwnership(); - console.log('📊 verifyOwnership result:', success); - return success; - } catch (error) { - console.error('❌ Failed to verify wallet:', error); - return false; - } finally { - console.log('🔄 Setting isVerifying to false'); - setIsVerifying(false); - } - }, [isAuthenticated, verificationStatus, verifyOwnership]); - - const delegateKey = useCallback( - async (duration: DelegationDuration): Promise => { - if (!isAuthenticated) return false; - - if (verificationStatus === EVerificationStatus.WALLET_UNCONNECTED) { - return false; - } - - setIsDelegating(true); - try { - const success = await baseDelegateKey(duration); - return success; - } catch (error) { - console.error('Failed to delegate key:', error); - return false; - } finally { - setIsDelegating(false); - } - }, - [isAuthenticated, verificationStatus, baseDelegateKey] - ); - - const clearDelegation = useCallback(async (): Promise => { - const delegationInfo = await getDelegationStatus(); - if (!delegationInfo.isValid) return true; - - try { - await baseClearDelegation(); - return true; - } catch (error) { - console.error('Failed to clear delegation:', error); - return false; - } - }, [getDelegationStatus, baseClearDelegation]); - - const renewDelegation = useCallback( - async (duration: DelegationDuration): Promise => { - const cleared = await clearDelegation(); - if (!cleared) return false; - return delegateKey(duration); - }, - [clearDelegation, delegateKey] - ); - - const checkVerificationStatus = useCallback(async (): Promise => { - if (!isAuthenticated) return; - // This would refresh verification status - simplified for now - }, [isAuthenticated]); - - return { - isConnecting, - isVerifying: isVerifying || isAuthenticating, - isDelegating, - isDisconnecting, - connectWallet, - disconnectWallet, - verifyWallet, - delegateKey, - clearDelegation, - renewDelegation, - checkVerificationStatus, - }; -} diff --git a/packages/react/src/hooks/actions/useForumActions.ts b/packages/react/src/hooks/actions/useForumActions.ts deleted file mode 100644 index 55c78b1..0000000 --- a/packages/react/src/hooks/actions/useForumActions.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { useCallback } from 'react'; -import { useForum } from '../../contexts/ForumContext'; -import { useAuth } from '../../contexts/AuthContext'; -import { usePermissions } from '../core/usePermissions'; -import { Cell, Post, Comment } from '@opchan/core'; - -export interface ForumActionStates { - isCreatingCell: boolean; - isCreatingPost: boolean; - isCreatingComment: boolean; - isVoting: boolean; - isModerating: boolean; -} - -export interface ForumActions extends ForumActionStates { - createCell: ( - name: string, - description: string, - icon?: string - ) => Promise; - - createPost: ( - cellId: string, - title: string, - content: string - ) => Promise; - votePost: (postId: string, isUpvote: boolean) => Promise; - moderatePost: ( - cellId: string, - postId: string, - reason?: string - ) => Promise; - unmoderatePost: ( - cellId: string, - postId: string, - reason?: string - ) => Promise; - - createComment: (postId: string, content: string) => Promise; - voteComment: (commentId: string, isUpvote: boolean) => Promise; - moderateComment: ( - cellId: string, - commentId: string, - reason?: string - ) => Promise; - unmoderateComment: ( - cellId: string, - commentId: string, - reason?: string - ) => Promise; - - moderateUser: ( - cellId: string, - userAddress: string, - reason?: string - ) => Promise; - unmoderateUser: ( - cellId: string, - userAddress: string, - reason?: string - ) => Promise; - - refreshData: () => Promise; -} - -export function useForumActions(): ForumActions { - const { actions, refreshData } = useForum(); - const { currentUser } = useAuth(); - const permissions = usePermissions(); - - const createCell = useCallback( - async ( - name: string, - description: string, - icon?: string - ): Promise => { - if (!permissions.canCreateCell) { - throw new Error(permissions.createCellReason); - } - - if (!name.trim() || !description.trim()) { - throw new Error('Please provide both a name and description for the cell.'); - } - - try { - const result = await actions.createCell( - { - name, - description, - icon, - currentUser, - isAuthenticated: !!currentUser, - }, - async () => {} // updateStateFromCache handled by ForumProvider - ); - return result.data || null; - } catch { - throw new Error('Failed to create cell. Please try again.'); - } - }, - [permissions.canCreateCell, permissions.createCellReason, actions, currentUser] - ); - - const createPost = useCallback( - async ( - cellId: string, - title: string, - content: string - ): Promise => { - if (!permissions.canPost) { - throw new Error('You need to verify Ordinal ownership to create posts.'); - } - - if (!title.trim() || !content.trim()) { - throw new Error('Please provide both a title and content for the post.'); - } - - try { - const result = await actions.createPost( - { - cellId, - title, - content, - currentUser, - isAuthenticated: !!currentUser, - }, - async () => {} - ); - return result.data || null; - } catch { - throw new Error('Failed to create post. Please try again.'); - } - }, - [permissions.canPost, actions, currentUser] - ); - - const createComment = useCallback( - async (postId: string, content: string): Promise => { - if (!permissions.canComment) { - throw new Error(permissions.commentReason); - } - - if (!content.trim()) { - throw new Error('Please provide content for the comment.'); - } - - try { - const result = await actions.createComment( - { - postId, - content, - currentUser, - isAuthenticated: !!currentUser, - }, - async () => {} - ); - return result.data || null; - } catch { - throw new Error('Failed to create comment. Please try again.'); - } - }, - [permissions.canComment, permissions.commentReason, actions, currentUser] - ); - - const votePost = useCallback( - async (postId: string, isUpvote: boolean): Promise => { - if (!permissions.canVote) { - throw new Error(permissions.voteReason); - } - - try { - const result = await actions.vote( - { - targetId: postId, - isUpvote, - currentUser, - isAuthenticated: !!currentUser, - }, - async () => {} - ); - return result.success; - } catch { - throw new Error('Failed to record your vote. Please try again.'); - } - }, - [permissions.canVote, permissions.voteReason, actions, currentUser] - ); - - const voteComment = useCallback( - async (commentId: string, isUpvote: boolean): Promise => { - if (!permissions.canVote) { - throw new Error(permissions.voteReason); - } - - try { - const result = await actions.vote( - { - targetId: commentId, - isUpvote, - currentUser, - isAuthenticated: !!currentUser, - }, - async () => {} - ); - return result.success; - } catch { - throw new Error('Failed to record your vote. Please try again.'); - } - }, - [permissions.canVote, permissions.voteReason, actions, currentUser] - ); - - // For now, return simple implementations - moderation actions would need cell owner checks - const moderatePost = useCallback( - async (cellId: string, postId: string, reason?: string): Promise => { - try { - const result = await actions.moderatePost( - { - cellId, - postId, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - const unmoderatePost = useCallback( - async (cellId: string, postId: string, reason?: string): Promise => { - try { - const result = await actions.unmoderatePost( - { - cellId, - postId, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - const moderateComment = useCallback( - async (cellId: string, commentId: string, reason?: string): Promise => { - try { - const result = await actions.moderateComment( - { - cellId, - commentId, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - const unmoderateComment = useCallback( - async (cellId: string, commentId: string, reason?: string): Promise => { - try { - const result = await actions.unmoderateComment( - { - cellId, - commentId, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - const moderateUser = useCallback( - async (cellId: string, userAddress: string, reason?: string): Promise => { - try { - const result = await actions.moderateUser( - { - cellId, - userAddress, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - const unmoderateUser = useCallback( - async (cellId: string, userAddress: string, reason?: string): Promise => { - try { - const result = await actions.unmoderateUser( - { - cellId, - userAddress, - reason, - currentUser, - isAuthenticated: !!currentUser, - cellOwner: currentUser?.address || '', - }, - async () => {} - ); - return result.success; - } catch { - return false; - } - }, - [actions, currentUser] - ); - - return { - // States - simplified for now - isCreatingCell: false, - isCreatingPost: false, - isCreatingComment: false, - isVoting: false, - isModerating: false, - - // Actions - createCell, - createPost, - createComment, - votePost, - voteComment, - moderatePost, - unmoderatePost, - moderateComment, - unmoderateComment, - moderateUser, - unmoderateUser, - refreshData, - }; -} diff --git a/packages/react/src/hooks/actions/useUserActions.ts b/packages/react/src/hooks/actions/useUserActions.ts deleted file mode 100644 index de83ea5..0000000 --- a/packages/react/src/hooks/actions/useUserActions.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { useCallback, useState } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; -import { usePermissions } from '../core/usePermissions'; -import { EDisplayPreference, localDatabase } from '@opchan/core'; - -export interface UserActionStates { - isUpdatingProfile: boolean; - isUpdatingCallSign: boolean; - isUpdatingDisplayPreference: boolean; -} - -export interface UserActions extends UserActionStates { - updateCallSign: (callSign: string) => Promise; - updateDisplayPreference: (preference: EDisplayPreference) => Promise; - updateProfile: (updates: { - callSign?: string; - displayPreference?: EDisplayPreference; - }) => Promise; - clearCallSign: () => Promise; -} - -export function useUserActions(): UserActions { - const { currentUser } = useAuth(); - const permissions = usePermissions(); - - const [isUpdatingProfile, setIsUpdatingProfile] = useState(false); - const [isUpdatingCallSign, setIsUpdatingCallSign] = useState(false); - const [isUpdatingDisplayPreference, setIsUpdatingDisplayPreference] = - useState(false); - - const updateCallSign = useCallback( - async (callSign: string): Promise => { - if (!permissions.canUpdateProfile) { - throw new Error('You need to connect your wallet to update your profile.'); - } - - if (!currentUser) { - throw new Error('User identity service is not available.'); - } - - if (!callSign.trim()) { - throw new Error('Call sign cannot be empty.'); - } - - if (callSign.length < 3 || callSign.length > 20) { - throw new Error('Call sign must be between 3 and 20 characters.'); - } - - if (!/^[a-zA-Z0-9_-]+$/.test(callSign)) { - throw new Error( - 'Call sign can only contain letters, numbers, underscores, and hyphens.' - ); - } - - setIsUpdatingCallSign(true); - try { - await localDatabase.upsertUserIdentity(currentUser.address, { - callSign, - lastUpdated: Date.now(), - }); - return true; - } catch (error) { - console.error('Failed to update call sign:', error); - throw new Error('An error occurred while updating your call sign.'); - } finally { - setIsUpdatingCallSign(false); - } - }, - [permissions.canUpdateProfile, currentUser] - ); - - const updateDisplayPreference = useCallback( - async (preference: EDisplayPreference): Promise => { - if (!permissions.canUpdateProfile) { - throw new Error('You need to connect your wallet to update your profile.'); - } - - if (!currentUser) { - throw new Error('User identity service is not available.'); - } - - setIsUpdatingDisplayPreference(true); - try { - // Persist to central identity store - await localDatabase.upsertUserIdentity(currentUser.address, { - displayPreference: preference, - lastUpdated: Date.now(), - }); - // Also persist on the lightweight user record if present - await localDatabase.storeUser({ - ...currentUser, - displayPreference: preference, - lastChecked: Date.now(), - }); - return true; - } catch (error) { - console.error('Failed to update display preference:', error); - throw new Error('An error occurred while updating your display preference.'); - } finally { - setIsUpdatingDisplayPreference(false); - } - }, - [permissions.canUpdateProfile, currentUser] - ); - - const updateProfile = useCallback( - async (updates: { - callSign?: string; - displayPreference?: EDisplayPreference; - }): Promise => { - if (!permissions.canUpdateProfile) { - throw new Error('You need to connect your wallet to update your profile.'); - } - - if (!currentUser) { - throw new Error('User identity service is not available.'); - } - - setIsUpdatingProfile(true); - try { - // Write a consolidated identity update to IndexedDB - await localDatabase.upsertUserIdentity(currentUser.address, { - ...(updates.callSign !== undefined ? { callSign: updates.callSign } : {}), - ...(updates.displayPreference !== undefined - ? { displayPreference: updates.displayPreference } - : {}), - lastUpdated: Date.now(), - }); - - // Update user lightweight record for displayPreference if present - if (updates.displayPreference !== undefined) { - await localDatabase.storeUser({ - ...currentUser, - displayPreference: updates.displayPreference, - lastChecked: Date.now(), - } as any); - } - - // Also call granular updaters for validation side-effects - if (updates.callSign !== undefined) { - await updateCallSign(updates.callSign); - } - if (updates.displayPreference !== undefined) { - await updateDisplayPreference(updates.displayPreference); - } - - return true; - } catch (error) { - console.error('Failed to update profile:', error); - throw new Error('An error occurred while updating your profile.'); - } finally { - setIsUpdatingProfile(false); - } - }, - [permissions.canUpdateProfile, currentUser, updateCallSign, updateDisplayPreference] - ); - - const clearCallSign = useCallback(async (): Promise => { - return updateCallSign(''); - }, [updateCallSign]); - - return { - isUpdatingProfile, - isUpdatingCallSign, - isUpdatingDisplayPreference, - updateCallSign, - updateDisplayPreference, - updateProfile, - clearCallSign, - }; -} diff --git a/packages/react/src/hooks/core/useBookmarks.ts b/packages/react/src/hooks/core/useBookmarks.ts deleted file mode 100644 index a92cc89..0000000 --- a/packages/react/src/hooks/core/useBookmarks.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Bookmark, BookmarkType, Post, Comment, BookmarkService } from '@opchan/core'; -import { useAuth } from '../../contexts/AuthContext'; - -export function useBookmarks() { - const { currentUser } = useAuth(); - const [bookmarks, setBookmarks] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const loadBookmarks = useCallback(async () => { - if (!currentUser?.address) return; - - setLoading(true); - setError(null); - try { - const userBookmarks = await BookmarkService.getUserBookmarks( - currentUser.address - ); - setBookmarks(userBookmarks); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load bookmarks'); - } finally { - setLoading(false); - } - }, [currentUser?.address]); - - useEffect(() => { - if (currentUser?.address) { - loadBookmarks(); - } else { - setBookmarks([]); - } - }, [currentUser?.address, loadBookmarks]); - - const bookmarkPost = useCallback( - async (post: Post, cellId?: string) => { - if (!currentUser?.address) return false; - - try { - const isBookmarked = await BookmarkService.togglePostBookmark( - post, - currentUser.address, - cellId - ); - await loadBookmarks(); - return isBookmarked; - } catch (err) { - setError( - err instanceof Error ? err.message : 'Failed to bookmark post' - ); - return false; - } - }, - [currentUser?.address, loadBookmarks] - ); - - const bookmarkComment = useCallback( - async (comment: Comment, postId?: string) => { - if (!currentUser?.address) return false; - - try { - const isBookmarked = await BookmarkService.toggleCommentBookmark( - comment, - currentUser.address, - postId - ); - await loadBookmarks(); - return isBookmarked; - } catch (err) { - setError( - err instanceof Error ? err.message : 'Failed to bookmark comment' - ); - return false; - } - }, - [currentUser?.address, loadBookmarks] - ); - - const removeBookmark = useCallback( - async (bookmarkId: string) => { - try { - const bookmark = BookmarkService.getBookmark(bookmarkId); - if (bookmark) { - await BookmarkService.removeBookmark( - bookmark.type, - bookmark.targetId - ); - await loadBookmarks(); - } - } catch (err) { - setError( - err instanceof Error ? err.message : 'Failed to remove bookmark' - ); - } - }, - [loadBookmarks] - ); - - const isPostBookmarked = useCallback( - (postId: string) => { - if (!currentUser?.address) return false; - return BookmarkService.isPostBookmarked(currentUser.address, postId); - }, - [currentUser?.address] - ); - - const isCommentBookmarked = useCallback( - (commentId: string) => { - if (!currentUser?.address) return false; - return BookmarkService.isCommentBookmarked( - currentUser.address, - commentId - ); - }, - [currentUser?.address] - ); - - const getBookmarksByType = useCallback( - (type: BookmarkType) => { - return bookmarks.filter(bookmark => bookmark.type === type); - }, - [bookmarks] - ); - - const clearAllBookmarks = useCallback(async () => { - if (!currentUser?.address) return; - - try { - await BookmarkService.clearUserBookmarks(currentUser.address); - setBookmarks([]); - } catch (err) { - setError( - err instanceof Error ? err.message : 'Failed to clear bookmarks' - ); - } - }, [currentUser?.address]); - - return { - bookmarks, - loading, - error, - bookmarkPost, - bookmarkComment, - removeBookmark, - isPostBookmarked, - isCommentBookmarked, - getBookmarksByType, - clearAllBookmarks, - refreshBookmarks: loadBookmarks, - }; -} - -export function usePostBookmark(post: Post | null, cellId?: string) { - const { currentUser } = useAuth(); - const [isBookmarked, setIsBookmarked] = useState(false); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (currentUser?.address && post?.id) { - const bookmarked = BookmarkService.isPostBookmarked( - currentUser.address, - post.id - ); - setIsBookmarked(bookmarked); - } else { - setIsBookmarked(false); - } - }, [currentUser?.address, post?.id]); - - const toggleBookmark = useCallback(async () => { - if (!currentUser?.address || !post) return false; - - setLoading(true); - try { - const newBookmarkStatus = await BookmarkService.togglePostBookmark( - post, - currentUser.address, - cellId - ); - setIsBookmarked(newBookmarkStatus); - return newBookmarkStatus; - } catch (err) { - console.error('Failed to toggle post bookmark:', err); - return false; - } finally { - setLoading(false); - } - }, [currentUser?.address, post, cellId]); - - return { - isBookmarked, - loading, - toggleBookmark, - }; -} - -export function useCommentBookmark(comment: Comment, postId?: string) { - const { currentUser } = useAuth(); - const [isBookmarked, setIsBookmarked] = useState(false); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (currentUser?.address) { - const bookmarked = BookmarkService.isCommentBookmarked( - currentUser.address, - comment.id - ); - setIsBookmarked(bookmarked); - } else { - setIsBookmarked(false); - } - }, [currentUser?.address, comment.id]); - - const toggleBookmark = useCallback(async () => { - if (!currentUser?.address) return false; - - setLoading(true); - try { - const newBookmarkStatus = await BookmarkService.toggleCommentBookmark( - comment, - currentUser.address, - postId - ); - setIsBookmarked(newBookmarkStatus); - return newBookmarkStatus; - } catch (err) { - console.error('Failed to toggle comment bookmark:', err); - return false; - } finally { - setLoading(false); - } - }, [currentUser?.address, comment, postId]); - - return { - isBookmarked, - loading, - toggleBookmark, - }; -} diff --git a/packages/react/src/hooks/core/useUserDisplay.ts b/packages/react/src/hooks/core/useUserDisplay.ts index e42918b..29a1c15 100644 --- a/packages/react/src/hooks/core/useUserDisplay.ts +++ b/packages/react/src/hooks/core/useUserDisplay.ts @@ -1,7 +1,7 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; -import { useForum } from '../../contexts/ForumContext'; -import { useAuth } from '../../contexts/AuthContext'; -import { EDisplayPreference, EVerificationStatus, UserIdentityService } from '@opchan/core'; +import { EDisplayPreference, EVerificationStatus } from '@opchan/core'; +import { useState, useEffect, useMemo } from 'react'; +import { useForumData } from './useForumData'; +import { useClient } from '../../contexts/ClientContext'; export interface UserDisplayInfo { displayName: string; @@ -14,9 +14,12 @@ export interface UserDisplayInfo { error: string | null; } +/** + * User display hook with caching and reactive updates + */ export function useUserDisplay(address: string): UserDisplayInfo { - const { userVerificationStatus } = useForum(); - const { currentUser } = useAuth(); + const client = useClient(); + const { userVerificationStatus } = useForumData(); const [displayInfo, setDisplayInfo] = useState({ displayName: `${address.slice(0, 6)}...${address.slice(-4)}`, callSign: null, @@ -28,21 +31,9 @@ export function useUserDisplay(address: string): UserDisplayInfo { error: null, }); const [refreshTrigger, setRefreshTrigger] = useState(0); - const isLoadingRef = useRef(false); - - // Check if this is the current user to get their direct ENS details - const isCurrentUser = currentUser && currentUser.address.toLowerCase() === address.toLowerCase(); + // Get verification status from forum context for reactive updates const verificationInfo = useMemo(() => { - if (isCurrentUser && currentUser) { - // Use current user's direct ENS details - return { - isVerified: currentUser.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED, - ensName: currentUser.ensDetails?.ensName || null, - verificationStatus: currentUser.verificationStatus, - }; - } - return ( userVerificationStatus[address] || { isVerified: false, @@ -50,7 +41,22 @@ export function useUserDisplay(address: string): UserDisplayInfo { verificationStatus: EVerificationStatus.WALLET_UNCONNECTED, } ); - }, [userVerificationStatus, address, isCurrentUser, currentUser]); + }, [userVerificationStatus, address]); + + // Set up refresh listener for user identity changes + useEffect(() => { + if (!client.userIdentityService || !address) return; + + const unsubscribe = client.userIdentityService.addRefreshListener( + updatedAddress => { + if (updatedAddress === address) { + setRefreshTrigger(prev => prev + 1); + } + } + ); + + return unsubscribe; + }, [client.userIdentityService, address]); useEffect(() => { const getUserDisplayInfo = async () => { @@ -63,49 +69,63 @@ export function useUserDisplay(address: string): UserDisplayInfo { return; } - // Prevent multiple simultaneous calls - if (isLoadingRef.current) { - return; - } - - isLoadingRef.current = true; - - try { - // Use UserIdentityService to get proper identity and display name from central store - const { UserIdentityService } = await import('@opchan/core'); - const userIdentityService = new UserIdentityService(null as any); // MessageService not needed for display - - // For current user, ensure their ENS details are in the database first - if (isCurrentUser && currentUser?.ensDetails?.ensName) { - const { localDatabase } = await import('@opchan/core'); - await localDatabase.upsertUserIdentity(address, { - ensName: currentUser.ensDetails.ensName, - verificationStatus: currentUser.verificationStatus, - lastUpdated: Date.now(), - }); - } - - // Get user identity which includes ENS name, callSign, etc. from central store - const identity = await userIdentityService.getUserIdentity(address); - - // Use the service's getDisplayName method which has the correct priority logic - const displayName = userIdentityService.getDisplayName(address); - + if (!client.userIdentityService) { + console.log( + 'useEnhancedUserDisplay: No service available, using fallback', + { address } + ); setDisplayInfo({ - displayName, - callSign: identity?.callSign || null, - ensName: identity?.ensName || verificationInfo.ensName || null, - ordinalDetails: identity?.ordinalDetails ? - `${identity.ordinalDetails.ordinalId}` : null, - verificationLevel: identity?.verificationStatus || + displayName: `${address.slice(0, 6)}...${address.slice(-4)}`, + callSign: null, + ensName: verificationInfo.ensName || null, + ordinalDetails: null, + verificationLevel: verificationInfo.verificationStatus || EVerificationStatus.WALLET_UNCONNECTED, - displayPreference: identity?.displayPreference || null, + displayPreference: null, isLoading: false, error: null, }); + return; + } + + try { + const identity = await client.userIdentityService.getUserIdentity(address); + + if (identity) { + const displayName = client.userIdentityService.getDisplayName(address); + + setDisplayInfo({ + displayName, + callSign: identity.callSign || null, + ensName: identity.ensName || null, + ordinalDetails: identity.ordinalDetails + ? identity.ordinalDetails.ordinalDetails + : null, + verificationLevel: identity.verificationStatus, + displayPreference: identity.displayPreference || null, + isLoading: false, + error: null, + }); + } else { + setDisplayInfo({ + displayName: client.userIdentityService.getDisplayName(address), + callSign: null, + ensName: verificationInfo.ensName || null, + ordinalDetails: null, + verificationLevel: + verificationInfo.verificationStatus || + EVerificationStatus.WALLET_UNCONNECTED, + displayPreference: null, + isLoading: false, + error: null, + }); + } } catch (error) { - console.error('useUserDisplay: Failed to get user display info:', error); + console.error( + 'useEnhancedUserDisplay: Failed to get user display info:', + error + ); setDisplayInfo({ displayName: `${address.slice(0, 6)}...${address.slice(-4)}`, callSign: null, @@ -116,18 +136,28 @@ export function useUserDisplay(address: string): UserDisplayInfo { isLoading: false, error: error instanceof Error ? error.message : 'Unknown error', }); - } finally { - isLoadingRef.current = false; } }; getUserDisplayInfo(); + }, [address, client.userIdentityService, verificationInfo, refreshTrigger]); - // Cleanup function to reset loading ref - return () => { - isLoadingRef.current = false; - }; - }, [address, refreshTrigger, verificationInfo.verificationStatus]); + // Update display info when verification status changes reactively + useEffect(() => { + if (!displayInfo.isLoading && verificationInfo) { + setDisplayInfo(prev => ({ + ...prev, + ensName: verificationInfo.ensName || prev.ensName, + verificationLevel: + verificationInfo.verificationStatus || prev.verificationLevel, + })); + } + }, [ + verificationInfo.ensName, + verificationInfo.verificationStatus, + displayInfo.isLoading, + verificationInfo, + ]); return displayInfo; } diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 3663b33..7b1e0ba 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -1,33 +1,24 @@ // Public hooks surface: aggregator and focused derived hooks -// Aggregator hook +// Aggregator hook (main API) export { useForumApi } from './useForum'; -// Core hooks +// Core hooks (complex logic) export { useForumData } from './core/useForumData'; export { usePermissions } from './core/usePermissions'; export { useUserDisplay } from './core/useUserDisplay'; -export { useBookmarks, usePostBookmark, useCommentBookmark } from './core/useBookmarks'; -// Action hooks -export { useForumActions } from './actions/useForumActions'; -export { useAuthActions } from './actions/useAuthActions'; -export { useUserActions } from './actions/useUserActions'; - -// Derived hooks +// Derived hooks (data slicing utilities) export { useCell } from './derived/useCell'; export { usePost } from './derived/usePost'; export { useCellPosts } from './derived/useCellPosts'; export { usePostComments } from './derived/usePostComments'; export { useUserVotes } from './derived/useUserVotes'; -// Utility hooks -export { useWakuHealth, useWakuReady, useWakuHealthStatus } from './utilities/useWakuHealth'; -export { useDelegation } from './utilities/useDelegation'; -export { useMessageSigning } from './utilities/useMessageSigning'; -export { usePending, usePendingVote } from './utilities/usePending'; +// Utility hooks (remaining complex logic) export { useWallet } from './utilities/useWallet'; export { useNetworkStatus } from './utilities/useNetworkStatus'; export { useForumSelectors } from './utilities/useForumSelectors'; +export { useBookmarks, usePostBookmark, useCommentBookmark } from './utilities/useBookmarks'; // Export types export type { @@ -45,20 +36,7 @@ export type { export type { UserDisplayInfo } from './core/useUserDisplay'; -export type { - ForumActionStates, - ForumActions, -} from './actions/useForumActions'; - -export type { - AuthActionStates, - AuthActions, -} from './actions/useAuthActions'; - -export type { - UserActionStates, - UserActions, -} from './actions/useUserActions'; +// Removed types from deleted action hooks - functionality now in useForumApi export type { CellData } from './derived/useCell'; export type { PostData } from './derived/usePost'; diff --git a/packages/react/src/hooks/useForum.ts b/packages/react/src/hooks/useForum.ts index 61d6737..8f3985a 100644 --- a/packages/react/src/hooks/useForum.ts +++ b/packages/react/src/hooks/useForum.ts @@ -1,22 +1,18 @@ -import { useMemo } from 'react'; +import { useMemo, useCallback, useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useForum as useForumContext } from '../contexts/ForumContext'; +import { useClient } from '../contexts/ClientContext'; import { usePermissions } from './core/usePermissions'; import { useForumData } from './core/useForumData'; import { useNetworkStatus } from './utilities/useNetworkStatus'; -import { useBookmarks } from './core/useBookmarks'; -import { useForumActions } from './actions/useForumActions'; -import { useUserActions } from './actions/useUserActions'; -import { useDelegation } from './utilities/useDelegation'; -import { useMessageSigning } from './utilities/useMessageSigning'; import { useForumSelectors } from './utilities/useForumSelectors'; -import { localDatabase } from '@opchan/core'; import type { Cell, Comment, Post, Bookmark, + User, DelegationDuration, EDisplayPreference, EVerificationStatus, @@ -171,6 +167,7 @@ export interface UseForumApi { } export function useForumApi(): UseForumApi { + const client = useClient(); const { currentUser, verificationStatus, connectWallet, disconnectWallet, verifyOwnership } = useAuth(); const { refreshData, @@ -179,14 +176,126 @@ export function useForumApi(): UseForumApi { const forumData: ForumData = useForumData(); const permissions = usePermissions(); const network = useNetworkStatus(); - const { bookmarks, bookmarkPost, bookmarkComment } = useBookmarks(); - const forumActions = useForumActions(); - const userActions = useUserActions(); - const { delegationStatus, createDelegation, clearDelegation } = useDelegation(); - const { signMessage, verifyMessage } = useMessageSigning(); - const selectors = useForumSelectors(forumData); + // Bookmarks state (moved from useBookmarks) + const [bookmarks, setBookmarks] = useState([]); + + // Delegation functionality (moved from useDelegation) + const [delegationStatus, setDelegationStatus] = useState({ + hasDelegation: false, + isValid: false, + timeRemaining: 0, + expiresAt: undefined as Date | undefined, + publicKey: undefined as string | undefined, + }); + + // Update delegation status + useEffect(() => { + const updateStatus = async () => { + if (currentUser) { + const status = await client.delegation.getStatus(currentUser.address, currentUser.walletType); + setDelegationStatus({ + hasDelegation: !!status, + isValid: status?.isValid || false, + timeRemaining: status?.timeRemaining || 0, + expiresAt: status?.proof?.expiryTimestamp ? new Date(status.proof.expiryTimestamp) : undefined, + publicKey: status?.publicKey, + }); + } + }; + updateStatus(); + }, [client.delegation, currentUser]); + + // Load bookmarks for current user + useEffect(() => { + const load = async () => { + if (!currentUser?.address) { + setBookmarks([]); + return; + } + try { + const list = await client.database.getUserBookmarks(currentUser.address); + setBookmarks(list); + } catch (e) { + console.error('Failed to load bookmarks', e); + } + }; + load(); + }, [client.database, currentUser?.address]); + + const createDelegation = useCallback(async (duration?: DelegationDuration): Promise => { + if (!currentUser) return false; + try { + // Use the delegate method from DelegationManager + const signFunction = async (message: string) => { + // This would need to be implemented based on your wallet signing approach + // For now, return empty string - this needs proper wallet integration + return ''; + }; + + const success = await client.delegation.delegate( + currentUser.address, + currentUser.walletType, + duration, + signFunction + ); + + if (success) { + // Update status after successful delegation + const status = await client.delegation.getStatus(currentUser.address, currentUser.walletType); + setDelegationStatus({ + hasDelegation: !!status, + isValid: status?.isValid || false, + timeRemaining: status?.timeRemaining || 0, + expiresAt: status?.proof?.expiryTimestamp ? new Date(status.proof.expiryTimestamp) : undefined, + publicKey: status?.publicKey, + }); + } + + return success; + } catch (error) { + console.error('Failed to create delegation:', error); + return false; + } + }, [client.delegation, currentUser]); + + const clearDelegation = useCallback(async (): Promise => { + // Clear delegation storage using the database directly + await client.database.clearDelegation(); + setDelegationStatus({ + hasDelegation: false, + isValid: false, + timeRemaining: 0, + expiresAt: undefined, + publicKey: undefined, + }); + }, [client.database]); + + // Message signing functionality (moved from useMessageSigning) + const signMessage = useCallback(async (message: OpchanMessage): Promise => { + if (!currentUser) { + console.warn('No current user. Cannot sign message.'); + return; + } + const status = await client.delegation.getStatus(currentUser.address, currentUser.walletType); + if (!status?.isValid) { + console.warn('No valid delegation found. Cannot sign message.'); + return; + } + await client.messageManager.sendMessage(message); + }, [client.delegation, client.messageManager, currentUser]); + + const verifyMessage = useCallback(async (message: OpchanMessage): Promise => { + try { + // Use message service to verify message + return await client.messageService.verifyMessage(message); + } catch (error) { + console.error('Failed to verify message:', error); + return false; + } + }, [client.messageService]); + type MaybeOrdinal = { ordinalId?: unknown } | null | undefined; const toOrdinal = (value: MaybeOrdinal): { ordinalId: string } | null => { if (value && typeof value === 'object' && typeof (value as { ordinalId?: unknown }).ordinalId === 'string') { @@ -220,7 +329,36 @@ export function useForumApi(): UseForumApi { delegateKey: async (duration?: DelegationDuration) => createDelegation(duration), clearDelegation: async () => { await clearDelegation(); }, updateProfile: async (updates: { callSign?: string; displayPreference?: EDisplayPreference }) => { - return userActions.updateProfile(updates); + if (!currentUser) { + throw new Error('User identity service is not available.'); + } + try { + // Update user identity in database + await client.database.upsertUserIdentity(currentUser.address, { + ...(updates.callSign !== undefined ? { callSign: updates.callSign } : {}), + ...(updates.displayPreference !== undefined ? { displayPreference: updates.displayPreference } : {}), + lastUpdated: Date.now(), + }); + + // Update user lightweight record for displayPreference if present + if (updates.displayPreference !== undefined) { + const updatedUser: User = { + address: currentUser.address, + walletType: currentUser.walletType!, + verificationStatus: currentUser.verificationStatus, + displayPreference: updates.displayPreference, + callSign: currentUser.callSign ?? undefined, + ensDetails: currentUser.ensDetails ?? undefined, + ordinalDetails: (currentUser as unknown as { ordinalDetails?: { ordinalId: string; ordinalDetails: string } | null }).ordinalDetails ?? undefined, + lastChecked: Date.now(), + }; + await client.database.storeUser(updatedUser); + } + return true; + } catch (error) { + console.error('Failed to update profile:', error); + return false; + } }, signMessage, verifyMessage, @@ -241,71 +379,198 @@ export function useForumApi(): UseForumApi { comments: forumData.filteredComments, }, createCell: async (input: { name: string; description: string; icon?: string }) => { - return forumActions.createCell(input.name, input.description, input.icon); + if (!permissions.canCreateCell) { + throw new Error(permissions.createCellReason); + } + if (!input.name.trim() || !input.description.trim()) { + throw new Error('Please provide both a name and description for the cell.'); + } + try { + const result = await client.forumActions.createCell( + { + name: input.name, + description: input.description, + icon: input.icon, + currentUser, + isAuthenticated: !!currentUser, + }, + async () => {} // updateStateFromCache handled by ForumProvider + ); + return result.data || null; + } catch { + throw new Error('Failed to create cell. Please try again.'); + } }, createPost: async (input: { cellId: string; title: string; content: string }) => { - return forumActions.createPost(input.cellId, input.title, input.content); + if (!permissions.canPost) { + throw new Error('You need to verify Ordinal ownership to create posts.'); + } + if (!input.title.trim() || !input.content.trim()) { + throw new Error('Please provide both a title and content for the post.'); + } + try { + const result = await client.forumActions.createPost( + { + cellId: input.cellId, + title: input.title, + content: input.content, + currentUser, + isAuthenticated: !!currentUser, + }, + async () => {} + ); + return result.data || null; + } catch { + throw new Error('Failed to create post. Please try again.'); + } }, createComment: async (input: { postId: string; content: string }) => { - return forumActions.createComment(input.postId, input.content); + if (!permissions.canComment) { + throw new Error('You need to connect your wallet to create comments.'); + } + if (!input.content.trim()) { + throw new Error('Please provide content for the comment.'); + } + try { + const result = await client.forumActions.createComment( + { + postId: input.postId, + content: input.content, + currentUser, + isAuthenticated: !!currentUser, + }, + async () => {} + ); + return result.data || null; + } catch { + throw new Error('Failed to create comment. Please try again.'); + } }, vote: async (input: { targetId: string; isUpvote: boolean }) => { - // useForumActions.vote handles both posts and comments by id + if (!permissions.canVote) { + throw new Error(permissions.voteReason); + } if (!input.targetId) return false; - // Try post vote first, then comment vote if needed try { - const ok = await forumActions.votePost(input.targetId, input.isUpvote); - if (ok) return true; - } catch {} - try { - return await forumActions.voteComment(input.targetId, input.isUpvote); + // Use the unified vote method from ForumActions + const result = await client.forumActions.vote( + { + targetId: input.targetId, + isUpvote: input.isUpvote, + currentUser, + isAuthenticated: !!currentUser, + }, + async () => {} + ); + return result.success; } catch { return false; } }, moderate: { post: async (cellId: string, postId: string, reason?: string) => { - try { return await forumActions.moderatePost(cellId, postId, reason); } catch { return false; } + try { + const result = await client.forumActions.moderatePost( + { cellId, postId, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, unpost: async (cellId: string, postId: string, reason?: string) => { - try { return await forumActions.unmoderatePost(cellId, postId, reason); } catch { return false; } + try { + const result = await client.forumActions.unmoderatePost( + { cellId, postId, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, comment: async (cellId: string, commentId: string, reason?: string) => { - try { return await forumActions.moderateComment(cellId, commentId, reason); } catch { return false; } + try { + const result = await client.forumActions.moderateComment( + { cellId, commentId, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, uncomment: async (cellId: string, commentId: string, reason?: string) => { - try { return await forumActions.unmoderateComment(cellId, commentId, reason); } catch { return false; } + try { + const result = await client.forumActions.unmoderateComment( + { cellId, commentId, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, user: async (cellId: string, userAddress: string, reason?: string) => { - try { return await forumActions.moderateUser(cellId, userAddress, reason); } catch { return false; } + try { + const result = await client.forumActions.moderateUser( + { cellId, userAddress, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, unuser: async (cellId: string, userAddress: string, reason?: string) => { - try { return await forumActions.unmoderateUser(cellId, userAddress, reason); } catch { return false; } + try { + const result = await client.forumActions.unmoderateUser( + { cellId, userAddress, reason, currentUser, isAuthenticated: !!currentUser, cellOwner: currentUser?.address || '' }, + async () => {} + ); + return result.success; + } catch { return false; } }, }, - togglePostBookmark: async (post: Post, cellId?: string) => bookmarkPost(post, cellId), - toggleCommentBookmark: async (comment: Comment, postId?: string) => bookmarkComment(comment, postId), + togglePostBookmark: async (post: Post, cellId?: string) => { + try { + if (!currentUser?.address) return false; + const { BookmarkService } = await import('@opchan/core'); + const added = await BookmarkService.togglePostBookmark(post, currentUser.address, cellId); + // Update local state snapshot from DB cache for immediate UI feedback + const updated = await client.database.getUserBookmarks(currentUser.address); + setBookmarks(updated); + return added; + } catch (e) { + console.error('togglePostBookmark failed', e); + return false; + } + }, + toggleCommentBookmark: async (comment: Comment, postId?: string) => { + try { + if (!currentUser?.address) return false; + const { BookmarkService } = await import('@opchan/core'); + const added = await BookmarkService.toggleCommentBookmark(comment, currentUser.address, postId); + const updated = await client.database.getUserBookmarks(currentUser.address); + setBookmarks(updated); + return added; + } catch (e) { + console.error('toggleCommentBookmark failed', e); + return false; + } + }, refresh: async () => { await refreshData(); }, pending: { isPending: (id?: string) => { - return id ? localDatabase.isPending(id) : false; + return id ? client.database.isPending(id) : false; }, isVotePending: (targetId?: string) => { if (!targetId || !currentUser?.address) return false; - return Object.values(localDatabase.cache.votes).some(v => { + return Object.values(client.database.cache.votes).some(v => { return ( v.targetId === targetId && v.author === currentUser.address && - localDatabase.isPending(v.id) + client.database.isPending(v.id) ); }); }, onChange: (cb: () => void) => { - return localDatabase.onPendingChange(cb); + return client.database.onPendingChange(cb); }, }, }; - }, [forumData, bookmarks, forumActions, bookmarkPost, bookmarkComment, refreshData, currentUser?.address]); + }, [forumData, bookmarks, refreshData, currentUser, permissions, client]); const permissionsSlice = useMemo(() => { return { @@ -361,3 +626,4 @@ export function useForumApi(): UseForumApi { } + diff --git a/packages/react/src/hooks/utilities/useBookmarks.ts b/packages/react/src/hooks/utilities/useBookmarks.ts new file mode 100644 index 0000000..d161aac --- /dev/null +++ b/packages/react/src/hooks/utilities/useBookmarks.ts @@ -0,0 +1,130 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAuth } from '../../contexts/AuthContext'; +import { useClient } from '../../contexts/ClientContext'; +import { Bookmark, BookmarkType, Post, Comment } from '@opchan/core'; +import { BookmarkService } from '@opchan/core'; + +export interface UseBookmarksReturn { + bookmarks: Bookmark[]; + loading: boolean; + error: string | null; + getBookmarksByType: (type: BookmarkType) => Bookmark[]; + removeBookmark: (bookmark: Bookmark) => Promise; + clearAllBookmarks: () => Promise; + togglePostBookmark: (post: Post, cellId?: string) => Promise; + toggleCommentBookmark: (comment: Comment, postId?: string) => Promise; +} + +export function useBookmarks(): UseBookmarksReturn { + const { currentUser } = useAuth(); + const client = useClient(); + const [bookmarks, setBookmarks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + if (!currentUser?.address) { + setBookmarks([]); + setLoading(false); + return; + } + try { + setLoading(true); + const list = await client.database.getUserBookmarks(currentUser.address); + setBookmarks(list); + setError(null); + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load bookmarks'); + } finally { + setLoading(false); + } + }, [client.database, currentUser?.address]); + + useEffect(() => { + refresh(); + }, [refresh]); + + const getBookmarksByType = useCallback( + (type: BookmarkType): Bookmark[] => + bookmarks.filter(b => b.type === type), + [bookmarks] + ); + + const removeBookmark = useCallback( + async (bookmark: Bookmark): Promise => { + await BookmarkService.removeBookmark(bookmark.type, bookmark.targetId); + await refresh(); + }, + [refresh] + ); + + const clearAllBookmarks = useCallback(async (): Promise => { + if (!currentUser?.address) return; + await BookmarkService.clearUserBookmarks(currentUser.address); + await refresh(); + }, [currentUser?.address, refresh]); + + const togglePostBookmark = useCallback( + async (post: Post, cellId?: string): Promise => { + if (!currentUser?.address) return false; + const added = await BookmarkService.togglePostBookmark( + post, + currentUser.address, + cellId + ); + await refresh(); + return added; + }, + [currentUser?.address, refresh] + ); + + const toggleCommentBookmark = useCallback( + async (comment: Comment, postId?: string): Promise => { + if (!currentUser?.address) return false; + const added = await BookmarkService.toggleCommentBookmark( + comment, + currentUser.address, + postId + ); + await refresh(); + return added; + }, + [currentUser?.address, refresh] + ); + + return useMemo( + () => ({ + bookmarks, + loading, + error, + getBookmarksByType, + removeBookmark, + clearAllBookmarks, + togglePostBookmark, + toggleCommentBookmark, + }), + [ + bookmarks, + loading, + error, + getBookmarksByType, + removeBookmark, + clearAllBookmarks, + togglePostBookmark, + toggleCommentBookmark, + ] + ); +} + +// Optional convenience hooks to match historic API surface +export function usePostBookmark() { + const { togglePostBookmark } = useBookmarks(); + return { togglePostBookmark }; +} + +export function useCommentBookmark() { + const { toggleCommentBookmark } = useBookmarks(); + return { toggleCommentBookmark }; +} + + diff --git a/packages/react/src/hooks/utilities/useDelegation.ts b/packages/react/src/hooks/utilities/useDelegation.ts deleted file mode 100644 index c5851ce..0000000 --- a/packages/react/src/hooks/utilities/useDelegation.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback, useState, useEffect } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; -import { DelegationDuration } from '@opchan/core'; - -export const useDelegation = () => { - const { - delegateKey: contextDelegateKey, - getDelegationStatus: contextGetDelegationStatus, - clearDelegation: contextClearDelegation, - isAuthenticating, - } = useAuth(); - - const createDelegation = useCallback( - async (duration?: DelegationDuration): Promise => { - return contextDelegateKey(duration); - }, - [contextDelegateKey] - ); - - const clearDelegation = useCallback(async (): Promise => { - await contextClearDelegation(); - }, [contextClearDelegation]); - - const [delegationStatus, setDelegationStatus] = useState<{ - hasDelegation: boolean; - isValid: boolean; - timeRemaining?: number; - expiresAt?: Date; - publicKey?: string; - address?: string; - walletType?: 'bitcoin' | 'ethereum'; - }>({ - hasDelegation: false, - isValid: false, - }); - - // Load delegation status - useEffect(() => { - contextGetDelegationStatus() - .then(status => { - setDelegationStatus({ - hasDelegation: status.hasDelegation, - isValid: status.isValid, - timeRemaining: status.timeRemaining, - expiresAt: status.timeRemaining - ? new Date(Date.now() + status.timeRemaining) - : undefined, - publicKey: status.publicKey, - address: status.address, - walletType: status.walletType, - }); - }) - .catch(console.error); - }, [contextGetDelegationStatus]); - - const formatTimeRemaining = useCallback((timeMs: number): string => { - const hours = Math.floor(timeMs / (1000 * 60 * 60)); - const minutes = Math.floor((timeMs % (1000 * 60 * 60)) / (1000 * 60)); - - if (hours > 24) { - const days = Math.floor(hours / 24); - return `${days} day${days === 1 ? '' : 's'}`; - } else if (hours > 0) { - return `${hours}h ${minutes}m`; - } else { - return `${minutes}m`; - } - }, []); - - return { - // Delegation status - delegationStatus, - isCreatingDelegation: isAuthenticating, - - // Delegation actions - createDelegation, - clearDelegation, - - // Utilities - formatTimeRemaining, - }; -}; diff --git a/packages/react/src/hooks/utilities/useMessageSigning.ts b/packages/react/src/hooks/utilities/useMessageSigning.ts deleted file mode 100644 index 6b25c65..0000000 --- a/packages/react/src/hooks/utilities/useMessageSigning.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; -import { OpchanMessage } from '@opchan/core'; - -export const useMessageSigning = () => { - const { - signMessage: contextSignMessage, - verifyMessage: contextVerifyMessage, - getDelegationStatus, - } = useAuth(); - - const signMessage = useCallback( - async (message: OpchanMessage): Promise => { - // Check if we have a valid delegation before attempting to sign - const delegationStatus = await getDelegationStatus(); - if (!delegationStatus.isValid) { - console.warn('No valid delegation found. Cannot sign message.'); - return; - } - - await contextSignMessage(message); - }, - [contextSignMessage, getDelegationStatus] - ); - - const verifyMessage = useCallback( - async (message: OpchanMessage): Promise => { - return await contextVerifyMessage(message); - }, - [contextVerifyMessage] - ); - - const canSignMessages = useCallback(async (): Promise => { - const delegationStatus = await getDelegationStatus(); - return delegationStatus.isValid; - }, [getDelegationStatus]); - - return { - // Message signing - signMessage, - verifyMessage, - canSignMessages, - }; -}; diff --git a/packages/react/src/hooks/utilities/useNetworkStatus.ts b/packages/react/src/hooks/utilities/useNetworkStatus.ts index 661c4b2..abcca3d 100644 --- a/packages/react/src/hooks/utilities/useNetworkStatus.ts +++ b/packages/react/src/hooks/utilities/useNetworkStatus.ts @@ -1,6 +1,7 @@ import { useMemo, useState, useEffect } from 'react'; import { useForum } from '../../contexts/ForumContext'; import { useAuth } from '../../contexts/AuthContext'; +import { useClient } from '../../contexts/ClientContext'; import { DelegationFullStatus } from '@opchan/core'; export interface NetworkHealth { @@ -58,11 +59,38 @@ export interface NetworkStatusData { export function useNetworkStatus(): NetworkStatusData { const { isNetworkConnected, isInitialLoading, isRefreshing, error } = useForum(); + const client = useClient(); const { isAuthenticated, currentUser, getDelegationStatus } = useAuth(); const [delegationInfo, setDelegationInfo] = useState(null); + // Track Waku ready state directly from the client to react to changes + const [wakuReady, setWakuReady] = useState( + Boolean((client)?.messageManager?.isReady) + ); + + useEffect(() => { + try { + // Prime from current state so UI updates immediately without navigation + try { + const nowReady = Boolean(client?.messageManager?.isReady); + setWakuReady(nowReady); + console.debug('[useNetworkStatus] primed wakuReady from client', { nowReady }); + } catch {} + + const off = client?.messageManager?.onHealthChange?.( + (ready: boolean) => { + console.debug('[useNetworkStatus] onHealthChange -> wakuReady', { ready }); + setWakuReady(Boolean(ready)); + } + ); + return () => { + try { off && off(); } catch {} + }; + } catch {} + }, [client]); + // Load delegation status useEffect(() => { getDelegationStatus().then(setDelegationInfo).catch(console.error); @@ -72,7 +100,10 @@ export function useNetworkStatus(): NetworkStatusData { const health = useMemo((): NetworkHealth => { const issues: string[] = []; - if (!isNetworkConnected) { + const fallbackConnected = Boolean(wakuReady); + const effectiveConnected = isNetworkConnected || wakuReady; + + if (!effectiveConnected) { issues.push('Waku network disconnected'); } @@ -88,14 +119,26 @@ export function useNetworkStatus(): NetworkStatusData { const lastSync = Date.now(); // This would come from actual sync tracking const syncAge = lastSync ? formatTimeAgo(lastSync) : null; + // Debug: surface the raw inputs to health computation + console.debug('[useNetworkStatus] health', { + forumIsNetworkConnected: isNetworkConnected, + fallbackConnected, + effectiveConnected, + isInitialLoading, + isRefreshing, + error, + delegationValid: delegationInfo?.isValid, + issues, + }); + return { - isConnected: isNetworkConnected, + isConnected: effectiveConnected, isHealthy, lastSync, syncAge, issues, }; - }, [isNetworkConnected, error, isAuthenticated, delegationInfo?.isValid]); + }, [client, isNetworkConnected, wakuReady, error, isAuthenticated, delegationInfo?.isValid]); // Sync status const sync = useMemo((): SyncStatus => { @@ -114,11 +157,12 @@ export function useNetworkStatus(): NetworkStatusData { // Connection status const connections = useMemo((): ConnectionStatus => { + const effectiveConnected = health.isConnected; return { waku: { - connected: isNetworkConnected, - peers: isNetworkConnected ? 3 : 0, // Mock peer count - status: isNetworkConnected ? 'connected' : 'disconnected', + connected: effectiveConnected, + peers: effectiveConnected ? 3 : 0, // Mock peer count + status: effectiveConnected ? 'connected' : 'disconnected', }, wallet: { connected: isAuthenticated, @@ -131,19 +175,28 @@ export function useNetworkStatus(): NetworkStatusData { status: delegationInfo?.isValid ? 'active' : 'expired', }, }; - }, [isNetworkConnected, isAuthenticated, currentUser, delegationInfo]); + }, [health.isConnected, isAuthenticated, currentUser, delegationInfo]); // Status assessment const canRefresh = !isRefreshing && !isInitialLoading; - const canSync = isNetworkConnected && !isRefreshing; + const canSync = health.isConnected && !isRefreshing; const needsAttention = !health.isHealthy || !delegationInfo?.isValid; // Helper methods const getStatusMessage = useMemo(() => { return (): string => { + console.debug('[useNetworkStatus] statusMessage inputs', { + isInitialLoading, + isRefreshing, + isNetworkConnected, + error, + issues: health.issues, + }); if (isInitialLoading) return 'Loading forum data...'; if (isRefreshing) return 'Refreshing data...'; - if (!isNetworkConnected) return 'Network disconnected'; + const fallbackConnected = Boolean(wakuReady); + const effectiveConnected = isNetworkConnected || fallbackConnected; + if (!effectiveConnected) return 'Network disconnected'; if (error) return `Error: ${error}`; if (health.issues.length > 0) return health.issues[0] || 'Unknown issue'; return 'All systems operational'; @@ -152,18 +205,30 @@ export function useNetworkStatus(): NetworkStatusData { isInitialLoading, isRefreshing, isNetworkConnected, + client, + wakuReady, error, health.issues, ]); const getHealthColor = useMemo(() => { return (): 'green' | 'yellow' | 'red' => { - if (!isNetworkConnected || error) return 'red'; + console.debug('[useNetworkStatus] healthColor inputs', { + isNetworkConnected, + error, + issues: health.issues, + delegationValid: delegationInfo?.isValid, + }); + const fallbackConnected = Boolean(wakuReady); + const effectiveConnected = isNetworkConnected || fallbackConnected; + if (!effectiveConnected || error) return 'red'; if (health.issues.length > 0 || !delegationInfo?.isValid) return 'yellow'; return 'green'; }; }, [ isNetworkConnected, + client, + wakuReady, error, health.issues.length, delegationInfo?.isValid, diff --git a/packages/react/src/hooks/utilities/usePending.ts b/packages/react/src/hooks/utilities/usePending.ts deleted file mode 100644 index ca52381..0000000 --- a/packages/react/src/hooks/utilities/usePending.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from 'react'; -import { localDatabase } from '@opchan/core'; -import { useAuth } from '../../contexts/AuthContext'; - -export function usePending(id: string | undefined) { - const [isPending, setIsPending] = useState( - id ? localDatabase.isPending(id) : false - ); - - useEffect(() => { - if (!id) return; - setIsPending(localDatabase.isPending(id)); - const unsubscribe = localDatabase.onPendingChange(() => { - setIsPending(localDatabase.isPending(id)); - }); - return unsubscribe; - }, [id]); - - return { isPending }; -} - -export function usePendingVote(targetId: string | undefined) { - const { currentUser } = useAuth(); - const [isPending, setIsPending] = useState(false); - - useEffect(() => { - const compute = () => { - if (!targetId || !currentUser?.address) return setIsPending(false); - // Find a vote authored by current user for this target that is pending - const pending = Object.values(localDatabase.cache.votes).some(v => { - return ( - v.targetId === targetId && - v.author === currentUser.address && - localDatabase.isPending(v.id) - ); - }); - setIsPending(pending); - }; - - compute(); - const unsub = localDatabase.onPendingChange(compute); - return unsub; - }, [targetId, currentUser?.address]); - - return { isPending }; -} diff --git a/packages/react/src/hooks/utilities/useWakuHealth.ts b/packages/react/src/hooks/utilities/useWakuHealth.ts deleted file mode 100644 index 2359621..0000000 --- a/packages/react/src/hooks/utilities/useWakuHealth.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { HealthStatus } from '@waku/sdk'; -import { messageManager } from '@opchan/core'; - -export interface WakuHealthState { - isReady: boolean; - health: HealthStatus; - isInitialized: boolean; - connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'error'; -} - -export function useWakuHealth(): WakuHealthState { - const [isReady, setIsReady] = useState(false); - const [health, setHealth] = useState(HealthStatus.Unhealthy); - const [isInitialized, setIsInitialized] = useState(false); - const [connectionStatus, setConnectionStatus] = useState< - 'connecting' | 'connected' | 'disconnected' | 'error' - >('connecting'); - - const updateHealth = useCallback( - (ready: boolean, currentHealth: HealthStatus) => { - setIsReady(ready); - setHealth(currentHealth); - - if (ready) { - setConnectionStatus('connected'); - } else if (currentHealth === HealthStatus.Unhealthy) { - setConnectionStatus('disconnected'); - } else { - setConnectionStatus('connecting'); - } - }, - [] - ); - - useEffect(() => { - try { - const currentHealth = messageManager.currentHealth ?? HealthStatus.Unhealthy; - const currentReady = messageManager.isReady; - - setIsInitialized(true); - updateHealth(currentReady, currentHealth); - - const unsubscribe = messageManager.onHealthChange(updateHealth); - return unsubscribe; - } catch (error) { - console.error('Failed to initialize Waku health monitoring:', error); - setConnectionStatus('error'); - setIsInitialized(false); - return undefined; - } - }, [updateHealth]); - - return { - isReady, - health, - isInitialized, - connectionStatus, - }; -} - -export function useWakuReady(): boolean { - const { isReady } = useWakuHealth(); - return isReady; -} - -export function useWakuHealthStatus() { - const { isReady, health, connectionStatus } = useWakuHealth(); - - const getHealthDescription = useCallback(() => { - switch (health) { - case HealthStatus.SufficientlyHealthy: - return 'Network is healthy and fully operational'; - case HealthStatus.MinimallyHealthy: - return 'Network is minimally healthy and functional'; - case HealthStatus.Unhealthy: - return 'Network is unhealthy or disconnected'; - default: - return 'Network status unknown'; - } - }, [health]); - - const getStatusColor = useCallback(() => { - if (isReady) return 'green'; - if (health === HealthStatus.Unhealthy) return 'red'; - return 'yellow'; - }, [isReady, health]); - - const getStatusMessage = useCallback(() => { - switch (connectionStatus) { - case 'connecting': - return 'Connecting to Waku network...'; - case 'connected': - return 'Connected to Waku network'; - case 'disconnected': - return 'Disconnected from Waku network'; - case 'error': - return 'Error connecting to Waku network'; - default: - return 'Unknown connection status'; - } - }, [connectionStatus]); - - return { - isReady, - health, - connectionStatus, - description: getHealthDescription(), - statusColor: getStatusColor(), - statusMessage: getStatusMessage(), - }; -} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 415b014..5d8ef04 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,6 @@ // Providers only (context hooks are internal) export * from './provider/OpChanProvider'; +export { ClientProvider, useClient } from './contexts/ClientContext'; export { AuthProvider, useAuth } from './contexts/AuthContext'; export { ForumProvider, useForum as useForumContext } from './contexts/ForumContext'; export { ModerationProvider, useModeration } from './contexts/ModerationContext'; diff --git a/packages/react/src/provider/OpChanProvider.tsx b/packages/react/src/provider/OpChanProvider.tsx index 0225a05..67509c2 100644 --- a/packages/react/src/provider/OpChanProvider.tsx +++ b/packages/react/src/provider/OpChanProvider.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { OpChanClient } from '@opchan/core'; import { localDatabase, messageManager } from '@opchan/core'; +import { ClientProvider } from '../contexts/ClientContext'; import { AuthProvider } from '../contexts/AuthContext'; import { ForumProvider } from '../contexts/ForumContext'; import { ModerationProvider } from '../contexts/ModerationContext'; @@ -49,11 +50,13 @@ export const OpChanProvider: React.FC = ({ const providers = useMemo(() => { if (!isReady || !clientRef.current) return null; return ( - - - {children} - - + + + + {children} + + + ); }, [isReady, children]);