mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-06 23:03:07 +00:00
fix: build and memory leak
This commit is contained in:
parent
cc29a30bd9
commit
9911c9c55e
@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useForumData, useForumActions, usePermissions } from '@/hooks';
|
import { useForumData, usePermissions } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
@ -141,7 +141,7 @@ const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => {
|
|||||||
|
|
||||||
const CellList = () => {
|
const CellList = () => {
|
||||||
const { cellsWithStats, isInitialLoading } = useForumData();
|
const { cellsWithStats, isInitialLoading } = useForumData();
|
||||||
const { refreshData } = useForumActions();
|
const { content } = useForum();
|
||||||
const { canCreateCell } = usePermissions();
|
const { canCreateCell } = usePermissions();
|
||||||
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ const CellList = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={refreshData}
|
onClick={content.refresh}
|
||||||
disabled={isInitialLoading}
|
disabled={isInitialLoading}
|
||||||
title="Refresh data"
|
title="Refresh data"
|
||||||
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
|
className="px-3 border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { useForumActions, usePermissions } from '@/hooks';
|
import { usePermissions } from '@/hooks';
|
||||||
|
import { useForum } from '@opchan/react';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -57,7 +58,9 @@ export function CreateCellDialog({
|
|||||||
open: externalOpen,
|
open: externalOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: CreateCellDialogProps = {}) {
|
}: CreateCellDialogProps = {}) {
|
||||||
const { createCell, isCreatingCell } = useForumActions();
|
const forum = useForum();
|
||||||
|
const {createCell} = forum.content;
|
||||||
|
const isCreatingCell = false;
|
||||||
const { canCreateCell } = usePermissions();
|
const { canCreateCell } = usePermissions();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [internalOpen, setInternalOpen] = React.useState(false);
|
const [internalOpen, setInternalOpen] = React.useState(false);
|
||||||
@ -84,12 +87,11 @@ export function CreateCellDialog({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ All validation handled in hook
|
const cell = await createCell({
|
||||||
const cell = await createCell(
|
name: values.title,
|
||||||
values.title,
|
description: values.description,
|
||||||
values.description,
|
icon: values.icon,
|
||||||
values.icon
|
});
|
||||||
);
|
|
||||||
if (cell) {
|
if (cell) {
|
||||||
form.reset();
|
form.reset();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useForum } from '@opchan/react';
|
import { useAuth, useForumContext, useNetworkStatus } from '@opchan/react';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
import { localDatabase } from '@opchan/core';
|
import { localDatabase } from '@opchan/core';
|
||||||
// Removed unused import
|
import { DelegationFullStatus } from '@opchan/core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
@ -50,10 +50,16 @@ import { useUserDisplay } from '@/hooks';
|
|||||||
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const forum = useForum();
|
const { currentUser, getDelegationStatus } = useAuth();
|
||||||
const { user, network } = forum;
|
const [delegationInfo, setDelegationInfo] =
|
||||||
|
useState<DelegationFullStatus | null>(null);
|
||||||
|
const network = useNetworkStatus();
|
||||||
|
const wakuHealth = {
|
||||||
|
statusMessage: network.getStatusMessage(),
|
||||||
|
};
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const forumContext = useForumContext();
|
||||||
|
|
||||||
// Use AppKit hooks for multi-chain support
|
// Use AppKit hooks for multi-chain support
|
||||||
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
||||||
@ -64,27 +70,26 @@ const Header = () => {
|
|||||||
const isBitcoinConnected = bitcoinAccount.isConnected;
|
const isBitcoinConnected = bitcoinAccount.isConnected;
|
||||||
const isEthereumConnected = ethereumAccount.isConnected;
|
const isEthereumConnected = ethereumAccount.isConnected;
|
||||||
const isConnected = isBitcoinConnected || isEthereumConnected;
|
const isConnected = isBitcoinConnected || isEthereumConnected;
|
||||||
|
|
||||||
// Use currentUser address (which has ENS details) instead of raw AppKit address
|
// Use currentUser address (which has ENS details) instead of raw AppKit address
|
||||||
const address =
|
const address = currentUser?.address || (isConnected
|
||||||
user.address ||
|
? isBitcoinConnected
|
||||||
(isConnected
|
? bitcoinAccount.address
|
||||||
? isBitcoinConnected
|
: ethereumAccount.address
|
||||||
? bitcoinAccount.address
|
: undefined);
|
||||||
: ethereumAccount.address
|
|
||||||
: undefined);
|
|
||||||
|
|
||||||
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
// ✅ Use UserIdentityService via useUserDisplay hook for centralized display logic
|
// ✅ Use UserIdentityService via useUserDisplay hook for centralized display logic
|
||||||
const { displayName, ensName, verificationLevel } = useUserDisplay(
|
const { displayName, verificationLevel } = useUserDisplay(address || '');
|
||||||
address || ''
|
|
||||||
);
|
// ✅ Removed console.log to prevent spam during development
|
||||||
|
|
||||||
// ✅ Removed console.log to prevent infinite loop spam
|
// Load delegation status
|
||||||
|
React.useEffect(() => {
|
||||||
// Delegation info is available directly from user.delegation
|
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||||
|
}, [getDelegationStatus]);
|
||||||
|
|
||||||
// Use LocalDatabase to persist wizard state across navigation
|
// Use LocalDatabase to persist wizard state across navigation
|
||||||
const getHasShownWizard = async (): Promise<boolean> => {
|
const getHasShownWizard = async (): Promise<boolean> => {
|
||||||
@ -125,7 +130,6 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
const handleDisconnect = async () => {
|
||||||
await user.disconnect();
|
|
||||||
await disconnect();
|
await disconnect();
|
||||||
await setHasShownWizard(false); // Reset so wizard can show again on next connection
|
await setHasShownWizard(false); // Reset so wizard can show again on next connection
|
||||||
toast({
|
toast({
|
||||||
@ -154,18 +158,16 @@ const Header = () => {
|
|||||||
const getStatusIcon = () => {
|
const getStatusIcon = () => {
|
||||||
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
if (!isConnected) return <CircleSlash className="w-4 h-4" />;
|
||||||
|
|
||||||
// Use verification status from user slice
|
// Use verification level from UserIdentityService (central database store)
|
||||||
if (
|
if (
|
||||||
user.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
verificationLevel === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||||
user.delegation.isValid
|
delegationInfo?.isValid
|
||||||
) {
|
) {
|
||||||
return <CheckCircle className="w-4 h-4" />;
|
return <CheckCircle className="w-4 h-4" />;
|
||||||
} else if (
|
} else if (verificationLevel === EVerificationStatus.WALLET_CONNECTED) {
|
||||||
user.verificationStatus === EVerificationStatus.WALLET_CONNECTED
|
|
||||||
) {
|
|
||||||
return <AlertTriangle className="w-4 h-4" />;
|
return <AlertTriangle className="w-4 h-4" />;
|
||||||
} else if (
|
} else if (
|
||||||
user.verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
verificationLevel === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||||
) {
|
) {
|
||||||
return <Key className="w-4 h-4" />;
|
return <Key className="w-4 h-4" />;
|
||||||
} else {
|
} else {
|
||||||
@ -195,13 +197,13 @@ const Header = () => {
|
|||||||
<div className="flex items-center space-x-2 px-3 py-1 bg-cyber-muted/20 rounded-full border border-cyber-muted/30">
|
<div className="flex items-center space-x-2 px-3 py-1 bg-cyber-muted/20 rounded-full border border-cyber-muted/30">
|
||||||
<WakuHealthDot />
|
<WakuHealthDot />
|
||||||
<span className="text-xs font-mono text-cyber-neutral">
|
<span className="text-xs font-mono text-cyber-neutral">
|
||||||
{network.statusMessage}
|
{wakuHealth.statusMessage}
|
||||||
</span>
|
</span>
|
||||||
{network.isConnected && (
|
{forumContext.lastSync && (
|
||||||
<div className="flex items-center space-x-1 text-xs text-cyber-neutral/70">
|
<div className="flex items-center space-x-1 text-xs text-cyber-neutral/70">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{new Date().toLocaleTimeString([], {
|
{new Date(forumContext.lastSync).toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
@ -225,11 +227,11 @@ const Header = () => {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`font-mono text-xs border-0 ${
|
className={`font-mono text-xs border-0 ${
|
||||||
user.verificationStatus ===
|
verificationLevel ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||||
user.delegation.isValid
|
delegationInfo?.isValid
|
||||||
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
||||||
: user.verificationStatus ===
|
: verificationLevel ===
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||||
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
|
? 'bg-orange-500/20 text-orange-400 border-orange-500/30'
|
||||||
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
||||||
@ -237,13 +239,11 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
{getStatusIcon()}
|
{getStatusIcon()}
|
||||||
<span className="ml-1">
|
<span className="ml-1">
|
||||||
{user.verificationStatus ===
|
{verificationLevel === EVerificationStatus.WALLET_UNCONNECTED
|
||||||
EVerificationStatus.WALLET_UNCONNECTED
|
|
||||||
? 'CONNECT'
|
? 'CONNECT'
|
||||||
: user.delegation.isValid
|
: delegationInfo?.isValid
|
||||||
? 'READY'
|
? 'READY'
|
||||||
: user.verificationStatus ===
|
: verificationLevel === EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
|
||||||
? 'EXPIRED'
|
? 'EXPIRED'
|
||||||
: 'DELEGATE'}
|
: 'DELEGATE'}
|
||||||
</span>
|
</span>
|
||||||
@ -475,10 +475,10 @@ const Header = () => {
|
|||||||
<div className="px-4 py-3 border-t border-cyber-muted/20">
|
<div className="px-4 py-3 border-t border-cyber-muted/20">
|
||||||
<div className="flex items-center space-x-2 text-xs text-cyber-neutral">
|
<div className="flex items-center space-x-2 text-xs text-cyber-neutral">
|
||||||
<WakuHealthDot />
|
<WakuHealthDot />
|
||||||
<span>{network.statusMessage}</span>
|
<span>{wakuHealth.statusMessage}</span>
|
||||||
{network.isConnected && (
|
{forumContext.lastSync && (
|
||||||
<span className="ml-auto">
|
<span className="ml-auto">
|
||||||
{new Date().toLocaleTimeString([], {
|
{new Date(forumContext.lastSync).toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,14 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import {
|
import { useCell, useCellPosts, usePermissions, useUserVotes, useAuth, useForumData } from '@/hooks';
|
||||||
useCell,
|
import { useForum } from '@opchan/react';
|
||||||
useCellPosts,
|
|
||||||
useForumActions,
|
|
||||||
usePermissions,
|
|
||||||
useUserVotes,
|
|
||||||
useAuth,
|
|
||||||
useForumData,
|
|
||||||
} from '@/hooks';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@ -40,16 +33,10 @@ const PostList = () => {
|
|||||||
// ✅ Use reactive hooks for data and actions
|
// ✅ Use reactive hooks for data and actions
|
||||||
const cell = useCell(cellId);
|
const cell = useCell(cellId);
|
||||||
const cellPosts = useCellPosts(cellId, { sortBy: 'relevance' });
|
const cellPosts = useCellPosts(cellId, { sortBy: 'relevance' });
|
||||||
const {
|
const forum = useForum();
|
||||||
createPost,
|
const { createPost, vote, moderate, refresh } = forum.content;
|
||||||
votePost,
|
const isCreatingPost = false;
|
||||||
moderatePost,
|
const isVoting = false;
|
||||||
unmoderatePost,
|
|
||||||
moderateUser,
|
|
||||||
refreshData,
|
|
||||||
isCreatingPost,
|
|
||||||
isVoting,
|
|
||||||
} = useForumActions();
|
|
||||||
const { canPost, canVote, canModerate } = usePermissions();
|
const { canPost, canVote, canModerate } = usePermissions();
|
||||||
const userVotes = useUserVotes();
|
const userVotes = useUserVotes();
|
||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
@ -117,7 +104,7 @@ const PostList = () => {
|
|||||||
if (!newPostContent.trim()) return;
|
if (!newPostContent.trim()) return;
|
||||||
|
|
||||||
// ✅ All validation handled in hook
|
// ✅ All validation handled in hook
|
||||||
const post = await createPost(cellId, newPostTitle, newPostContent);
|
const post = await createPost({ cellId, title: newPostTitle, content: newPostContent });
|
||||||
if (post) {
|
if (post) {
|
||||||
setNewPostTitle('');
|
setNewPostTitle('');
|
||||||
setNewPostContent('');
|
setNewPostContent('');
|
||||||
@ -139,7 +126,7 @@ const PostList = () => {
|
|||||||
|
|
||||||
const handleVotePost = async (postId: string, isUpvote: boolean) => {
|
const handleVotePost = async (postId: string, isUpvote: boolean) => {
|
||||||
// ✅ Permission checking handled in hook
|
// ✅ Permission checking handled in hook
|
||||||
await votePost(postId, isUpvote);
|
await vote({ targetId: postId, isUpvote });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPostVoteType = (postId: string) => {
|
const getPostVoteType = (postId: string) => {
|
||||||
@ -154,14 +141,14 @@ const PostList = () => {
|
|||||||
window.prompt('Enter a reason for moderation (optional):') || undefined;
|
window.prompt('Enter a reason for moderation (optional):') || undefined;
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
// ✅ All validation handled in hook
|
// ✅ All validation handled in hook
|
||||||
await moderatePost(cell.id, postId, reason);
|
await moderate.post(cell.id, postId, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnmoderate = async (postId: string) => {
|
const handleUnmoderate = async (postId: string) => {
|
||||||
const reason =
|
const reason =
|
||||||
window.prompt('Optional note for unmoderation?') || undefined;
|
window.prompt('Optional note for unmoderation?') || undefined;
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
await unmoderatePost(cell.id, postId, reason);
|
await moderate.unpost(cell.id, postId, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModerateUser = async (userAddress: string) => {
|
const handleModerateUser = async (userAddress: string) => {
|
||||||
@ -169,7 +156,7 @@ const PostList = () => {
|
|||||||
window.prompt('Reason for moderating this user? (optional)') || undefined;
|
window.prompt('Reason for moderating this user? (optional)') || undefined;
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
// ✅ All validation handled in hook
|
// ✅ All validation handled in hook
|
||||||
await moderateUser(cell.id, userAddress, reason);
|
await moderate.user(cell.id, userAddress, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -196,7 +183,7 @@ const PostList = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={refreshData}
|
onClick={refresh}
|
||||||
disabled={cellPosts.isLoading}
|
disabled={cellPosts.isLoading}
|
||||||
title="Refresh data"
|
title="Refresh data"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Loader2, User, Hash } from 'lucide-react';
|
import { Loader2, User, Hash } from 'lucide-react';
|
||||||
import { useAuth, useUserActions, useForumActions } from '@/hooks';
|
import { useAuth } from '@/hooks';
|
||||||
|
import { useForum } from '@opchan/react';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -55,8 +56,9 @@ export function CallSignSetupDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: CallSignSetupDialogProps = {}) {
|
}: CallSignSetupDialogProps = {}) {
|
||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
const { updateProfile } = useUserActions();
|
const forum = useForum();
|
||||||
const { refreshData } = useForumActions();
|
const { updateProfile } = forum.user;
|
||||||
|
const { refresh } = forum.content;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [internalOpen, setInternalOpen] = useState(false);
|
const [internalOpen, setInternalOpen] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -93,7 +95,7 @@ export function CallSignSetupDialog({
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Refresh forum data to update user display
|
// Refresh forum data to update user display
|
||||||
await refreshData();
|
await refresh();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { useAuth, useAuthActions } from '@opchan/react';
|
import { useAuth } from '@opchan/react';
|
||||||
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
|
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
|
||||||
import { DelegationDuration, DelegationFullStatus } from '@opchan/core';
|
import { DelegationDuration, DelegationFullStatus } from '@opchan/core';
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export function DelegationStep({
|
|||||||
const { currentUser, isAuthenticating, getDelegationStatus } = useAuth();
|
const { currentUser, isAuthenticating, getDelegationStatus } = useAuth();
|
||||||
const [delegationInfo, setDelegationInfo] =
|
const [delegationInfo, setDelegationInfo] =
|
||||||
useState<DelegationFullStatus | null>(null);
|
useState<DelegationFullStatus | null>(null);
|
||||||
const { delegateKey, clearDelegation } = useAuthActions();
|
const { delegateKey, clearDelegation } = useAuth();
|
||||||
|
|
||||||
// Load delegation status
|
// Load delegation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth, useAuthActions } from '@/hooks';
|
import { useAuth } from '@/hooks';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
import { useAppKitAccount } from '@reown/appkit/react';
|
import { useAppKitAccount } from '@reown/appkit/react';
|
||||||
import { OrdinalDetails, EnsDetails } from '@opchan/core';
|
import { OrdinalDetails, EnsDetails } from '@opchan/core';
|
||||||
@ -26,8 +26,7 @@ export function VerificationStep({
|
|||||||
isLoading,
|
isLoading,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
}: VerificationStepProps) {
|
}: VerificationStepProps) {
|
||||||
const { currentUser, verificationStatus, isAuthenticating } = useAuth();
|
const { currentUser, verificationStatus, isAuthenticating, verifyOwnership } = useAuth();
|
||||||
const { verifyWallet } = useAuthActions();
|
|
||||||
|
|
||||||
// Get account info to determine wallet type
|
// Get account info to determine wallet type
|
||||||
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
||||||
@ -99,7 +98,7 @@ export function VerificationStep({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('📞 Calling verifyWallet()...');
|
console.log('📞 Calling verifyWallet()...');
|
||||||
const success = await verifyWallet();
|
const success = await verifyOwnership();
|
||||||
console.log('📊 verifyWallet returned:', success);
|
console.log('📊 verifyWallet returned:', success);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Wifi, WifiOff, AlertTriangle, CheckCircle } from 'lucide-react';
|
import { Wifi, WifiOff, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||||
import { useWakuHealthStatus } from '@opchan/react';
|
import { useNetworkStatus } from '@opchan/react';
|
||||||
import { cn } from '@opchan/core';
|
import { cn } from '@opchan/core';
|
||||||
|
|
||||||
interface WakuHealthIndicatorProps {
|
interface WakuHealthIndicatorProps {
|
||||||
@ -13,8 +13,12 @@ export function WakuHealthIndicator({
|
|||||||
showText = true,
|
showText = true,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: WakuHealthIndicatorProps) {
|
}: WakuHealthIndicatorProps) {
|
||||||
const { connectionStatus, statusColor, statusMessage } =
|
const network = useNetworkStatus();
|
||||||
useWakuHealthStatus();
|
const connectionStatus = network.health.isConnected
|
||||||
|
? 'connected'
|
||||||
|
: 'disconnected';
|
||||||
|
const statusColor = network.getHealthColor();
|
||||||
|
const statusMessage = network.getStatusMessage();
|
||||||
|
|
||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
switch (connectionStatus) {
|
switch (connectionStatus) {
|
||||||
@ -66,7 +70,8 @@ export function WakuHealthIndicator({
|
|||||||
* Useful for compact displays like headers or status bars
|
* Useful for compact displays like headers or status bars
|
||||||
*/
|
*/
|
||||||
export function WakuHealthDot({ className }: { className?: string }) {
|
export function WakuHealthDot({ className }: { className?: string }) {
|
||||||
const { statusColor } = useWakuHealthStatus();
|
const { getHealthColor } = useNetworkStatus();
|
||||||
|
const statusColor = getHealthColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CheckCircle, Circle, Loader2 } from 'lucide-react';
|
import { CheckCircle, Circle, Loader2 } from 'lucide-react';
|
||||||
import { useAuth } from '@/hooks';
|
import { useAuth } from '@/hooks';
|
||||||
import { useDelegation } from '@opchan/react';
|
import { EVerificationStatus, DelegationFullStatus } from '@opchan/core';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
|
||||||
import { WalletConnectionStep } from './wallet-connection-step';
|
import { WalletConnectionStep } from './wallet-connection-step';
|
||||||
import { VerificationStep } from './verification-step';
|
import { VerificationStep } from './verification-step';
|
||||||
import { DelegationStep } from './delegation-step';
|
import { DelegationStep } from './delegation-step';
|
||||||
@ -30,8 +29,12 @@ export function WalletWizard({
|
|||||||
}: WalletWizardProps) {
|
}: WalletWizardProps) {
|
||||||
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
|
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const { isAuthenticated, verificationStatus } = useAuth();
|
const { isAuthenticated, verificationStatus, getDelegationStatus } = useAuth();
|
||||||
const { delegationStatus } = useDelegation();
|
const [delegationStatus, setDelegationStatus] = React.useState<DelegationFullStatus | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
getDelegationStatus().then(setDelegationStatus).catch(console.error);
|
||||||
|
}, [getDelegationStatus]);
|
||||||
|
|
||||||
// Reset wizard when opened - always start at step 1 for simplicity
|
// Reset wizard when opened - always start at step 1 for simplicity
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -65,7 +68,7 @@ export function WalletWizard({
|
|||||||
case 2:
|
case 2:
|
||||||
return verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED;
|
return verificationStatus !== EVerificationStatus.WALLET_UNCONNECTED;
|
||||||
case 3:
|
case 3:
|
||||||
return delegationStatus.isValid;
|
return delegationStatus?.isValid ?? false;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Core hooks - Re-exported from @opchan/react
|
|
||||||
export {
|
export {
|
||||||
useForumData,
|
useForumData,
|
||||||
useAuth,
|
useAuth,
|
||||||
@ -8,7 +8,6 @@ export {
|
|||||||
useCommentBookmark,
|
useCommentBookmark,
|
||||||
} from '@opchan/react';
|
} from '@opchan/react';
|
||||||
|
|
||||||
// Core types - Re-exported from @opchan/react
|
|
||||||
export type {
|
export type {
|
||||||
ForumData,
|
ForumData,
|
||||||
CellWithStats,
|
CellWithStats,
|
||||||
@ -20,11 +19,9 @@ export type {
|
|||||||
UserDisplayInfo,
|
UserDisplayInfo,
|
||||||
} from '@opchan/react';
|
} from '@opchan/react';
|
||||||
|
|
||||||
// Derived hooks - Re-exported from @opchan/react
|
|
||||||
export { useCell, usePost } from '@opchan/react';
|
export { useCell, usePost } from '@opchan/react';
|
||||||
export type { CellData, PostData } from '@opchan/react';
|
export type { CellData, PostData } from '@opchan/react';
|
||||||
|
|
||||||
// Derived hooks - Re-exported from @opchan/react
|
|
||||||
export { useCellPosts, usePostComments, useUserVotes } from '@opchan/react';
|
export { useCellPosts, usePostComments, useUserVotes } from '@opchan/react';
|
||||||
export type {
|
export type {
|
||||||
CellPostsOptions,
|
CellPostsOptions,
|
||||||
@ -34,26 +31,11 @@ export type {
|
|||||||
UserVoteData,
|
UserVoteData,
|
||||||
} from '@opchan/react';
|
} from '@opchan/react';
|
||||||
|
|
||||||
// Action hooks - Re-exported from @opchan/react
|
|
||||||
export { useForumActions, useUserActions, useAuthActions } from '@opchan/react';
|
|
||||||
export type {
|
|
||||||
ForumActionStates,
|
|
||||||
ForumActions,
|
|
||||||
UserActionStates,
|
|
||||||
UserActions,
|
|
||||||
AuthActionStates,
|
|
||||||
AuthActions,
|
|
||||||
} from '@opchan/react';
|
|
||||||
|
|
||||||
// Utility hooks - Re-exported from @opchan/react
|
|
||||||
export {
|
export {
|
||||||
usePermissions,
|
usePermissions,
|
||||||
useNetworkStatus,
|
useNetworkStatus,
|
||||||
useForumSelectors,
|
useForumSelectors,
|
||||||
useDelegation,
|
|
||||||
useMessageSigning,
|
|
||||||
usePending,
|
|
||||||
usePendingVote,
|
|
||||||
useWallet,
|
useWallet,
|
||||||
} from '@opchan/react';
|
} from '@opchan/react';
|
||||||
export type {
|
export type {
|
||||||
@ -64,17 +46,7 @@ export type {
|
|||||||
ForumSelectors,
|
ForumSelectors,
|
||||||
} from '@opchan/react';
|
} from '@opchan/react';
|
||||||
|
|
||||||
// Legacy hooks (for backward compatibility - will be removed)
|
|
||||||
// export { useForum } from '@/contexts/useForum'; // Use useForumData instead
|
|
||||||
// export { useAuth as useLegacyAuth } from '@/contexts/useAuth'; // Use enhanced useAuth instead
|
|
||||||
|
|
||||||
// Re-export existing hooks that don't need changes (UI-specific)
|
|
||||||
export { useIsMobile as useMobile } from './use-mobile';
|
export { useIsMobile as useMobile } from './use-mobile';
|
||||||
export { useToast } from './use-toast';
|
export { useToast } from './use-toast';
|
||||||
|
|
||||||
// Waku health hooks - Re-exported from @opchan/react
|
|
||||||
export {
|
|
||||||
useWakuHealth,
|
|
||||||
useWakuReady,
|
|
||||||
useWakuHealthStatus,
|
|
||||||
} from '@opchan/react';
|
|
||||||
|
|||||||
@ -12,14 +12,15 @@ import {
|
|||||||
import PostCard from '@/components/PostCard';
|
import PostCard from '@/components/PostCard';
|
||||||
import FeedSidebar from '@/components/FeedSidebar';
|
import FeedSidebar from '@/components/FeedSidebar';
|
||||||
import { ModerationToggle } from '@/components/ui/moderation-toggle';
|
import { ModerationToggle } from '@/components/ui/moderation-toggle';
|
||||||
import { useForumData, useAuth, useForumActions } from '@/hooks';
|
import { useForumData, useAuth } from '@/hooks';
|
||||||
|
import { useForum } from '@opchan/react';
|
||||||
import { EVerificationStatus } from '@opchan/core';
|
import { EVerificationStatus } from '@opchan/core';
|
||||||
import { sortPosts, SortOption } from '@opchan/core';
|
import { sortPosts, SortOption } from '@opchan/core';
|
||||||
|
|
||||||
const FeedPage: React.FC = () => {
|
const FeedPage: React.FC = () => {
|
||||||
const forumData = useForumData();
|
const forumData = useForumData();
|
||||||
const { verificationStatus } = useAuth();
|
const { verificationStatus } = useAuth();
|
||||||
const { refreshData } = useForumActions();
|
const { content } = useForum();
|
||||||
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -136,7 +137,7 @@ const FeedPage: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={refreshData}
|
onClick={content.refresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
className="flex items-center space-x-2"
|
className="flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import CellList from '@/components/CellList';
|
import CellList from '@/components/CellList';
|
||||||
import { useForumActions } from '@/hooks';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Wifi } from 'lucide-react';
|
import { Wifi } from 'lucide-react';
|
||||||
import { useForum } from '@opchan/react';
|
import { useForum } from '@opchan/react';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {network} = useForum()
|
const { network, content } = useForum();
|
||||||
const { refreshData } = useForumActions();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
@ -17,7 +15,7 @@ const Index = () => {
|
|||||||
{!network.isConnected && (
|
{!network.isConnected && (
|
||||||
<div className="fixed bottom-4 right-4">
|
<div className="fixed bottom-4 right-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={refreshData}
|
onClick={content.refresh}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="flex items-center gap-2 shadow-lg animate-pulse"
|
className="flex items-center gap-2 shadow-lg animate-pulse"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useUserActions, useForumActions } from '@/hooks';
|
import { useForum } from '@opchan/react';
|
||||||
import { useAuth } from '@opchan/react';
|
import { useAuth } from '@opchan/react';
|
||||||
import { useUserDisplay } from '@/hooks';
|
import { useUserDisplay } from '@/hooks';
|
||||||
import { useDelegation } from '@opchan/react';
|
|
||||||
import { DelegationFullStatus } from '@opchan/core';
|
import { DelegationFullStatus } from '@opchan/core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@ -36,13 +35,13 @@ import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { updateProfile } = useUserActions();
|
const forum = useForum();
|
||||||
const { refreshData } = useForumActions();
|
const { updateProfile } = forum.user;
|
||||||
|
const { refresh } = forum.content;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Get current user from auth context for the address
|
// Get current user from auth context for the address
|
||||||
const { currentUser, getDelegationStatus } = useAuth();
|
const { currentUser, getDelegationStatus } = useAuth();
|
||||||
const { delegationStatus } = useDelegation();
|
|
||||||
const [delegationInfo, setDelegationInfo] =
|
const [delegationInfo, setDelegationInfo] =
|
||||||
useState<DelegationFullStatus | null>(null);
|
useState<DelegationFullStatus | null>(null);
|
||||||
const address = currentUser?.address;
|
const address = currentUser?.address;
|
||||||
@ -171,7 +170,7 @@ export default function ProfilePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await refreshData();
|
await refresh();
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
toast({
|
toast({
|
||||||
title: 'Profile Updated',
|
title: 'Profile Updated',
|
||||||
@ -494,7 +493,7 @@ export default function ProfilePage() {
|
|||||||
<Shield className="h-5 w-5 text-cyber-accent" />
|
<Shield className="h-5 w-5 text-cyber-accent" />
|
||||||
Security
|
Security
|
||||||
</div>
|
</div>
|
||||||
{(delegationStatus.hasDelegation ||
|
{(forum.user.delegation.hasDelegation ||
|
||||||
delegationInfo?.hasDelegation) && (
|
delegationInfo?.hasDelegation) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -503,7 +502,7 @@ export default function ProfilePage() {
|
|||||||
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
|
className="border-cyber-muted/30 text-cyber-neutral hover:bg-cyber-muted/30"
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
{delegationStatus.isValid || delegationInfo?.isValid
|
{forum.user.delegation.isValid || delegationInfo?.isValid
|
||||||
? 'Renew'
|
? 'Renew'
|
||||||
: 'Setup'}
|
: 'Setup'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -519,24 +518,24 @@ export default function ProfilePage() {
|
|||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
delegationStatus.isValid || delegationInfo?.isValid
|
forum.user.delegation.isValid || delegationInfo?.isValid
|
||||||
? 'default'
|
? 'default'
|
||||||
: 'secondary'
|
: 'secondary'
|
||||||
}
|
}
|
||||||
className={
|
className={
|
||||||
delegationStatus.isValid || delegationInfo?.isValid
|
forum.user.delegation.isValid || delegationInfo?.isValid
|
||||||
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
? 'bg-green-500/20 text-green-400 border-green-500/30'
|
||||||
: 'bg-red-500/20 text-red-400 border-red-500/30'
|
: 'bg-red-500/20 text-red-400 border-red-500/30'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delegationStatus.isValid || delegationInfo?.isValid
|
{forum.user.delegation.isValid || delegationInfo?.isValid
|
||||||
? 'Active'
|
? 'Active'
|
||||||
: 'Inactive'}
|
: 'Inactive'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expiry Date */}
|
{/* Expiry Date */}
|
||||||
{(delegationStatus.expiresAt ||
|
{(forum.user.delegation.expiresAt ||
|
||||||
currentUser.delegationExpiry) && (
|
currentUser.delegationExpiry) && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-cyber-neutral">
|
<span className="text-xs text-cyber-neutral">
|
||||||
@ -544,7 +543,7 @@ export default function ProfilePage() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="text-sm font-mono text-cyber-light">
|
<div className="text-sm font-mono text-cyber-light">
|
||||||
{(
|
{(
|
||||||
delegationStatus.expiresAt ||
|
forum.user.delegation.expiresAt ||
|
||||||
new Date(currentUser.delegationExpiry!)
|
new Date(currentUser.delegationExpiry!)
|
||||||
).toLocaleDateString()}
|
).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
@ -559,13 +558,13 @@ export default function ProfilePage() {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={
|
className={
|
||||||
delegationStatus.isValid ||
|
forum.user.delegation.isValid ||
|
||||||
currentUser.delegationSignature === 'valid'
|
currentUser.delegationSignature === 'valid'
|
||||||
? 'text-green-400 border-green-500/30 bg-green-500/10'
|
? 'text-green-400 border-green-500/30 bg-green-500/10'
|
||||||
: 'text-red-400 border-red-500/30 bg-red-500/10'
|
: 'text-red-400 border-red-500/30 bg-red-500/10'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delegationStatus.isValid ||
|
{forum.user.delegation.isValid ||
|
||||||
currentUser.delegationSignature === 'valid'
|
currentUser.delegationSignature === 'valid'
|
||||||
? 'Valid'
|
? 'Valid'
|
||||||
: 'Not signed'}
|
: 'Not signed'}
|
||||||
@ -580,18 +579,18 @@ export default function ProfilePage() {
|
|||||||
</Label>
|
</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 font-mono text-xs bg-cyber-dark/50 border border-cyber-muted/30 px-2 py-1 rounded text-cyber-light">
|
<div className="flex-1 font-mono text-xs bg-cyber-dark/50 border border-cyber-muted/30 px-2 py-1 rounded text-cyber-light">
|
||||||
{delegationStatus.publicKey || currentUser.browserPubKey
|
{forum.user.delegation.publicKey || currentUser.browserPubKey
|
||||||
? `${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(0, 12)}...${(delegationStatus.publicKey || currentUser.browserPubKey!).slice(-8)}`
|
? `${(forum.user.delegation.publicKey || currentUser.browserPubKey!).slice(0, 12)}...${(forum.user.delegation.publicKey || currentUser.browserPubKey!).slice(-8)}`
|
||||||
: 'Not delegated'}
|
: 'Not delegated'}
|
||||||
</div>
|
</div>
|
||||||
{(delegationStatus.publicKey ||
|
{(forum.user.delegation.publicKey ||
|
||||||
currentUser.browserPubKey) && (
|
currentUser.browserPubKey) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
delegationStatus.publicKey ||
|
forum.user.delegation.publicKey ||
|
||||||
currentUser.browserPubKey!,
|
currentUser.browserPubKey!,
|
||||||
'Public Key'
|
'Public Key'
|
||||||
)
|
)
|
||||||
@ -605,8 +604,8 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Warning for expired delegation */}
|
{/* Warning for expired delegation */}
|
||||||
{(!delegationStatus.isValid &&
|
{(!forum.user.delegation.isValid &&
|
||||||
delegationStatus.hasDelegation) ||
|
forum.user.delegation.hasDelegation) ||
|
||||||
(!delegationInfo?.isValid &&
|
(!delegationInfo?.isValid &&
|
||||||
delegationInfo?.hasDelegation && (
|
delegationInfo?.hasDelegation && (
|
||||||
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md">
|
<div className="p-3 bg-orange-500/10 border border-orange-500/30 rounded-md">
|
||||||
|
|||||||
@ -43,82 +43,42 @@ export const AuthProvider: React.FC<{
|
|||||||
|
|
||||||
// Define verifyOwnership function early so it can be used in useEffect dependencies
|
// Define verifyOwnership function early so it can be used in useEffect dependencies
|
||||||
const verifyOwnership = useCallback(async (): Promise<boolean> => {
|
const verifyOwnership = useCallback(async (): Promise<boolean> => {
|
||||||
console.log('🔍 verifyOwnership called, currentUser:', currentUser);
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
console.log('❌ No currentUser, returning false');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🚀 Starting verification for', currentUser.walletType, 'wallet:', currentUser.address);
|
// Centralize identity resolution in core service
|
||||||
|
const identity = await client.userIdentityService.getUserIdentityFresh(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;
|
|
||||||
|
|
||||||
if (currentUser.walletType === 'ethereum') {
|
const newVerificationStatus = identity?.verificationStatus ?? EVerificationStatus.WALLET_CONNECTED;
|
||||||
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 updatedUser = {
|
const updatedUser = {
|
||||||
...currentUser,
|
...currentUser,
|
||||||
verificationStatus: newVerificationStatus,
|
verificationStatus: newVerificationStatus,
|
||||||
ensDetails: ensName ? { ensName } : undefined,
|
ensDetails: identity?.ensName ? { ensName: identity.ensName } : undefined,
|
||||||
ordinalDetails,
|
ordinalDetails: identity?.ordinalDetails,
|
||||||
};
|
} as User;
|
||||||
|
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
await localDatabase.storeUser(updatedUser);
|
await localDatabase.storeUser(updatedUser);
|
||||||
|
|
||||||
// Also update the user identities cache so UserIdentityService can access ENS details
|
|
||||||
await localDatabase.upsertUserIdentity(currentUser.address, {
|
await localDatabase.upsertUserIdentity(currentUser.address, {
|
||||||
ensName: ensName || undefined,
|
ensName: identity?.ensName || undefined,
|
||||||
ordinalDetails,
|
ordinalDetails: identity?.ordinalDetails,
|
||||||
verificationStatus: newVerificationStatus,
|
verificationStatus: newVerificationStatus,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Verification completed successfully, hasOwnership:', hasOwnership);
|
return newVerificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||||
return hasOwnership;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Verification failed:', error);
|
console.error('❌ Verification failed:', error);
|
||||||
// Fall back to wallet connected status
|
const updatedUser = { ...currentUser, verificationStatus: EVerificationStatus.WALLET_CONNECTED } as User;
|
||||||
const updatedUser = { ...currentUser, verificationStatus: EVerificationStatus.WALLET_CONNECTED };
|
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
await localDatabase.storeUser(updatedUser);
|
await localDatabase.storeUser(updatedUser);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [currentUser]);
|
}, [client.userIdentityService, currentUser]);
|
||||||
|
|
||||||
// Hydrate user from LocalDatabase on mount
|
// Hydrate user from LocalDatabase on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
29
packages/react/src/contexts/ClientContext.tsx
Normal file
29
packages/react/src/contexts/ClientContext.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { OpChanClient } from '@opchan/core';
|
||||||
|
|
||||||
|
export interface ClientContextValue {
|
||||||
|
client: OpChanClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext<ClientContextValue | null>(null);
|
||||||
|
|
||||||
|
export const ClientProvider: React.FC<{
|
||||||
|
client: OpChanClient;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ client, children }) => {
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={{ client }}>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useClient(): OpChanClient {
|
||||||
|
const context = useContext(ClientContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useClient must be used within OpChanProvider');
|
||||||
|
}
|
||||||
|
return context.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ClientContext };
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
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 { transformCell, transformPost, transformComment } from '@opchan/core';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { Cell, Post, Comment, UserVerificationStatus } from '@opchan/core';
|
import { Cell, Post, Comment, UserVerificationStatus } from '@opchan/core';
|
||||||
@ -63,47 +63,58 @@ export const ForumProvider: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [updateFromCache]);
|
}, [updateFromCache]);
|
||||||
|
|
||||||
|
// 1) Initial cache hydrate only – decoupled from network subscriptions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsubHealth: (() => void) | null = null;
|
|
||||||
let unsubMsg: (() => void) | null = null;
|
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
// Ensure LocalDatabase is opened before hydrating
|
|
||||||
if (!localDatabase.getSyncState) {
|
|
||||||
console.log('📥 Opening LocalDatabase for ForumProvider...');
|
|
||||||
await localDatabase.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateFromCache();
|
await updateFromCache();
|
||||||
setIsInitialLoading(false);
|
setIsInitialLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to initialize');
|
setError(e instanceof Error ? e.message : 'Failed to initialize');
|
||||||
setIsInitialLoading(false);
|
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();
|
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 () => {
|
return () => {
|
||||||
try { unsubHealth && unsubHealth(); } catch {}
|
try { unsubHealth && unsubHealth(); } catch {}
|
||||||
try { unsubMsg && unsubMsg(); } 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(() => ({
|
const ctx: ForumContextValue = useMemo(() => ({
|
||||||
cells,
|
cells,
|
||||||
|
|||||||
@ -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<boolean>;
|
|
||||||
disconnectWallet: () => Promise<boolean>;
|
|
||||||
verifyWallet: () => Promise<boolean>;
|
|
||||||
delegateKey: (duration: DelegationDuration) => Promise<boolean>;
|
|
||||||
clearDelegation: () => Promise<boolean>;
|
|
||||||
renewDelegation: (duration: DelegationDuration) => Promise<boolean>;
|
|
||||||
checkVerificationStatus: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
const cleared = await clearDelegation();
|
|
||||||
if (!cleared) return false;
|
|
||||||
return delegateKey(duration);
|
|
||||||
},
|
|
||||||
[clearDelegation, delegateKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
const checkVerificationStatus = useCallback(async (): Promise<void> => {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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<Cell | null>;
|
|
||||||
|
|
||||||
createPost: (
|
|
||||||
cellId: string,
|
|
||||||
title: string,
|
|
||||||
content: string
|
|
||||||
) => Promise<Post | null>;
|
|
||||||
votePost: (postId: string, isUpvote: boolean) => Promise<boolean>;
|
|
||||||
moderatePost: (
|
|
||||||
cellId: string,
|
|
||||||
postId: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
unmoderatePost: (
|
|
||||||
cellId: string,
|
|
||||||
postId: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
|
|
||||||
createComment: (postId: string, content: string) => Promise<Comment | null>;
|
|
||||||
voteComment: (commentId: string, isUpvote: boolean) => Promise<boolean>;
|
|
||||||
moderateComment: (
|
|
||||||
cellId: string,
|
|
||||||
commentId: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
unmoderateComment: (
|
|
||||||
cellId: string,
|
|
||||||
commentId: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
|
|
||||||
moderateUser: (
|
|
||||||
cellId: string,
|
|
||||||
userAddress: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
unmoderateUser: (
|
|
||||||
cellId: string,
|
|
||||||
userAddress: string,
|
|
||||||
reason?: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
|
|
||||||
refreshData: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Cell | null> => {
|
|
||||||
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<Post | null> => {
|
|
||||||
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<Comment | null> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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<boolean>;
|
|
||||||
updateDisplayPreference: (preference: EDisplayPreference) => Promise<boolean>;
|
|
||||||
updateProfile: (updates: {
|
|
||||||
callSign?: string;
|
|
||||||
displayPreference?: EDisplayPreference;
|
|
||||||
}) => Promise<boolean>;
|
|
||||||
clearCallSign: () => Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
return updateCallSign('');
|
|
||||||
}, [updateCallSign]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isUpdatingProfile,
|
|
||||||
isUpdatingCallSign,
|
|
||||||
isUpdatingDisplayPreference,
|
|
||||||
updateCallSign,
|
|
||||||
updateDisplayPreference,
|
|
||||||
updateProfile,
|
|
||||||
clearCallSign,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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<Bookmark[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
import { EDisplayPreference, EVerificationStatus } from '@opchan/core';
|
||||||
import { useForum } from '../../contexts/ForumContext';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useForumData } from './useForumData';
|
||||||
import { EDisplayPreference, EVerificationStatus, UserIdentityService } from '@opchan/core';
|
import { useClient } from '../../contexts/ClientContext';
|
||||||
|
|
||||||
export interface UserDisplayInfo {
|
export interface UserDisplayInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -14,9 +14,12 @@ export interface UserDisplayInfo {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User display hook with caching and reactive updates
|
||||||
|
*/
|
||||||
export function useUserDisplay(address: string): UserDisplayInfo {
|
export function useUserDisplay(address: string): UserDisplayInfo {
|
||||||
const { userVerificationStatus } = useForum();
|
const client = useClient();
|
||||||
const { currentUser } = useAuth();
|
const { userVerificationStatus } = useForumData();
|
||||||
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,
|
||||||
@ -28,21 +31,9 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
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(() => {
|
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 (
|
return (
|
||||||
userVerificationStatus[address] || {
|
userVerificationStatus[address] || {
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
@ -50,7 +41,22 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
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(() => {
|
useEffect(() => {
|
||||||
const getUserDisplayInfo = async () => {
|
const getUserDisplayInfo = async () => {
|
||||||
@ -63,49 +69,63 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent multiple simultaneous calls
|
if (!client.userIdentityService) {
|
||||||
if (isLoadingRef.current) {
|
console.log(
|
||||||
return;
|
'useEnhancedUserDisplay: No service available, using fallback',
|
||||||
}
|
{ address }
|
||||||
|
);
|
||||||
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);
|
|
||||||
|
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName,
|
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
||||||
callSign: identity?.callSign || null,
|
callSign: null,
|
||||||
ensName: identity?.ensName || verificationInfo.ensName || null,
|
ensName: verificationInfo.ensName || null,
|
||||||
ordinalDetails: identity?.ordinalDetails ?
|
ordinalDetails: null,
|
||||||
`${identity.ordinalDetails.ordinalId}` : null,
|
verificationLevel:
|
||||||
verificationLevel: identity?.verificationStatus ||
|
|
||||||
verificationInfo.verificationStatus ||
|
verificationInfo.verificationStatus ||
|
||||||
EVerificationStatus.WALLET_UNCONNECTED,
|
EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
displayPreference: identity?.displayPreference || null,
|
displayPreference: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
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) {
|
} catch (error) {
|
||||||
console.error('useUserDisplay: Failed to get user display info:', error);
|
console.error(
|
||||||
|
'useEnhancedUserDisplay: Failed to get user display info:',
|
||||||
|
error
|
||||||
|
);
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
|
||||||
callSign: null,
|
callSign: null,
|
||||||
@ -116,18 +136,28 @@ export function useUserDisplay(address: string): UserDisplayInfo {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
isLoadingRef.current = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getUserDisplayInfo();
|
getUserDisplayInfo();
|
||||||
|
}, [address, client.userIdentityService, verificationInfo, refreshTrigger]);
|
||||||
|
|
||||||
// Cleanup function to reset loading ref
|
// Update display info when verification status changes reactively
|
||||||
return () => {
|
useEffect(() => {
|
||||||
isLoadingRef.current = false;
|
if (!displayInfo.isLoading && verificationInfo) {
|
||||||
};
|
setDisplayInfo(prev => ({
|
||||||
}, [address, refreshTrigger, verificationInfo.verificationStatus]);
|
...prev,
|
||||||
|
ensName: verificationInfo.ensName || prev.ensName,
|
||||||
|
verificationLevel:
|
||||||
|
verificationInfo.verificationStatus || prev.verificationLevel,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
verificationInfo.ensName,
|
||||||
|
verificationInfo.verificationStatus,
|
||||||
|
displayInfo.isLoading,
|
||||||
|
verificationInfo,
|
||||||
|
]);
|
||||||
|
|
||||||
return displayInfo;
|
return displayInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,24 @@
|
|||||||
// Public hooks surface: aggregator and focused derived hooks
|
// Public hooks surface: aggregator and focused derived hooks
|
||||||
// Aggregator hook
|
// Aggregator hook (main API)
|
||||||
export { useForumApi } from './useForum';
|
export { useForumApi } from './useForum';
|
||||||
|
|
||||||
// Core hooks
|
// Core hooks (complex logic)
|
||||||
export { useForumData } from './core/useForumData';
|
export { useForumData } from './core/useForumData';
|
||||||
export { usePermissions } from './core/usePermissions';
|
export { usePermissions } from './core/usePermissions';
|
||||||
export { useUserDisplay } from './core/useUserDisplay';
|
export { useUserDisplay } from './core/useUserDisplay';
|
||||||
export { useBookmarks, usePostBookmark, useCommentBookmark } from './core/useBookmarks';
|
|
||||||
|
|
||||||
// Action hooks
|
// Derived hooks (data slicing utilities)
|
||||||
export { useForumActions } from './actions/useForumActions';
|
|
||||||
export { useAuthActions } from './actions/useAuthActions';
|
|
||||||
export { useUserActions } from './actions/useUserActions';
|
|
||||||
|
|
||||||
// Derived hooks
|
|
||||||
export { useCell } from './derived/useCell';
|
export { useCell } from './derived/useCell';
|
||||||
export { usePost } from './derived/usePost';
|
export { usePost } from './derived/usePost';
|
||||||
export { useCellPosts } from './derived/useCellPosts';
|
export { useCellPosts } from './derived/useCellPosts';
|
||||||
export { usePostComments } from './derived/usePostComments';
|
export { usePostComments } from './derived/usePostComments';
|
||||||
export { useUserVotes } from './derived/useUserVotes';
|
export { useUserVotes } from './derived/useUserVotes';
|
||||||
|
|
||||||
// Utility hooks
|
// Utility hooks (remaining complex logic)
|
||||||
export { useWakuHealth, useWakuReady, useWakuHealthStatus } from './utilities/useWakuHealth';
|
|
||||||
export { useDelegation } from './utilities/useDelegation';
|
|
||||||
export { useMessageSigning } from './utilities/useMessageSigning';
|
|
||||||
export { usePending, usePendingVote } from './utilities/usePending';
|
|
||||||
export { useWallet } from './utilities/useWallet';
|
export { useWallet } from './utilities/useWallet';
|
||||||
export { useNetworkStatus } from './utilities/useNetworkStatus';
|
export { useNetworkStatus } from './utilities/useNetworkStatus';
|
||||||
export { useForumSelectors } from './utilities/useForumSelectors';
|
export { useForumSelectors } from './utilities/useForumSelectors';
|
||||||
|
export { useBookmarks, usePostBookmark, useCommentBookmark } from './utilities/useBookmarks';
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type {
|
export type {
|
||||||
@ -45,20 +36,7 @@ export type {
|
|||||||
|
|
||||||
export type { UserDisplayInfo } from './core/useUserDisplay';
|
export type { UserDisplayInfo } from './core/useUserDisplay';
|
||||||
|
|
||||||
export type {
|
// Removed types from deleted action hooks - functionality now in useForumApi
|
||||||
ForumActionStates,
|
|
||||||
ForumActions,
|
|
||||||
} from './actions/useForumActions';
|
|
||||||
|
|
||||||
export type {
|
|
||||||
AuthActionStates,
|
|
||||||
AuthActions,
|
|
||||||
} from './actions/useAuthActions';
|
|
||||||
|
|
||||||
export type {
|
|
||||||
UserActionStates,
|
|
||||||
UserActions,
|
|
||||||
} from './actions/useUserActions';
|
|
||||||
|
|
||||||
export type { CellData } from './derived/useCell';
|
export type { CellData } from './derived/useCell';
|
||||||
export type { PostData } from './derived/usePost';
|
export type { PostData } from './derived/usePost';
|
||||||
|
|||||||
@ -1,22 +1,18 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useCallback, useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useForum as useForumContext } from '../contexts/ForumContext';
|
import { useForum as useForumContext } from '../contexts/ForumContext';
|
||||||
|
import { useClient } from '../contexts/ClientContext';
|
||||||
import { usePermissions } from './core/usePermissions';
|
import { usePermissions } from './core/usePermissions';
|
||||||
import { useForumData } from './core/useForumData';
|
import { useForumData } from './core/useForumData';
|
||||||
import { useNetworkStatus } from './utilities/useNetworkStatus';
|
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 { useForumSelectors } from './utilities/useForumSelectors';
|
||||||
import { localDatabase } from '@opchan/core';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Cell,
|
Cell,
|
||||||
Comment,
|
Comment,
|
||||||
Post,
|
Post,
|
||||||
Bookmark,
|
Bookmark,
|
||||||
|
User,
|
||||||
DelegationDuration,
|
DelegationDuration,
|
||||||
EDisplayPreference,
|
EDisplayPreference,
|
||||||
EVerificationStatus,
|
EVerificationStatus,
|
||||||
@ -171,6 +167,7 @@ export interface UseForumApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useForumApi(): UseForumApi {
|
export function useForumApi(): UseForumApi {
|
||||||
|
const client = useClient();
|
||||||
const { currentUser, verificationStatus, connectWallet, disconnectWallet, verifyOwnership } = useAuth();
|
const { currentUser, verificationStatus, connectWallet, disconnectWallet, verifyOwnership } = useAuth();
|
||||||
const {
|
const {
|
||||||
refreshData,
|
refreshData,
|
||||||
@ -179,14 +176,126 @@ export function useForumApi(): UseForumApi {
|
|||||||
const forumData: ForumData = useForumData();
|
const forumData: ForumData = useForumData();
|
||||||
const permissions = usePermissions();
|
const permissions = usePermissions();
|
||||||
const network = useNetworkStatus();
|
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);
|
const selectors = useForumSelectors(forumData);
|
||||||
|
|
||||||
|
// Bookmarks state (moved from useBookmarks)
|
||||||
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
||||||
|
|
||||||
|
// 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<boolean> => {
|
||||||
|
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<void> => {
|
||||||
|
// 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<void> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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;
|
type MaybeOrdinal = { ordinalId?: unknown } | null | undefined;
|
||||||
const toOrdinal = (value: MaybeOrdinal): { ordinalId: string } | null => {
|
const toOrdinal = (value: MaybeOrdinal): { ordinalId: string } | null => {
|
||||||
if (value && typeof value === 'object' && typeof (value as { ordinalId?: unknown }).ordinalId === 'string') {
|
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),
|
delegateKey: async (duration?: DelegationDuration) => createDelegation(duration),
|
||||||
clearDelegation: async () => { await clearDelegation(); },
|
clearDelegation: async () => { await clearDelegation(); },
|
||||||
updateProfile: async (updates: { callSign?: string; displayPreference?: EDisplayPreference }) => {
|
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,
|
signMessage,
|
||||||
verifyMessage,
|
verifyMessage,
|
||||||
@ -241,71 +379,198 @@ export function useForumApi(): UseForumApi {
|
|||||||
comments: forumData.filteredComments,
|
comments: forumData.filteredComments,
|
||||||
},
|
},
|
||||||
createCell: async (input: { name: string; description: string; icon?: string }) => {
|
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 }) => {
|
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 }) => {
|
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 }) => {
|
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;
|
if (!input.targetId) return false;
|
||||||
// Try post vote first, then comment vote if needed
|
|
||||||
try {
|
try {
|
||||||
const ok = await forumActions.votePost(input.targetId, input.isUpvote);
|
// Use the unified vote method from ForumActions
|
||||||
if (ok) return true;
|
const result = await client.forumActions.vote(
|
||||||
} catch {}
|
{
|
||||||
try {
|
targetId: input.targetId,
|
||||||
return await forumActions.voteComment(input.targetId, input.isUpvote);
|
isUpvote: input.isUpvote,
|
||||||
|
currentUser,
|
||||||
|
isAuthenticated: !!currentUser,
|
||||||
|
},
|
||||||
|
async () => {}
|
||||||
|
);
|
||||||
|
return result.success;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moderate: {
|
moderate: {
|
||||||
post: async (cellId: string, postId: string, reason?: string) => {
|
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) => {
|
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) => {
|
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) => {
|
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) => {
|
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) => {
|
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),
|
togglePostBookmark: async (post: Post, cellId?: string) => {
|
||||||
toggleCommentBookmark: async (comment: Comment, postId?: string) => bookmarkComment(comment, postId),
|
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(); },
|
refresh: async () => { await refreshData(); },
|
||||||
pending: {
|
pending: {
|
||||||
isPending: (id?: string) => {
|
isPending: (id?: string) => {
|
||||||
return id ? localDatabase.isPending(id) : false;
|
return id ? client.database.isPending(id) : false;
|
||||||
},
|
},
|
||||||
isVotePending: (targetId?: string) => {
|
isVotePending: (targetId?: string) => {
|
||||||
if (!targetId || !currentUser?.address) return false;
|
if (!targetId || !currentUser?.address) return false;
|
||||||
return Object.values(localDatabase.cache.votes).some(v => {
|
return Object.values(client.database.cache.votes).some(v => {
|
||||||
return (
|
return (
|
||||||
v.targetId === targetId &&
|
v.targetId === targetId &&
|
||||||
v.author === currentUser.address &&
|
v.author === currentUser.address &&
|
||||||
localDatabase.isPending(v.id)
|
client.database.isPending(v.id)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onChange: (cb: () => void) => {
|
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(() => {
|
const permissionsSlice = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -361,3 +626,4 @@ export function useForumApi(): UseForumApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
130
packages/react/src/hooks/utilities/useBookmarks.ts
Normal file
130
packages/react/src/hooks/utilities/useBookmarks.ts
Normal file
@ -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<void>;
|
||||||
|
clearAllBookmarks: () => Promise<void>;
|
||||||
|
togglePostBookmark: (post: Post, cellId?: string) => Promise<boolean>;
|
||||||
|
toggleCommentBookmark: (comment: Comment, postId?: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBookmarks(): UseBookmarksReturn {
|
||||||
|
const { currentUser } = useAuth();
|
||||||
|
const client = useClient();
|
||||||
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(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<void> => {
|
||||||
|
await BookmarkService.removeBookmark(bookmark.type, bookmark.targetId);
|
||||||
|
await refresh();
|
||||||
|
},
|
||||||
|
[refresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearAllBookmarks = useCallback(async (): Promise<void> => {
|
||||||
|
if (!currentUser?.address) return;
|
||||||
|
await BookmarkService.clearUserBookmarks(currentUser.address);
|
||||||
|
await refresh();
|
||||||
|
}, [currentUser?.address, refresh]);
|
||||||
|
|
||||||
|
const togglePostBookmark = useCallback(
|
||||||
|
async (post: Post, cellId?: string): Promise<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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<boolean> => {
|
|
||||||
return contextDelegateKey(duration);
|
|
||||||
},
|
|
||||||
[contextDelegateKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearDelegation = useCallback(async (): Promise<void> => {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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<void> => {
|
|
||||||
// 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<boolean> => {
|
|
||||||
return await contextVerifyMessage(message);
|
|
||||||
},
|
|
||||||
[contextVerifyMessage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const canSignMessages = useCallback(async (): Promise<boolean> => {
|
|
||||||
const delegationStatus = await getDelegationStatus();
|
|
||||||
return delegationStatus.isValid;
|
|
||||||
}, [getDelegationStatus]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Message signing
|
|
||||||
signMessage,
|
|
||||||
verifyMessage,
|
|
||||||
canSignMessages,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState, useEffect } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { useForum } from '../../contexts/ForumContext';
|
import { useForum } from '../../contexts/ForumContext';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { useClient } from '../../contexts/ClientContext';
|
||||||
import { DelegationFullStatus } from '@opchan/core';
|
import { DelegationFullStatus } from '@opchan/core';
|
||||||
|
|
||||||
export interface NetworkHealth {
|
export interface NetworkHealth {
|
||||||
@ -58,11 +59,38 @@ export interface NetworkStatusData {
|
|||||||
export function useNetworkStatus(): NetworkStatusData {
|
export function useNetworkStatus(): NetworkStatusData {
|
||||||
const { isNetworkConnected, isInitialLoading, isRefreshing, error } =
|
const { isNetworkConnected, isInitialLoading, isRefreshing, error } =
|
||||||
useForum();
|
useForum();
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
const { isAuthenticated, currentUser, getDelegationStatus } = useAuth();
|
const { isAuthenticated, currentUser, getDelegationStatus } = useAuth();
|
||||||
const [delegationInfo, setDelegationInfo] =
|
const [delegationInfo, setDelegationInfo] =
|
||||||
useState<DelegationFullStatus | null>(null);
|
useState<DelegationFullStatus | null>(null);
|
||||||
|
|
||||||
|
// Track Waku ready state directly from the client to react to changes
|
||||||
|
const [wakuReady, setWakuReady] = useState<boolean>(
|
||||||
|
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
|
// Load delegation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||||
@ -72,7 +100,10 @@ export function useNetworkStatus(): NetworkStatusData {
|
|||||||
const health = useMemo((): NetworkHealth => {
|
const health = useMemo((): NetworkHealth => {
|
||||||
const issues: string[] = [];
|
const issues: string[] = [];
|
||||||
|
|
||||||
if (!isNetworkConnected) {
|
const fallbackConnected = Boolean(wakuReady);
|
||||||
|
const effectiveConnected = isNetworkConnected || wakuReady;
|
||||||
|
|
||||||
|
if (!effectiveConnected) {
|
||||||
issues.push('Waku network disconnected');
|
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 lastSync = Date.now(); // This would come from actual sync tracking
|
||||||
const syncAge = lastSync ? formatTimeAgo(lastSync) : null;
|
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 {
|
return {
|
||||||
isConnected: isNetworkConnected,
|
isConnected: effectiveConnected,
|
||||||
isHealthy,
|
isHealthy,
|
||||||
lastSync,
|
lastSync,
|
||||||
syncAge,
|
syncAge,
|
||||||
issues,
|
issues,
|
||||||
};
|
};
|
||||||
}, [isNetworkConnected, error, isAuthenticated, delegationInfo?.isValid]);
|
}, [client, isNetworkConnected, wakuReady, error, isAuthenticated, delegationInfo?.isValid]);
|
||||||
|
|
||||||
// Sync status
|
// Sync status
|
||||||
const sync = useMemo((): SyncStatus => {
|
const sync = useMemo((): SyncStatus => {
|
||||||
@ -114,11 +157,12 @@ export function useNetworkStatus(): NetworkStatusData {
|
|||||||
|
|
||||||
// Connection status
|
// Connection status
|
||||||
const connections = useMemo((): ConnectionStatus => {
|
const connections = useMemo((): ConnectionStatus => {
|
||||||
|
const effectiveConnected = health.isConnected;
|
||||||
return {
|
return {
|
||||||
waku: {
|
waku: {
|
||||||
connected: isNetworkConnected,
|
connected: effectiveConnected,
|
||||||
peers: isNetworkConnected ? 3 : 0, // Mock peer count
|
peers: effectiveConnected ? 3 : 0, // Mock peer count
|
||||||
status: isNetworkConnected ? 'connected' : 'disconnected',
|
status: effectiveConnected ? 'connected' : 'disconnected',
|
||||||
},
|
},
|
||||||
wallet: {
|
wallet: {
|
||||||
connected: isAuthenticated,
|
connected: isAuthenticated,
|
||||||
@ -131,19 +175,28 @@ export function useNetworkStatus(): NetworkStatusData {
|
|||||||
status: delegationInfo?.isValid ? 'active' : 'expired',
|
status: delegationInfo?.isValid ? 'active' : 'expired',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [isNetworkConnected, isAuthenticated, currentUser, delegationInfo]);
|
}, [health.isConnected, isAuthenticated, currentUser, delegationInfo]);
|
||||||
|
|
||||||
// Status assessment
|
// Status assessment
|
||||||
const canRefresh = !isRefreshing && !isInitialLoading;
|
const canRefresh = !isRefreshing && !isInitialLoading;
|
||||||
const canSync = isNetworkConnected && !isRefreshing;
|
const canSync = health.isConnected && !isRefreshing;
|
||||||
const needsAttention = !health.isHealthy || !delegationInfo?.isValid;
|
const needsAttention = !health.isHealthy || !delegationInfo?.isValid;
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
const getStatusMessage = useMemo(() => {
|
const getStatusMessage = useMemo(() => {
|
||||||
return (): string => {
|
return (): string => {
|
||||||
|
console.debug('[useNetworkStatus] statusMessage inputs', {
|
||||||
|
isInitialLoading,
|
||||||
|
isRefreshing,
|
||||||
|
isNetworkConnected,
|
||||||
|
error,
|
||||||
|
issues: health.issues,
|
||||||
|
});
|
||||||
if (isInitialLoading) return 'Loading forum data...';
|
if (isInitialLoading) return 'Loading forum data...';
|
||||||
if (isRefreshing) return 'Refreshing 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 (error) return `Error: ${error}`;
|
||||||
if (health.issues.length > 0) return health.issues[0] || 'Unknown issue';
|
if (health.issues.length > 0) return health.issues[0] || 'Unknown issue';
|
||||||
return 'All systems operational';
|
return 'All systems operational';
|
||||||
@ -152,18 +205,30 @@ export function useNetworkStatus(): NetworkStatusData {
|
|||||||
isInitialLoading,
|
isInitialLoading,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isNetworkConnected,
|
isNetworkConnected,
|
||||||
|
client,
|
||||||
|
wakuReady,
|
||||||
error,
|
error,
|
||||||
health.issues,
|
health.issues,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getHealthColor = useMemo(() => {
|
const getHealthColor = useMemo(() => {
|
||||||
return (): 'green' | 'yellow' | 'red' => {
|
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';
|
if (health.issues.length > 0 || !delegationInfo?.isValid) return 'yellow';
|
||||||
return 'green';
|
return 'green';
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
isNetworkConnected,
|
isNetworkConnected,
|
||||||
|
client,
|
||||||
|
wakuReady,
|
||||||
error,
|
error,
|
||||||
health.issues.length,
|
health.issues.length,
|
||||||
delegationInfo?.isValid,
|
delegationInfo?.isValid,
|
||||||
|
|||||||
@ -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<boolean>(
|
|
||||||
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<boolean>(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 };
|
|
||||||
}
|
|
||||||
@ -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>(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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// Providers only (context hooks are internal)
|
// Providers only (context hooks are internal)
|
||||||
export * from './provider/OpChanProvider';
|
export * from './provider/OpChanProvider';
|
||||||
|
export { ClientProvider, useClient } from './contexts/ClientContext';
|
||||||
export { AuthProvider, useAuth } from './contexts/AuthContext';
|
export { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
export { ForumProvider, useForum as useForumContext } from './contexts/ForumContext';
|
export { ForumProvider, useForum as useForumContext } from './contexts/ForumContext';
|
||||||
export { ModerationProvider, useModeration } from './contexts/ModerationContext';
|
export { ModerationProvider, useModeration } from './contexts/ModerationContext';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { OpChanClient } from '@opchan/core';
|
import { OpChanClient } from '@opchan/core';
|
||||||
import { localDatabase, messageManager } from '@opchan/core';
|
import { localDatabase, messageManager } from '@opchan/core';
|
||||||
|
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';
|
||||||
@ -49,11 +50,13 @@ export const OpChanProvider: React.FC<OpChanProviderProps> = ({
|
|||||||
const providers = useMemo(() => {
|
const providers = useMemo(() => {
|
||||||
if (!isReady || !clientRef.current) return null;
|
if (!isReady || !clientRef.current) return null;
|
||||||
return (
|
return (
|
||||||
<AuthProvider client={clientRef.current}>
|
<ClientProvider 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>
|
||||||
|
</ClientProvider>
|
||||||
);
|
);
|
||||||
}, [isReady, children]);
|
}, [isReady, children]);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user