mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 21:33:09 +00:00
chore: IdentityContext
This commit is contained in:
parent
1984de3281
commit
6601e7cb5c
95
packages/react/src/contexts/IdentityContext.tsx
Normal file
95
packages/react/src/contexts/IdentityContext.tsx
Normal 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 };
|
||||||
|
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user