fix: moderation

This commit is contained in:
Danish Arora 2025-09-05 16:50:30 +05:30
parent d2a512211f
commit cad1dcb5b4
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 116 additions and 42 deletions

View File

@ -17,12 +17,19 @@ import {
MessageCircle, MessageCircle,
Send, Send,
Loader2, Loader2,
Shield,
UserX,
} from 'lucide-react'; } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { RelevanceIndicator } from './ui/relevance-indicator'; import { RelevanceIndicator } from './ui/relevance-indicator';
import { AuthorDisplay } from './ui/author-display'; import { AuthorDisplay } from './ui/author-display';
import { usePending, usePendingVote } from '@/hooks/usePending'; import { usePending, usePendingVote } from '@/hooks/usePending';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
// Extracted child component to respect Rules of Hooks // Extracted child component to respect Rules of Hooks
const PendingBadge: React.FC<{ id: string }> = ({ id }) => { const PendingBadge: React.FC<{ id: string }> = ({ id }) => {
@ -358,26 +365,40 @@ const PostDetail = () => {
</div> </div>
<p className="text-sm break-words">{comment.content}</p> <p className="text-sm break-words">{comment.content}</p>
{canModerate(cell?.id || '') && !comment.moderated && ( {canModerate(cell?.id || '') && !comment.moderated && (
<Button <Tooltip>
size="sm" <TooltipTrigger asChild>
variant="destructive" <Button
className="ml-2" size="icon"
onClick={() => handleModerateComment(comment.id)} variant="ghost"
> className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
Moderate onClick={() => handleModerateComment(comment.id)}
</Button> >
<Shield className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate comment</p>
</TooltipContent>
</Tooltip>
)} )}
{post.cell && {post.cell &&
canModerate(post.cell.id) && canModerate(post.cell.id) &&
comment.author !== post.author && ( comment.author !== post.author && (
<Button <Tooltip>
size="sm" <TooltipTrigger asChild>
variant="destructive" <Button
className="ml-2" size="icon"
onClick={() => handleModerateUser(comment.author)} variant="ghost"
> className="h-6 w-6 text-cyber-neutral hover:text-red-500"
Moderate User onClick={() => handleModerateUser(comment.author)}
</Button> >
<UserX className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate user</p>
</TooltipContent>
</Tooltip>
)} )}
{comment.moderated && ( {comment.moderated && (
<span className="ml-2 text-xs text-red-500"> <span className="ml-2 text-xs text-red-500">

View File

@ -21,11 +21,18 @@ import {
ArrowDown, ArrowDown,
RefreshCw, RefreshCw,
Eye, Eye,
Shield,
UserX,
} from 'lucide-react'; } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { CypherImage } from './ui/CypherImage'; import { CypherImage } from './ui/CypherImage';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { AuthorDisplay } from './ui/author-display'; import { AuthorDisplay } from './ui/author-display';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
const PostList = () => { const PostList = () => {
const { cellId } = useParams<{ cellId: string }>(); const { cellId } = useParams<{ cellId: string }>();
@ -312,24 +319,38 @@ const PostList = () => {
</div> </div>
</Link> </Link>
{canModerate(cell.id) && !post.moderated && ( {canModerate(cell.id) && !post.moderated && (
<Button <Tooltip>
size="sm" <TooltipTrigger asChild>
variant="destructive" <Button
className="ml-2" size="icon"
onClick={() => handleModerate(post.id)} variant="ghost"
> className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
Moderate onClick={() => handleModerate(post.id)}
</Button> >
<Shield className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate post</p>
</TooltipContent>
</Tooltip>
)} )}
{canModerate(cell.id) && post.author !== cell.signature && ( {canModerate(cell.id) && post.author !== cell.author && (
<Button <Tooltip>
size="sm" <TooltipTrigger asChild>
variant="destructive" <Button
className="ml-2" size="icon"
onClick={() => handleModerateUser(post.author)} variant="ghost"
> className="h-6 w-6 text-cyber-neutral hover:text-red-500"
Moderate User onClick={() => handleModerateUser(post.author)}
</Button> >
<UserX className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Moderate user</p>
</TooltipContent>
</Tooltip>
)} )}
{post.moderated && ( {post.moderated && (
<span className="ml-2 text-xs text-red-500"> <span className="ml-2 text-xs text-red-500">

View File

@ -291,7 +291,7 @@ export function useForumActions(): ForumActions {
const canModerate = const canModerate =
permissions.canModerate(cellId) && permissions.canModerate(cellId) &&
cell && cell &&
currentUser?.address === cell.signature; currentUser?.address === cell.author;
if (!canModerate) { if (!canModerate) {
toast({ toast({
@ -307,7 +307,7 @@ export function useForumActions(): ForumActions {
cellId, cellId,
postId, postId,
reason, reason,
cell.signature cell.author
); );
if (result) { if (result) {
toast({ toast({
@ -339,7 +339,7 @@ export function useForumActions(): ForumActions {
const canModerate = const canModerate =
permissions.canModerate(cellId) && permissions.canModerate(cellId) &&
cell && cell &&
currentUser?.address === cell.signature; currentUser?.address === cell.author;
if (!canModerate) { if (!canModerate) {
toast({ toast({
@ -355,7 +355,7 @@ export function useForumActions(): ForumActions {
cellId, cellId,
commentId, commentId,
reason, reason,
cell.signature cell.author
); );
if (result) { if (result) {
toast({ toast({
@ -387,7 +387,7 @@ export function useForumActions(): ForumActions {
const canModerate = const canModerate =
permissions.canModerate(cellId) && permissions.canModerate(cellId) &&
cell && cell &&
currentUser?.address === cell.signature; currentUser?.address === cell.author;
if (!canModerate) { if (!canModerate) {
toast({ toast({
@ -412,7 +412,7 @@ export function useForumActions(): ForumActions {
cellId, cellId,
userAddress, userAddress,
reason, reason,
cell.signature cell.author
); );
if (result) { if (result) {
toast({ toast({

View File

@ -6,8 +6,18 @@ import {
EDisplayPreference, EDisplayPreference,
} from '@/types/identity'; } from '@/types/identity';
import { VoteMessage, MessageType } from '@/types/waku'; import { VoteMessage, MessageType } from '@/types/waku';
import { DelegationProof } from '@/lib/delegation/types';
import { expect, describe, beforeEach, it } from 'vitest'; import { expect, describe, beforeEach, it } from 'vitest';
// Mock delegation proof for tests
const mockDelegationProof: DelegationProof = {
authMessage: 'I authorize browser key: test-key until 9999999999',
walletSignature: 'mock-signature',
expiryTimestamp: 9999999999,
walletAddress: 'test-address',
walletType: 'ethereum',
};
describe('RelevanceCalculator', () => { describe('RelevanceCalculator', () => {
let calculator: RelevanceCalculator; let calculator: RelevanceCalculator;
let mockUserVerificationStatus: UserVerificationStatus; let mockUserVerificationStatus: UserVerificationStatus;
@ -36,6 +46,7 @@ describe('RelevanceCalculator', () => {
downvotes: [], downvotes: [],
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const result = calculator.calculatePostScore( const result = calculator.calculatePostScore(
@ -64,6 +75,7 @@ describe('RelevanceCalculator', () => {
downvotes: [], downvotes: [],
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const result = calculator.calculatePostScore( const result = calculator.calculatePostScore(
@ -141,6 +153,7 @@ describe('RelevanceCalculator', () => {
moderated: true, moderated: true,
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const result = calculator.calculatePostScore( const result = calculator.calculatePostScore(
@ -168,6 +181,7 @@ describe('RelevanceCalculator', () => {
downvotes: [], downvotes: [],
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const votes: VoteMessage[] = [ const votes: VoteMessage[] = [
@ -180,6 +194,7 @@ describe('RelevanceCalculator', () => {
type: MessageType.VOTE, type: MessageType.VOTE,
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}, },
{ {
id: 'vote2', id: 'vote2',
@ -190,6 +205,7 @@ describe('RelevanceCalculator', () => {
type: MessageType.VOTE, type: MessageType.VOTE,
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}, },
]; ];
@ -206,6 +222,7 @@ describe('RelevanceCalculator', () => {
author: 'user1', author: 'user1',
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}, },
]; ];
@ -241,6 +258,7 @@ describe('RelevanceCalculator', () => {
downvotes: [], downvotes: [],
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const oldPost: Post = { const oldPost: Post = {
@ -256,6 +274,7 @@ describe('RelevanceCalculator', () => {
downvotes: [], downvotes: [],
signature: 'test', signature: 'test',
browserPubKey: 'test', browserPubKey: 'test',
delegationProof: mockDelegationProof,
}; };
const recentResult = calculator.calculatePostScore( const recentResult = calculator.calculatePostScore(

View File

@ -29,6 +29,7 @@ export const transformCell = async (
timestamp: cellMessage.timestamp, timestamp: cellMessage.timestamp,
signature: cellMessage.signature, signature: cellMessage.signature,
browserPubKey: cellMessage.browserPubKey, browserPubKey: cellMessage.browserPubKey,
delegationProof: cellMessage.delegationProof,
}; };
// Calculate relevance score if user verification status and posts are provided // Calculate relevance score if user verification status and posts are provided
@ -100,6 +101,7 @@ export const transformPost = async (
timestamp: postMessage.timestamp, timestamp: postMessage.timestamp,
signature: postMessage.signature, signature: postMessage.signature,
browserPubKey: postMessage.browserPubKey, browserPubKey: postMessage.browserPubKey,
delegationProof: postMessage.delegationProof,
upvotes, upvotes,
downvotes, downvotes,
moderated: isPostModerated || isUserModerated, moderated: isPostModerated || isUserModerated,
@ -193,12 +195,16 @@ export const transformComment = async (
const modMsg = messageManager.messageCache.moderations[commentMessage.id]; const modMsg = messageManager.messageCache.moderations[commentMessage.id];
const isCommentModerated = !!modMsg && modMsg.targetType === 'comment'; const isCommentModerated = !!modMsg && modMsg.targetType === 'comment';
// Find the post to get the correct cell ID
const parentPost = Object.values(messageManager.messageCache.posts).find(
post => post.id === commentMessage.postId
);
const userModMsg = Object.values( const userModMsg = Object.values(
messageManager.messageCache.moderations messageManager.messageCache.moderations
).find( ).find(
m => m =>
m.targetType === 'user' && m.targetType === 'user' &&
m.cellId === commentMessage.postId.split('-')[0] && m.cellId === parentPost?.cellId &&
m.targetId === commentMessage.author m.targetId === commentMessage.author
); );
const isUserModerated = !!userModMsg; const isUserModerated = !!userModMsg;
@ -213,6 +219,7 @@ export const transformComment = async (
timestamp: commentMessage.timestamp, timestamp: commentMessage.timestamp,
signature: commentMessage.signature, signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey, browserPubKey: commentMessage.browserPubKey,
delegationProof: commentMessage.delegationProof,
upvotes, upvotes,
downvotes, downvotes,
moderated: isCommentModerated || isUserModerated, moderated: isCommentModerated || isUserModerated,

View File

@ -239,6 +239,7 @@ export class UserIdentityService {
displayPreference, displayPreference,
signature: signedMessage.signature, signature: signedMessage.signature,
browserPubKey: signedMessage.browserPubKey, browserPubKey: signedMessage.browserPubKey,
delegationProof: signedMessage.delegationProof,
}; };
if (callSign && callSign.trim()) { if (callSign && callSign.trim()) {
profileMessage.callSign = callSign.trim(); profileMessage.callSign = callSign.trim();

View File

@ -5,6 +5,7 @@ import {
PostMessage, PostMessage,
CommentMessage, CommentMessage,
VoteMessage, VoteMessage,
ModerateMessage,
} from '../../types/waku'; } from '../../types/waku';
import { CONTENT_TOPIC } from './constants'; import { CONTENT_TOPIC } from './constants';
import { OpchanMessage } from '@/types/forum'; import { OpchanMessage } from '@/types/forum';
@ -42,10 +43,12 @@ export class CodecManager {
return message as CommentMessage; return message as CommentMessage;
case MessageType.VOTE: case MessageType.VOTE:
return message as VoteMessage; return message as VoteMessage;
case MessageType.MODERATE:
return message as ModerateMessage;
case MessageType.USER_PROFILE_UPDATE: case MessageType.USER_PROFILE_UPDATE:
return message as UserProfileUpdateMessage; return message as UserProfileUpdateMessage;
default: default:
throw new Error(`Unknown message type: ${message}`); throw new Error(`Unknown message type: `, message);
} }
} }

View File

@ -111,7 +111,7 @@ export interface Comment extends CommentMessage {
export interface SignedMessage { export interface SignedMessage {
signature: string; signature: string;
browserPubKey: string; browserPubKey: string;
delegationProof?: DelegationProof; // Cryptographic proof that browser key was authorized delegationProof: DelegationProof; // Cryptographic proof that browser key was authorized - REQUIRED
} }
/** /**

View File

@ -1,4 +1,5 @@
import { EDisplayPreference, EVerificationStatus } from './identity'; import { EDisplayPreference, EVerificationStatus } from './identity';
import { DelegationProof } from '@/lib/delegation/types';
/** /**
* Message types for Waku communication * Message types for Waku communication
@ -28,6 +29,7 @@ export interface UnsignedBaseMessage {
export interface BaseMessage extends UnsignedBaseMessage { export interface BaseMessage extends UnsignedBaseMessage {
signature: string; // Message signature for verification signature: string; // Message signature for verification
browserPubKey: string; // Public key that signed the message browserPubKey: string; // Public key that signed the message
delegationProof: DelegationProof; // Cryptographic proof that browser key was authorized
} }
/** /**