mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore: cleanup & fix: lint
This commit is contained in:
parent
b3eb0fd276
commit
2140e41144
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -40,4 +40,4 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
)
|
||||
Badge.displayName = "Badge"
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge }
|
||||
|
||||
@ -53,4 +53,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button }
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -165,7 +165,6 @@ const FormMessage = React.forwardRef<
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
|
||||
@ -116,7 +116,6 @@ NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
|
||||
@ -757,5 +757,4 @@ export {
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
23
src/components/ui/toggle-variants.ts
Normal file
23
src/components/ui/toggle-variants.ts
Normal 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",
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -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 }
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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';
|
||||
@ -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 };
|
||||
};
|
||||
@ -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
10
src/contexts/useAuth.ts
Normal 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
10
src/contexts/useForum.ts
Normal 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
405
src/lib/forum/actions.ts
Normal 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;
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
};
|
||||
192
src/lib/identity/services/AuthService.ts
Normal file
192
src/lib/identity/services/AuthService.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
71
src/lib/identity/services/MessageService.ts
Normal file
71
src/lib/identity/services/MessageService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
2
src/lib/identity/services/index.ts
Normal file
2
src/lib/identity/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { AuthService } from './AuthService';
|
||||
export { MessageService } from './MessageService';
|
||||
@ -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
93
src/lib/waku/network.ts
Normal 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 };
|
||||
};
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user