feat: integrate Waku, remove mock functions

This commit is contained in:
Danish Arora 2025-04-22 10:39:32 +05:30
parent e3128876de
commit 03d4ba38a0
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
15 changed files with 757 additions and 255 deletions

30
package-lock.json generated
View File

@ -56,6 +56,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
@ -65,6 +66,7 @@
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
@ -3485,6 +3487,13 @@
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -3770,6 +3779,19 @@
}
}
},
"node_modules/@waku/core/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@waku/discovery": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@waku/discovery/-/discovery-0.0.7.tgz",
@ -8382,16 +8404,16 @@
"license": "MIT"
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vaul": {

View File

@ -59,6 +59,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
@ -68,6 +69,7 @@
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",

View File

@ -1,3 +1,15 @@
//TODO: research into having signatures somehow?
//TODO research: **each message sent should not be able to be spoofed**
/**
* Reference:
* https://www.notion.so/Logos-Forum-PoC-Waku-Powered-Opchan-1968f96fb65c8078b343c43429d66d0a#1968f96fb65c8025a929c2c9255a57c4
* Also note that for UX purposes, **we should not ask a user to sign with their Bitcoin wallet for every action.**
*
* Instead, a key delegation system should be developed.
*
* - User sign an in-browser key with their wallet and broadcast it
* - Browser uses in-browser key to sign messages moving forward
*/
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";

View File

