mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-03 13:23:08 +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,
|
isPostingComment,
|
||||||
isVoting,
|
isVoting,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
moderateComment
|
refreshData,
|
||||||
|
moderateComment,
|
||||||
|
moderateUser
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
|
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
@ -107,6 +109,12 @@ const PostDetail = () => {
|
|||||||
await moderateComment(cell.id, commentId, reason, cell.signature);
|
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 (
|
return (
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@ -254,6 +262,11 @@ const PostDetail = () => {
|
|||||||
Moderate
|
Moderate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isCellAdmin && comment.authorAddress !== cell.signature && (
|
||||||
|
<Button size="sm" variant="destructive" className="ml-2" onClick={() => handleModerateUser(comment.authorAddress)}>
|
||||||
|
Moderate User
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{comment.moderated && (
|
{comment.moderated && (
|
||||||
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -25,7 +25,8 @@ const PostList = () => {
|
|||||||
votePost,
|
votePost,
|
||||||
isVoting,
|
isVoting,
|
||||||
posts,
|
posts,
|
||||||
moderatePost
|
moderatePost,
|
||||||
|
moderateUser
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
|
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
|
||||||
const [newPostTitle, setNewPostTitle] = useState('');
|
const [newPostTitle, setNewPostTitle] = useState('');
|
||||||
@ -121,6 +122,12 @@ const PostList = () => {
|
|||||||
await moderatePost(cell.id, postId, reason, cell.signature);
|
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 (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@ -259,6 +266,11 @@ const PostList = () => {
|
|||||||
Moderate
|
Moderate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isCellAdmin && post.authorAddress !== cell.signature && (
|
||||||
|
<Button size="sm" variant="destructive" className="ml-2" onClick={() => handleModerateUser(post.authorAddress)}>
|
||||||
|
Moderate User
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{post.moderated && (
|
{post.moderated && (
|
||||||
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import {
|
|||||||
vote,
|
vote,
|
||||||
createCell,
|
createCell,
|
||||||
moderatePost,
|
moderatePost,
|
||||||
moderateComment
|
moderateComment,
|
||||||
|
moderateUser
|
||||||
} from './forum/actions';
|
} from './forum/actions';
|
||||||
import {
|
import {
|
||||||
setupPeriodicQueries,
|
setupPeriodicQueries,
|
||||||
@ -53,6 +54,12 @@ interface ForumContextType {
|
|||||||
reason: string | undefined,
|
reason: string | undefined,
|
||||||
cellOwner: string
|
cellOwner: string
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
moderateUser: (
|
||||||
|
cellId: string,
|
||||||
|
userAddress: string,
|
||||||
|
reason: string | undefined,
|
||||||
|
cellOwner: string
|
||||||
|
) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
||||||
@ -232,6 +239,63 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return result;
|
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 (
|
return (
|
||||||
<ForumContext.Provider
|
<ForumContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -254,7 +318,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
votePost: handleVotePost,
|
votePost: handleVotePost,
|
||||||
voteComment: handleVoteComment,
|
voteComment: handleVoteComment,
|
||||||
createCell: handleCreateCell,
|
createCell: handleCreateCell,
|
||||||
refreshData: handleRefreshData
|
refreshData: handleRefreshData,
|
||||||
|
moderatePost: handleModeratePost,
|
||||||
|
moderateComment: handleModerateComment,
|
||||||
|
moderateUser: handleModerateUser
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -468,4 +468,55 @@ export const moderateComment = async (
|
|||||||
});
|
});
|
||||||
return false;
|
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 upvotes = filteredVotes.filter(vote => vote.value === 1);
|
||||||
const downvotes = 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 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 {
|
return {
|
||||||
id: postMessage.id,
|
id: postMessage.id,
|
||||||
@ -61,10 +68,10 @@ export const transformPost = (
|
|||||||
downvotes: downvotes,
|
downvotes: downvotes,
|
||||||
signature: postMessage.signature,
|
signature: postMessage.signature,
|
||||||
browserPubKey: postMessage.browserPubKey,
|
browserPubKey: postMessage.browserPubKey,
|
||||||
moderated: isModerated,
|
moderated: isPostModerated || isUserModerated,
|
||||||
moderatedBy: isModerated ? modMsg.author : undefined,
|
moderatedBy: isPostModerated ? modMsg.author : isUserModerated ? userModMsg!.author : undefined,
|
||||||
moderationReason: isModerated ? modMsg.reason : undefined,
|
moderationReason: isPostModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined,
|
||||||
moderationTimestamp: isModerated ? modMsg.timestamp : 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 upvotes = filteredVotes.filter(vote => vote.value === 1);
|
||||||
const downvotes = 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 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 {
|
return {
|
||||||
id: commentMessage.id,
|
id: commentMessage.id,
|
||||||
@ -106,10 +119,10 @@ export const transformComment = (
|
|||||||
downvotes: downvotes,
|
downvotes: downvotes,
|
||||||
signature: commentMessage.signature,
|
signature: commentMessage.signature,
|
||||||
browserPubKey: commentMessage.browserPubKey,
|
browserPubKey: commentMessage.browserPubKey,
|
||||||
moderated: isModerated,
|
moderated: isCommentModerated || isUserModerated,
|
||||||
moderatedBy: isModerated ? modMsg.author : undefined,
|
moderatedBy: isCommentModerated ? modMsg.author : isUserModerated ? userModMsg!.author : undefined,
|
||||||
moderationReason: isModerated ? modMsg.reason : undefined,
|
moderationReason: isCommentModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined,
|
||||||
moderationTimestamp: isModerated ? modMsg.timestamp : undefined,
|
moderationTimestamp: isCommentModerated ? modMsg.timestamp : isUserModerated ? userModMsg!.timestamp : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -68,8 +68,8 @@ export interface VoteMessage extends BaseMessage {
|
|||||||
export interface ModerateMessage extends BaseMessage {
|
export interface ModerateMessage extends BaseMessage {
|
||||||
type: MessageType.MODERATE;
|
type: MessageType.MODERATE;
|
||||||
cellId: string;
|
cellId: string;
|
||||||
targetType: 'post' | 'comment';
|
targetType: 'post' | 'comment' | 'user';
|
||||||
targetId: string;
|
targetId: string; // postId, commentId, or user address (for user moderation)
|
||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user