chore: cleanup & fix: lint

This commit is contained in:
Danish Arora 2025-07-30 13:22:06 +05:30
parent b3eb0fd276
commit 2140e41144
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
34 changed files with 941 additions and 1020 deletions

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useForum } from '@/contexts/ForumContext';
import { useForum } from '@/contexts/useForum';
import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
import { useForum } from '@/contexts/useForum';
import { Layout, MessageSquare, RefreshCw, Loader2 } from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button';

View File

@ -4,7 +4,7 @@ import { CypherImage } from './ui/CypherImage'
// Mock external dependencies
vi.mock('@/lib/utils', () => ({
cn: (...classes: any[]) => classes.filter(Boolean).join(' ')
cn: (...classes: (string | undefined | null)[]) => classes.filter(Boolean).join(' ')
}))
describe('Create Cell Without Icon - CypherImage Fallback', () => {

View File

@ -3,8 +3,8 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2 } from "lucide-react";
import { useForum } from "@/contexts/ForumContext";
import { useAuth } from "@/contexts/AuthContext";
import { useForum } from "@/contexts/useForum";
import { useAuth } from "@/contexts/useAuth";
import {
Form,
FormControl,

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { useForum } from '@/contexts/ForumContext';
import { useAuth } from '@/contexts/useAuth';
import { useForum } from '@/contexts/useForum';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react';

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
import { useAuth } from '@/contexts/AuthContext';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye, Loader2 } from 'lucide-react';

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
import { useAuth } from '@/contexts/AuthContext';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';

View File

@ -40,4 +40,4 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
)
Badge.displayName = "Badge"
export { Badge, badgeVariants }
export { Badge }

View File

@ -53,4 +53,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = "Button"
export { Button, buttonVariants }
export { Button }

View File

@ -21,9 +21,8 @@ const Command = React.forwardRef<
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">

View File

@ -165,7 +165,6 @@ const FormMessage = React.forwardRef<
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,

View File

@ -116,7 +116,6 @@ NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,

View File

@ -757,5 +757,4 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@ -1,5 +1,5 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, toast } from "sonner"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
@ -26,4 +26,4 @@ const Toaster = ({ ...props }: ToasterProps) => {
)
}
export { Toaster, toast }
export { Toaster }

View File

