chore: IdentityContext

This commit is contained in:
Danish Arora 2025-09-22 17:49:02 +05:30
parent 1984de3281
commit 6601e7cb5c
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
3 changed files with 137 additions and 110 deletions

View File

@ -0,0 +1,95 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { OpChanClient, type EVerificationStatus } from '@opchan/core';
export interface IdentityRecord {
address: string;
displayName: string;
callSign: string | null;
ensName: string | null;
ordinalDetails: string | null;
verificationStatus: EVerificationStatus;
lastUpdated: number;
}
export interface IdentityContextValue {
getIdentity: (address: string) => IdentityRecord | null;
getDisplayName: (address: string) => string;
}
const IdentityContext = createContext<IdentityContextValue | null>(null);
export const IdentityProvider: React.FC<{ client: OpChanClient; children: React.ReactNode }> = ({ client, children }) => {
const [cache, setCache] = useState<Record<string, IdentityRecord>>({});
useEffect(() => {
let mounted = true;
const seedFromService = async () => {
try {
// Warm snapshot of any already-cached identities
const identities = client.userIdentityService.getAllUserIdentities();
if (!mounted) return;
const next: Record<string, IdentityRecord> = {};
identities.forEach(id => {
next[id.address] = {
address: id.address,
displayName: client.userIdentityService.getDisplayName(id.address),
callSign: id.callSign ?? null,
ensName: id.ensName ?? null,
ordinalDetails: id.ordinalDetails?.ordinalDetails ?? null,
verificationStatus: id.verificationStatus,
lastUpdated: id.lastUpdated,
};
});
setCache(next);
} catch {}
};
seedFromService();
// Subscribe to identity refresh events for live updates
const off = client.userIdentityService.addRefreshListener(async (address: string) => {
try {
const fresh = await client.userIdentityService.getUserIdentity(address);
if (!fresh) return;
setCache(prev => ({
...prev,
[address]: {
address,
displayName: client.userIdentityService.getDisplayName(address),
callSign: fresh.callSign ?? null,
ensName: fresh.ensName ?? null,
ordinalDetails: fresh.ordinalDetails?.ordinalDetails ?? null,
verificationStatus: fresh.verificationStatus,
lastUpdated: fresh.lastUpdated,
},
}));
} catch {}
});
return () => {
try { off && off(); } catch {}
mounted = false;
};
}, [client]);
const getIdentity = useMemo(() => {
return (address: string): IdentityRecord | null => cache[address] ?? null;
}, [cache]);
const getDisplayName = useMemo(() => {
return (address: string): string => client.userIdentityService.getDisplayName(address);
}, [client]);
const value: IdentityContextValue = useMemo(() => ({ getIdentity, getDisplayName }), [getIdentity, getDisplayName]);
return <IdentityContext.Provider value={value}>{children}</IdentityContext.Provider>;
};
export function useIdentity(): IdentityContextValue {
const ctx = useContext(IdentityContext);
if (!ctx) throw new Error('useIdentity must be used within OpChanProvider');
return ctx;
}
export { IdentityContext };

View File

