From 63bbdde5e2dd048c0ab8e27283dfcfa8d0dbdd53 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Fri, 6 Jun 2025 16:45:14 +0530 Subject: [PATCH] feat(moderation): cell admin can mark users as moderated in a cell\n\n- Adds user moderation to ModerateMessage and transformers\n- Cell admin can moderate a user, hiding all their posts/comments in that cell for others\n- UI: admin-only 'Moderate User' button for each post/comment author (except admin themselves)\n- Fully coherent with OpChan PoC proposal moderation requirements --- src/components/PostDetail.tsx | 15 ++++++- src/components/PostList.tsx | 14 +++++- src/contexts/ForumContext.tsx | 71 +++++++++++++++++++++++++++++- src/contexts/forum/actions.ts | 51 +++++++++++++++++++++ src/contexts/forum/transformers.ts | 35 ++++++++++----- src/lib/waku/types.ts | 4 +- 6 files changed, 173 insertions(+), 17 deletions(-) diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx index 061de25..82e8a93 100644 --- a/src/components/PostDetail.tsx +++ b/src/components/PostDetail.tsx @@ -25,7 +25,9 @@ const PostDetail = () => { isPostingComment, isVoting, isRefreshing, - moderateComment + refreshData, + moderateComment, + moderateUser } = useForum(); const { currentUser, isAuthenticated, verificationStatus } = useAuth(); const [newComment, setNewComment] = useState(''); @@ -107,6 +109,12 @@ const PostDetail = () => { await moderateComment(cell.id, commentId, reason, cell.signature); }; + const handleModerateUser = async (userAddress: string) => { + if (!cell) return; + const reason = window.prompt('Reason for moderating this user? (optional)') || undefined; + await moderateUser(cell.id, userAddress, reason, cell.signature); + }; + return (
@@ -254,6 +262,11 @@ const PostDetail = () => { Moderate )} + {isCellAdmin && comment.authorAddress !== cell.signature && ( + + )} {comment.moderated && ( [Moderated] )} diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx index 9583300..8a671cd 100644 --- a/src/components/PostList.tsx +++ b/src/components/PostList.tsx @@ -25,7 +25,8 @@ const PostList = () => { votePost, isVoting, posts, - moderatePost + moderatePost, + moderateUser } = useForum(); const { isAuthenticated, currentUser, verificationStatus } = useAuth(); const [newPostTitle, setNewPostTitle] = useState(''); @@ -121,6 +122,12 @@ const PostList = () => { await moderatePost(cell.id, postId, reason, cell.signature); }; + const handleModerateUser = async (userAddress: string) => { + const reason = window.prompt('Reason for moderating this user? (optional)') || undefined; + if (!cell) return; + await moderateUser(cell.id, userAddress, reason, cell.signature); + }; + return (
@@ -259,6 +266,11 @@ const PostList = () => { Moderate )} + {isCellAdmin && post.authorAddress !== cell.signature && ( + + )} {post.moderated && ( [Moderated] )} diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 5f46cac..909ef7b 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -8,7 +8,8 @@ import { vote, createCell, moderatePost, - moderateComment + moderateComment, + moderateUser } from './forum/actions'; import { setupPeriodicQueries, @@ -53,6 +54,12 @@ interface ForumContextType { reason: string | undefined, cellOwner: string ) => Promise; + moderateUser: ( + cellId: string, + userAddress: string, + reason: string | undefined, + cellOwner: string + ) => Promise; } const ForumContext = createContext(undefined); @@ -232,6 +239,63 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { return result; }; + const handleModeratePost = async ( + cellId: string, + postId: string, + reason: string | undefined, + cellOwner: string + ) => { + return moderatePost( + cellId, + postId, + reason, + currentUser, + isAuthenticated, + cellOwner, + toast, + updateStateFromCache, + messageSigning + ); + }; + + const handleModerateComment = async ( + cellId: string, + commentId: string, + reason: string | undefined, + cellOwner: string + ) => { + return moderateComment( + cellId, + commentId, + reason, + currentUser, + isAuthenticated, + cellOwner, + toast, + updateStateFromCache, + messageSigning + ); + }; + + const handleModerateUser = async ( + cellId: string, + userAddress: string, + reason: string | undefined, + cellOwner: string + ) => { + return moderateUser( + cellId, + userAddress, + reason, + currentUser, + isAuthenticated, + cellOwner, + toast, + updateStateFromCache, + messageSigning + ); + }; + return ( {children} diff --git a/src/contexts/forum/actions.ts b/src/contexts/forum/actions.ts index 0923b18..65ab831 100644 --- a/src/contexts/forum/actions.ts +++ b/src/contexts/forum/actions.ts @@ -468,4 +468,55 @@ export const moderateComment = async ( }); return false; } +}; + +export const moderateUser = async ( + cellId: string, + userAddress: string, + reason: string | undefined, + currentUser: User | null, + isAuthenticated: boolean, + cellOwner: string, + toast: ToastFunction, + updateStateFromCache: () => void, + messageSigning?: MessageSigning +): Promise => { + if (!isAuthenticated || !currentUser) { + toast({ + title: "Authentication Required", + description: "You need to verify Ordinal ownership to moderate users.", + variant: "destructive", + }); + return false; + } + if (currentUser.address !== cellOwner) { + toast({ + title: "Not Authorized", + description: "Only the cell admin can moderate users.", + variant: "destructive", + }); + return false; + } + const message: ModerateMessage = { + type: MessageType.MODERATE, + cellId, + targetType: 'user', + targetId: userAddress, + reason, + author: currentUser.address, + timestamp: Date.now(), + signature: '', + browserPubKey: currentUser.browserPubKey, + }; + const sent = await signAndSendMessage(message, currentUser, messageSigning!, toast); + if (sent) { + updateStateFromCache(); + toast({ + title: "User Moderated", + description: `User ${userAddress} has been moderated in this cell.`, + variant: "default", + }); + return true; + } + return false; }; \ No newline at end of file diff --git a/src/contexts/forum/transformers.ts b/src/contexts/forum/transformers.ts index 584a69f..dbc20d7 100644 --- a/src/contexts/forum/transformers.ts +++ b/src/contexts/forum/transformers.ts @@ -47,8 +47,15 @@ export const transformPost = ( const upvotes = filteredVotes.filter(vote => vote.value === 1); const downvotes = filteredVotes.filter(vote => vote.value === -1); + // Check for post moderation const modMsg = messageManager.messageCache.moderations[postMessage.id]; - const isModerated = !!modMsg && modMsg.targetType === 'post'; + const isPostModerated = !!modMsg && modMsg.targetType === 'post'; + + // Check for user moderation in this cell + const userModMsg = Object.values(messageManager.messageCache.moderations).find( + m => m.targetType === 'user' && m.cellId === postMessage.cellId && m.targetId === postMessage.author + ); + const isUserModerated = !!userModMsg; return { id: postMessage.id, @@ -61,10 +68,10 @@ export const transformPost = ( downvotes: downvotes, signature: postMessage.signature, browserPubKey: postMessage.browserPubKey, - moderated: isModerated, - moderatedBy: isModerated ? modMsg.author : undefined, - moderationReason: isModerated ? modMsg.reason : undefined, - moderationTimestamp: isModerated ? modMsg.timestamp : undefined, + moderated: isPostModerated || isUserModerated, + moderatedBy: isPostModerated ? modMsg.author : isUserModerated ? userModMsg!.author : undefined, + moderationReason: isPostModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined, + moderationTimestamp: isPostModerated ? modMsg.timestamp : isUserModerated ? userModMsg!.timestamp : undefined, }; }; @@ -92,9 +99,15 @@ export const transformComment = ( const upvotes = filteredVotes.filter(vote => vote.value === 1); const downvotes = filteredVotes.filter(vote => vote.value === -1); - // Check for moderation + // Check for comment moderation const modMsg = messageManager.messageCache.moderations[commentMessage.id]; - const isModerated = !!modMsg && modMsg.targetType === 'comment'; + const isCommentModerated = !!modMsg && modMsg.targetType === 'comment'; + + // Check for user moderation in this cell + const userModMsg = Object.values(messageManager.messageCache.moderations).find( + m => m.targetType === 'user' && m.cellId === commentMessage.postId.split('-')[0] && m.targetId === commentMessage.author + ); + const isUserModerated = !!userModMsg; return { id: commentMessage.id, @@ -106,10 +119,10 @@ export const transformComment = ( downvotes: downvotes, signature: commentMessage.signature, browserPubKey: commentMessage.browserPubKey, - moderated: isModerated, - moderatedBy: isModerated ? modMsg.author : undefined, - moderationReason: isModerated ? modMsg.reason : undefined, - moderationTimestamp: isModerated ? modMsg.timestamp : undefined, + moderated: isCommentModerated || isUserModerated, + moderatedBy: isCommentModerated ? modMsg.author : isUserModerated ? userModMsg!.author : undefined, + moderationReason: isCommentModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined, + moderationTimestamp: isCommentModerated ? modMsg.timestamp : isUserModerated ? userModMsg!.timestamp : undefined, }; }; diff --git a/src/lib/waku/types.ts b/src/lib/waku/types.ts index 1d6e49f..c5db885 100644 --- a/src/lib/waku/types.ts +++ b/src/lib/waku/types.ts @@ -68,8 +68,8 @@ export interface VoteMessage extends BaseMessage { export interface ModerateMessage extends BaseMessage { type: MessageType.MODERATE; cellId: string; - targetType: 'post' | 'comment'; - targetId: string; + targetType: 'post' | 'comment' | 'user'; + targetId: string; // postId, commentId, or user address (for user moderation) reason?: string; }