@ -2,10 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
({ className, ...props }, ref) => {
return (
<textarea

View File

@ -0,0 +1,23 @@
import { cva } from "class-variance-authority"
export const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

View File

@ -1,30 +1,9 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
import { toggleVariants } from "./toggle-variants"
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
@ -40,4 +19,4 @@ const Toggle = React.forwardRef<
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }
export { Toggle }

View File

@ -1,11 +1,8 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { User } from '@/types';
import { OrdinalAPI } from '@/lib/identity/ordinal';
import { KeyDelegation } from '@/lib/identity/signatures/key-delegation';
import { PhantomWalletAdapter } from '@/lib/identity/wallets/phantom';
import { MessageSigning } from '@/lib/identity/signatures/message-signing';
import { WalletConnectionStatus } from '@/lib/identity/wallets/types';
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
import { OpchanMessage } from '@/types';
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
@ -20,106 +17,60 @@ interface AuthContextType {
delegateKey: () => Promise<boolean>;
isDelegationValid: () => boolean;
delegationTimeRemaining: () => number;
messageSigning: MessageSigning;
messageSigning: {
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
};
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export { AuthContext };
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
const { toast } = useToast();
const ordinalApi = new OrdinalAPI();
// Create refs for our services so they persist between renders
const phantomWalletRef = useRef(new PhantomWalletAdapter());
const keyDelegationRef = useRef(new KeyDelegation());
const messageSigningRef = useRef(new MessageSigning(keyDelegationRef.current));
// Create ref for AuthService so it persists between renders
const authServiceRef = useRef(new AuthService());
useEffect(() => {
const storedUser = localStorage.getItem('opchan-user');
const storedUser = authServiceRef.current.loadStoredUser();
if (storedUser) {
try {
const user = JSON.parse(storedUser);
const lastChecked = user.lastChecked || 0;
const expiryTime = 24 * 60 * 60 * 1000;
if (Date.now() - lastChecked < expiryTime) {
setCurrentUser(user);
if ('ordinalOwnership' in user) {
setVerificationStatus(user.ordinalOwnership ? 'verified-owner' : 'verified-none');
} else {
setVerificationStatus('unverified');
}
restoreWalletConnection(user).catch(error => {
console.warn('Background wallet reconnection failed:', error);
});
} else {
localStorage.removeItem('opchan-user');
setVerificationStatus('unverified');
}
} catch (e) {
console.error("Failed to parse stored user data", e);
localStorage.removeItem('opchan-user');
setCurrentUser(storedUser);
if ('ordinalOwnership' in storedUser) {
setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
} else {
setVerificationStatus('unverified');
}
}
}, []);
/**
* Attempts to restore the wallet connection when user data is loaded from localStorage
*/
const restoreWalletConnection = async (user?: User) => {
try {
const userToCheck = user || currentUser;
if (!phantomWalletRef.current.isInstalled() || !userToCheck?.address) {
return;
}
const address = await phantomWalletRef.current.connect();
if (address === userToCheck.address) {
console.log('Wallet connection restored successfully');
} else {
console.warn('Stored address does not match connected address, clearing stored data');
localStorage.removeItem('opchan-user');
setCurrentUser(null);
setVerificationStatus('unverified');
}
} catch (error) {
console.warn('Failed to restore wallet connection:', error);
}
};
const connectWallet = async () => {
setIsAuthenticating(true);
try {
// Check if Phantom wallet is installed
if (!phantomWalletRef.current.isInstalled()) {
const result: AuthResult = await authServiceRef.current.connectWallet();
if (!result.success) {
toast({
title: "Wallet Not Found",
description: "Please install Phantom wallet to continue.",
title: "Connection Failed",
description: result.error || "Failed to connect to wallet. Please try again.",
variant: "destructive",
});
throw new Error("Phantom wallet not installed");
throw new Error(result.error);
}
const address = await phantomWalletRef.current.connect();
const newUser: User = {
address,
lastChecked: Date.now(),
};
const newUser = result.user!;
setCurrentUser(newUser);
localStorage.setItem('opchan-user', JSON.stringify(newUser));
authServiceRef.current.saveUser(newUser);
setVerificationStatus('unverified');
toast({
title: "Wallet Connected",
description: `Connected with address ${address.slice(0, 6)}...${address.slice(-4)}`,
description: `Connected with address ${newUser.address.slice(0, 6)}...${newUser.address.slice(-4)}`,
});
toast({
@ -141,11 +92,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};
const disconnectWallet = () => {
phantomWalletRef.current.disconnect();
authServiceRef.current.disconnectWallet();
authServiceRef.current.clearStoredUser();
setCurrentUser(null);
localStorage.removeItem('opchan-user');
keyDelegationRef.current.clearDelegation();
setVerificationStatus('unverified');
toast({
@ -154,7 +104,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
});
};
const verifyOrdinal = async () => {
const verifyOrdinal = async (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
toast({
title: "Not Connected",
@ -173,24 +123,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
description: "Checking your wallet for Ordinal Operators..."
});
//TODO: revert when the API is ready
// const response = await ordinalApi.getOperatorDetails(currentUser.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
const updatedUser = {
...currentUser,
ordinalOwnership: hasOperators,
lastChecked: Date.now(),
};
const result: AuthResult = await authServiceRef.current.verifyOrdinal(currentUser);
if (!result.success) {
throw new Error(result.error);
}
const updatedUser = result.user!;
setCurrentUser(updatedUser);
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
authServiceRef.current.saveUser(updatedUser);
// Update verification status
setVerificationStatus(hasOperators ? 'verified-owner' : 'verified-none');
setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
if (hasOperators) {
if (updatedUser.ordinalOwnership) {
toast({
title: "Ordinal Verified",
description: "You now have full access. We recommend delegating a key for better UX.",
@ -203,7 +149,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
});
}
return hasOperators;
return Boolean(updatedUser.ordinalOwnership);
} catch (error) {
console.error("Error verifying Ordinal:", error);
setVerificationStatus('unverified');
@ -225,10 +171,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
};
/**
* Creates a key delegation by generating a browser keypair, having the
* wallet sign a delegation message, and storing the delegation
*/
const delegateKey = async (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
toast({
@ -242,75 +184,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsAuthenticating(true);
try {
const walletStatus = phantomWalletRef.current.getStatus();
console.log('Current wallet status:', walletStatus);
if (walletStatus !== WalletConnectionStatus.Connected) {
console.log('Wallet not connected, attempting to reconnect...');
try {
await phantomWalletRef.current.connect();
console.log('Wallet reconnection successful');
} catch (reconnectError) {
console.error('Failed to reconnect wallet:', reconnectError);
toast({
title: "Wallet Connection Required",
description: "Please reconnect your wallet to delegate a key.",
variant: "destructive",
});
return false;
}
}
toast({
title: "Starting Key Delegation",
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
});
// Generate a browser keypair
const keypair = await keyDelegationRef.current.generateKeypair();
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser);
// Calculate expiry time (24 hours from now)
const expiryHours = 24;
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
if (!result.success) {
throw new Error(result.error);
}
// Create delegation message
const delegationMessage = keyDelegationRef.current.createDelegationMessage(
keypair.publicKey,
currentUser.address,
expiryTimestamp
);
const updatedUser = result.user!;
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
// Format date for user-friendly display
const expiryDate = new Date(expiryTimestamp);
const expiryDate = new Date(updatedUser.delegationExpiry!);
const formattedExpiry = expiryDate.toLocaleString();
toast({
title: "Wallet Signature Required",
description: `Please sign with your wallet to authorize a temporary key valid until ${formattedExpiry}. This improves UX by reducing wallet prompts.`,
});
const signature = await phantomWalletRef.current.signMessage(delegationMessage);
const delegationInfo = keyDelegationRef.current.createDelegation(
currentUser.address,
signature,
keypair.publicKey,
keypair.privateKey,
expiryHours
);
keyDelegationRef.current.storeDelegation(delegationInfo);
const updatedUser = {
...currentUser,
browserPubKey: keypair.publicKey,
delegationSignature: signature,
delegationExpiry: expiryTimestamp,
};
setCurrentUser(updatedUser);
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
toast({
title: "Key Delegation Successful",
description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
@ -345,18 +237,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
};
/**
* Checks if the current delegation is valid
*/
const isDelegationValid = (): boolean => {
return keyDelegationRef.current.isDelegationValid();
return authServiceRef.current.isDelegationValid();
};
/**
* Returns the time remaining on the current delegation in milliseconds
*/
const delegationTimeRemaining = (): number => {
return keyDelegationRef.current.getDelegationTimeRemaining();
return authServiceRef.current.getDelegationTimeRemaining();
};
return (
@ -372,7 +258,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
delegateKey,
isDelegationValid,
delegationTimeRemaining,
messageSigning: messageSigningRef.current,
messageSigning: {
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),
},
}}
>
{children}
@ -380,10 +269,4 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@ -1,7 +1,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { useToast } from '@/components/ui/use-toast';
import { useAuth } from '@/contexts/AuthContext';
import { useAuth } from '@/contexts/useAuth';
import {
createPost,
createComment,
@ -10,14 +10,15 @@ import {
moderatePost,
moderateComment,
moderateUser
} from './forum/actions';
} from '@/lib/forum/actions';
import {
setupPeriodicQueries,
monitorNetworkHealth,
initializeNetwork
} from './forum/network';
} from '@/lib/waku/network';
import messageManager from '@/lib/waku';
import { transformCell, transformComment, transformPost } from './forum/transformers';
import { transformCell, transformComment, transformPost } from '@/lib/forum/transformers';
import { AuthService } from '@/lib/identity/services/AuthService';
interface ForumContextType {
cells: Cell[];
@ -64,6 +65,8 @@ interface ForumContextType {
const ForumContext = createContext<ForumContextType | undefined>(undefined);
export { ForumContext };
export function ForumProvider({ children }: { children: React.ReactNode }) {
const [cells, setCells] = useState<Cell[]>([]);
const [posts, setPosts] = useState<Post[]>([]);
@ -78,13 +81,15 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const { currentUser, isAuthenticated, messageSigning } = useAuth();
const { currentUser, isAuthenticated } = useAuth();
const authService = useMemo(() => new AuthService(), []);
// Transform message cache data to the expected types
const updateStateFromCache = () => {
// Use the verifyMessage function from messageSigning if available
const verifyFn = isAuthenticated && messageSigning ?
(message: OpchanMessage) => messageSigning.verifyMessage(message) :
const updateStateFromCache = useCallback(() => {
// Use the verifyMessage function from authService if available
const verifyFn = isAuthenticated ?
(message: OpchanMessage) => authService.verifyMessage(message) :
undefined;
// Transform cells with verification
@ -107,7 +112,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
.map(comment => transformComment(comment, verifyFn))
.filter(comment => comment !== null) as Comment[]
);
};
}, [authService, isAuthenticated]);
const handleRefreshData = async () => {
setIsRefreshing(true);
@ -146,7 +151,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const { cleanup } = setupPeriodicQueries(isNetworkConnected, updateStateFromCache);
return cleanup;
}, [toast]);
}, [isNetworkConnected, toast, updateStateFromCache]);
const getCellById = (id: string): Cell | undefined => {
return cells.find(cell => cell.id === id);
@ -172,7 +177,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
isAuthenticated,
toast,
updateStateFromCache,
messageSigning
authService
);
setIsPostingPost(false);
return result;
@ -187,7 +192,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
isAuthenticated,
toast,
updateStateFromCache,
messageSigning
authService
);
setIsPostingComment(false);
return result;
@ -202,7 +207,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
isAuthenticated,
toast,
updateStateFromCache,
messageSigning
authService
);
setIsVoting(false);
return result;
@ -217,7 +222,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
isAuthenticated,
toast,
updateStateFromCache,
messageSigning
authService
);
setIsVoting(false);
return result;
@ -233,7 +238,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
isAuthenticated,
toast,
updateStateFromCache,
messageSigning
authService
);
setIsPostingCell(false);
return result;
@ -254,7 +259,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
cellOwner,
toast,
updateStateFromCache,
messageSigning
authService
);
};
@ -273,7 +278,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
cellOwner,
toast,
updateStateFromCache,
messageSigning
authService
);
};
@ -292,7 +297,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
cellOwner,
toast,
updateStateFromCache,
messageSigning
authService
);
};
@ -329,10 +334,4 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
);
}
export const useForum = () => {
const context = useContext(ForumContext);
if (context === undefined) {
throw new Error("useForum must be used within a ForumProvider");
}
return context;
};

