chore: use only one content topic + message channel

This commit is contained in:
Danish Arora 2025-09-05 14:06:31 +05:30
parent aa17bda249
commit cbe93afe7a
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
4 changed files with 43 additions and 467 deletions

View File

@ -1,390 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
useForumData,
useAuth,
useUserVotes,
useForumActions,
useUserActions,
useAuthActions,
usePermissions,
useNetworkStatus,
useForumSelectors,
} from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { DelegationFullStatus } from '@/lib/delegation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
/**
* Demonstration component showing how to use the new reactive hooks
* This replaces direct context usage and business logic in components
*/
export function HookDemoComponent() {
// Core data hooks - reactive and optimized
const forumData = useForumData();
const auth = useAuth();
const { getDelegationStatus } = useAuthContext();
const [delegationStatus, setDelegationStatus] = useState<DelegationFullStatus | null>(null);
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationStatus).catch(console.error);
}, [getDelegationStatus]);
// Derived hooks for specific data
const userVotes = useUserVotes();
// Action hooks with loading states and error handling
const forumActions = useForumActions();
const userActions = useUserActions();
const authActions = useAuthActions();
// Utility hooks for permissions and status
const permissions = usePermissions();
const networkStatus = useNetworkStatus();
// Selector hooks for data transformation
const selectors = useForumSelectors(forumData);
// Example of using selectors
const trendingPosts = selectors.selectTrendingPosts();
const stats = selectors.selectStats();
// Example action handlers (no business logic in component!)
const handleCreatePost = async () => {
const result = await forumActions.createPost(
'example-cell-id',
'Example Post Title',
'This is an example post created using the new hook system!'
);
if (result) {
console.log('Post created successfully:', result);
}
};
const handleVotePost = async (postId: string, isUpvote: boolean) => {
const success = await forumActions.votePost(postId, isUpvote);
if (success) {
console.log(`${isUpvote ? 'Upvoted' : 'Downvoted'} post ${postId}`);
}
};
const handleUpdateCallSign = async () => {
const success = await userActions.updateCallSign('NewCallSign');
if (success) {
console.log('Call sign updated successfully');
}
};
const handleDelegateKey = async () => {
const success = await authActions.delegateKey('7days');
if (success) {
console.log('Key delegated successfully');
}
};
if (forumData.isInitialLoading) {
return <div>Loading forum data...</div>;
}
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Reactive Hook System Demo</h1>
{/* Network Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Network Status
<Badge
variant={
networkStatus.getHealthColor() === 'green'
? 'default'
: 'destructive'
}
>
{networkStatus.getStatusMessage()}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<strong>Waku:</strong> {networkStatus.connections.waku.status}
</div>
<div>
<strong>Wallet:</strong> {networkStatus.connections.wallet.status}
</div>
<div>
<strong>Delegation:</strong>{' '}
{networkStatus.connections.delegation.status}
</div>
</div>
</CardContent>
</Card>
{/* Auth Status */}
<Card>
<CardHeader>
<CardTitle>Authentication Status</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<strong>User:</strong> {auth.getDisplayName()}
{auth.getVerificationBadge() && (
<Badge>{auth.getVerificationBadge()}</Badge>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<strong>Verification Level:</strong> {auth.verificationStatus}
</div>
<div>
<strong>Delegation Active:</strong>{' '}
{delegationStatus?.isValid ? 'Yes' : 'No'}
</div>
</div>
<div className="flex gap-2">
<Button
onClick={handleDelegateKey}
disabled={authActions.isDelegating || !permissions.canDelegate}
>
{authActions.isDelegating ? 'Delegating...' : 'Delegate Key'}
</Button>
<Button
onClick={handleUpdateCallSign}
disabled={userActions.isUpdatingCallSign}
>
{userActions.isUpdatingCallSign
? 'Updating...'
: 'Update Call Sign'}
</Button>
</div>
</CardContent>
</Card>
{/* Permissions */}
<Card>
<CardHeader>
<CardTitle>User Permissions</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between">
<span>Can Vote:</span>
<Badge variant={permissions.canVote ? 'default' : 'secondary'}>
{permissions.canVote ? 'Yes' : 'No'}
</Badge>
</div>
<div className="flex justify-between">
<span>Can Post:</span>
<Badge variant={permissions.canPost ? 'default' : 'secondary'}>
{permissions.canPost ? 'Yes' : 'No'}
</Badge>
</div>
<div className="flex justify-between">
<span>Can Comment:</span>
<Badge
variant={permissions.canComment ? 'default' : 'secondary'}
>
{permissions.canComment ? 'Yes' : 'No'}
</Badge>
</div>
</div>
<div className="space-y-2">
<div>
<strong>Vote Reason:</strong> {permissions.voteReason}
</div>
<div>
<strong>Post Reason:</strong> {permissions.postReason}
</div>
<div>
<strong>Comment Reason:</strong> {permissions.commentReason}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Forum Data Overview */}
<Card>
<CardHeader>
<CardTitle>Forum Statistics</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold">{stats.totalCells}</div>
<div className="text-sm text-muted-foreground">Cells</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{stats.totalPosts}</div>
<div className="text-sm text-muted-foreground">Posts</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{stats.totalComments}</div>
<div className="text-sm text-muted-foreground">Comments</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{stats.verifiedUsers}</div>
<div className="text-sm text-muted-foreground">
Verified Users
</div>
</div>
</div>
</CardContent>
</Card>
{/* Trending Posts */}
<Card>
<CardHeader>
<CardTitle>Trending Posts (via Selectors)</CardTitle>
</CardHeader>
<CardContent>
{trendingPosts.slice(0, 3).map(post => (
<div key={post.id} className="mb-4 p-3 border rounded">
<h3 className="font-semibold">{post.title}</h3>
<p className="text-sm text-muted-foreground">
Score: {post.upvotes.length - post.downvotes.length} | Author:{' '}
{post.author.slice(0, 8)}... | Cell:{' '}
{forumData.cells.find(c => c.id === post.cellId)?.name ||
'Unknown'}
</p>
<div className="flex gap-2 mt-2">
<Button
size="sm"
variant="outline"
onClick={() => handleVotePost(post.id, true)}
disabled={forumActions.isVoting || !permissions.canVote}
>
{post.upvotes.length}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleVotePost(post.id, false)}
disabled={forumActions.isVoting || !permissions.canVote}
>
{post.downvotes.length}
</Button>
</div>
</div>
))}
</CardContent>
</Card>
{/* User Voting History */}
<Card>
<CardHeader>
<CardTitle>Your Voting Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-xl font-bold">{userVotes.totalVotes}</div>
<div className="text-sm text-muted-foreground">Total Votes</div>
</div>
<div className="text-center">
<div className="text-xl font-bold">
{Math.round(userVotes.upvoteRatio * 100)}%
</div>
<div className="text-sm text-muted-foreground">Upvote Ratio</div>
</div>
<div className="text-center">
<div className="text-xl font-bold">
{userVotes.votedPosts.size}
</div>
<div className="text-sm text-muted-foreground">Posts Voted</div>
</div>
</div>
</CardContent>
</Card>
{/* Action States */}
<Card>
<CardHeader>
<CardTitle>Action States</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span>Creating Post:</span>
<Badge
variant={forumActions.isCreatingPost ? 'default' : 'secondary'}
>
{forumActions.isCreatingPost ? 'Active' : 'Idle'}
</Badge>
</div>
<div className="flex justify-between">
<span>Voting:</span>
<Badge variant={forumActions.isVoting ? 'default' : 'secondary'}>
{forumActions.isVoting ? 'Active' : 'Idle'}
</Badge>
</div>
<div className="flex justify-between">
<span>Updating Profile:</span>
<Badge
variant={
userActions.isUpdatingProfile ? 'default' : 'secondary'
}
>
{userActions.isUpdatingProfile ? 'Active' : 'Idle'}
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Actions */}
<Card>
<CardHeader>
<CardTitle>Example Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
onClick={handleCreatePost}
disabled={forumActions.isCreatingPost || !permissions.canPost}
>
{forumActions.isCreatingPost
? 'Creating...'
: 'Create Example Post'}
</Button>
<Button
onClick={forumActions.refreshData}
disabled={forumActions.isVoting}
>
Refresh Forum Data
</Button>
</div>
</CardContent>
</Card>
<Separator />
<div className="text-sm text-muted-foreground">
<p>
<strong>Key Benefits Demonstrated:</strong>
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li>
Zero business logic in this component - all handled by hooks
</li>
<li>
Reactive updates - data changes automatically trigger re-renders
</li>
<li> Centralized permissions - consistent across all components</li>
<li> Optimized selectors - expensive computations are memoized</li>
<li> Loading states and error handling built into actions</li>
<li> Type-safe interfaces for all hook returns</li>
</ul>
</div>
</div>
);
}

