diff --git a/README.md b/README.md index e69de29..6df9d8a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,4 @@ +## TODOs +- [ ] replace mock wallet connection/disconnection +- [ ] replace mock Ordinal verification (API) +- [ ] \ No newline at end of file diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index f36fcbd..56a0d81 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -46,8 +46,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const connectWallet = async () => { setIsAuthenticating(true); try { - // In a real app, this would connect to a Bitcoin wallet - // For now, we'll simulate a wallet connection with a mock address + //TODO: replace with actual wallet connection const mockAddress = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"; // Create a new user object @@ -101,8 +100,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsAuthenticating(true); try { - // In a real app, this would verify Ordinal ownership - // For demo purposes, we'll simulate a successful verification + //TODO: replace with actual Ordinal verification const updatedUser = { ...currentUser, ordinalOwnership: true, diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index be2fc46..a403754 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -2,34 +2,18 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { useToast } from '@/components/ui/use-toast'; import { Cell, Post, Comment } from '@/types'; 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[]; - // 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; - createComment: (postId: string, content: string) => Promise; - votePost: (postId: string, isUpvote: boolean) => Promise; - voteComment: (commentId: string, isUpvote: boolean) => Promise; - createCell: (name: string, description: string, icon: string) => Promise; - refreshData: () => Promise; -} +import { + getDataFromCache, + createPost, + createComment, + vote, + createCell, + refreshData as refreshNetworkData, + initializeNetwork, + setupPeriodicQueries, + monitorNetworkHealth, + ForumContextType +} from './forum'; const ForumContext = createContext(undefined); @@ -38,7 +22,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { const [posts, setPosts] = useState([]); const [comments, setComments] = useState([]); - // Replace single loading state with granular loading states +// Loading states const [isInitialLoading, setIsInitialLoading] = useState(true); const [isPostingCell, setIsPostingCell] = useState(false); const [isPostingPost, setIsPostingPost] = useState(false); @@ -53,213 +37,40 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { 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); + const data = getDataFromCache(); + setCells(data.cells); + setPosts(data.posts); + setComments(data.comments); }; // 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); - } + const handleRefreshData = async () => { + setIsRefreshing(true); + await refreshNetworkData(isNetworkConnected, toast, updateStateFromCache, setError); + 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(); - }; + const { unsubscribe } = monitorNetworkHealth(setIsNetworkConnected, toast); + return unsubscribe; }, [toast]); useEffect(() => { const loadData = async () => { - try { - 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 { - setIsInitialLoading(false); - } + setIsInitialLoading(true); + await initializeNetwork(toast, updateStateFromCache, setError); + 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); + // Set up periodic queries + const { cleanup } = setupPeriodicQueries(isNetworkConnected, updateStateFromCache); - return () => { - clearInterval(uiRefreshInterval); - clearInterval(networkQueryInterval); - // You might want to clean up subscriptions here - }; + return cleanup; }, [toast]); const getCellById = (id: string): Cell | undefined => { @@ -276,280 +87,39 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { .sort((a, b) => a.timestamp - b.timestamp); }; - const createPost = async (cellId: string, title: string, content: string): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to post.", - variant: "destructive", - }); - return null; - } - - try { - 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, - title, - content, - timestamp: Date.now(), - author: currentUser.address - }; - - // 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 the transformed post - return transformPost(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; - } finally { - setIsPostingPost(false); - } + const handleCreatePost = async (cellId: string, title: string, content: string): Promise => { + setIsPostingPost(true); + const result = await createPost(cellId, title, content, currentUser, isAuthenticated, toast, updateStateFromCache); + setIsPostingPost(false); + return result; }; - const createComment = async (postId: string, content: string): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to comment.", - variant: "destructive", - }); - return null; - } - - try { - 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, - content, - timestamp: Date.now(), - author: currentUser.address - }; - - // 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 the transformed comment - return transformComment(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; - } finally { - setIsPostingComment(false); - } + const handleCreateComment = async (postId: string, content: string): Promise => { + setIsPostingComment(true); + const result = await createComment(postId, content, currentUser, isAuthenticated, toast, updateStateFromCache); + setIsPostingComment(false); + return result; }; - const votePost = async (postId: string, isUpvote: boolean): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to vote.", - variant: "destructive", - }); - return false; - } - - try { - 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) { - console.error("Error voting on post:", error); - toast({ - title: "Vote Failed", - description: "Failed to register your vote. Please try again.", - variant: "destructive", - }); - return false; - } finally { - setIsVoting(false); - } + const handleVotePost = async (postId: string, isUpvote: boolean): Promise => { + setIsVoting(true); + const result = await vote(postId, isUpvote, currentUser, isAuthenticated, toast, updateStateFromCache); + setIsVoting(false); + return result; }; - const voteComment = async (commentId: string, isUpvote: boolean): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to vote.", - variant: "destructive", - }); - return false; - } - - try { - 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) { - console.error("Error voting on comment:", error); - toast({ - title: "Vote Failed", - description: "Failed to register your vote. Please try again.", - variant: "destructive", - }); - return false; - } finally { - setIsVoting(false); - } + const handleVoteComment = async (commentId: string, isUpvote: boolean): Promise => { + setIsVoting(true); + const result = await vote(commentId, isUpvote, currentUser, isAuthenticated, toast, updateStateFromCache); + setIsVoting(false); + return result; }; - const createCell = async (name: string, description: string, icon: string): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to create a cell.", - variant: "destructive", - }); - return null; - } - - try { - 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 - }; - - // 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 transformCell(cellMessage); - } catch (error) { - console.error("Error creating cell:", error); - toast({ - title: "Cell Creation Failed", - description: "Failed to create cell. Please try again.", - variant: "destructive", - }); - return null; - } finally { - setIsPostingCell(false); - } + const handleCreateCell = async (name: string, description: string, icon: string): Promise => { + setIsPostingCell(true); + const result = await createCell(name, description, icon, currentUser, isAuthenticated, toast, updateStateFromCache); + setIsPostingCell(false); + return result; }; return ( @@ -569,12 +139,12 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { getCellById, getPostsByCell, getCommentsByPost, - createPost, - createComment, - votePost, - voteComment, - createCell, - refreshData + createPost: handleCreatePost, + createComment: handleCreateComment, + votePost: handleVotePost, + voteComment: handleVoteComment, + createCell: handleCreateCell, + refreshData: handleRefreshData }} > {children} diff --git a/src/contexts/forum/actions.ts b/src/contexts/forum/actions.ts new file mode 100644 index 0000000..7befffa --- /dev/null +++ b/src/contexts/forum/actions.ts @@ -0,0 +1,250 @@ +import { v4 as uuidv4 } from 'uuid'; +import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types'; +import messageManager from '@/lib/waku'; +import { Cell, Comment, Post, User } from '@/types'; +import { transformCell, transformComment, transformPost } from './transformers'; + +type ToastFunction = (props: { + title: string; + description: string; + variant?: "default" | "destructive"; +}) => void; + +// Create a post +export const createPost = async ( + cellId: string, + title: string, + content: string, + currentUser: User | null, + isAuthenticated: boolean, + toast: ToastFunction, + updateStateFromCache: () => void +): Promise => { + 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 + }; + + // 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 the transformed post + return transformPost(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; + } +}; + +// Create a comment +export const createComment = async ( + postId: string, + content: string, + currentUser: User | null, + isAuthenticated: boolean, + toast: ToastFunction, + updateStateFromCache: () => void +): Promise => { + 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 + }; + + // 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 the transformed comment + return transformComment(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; + } +}; + +// Vote on a post or comment +export const vote = async ( + targetId: string, + isUpvote: boolean, + currentUser: User | null, + isAuthenticated: boolean, + toast: ToastFunction, + updateStateFromCache: () => void +): Promise => { + 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 + }; + + // 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) { + console.error("Error voting:", error); + toast({ + title: "Vote Failed", + description: "Failed to register your vote. Please try again.", + variant: "destructive", + }); + return false; + } +}; + +// Create a cell +export const createCell = async ( + name: string, + description: string, + icon: string, + currentUser: User | null, + isAuthenticated: boolean, + toast: ToastFunction, + updateStateFromCache: () => void +): Promise => { + 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, + timestamp: Date.now(), + author: currentUser.address + }; + + // 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 transformCell(cellMessage); + } catch (error) { + console.error("Error creating cell:", error); + toast({ + title: "Cell Creation Failed", + description: "Failed to create cell. Please try again.", + variant: "destructive", + }); + return null; + } +}; \ No newline at end of file diff --git a/src/contexts/forum/index.ts b/src/contexts/forum/index.ts new file mode 100644 index 0000000..e4ed673 --- /dev/null +++ b/src/contexts/forum/index.ts @@ -0,0 +1,11 @@ +// Export types +export * from './types'; + +// Export transformers +export * from './transformers'; + +// Export actions +export * from './actions'; + +// Export network functions +export * from './network'; \ No newline at end of file diff --git a/src/contexts/forum/network.ts b/src/contexts/forum/network.ts new file mode 100644 index 0000000..3613e3c --- /dev/null +++ b/src/contexts/forum/network.ts @@ -0,0 +1,155 @@ +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 => { + 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 => { + 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 }; +}; \ No newline at end of file diff --git a/src/contexts/forum/transformers.ts b/src/contexts/forum/transformers.ts new file mode 100644 index 0000000..64ff65d --- /dev/null +++ b/src/contexts/forum/transformers.ts @@ -0,0 +1,70 @@ +import { Cell, Post, Comment } from '@/types'; +import { CellMessage, CommentMessage, PostMessage } from '@/lib/waku/types'; +import messageManager from '@/lib/waku'; + +// Helper function to transform CellMessage to Cell +export 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 +export 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 +export 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 +export const getDataFromCache = () => { + // Transform cells + const cells = Object.values(messageManager.messageCache.cells).map(transformCell); + + // Transform posts + const posts = Object.values(messageManager.messageCache.posts).map(transformPost); + + // Transform comments + const comments = Object.values(messageManager.messageCache.comments).map(transformComment); + + return { cells, posts, comments }; +}; \ No newline at end of file diff --git a/src/contexts/forum/types.ts b/src/contexts/forum/types.ts new file mode 100644 index 0000000..3a9edd0 --- /dev/null +++ b/src/contexts/forum/types.ts @@ -0,0 +1,26 @@ +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; + createComment: (postId: string, content: string) => Promise; + votePost: (postId: string, isUpvote: boolean) => Promise; + voteComment: (commentId: string, isUpvote: boolean) => Promise; + createCell: (name: string, description: string, icon: string) => Promise; + refreshData: () => Promise; +} \ No newline at end of file