View File

@ -1,522 +0,0 @@
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;
};

View File

@ -1,11 +0,0 @@
// Export types
export * from './types';
// Export transformers
export * from './transformers';
// Export actions
export * from './actions';
// Export network functions
export * from './network';

View File

@ -1,155 +0,0 @@
import messageManager from '@/lib/waku';
type ToastFunction = (props: {
title: string;
description: string;
variant?: "default" | "destructive";
}) => void;
// Function to refresh data from the network
export const refreshData = async (
isNetworkConnected: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
setError: (error: string | null) => void
): Promise<void> => {
try {
toast({
title: "Refreshing data",
description: "Fetching latest messages from the network...",
});
// Try to connect if not already connected
if (!isNetworkConnected) {
try {
await messageManager.waitForRemotePeer(10000);
} catch (err) {
console.warn("Could not connect to peer during refresh:", err);
}
}
// Query historical messages from the store
await messageManager.queryStore();
// Update UI state from the cache
updateStateFromCache();
toast({
title: "Data refreshed",
description: "Your view has been updated with the latest messages.",
});
} catch (err) {
console.error("Error refreshing data:", err);
toast({
title: "Refresh failed",
description: "Could not fetch the latest messages. Please try again.",
variant: "destructive",
});
setError("Failed to refresh data. Please try again later.");
}
};
// Function to initialize data loading
export const initializeNetwork = async (
toast: ToastFunction,
updateStateFromCache: () => void,
setError: (error: string | null) => void
): Promise<void> => {
try {
toast({
title: "Loading data",
description: "Connecting to the Waku network...",
});
// Wait for peer connection with timeout
try {
await messageManager.waitForRemotePeer(15000);
} catch (err) {
toast({
title: "Connection timeout",
description: "Could not connect to any peers. Some features may be unavailable.",
variant: "destructive",
});
console.warn("Timeout connecting to peer:", err);
}
// Query historical messages from the store
await messageManager.queryStore();
// Subscribe to new messages
await messageManager.subscribeToMessages();
// Update UI state from the cache
updateStateFromCache();
} catch (err) {
console.error("Error loading forum data:", err);
setError("Failed to load forum data. Please try again later.");
toast({
title: "Connection error",
description: "Failed to connect to Waku network. Please try refreshing.",
variant: "destructive",
});
}
};
// Function to setup periodic network queries
export const setupPeriodicQueries = (
isNetworkConnected: boolean,
updateStateFromCache: () => void
): { cleanup: () => void } => {
// Set up a polling mechanism to refresh the UI every few seconds
// This is a temporary solution until we implement real-time updates with message callbacks
const uiRefreshInterval = setInterval(() => {
updateStateFromCache();
}, 5000);
// Set up regular network queries to fetch new messages
const networkQueryInterval = setInterval(async () => {
if (isNetworkConnected) {
try {
await messageManager.queryStore();
// No need to call updateStateFromCache() here as the UI refresh interval will handle that
} catch (err) {
console.warn("Error during scheduled network query:", err);
}
}
}, 3000);
// Return a cleanup function to clear the intervals
return {
cleanup: () => {
clearInterval(uiRefreshInterval);
clearInterval(networkQueryInterval);
}
};
};
// Function to monitor network health
export const monitorNetworkHealth = (
setIsNetworkConnected: (isConnected: boolean) => void,
toast: ToastFunction
): { unsubscribe: () => void } => {
// Initial status
setIsNetworkConnected(messageManager.isReady);
// Subscribe to health changes
const unsubscribe = messageManager.onHealthChange((isReady) => {
setIsNetworkConnected(isReady);
if (isReady) {
toast({
title: "Network connected",
description: "Connected to the Waku network",
});
} else {
toast({
title: "Network disconnected",
description: "Lost connection to the Waku network",
variant: "destructive",
});
}
});
return { unsubscribe };
};