@ -1,7 +1,7 @@
import { EDisplayPreference, EVerificationStatus } from '@opchan/core'; import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
import { useState, useEffect, useMemo } from 'react'; import { useEffect, useState } from 'react';
import { useForumData } from './useForumData';
import { useClient } from '../../contexts/ClientContext'; import { useClient } from '../../contexts/ClientContext';
import { useIdentity } from '../../contexts/IdentityContext';
export interface UserDisplayInfo { export interface UserDisplayInfo {
displayName: string; displayName: string;
@ -19,7 +19,7 @@ export interface UserDisplayInfo {
*/ */
export function useUserDisplay(address: string): UserDisplayInfo { export function useUserDisplay(address: string): UserDisplayInfo {
const client = useClient(); const client = useClient();
const { userVerificationStatus } = useForumData(); const { getIdentity, getDisplayName } = useIdentity();
const [displayInfo, setDisplayInfo] = useState<UserDisplayInfo>({ const [displayInfo, setDisplayInfo] = useState<UserDisplayInfo>({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`, displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
callSign: null, callSign: null,
@ -30,73 +30,17 @@ export function useUserDisplay(address: string): UserDisplayInfo {
isLoading: true, isLoading: true,
error: null, error: null,
}); });
const [refreshTrigger, setRefreshTrigger] = useState(0); // Subscribe via IdentityContext by relying on its internal listener
// Get verification status from forum context for reactive updates
const verificationInfo = useMemo(() => {
return (
userVerificationStatus[address] || {
isVerified: false,
ensName: null,
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
}
);
}, [userVerificationStatus, address]);
// Set up refresh listener for user identity changes
useEffect(() => { useEffect(() => {
if (!client.userIdentityService || !address) return; let cancelled = false;
const prime = async () => {
const unsubscribe = client.userIdentityService.addRefreshListener( if (!address) return;
updatedAddress => {
if (updatedAddress === address) {
setRefreshTrigger(prev => prev + 1);
}
}
);
return unsubscribe;
}, [client.userIdentityService, address]);
useEffect(() => {
const getUserDisplayInfo = async () => {
if (!address) {
setDisplayInfo(prev => ({
...prev,
isLoading: false,
error: 'No address provided',
}));
return;
}
if (!client.userIdentityService) {
console.log(
'useEnhancedUserDisplay: No service available, using fallback',
{ address }
);
setDisplayInfo({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
callSign: null,
ensName: verificationInfo.ensName || null,
ordinalDetails: null,
verificationLevel:
verificationInfo.verificationStatus ||
EVerificationStatus.WALLET_UNCONNECTED,
displayPreference: null,
isLoading: false,
error: null,
});
return;
}
try { try {
const identity = await client.userIdentityService.getUserIdentity(address); const identity = await client.userIdentityService.getUserIdentity(address);
if (cancelled) return;
if (identity) { if (identity) {
const displayName = client.userIdentityService.getDisplayName(address);
setDisplayInfo({ setDisplayInfo({
displayName, displayName: getDisplayName(address),
callSign: identity.callSign || null, callSign: identity.callSign || null,
ensName: identity.ensName || null, ensName: identity.ensName || null,
ordinalDetails: identity.ordinalDetails ordinalDetails: identity.ordinalDetails
@ -108,56 +52,41 @@ export function useUserDisplay(address: string): UserDisplayInfo {
error: null, error: null,
}); });
} else { } else {
setDisplayInfo({ setDisplayInfo(prev => ({
displayName: client.userIdentityService.getDisplayName(address), ...prev,
callSign: null, displayName: getDisplayName(address),
ensName: verificationInfo.ensName || null,
ordinalDetails: null,
verificationLevel:
verificationInfo.verificationStatus ||
EVerificationStatus.WALLET_UNCONNECTED,
displayPreference: null,
isLoading: false, isLoading: false,
error: null, error: null,
}); }));
} }
} catch (error) { } catch (error) {
console.error( setDisplayInfo(prev => ({
'useEnhancedUserDisplay: Failed to get user display info:', ...prev,
error
);
setDisplayInfo({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
callSign: null,
ensName: null,
ordinalDetails: null,
verificationLevel: EVerificationStatus.WALLET_UNCONNECTED,
displayPreference: null,
isLoading: false, isLoading: false,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
}); }));
} }
}; };
prime();
return () => { cancelled = true; };
}, [address, client.userIdentityService, getDisplayName]);
getUserDisplayInfo(); // Reactively reflect IdentityContext cache changes
}, [address, client.userIdentityService, verificationInfo, refreshTrigger]);
// Update display info when verification status changes reactively
useEffect(() => { useEffect(() => {
if (!displayInfo.isLoading && verificationInfo) { if (!address) return;
setDisplayInfo(prev => ({ const id = getIdentity(address);
...prev, if (!id) return;
ensName: verificationInfo.ensName || prev.ensName, setDisplayInfo(prev => ({
verificationLevel: ...prev,
verificationInfo.verificationStatus || prev.verificationLevel, displayName: getDisplayName(address),
})); callSign: id.callSign,
} ensName: id.ensName,
}, [ ordinalDetails: id.ordinalDetails,
verificationInfo.ensName, verificationLevel: id.verificationStatus,
verificationInfo.verificationStatus, isLoading: false,
displayInfo.isLoading, error: null,
verificationInfo, }));
]); }, [address, getIdentity, getDisplayName]);
return displayInfo; return displayInfo;
} }

View File

@ -5,6 +5,7 @@ import { ClientProvider } from '../contexts/ClientContext';
import { AuthProvider } from '../contexts/AuthContext'; import { AuthProvider } from '../contexts/AuthContext';
import { ForumProvider } from '../contexts/ForumContext'; import { ForumProvider } from '../contexts/ForumContext';
import { ModerationProvider } from '../contexts/ModerationContext'; import { ModerationProvider } from '../contexts/ModerationContext';
import { IdentityProvider } from '../contexts/IdentityContext';
export interface OpChanProviderProps { export interface OpChanProviderProps {
ordiscanApiKey: string; ordiscanApiKey: string;
@ -57,11 +58,13 @@ export const OpChanProvider: React.FC<OpChanProviderProps> = ({
if (!isReady || !clientRef.current) return null; if (!isReady || !clientRef.current) return null;
return ( return (
<ClientProvider client={clientRef.current}> <ClientProvider client={clientRef.current}>
<AuthProvider client={clientRef.current}> <IdentityProvider client={clientRef.current}>
<ModerationProvider> <AuthProvider client={clientRef.current}>
<ForumProvider client={clientRef.current}>{children}</ForumProvider> <ModerationProvider>
</ModerationProvider> <ForumProvider client={clientRef.current}>{children}</ForumProvider>
</AuthProvider> </ModerationProvider>
</AuthProvider>
</IdentityProvider>
</ClientProvider> </ClientProvider>
); );
}, [isReady, children]); }, [isReady, children]);