@ -1,15 +1,15 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
import { Skeleton } from '@/components/ui/skeleton';
import { Layout, MessageSquare } from 'lucide-react';
import { Layout, MessageSquare, RefreshCw } from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button';
const CellList = () => {
const { cells, loading, posts } = useForum();
const { cells, isInitialLoading, posts, refreshData, isRefreshing } = useForum();
if (loading) {
if (isInitialLoading) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<h1 className="text-2xl font-bold mb-6 text-glow">Loading Cells...</h1>
@ -42,29 +42,48 @@ const CellList = () => {
<Layout className="text-cyber-accent w-6 h-6" />
<h1 className="text-2xl font-bold text-glow">Cells</h1>
</div>
<CreateCellDialog />
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={isRefreshing}
title="Refresh data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button>
<CreateCellDialog />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{cells.map((cell) => (
<Link to={`/cell/${cell.id}`} key={cell.id} className="board-card group">
<div className="flex gap-4 items-start">
<img
src={cell.icon}
alt={cell.name}
className="w-16 h-16 object-cover rounded-sm border border-cyber-muted group-hover:border-cyber-accent transition-colors"
/>
<div className="flex-1">
<h2 className="text-xl font-bold mb-1 group-hover:text-cyber-accent transition-colors">{cell.name}</h2>
<p className="text-sm text-cyber-neutral mb-2">{cell.description}</p>
<div className="flex items-center text-xs text-cyber-neutral">
<MessageSquare className="w-3 h-3 mr-1" />
<span>{getPostCount(cell.id)} threads</span>
{cells.length === 0 ? (
<div className="col-span-2 text-center py-12">
<div className="text-cyber-neutral mb-4">
No cells found. Be the first to create one!
</div>
</div>
) : (
cells.map((cell) => (
<Link to={`/cell/${cell.id}`} key={cell.id} className="board-card group">
<div className="flex gap-4 items-start">
<img
src={cell.icon}
alt={cell.name}
className="w-16 h-16 object-cover rounded-sm border border-cyber-muted group-hover:border-cyber-accent transition-colors"
/>
<div className="flex-1">
<h2 className="text-xl font-bold mb-1 group-hover:text-cyber-accent transition-colors">{cell.name}</h2>
<p className="text-sm text-cyber-neutral mb-2">{cell.description}</p>
<div className="flex items-center text-xs text-cyber-neutral">
<MessageSquare className="w-3 h-3 mr-1" />
<span>{getPostCount(cell.id)} threads</span>
</div>
</div>
</div>
</div>
</Link>
))}
</Link>
))
)}
</div>
</div>
);

View File

@ -1,4 +1,3 @@
import React from 'react';
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@ -32,7 +31,7 @@ const formSchema = z.object({
});
export function CreateCellDialog() {
const { createCell } = useForum();
const { createCell, isPostingCell } = useForum();
const { isAuthenticated } = useAuth();
const [open, setOpen] = React.useState(false);
@ -73,7 +72,7 @@ export function CreateCellDialog() {
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter cell title" {...field} />
<Input placeholder="Enter cell title" {...field} disabled={isPostingCell} />
</FormControl>
<FormMessage />
</FormItem>
@ -89,6 +88,7 @@ export function CreateCellDialog() {
<Textarea
placeholder="Enter cell description"
{...field}
disabled={isPostingCell}
/>
</FormControl>
<FormMessage />
@ -106,6 +106,7 @@ export function CreateCellDialog() {
placeholder="Enter icon URL"
type="url"
{...field}
disabled={isPostingCell}
/>
</FormControl>
<FormMessage />
@ -115,9 +116,9 @@ export function CreateCellDialog() {
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
disabled={isPostingCell}
>
{form.formState.isSubmitting && (
{isPostingCell && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Create Cell

View File

@ -1,12 +1,15 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { useForum } from '@/contexts/ForumContext';
import { Button } from '@/components/ui/button';
import { ShieldCheck, LogOut, Terminal } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
const Header = () => {
const { currentUser, isAuthenticated, connectWallet, disconnectWallet, verifyOrdinal } = useAuth();
const { isNetworkConnected, isRefreshing } = useForum();
const handleConnect = async () => {
await connectWallet();
@ -33,7 +36,36 @@ const Header = () => {
</span>
</div>
<div className="flex gap-2">
<div className="flex gap-2 items-center">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Badge
variant={isNetworkConnected ? "default" : "destructive"}
className="flex items-center gap-1 mr-2"
>
{isNetworkConnected ? (
<>
<Wifi className="w-3 h-3" />
<span className="text-xs">Connected</span>
</>
) : (
<>
<WifiOff className="w-3 h-3" />
<span className="text-xs">Offline</span>
</>
)}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{isNetworkConnected
? "Connected to Waku network"
: "Not connected to Waku network. Some features may be unavailable."}</p>
{isRefreshing && <p>Refreshing data...</p>}
</TooltipContent>
</Tooltip>
{!currentUser ? (
<Button
variant="outline"

View File

@ -1,4 +1,3 @@
import React, { useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
@ -6,19 +5,31 @@ import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton';
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send } from 'lucide-react';
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { Comment } from '@/types';
const PostDetail = () => {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const { posts, comments, getCommentsByPost, createComment, votePost, voteComment, getCellById, loading } = useForum();
const {
posts,
comments,
getCommentsByPost,
createComment,
votePost,
voteComment,
getCellById,
isInitialLoading,
isPostingComment,
isVoting,
isRefreshing,
refreshData
} = useForum();
const { currentUser, isAuthenticated } = useAuth();
const [newComment, setNewComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
if (!postId || loading) {
if (!postId || isInitialLoading) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
@ -77,14 +88,13 @@ const PostDetail = () => {
if (!newComment.trim()) return;
setIsSubmitting(true);
try {
const result = await createComment(postId, newComment);
if (result) {
setNewComment('');
}
} finally {
setIsSubmitting(false);
} catch (error) {
console.error("Error creating comment:", error);
}
};
@ -98,19 +108,18 @@ const PostDetail = () => {
await voteComment(commentId, isUpvote);
};
const isPostUpvoted = currentUser && post.upvotes.includes(currentUser.address);
const isPostDownvoted = currentUser && post.downvotes.includes(currentUser.address);
const isPostUpvoted = currentUser && post.upvotes.some(vote => vote.author === currentUser.address);
const isPostDownvoted = currentUser && post.downvotes.some(vote => vote.author === currentUser.address);
const isCommentVoted = (comment: Comment, isUpvote: boolean) => {
if (!currentUser) return false;
return isUpvote
? comment.upvotes.includes(currentUser.address)
: comment.downvotes.includes(currentUser.address);
const votes = isUpvote ? comment.upvotes : comment.downvotes;
return votes.some(vote => vote.author === currentUser.address);
};
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
<div className="mb-6 flex items-center justify-between">
<Link
to={cell ? `/cell/${cell.id}` : '/'}
className="text-cyber-accent hover:underline flex items-center gap-1 text-sm"
@ -118,6 +127,15 @@ const PostDetail = () => {
<ArrowLeft className="w-4 h-4" />
{cell ? `Back to ${cell.name}` : 'Back to Cells'}
</Link>
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={isRefreshing}
title="Refresh data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button>
</div>
<div className="border border-cyber-muted rounded-sm p-4 mb-8">
@ -126,7 +144,7 @@ const PostDetail = () => {
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostUpvoted ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(true)}
disabled={!isAuthenticated}
disabled={!isAuthenticated || isVoting}
title={isAuthenticated ? "Upvote" : "Verify Ordinal to vote"}
>
<ArrowUp className="w-5 h-5" />
@ -135,7 +153,7 @@ const PostDetail = () => {
<button
className={`p-1 rounded-sm hover:bg-cyber-muted/50 ${isPostDownvoted ? 'text-cyber-accent' : ''}`}
onClick={() => handleVotePost(false)}
disabled={!isAuthenticated}
disabled={!isAuthenticated || isVoting}
title={isAuthenticated ? "Downvote" : "Verify Ordinal to vote"}
>
<ArrowDown className="w-5 h-5" />
@ -143,6 +161,7 @@ const PostDetail = () => {
</div>
<div className="flex-1">
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
<p className="text-lg mb-4">{post.content}</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
<span className="flex items-center">
@ -170,11 +189,11 @@ const PostDetail = () => {
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="flex-1 bg-cyber-muted/50 border-cyber-muted resize-none"
disabled={isSubmitting}
disabled={isPostingComment}
/>
<Button
type="submit"
disabled={isSubmitting || !newComment.trim()}
disabled={isPostingComment || !newComment.trim()}
size="icon"
>
<Send className="w-4 h-4" />
@ -204,7 +223,7 @@ const PostDetail = () => {
<button
className={`p-0.5 rounded-sm hover:bg-cyber-muted/50 ${isCommentVoted(comment, true) ? 'text-cyber-accent' : ''}`}
onClick={() => handleVoteComment(comment.id, true)}
disabled={!isAuthenticated}
disabled={!isAuthenticated || isVoting}
title={isAuthenticated ? "Upvote" : "Verify Ordinal to vote"}
>
<ArrowUp className="w-4 h-4" />
@ -213,7 +232,7 @@ const PostDetail = () => {
<button
className={`p-0.5 rounded-sm hover:bg-cyber-muted/50 ${isCommentVoted(comment, false) ? 'text-cyber-accent' : ''}`}
onClick={() => handleVoteComment(comment.id, false)}
disabled={!isAuthenticated}
disabled={!isAuthenticated || isVoting}
title={isAuthenticated ? "Downvote" : "Verify Ordinal to vote"}
>
<ArrowDown className="w-4 h-4" />

View File

@ -1,22 +1,31 @@
import React, { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { useForum } from '@/contexts/ForumContext';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton';
import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock } from 'lucide-react';
import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, RefreshCw } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
const PostList = () => {
const { cellId } = useParams<{ cellId: string }>();
const { getCellById, getPostsByCell, createPost, loading } = useForum();
const {
getCellById,
getPostsByCell,
getCommentsByPost,
createPost,
isInitialLoading,
isPostingPost,
isRefreshing,
refreshData
} = useForum();
const { isAuthenticated } = useAuth();
const [newPostTitle, setNewPostTitle] = useState('');
const [newPostContent, setNewPostContent] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
if (!cellId || loading) {
if (!cellId || isInitialLoading) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-6">
@ -70,14 +79,14 @@ const PostList = () => {
if (!newPostContent.trim()) return;
setIsSubmitting(true);
try {
const post = await createPost(cellId, newPostContent);
const post = await createPost(cellId, newPostTitle, newPostContent);
if (post) {
setNewPostTitle('');
setNewPostContent('');
}
} finally {
setIsSubmitting(false);
} catch (error) {
console.error("Error creating post:", error);
}
};
@ -95,8 +104,19 @@ const PostList = () => {
alt={cell.name}
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted"
/>
<div>
<h1 className="text-2xl font-bold text-glow">{cell.name}</h1>
<div className="flex-1">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-glow">{cell.name}</h1>
<Button
variant="outline"
size="icon"
onClick={refreshData}
disabled={isRefreshing}
title="Refresh data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button>
</div>
<p className="text-cyber-neutral">{cell.description}</p>
</div>
</div>
@ -108,19 +128,28 @@ const PostList = () => {
<MessageSquare className="w-4 h-4" />
New Thread
</h2>
<Textarea
placeholder="What's on your mind?"
value={newPostContent}
onChange={(e) => setNewPostContent(e.target.value)}
className="mb-3 bg-cyber-muted/50 border-cyber-muted resize-none"
disabled={isSubmitting}
/>
<div className="mb-3">
<Input
placeholder="Thread title"
value={newPostTitle}
onChange={(e) => setNewPostTitle(e.target.value)}
className="mb-3 bg-cyber-muted/50 border-cyber-muted"
disabled={isPostingPost}
/>
<Textarea
placeholder="What's on your mind?"
value={newPostContent}
onChange={(e) => setNewPostContent(e.target.value)}
className="bg-cyber-muted/50 border-cyber-muted resize-none"
disabled={isPostingPost}
/>
</div>
<div className="flex justify-end">
<Button
type="submit"
disabled={isSubmitting || !newPostContent.trim()}
disabled={isPostingPost || !newPostContent.trim() || !newPostTitle.trim()}
>
{isSubmitting ? 'Posting...' : 'Post Thread'}
{isPostingPost ? 'Posting...' : 'Post Thread'}
</Button>
</div>
</form>
@ -142,6 +171,7 @@ const PostList = () => {
posts.map(post => (
<Link to={`/post/${post.id}`} key={post.id} className="thread-card block">
<div>
<h3 className="font-medium mb-1">{post.title}</h3>
<p className="text-sm mb-3">{post.content}</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
<span className="flex items-center">
@ -150,8 +180,7 @@ const PostList = () => {
</span>
<span className="flex items-center">
<MessageCircle className="w-3 h-3 mr-1" />
{/* This would need to be calculated based on actual comments */}
0 comments
{getCommentsByPost(post.id).length} comments
</span>
<div className="flex items-center">
<ArrowUp className="w-3 h-3 mr-1" />

View File

@ -1,23 +1,34 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { Cell, Post, Comment } from '@/types';
import { mockCells, mockPosts, mockComments } from '@/data/mockData';
import { useAuth } from './AuthContext';
import messageManager from '@/lib/waku';
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
import { v4 as uuidv4 } from 'uuid';
interface ForumContextType {
cells: Cell[];
posts: Post[];
comments: Comment[];
loading: boolean;
// 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, content: string) => Promise<Post | null>;
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: (title: string, description: string, icon: string) => Promise<Cell | null>;
createCell: (name: string, description: string, icon: string) => Promise<Cell | null>;
refreshData: () => Promise<void>;
}
const ForumContext = createContext<ForumContextType | undefined>(undefined);
@ -26,28 +37,230 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const [cells, setCells] = useState<Cell[]>([]);
const [posts, setPosts] = useState<Post[]>([]);
const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState(true);
// Replace single loading state with granular loading states
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isPostingCell, setIsPostingCell] = useState(false);
const [isPostingPost, setIsPostingPost] = useState(false);
const [isPostingComment, setIsPostingComment] = useState(false);
const [isVoting, setIsVoting] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
// Network connection status
const [isNetworkConnected, setIsNetworkConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const { currentUser, isAuthenticated } = useAuth();
const { toast } = useToast();
// Helper function to transform CellMessage to Cell
const transformCell = (cellMessage: CellMessage): Cell => {
return {
id: cellMessage.id,
name: cellMessage.name,
description: cellMessage.description,
icon: cellMessage.icon
};
};
// Helper function to transform PostMessage to Post with vote aggregation
const transformPost = (postMessage: PostMessage): Post => {
// Find all votes related to this post
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === postMessage.id
);
const upvotes = votes.filter(vote => vote.value === 1);
const downvotes = votes.filter(vote => vote.value === -1);
return {
id: postMessage.id,
cellId: postMessage.cellId,
authorAddress: postMessage.author,
title: postMessage.title,
content: postMessage.content,
timestamp: postMessage.timestamp,
upvotes: upvotes,
downvotes: downvotes
};
};
// Helper function to transform CommentMessage to Comment with vote aggregation
const transformComment = (commentMessage: CommentMessage): Comment => {
// Find all votes related to this comment
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === commentMessage.id
);
const upvotes = votes.filter(vote => vote.value === 1);
const downvotes = votes.filter(vote => vote.value === -1);
return {
id: commentMessage.id,
postId: commentMessage.postId,
authorAddress: commentMessage.author,
content: commentMessage.content,
timestamp: commentMessage.timestamp,
upvotes: upvotes,
downvotes: downvotes
};
};
// Function to update UI state from message cache
const updateStateFromCache = () => {
// Transform cells
const cellsArray = Object.values(messageManager.messageCache.cells).map(transformCell);
// Transform posts
const postsArray = Object.values(messageManager.messageCache.posts).map(transformPost);
// Transform comments
const commentsArray = Object.values(messageManager.messageCache.comments).map(transformComment);
setCells(cellsArray);
setPosts(postsArray);
setComments(commentsArray);
};
// Function to refresh data from the network
const refreshData = async () => {
try {
setIsRefreshing(true);
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.");
} finally {
setIsRefreshing(false);
}
};
// Monitor network connection status
useEffect(() => {
// 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();
};
}, [toast]);
useEffect(() => {
const loadData = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setCells(mockCells);
setPosts(mockPosts);
setComments(mockComments);
setIsInitialLoading(true);
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",
});
} finally {
setLoading(false);
setIsInitialLoading(false);
}
};
loadData();
}, []);
// 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 () => {
clearInterval(uiRefreshInterval);
clearInterval(networkQueryInterval);
// You might want to clean up subscriptions here
};
}, [toast]);
const getCellById = (id: string): Cell | undefined => {
return cells.find(cell => cell.id === id);
@ -63,7 +276,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
.sort((a, b) => a.timestamp - b.timestamp);
};
const createPost = async (cellId: string, content: string): Promise<Post | null> => {
const createPost = async (cellId: string, title: string, content: string): Promise<Post | null> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
@ -74,24 +287,38 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
}
try {
const newPost: Post = {
id: `post-${Date.now()}`,
setIsPostingPost(true);
toast({
title: "Creating post",
description: "Sending your post to the network...",
});
const postId = uuidv4();
const postMessage: PostMessage = {
type: MessageType.POST,
id: postId,
cellId,
authorAddress: currentUser.address,
title,
content,
timestamp: Date.now(),
upvotes: [],
downvotes: [],
author: currentUser.address
};
setPosts(prev => [newPost, ...prev]);
// Send the message to the network
await messageManager.sendMessage(postMessage);
// Update UI (the cache is already updated in sendMessage)
updateStateFromCache();
toast({
title: "Post Created",
description: "Your post has been published successfully.",
});
return newPost;
// Return the transformed post
return transformPost(postMessage);
} catch (error) {
console.error("Error creating post:", error);
toast({
@ -100,6 +327,8 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
variant: "destructive",
});
return null;
} finally {
setIsPostingPost(false);
}
};
@ -114,24 +343,37 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
}
try {
const newComment: Comment = {
id: `comment-${Date.now()}`,
setIsPostingComment(true);
toast({
title: "Posting comment",
description: "Sending your comment to the network...",
});
const commentId = uuidv4();
const commentMessage: CommentMessage = {
type: MessageType.COMMENT,
id: commentId,
postId,
authorAddress: currentUser.address,
content,
timestamp: Date.now(),
upvotes: [],
downvotes: [],
author: currentUser.address
};
setComments(prev => [...prev, newComment]);
// Send the message to the network
await messageManager.sendMessage(commentMessage);
// Update UI (the cache is already updated in sendMessage)
updateStateFromCache();
toast({
title: "Comment Added",
description: "Your comment has been published.",
});
return newComment;
// Return the transformed comment
return transformComment(commentMessage);
} catch (error) {
console.error("Error creating comment:", error);
toast({
@ -140,6 +382,8 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
variant: "destructive",
});
return null;
} finally {
setIsPostingComment(false);
}
};
@ -154,29 +398,35 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
}
try {
setPosts(prev => prev.map(post => {
if (post.id === postId) {
const userAddress = currentUser.address;
const filteredUpvotes = post.upvotes.filter(addr => addr !== userAddress);
const filteredDownvotes = post.downvotes.filter(addr => addr !== userAddress);
if (isUpvote) {
return {
...post,
upvotes: [...filteredUpvotes, userAddress],
downvotes: filteredDownvotes,
};
} else {
return {
...post,
upvotes: filteredUpvotes,
downvotes: [...filteredDownvotes, userAddress],
};
}
}
return post;
}));
setIsVoting(true);
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: postId,
value: isUpvote ? 1 : -1,
timestamp: Date.now(),
author: currentUser.address
};
// Send the vote message to the network
await messageManager.sendMessage(voteMessage);
// Update UI (the cache is already updated in sendMessage)
updateStateFromCache();
toast({
title: "Vote Recorded",
description: `Your ${voteType} has been registered.`,
});
return true;
} catch (error) {
@ -187,6 +437,8 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
variant: "destructive",
});
return false;
} finally {
setIsVoting(false);
}
};
@ -201,29 +453,35 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
}
try {
setComments(prev => prev.map(comment => {
if (comment.id === commentId) {
const userAddress = currentUser.address;
const filteredUpvotes = comment.upvotes.filter(addr => addr !== userAddress);
const filteredDownvotes = comment.downvotes.filter(addr => addr !== userAddress);
if (isUpvote) {
return {
...comment,
upvotes: [...filteredUpvotes, userAddress],
downvotes: filteredDownvotes,
};
} else {
return {
...comment,
upvotes: filteredUpvotes,
downvotes: [...filteredDownvotes, userAddress],
};
}
}
return comment;
}));
setIsVoting(true);
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: commentId,
value: isUpvote ? 1 : -1,
timestamp: Date.now(),
author: currentUser.address
};
// Send the vote message to the network
await messageManager.sendMessage(voteMessage);
// Update UI (the cache is already updated in sendMessage)
updateStateFromCache();
toast({
title: "Vote Recorded",
description: `Your ${voteType} has been registered.`,
});
return true;
} catch (error) {
@ -234,10 +492,12 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
variant: "destructive",
});
return false;
} finally {
setIsVoting(false);
}
};
const createCell = async (title: string, description: string, icon: string): Promise<Cell | null> => {
const createCell = async (name: string, description: string, icon: string): Promise<Cell | null> => {
if (!isAuthenticated || !currentUser) {
toast({
title: "Authentication Required",
@ -248,29 +508,47 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
}
try {
const newCell: Cell = {
id: `cell-${Date.now()}`,
name: title,
setIsPostingCell(true);
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,
timestamp: Date.now(),
author: currentUser.address
};
setCells(prev => [...prev, newCell]);
// Send the cell message to the network
await messageManager.sendMessage(cellMessage);
// Update UI (the cache is already updated in sendMessage)
updateStateFromCache();
toast({
title: "Cell Created",
description: "Your cell has been created successfully.",
});
return newCell;
return transformCell(cellMessage);
} catch (error) {
console.error("Error creating cell:", error);
toast({
title: "Creation Failed",
title: "Cell Creation Failed",
description: "Failed to create cell. Please try again.",
variant: "destructive",
});
return null;
} finally {
setIsPostingCell(false);
}
};
@ -280,7 +558,13 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
cells,
posts,
comments,
loading,
isInitialLoading,
isPostingCell,
isPostingPost,
isPostingComment,
isVoting,
isRefreshing,
isNetworkConnected,
error,
getCellById,
getPostsByCell,
@ -290,6 +574,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
votePost,
voteComment,
createCell,
refreshData
}}
>
{children}
@ -300,7 +585,7 @@ 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");
throw new Error('useForum must be used within a ForumProvider');
}
return context;
};

View File

@ -1,29 +1,45 @@
import { createDecoder, createEncoder } from '@waku/sdk';
import { MessageType } from './types';
import { CellMessage, PostMessage, CommentMessage, VoteMessage } from './types';
import { CONTENT_TOPICS } from './constants';
import { CONTENT_TOPICS, NETWORK_CONFIG } from './constants';
import { OpchanMessage } from '@/types';
export const encoders = {
[MessageType.CELL]: createEncoder({
contentTopic: CONTENT_TOPICS['cell'],
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
}),
[MessageType.POST]: createEncoder({
contentTopic: CONTENT_TOPICS['post'],
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
}),
[MessageType.COMMENT]: createEncoder({
contentTopic: CONTENT_TOPICS['comment'],
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
}),
[MessageType.VOTE]: createEncoder({
contentTopic: CONTENT_TOPICS['vote'],
pubsubTopicShardInfo: {clusterId: NETWORK_CONFIG.clusterId, shard: 0}
}),
}
export const decoders = {
[MessageType.CELL]: createDecoder(CONTENT_TOPICS['cell']),
[MessageType.POST]: createDecoder(CONTENT_TOPICS['post']),
[MessageType.COMMENT]: createDecoder(CONTENT_TOPICS['comment']),
[MessageType.VOTE]: createDecoder(CONTENT_TOPICS['vote']),
[MessageType.CELL]: createDecoder(CONTENT_TOPICS['cell'], {
clusterId: NETWORK_CONFIG.clusterId,
shard: 0
}),
[MessageType.POST]: createDecoder(CONTENT_TOPICS['post'], {
clusterId: NETWORK_CONFIG.clusterId,
shard: 0
}),
[MessageType.COMMENT]: createDecoder(CONTENT_TOPICS['comment'], {
clusterId: NETWORK_CONFIG.clusterId,
shard: 0
}),
[MessageType.VOTE]: createDecoder(CONTENT_TOPICS['vote'], {
clusterId: NETWORK_CONFIG.clusterId,
shard: 0
}),
}
/**
@ -35,36 +51,24 @@ export function encodeMessage(message: OpchanMessage): Uint8Array {
}
/**
* Type-specific decoders
*/
export function decodeCellMessage(payload: Uint8Array): CellMessage {
return decodeMessage(payload, MessageType.CELL) as CellMessage;
}
export function decodePostMessage(payload: Uint8Array): PostMessage {
return decodeMessage(payload, MessageType.POST) as PostMessage;
}
export function decodeCommentMessage(payload: Uint8Array): CommentMessage {
return decodeMessage(payload, MessageType.COMMENT) as CommentMessage;
}
export function decodeVoteMessage(payload: Uint8Array): VoteMessage {
return decodeMessage(payload, MessageType.VOTE) as VoteMessage;
}
/**
* Decode a message from a Uint8Array based on its type
*/
function decodeMessage(payload: Uint8Array, type?: MessageType): OpchanMessage {
export function decodeMessage(payload: Uint8Array): CellMessage | PostMessage | CommentMessage | VoteMessage {
const messageJson = new TextDecoder().decode(payload);
const message = JSON.parse(messageJson) as OpchanMessage;
if (type && message.type !== type) {
throw new Error(`Expected message of type ${type}, but got ${message.type}`);
}
// Return the decoded message
return message;
switch(message.type) {
case MessageType.CELL:
return message as CellMessage;
case MessageType.POST:
return message as PostMessage;
case MessageType.COMMENT:
return message as CommentMessage;
case MessageType.VOTE:
return message as VoteMessage;
default:
throw new Error(`Unknown message type: ${message}`);
}
}

View File

@ -12,9 +12,9 @@ export const CONTENT_TOPICS: Record<MessageType, string> = {
};
export const NETWORK_CONFIG: NetworkConfig = {
contentTopics: Object.values(CONTENT_TOPICS),
shards: [1],
clusterId: 42
// contentTopics: Object.values(CONTENT_TOPICS),
clusterId: 42,
shards: [0]
}
/**
@ -22,8 +22,10 @@ export const NETWORK_CONFIG: NetworkConfig = {
* These are public Waku nodes that our node will connect to on startup
*/
export const BOOTSTRAP_NODES = {
"42": ["/dns4/waku-test.bloxy.one/tcp/8095/wss/p2p/16Uiu2HAmSZbDB7CusdRhgkD81VssRjQV5ZH13FbzCGcdnbbh6VwZ",
"42": [
"/dns4/waku-test.bloxy.one/tcp/8095/wss/p2p/16Uiu2HAmSZbDB7CusdRhgkD81VssRjQV5ZH13FbzCGcdnbbh6VwZ",
"/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb",
"/dns4/waku.fryorcraken.xyz/tcp/8000/wss/p2p/16Uiu2HAmMRvhDHrtiHft1FTUYnn6cVA8AWVrTyLUayJJ3MWpUZDB",
"/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk"]
// "/dns4/waku.fryorcraken.xyz/tcp/8000/wss/p2p/16Uiu2HAmMRvhDHrtiHft1FTUYnn6cVA8AWVrTyLUayJJ3MWpUZDB",
// "/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk"
]
};

View File

@ -1,4 +1,7 @@
import { createDecoder, createLightNode, LightNode } from "@waku/sdk";
//TODO: perhaps store all messages in an indexed DB? (helpful when Waku is down)
// with a `isPublished` flag to indicate if the message has been sent to the network
import { createDecoder, createLightNode, HealthStatus, HealthStatusChangeEvents, LightNode } from "@waku/sdk";
import { BOOTSTRAP_NODES } from "./constants";
import StoreManager from "./store";
import { CommentCache, MessageType, VoteCache } from "./types";
@ -8,11 +11,16 @@ import { OpchanMessage } from "@/types";
import { EphemeralProtocolsManager } from "./lightpush_filter";
import { NETWORK_CONFIG } from "./constants";
export type HealthChangeCallback = (isReady: boolean) => void;
class MessageManager {
private node: LightNode;
//TODO: implement SDS?
private ephemeralProtocolsManager: EphemeralProtocolsManager;
private storeManager: StoreManager;
private _isReady: boolean = false;
private healthListeners: Set<HealthChangeCallback> = new Set();
private peerCheckInterval: NodeJS.Timeout | null = null;
public readonly messageCache: {
@ -33,19 +41,115 @@ class MessageManager {
networkConfig: NETWORK_CONFIG,
autoStart: true,
bootstrapPeers: BOOTSTRAP_NODES[42],
lightPush:{autoRetry: true, retryIntervalMs: 1000}
});
return new MessageManager(node);
}
public async stop() {
if (this.peerCheckInterval) {
clearInterval(this.peerCheckInterval);
this.peerCheckInterval = null;
}
await this.node.stop();
this.setIsReady(false);
}
private constructor(node: LightNode) {
this.node = node;
this.ephemeralProtocolsManager = new EphemeralProtocolsManager(node);
this.storeManager = new StoreManager(node);
// Start peer monitoring
this.startPeerMonitoring();
}
/**
* Start monitoring connected peers to determine node health
* Runs every 1 second to check if we have at least one peer
*/
private startPeerMonitoring() {
// Initial peer check
this.checkPeers();
// Regular peer checking
this.peerCheckInterval = setInterval(() => {
this.checkPeers();
}, 1000);
}
/**
* Check if we have connected peers and update ready state
*/
private async checkPeers() {
try {
const peers = await this.node.getConnectedPeers();
this.setIsReady(peers.length >= 1);
} catch (err) {
console.error("Error checking peers:", err);
this.setIsReady(false);
}
}
private setIsReady(isReady: boolean) {
if (this._isReady !== isReady) {
this._isReady = isReady;
// Notify all health listeners
this.healthListeners.forEach(listener => listener(isReady));
}
}
/**
* Returns whether the node is currently healthy and ready for use
*/
public get isReady(): boolean {
return this._isReady;
}
/**
* Subscribe to health status changes
* @param callback Function to call when health status changes
* @returns Function to unsubscribe
*/
public onHealthChange(callback: HealthChangeCallback): () => void {
this.healthListeners.add(callback);
// Immediately call with current status
callback(this._isReady);
// Return unsubscribe function
return () => {
this.healthListeners.delete(callback);
};
}
/**
* Waits for the node to connect to at least one peer
* @param timeoutMs Maximum time to wait in milliseconds
* @returns Promise that resolves when connected or rejects on timeout
*/
public async waitForRemotePeer(timeoutMs: number = 15000): Promise<boolean> {
if (this._isReady) return true;
return new Promise<boolean>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Timed out waiting for remote peer after ${timeoutMs}ms`));
}, timeoutMs);
const checkHandler = (isReady: boolean) => {
if (isReady) {
clearTimeout(timeout);
this.healthListeners.delete(checkHandler);
resolve(true);
}
};
// Add temporary listener for peer connection
this.healthListeners.add(checkHandler);
// Also do an immediate check in case we already have peers
this.checkPeers();
});
}
public async queryStore() {
@ -97,9 +201,8 @@ class MessageManager {
break;
}
}
}
// Create singleton instance
const messageManager = await MessageManager.create();
export default messageManager;

View File

@ -1,8 +1,8 @@
import { LightNode } from "@waku/sdk";
import { decodeCellMessage, decodeCommentMessage, decodePostMessage, decoders, decodeVoteMessage, encodeMessage, encoders } from "./codec";
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from "./types";
import { CONTENT_TOPICS } from "./constants";
import { OpchanMessage } from "@/types";
import { encodeMessage, encoders, decoders, decodeMessage } from "./codec";
export class EphemeralProtocolsManager {
private node: LightNode;
@ -13,45 +13,21 @@ export class EphemeralProtocolsManager {
public async sendMessage(message: OpchanMessage) {
const encodedMessage = encodeMessage(message);
await this.node.lightPush.send(encoders[message.type], {
const result = await this.node.lightPush.send(encoders[message.type], {
payload: encodedMessage
});
return result;
}
public async subscribeToMessages(types: MessageType[]) {
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = [];
const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => {
const {contentTopic, payload} = message;
const toDecode = [
types.includes(MessageType.CELL) ? decodeCellMessage(payload) : null,
types.includes(MessageType.POST) ? decodePostMessage(payload) : null,
types.includes(MessageType.COMMENT) ? decodeCommentMessage(payload) : null,
types.includes(MessageType.VOTE) ? decodeVoteMessage(payload) : null
]
const decodedMessage = await Promise.race(toDecode);
const {payload} = message;
let parsedMessage: OpchanMessage | null = null;
switch(contentTopic) {
case CONTENT_TOPICS['cell']:
parsedMessage = decodedMessage as CellMessage;
break;
case CONTENT_TOPICS['post']:
parsedMessage = decodedMessage as PostMessage;
break;
case CONTENT_TOPICS['comment']:
parsedMessage = decodedMessage as CommentMessage;
break;
case CONTENT_TOPICS['vote']:
parsedMessage = decodedMessage as VoteMessage;
break;
default:
console.error(`Unknown content topic: ${contentTopic}`);
return;
}
if (parsedMessage) {
result.push(parsedMessage);
const decodedMessage = decodeMessage(payload);
if (types.includes(decodedMessage.type)) {
result.push(decodedMessage);
}
});

View File

@ -1,5 +1,5 @@
import { IDecodedMessage, LightNode } from "@waku/sdk";
import { decoders, decodeCellMessage, decodePostMessage, decodeCommentMessage, decodeVoteMessage } from "./codec";
import { decodeMessage, decoders} from "./codec";
import { CONTENT_TOPICS } from "./constants";
import { CellMessage, PostMessage, CommentMessage, VoteMessage } from "./types";
@ -16,32 +16,12 @@ class StoreManager {
await this.node.store.queryWithOrderedCallback(
Object.values(decoders),
(message: IDecodedMessage) => {
const {contentTopic, payload} = message;
let parsedMessage: (CellMessage | PostMessage | CommentMessage | VoteMessage) | null = null;
switch(contentTopic) {
case CONTENT_TOPICS['cell']:
parsedMessage = decodeCellMessage(payload) as CellMessage;
break;
case CONTENT_TOPICS['post']:
parsedMessage = decodePostMessage(payload) as PostMessage;
break;
case CONTENT_TOPICS['comment']:
parsedMessage = decodeCommentMessage(payload) as CommentMessage;
break;
case CONTENT_TOPICS['vote']:
parsedMessage = decodeVoteMessage(payload) as VoteMessage;
break;
default:
console.error(`Unknown content topic: ${contentTopic}`);
return;
}
if (parsedMessage) {
result.push(parsedMessage);
}
const { payload} = message;
const decodedMessage = decodeMessage(payload);
result.push(decodedMessage);
}
);
return result;
}

View File

@ -1,14 +1,30 @@
import React from 'react';
import Header from '@/components/Header';
import CellList from '@/components/CellList';
import { useForum } from '@/contexts/ForumContext';
import { Button } from '@/components/ui/button';
import { Wifi } from 'lucide-react';
const Index = () => {
const { isNetworkConnected, refreshData } = useForum();
return (
<div className="min-h-screen flex flex-col bg-cyber-dark text-white">
<Header />
<main className="flex-1">
<main className="flex-1 relative">
<CellList />
{!isNetworkConnected && (
<div className="fixed bottom-4 right-4">
<Button
onClick={refreshData}
variant="destructive"
className="flex items-center gap-2 shadow-lg animate-pulse"
>
<Wifi className="w-4 h-4" />
Reconnect
</Button>
</div>
)}
</main>
<footer className="border-t border-cyber-muted py-4 text-center text-xs text-cyber-neutral">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>