View File

@ -1,26 +0,0 @@
import { Cell, Post, Comment } from '@/types';
export interface ForumContextType {
cells: Cell[];
posts: Post[];
comments: Comment[];
// Granular loading states
isInitialLoading: boolean;
isPostingCell: boolean;
isPostingPost: boolean;
isPostingComment: boolean;
isVoting: boolean;
isRefreshing: boolean;
// Network status
isNetworkConnected: boolean;
error: string | null;
getCellById: (id: string) => Cell | undefined;
getPostsByCell: (cellId: string) => Post[];
getCommentsByPost: (postId: string) => Comment[];
createPost: (cellId: string, title: string, content: string) => Promise<Post | null>;
createComment: (postId: string, content: string) => Promise<Comment | null>;
votePost: (postId: string, isUpvote: boolean) => Promise<boolean>;
voteComment: (commentId: string, isUpvote: boolean) => Promise<boolean>;
createCell: (name: string, description: string, icon?: string) => Promise<Cell | null>;
refreshData: () => Promise<void>;
}

10
src/contexts/useAuth.ts Normal file
View File

@ -0,0 +1,10 @@
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

10
src/contexts/useForum.ts Normal file
View File

@ -0,0 +1,10 @@
import { useContext } from 'react';
import { ForumContext } from './ForumContext';
export const useForum = () => {
const context = useContext(ForumContext);
if (context === undefined) {
throw new Error('useForum must be used within a ForumProvider');
}
return context;
};

