OpChan/packages/react/src/v1/hooks/useContent.ts

313 lines
12 KiB
TypeScript
Raw Normal View History

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<typeof useClient>): 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<string, Post[]> = {};
for (const p of content.posts) {
(map[p.cellId] ||= []).push(p);
}
return map;
}, [content.posts]);
const commentsByPost = React.useMemo(() => {
const map: Record<string, Comment[]> = {};
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<string, { postCount: number; activeUsers: Set<string>; 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<string>(), 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<string>(), 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<string>(), 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<Cell | null> => {
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<Post | null> => {
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<Comment | null> => {
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<boolean> => {
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);
2025-10-03 19:06:11 +05:30
const comment = content.comments.find(c => c.id === commentId);
const res = await client.forumActions.moderateComment(
2025-10-03 19:06:11 +05:30
{ 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);
2025-10-03 19:06:11 +05:30
const comment = content.comments.find(c => c.id === commentId);
const res = await client.forumActions.unmoderateComment(
2025-10-03 19:06:11 +05:30
{ 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<boolean> => {
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<boolean> => {
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<void> => {
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<void> => {
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;
}