mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
fix: moderation
This commit is contained in:
parent
d2a512211f
commit
cad1dcb5b4
@ -17,12 +17,19 @@ import {
|
||||
MessageCircle,
|
||||
Send,
|
||||
Loader2,
|
||||
Shield,
|
||||
UserX,
|
||||
} from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
import { RelevanceIndicator } from './ui/relevance-indicator';
|
||||
import { AuthorDisplay } from './ui/author-display';
|
||||
import { usePending, usePendingVote } from '@/hooks/usePending';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
// Extracted child component to respect Rules of Hooks
|
||||
const PendingBadge: React.FC<{ id: string }> = ({ id }) => {
|
||||
@ -358,26 +365,40 @@ const PostDetail = () => {
|
||||
</div>
|
||||
<p className="text-sm break-words">{comment.content}</p>
|
||||
{canModerate(cell?.id || '') && !comment.moderated && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="ml-2"
|
||||
onClick={() => handleModerateComment(comment.id)}
|
||||
>
|
||||
Moderate
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
|
||||
onClick={() => handleModerateComment(comment.id)}
|
||||
>
|
||||
<Shield className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Moderate comment</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{post.cell &&
|
||||
canModerate(post.cell.id) &&
|
||||
comment.author !== post.author && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="ml-2"
|
||||
onClick={() => handleModerateUser(comment.author)}
|
||||
>
|
||||
Moderate User
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-cyber-neutral hover:text-red-500"
|
||||
onClick={() => handleModerateUser(comment.author)}
|
||||
>
|
||||
<UserX className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Moderate user</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{comment.moderated && (
|
||||
<span className="ml-2 text-xs text-red-500">
|
||||
|
||||
@ -21,11 +21,18 @@ import {
|
||||
ArrowDown,
|
||||
RefreshCw,
|
||||
Eye,
|
||||
Shield,
|
||||
UserX,
|
||||
} from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { CypherImage } from './ui/CypherImage';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { AuthorDisplay } from './ui/author-display';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
const PostList = () => {
|
||||
const { cellId } = useParams<{ cellId: string }>();
|
||||
@ -312,24 +319,38 @@ const PostList = () => {
|
||||
</div>
|
||||
</Link>
|
||||
{canModerate(cell.id) && !post.moderated && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="ml-2"
|
||||
onClick={() => handleModerate(post.id)}
|
||||
>
|
||||
Moderate
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
|
||||
onClick={() => handleModerate(post.id)}
|
||||
>
|
||||
<Shield className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Moderate post</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canModerate(cell.id) && post.author !== cell.signature && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="ml-2"
|
||||
onClick={() => handleModerateUser(post.author)}
|
||||
>
|
||||
Moderate User
|
||||
</Button>
|
||||
{canModerate(cell.id) && post.author !== cell.author && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-cyber-neutral hover:text-red-500"
|
||||
onClick={() => handleModerateUser(post.author)}
|
||||
>
|
||||
<UserX className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Moderate user</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{post.moderated && (
|
||||
<span className="ml-2 text-xs text-red-500">
|
||||
|
||||
@ -291,7 +291,7 @@ export function useForumActions(): ForumActions {
|
||||
const canModerate =
|
||||
permissions.canModerate(cellId) &&
|
||||
cell &&
|
||||
currentUser?.address === cell.signature;
|
||||
currentUser?.address === cell.author;
|
||||
|
||||
if (!canModerate) {
|
||||
toast({
|
||||
@ -307,7 +307,7 @@ export function useForumActions(): ForumActions {
|
||||
cellId,
|
||||
postId,
|
||||
reason,
|
||||
cell.signature
|
||||
cell.author
|
||||
);
|
||||
if (result) {
|
||||
toast({
|
||||
@ -339,7 +339,7 @@ export function useForumActions(): ForumActions {
|
||||
const canModerate =
|
||||
permissions.canModerate(cellId) &&
|
||||
cell &&
|
||||
currentUser?.address === cell.signature;
|
||||
currentUser?.address === cell.author;
|
||||
|
||||
if (!canModerate) {
|
||||
toast({
|
||||
@ -355,7 +355,7 @@ export function useForumActions(): ForumActions {
|
||||
cellId,
|
||||
commentId,
|
||||
reason,
|
||||
cell.signature
|
||||
cell.author
|
||||
);
|
||||
if (result) {
|
||||
toast({
|
||||
@ -387,7 +387,7 @@ export function useForumActions(): ForumActions {
|
||||
const canModerate =
|
||||
permissions.canModerate(cellId) &&
|
||||
cell &&
|
||||
currentUser?.address === cell.signature;
|
||||
currentUser?.address === cell.author;
|
||||
|
||||
if (!canModerate) {
|
||||
toast({
|
||||
@ -412,7 +412,7 @@ export function useForumActions(): ForumActions {
|
||||
cellId,
|
||||
userAddress,
|
||||
reason,
|
||||
cell.signature
|
||||
cell.author
|
||||
);
|
||||
if (result) {
|
||||
toast({
|
||||
|
||||
@ -6,8 +6,18 @@ import {
|
||||
EDisplayPreference,
|
||||
} from '@/types/identity';
|
||||
import { VoteMessage, MessageType } from '@/types/waku';
|
||||
import { DelegationProof } from '@/lib/delegation/types';
|
||||
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', () => {
|
||||
let calculator: RelevanceCalculator;
|
||||
let mockUserVerificationStatus: UserVerificationStatus;
|
||||
@ -36,6 +46,7 @@ describe('RelevanceCalculator', () => {
|
||||
downvotes: [],
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const result = calculator.calculatePostScore(
|
||||
@ -64,6 +75,7 @@ describe('RelevanceCalculator', () => {
|
||||
downvotes: [],
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const result = calculator.calculatePostScore(
|
||||
@ -141,6 +153,7 @@ describe('RelevanceCalculator', () => {
|
||||
moderated: true,
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const result = calculator.calculatePostScore(
|
||||
@ -168,6 +181,7 @@ describe('RelevanceCalculator', () => {
|
||||
downvotes: [],
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const votes: VoteMessage[] = [
|
||||
@ -180,6 +194,7 @@ describe('RelevanceCalculator', () => {
|
||||
type: MessageType.VOTE,
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
},
|
||||
{
|
||||
id: 'vote2',
|
||||
@ -190,6 +205,7 @@ describe('RelevanceCalculator', () => {
|
||||
type: MessageType.VOTE,
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
},
|
||||
];
|
||||
|
||||
@ -206,6 +222,7 @@ describe('RelevanceCalculator', () => {
|
||||
author: 'user1',
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
},
|
||||
];
|
||||
|
||||
@ -241,6 +258,7 @@ describe('RelevanceCalculator', () => {
|
||||
downvotes: [],
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const oldPost: Post = {
|
||||
@ -256,6 +274,7 @@ describe('RelevanceCalculator', () => {
|
||||
downvotes: [],
|
||||
signature: 'test',
|
||||
browserPubKey: 'test',
|
||||
delegationProof: mockDelegationProof,
|
||||
};
|
||||
|
||||
const recentResult = calculator.calculatePostScore(
|
||||
|
||||
@ -29,6 +29,7 @@ export const transformCell = async (
|
||||
timestamp: cellMessage.timestamp,
|
||||
signature: cellMessage.signature,
|
||||
browserPubKey: cellMessage.browserPubKey,
|
||||
delegationProof: cellMessage.delegationProof,
|
||||
};
|
||||
|
||||
// Calculate relevance score if user verification status and posts are provided
|
||||
@ -100,6 +101,7 @@ export const transformPost = async (
|
||||
timestamp: postMessage.timestamp,
|
||||
signature: postMessage.signature,
|
||||
browserPubKey: postMessage.browserPubKey,
|
||||
delegationProof: postMessage.delegationProof,
|
||||
upvotes,
|
||||
downvotes,
|
||||
moderated: isPostModerated || isUserModerated,
|
||||
@ -193,12 +195,16 @@ export const transformComment = async (
|
||||
|
||||
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
|
||||
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(
|
||||
messageManager.messageCache.moderations
|
||||
).find(
|
||||
m =>
|
||||
m.targetType === 'user' &&
|
||||
m.cellId === commentMessage.postId.split('-')[0] &&
|
||||
m.cellId === parentPost?.cellId &&
|
||||
m.targetId === commentMessage.author
|
||||
);
|
||||
const isUserModerated = !!userModMsg;
|
||||
@ -213,6 +219,7 @@ export const transformComment = async (
|
||||
timestamp: commentMessage.timestamp,
|
||||
signature: commentMessage.signature,
|
||||
browserPubKey: commentMessage.browserPubKey,
|
||||
delegationProof: commentMessage.delegationProof,
|
||||
upvotes,
|
||||
downvotes,
|
||||
moderated: isCommentModerated || isUserModerated,
|
||||
|
||||
@ -239,6 +239,7 @@ export class UserIdentityService {
|
||||
displayPreference,
|
||||
signature: signedMessage.signature,
|
||||
browserPubKey: signedMessage.browserPubKey,
|
||||
delegationProof: signedMessage.delegationProof,
|
||||
};
|
||||
if (callSign && callSign.trim()) {
|
||||
profileMessage.callSign = callSign.trim();
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
PostMessage,
|
||||
CommentMessage,
|
||||
VoteMessage,
|
||||
ModerateMessage,
|
||||
} from '../../types/waku';
|
||||
import { CONTENT_TOPIC } from './constants';
|
||||
import { OpchanMessage } from '@/types/forum';
|
||||
@ -42,10 +43,12 @@ export class CodecManager {
|
||||
return message as CommentMessage;
|
||||
case MessageType.VOTE:
|
||||
return message as VoteMessage;
|
||||
case MessageType.MODERATE:
|
||||
return message as ModerateMessage;
|
||||
case MessageType.USER_PROFILE_UPDATE:
|
||||
return message as UserProfileUpdateMessage;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${message}`);
|
||||
throw new Error(`Unknown message type: `, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ export interface Comment extends CommentMessage {
|
||||
export interface SignedMessage {
|
||||
signature: string;
|
||||
browserPubKey: string;
|
||||
delegationProof?: DelegationProof; // Cryptographic proof that browser key was authorized
|
||||
delegationProof: DelegationProof; // Cryptographic proof that browser key was authorized - REQUIRED
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { EDisplayPreference, EVerificationStatus } from './identity';
|
||||
import { DelegationProof } from '@/lib/delegation/types';
|
||||
|
||||
/**
|
||||
* Message types for Waku communication
|
||||
@ -28,6 +29,7 @@ export interface UnsignedBaseMessage {
|
||||
export interface BaseMessage extends UnsignedBaseMessage {
|
||||
signature: string; // Message signature for verification
|
||||
browserPubKey: string; // Public key that signed the message
|
||||
delegationProof: DelegationProof; // Cryptographic proof that browser key was authorized
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user