405
src/lib/forum/actions.ts Normal file
View File

@ -0,0 +1,405 @@
import { v4 as uuidv4 } from 'uuid';
import {
CellMessage,
CommentMessage,
MessageType,
PostMessage,
VoteMessage,
ModerateMessage,
} from '@/lib/waku/types';
import { Cell, Comment, Post, User } from '@/types';
import { transformCell, transformComment, transformPost } from './transformers';
import { MessageService } from '@/lib/identity/services/MessageService';
import { AuthService } from '@/lib/identity/services/AuthService';
type ToastFunction = (props: {
title: string;
description: string;
variant?: 'default' | 'destructive';
}) => void;
/* ------------------------------------------------------------------
POST / COMMENT / CELL CREATION
-------------------------------------------------------------------*/
export const createPost = async (
cellId: string,
title: string,
content: string,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(postMessage);
if (!result.success) {
toast({
title: 'Post Failed',
description: result.error || 'Failed to create post. Please try again.',
variant: 'destructive',
});
return null;
}
updateStateFromCache();
toast({ title: 'Post Created', description: 'Your post has been published successfully.' });
return transformPost(result.message! as PostMessage);
} 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,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(commentMessage);
if (!result.success) {
toast({
title: 'Comment Failed',
description: result.error || 'Failed to add comment. Please try again.',
variant: 'destructive',
});
return null;
}
updateStateFromCache();
toast({ title: 'Comment Added', description: 'Your comment has been published.' });
return transformComment(result.message! as CommentMessage);
} 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,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(cellMessage);
if (!result.success) {
toast({
title: 'Cell Failed',
description: result.error || 'Failed to create cell. Please try again.',
variant: 'destructive',
});
return null;
}
updateStateFromCache();
toast({ title: 'Cell Created', description: 'Your cell has been published.' });
return transformCell(result.message! as CellMessage);
} catch (error) {
console.error('Error creating cell:', error);
toast({
title: 'Cell Failed',
description: 'Failed to create cell. Please try again.',
variant: 'destructive',
});
return null;
}
};
/* ------------------------------------------------------------------
VOTING
-------------------------------------------------------------------*/
export const vote = async (
targetId: string,
isUpvote: boolean,
currentUser: User | null,
isAuthenticated: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(voteMessage);
if (!result.success) {
toast({
title: 'Vote Failed',
description: result.error || 'Failed to register your vote. Please try again.',
variant: 'destructive',
});
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;
}
};
/* ------------------------------------------------------------------
MODERATION
-------------------------------------------------------------------*/
export const moderatePost = async (
cellId: string,
postId: string,
reason: string | undefined,
currentUser: User | null,
isAuthenticated: boolean,
cellOwner: string,
toast: ToastFunction,
updateStateFromCache: () => void,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(modMsg);
if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate post. Please try again.', variant: 'destructive' });
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,
authService?: AuthService,
): 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 messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(modMsg);
if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate comment. Please try again.', variant: 'destructive' });
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,
authService?: AuthService,
): 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 modMsg: ModerateMessage = {
type: MessageType.MODERATE,
cellId,
targetType: 'user',
targetId: userAddress,
reason,
author: currentUser.address,
timestamp: Date.now(),
signature: '',
browserPubKey: currentUser.browserPubKey,
};
const messageService = new MessageService(authService!);
const result = await messageService.signAndSendMessage(modMsg);
if (!result.success) {
toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate user. Please try again.', variant: 'destructive' });
return false;
}
updateStateFromCache();
toast({ title: 'User Moderated', description: `User ${userAddress} has been moderated in this cell.` });
return true;
};

