OpChan/src/contexts/ForumContext.tsx
2025-08-28 19:07:26 +05:30

417 lines
12 KiB
TypeScript

import React, { createContext, useState, useEffect, useCallback, useMemo } from 'react';
import { Cell, Post, Comment, OpchanMessage, User, EVerificationStatus } from '@/types/forum';
import { useToast } from '@/components/ui/use-toast';
import { useAuth } from '@/contexts/useAuth';
import {
createPost,
createComment,
vote,
createCell,
moderatePost,
moderateComment,
moderateUser
} from '@/lib/forum/actions';
import {
setupPeriodicQueries,
monitorNetworkHealth,
initializeNetwork
} from '@/lib/waku/network';
import messageManager from '@/lib/waku';
import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator } from '@/lib/forum/relevance';
import { UserVerificationStatus } from '@/types/forum';
import { CryptoService, AuthService } from '@/lib/services';
import { getEnsName } from '@wagmi/core';
import { config } from '@/lib/identity/wallets/appkit';
interface ForumContextType {
cells: Cell[];
posts: Post[];
comments: Comment[];
// User verification status for display
userVerificationStatus: UserVerificationStatus;
// Granular loading states
isInitialLoading: boolean;
isPostingCell: boolean;
isPostingPost: boolean;
isPostingComment: boolean;
isVoting: boolean;
isRefreshing: boolean;
// Network status
isNetworkConnected: boolean;
error: string | null;
getCellById: (id: string) => Cell | undefined;
getPostsByCell: (cellId: string) => Post[];
getCommentsByPost: (postId: string) => Comment[];
createPost: (cellId: string, title: string, content: string) => Promise<Post | null>;
createComment: (postId: string, content: string) => Promise<Comment | null>;
votePost: (postId: string, isUpvote: boolean) => Promise<boolean>;
voteComment: (commentId: string, isUpvote: boolean) => Promise<boolean>;
createCell: (name: string, description: string, icon?: string) => Promise<Cell | null>;
refreshData: () => Promise<void>;
moderatePost: (
cellId: string,
postId: string,
reason: string | undefined,
cellOwner: string
) => Promise<boolean>;
moderateComment: (
cellId: string,
commentId: string,
reason: string | undefined,
cellOwner: string
) => Promise<boolean>;
moderateUser: (
cellId: string,
userAddress: string,
reason: string | undefined,
cellOwner: string
) => Promise<boolean>;
}
const ForumContext = createContext<ForumContextType | undefined>(undefined);
export { ForumContext };
export function ForumProvider({ children }: { children: React.ReactNode }) {
const [cells, setCells] = useState<Cell[]>([]);
const [posts, setPosts] = useState<Post[]>([]);
const [comments, setComments] = useState<Comment[]>([]);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isPostingCell, setIsPostingCell] = useState(false);
const [isPostingPost, setIsPostingPost] = useState(false);
const [isPostingComment, setIsPostingComment] = useState(false);
const [isVoting, setIsVoting] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isNetworkConnected, setIsNetworkConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const [userVerificationStatus, setUserVerificationStatus] = useState<UserVerificationStatus>({});
const { toast } = useToast();
const { currentUser, isAuthenticated } = useAuth();
const cryptoService = useMemo(() => new CryptoService(), []);
const authService = useMemo(() => new AuthService(cryptoService), [cryptoService]);
// Transform message cache data to the expected types
const updateStateFromCache = useCallback(() => {
// Use the verifyMessage function from cryptoService if available
const verifyFn = isAuthenticated ?
(message: OpchanMessage) => cryptoService.verifyMessage(message) :
undefined;
// Build user verification status for relevance calculation
const relevanceCalculator = new RelevanceCalculator();
const allUsers: User[] = [];
// Collect all unique users from posts, comments, and votes
const userAddresses = new Set<string>();
// Add users from posts
Object.values(messageManager.messageCache.posts).forEach(post => {
userAddresses.add(post.author);
});
// Add users from comments
Object.values(messageManager.messageCache.comments).forEach(comment => {
userAddresses.add(comment.author);
});
// Add users from votes
Object.values(messageManager.messageCache.votes).forEach(vote => {
userAddresses.add(vote.author);
});
// Create user objects for verification status
Array.from(userAddresses).forEach(address => {
// Check if this address matches the current user's address
if (currentUser && currentUser.address === address) {
// Use the current user's actual verification status
allUsers.push({
address,
walletType: currentUser.walletType,
verificationStatus: currentUser.verificationStatus,
ensOwnership: currentUser.ensOwnership,
ensName: currentUser.ensName,
ensAvatar: currentUser.ensAvatar,
ordinalOwnership: currentUser.ordinalOwnership,
lastChecked: currentUser.lastChecked
});
} else {
// Create generic user object for other addresses
allUsers.push({
address,
walletType: address.startsWith('0x') ? 'ethereum' : 'bitcoin',
verificationStatus: EVerificationStatus.UNVERIFIED
});
}
});
const initialStatus = relevanceCalculator.buildUserVerificationStatus(allUsers);
// Transform data with relevance calculation (initial pass)
const { cells, posts, comments } = getDataFromCache(verifyFn, initialStatus);
setCells(cells);
setPosts(posts);
setComments(comments);
setUserVerificationStatus(initialStatus);
// Enrich: resolve ENS for ethereum addresses asynchronously and update
(async () => {
const targets = allUsers.filter(u => u.walletType === 'ethereum' && !u.ensOwnership);
if (targets.length === 0) return;
const lookups = await Promise.all(targets.map(async (u) => {
try {
const name = await getEnsName(config, { address: u.address as `0x${string}` });
return { address: u.address, ensName: name || undefined };
} catch {
return { address: u.address, ensName: undefined };
}
}));
const ensByAddress = new Map<string, string | undefined>(lookups.map(l => [l.address, l.ensName]));
const enrichedUsers: User[] = allUsers.map(u => {
const ensName = ensByAddress.get(u.address);
if (ensName) {
return {
...u,
walletType: 'ethereum',
ensOwnership: true,
ensName,
verificationStatus: 'verified-owner'
} as User;
}
return u;
});
const enrichedStatus = relevanceCalculator.buildUserVerificationStatus(enrichedUsers);
const transformed = getDataFromCache(verifyFn, enrichedStatus);
setCells(transformed.cells);
setPosts(transformed.posts);
setComments(transformed.comments);
setUserVerificationStatus(enrichedStatus);
})();
}, [cryptoService, isAuthenticated, currentUser]);
const handleRefreshData = async () => {
setIsRefreshing(true);
try {
// Manually query the network for updates
await messageManager.queryStore();
updateStateFromCache();
} catch (error) {
console.error("Error refreshing data:", error);
toast({
title: "Refresh Failed",
description: "Could not fetch the latest data. Please try again.",
variant: "destructive",
});
} finally {
setIsRefreshing(false);
}
};
// Monitor network connection status
useEffect(() => {
const { unsubscribe } = monitorNetworkHealth(setIsNetworkConnected, toast);
return unsubscribe;
}, [toast]);
useEffect(() => {
const loadData = async () => {
setIsInitialLoading(true);
await initializeNetwork(toast, updateStateFromCache, setError);
setIsInitialLoading(false);
};
loadData();
// Set up periodic queries
const { cleanup } = setupPeriodicQueries(isNetworkConnected, updateStateFromCache);
return cleanup;
}, [isNetworkConnected, toast, updateStateFromCache]);
const getCellById = (id: string): Cell | undefined => {
return cells.find(cell => cell.id === id);
};
const getPostsByCell = (cellId: string): Post[] => {
return posts.filter(post => post.cellId === cellId)
.sort((a, b) => b.timestamp - a.timestamp);
};
const getCommentsByPost = (postId: string): Comment[] => {
return comments.filter(comment => comment.postId === postId)
.sort((a, b) => a.timestamp - b.timestamp);
};
const handleCreatePost = async (cellId: string, title: string, content: string): Promise<Post | null> => {
setIsPostingPost(true);
const result = await createPost(
cellId,
title,
content,
currentUser,
isAuthenticated,
toast,
updateStateFromCache,
authService
);
setIsPostingPost(false);
return result;
};
const handleCreateComment = async (postId: string, content: string): Promise<Comment | null> => {
setIsPostingComment(true);
const result = await createComment(
postId,
content,
currentUser,
isAuthenticated,
toast,
updateStateFromCache,
authService
);
setIsPostingComment(false);
return result;
};
const handleVotePost = async (postId: string, isUpvote: boolean): Promise<boolean> => {
setIsVoting(true);
const result = await vote(
postId,
isUpvote,
currentUser,
isAuthenticated,
toast,
updateStateFromCache,
authService
);
setIsVoting(false);
return result;
};
const handleVoteComment = async (commentId: string, isUpvote: boolean): Promise<boolean> => {
setIsVoting(true);
const result = await vote(
commentId,
isUpvote,
currentUser,
isAuthenticated,
toast,
updateStateFromCache,
authService
);
setIsVoting(false);
return result;
};
const handleCreateCell = async (name: string, description: string, icon?: string): Promise<Cell | null> => {
setIsPostingCell(true);
const result = await createCell(
name,
description,
icon,
currentUser,
isAuthenticated,
toast,
updateStateFromCache,
authService
);
setIsPostingCell(false);
return result;
};
const handleModeratePost = async (
cellId: string,
postId: string,
reason: string | undefined,
cellOwner: string
) => {
return moderatePost(
cellId,
postId,
reason,
currentUser,
isAuthenticated,
cellOwner,
toast,
updateStateFromCache,
authService
);
};
const handleModerateComment = async (
cellId: string,
commentId: string,
reason: string | undefined,
cellOwner: string
) => {
return moderateComment(
cellId,
commentId,
reason,
currentUser,
isAuthenticated,
cellOwner,
toast,
updateStateFromCache,
authService
);
};
const handleModerateUser = async (
cellId: string,
userAddress: string,
reason: string | undefined,
cellOwner: string
) => {
return moderateUser(
cellId,
userAddress,
reason,
currentUser,
isAuthenticated,
cellOwner,
toast,
updateStateFromCache,
authService
);
};
return (
<ForumContext.Provider
value={{
cells,
posts,
comments,
userVerificationStatus,
isInitialLoading,
isPostingCell,
isPostingPost,
isPostingComment,
isVoting,
isRefreshing,
isNetworkConnected,
error,
getCellById,
getPostsByCell,
getCommentsByPost,
createPost: handleCreatePost,
createComment: handleCreateComment,
votePost: handleVotePost,
voteComment: handleVoteComment,
createCell: handleCreateCell,
refreshData: handleRefreshData,
moderatePost: handleModeratePost,
moderateComment: handleModerateComment,
moderateUser: handleModerateUser
}}
>
{children}
</ForumContext.Provider>
);
}