mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-05 22:33:07 +00:00
fix: moderation
This commit is contained in:
parent
d2a512211f
commit
cad1dcb5b4
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user