mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-07 23:33:07 +00:00
chore: user cannot moderate themselves
This commit is contained in:
parent
f9863121ba
commit
3d3eafd626
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArrowUp, ArrowDown, Clock, Shield, UserX } from 'lucide-react';
|
import { ArrowUp, ArrowDown, Clock, MessageSquareX, UserX } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import type { Comment } from '@opchan/core';
|
import type { Comment } from '@opchan/core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -174,7 +174,7 @@ const CommentCard: React.FC<CommentCardProps> = ({
|
|||||||
className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
|
className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
|
||||||
onClick={() => onModerateComment(comment.id)}
|
onClick={() => onModerateComment(comment.id)}
|
||||||
>
|
>
|
||||||
<Shield className="h-3 w-3" />
|
<MessageSquareX className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@ -24,7 +24,11 @@ export interface LocalDatabaseCache {
|
|||||||
posts: PostCache;
|
posts: PostCache;
|
||||||
comments: CommentCache;
|
comments: CommentCache;
|
||||||
votes: VoteCache;
|
votes: VoteCache;
|
||||||
moderations: { [targetId: string]: ModerateMessage };
|
// Moderations keyed by composite key:
|
||||||
|
// - post: 'post:<postId>'
|
||||||
|
// - comment: 'comment:<commentId>'
|
||||||
|
// - user: '<cellId>:user:<address>'
|
||||||
|
moderations: { [key: string]: (ModerateMessage & { key?: string }) };
|
||||||
userIdentities: UserIdentityCache;
|
userIdentities: UserIdentityCache;
|
||||||
bookmarks: BookmarkCache;
|
bookmarks: BookmarkCache;
|
||||||
}
|
}
|
||||||
@ -212,13 +216,20 @@ export class LocalDatabase {
|
|||||||
}
|
}
|
||||||
case MessageType.MODERATE: {
|
case MessageType.MODERATE: {
|
||||||
const modMsg = message as ModerateMessage;
|
const modMsg = message as ModerateMessage;
|
||||||
if (
|
// Compose key:
|
||||||
!this.cache.moderations[modMsg.targetId] ||
|
// - post: `post:${postId}`
|
||||||
this.cache.moderations[modMsg.targetId]?.timestamp !==
|
// - comment: `comment:${commentId}`
|
||||||
modMsg.timestamp
|
// - user: `${cellId}:user:${address}` (per-cell user moderation)
|
||||||
) {
|
const key =
|
||||||
this.cache.moderations[modMsg.targetId] = modMsg;
|
modMsg.targetType === 'user'
|
||||||
this.put(STORE.MODERATIONS, modMsg);
|
? `${modMsg.cellId}:user:${modMsg.targetId}`
|
||||||
|
: `${modMsg.targetType}:${modMsg.targetId}`;
|
||||||
|
|
||||||
|
const existing = this.cache.moderations[key];
|
||||||
|
if (!existing || modMsg.timestamp > existing.timestamp) {
|
||||||
|
// Store in cache and persist with computed key
|
||||||
|
this.cache.moderations[key] = { ...(modMsg as ModerateMessage), key };
|
||||||
|
this.put(STORE.MODERATIONS, { ...(modMsg as ModerateMessage), key });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -267,7 +278,7 @@ export class LocalDatabase {
|
|||||||
PostMessage[],
|
PostMessage[],
|
||||||
CommentMessage[],
|
CommentMessage[],
|
||||||
(VoteMessage & { key: string })[],
|
(VoteMessage & { key: string })[],
|
||||||
ModerateMessage[],
|
(ModerateMessage & { key: string })[],
|
||||||
({ address: string } & UserIdentityCache[string])[],
|
({ address: string } & UserIdentityCache[string])[],
|
||||||
Bookmark[],
|
Bookmark[],
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
@ -275,7 +286,7 @@ export class LocalDatabase {
|
|||||||
this.getAllFromStore<PostMessage>(STORE.POSTS),
|
this.getAllFromStore<PostMessage>(STORE.POSTS),
|
||||||
this.getAllFromStore<CommentMessage>(STORE.COMMENTS),
|
this.getAllFromStore<CommentMessage>(STORE.COMMENTS),
|
||||||
this.getAllFromStore<VoteMessage & { key: string }>(STORE.VOTES),
|
this.getAllFromStore<VoteMessage & { key: string }>(STORE.VOTES),
|
||||||
this.getAllFromStore<ModerateMessage>(STORE.MODERATIONS),
|
this.getAllFromStore<ModerateMessage & { key: string }>(STORE.MODERATIONS),
|
||||||
this.getAllFromStore<{ address: string } & UserIdentityCache[string]>(
|
this.getAllFromStore<{ address: string } & UserIdentityCache[string]>(
|
||||||
STORE.USER_IDENTITIES
|
STORE.USER_IDENTITIES
|
||||||
),
|
),
|
||||||
@ -293,7 +304,7 @@ export class LocalDatabase {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.cache.moderations = Object.fromEntries(
|
this.cache.moderations = Object.fromEntries(
|
||||||
moderations.map(m => [m.targetId, m])
|
moderations.map(m => [m.key, m])
|
||||||
);
|
);
|
||||||
this.cache.userIdentities = Object.fromEntries(
|
this.cache.userIdentities = Object.fromEntries(
|
||||||
identities.map(u => {
|
identities.map(u => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export const DB_NAME = 'opchan-local';
|
export const DB_NAME = 'opchan-local';
|
||||||
export const DB_VERSION = 4;
|
export const DB_VERSION = 5;
|
||||||
|
|
||||||
export const STORE = {
|
export const STORE = {
|
||||||
CELLS: 'cells',
|
CELLS: 'cells',
|
||||||
@ -49,10 +49,12 @@ export function openLocalDB(): Promise<IDBDatabase> {
|
|||||||
// Votes are keyed by composite key `${targetId}:${author}`
|
// Votes are keyed by composite key `${targetId}:${author}`
|
||||||
db.createObjectStore(STORE.VOTES, { keyPath: 'key' });
|
db.createObjectStore(STORE.VOTES, { keyPath: 'key' });
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains(STORE.MODERATIONS)) {
|
// Moderations store: recreate with composite key support
|
||||||
// Moderations keyed by targetId
|
if (db.objectStoreNames.contains(STORE.MODERATIONS)) {
|
||||||
db.createObjectStore(STORE.MODERATIONS, { keyPath: 'targetId' });
|
db.deleteObjectStore(STORE.MODERATIONS);
|
||||||
}
|
}
|
||||||
|
// Moderations keyed by computed 'key' (e.g., 'post:postId', 'comment:commentId', 'cellId:user:userAddress')
|
||||||
|
db.createObjectStore(STORE.MODERATIONS, { keyPath: 'key' });
|
||||||
if (!db.objectStoreNames.contains(STORE.USER_IDENTITIES)) {
|
if (!db.objectStoreNames.contains(STORE.USER_IDENTITIES)) {
|
||||||
// User identities keyed by address
|
// User identities keyed by address
|
||||||
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });
|
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });
|
||||||
|
|||||||
@ -431,6 +431,7 @@ export class ForumActions {
|
|||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
cellOwner,
|
cellOwner,
|
||||||
|
commentAuthor,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!isAuthenticated || !currentUser) {
|
if (!isAuthenticated || !currentUser) {
|
||||||
@ -446,6 +447,12 @@ export class ForumActions {
|
|||||||
error: 'Not authorized. Only the cell admin can moderate comments.',
|
error: 'Not authorized. Only the cell admin can moderate comments.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (currentUser.address === commentAuthor) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'You cannot moderate your own comments.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unsignedMod: UnsignedModerateMessage = {
|
const unsignedMod: UnsignedModerateMessage = {
|
||||||
@ -520,6 +527,12 @@ export class ForumActions {
|
|||||||
error: 'Not authorized. Only the cell admin can moderate users.',
|
error: 'Not authorized. Only the cell admin can moderate users.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (currentUser.address === userAddress) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'You cannot moderate yourself.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unsignedMod: UnsignedModerateMessage = {
|
const unsignedMod: UnsignedModerateMessage = {
|
||||||
@ -647,6 +660,7 @@ export class ForumActions {
|
|||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
cellOwner,
|
cellOwner,
|
||||||
|
commentAuthor,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!isAuthenticated || !currentUser) {
|
if (!isAuthenticated || !currentUser) {
|
||||||
@ -662,6 +676,12 @@ export class ForumActions {
|
|||||||
error: 'Not authorized. Only the cell admin can unmoderate comments.',
|
error: 'Not authorized. Only the cell admin can unmoderate comments.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (currentUser.address === commentAuthor) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'You cannot unmoderate your own comments.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unsignedMod: UnsignedModerateMessage = {
|
const unsignedMod: UnsignedModerateMessage = {
|
||||||
@ -736,6 +756,12 @@ export class ForumActions {
|
|||||||
error: 'Not authorized. Only the cell admin can unmoderate users.',
|
error: 'Not authorized. Only the cell admin can unmoderate users.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (currentUser.address === userAddress) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'You cannot unmoderate yourself.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unsignedMod: UnsignedModerateMessage = {
|
const unsignedMod: UnsignedModerateMessage = {
|
||||||
@ -826,6 +852,7 @@ interface CommentModerationParams extends BaseActionParams {
|
|||||||
commentId: string;
|
commentId: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
cellOwner: string;
|
cellOwner: string;
|
||||||
|
commentAuthor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserModerationParams extends BaseActionParams {
|
interface UserModerationParams extends BaseActionParams {
|
||||||
|
|||||||
@ -18,27 +18,11 @@ export const transformCell = async (
|
|||||||
userVerificationStatus?: UserVerificationStatus,
|
userVerificationStatus?: UserVerificationStatus,
|
||||||
posts?: Post[]
|
posts?: Post[]
|
||||||
): Promise<Cell | null> => {
|
): Promise<Cell | null> => {
|
||||||
// Message validity already enforced upstream
|
|
||||||
|
|
||||||
const transformedCell: Cell = {
|
|
||||||
id: cellMessage.id,
|
|
||||||
type: cellMessage.type,
|
|
||||||
author: cellMessage.author,
|
|
||||||
name: cellMessage.name,
|
|
||||||
description: cellMessage.description,
|
|
||||||
icon: cellMessage.icon || '',
|
|
||||||
timestamp: cellMessage.timestamp,
|
|
||||||
signature: cellMessage.signature,
|
|
||||||
browserPubKey: cellMessage.browserPubKey,
|
|
||||||
delegationProof: cellMessage.delegationProof,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate relevance score if user verification status and posts are provided
|
|
||||||
if (userVerificationStatus && posts) {
|
if (userVerificationStatus && posts) {
|
||||||
const relevanceCalculator = new RelevanceCalculator();
|
const relevanceCalculator = new RelevanceCalculator();
|
||||||
|
|
||||||
const relevanceResult = relevanceCalculator.calculateCellScore(
|
const relevanceResult = relevanceCalculator.calculateCellScore(
|
||||||
transformedCell,
|
cellMessage,
|
||||||
posts
|
posts
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -50,14 +34,14 @@ export const transformCell = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transformedCell,
|
...cellMessage,
|
||||||
relevanceScore: relevanceResult.score,
|
relevanceScore: relevanceResult.score,
|
||||||
activeMemberCount: activeMembers.size,
|
activeMemberCount: activeMembers.size,
|
||||||
relevanceDetails: relevanceResult.details,
|
relevanceDetails: relevanceResult.details,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformedCell;
|
return cellMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformPost = async (
|
export const transformPost = async (
|
||||||
@ -97,17 +81,8 @@ export const transformPost = async (
|
|||||||
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
|
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
|
||||||
|
|
||||||
const transformedPost: Post = {
|
const transformedPost: Post = {
|
||||||
id: postMessage.id,
|
|
||||||
type: postMessage.type,
|
|
||||||
author: postMessage.author,
|
|
||||||
cellId: postMessage.cellId,
|
|
||||||
authorAddress: postMessage.author,
|
authorAddress: postMessage.author,
|
||||||
title: postMessage.title,
|
...postMessage,
|
||||||
content: postMessage.content,
|
|
||||||
timestamp: postMessage.timestamp,
|
|
||||||
signature: postMessage.signature,
|
|
||||||
browserPubKey: postMessage.browserPubKey,
|
|
||||||
delegationProof: postMessage.delegationProof,
|
|
||||||
upvotes,
|
upvotes,
|
||||||
downvotes,
|
downvotes,
|
||||||
moderated: isPostModerated || isUserModerated,
|
moderated: isPostModerated || isUserModerated,
|
||||||
@ -223,16 +198,8 @@ export const transformComment = async (
|
|||||||
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
|
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
|
||||||
|
|
||||||
const transformedComment: Comment = {
|
const transformedComment: Comment = {
|
||||||
id: commentMessage.id,
|
...commentMessage,
|
||||||
type: commentMessage.type,
|
|
||||||
author: commentMessage.author,
|
|
||||||
postId: commentMessage.postId,
|
|
||||||
authorAddress: commentMessage.author,
|
authorAddress: commentMessage.author,
|
||||||
content: commentMessage.content,
|
|
||||||
timestamp: commentMessage.timestamp,
|
|
||||||
signature: commentMessage.signature,
|
|
||||||
browserPubKey: commentMessage.browserPubKey,
|
|
||||||
delegationProof: commentMessage.delegationProof,
|
|
||||||
upvotes,
|
upvotes,
|
||||||
downvotes,
|
downvotes,
|
||||||
moderated: isCommentModerated || isUserModerated,
|
moderated: isCommentModerated || isUserModerated,
|
||||||
|
|||||||
@ -193,8 +193,9 @@ export function useContent() {
|
|||||||
const currentUser = session.currentUser;
|
const currentUser = session.currentUser;
|
||||||
const isAuthenticated = Boolean(currentUser);
|
const isAuthenticated = Boolean(currentUser);
|
||||||
const cell = content.cells.find(c => c.id === cellId);
|
const cell = content.cells.find(c => c.id === cellId);
|
||||||
|
const comment = content.comments.find(c => c.id === commentId);
|
||||||
const res = await client.forumActions.moderateComment(
|
const res = await client.forumActions.moderateComment(
|
||||||
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' },
|
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' },
|
||||||
() => reflectCache(client)
|
() => reflectCache(client)
|
||||||
);
|
);
|
||||||
reflectCache(client);
|
reflectCache(client);
|
||||||
@ -204,8 +205,9 @@ export function useContent() {
|
|||||||
const currentUser = session.currentUser;
|
const currentUser = session.currentUser;
|
||||||
const isAuthenticated = Boolean(currentUser);
|
const isAuthenticated = Boolean(currentUser);
|
||||||
const cell = content.cells.find(c => c.id === cellId);
|
const cell = content.cells.find(c => c.id === cellId);
|
||||||
|
const comment = content.comments.find(c => c.id === commentId);
|
||||||
const res = await client.forumActions.unmoderateComment(
|
const res = await client.forumActions.unmoderateComment(
|
||||||
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' },
|
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' },
|
||||||
() => reflectCache(client)
|
() => reflectCache(client)
|
||||||
);
|
);
|
||||||
reflectCache(client);
|
reflectCache(client);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user