View File

@ -6,27 +6,16 @@ import {
CommentMessage,
VoteMessage,
} from '../../types/waku';
import { CONTENT_TOPICS } from './constants';
import { CONTENT_TOPIC } from './constants';
import { OpchanMessage } from '@/types/forum';
export class CodecManager {
private encoders: Map<MessageType, IEncoder> = new Map();
private decoders: Map<MessageType, IDecoder<IDecodedMessage>> = new Map();
private encoder: IEncoder;
private decoder: IDecoder<IDecodedMessage>;
constructor(private node: LightNode) {
this.encoders = new Map(
Object.values(MessageType).map(type => [
type,
this.node.createEncoder({ contentTopic: CONTENT_TOPICS[type] }),
])
);
this.decoders = new Map(
Object.values(MessageType).map(type => [
type,
this.node.createDecoder({ contentTopic: CONTENT_TOPICS[type] }),
])
);
this.encoder = this.node.createEncoder({ contentTopic: CONTENT_TOPIC });
this.decoder = this.node.createDecoder({ contentTopic: CONTENT_TOPIC });
}
/**
@ -61,38 +50,30 @@ export class CodecManager {
}
/**
* Get encoder for a specific message type
* Get the single encoder for all message types
*/
getEncoder(messageType: MessageType): IEncoder {
const encoder = this.encoders.get(messageType);
if (!encoder) {
throw new Error(`No encoder found for message type: ${messageType}`);
}
return encoder;
getEncoder(): IEncoder {
return this.encoder;
}
/**
* Get decoder for a specific message type
* Get the single decoder for all message types
*/
getDecoder(messageType: MessageType): IDecoder<IDecodedMessage> {
const decoder = this.decoders.get(messageType);
if (!decoder) {
throw new Error(`No decoder found for message type: ${messageType}`);
}
return decoder;
getDecoder(): IDecoder<IDecodedMessage> {
return this.decoder;
}
/**
* Get all decoders for subscribing to multiple message types
* Get all decoders (returns single decoder in array for compatibility)
*/
getAllDecoders(): IDecoder<IDecodedMessage>[] {
return Array.from(this.decoders.values());
return [this.decoder];
}
/**
* Get decoders for specific message types
* Get decoders for specific message types (returns single decoder for all types)
*/
getDecoders(messageTypes: MessageType[]): IDecoder<IDecodedMessage>[] {
return messageTypes.map(type => this.getDecoder(type));
getDecoders(_messageTypes: MessageType[]): IDecoder<IDecodedMessage>[] {
return [this.decoder];
}
}

View File

@ -1,16 +1,8 @@
import { MessageType } from '../../types/waku';
/**
* Content topics for different message types
* Single content topic for all message types
* Different message types are parsed from the message content itself
*/
export const CONTENT_TOPICS: Record<MessageType, string> = {
[MessageType.CELL]: '/opchan-sds-ab/1/cell/proto',
[MessageType.POST]: '/opchan-sds-ab/1/post/proto',
[MessageType.COMMENT]: '/opchan-ab-xyz/1/comment/proto',
[MessageType.VOTE]: '/opchan-sds-ab/1/vote/proto',
[MessageType.MODERATE]: '/opchan-sds-ab/1/moderate/proto',
[MessageType.USER_PROFILE_UPDATE]: '/opchan-sds-ab/1/profile/proto',
};
export const CONTENT_TOPIC = '/opchan-sds-ab/1/messages/proto';
/**
* Bootstrap nodes for the Waku network

View File

@ -4,7 +4,6 @@ import {
ReliableChannel,
ReliableChannelEvent,
} from '@waku/sdk';
import { MessageType } from '../../../types/waku';
import { CodecManager } from '../CodecManager';
import { generateStringId } from '@/lib/utils';
import { OpchanMessage } from '@/types/forum';
@ -18,43 +17,38 @@ export interface MessageStatusCallback {
export type IncomingMessageCallback = (message: OpchanMessage) => void;
export class ReliableMessaging {
private channels: Map<MessageType, ReliableChannel<IDecodedMessage>> =
new Map();
private channel: ReliableChannel<IDecodedMessage> | null = null;
private messageCallbacks: Map<string, MessageStatusCallback> = new Map();
private incomingMessageCallbacks: Set<IncomingMessageCallback> = new Set();
private codecManager: CodecManager;
constructor(node: LightNode) {
this.codecManager = new CodecManager(node);
this.initializeChannels(node);
this.initializeChannel(node);
}
private async initializeChannels(node: LightNode): Promise<void> {
for (const type of Object.values(MessageType)) {
const encoder = this.codecManager.getEncoder(type);
const decoder = this.codecManager.getDecoder(type);
const senderId = generateStringId();
const channelId = `opchan-${type}`;
private async initializeChannel(node: LightNode): Promise<void> {
const encoder = this.codecManager.getEncoder();
const decoder = this.codecManager.getDecoder();
const senderId = generateStringId();
const channelId = 'opchan-messages';
try {
const channel = await ReliableChannel.create(
node,
channelId,
senderId,
encoder,
decoder
);
this.channels.set(type, channel);
this.setupChannelListeners(channel, type);
} catch (error) {
console.error(`Failed to create reliable channel for ${type}:`, error);
}
try {
this.channel = await ReliableChannel.create(
node,
channelId,
senderId,
encoder,
decoder
);
this.setupChannelListeners(this.channel);
} catch (error) {
console.error('Failed to create reliable channel:', error);
}
}
private setupChannelListeners(
channel: ReliableChannel<IDecodedMessage>,
type: MessageType
channel: ReliableChannel<IDecodedMessage>
): void {
channel.addEventListener(ReliableChannelEvent.InMessageReceived, event => {
try {
@ -68,7 +62,7 @@ export class ReliableMessaging {
);
}
} catch (error) {
console.error(`Failed to process incoming message for ${type}:`, error);
console.error('Failed to process incoming message:', error);
}
});
@ -105,9 +99,8 @@ export class ReliableMessaging {
message: OpchanMessage,
statusCallback?: MessageStatusCallback
): Promise<void> {
const channel = this.channels.get(message.type);
if (!channel) {
throw new Error(`No reliable channel for message type: ${message.type}`);
if (!this.channel) {
throw new Error('Reliable channel not initialized');
}
const encodedMessage = this.codecManager.encodeMessage(message);
@ -118,7 +111,7 @@ export class ReliableMessaging {
}
try {
return channel.send(encodedMessage);
return this.channel.send(encodedMessage);
} catch (error) {
this.messageCallbacks.delete(messageId);
throw error;
@ -133,6 +126,6 @@ export class ReliableMessaging {
public cleanup(): void {
this.messageCallbacks.clear();
this.incomingMessageCallbacks.clear();
this.channels.clear();
this.channel = null;
}
}