mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-09 00:03:12 +00:00
feat(moderation): cell admin can mark posts and comments as moderated - Adds ModerateMessage type and Waku support - Cell admin can moderate posts/comments, which are then hidden for others - Moderation is local, decentralized, and reversible - UI: admin-only 'Moderate' button, [Moderated] label, filtering - Fully coherent with OpChan PoC proposal moderation requirements
This commit is contained in:
parent
0eff3031ad
commit
a4fc3c1aaf
@ -4,3 +4,5 @@
|
|||||||
- [x] replace mock Ordinal verification (API)
|
- [x] replace mock Ordinal verification (API)
|
||||||
- [ ] figure out using actual icons for cells
|
- [ ] figure out using actual icons for cells
|
||||||
- [ ] store message cache in indexedDB -- make app local-first (update from/to Waku when available)
|
- [ ] store message cache in indexedDB -- make app local-first (update from/to Waku when available)
|
||||||
|
- [ ] moderation
|
||||||
|
- [ ] admins can "moderate" comments/posts
|
||||||
@ -25,7 +25,7 @@ const PostDetail = () => {
|
|||||||
isPostingComment,
|
isPostingComment,
|
||||||
isVoting,
|
isVoting,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
refreshData
|
moderateComment
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
|
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
@ -58,6 +58,11 @@ const PostDetail = () => {
|
|||||||
const cell = getCellById(post.cellId);
|
const cell = getCellById(post.cellId);
|
||||||
const postComments = getCommentsByPost(post.id);
|
const postComments = getCommentsByPost(post.id);
|
||||||
|
|
||||||
|
const isCellAdmin = currentUser && cell && currentUser.address === cell.signature;
|
||||||
|
const visibleComments = isCellAdmin
|
||||||
|
? postComments
|
||||||
|
: postComments.filter(comment => !comment.moderated);
|
||||||
|
|
||||||
const handleCreateComment = async (e: React.FormEvent) => {
|
const handleCreateComment = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -96,6 +101,12 @@ const PostDetail = () => {
|
|||||||
return `https://api.dicebear.com/7.x/identicon/svg?seed=${address}`;
|
return `https://api.dicebear.com/7.x/identicon/svg?seed=${address}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModerateComment = async (commentId: string) => {
|
||||||
|
const reason = window.prompt('Enter a reason for moderation (optional):') || undefined;
|
||||||
|
if (!cell) return;
|
||||||
|
await moderateComment(cell.id, commentId, 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">
|
||||||
@ -199,7 +210,7 @@ const PostDetail = () => {
|
|||||||
<p>No comments yet</p>
|
<p>No comments yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
postComments.map(comment => (
|
visibleComments.map(comment => (
|
||||||
<div key={comment.id} className="comment-card" id={`comment-${comment.id}`}>
|
<div key={comment.id} className="comment-card" id={`comment-${comment.id}`}>
|
||||||
<div className="flex gap-2 items-start">
|
<div className="flex gap-2 items-start">
|
||||||
<div className="flex flex-col items-center w-5 pt-0.5">
|
<div className="flex flex-col items-center w-5 pt-0.5">
|
||||||
@ -238,6 +249,14 @@ const PostDetail = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm break-words">{comment.content}</p>
|
<p className="text-sm break-words">{comment.content}</p>
|
||||||
|
{isCellAdmin && !comment.moderated && (
|
||||||
|
<Button size="sm" variant="destructive" className="ml-2" onClick={() => handleModerateComment(comment.id)}>
|
||||||
|
Moderate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{comment.moderated && (
|
||||||
|
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -24,7 +24,8 @@ const PostList = () => {
|
|||||||
refreshData,
|
refreshData,
|
||||||
votePost,
|
votePost,
|
||||||
isVoting,
|
isVoting,
|
||||||
posts
|
posts,
|
||||||
|
moderatePost
|
||||||
} = useForum();
|
} = useForum();
|
||||||
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
|
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
|
||||||
const [newPostTitle, setNewPostTitle] = useState('');
|
const [newPostTitle, setNewPostTitle] = useState('');
|
||||||
@ -108,6 +109,18 @@ const PostList = () => {
|
|||||||
return votes.some(vote => vote.author === currentUser.address);
|
return votes.some(vote => vote.author === currentUser.address);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only show unmoderated posts, or all if admin
|
||||||
|
const isCellAdmin = currentUser && cell && currentUser.address === cell.signature;
|
||||||
|
const visiblePosts = isCellAdmin
|
||||||
|
? cellPosts
|
||||||
|
: cellPosts.filter(post => !post.moderated);
|
||||||
|
|
||||||
|
const handleModerate = async (postId: string) => {
|
||||||
|
const reason = window.prompt('Enter a reason for moderation (optional):') || undefined;
|
||||||
|
if (!cell) return;
|
||||||
|
await moderatePost(cell.id, postId, 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">
|
||||||
@ -209,7 +222,7 @@ const PostList = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
cellPosts.map(post => (
|
visiblePosts.map(post => (
|
||||||
<div key={post.id} className="post-card p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 hover:bg-cyber-muted/30 transition duration-200">
|
<div key={post.id} className="post-card p-4 border border-cyber-muted rounded-sm bg-cyber-muted/20 hover:bg-cyber-muted/30 transition duration-200">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@ -241,6 +254,14 @@ const PostList = () => {
|
|||||||
<span>by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
|
<span>by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
{isCellAdmin && !post.moderated && (
|
||||||
|
<Button size="xs" variant="destructive" className="ml-2" onClick={() => handleModerate(post.id)}>
|
||||||
|
Moderate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{post.moderated && (
|
||||||
|
<span className="ml-2 text-xs text-red-500">[Moderated]</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import {
|
|||||||
createPost,
|
createPost,
|
||||||
createComment,
|
createComment,
|
||||||
vote,
|
vote,
|
||||||
createCell
|
createCell,
|
||||||
|
moderatePost,
|
||||||
|
moderateComment
|
||||||
} from './forum/actions';
|
} from './forum/actions';
|
||||||
import {
|
import {
|
||||||
setupPeriodicQueries,
|
setupPeriodicQueries,
|
||||||
@ -39,6 +41,18 @@ interface ForumContextType {
|
|||||||
voteComment: (commentId: string, isUpvote: boolean) => Promise<boolean>;
|
voteComment: (commentId: string, isUpvote: boolean) => Promise<boolean>;
|
||||||
createCell: (name: string, description: string, icon: string) => Promise<Cell | null>;
|
createCell: (name: string, description: string, icon: string) => Promise<Cell | null>;
|
||||||
refreshData: () => Promise<void>;
|
refreshData: () => Promise<void>;
|
||||||
|
moderatePost: (
|
||||||
|
cellId: string,
|
||||||
|
postId: string,
|
||||||
|
reason: string | undefined,
|
||||||
|
cellOwner: string
|
||||||
|
) => Promise<boolean>;
|
||||||
|
moderateComment: (
|
||||||
|
cellId: string,
|
||||||
|
commentId: string,
|
||||||
|
reason: string | undefined,
|
||||||
|
cellOwner: string
|
||||||
|
) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
|
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage, ModerateMessage } from '@/lib/waku/types';
|
||||||
import messageManager from '@/lib/waku';
|
import messageManager from '@/lib/waku';
|
||||||
import { Cell, Comment, Post, User } from '@/types';
|
import { Cell, Comment, Post, User } from '@/types';
|
||||||
import { transformCell, transformComment, transformPost } from './transformers';
|
import { transformCell, transformComment, transformPost } from './transformers';
|
||||||
@ -11,7 +11,9 @@ type ToastFunction = (props: {
|
|||||||
variant?: "default" | "destructive";
|
variant?: "default" | "destructive";
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteMessage | CellMessage>(
|
type AllowedMessages = PostMessage | CommentMessage | VoteMessage | CellMessage | ModerateMessage;
|
||||||
|
|
||||||
|
async function signAndSendMessage<T extends AllowedMessages>(
|
||||||
message: T,
|
message: T,
|
||||||
currentUser: User | null,
|
currentUser: User | null,
|
||||||
messageSigning: MessageSigning,
|
messageSigning: MessageSigning,
|
||||||
@ -337,3 +339,133 @@ export const vote = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const moderatePost = async (
|
||||||
|
cellId: string,
|
||||||
|
postId: 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 posts.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentUser.address !== cellOwner) {
|
||||||
|
toast({
|
||||||
|
title: "Not Authorized",
|
||||||
|
description: "Only the cell admin can moderate posts.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
toast({
|
||||||
|
title: "Moderating Post",
|
||||||
|
description: "Sending moderation message to the network...",
|
||||||
|
});
|
||||||
|
const modMsg: ModerateMessage = {
|
||||||
|
type: MessageType.MODERATE,
|
||||||
|
cellId,
|
||||||
|
targetType: 'post',
|
||||||
|
targetId: postId,
|
||||||
|
reason,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
author: currentUser.address,
|
||||||
|
};
|
||||||
|
const sentMessage = await signAndSendMessage(
|
||||||
|
modMsg,
|
||||||
|
currentUser,
|
||||||
|
messageSigning!,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
if (!sentMessage) return false;
|
||||||
|
updateStateFromCache();
|
||||||
|
toast({
|
||||||
|
title: "Post Moderated",
|
||||||
|
description: "The post has been marked as moderated.",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error moderating post:", error);
|
||||||
|
toast({
|
||||||
|
title: "Moderation Failed",
|
||||||
|
description: "Failed to moderate post. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moderateComment = async (
|
||||||
|
cellId: string,
|
||||||
|
commentId: 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 comments.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentUser.address !== cellOwner) {
|
||||||
|
toast({
|
||||||
|
title: "Not Authorized",
|
||||||
|
description: "Only the cell admin can moderate comments.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
toast({
|
||||||
|
title: "Moderating Comment",
|
||||||
|
description: "Sending moderation message to the network...",
|
||||||
|
});
|
||||||
|
const modMsg: ModerateMessage = {
|
||||||
|
type: MessageType.MODERATE,
|
||||||
|
cellId,
|
||||||
|
targetType: 'comment',
|
||||||
|
targetId: commentId,
|
||||||
|
reason,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
author: currentUser.address,
|
||||||
|
};
|
||||||
|
const sentMessage = await signAndSendMessage(
|
||||||
|
modMsg,
|
||||||
|
currentUser,
|
||||||
|
messageSigning!,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
if (!sentMessage) return false;
|
||||||
|
updateStateFromCache();
|
||||||
|
toast({
|
||||||
|
title: "Comment Moderated",
|
||||||
|
description: "The comment has been marked as moderated.",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error moderating comment:", error);
|
||||||
|
toast({
|
||||||
|
title: "Moderation Failed",
|
||||||
|
description: "Failed to moderate comment. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -2,15 +2,12 @@ import { Cell, Post, Comment, OpchanMessage } from '@/types';
|
|||||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
|
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
|
||||||
import messageManager from '@/lib/waku';
|
import messageManager from '@/lib/waku';
|
||||||
|
|
||||||
// Type for the verification function
|
|
||||||
type VerifyFunction = (message: OpchanMessage) => boolean;
|
type VerifyFunction = (message: OpchanMessage) => boolean;
|
||||||
|
|
||||||
// Helper function to transform CellMessage to Cell
|
|
||||||
export const transformCell = (
|
export const transformCell = (
|
||||||
cellMessage: CellMessage,
|
cellMessage: CellMessage,
|
||||||
verifyMessage?: VerifyFunction
|
verifyMessage?: VerifyFunction
|
||||||
): Cell | null => {
|
): Cell | null => {
|
||||||
// Verify the message if a verification function is provided
|
|
||||||
if (verifyMessage && !verifyMessage(cellMessage)) {
|
if (verifyMessage && !verifyMessage(cellMessage)) {
|
||||||
console.warn(`Cell message ${cellMessage.id} failed verification`);
|
console.warn(`Cell message ${cellMessage.id} failed verification`);
|
||||||
return null;
|
return null;
|
||||||
@ -21,7 +18,6 @@ export const transformCell = (
|
|||||||
name: cellMessage.name,
|
name: cellMessage.name,
|
||||||
description: cellMessage.description,
|
description: cellMessage.description,
|
||||||
icon: cellMessage.icon,
|
icon: cellMessage.icon,
|
||||||
// Include signature information for future verification if needed
|
|
||||||
signature: cellMessage.signature,
|
signature: cellMessage.signature,
|
||||||
browserPubKey: cellMessage.browserPubKey
|
browserPubKey: cellMessage.browserPubKey
|
||||||
};
|
};
|
||||||
@ -51,6 +47,9 @@ 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);
|
||||||
|
|
||||||
|
const modMsg = messageManager.messageCache.moderations[postMessage.id];
|
||||||
|
const isModerated = !!modMsg && modMsg.targetType === 'post';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: postMessage.id,
|
id: postMessage.id,
|
||||||
cellId: postMessage.cellId,
|
cellId: postMessage.cellId,
|
||||||
@ -60,9 +59,12 @@ export const transformPost = (
|
|||||||
timestamp: postMessage.timestamp,
|
timestamp: postMessage.timestamp,
|
||||||
upvotes: upvotes,
|
upvotes: upvotes,
|
||||||
downvotes: downvotes,
|
downvotes: downvotes,
|
||||||
// Include signature information for future verification if needed
|
|
||||||
signature: postMessage.signature,
|
signature: postMessage.signature,
|
||||||
browserPubKey: postMessage.browserPubKey
|
browserPubKey: postMessage.browserPubKey,
|
||||||
|
moderated: isModerated,
|
||||||
|
moderatedBy: isModerated ? modMsg.author : undefined,
|
||||||
|
moderationReason: isModerated ? modMsg.reason : undefined,
|
||||||
|
moderationTimestamp: isModerated ? modMsg.timestamp : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,6 +92,10 @@ 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
|
||||||
|
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
|
||||||
|
const isModerated = !!modMsg && modMsg.targetType === 'comment';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: commentMessage.id,
|
id: commentMessage.id,
|
||||||
postId: commentMessage.postId,
|
postId: commentMessage.postId,
|
||||||
@ -98,9 +104,12 @@ export const transformComment = (
|
|||||||
timestamp: commentMessage.timestamp,
|
timestamp: commentMessage.timestamp,
|
||||||
upvotes: upvotes,
|
upvotes: upvotes,
|
||||||
downvotes: downvotes,
|
downvotes: downvotes,
|
||||||
// Include signature information for future verification if needed
|
|
||||||
signature: commentMessage.signature,
|
signature: commentMessage.signature,
|
||||||
browserPubKey: commentMessage.browserPubKey
|
browserPubKey: commentMessage.browserPubKey,
|
||||||
|
moderated: isModerated,
|
||||||
|
moderatedBy: isModerated ? modMsg.author : undefined,
|
||||||
|
moderationReason: isModerated ? modMsg.reason : undefined,
|
||||||
|
moderationTimestamp: isModerated ? modMsg.timestamp : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,9 +24,9 @@ export const NETWORK_CONFIG: NetworkConfig = {
|
|||||||
export const BOOTSTRAP_NODES = {
|
export const BOOTSTRAP_NODES = {
|
||||||
"42": [
|
"42": [
|
||||||
"/dns4/waku-test.bloxy.one/tcp/8095/wss/p2p/16Uiu2HAmSZbDB7CusdRhgkD81VssRjQV5ZH13FbzCGcdnbbh6VwZ",
|
"/dns4/waku-test.bloxy.one/tcp/8095/wss/p2p/16Uiu2HAmSZbDB7CusdRhgkD81VssRjQV5ZH13FbzCGcdnbbh6VwZ",
|
||||||
// "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb",
|
"/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb",
|
||||||
// "/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk"
|
"/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk"
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOCAL_STORAGE_KEYS = {
|
export const LOCAL_STORAGE_KEYS = {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
import { createDecoder, createLightNode, HealthStatus, HealthStatusChangeEvents, LightNode } from "@waku/sdk";
|
import { createDecoder, createLightNode, HealthStatus, HealthStatusChangeEvents, LightNode } from "@waku/sdk";
|
||||||
import { BOOTSTRAP_NODES } from "./constants";
|
import { BOOTSTRAP_NODES } from "./constants";
|
||||||
import StoreManager from "./store";
|
import StoreManager from "./store";
|
||||||
import { CommentCache, MessageType, VoteCache } from "./types";
|
import { CommentCache, MessageType, VoteCache, ModerateMessage } from "./types";
|
||||||
import { PostCache } from "./types";
|
import { PostCache } from "./types";
|
||||||
import { CellCache } from "./types";
|
import { CellCache } from "./types";
|
||||||
import { OpchanMessage } from "@/types";
|
import { OpchanMessage } from "@/types";
|
||||||
@ -28,11 +28,13 @@ class MessageManager {
|
|||||||
posts: PostCache;
|
posts: PostCache;
|
||||||
comments: CommentCache;
|
comments: CommentCache;
|
||||||
votes: VoteCache;
|
votes: VoteCache;
|
||||||
|
moderations: { [targetId: string]: ModerateMessage };
|
||||||
} = {
|
} = {
|
||||||
cells: {},
|
cells: {},
|
||||||
posts: {},
|
posts: {},
|
||||||
comments: {},
|
comments: {},
|
||||||
votes: {}
|
votes: {},
|
||||||
|
moderations: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async create(): Promise<MessageManager> {
|
public static async create(): Promise<MessageManager> {
|
||||||
@ -169,7 +171,7 @@ class MessageManager {
|
|||||||
this.updateCache(message);
|
this.updateCache(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async subscribeToMessages(types: MessageType[] = [MessageType.CELL, MessageType.POST, MessageType.COMMENT, MessageType.VOTE]) {
|
public async subscribeToMessages(types: MessageType[] = [MessageType.CELL, MessageType.POST, MessageType.COMMENT, MessageType.VOTE, MessageType.MODERATE]) {
|
||||||
const { result, subscription } = await this.ephemeralProtocolsManager.subscribeToMessages(types);
|
const { result, subscription } = await this.ephemeralProtocolsManager.subscribeToMessages(types);
|
||||||
|
|
||||||
for (const message of result) {
|
for (const message of result) {
|
||||||
@ -196,6 +198,12 @@ class MessageManager {
|
|||||||
this.messageCache.votes[voteKey] = message;
|
this.messageCache.votes[voteKey] = message;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case MessageType.MODERATE: {
|
||||||
|
// Type guard for ModerateMessage
|
||||||
|
const modMsg = message as ModerateMessage;
|
||||||
|
this.messageCache.moderations[modMsg.targetId] = modMsg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// TypeScript should ensure we don't reach this case with proper OpchanMessage types
|
// TypeScript should ensure we don't reach this case with proper OpchanMessage types
|
||||||
console.warn("Received message with unknown type");
|
console.warn("Received message with unknown type");
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { LightNode } from "@waku/sdk";
|
import { LightNode } from "@waku/sdk";
|
||||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from "./types";
|
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage, ModerateMessage } from "./types";
|
||||||
import { CONTENT_TOPICS } from "./constants";
|
|
||||||
import { OpchanMessage } from "@/types";
|
import { OpchanMessage } from "@/types";
|
||||||
import { encodeMessage, encoders, decoders, decodeMessage } from "./codec";
|
import { encodeMessage, encoders, decoders, decodeMessage } from "./codec";
|
||||||
|
|
||||||
@ -20,7 +19,7 @@ export class EphemeralProtocolsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async subscribeToMessages(types: MessageType[]) {
|
public async subscribeToMessages(types: MessageType[]) {
|
||||||
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = [];
|
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage)[] = [];
|
||||||
|
|
||||||
const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => {
|
const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => {
|
||||||
const {payload} = message;
|
const {payload} = message;
|
||||||
|
|||||||
@ -5,7 +5,8 @@ export enum MessageType {
|
|||||||
CELL = 'cell',
|
CELL = 'cell',
|
||||||
POST = 'post',
|
POST = 'post',
|
||||||
COMMENT = 'comment',
|
COMMENT = 'comment',
|
||||||
VOTE = 'vote'
|
VOTE = 'vote',
|
||||||
|
MODERATE = 'moderate',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +62,17 @@ export interface VoteMessage extends BaseMessage {
|
|||||||
value: 1 | -1;
|
value: 1 | -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a moderate message
|
||||||
|
*/
|
||||||
|
export interface ModerateMessage extends BaseMessage {
|
||||||
|
type: MessageType.MODERATE;
|
||||||
|
cellId: string;
|
||||||
|
targetType: 'post' | 'comment';
|
||||||
|
targetId: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache objects for storing messages
|
* Cache objects for storing messages
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CellMessage, CommentMessage, PostMessage, VoteMessage } from "@/lib/waku/types";
|
import { CellMessage, CommentMessage, PostMessage, VoteMessage, ModerateMessage } from "@/lib/waku/types";
|
||||||
|
|
||||||
export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage;
|
export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage;
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
address: string;
|
address: string;
|
||||||
@ -32,6 +32,10 @@ export interface Post {
|
|||||||
downvotes: VoteMessage[];
|
downvotes: VoteMessage[];
|
||||||
signature?: string; // Message signature
|
signature?: string; // Message signature
|
||||||
browserPubKey?: string; // Public key that signed the message
|
browserPubKey?: string; // Public key that signed the message
|
||||||
|
moderated?: boolean;
|
||||||
|
moderatedBy?: string;
|
||||||
|
moderationReason?: string;
|
||||||
|
moderationTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
@ -44,6 +48,10 @@ export interface Comment {
|
|||||||
downvotes: VoteMessage[];
|
downvotes: VoteMessage[];
|
||||||
signature?: string; // Message signature
|
signature?: string; // Message signature
|
||||||
browserPubKey?: string; // Public key that signed the message
|
browserPubKey?: string; // Public key that signed the message
|
||||||
|
moderated?: boolean;
|
||||||
|
moderatedBy?: string;
|
||||||
|
moderationReason?: string;
|
||||||
|
moderationTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended message types for verification
|
// Extended message types for verification
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user