mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-08 15:53:08 +00:00
chore: cleanup & fix: lint
This commit is contained in:
parent
b3eb0fd276
commit
2140e41144
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForum } from '@/contexts/ForumContext';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 { Layout, MessageSquare, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
import { CreateCellDialog } from './CreateCellDialog';
|
import { CreateCellDialog } from './CreateCellDialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { CypherImage } from './ui/CypherImage'
|
|||||||
|
|
||||||
// Mock external dependencies
|
// Mock external dependencies
|
||||||
vi.mock('@/lib/utils', () => ({
|
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', () => {
|
describe('Create Cell Without Icon - CypherImage Fallback', () => {
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useForum } from "@/contexts/ForumContext";
|
import { useForum } from "@/contexts/useForum";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/useAuth";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/useAuth';
|
||||||
import { useForum } from '@/contexts/ForumContext';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react';
|
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useParams, useNavigate } from 'react-router-dom';
|
import { Link, useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useForum } from '@/contexts/ForumContext';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/useAuth';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye, Loader2 } from 'lucide-react';
|
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye, Loader2 } from 'lucide-react';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { useForum } from '@/contexts/ForumContext';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/useAuth';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|||||||
@ -40,4 +40,4 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
|||||||
)
|
)
|
||||||
Badge.displayName = "Badge"
|
Badge.displayName = "Badge"
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge }
|
||||||
|
|||||||
@ -53,4 +53,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
)
|
)
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button }
|
||||||
|
|||||||
@ -21,9 +21,8 @@ const Command = React.forwardRef<
|
|||||||
))
|
))
|
||||||
Command.displayName = CommandPrimitive.displayName
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
|
|||||||
@ -165,7 +165,6 @@ const FormMessage = React.forwardRef<
|
|||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
|||||||
@ -116,7 +116,6 @@ NavigationMenuIndicator.displayName =
|
|||||||
NavigationMenuPrimitive.Indicator.displayName
|
NavigationMenuPrimitive.Indicator.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
|
|||||||
@ -757,5 +757,4 @@ export {
|
|||||||
SidebarRail,
|
SidebarRail,
|
||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner, toast } from "sonner"
|
import { Toaster as Sonner } from "sonner"
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof 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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export interface TextareaProps
|
const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<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 React from "react"
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { toggleVariants } from "./toggle-variants"
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toggle = React.forwardRef<
|
const Toggle = React.forwardRef<
|
||||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||||
@ -40,4 +19,4 @@ const Toggle = React.forwardRef<
|
|||||||
|
|
||||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
export { Toggle }
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
import { OrdinalAPI } from '@/lib/identity/ordinal';
|
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
|
||||||
import { KeyDelegation } from '@/lib/identity/signatures/key-delegation';
|
import { OpchanMessage } from '@/types';
|
||||||
import { PhantomWalletAdapter } from '@/lib/identity/wallets/phantom';
|
|
||||||
import { MessageSigning } from '@/lib/identity/signatures/message-signing';
|
|
||||||
import { WalletConnectionStatus } from '@/lib/identity/wallets/types';
|
|
||||||
|
|
||||||
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
||||||
|
|
||||||
@ -20,106 +17,60 @@ interface AuthContextType {
|
|||||||
delegateKey: () => Promise<boolean>;
|
delegateKey: () => Promise<boolean>;
|
||||||
isDelegationValid: () => boolean;
|
isDelegationValid: () => boolean;
|
||||||
delegationTimeRemaining: () => number;
|
delegationTimeRemaining: () => number;
|
||||||
messageSigning: MessageSigning;
|
messageSigning: {
|
||||||
|
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
|
||||||
|
verifyMessage: (message: OpchanMessage) => boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export { AuthContext };
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||||
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
|
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const ordinalApi = new OrdinalAPI();
|
|
||||||
|
|
||||||
// Create refs for our services so they persist between renders
|
// Create ref for AuthService so it persists between renders
|
||||||
const phantomWalletRef = useRef(new PhantomWalletAdapter());
|
const authServiceRef = useRef(new AuthService());
|
||||||
const keyDelegationRef = useRef(new KeyDelegation());
|
|
||||||
const messageSigningRef = useRef(new MessageSigning(keyDelegationRef.current));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('opchan-user');
|
const storedUser = authServiceRef.current.loadStoredUser();
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
try {
|
setCurrentUser(storedUser);
|
||||||
const user = JSON.parse(storedUser);
|
|
||||||
const lastChecked = user.lastChecked || 0;
|
if ('ordinalOwnership' in storedUser) {
|
||||||
const expiryTime = 24 * 60 * 60 * 1000;
|
setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
||||||
|
} else {
|
||||||
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');
|
|
||||||
setVerificationStatus('unverified');
|
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 () => {
|
const connectWallet = async () => {
|
||||||
setIsAuthenticating(true);
|
setIsAuthenticating(true);
|
||||||
try {
|
try {
|
||||||
// Check if Phantom wallet is installed
|
const result: AuthResult = await authServiceRef.current.connectWallet();
|
||||||
if (!phantomWalletRef.current.isInstalled()) {
|
|
||||||
|
if (!result.success) {
|
||||||
toast({
|
toast({
|
||||||
title: "Wallet Not Found",
|
title: "Connection Failed",
|
||||||
description: "Please install Phantom wallet to continue.",
|
description: result.error || "Failed to connect to wallet. Please try again.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
throw new Error("Phantom wallet not installed");
|
throw new Error(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await phantomWalletRef.current.connect();
|
const newUser = result.user!;
|
||||||
|
|
||||||
const newUser: User = {
|
|
||||||
address,
|
|
||||||
lastChecked: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentUser(newUser);
|
setCurrentUser(newUser);
|
||||||
localStorage.setItem('opchan-user', JSON.stringify(newUser));
|
authServiceRef.current.saveUser(newUser);
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Wallet Connected",
|
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({
|
toast({
|
||||||
@ -141,11 +92,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const disconnectWallet = () => {
|
const disconnectWallet = () => {
|
||||||
phantomWalletRef.current.disconnect();
|
authServiceRef.current.disconnectWallet();
|
||||||
|
authServiceRef.current.clearStoredUser();
|
||||||
|
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
localStorage.removeItem('opchan-user');
|
|
||||||
keyDelegationRef.current.clearDelegation();
|
|
||||||
setVerificationStatus('unverified');
|
setVerificationStatus('unverified');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -154,7 +104,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyOrdinal = async () => {
|
const verifyOrdinal = async (): Promise<boolean> => {
|
||||||
if (!currentUser || !currentUser.address) {
|
if (!currentUser || !currentUser.address) {
|
||||||
toast({
|
toast({
|
||||||
title: "Not Connected",
|
title: "Not Connected",
|
||||||
@ -173,24 +123,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
description: "Checking your wallet for Ordinal Operators..."
|
description: "Checking your wallet for Ordinal Operators..."
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: revert when the API is ready
|
const result: AuthResult = await authServiceRef.current.verifyOrdinal(currentUser);
|
||||||
// const response = await ordinalApi.getOperatorDetails(currentUser.address);
|
|
||||||
// const hasOperators = response.has_operators;
|
|
||||||
const hasOperators = true;
|
|
||||||
|
|
||||||
const updatedUser = {
|
|
||||||
...currentUser,
|
|
||||||
ordinalOwnership: hasOperators,
|
|
||||||
lastChecked: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = result.user!;
|
||||||
setCurrentUser(updatedUser);
|
setCurrentUser(updatedUser);
|
||||||
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
authServiceRef.current.saveUser(updatedUser);
|
||||||
|
|
||||||
// Update verification status
|
// Update verification status
|
||||||
setVerificationStatus(hasOperators ? 'verified-owner' : 'verified-none');
|
setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
||||||
|
|
||||||
if (hasOperators) {
|
if (updatedUser.ordinalOwnership) {
|
||||||
toast({
|
toast({
|
||||||
title: "Ordinal Verified",
|
title: "Ordinal Verified",
|
||||||
description: "You now have full access. We recommend delegating a key for better UX.",
|
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) {
|
} catch (error) {
|
||||||
console.error("Error verifying Ordinal:", error);
|
console.error("Error verifying Ordinal:", error);
|
||||||
setVerificationStatus('unverified');
|
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> => {
|
const delegateKey = async (): Promise<boolean> => {
|
||||||
if (!currentUser || !currentUser.address) {
|
if (!currentUser || !currentUser.address) {
|
||||||
toast({
|
toast({
|
||||||
@ -242,75 +184,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setIsAuthenticating(true);
|
setIsAuthenticating(true);
|
||||||
|
|
||||||
try {
|
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({
|
toast({
|
||||||
title: "Starting Key Delegation",
|
title: "Starting Key Delegation",
|
||||||
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
|
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate a browser keypair
|
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser);
|
||||||
const keypair = await keyDelegationRef.current.generateKeypair();
|
|
||||||
|
|
||||||
// Calculate expiry time (24 hours from now)
|
if (!result.success) {
|
||||||
const expiryHours = 24;
|
throw new Error(result.error);
|
||||||
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
|
}
|
||||||
|
|
||||||
// Create delegation message
|
const updatedUser = result.user!;
|
||||||
const delegationMessage = keyDelegationRef.current.createDelegationMessage(
|
setCurrentUser(updatedUser);
|
||||||
keypair.publicKey,
|
authServiceRef.current.saveUser(updatedUser);
|
||||||
currentUser.address,
|
|
||||||
expiryTimestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format date for user-friendly display
|
// Format date for user-friendly display
|
||||||
const expiryDate = new Date(expiryTimestamp);
|
const expiryDate = new Date(updatedUser.delegationExpiry!);
|
||||||
const formattedExpiry = expiryDate.toLocaleString();
|
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({
|
toast({
|
||||||
title: "Key Delegation Successful",
|
title: "Key Delegation Successful",
|
||||||
description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
|
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 => {
|
const isDelegationValid = (): boolean => {
|
||||||
return keyDelegationRef.current.isDelegationValid();
|
return authServiceRef.current.isDelegationValid();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the time remaining on the current delegation in milliseconds
|
|
||||||
*/
|
|
||||||
const delegationTimeRemaining = (): number => {
|
const delegationTimeRemaining = (): number => {
|
||||||
return keyDelegationRef.current.getDelegationTimeRemaining();
|
return authServiceRef.current.getDelegationTimeRemaining();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -372,7 +258,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
delegateKey,
|
delegateKey,
|
||||||
isDelegationValid,
|
isDelegationValid,
|
||||||
delegationTimeRemaining,
|
delegationTimeRemaining,
|
||||||
messageSigning: messageSigningRef.current,
|
messageSigning: {
|
||||||
|
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
|
||||||
|
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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 { Cell, Post, Comment, OpchanMessage } from '@/types';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/useAuth';
|
||||||
import {
|
import {
|
||||||
createPost,
|
createPost,
|
||||||
createComment,
|
createComment,
|
||||||
@ -10,14 +10,15 @@ import {
|
|||||||
moderatePost,
|
moderatePost,
|
||||||
moderateComment,
|
moderateComment,
|
||||||
moderateUser
|
moderateUser
|
||||||
} from './forum/actions';
|
} from '@/lib/forum/actions';
|
||||||
import {
|
import {
|
||||||
setupPeriodicQueries,
|
setupPeriodicQueries,
|
||||||
monitorNetworkHealth,
|
monitorNetworkHealth,
|
||||||
initializeNetwork
|
initializeNetwork
|
||||||
} from './forum/network';
|
} from '@/lib/waku/network';
|
||||||
import messageManager from '@/lib/waku';
|
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 {
|
interface ForumContextType {
|
||||||
cells: Cell[];
|
cells: Cell[];
|
||||||
@ -64,6 +65,8 @@ interface ForumContextType {
|
|||||||
|
|
||||||
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
const ForumContext = createContext<ForumContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export { ForumContext };
|
||||||
|
|
||||||
export function ForumProvider({ children }: { children: React.ReactNode }) {
|
export function ForumProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [cells, setCells] = useState<Cell[]>([]);
|
const [cells, setCells] = useState<Cell[]>([]);
|
||||||
const [posts, setPosts] = useState<Post[]>([]);
|
const [posts, setPosts] = useState<Post[]>([]);
|
||||||
@ -78,13 +81,15 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const { toast } = useToast();
|
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
|
// Transform message cache data to the expected types
|
||||||
const updateStateFromCache = () => {
|
const updateStateFromCache = useCallback(() => {
|
||||||
// Use the verifyMessage function from messageSigning if available
|
// Use the verifyMessage function from authService if available
|
||||||
const verifyFn = isAuthenticated && messageSigning ?
|
const verifyFn = isAuthenticated ?
|
||||||
(message: OpchanMessage) => messageSigning.verifyMessage(message) :
|
(message: OpchanMessage) => authService.verifyMessage(message) :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
// Transform cells with verification
|
// Transform cells with verification
|
||||||
@ -107,7 +112,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
.map(comment => transformComment(comment, verifyFn))
|
.map(comment => transformComment(comment, verifyFn))
|
||||||
.filter(comment => comment !== null) as Comment[]
|
.filter(comment => comment !== null) as Comment[]
|
||||||
);
|
);
|
||||||
};
|
}, [authService, isAuthenticated]);
|
||||||
|
|
||||||
const handleRefreshData = async () => {
|
const handleRefreshData = async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
@ -146,7 +151,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const { cleanup } = setupPeriodicQueries(isNetworkConnected, updateStateFromCache);
|
const { cleanup } = setupPeriodicQueries(isNetworkConnected, updateStateFromCache);
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [toast]);
|
}, [isNetworkConnected, toast, updateStateFromCache]);
|
||||||
|
|
||||||
const getCellById = (id: string): Cell | undefined => {
|
const getCellById = (id: string): Cell | undefined => {
|
||||||
return cells.find(cell => cell.id === id);
|
return cells.find(cell => cell.id === id);
|
||||||
@ -172,7 +177,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
setIsPostingPost(false);
|
setIsPostingPost(false);
|
||||||
return result;
|
return result;
|
||||||
@ -187,7 +192,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
setIsPostingComment(false);
|
setIsPostingComment(false);
|
||||||
return result;
|
return result;
|
||||||
@ -202,7 +207,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
setIsVoting(false);
|
setIsVoting(false);
|
||||||
return result;
|
return result;
|
||||||
@ -217,7 +222,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
setIsVoting(false);
|
setIsVoting(false);
|
||||||
return result;
|
return result;
|
||||||
@ -233,7 +238,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
setIsPostingCell(false);
|
setIsPostingCell(false);
|
||||||
return result;
|
return result;
|
||||||
@ -254,7 +259,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
cellOwner,
|
cellOwner,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -273,7 +278,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
cellOwner,
|
cellOwner,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
updateStateFromCache,
|
||||||
messageSigning
|
authService
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -292,7 +297,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
cellOwner,
|
cellOwner,
|
||||||
toast,
|
toast,
|
||||||
updateStateFromCache,
|
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;
|
type VerifyFunction = (message: OpchanMessage) => boolean;
|
||||||
|
|
||||||
export const transformCell = (
|
export const transformCell = (
|
||||||
cellMessage: CellMessage,
|
cellMessage: CellMessage,
|
||||||
verifyMessage?: VerifyFunction
|
verifyMessage?: VerifyFunction
|
||||||
): Cell | null => {
|
): Cell | null => {
|
||||||
if (verifyMessage && !verifyMessage(cellMessage)) {
|
if (verifyMessage && !verifyMessage(cellMessage)) {
|
||||||
@ -19,41 +19,32 @@ export const transformCell = (
|
|||||||
description: cellMessage.description,
|
description: cellMessage.description,
|
||||||
icon: cellMessage.icon || '',
|
icon: cellMessage.icon || '',
|
||||||
signature: cellMessage.signature,
|
signature: cellMessage.signature,
|
||||||
browserPubKey: cellMessage.browserPubKey
|
browserPubKey: cellMessage.browserPubKey,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to transform PostMessage to Post with vote aggregation
|
|
||||||
export const transformPost = (
|
export const transformPost = (
|
||||||
postMessage: PostMessage,
|
postMessage: PostMessage,
|
||||||
verifyMessage?: VerifyFunction
|
verifyMessage?: VerifyFunction
|
||||||
): Post | null => {
|
): Post | null => {
|
||||||
// Verify the message if a verification function is provided
|
|
||||||
if (verifyMessage && !verifyMessage(postMessage)) {
|
if (verifyMessage && !verifyMessage(postMessage)) {
|
||||||
console.warn(`Post message ${postMessage.id} failed verification`);
|
console.warn(`Post message ${postMessage.id} failed verification`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all votes related to this post
|
|
||||||
const votes = Object.values(messageManager.messageCache.votes).filter(
|
const votes = Object.values(messageManager.messageCache.votes).filter(
|
||||||
vote => vote.targetId === postMessage.id
|
(vote) => vote.targetId === postMessage.id,
|
||||||
);
|
);
|
||||||
|
const filteredVotes = verifyMessage
|
||||||
// Only include verified votes if verification function is provided
|
? votes.filter((vote) => verifyMessage(vote))
|
||||||
const filteredVotes = verifyMessage
|
|
||||||
? votes.filter(vote => verifyMessage(vote))
|
|
||||||
: votes;
|
: 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 modMsg = messageManager.messageCache.moderations[postMessage.id];
|
||||||
const isPostModerated = !!modMsg && modMsg.targetType === 'post';
|
const isPostModerated = !!modMsg && modMsg.targetType === 'post';
|
||||||
|
|
||||||
// Check for user moderation in this cell
|
|
||||||
const userModMsg = Object.values(messageManager.messageCache.moderations).find(
|
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;
|
const isUserModerated = !!userModMsg;
|
||||||
|
|
||||||
@ -64,8 +55,8 @@ export const transformPost = (
|
|||||||
title: postMessage.title,
|
title: postMessage.title,
|
||||||
content: postMessage.content,
|
content: postMessage.content,
|
||||||
timestamp: postMessage.timestamp,
|
timestamp: postMessage.timestamp,
|
||||||
upvotes: upvotes,
|
upvotes,
|
||||||
downvotes: downvotes,
|
downvotes,
|
||||||
signature: postMessage.signature,
|
signature: postMessage.signature,
|
||||||
browserPubKey: postMessage.browserPubKey,
|
browserPubKey: postMessage.browserPubKey,
|
||||||
moderated: isPostModerated || isUserModerated,
|
moderated: isPostModerated || isUserModerated,
|
||||||
@ -75,37 +66,30 @@ export const transformPost = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to transform CommentMessage to Comment with vote aggregation
|
|
||||||
export const transformComment = (
|
export const transformComment = (
|
||||||
commentMessage: CommentMessage,
|
commentMessage: CommentMessage,
|
||||||
verifyMessage?: VerifyFunction
|
verifyMessage?: VerifyFunction,
|
||||||
): Comment | null => {
|
): Comment | null => {
|
||||||
// Verify the message if a verification function is provided
|
|
||||||
if (verifyMessage && !verifyMessage(commentMessage)) {
|
if (verifyMessage && !verifyMessage(commentMessage)) {
|
||||||
console.warn(`Comment message ${commentMessage.id} failed verification`);
|
console.warn(`Comment message ${commentMessage.id} failed verification`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all votes related to this comment
|
|
||||||
const votes = Object.values(messageManager.messageCache.votes).filter(
|
const votes = Object.values(messageManager.messageCache.votes).filter(
|
||||||
vote => vote.targetId === commentMessage.id
|
(vote) => vote.targetId === commentMessage.id,
|
||||||
);
|
);
|
||||||
|
const filteredVotes = verifyMessage
|
||||||
// Only include verified votes if verification function is provided
|
? votes.filter((vote) => verifyMessage(vote))
|
||||||
const filteredVotes = verifyMessage
|
|
||||||
? votes.filter(vote => verifyMessage(vote))
|
|
||||||
: votes;
|
: votes;
|
||||||
|
const upvotes = filteredVotes.filter((vote) => vote.value === 1);
|
||||||
const upvotes = filteredVotes.filter(vote => vote.value === 1);
|
const downvotes = filteredVotes.filter((vote) => vote.value === -1);
|
||||||
const downvotes = filteredVotes.filter(vote => vote.value === -1);
|
|
||||||
|
|
||||||
// Check for comment moderation
|
|
||||||
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
|
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
|
||||||
const isCommentModerated = !!modMsg && modMsg.targetType === 'comment';
|
const isCommentModerated = !!modMsg && modMsg.targetType === 'comment';
|
||||||
|
|
||||||
// Check for user moderation in this cell
|
|
||||||
const userModMsg = Object.values(messageManager.messageCache.moderations).find(
|
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;
|
const isUserModerated = !!userModMsg;
|
||||||
|
|
||||||
@ -115,8 +99,8 @@ export const transformComment = (
|
|||||||
authorAddress: commentMessage.author,
|
authorAddress: commentMessage.author,
|
||||||
content: commentMessage.content,
|
content: commentMessage.content,
|
||||||
timestamp: commentMessage.timestamp,
|
timestamp: commentMessage.timestamp,
|
||||||
upvotes: upvotes,
|
upvotes,
|
||||||
downvotes: downvotes,
|
downvotes,
|
||||||
signature: commentMessage.signature,
|
signature: commentMessage.signature,
|
||||||
browserPubKey: commentMessage.browserPubKey,
|
browserPubKey: commentMessage.browserPubKey,
|
||||||
moderated: isCommentModerated || isUserModerated,
|
moderated: isCommentModerated || isUserModerated,
|
||||||
@ -126,36 +110,26 @@ export const transformComment = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to transform VoteMessage (new)
|
|
||||||
export const transformVote = (
|
export const transformVote = (
|
||||||
voteMessage: VoteMessage,
|
voteMessage: VoteMessage,
|
||||||
verifyMessage?: VerifyFunction
|
verifyMessage?: VerifyFunction,
|
||||||
): VoteMessage | null => {
|
): VoteMessage | null => {
|
||||||
// Verify the message if a verification function is provided
|
|
||||||
if (verifyMessage && !verifyMessage(voteMessage)) {
|
if (verifyMessage && !verifyMessage(voteMessage)) {
|
||||||
console.warn(`Vote message ${voteMessage.id} failed verification`);
|
console.warn(`Vote message ${voteMessage.id} failed verification`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return voteMessage;
|
return voteMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to update UI state from message cache with verification
|
|
||||||
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
|
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
|
||||||
// Transform cells with verification
|
|
||||||
const cells = Object.values(messageManager.messageCache.cells)
|
const cells = Object.values(messageManager.messageCache.cells)
|
||||||
.map(cell => transformCell(cell, verifyMessage))
|
.map((cell) => transformCell(cell, verifyMessage))
|
||||||
.filter(cell => cell !== null) as Cell[];
|
.filter(Boolean) as Cell[];
|
||||||
|
|
||||||
// Transform posts with verification
|
|
||||||
const posts = Object.values(messageManager.messageCache.posts)
|
const posts = Object.values(messageManager.messageCache.posts)
|
||||||
.map(post => transformPost(post, verifyMessage))
|
.map((post) => transformPost(post, verifyMessage))
|
||||||
.filter(post => post !== null) as Post[];
|
.filter(Boolean) as Post[];
|
||||||
|
|
||||||
// Transform comments with verification
|
|
||||||
const comments = Object.values(messageManager.messageCache.comments)
|
const comments = Object.values(messageManager.messageCache.comments)
|
||||||
.map(comment => transformComment(comment, verifyMessage))
|
.map((c) => transformComment(c, verifyMessage))
|
||||||
.filter(comment => comment !== null) as Comment[];
|
.filter(Boolean) as Comment[];
|
||||||
|
|
||||||
return { cells, posts, comments };
|
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 btoa(binString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return signature as unknown as string;
|
return String(signature);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error signing message:', error);
|
console.error('Error signing message:', error);
|
||||||
throw 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 React from 'react';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import CellList from '@/components/CellList';
|
import CellList from '@/components/CellList';
|
||||||
import { useForum } from '@/contexts/ForumContext';
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Wifi } from 'lucide-react';
|
import { Wifi } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
import tailwindcssAnimate from "tailwindcss-animate";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
@ -106,8 +107,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'pulse-slow': {
|
'pulse-slow': {
|
||||||
'0%, 100%': { opacity: 1 },
|
'0%, 100%': { opacity: '1' },
|
||||||
'50%': { opacity: 0.6 },
|
'50%': { opacity: '0.6' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
@ -122,5 +123,5 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [tailwindcssAnimate],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user