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

This commit is contained in:
Danish Arora 2025-06-06 16:45:14 +05:30
parent bf0ccda6ed
commit 63bbdde5e2
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 173 additions and 17 deletions

View File

@ -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 (
<div className="container mx-auto px-4 py-6">
<div className="mb-6">
@ -254,6 +262,11 @@ const PostDetail = () => {
Moderate
</Button>
)}
{isCellAdmin && comment.authorAddress !== cell.signature && (
<Button size="sm" variant="destructive" className="ml-2" onClick={() => handleModerateUser(comment.authorAddress)}>
Moderate User
</Button>
)}
{comment.moderated && (
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
)}

View File

@ -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 (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
@ -259,6 +266,11 @@ const PostList = () => {
Moderate
</Button>
)}
{isCellAdmin && post.authorAddress !== cell.signature && (
<Button size="sm" variant="destructive" className="ml-2" onClick={() => handleModerateUser(post.authorAddress)}>
Moderate User
</Button>
)}
{post.moderated && (
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
)}

View File

@ -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<boolean>;
moderateUser: (
cellId: string,
userAddress: string,
reason: string | undefined,
cellOwner: string
) => Promise<boolean>;
}
const ForumContext = createContext<ForumContextType | undefined>(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 (
<ForumContext.Provider
value={{
@ -254,7 +318,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
votePost: handleVotePost,
voteComment: handleVoteComment,
createCell: handleCreateCell,
refreshData: handleRefreshData
refreshData: handleRefreshData,
moderatePost: handleModeratePost,
moderateComment: handleModerateComment,
moderateUser: handleModerateUser
}}
>
{children}

View File

@ -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<boolean> => {
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;
};

View File

@ -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,
};
};

View File

@ -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;
}