mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
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:
parent
bf0ccda6ed
commit
63bbdde5e2
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user