mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-04 05:43:10 +00:00
chore: use only one content topic + message channel
This commit is contained in:
parent
aa17bda249
commit
cbe93afe7a
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user