OpChan/src/contexts/forum/actions.ts
Ashis Kumar Naik 35bc6ac15f feat: implement optional cell icon with URL validation
- Add urlLoads utility for image validation with 5s timeout
    - Update CreateCellDialog form schema to make icon optional
    - Add client-side URL validation with error toast
    - Update CypherImage to handle missing/empty icon sources
    - Update all backend actions and transformers for optional icon
    - Maintain backward compatibility with existing cell messages

Signed-off-by: Ashis Kumar Naik <ashishami2002@gmail.com>
2025-06-28 07:13:32 +05:30

522 lines
13 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage, ModerateMessage } from '@/lib/waku/types';
import messageManager from '@/lib/waku';
import { Cell, Comment, Post, User } from '@/types';
import { transformCell, transformComment, transformPost } from './transformers';
import { MessageSigning } from '@/lib/identity/signatures/message-signing';
type ToastFunction = (props: {
title: string;
description: string;
variant?: "default" | "destructive";
}) => void;
type AllowedMessages = PostMessage | CommentMessage | VoteMessage | CellMessage | ModerateMessage;
async function signAndSendMessage<T extends AllowedMessages>(
message: T,
currentUser: User | null,
messageSigning: MessageSigning,
toast: ToastFunction
): Promise<T | null> {
if (!currentUser) {
toast({
title: "Authentication Required",
description: "You need to be authenticated to perform this action.",
variant: "destructive",
});
return null;
}
try {
let signedMessage: T | null = null;
if (messageSigning) {
signedMessage = await messageSigning.signMessage(message);
if (!signedMessage) {
// Check if delegation exists but is expired
const isDelegationExpired = messageSigning['keyDelegation'] &&
!messageSigning['keyDelegation'].isDelegationValid() &&
messageSigning['keyDelegation'].retrieveDelegation();
if (isDelegationExpired) {
toast({
title: "Key Delegation Expired",
description: "Your signing key has expired. Please re-delegate your key through the profile menu.",
variant: "destructive",
});
} else {
toast({
title: "Key Delegation Required",
description: "Please delegate a signing key from your profile menu to post without wallet approval for each action.",
variant: "destructive",
});
}
return null;
}
} else {
signedMessage = message;
}
await messageManager.sendMessage(signedMessage);
return signedMessage;
} catch (error) {
console.error("Error signing and sending message:", error);
let errorMessage = "Failed to sign and send message. Please try again.";
if (error instanceof Error) {
if (error.message.includes("timeout") || error.message.includes("network")) {
errorMessage = "Network issue detected. Please check your connection and try again.";
} else if (error.message.includes("rejected") || error.message.includes("denied")) {
errorMessage = "Wallet signature request was rejected. Please approve signing to continue.";
}
}
toast({
title: "Message Error",
description: errorMessage,
variant: "destructive",
});
return null;
}
}
export const createPost = async (
cellId: string,
title: string,
content: string,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
messageSigning?: MessageSigning
): Promise<Post | null> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
description: "You need to verify Ordinal ownership to post.",
variant: "destructive",
});
return null;
}
try {
toast({
title: "Creating post",
description: "Sending your post to the network...",
});
const postId = uuidv4();
const postMessage: PostMessage = {
type: MessageType.POST,
id: postId,
cellId,
title,
content,
timestamp: Date.now(),
author: currentUser.address
};
const sentMessage = await signAndSendMessage(
postMessage,
currentUser,
messageSigning!,
toast
);
if (!sentMessage) return null;
updateStateFromCache();
toast({
title: "Post Created",
description: "Your post has been published successfully.",
});
return transformPost(sentMessage);
} catch (error) {
console.error("Error creating post:", error);
toast({
title: "Post Failed",
description: "Failed to create post. Please try again.",
variant: "destructive",
});
return null;
}
};
export const createComment = async (
postId: string,
content: string,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
messageSigning?: MessageSigning
): Promise<Comment | null> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
description: "You need to verify Ordinal ownership to comment.",
variant: "destructive",
});
return null;
}
try {
toast({
title: "Posting comment",
description: "Sending your comment to the network...",
});
const commentId = uuidv4();
const commentMessage: CommentMessage = {
type: MessageType.COMMENT,
id: commentId,
postId,
content,
timestamp: Date.now(),
author: currentUser.address
};
const sentMessage = await signAndSendMessage(
commentMessage,
currentUser,
messageSigning!,
toast
);
if (!sentMessage) return null;
updateStateFromCache();
toast({
title: "Comment Added",
description: "Your comment has been published.",
});
return transformComment(sentMessage);
} catch (error) {
console.error("Error creating comment:", error);
toast({
title: "Comment Failed",
description: "Failed to add comment. Please try again.",
variant: "destructive",
});
return null;
}
};
export const createCell = async (
name: string,
description: string,
icon: string | undefined,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
messageSigning?: MessageSigning
): Promise<Cell | null> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
description: "You need to verify Ordinal ownership to create a cell.",
variant: "destructive",
});
return null;
}
try {
toast({
title: "Creating cell",
description: "Sending your cell to the network...",
});
const cellId = uuidv4();
const cellMessage: CellMessage = {
type: MessageType.CELL,
id: cellId,
name,
description,
...(icon && { icon }),
timestamp: Date.now(),
author: currentUser.address
};
const sentMessage = await signAndSendMessage(
cellMessage,
currentUser,
messageSigning!,
toast
);
if (!sentMessage) return null;
updateStateFromCache();
toast({
title: "Cell Created",
description: "Your cell has been published.",
});
return transformCell(sentMessage);
} catch (error) {
console.error("Error creating cell:", error);
toast({
title: "Cell Failed",
description: "Failed to create cell. Please try again.",
variant: "destructive",
});
return null;
}
};
export const vote = async (
targetId: string,
isUpvote: boolean,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
messageSigning?: MessageSigning
): Promise<boolean> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
description: "You need to verify Ordinal ownership to vote.",
variant: "destructive",
});
return false;
}
try {
const voteType = isUpvote ? "upvote" : "downvote";
toast({
title: `Sending ${voteType}`,
description: "Recording your vote on the network...",
});
const voteId = uuidv4();
const voteMessage: VoteMessage = {
type: MessageType.VOTE,
id: voteId,
targetId,
value: isUpvote ? 1 : -1,
timestamp: Date.now(),
author: currentUser.address
};
const sentMessage = await signAndSendMessage(
voteMessage,
currentUser,
messageSigning!,
toast
);
if (!sentMessage) return false;
updateStateFromCache();
toast({
title: "Vote Recorded",
description: `Your ${voteType} has been registered.`,
});
return true;
} catch (error) {
console.error("Error voting:", error);
toast({
title: "Vote Failed",
description: "Failed to register your vote. Please try again.",
variant: "destructive",
});
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;
}
};
export const moderateUser = async (
cellId: string,
userAddress: 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 users.",
variant: "destructive",
});
return false;
}
if (currentUser.address !== cellOwner) {
toast({
title: "Not Authorized",
description: "Only the cell admin can moderate users.",
variant: "destructive",
});
return false;
}
const message: ModerateMessage = {
type: MessageType.MODERATE,
cellId,
targetType: 'user',
targetId: userAddress,
reason,
author: currentUser.address,
timestamp: Date.now(),
signature: '',
browserPubKey: currentUser.browserPubKey,
};
const sent = await signAndSendMessage(message, currentUser, messageSigning!, toast);
if (sent) {
updateStateFromCache();
toast({
title: "User Moderated",
description: `User ${userAddress} has been moderated in this cell.`,
variant: "default",
});
return true;
}
return false;
};