View File

@ -5,7 +5,7 @@ import messageManager from '@/lib/waku';
type VerifyFunction = (message: OpchanMessage) => boolean;
export const transformCell = (
cellMessage: CellMessage,
cellMessage: CellMessage,
verifyMessage?: VerifyFunction
): Cell | null => {
if (verifyMessage && !verifyMessage(cellMessage)) {
@ -19,41 +19,32 @@ export const transformCell = (
description: cellMessage.description,
icon: cellMessage.icon || '',
signature: cellMessage.signature,
browserPubKey: cellMessage.browserPubKey
browserPubKey: cellMessage.browserPubKey,
};
};
// Helper function to transform PostMessage to Post with vote aggregation
export const transformPost = (
postMessage: PostMessage,
verifyMessage?: VerifyFunction
): Post | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(postMessage)) {
console.warn(`Post message ${postMessage.id} failed verification`);
return null;
}
// Find all votes related to this post
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === postMessage.id
(vote) => vote.targetId === postMessage.id,
);
// Only include verified votes if verification function is provided
const filteredVotes = verifyMessage
? votes.filter(vote => verifyMessage(vote))
const filteredVotes = verifyMessage
? votes.filter((vote) => verifyMessage(vote))
: votes;
const upvotes = filteredVotes.filter((vote) => vote.value === 1);
const downvotes = filteredVotes.filter((vote) => vote.value === -1);
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
// Check for post moderation
const modMsg = messageManager.messageCache.moderations[postMessage.id];
const isPostModerated = !!modMsg && modMsg.targetType === 'post';
// Check for user moderation in this cell
const userModMsg = Object.values(messageManager.messageCache.moderations).find(
m => m.targetType === 'user' && m.cellId === postMessage.cellId && m.targetId === postMessage.author
(m) => m.targetType === 'user' && m.cellId === postMessage.cellId && m.targetId === postMessage.author,
);
const isUserModerated = !!userModMsg;
@ -64,8 +55,8 @@ export const transformPost = (
title: postMessage.title,
content: postMessage.content,
timestamp: postMessage.timestamp,
upvotes: upvotes,
downvotes: downvotes,
upvotes,
downvotes,
signature: postMessage.signature,
browserPubKey: postMessage.browserPubKey,
moderated: isPostModerated || isUserModerated,
@ -75,37 +66,30 @@ export const transformPost = (
};
};
// Helper function to transform CommentMessage to Comment with vote aggregation
export const transformComment = (
commentMessage: CommentMessage,
verifyMessage?: VerifyFunction
verifyMessage?: VerifyFunction,
): Comment | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(commentMessage)) {
console.warn(`Comment message ${commentMessage.id} failed verification`);
return null;
}
// Find all votes related to this comment
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === commentMessage.id
(vote) => vote.targetId === commentMessage.id,
);
// Only include verified votes if verification function is provided
const filteredVotes = verifyMessage
? votes.filter(vote => verifyMessage(vote))
const filteredVotes = verifyMessage
? votes.filter((vote) => verifyMessage(vote))
: votes;
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
const upvotes = filteredVotes.filter((vote) => vote.value === 1);
const downvotes = filteredVotes.filter((vote) => vote.value === -1);
// Check for comment moderation
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
const isCommentModerated = !!modMsg && modMsg.targetType === 'comment';
// Check for user moderation in this cell
const userModMsg = Object.values(messageManager.messageCache.moderations).find(
m => m.targetType === 'user' && m.cellId === commentMessage.postId.split('-')[0] && m.targetId === commentMessage.author
(m) =>
m.targetType === 'user' &&
m.cellId === commentMessage.postId.split('-')[0] &&
m.targetId === commentMessage.author,
);
const isUserModerated = !!userModMsg;
@ -115,8 +99,8 @@ export const transformComment = (
authorAddress: commentMessage.author,
content: commentMessage.content,
timestamp: commentMessage.timestamp,
upvotes: upvotes,
downvotes: downvotes,
upvotes,
downvotes,
signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey,
moderated: isCommentModerated || isUserModerated,
@ -126,36 +110,26 @@ export const transformComment = (
};
};
// Helper function to transform VoteMessage (new)
export const transformVote = (
voteMessage: VoteMessage,
verifyMessage?: VerifyFunction
verifyMessage?: VerifyFunction,
): VoteMessage | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(voteMessage)) {
console.warn(`Vote message ${voteMessage.id} failed verification`);
return null;
}
return voteMessage;
};
// Function to update UI state from message cache with verification
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
// Transform cells with verification
const cells = Object.values(messageManager.messageCache.cells)
.map(cell => transformCell(cell, verifyMessage))
.filter(cell => cell !== null) as Cell[];
// Transform posts with verification
.map((cell) => transformCell(cell, verifyMessage))
.filter(Boolean) as Cell[];
const posts = Object.values(messageManager.messageCache.posts)
.map(post => transformPost(post, verifyMessage))
.filter(post => post !== null) as Post[];
// Transform comments with verification
.map((post) => transformPost(post, verifyMessage))
.filter(Boolean) as Post[];
const comments = Object.values(messageManager.messageCache.comments)
.map(comment => transformComment(comment, verifyMessage))
.filter(comment => comment !== null) as Comment[];
.map((c) => transformComment(c, verifyMessage))
.filter(Boolean) as Comment[];
return { cells, posts, comments };
};
};

