import React from 'react'; import { useClient } from '../context/ClientContext'; import { useOpchanStore, setOpchanState } from '../store/opchanStore'; import { Post, Comment, Cell, EVerificationStatus, UserVerificationStatus, BookmarkType, getDataFromCache, } from '@opchan/core'; import { BookmarkService } from '@opchan/core'; function reflectCache(client: ReturnType): void { getDataFromCache().then(({ cells, posts, comments }: { cells: Cell[]; posts: Post[]; comments: Comment[] }) => { setOpchanState(prev => ({ ...prev, content: { ...prev.content, cells, posts, comments, bookmarks: Object.values(client.database.cache.bookmarks), lastSync: client.database.getSyncState().lastSync, pendingIds: prev.content.pendingIds, pendingVotes: prev.content.pendingVotes, }, })); }).catch((err: Error) => { console.error('reflectCache failed', err); }); } export function useContent() { const client = useClient(); const content = useOpchanStore(s => s.content); const session = useOpchanStore(s => s.session); // Re-render on pending changes from LocalDatabase so isPending reflects current state const [, forceRender] = React.useReducer((x: number) => x + 1, 0); React.useEffect(() => { const off = client.database.onPendingChange(() => { forceRender(); }); return () => { try { off(); } catch (err) { console.error('Error cleaning up pending change listener:', err); } }; }, [client]); // Derived maps const postsByCell = React.useMemo(() => { const map: Record = {}; for (const p of content.posts) { (map[p.cellId] ||= []).push(p); } return map; }, [content.posts]); const commentsByPost = React.useMemo(() => { const map: Record = {}; for (const c of content.comments) { (map[c.postId] ||= []).push(c); } for (const postId in map) { map[postId].sort((a, b) => a.timestamp - b.timestamp); } return map; }, [content.comments]); // Derived: user verification status from identity cache const userVerificationStatus: UserVerificationStatus = React.useMemo(() => { const identities = client.database.cache.userIdentities; const result: UserVerificationStatus = {}; for (const [address, rec] of Object.entries(identities)) { if (rec) { const hasEns = Boolean(rec.ensName); const isVerified = rec.verificationStatus === EVerificationStatus.ENS_VERIFIED; result[address] = { isVerified, hasENS: hasEns, ensName: rec.ensName, verificationStatus: rec.verificationStatus, }; } } return result; }, [client.database.cache.userIdentities]); // Derived: cells with stats for sidebar/trending const cellsWithStats = React.useMemo(() => { const byCell: Record; recentActivity: number }> = {}; const now = Date.now(); const recentWindowMs = 7 * 24 * 60 * 60 * 1000; // 7 days for (const p of content.posts) { const entry = (byCell[p.cellId] ||= { postCount: 0, activeUsers: new Set(), recentActivity: 0 }); entry.postCount += 1; entry.activeUsers.add(p.author); if (now - p.timestamp <= recentWindowMs) entry.recentActivity += 1; } for (const c of content.comments) { // find post for cell reference const post = content.posts.find(pp => pp.id === c.postId); if (!post) continue; const entry = (byCell[post.cellId] ||= { postCount: 0, activeUsers: new Set(), recentActivity: 0 }); entry.activeUsers.add(c.author); if (now - c.timestamp <= recentWindowMs) entry.recentActivity += 1; } return content.cells.map(cell => { const stats = byCell[cell.id] || { postCount: 0, activeUsers: new Set(), recentActivity: 0 }; return { ...cell, postCount: stats.postCount, activeUsers: stats.activeUsers.size, recentActivity: stats.recentActivity, } as Cell & { postCount: number; activeUsers: number; recentActivity: number }; }); }, [content.cells, content.posts, content.comments]); // Actions const createCell = React.useCallback(async (input: { name: string; description: string; icon?: string }): Promise => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const result = await client.forumActions.createCell( { ...input, currentUser, isAuthenticated }, () => reflectCache(client) ); reflectCache(client); return result.data ?? null; }, [client, session.currentUser]); const createPost = React.useCallback(async (input: { cellId: string; title: string; content: string }): Promise => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const result = await client.forumActions.createPost( { ...input, currentUser, isAuthenticated }, () => reflectCache(client) ); reflectCache(client); return result.data ?? null; }, [client, session.currentUser]); const createComment = React.useCallback(async (input: { postId: string; content: string }): Promise => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const result = await client.forumActions.createComment( { ...input, currentUser, isAuthenticated }, () => reflectCache(client) ); reflectCache(client); return result.data ?? null; }, [client, session.currentUser]); const vote = React.useCallback(async (input: { targetId: string; isUpvote: boolean }): Promise => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const result = await client.forumActions.vote( { ...input, currentUser, isAuthenticated }, () => reflectCache(client) ); reflectCache(client); return result.data ?? false; }, [client, session.currentUser]); const moderate = React.useMemo(() => ({ post: async (cellId: string, postId: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const res = await client.forumActions.moderatePost( { cellId, postId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, unpost: async (cellId: string, postId: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const res = await client.forumActions.unmoderatePost( { cellId, postId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, comment: async (cellId: string, commentId: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const comment = content.comments.find(c => c.id === commentId); const res = await client.forumActions.moderateComment( { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, uncomment: async (cellId: string, commentId: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const comment = content.comments.find(c => c.id === commentId); const res = await client.forumActions.unmoderateComment( { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, user: async (cellId: string, userAddress: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const res = await client.forumActions.moderateUser( { cellId, userAddress, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, unuser: async (cellId: string, userAddress: string, reason?: string) => { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); const res = await client.forumActions.unmoderateUser( { cellId, userAddress, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); return res.data ?? false; }, }), [client, session.currentUser, content.cells]); const togglePostBookmark = React.useCallback(async (post: Post, cellId?: string): Promise => { const address = session.currentUser?.address; if (!address) return false; const added = await BookmarkService.togglePostBookmark(post, address, cellId); const updated = await client.database.getUserBookmarks(address); setOpchanState(prev => ({ ...prev, content: { ...prev.content, bookmarks: updated } })); return added; }, [client, session.currentUser?.address]); const toggleCommentBookmark = React.useCallback(async (comment: Comment, postId?: string): Promise => { const address = session.currentUser?.address; if (!address) return false; const added = await BookmarkService.toggleCommentBookmark(comment, address, postId); const updated = await client.database.getUserBookmarks(address); setOpchanState(prev => ({ ...prev, content: { ...prev.content, bookmarks: updated } })); return added; }, [client, session.currentUser?.address]); const removeBookmark = React.useCallback(async (bookmarkId: string): Promise => { const address = session.currentUser?.address; if (!address) return; const [typeStr, targetId] = bookmarkId.split(':'); const type = typeStr === 'post' ? BookmarkType.POST : BookmarkType.COMMENT; await BookmarkService.removeBookmark(type, targetId); const updated = await client.database.getUserBookmarks(address); setOpchanState(prev => ({ ...prev, content: { ...prev.content, bookmarks: updated } })); }, [client, session.currentUser?.address]); const clearAllBookmarks = React.useCallback(async (): Promise => { const address = session.currentUser?.address; if (!address) return; await BookmarkService.clearUserBookmarks(address); const updated = await client.database.getUserBookmarks(address); setOpchanState(prev => ({ ...prev, content: { ...prev.content, bookmarks: updated } })); }, [client, session.currentUser?.address]); const refresh = React.useCallback(async () => { // Minimal refresh: re-reflect cache; network refresh is via useNetwork reflectCache(client); }, [client]); return { // data cells: content.cells, posts: content.posts, comments: content.comments, bookmarks: content.bookmarks, postsByCell, commentsByPost, cellsWithStats, userVerificationStatus, pending: { isPending: (id?: string) => (id ? client.database.isPending(id) : false), onChange: (cb: () => void) => client.database.onPendingChange(cb), }, lastSync: content.lastSync, // actions createCell, createPost, createComment, vote, moderate, togglePostBookmark, toggleCommentBookmark, removeBookmark, clearAllBookmarks, refresh, } as const; }