View File

@ -0,0 +1,192 @@
import { User } from '@/types';
import { WalletService } from '../wallets';
import { OrdinalAPI } from '../ordinal';
import { MessageSigning } from '../signatures/message-signing';
import { OpchanMessage } from '@/types';
export interface AuthResult {
success: boolean;
user?: User;
error?: string;
}
export class AuthService {
private walletService: WalletService;
private ordinalApi: OrdinalAPI;
private messageSigning: MessageSigning;
constructor() {
this.walletService = new WalletService();
this.ordinalApi = new OrdinalAPI();
this.messageSigning = new MessageSigning(this.walletService['keyDelegation']);
}
/**
* Connect to wallet and create user
*/
async connectWallet(): Promise<AuthResult> {
try {
if (!this.walletService.isWalletAvailable('phantom')) {
return {
success: false,
error: 'Phantom wallet not installed'
};
}
const address = await this.walletService.connectWallet('phantom');
const user: User = {
address,
lastChecked: Date.now(),
};
return {
success: true,
user
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to connect wallet'
};
}
}
/**
* Disconnect wallet and clear user data
*/
async disconnectWallet(): Promise<void> {
await this.walletService.disconnectWallet('phantom');
}
/**
* Verify ordinal ownership for a user
*/
async verifyOrdinal(user: User): Promise<AuthResult> {
try {
// TODO: revert when the API is ready
// const response = await this.ordinalApi.getOperatorDetails(user.address);
// const hasOperators = response.has_operators;
const hasOperators = true;
const updatedUser = {
...user,
ordinalOwnership: hasOperators,
lastChecked: Date.now(),
};
return {
success: true,
user: updatedUser
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to verify ordinal'
};
}
}
/**
* Set up key delegation for the user
*/
async delegateKey(user: User): Promise<AuthResult> {
try {
const delegationInfo = await this.walletService.setupKeyDelegation(
user.address,
'phantom'
);
const updatedUser = {
...user,
browserPubKey: delegationInfo.browserPublicKey,
delegationSignature: delegationInfo.signature,
delegationExpiry: delegationInfo.expiryTimestamp,
};
return {
success: true,
user: updatedUser
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delegate key'
};
}
}
/**
* Sign a message using delegated key
*/
async signMessage(message: OpchanMessage): Promise<OpchanMessage | null> {
return this.messageSigning.signMessage(message);
}
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
return this.messageSigning.verifyMessage(message);
}
/**
* Check if delegation is valid
*/
isDelegationValid(): boolean {
return this.walletService.isDelegationValid();
}
/**
* Get delegation time remaining
*/
getDelegationTimeRemaining(): number {
return this.walletService.getDelegationTimeRemaining();
}
/**
* Get current wallet info
*/
getWalletInfo() {
return this.walletService.getWalletInfo();
}
/**
* Load user from localStorage
*/
loadStoredUser(): User | null {
const storedUser = localStorage.getItem('opchan-user');
if (!storedUser) return null;
try {
const user = JSON.parse(storedUser);
const lastChecked = user.lastChecked || 0;
const expiryTime = 24 * 60 * 60 * 1000;
if (Date.now() - lastChecked < expiryTime) {
return user;
} else {
localStorage.removeItem('opchan-user');
return null;
}
} catch (e) {
console.error("Failed to parse stored user data", e);
localStorage.removeItem('opchan-user');
return null;
}
}
/**
* Save user to localStorage
*/
saveUser(user: User): void {
localStorage.setItem('opchan-user', JSON.stringify(user));
}
/**
* Clear stored user data
*/
clearStoredUser(): void {
localStorage.removeItem('opchan-user');
}
}

View File

@ -0,0 +1,71 @@
import { OpchanMessage } from '@/types';
import { AuthService } from './AuthService';
import messageManager from '@/lib/waku';
export interface MessageResult {
success: boolean;
message?: OpchanMessage;
error?: string;
}
export class MessageService {
private authService: AuthService;
constructor(authService: AuthService) {
this.authService = authService;
}
/**
* Sign and send a message to the Waku network
*/
async signAndSendMessage(message: OpchanMessage): Promise<MessageResult> {
try {
const signedMessage = await this.authService.signMessage(message);
if (!signedMessage) {
// Check if delegation exists but is expired
const isDelegationExpired = this.authService.isDelegationValid() === false;
return {
success: false,
error: isDelegationExpired
? 'Key delegation expired. Please re-delegate your key through the profile menu.'
: 'Key delegation required. Please delegate a signing key from your profile menu to post without wallet approval for each action.'
};
}
await messageManager.sendMessage(signedMessage);
return {
success: true,
message: 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.";
} else {
errorMessage = error.message;
}
}
return {
success: false,
error: errorMessage
};
}
}
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
return this.authService.verifyMessage(message);
}
}

View File

@ -0,0 +1,2 @@
export { AuthService } from './AuthService';
export { MessageService } from './MessageService';

View File

@ -113,7 +113,7 @@ export class PhantomWalletAdapter {
return btoa(binString);
}
return signature as unknown as string;
return String(signature);
} catch (error) {
console.error('Error signing message:', error);
throw error;

93
src/lib/waku/network.ts Normal file
View File

@ -0,0 +1,93 @@
import messageManager from '@/lib/waku';
export type ToastFunction = (props: {
title: string;
description: string;
variant?: 'default' | 'destructive';
}) => void;
export const refreshData = async (
isNetworkConnected: boolean,
toast: ToastFunction,
updateStateFromCache: () => void,
setError: (error: string | null) => void,
): Promise<void> => {
try {
toast({ title: 'Refreshing data', description: 'Fetching latest messages from the network...' });
if (!isNetworkConnected) {
try {
await messageManager.waitForRemotePeer(10000);
} catch (err) {
console.warn('Could not connect to peer during refresh:', err);
}
}
await messageManager.queryStore();
updateStateFromCache();
toast({ title: 'Data refreshed', description: 'Your view has been updated with the latest messages.' });
} catch (err) {
console.error('Error refreshing data:', err);
toast({ title: 'Refresh failed', description: 'Could not fetch the latest messages. Please try again.', variant: 'destructive' });
setError('Failed to refresh data. Please try again later.');
}
};
export const initializeNetwork = async (
toast: ToastFunction,
updateStateFromCache: () => void,
setError: (error: string | null) => void,
): Promise<void> => {
try {
toast({ title: 'Loading data', description: 'Connecting to the Waku network...' });
try {
await messageManager.waitForRemotePeer(15000);
} catch (err) {
toast({ title: 'Connection timeout', description: 'Could not connect to any peers. Some features may be unavailable.', variant: 'destructive' });
console.warn('Timeout connecting to peer:', err);
}
await messageManager.queryStore();
await messageManager.subscribeToMessages();
updateStateFromCache();
} catch (err) {
console.error('Error loading forum data:', err);
setError('Failed to load forum data. Please try again later.');
toast({ title: 'Connection error', description: 'Failed to connect to Waku network. Please try refreshing.', variant: 'destructive' });
}
};
export const setupPeriodicQueries = (
isNetworkConnected: boolean,
updateStateFromCache: () => void,
): { cleanup: () => void } => {
const uiRefreshInterval = setInterval(updateStateFromCache, 5000);
const networkQueryInterval = setInterval(async () => {
if (isNetworkConnected) {
try {
await messageManager.queryStore();
} catch (err) {
console.warn('Error during scheduled network query:', err);
}
}
}, 3000);
return {
cleanup: () => {
clearInterval(uiRefreshInterval);
clearInterval(networkQueryInterval);
},
};
};
export const monitorNetworkHealth = (
setIsNetworkConnected: (isConnected: boolean) => void,
toast: ToastFunction,
): { unsubscribe: () => void } => {
setIsNetworkConnected(messageManager.isReady);
const unsubscribe = messageManager.onHealthChange((isReady) => {
setIsNetworkConnected(isReady);
if (isReady) {
toast({ title: 'Network connected', description: 'Connected to the Waku network' });
} else {
toast({ title: 'Network disconnected', description: 'Lost connection to the Waku network', variant: 'destructive' });
}
});
return { unsubscribe };
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import Header from '@/components/Header';
import CellList from '@/components/CellList';
import { useForum } from '@/contexts/ForumContext';
import { useForum } from '@/contexts/useForum';
import { Button } from '@/components/ui/button';
import { Wifi } from 'lucide-react';

View File

@ -1,5 +1,6 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
export default {
darkMode: ["class"],
@ -106,8 +107,8 @@ export default {
}
},
'pulse-slow': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.6 },
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.6' },
},
},
animation: {
@ -122,5 +123,5 @@ export default {
},
}
},
plugins: [require("tailwindcss-animate")],
plugins: [tailwindcssAnimate],
} satisfies Config;