feat: integrate relevance algo into UI + sorting

This commit is contained in:
Danish Arora 2025-08-11 12:13:28 +05:30
parent 3a257770ab
commit b55c1255a6
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
10 changed files with 566 additions and 48 deletions

View File

@ -1,13 +1,22 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { useForum } from '@/contexts/useForum';
import { Layout, MessageSquare, RefreshCw, Loader2 } from 'lucide-react';
import { Layout, MessageSquare, RefreshCw, Loader2, TrendingUp, Clock } from 'lucide-react';
import { CreateCellDialog } from './CreateCellDialog';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { CypherImage } from './ui/CypherImage';
import { RelevanceIndicator } from './ui/relevance-indicator';
import { sortCells, SortOption } from '@/lib/forum/sorting';
const CellList = () => {
const { cells, isInitialLoading, posts, refreshData, isRefreshing } = useForum();
const [sortOption, setSortOption] = useState<SortOption>('relevance');
// Apply sorting to cells
const sortedCells = useMemo(() => {
return sortCells(cells, sortOption);
}, [cells, sortOption]);
if (isInitialLoading) {
return (
@ -31,6 +40,26 @@ const CellList = () => {
<h1 className="text-2xl font-bold text-glow">Cells</h1>
</div>
<div className="flex gap-2">
<Select value={sortOption} onValueChange={(value: SortOption) => setSortOption(value)}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
<span>Relevance</span>
</div>
</SelectItem>
<SelectItem value="time">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>Newest</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
@ -52,7 +81,7 @@ const CellList = () => {
</div>
</div>
) : (
cells.map((cell) => (
sortedCells.map((cell) => (
<Link to={`/cell/${cell.id}`} key={cell.id} className="board-card group">
<div className="flex gap-4 items-start">
<CypherImage
@ -64,9 +93,20 @@ const CellList = () => {
<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 className="flex items-center text-xs text-cyber-neutral gap-2">
<div className="flex items-center">
<MessageSquare className="w-3 h-3 mr-1" />
<span>{getPostCount(cell.id)} threads</span>
</div>
{cell.relevanceScore !== undefined && (
<RelevanceIndicator
score={cell.relevanceScore}
details={cell.relevanceDetails}
type="cell"
className="text-xs"
showTooltip={true}
/>
)}
</div>
</div>
</div>

View File

@ -5,6 +5,7 @@ import { formatDistanceToNow } from 'date-fns';
import { Post } from '@/types';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
interface PostCardProps {
post: Post;
@ -82,6 +83,18 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
<span>Posted by u/{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
<span></span>
<span>{formatDistanceToNow(new Date(post.timestamp), { addSuffix: true })}</span>
{post.relevanceScore !== undefined && (
<>
<span></span>
<RelevanceIndicator
score={post.relevanceScore}
details={post.relevanceDetails}
type="post"
className="text-xs"
showTooltip={true}
/>
</>
)}
</div>
{/* Post title */}

View File

@ -9,6 +9,7 @@ import { formatDistanceToNow } from 'date-fns';
import { Comment } from '@/types';
import { CypherImage } from './ui/CypherImage';
import { Badge } from '@/components/ui/badge';
import { RelevanceIndicator } from './ui/relevance-indicator';
const PostDetail = () => {
const { postId } = useParams<{ postId: string }>();
@ -165,6 +166,15 @@ const PostDetail = () => {
<span className="truncate max-w-[150px]">
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
</span>
{post.relevanceScore !== undefined && (
<RelevanceIndicator
score={post.relevanceScore}
details={post.relevanceDetails}
type="post"
className="text-xs"
showTooltip={true}
/>
)}
</div>
</div>
</div>
@ -252,9 +262,20 @@ const PostDetail = () => {
{comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
</span>
</div>
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(comment.timestamp, { addSuffix: true })}
</span>
<div className="flex items-center gap-2">
{comment.relevanceScore !== undefined && (
<RelevanceIndicator
score={comment.relevanceScore}
details={comment.relevanceDetails}
type="comment"
className="text-xs"
showTooltip={true}
/>
)}
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(comment.timestamp, { addSuffix: true })}
</span>
</div>
</div>
<p className="text-sm break-words">{comment.content}</p>
{isCellAdmin && !comment.moderated && (

View File

@ -0,0 +1,205 @@
import React, { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { TrendingUp, Clock, Shield, UserCheck, MessageSquare, ThumbsUp } from 'lucide-react';
import { RelevanceScoreDetails } from '@/lib/forum/relevance';
interface RelevanceIndicatorProps {
score: number;
details?: RelevanceScoreDetails;
type: 'post' | 'comment' | 'cell';
className?: string;
showTooltip?: boolean;
}
export function RelevanceIndicator({ score, details, type, className, showTooltip = false }: RelevanceIndicatorProps) {
const [isOpen, setIsOpen] = useState(false);
const getScoreColor = (score: number) => {
if (score >= 15) return 'bg-green-500 hover:bg-green-600';
if (score >= 10) return 'bg-blue-500 hover:bg-blue-600';
if (score >= 5) return 'bg-yellow-500 hover:bg-yellow-600';
return 'bg-gray-500 hover:bg-gray-600';
};
const formatScore = (score: number) => {
return score.toFixed(1);
};
const createTooltipContent = () => {
if (!details) return `Relevance Score: ${formatScore(score)}`;
return (
<div className="text-xs space-y-1">
<div className="font-semibold">Relevance Score: {formatScore(score)}</div>
<div>Base: {formatScore(details.baseScore)}</div>
<div>Engagement: +{formatScore(details.engagementScore)}</div>
{details.authorVerificationBonus > 0 && (
<div>Author Bonus: +{formatScore(details.authorVerificationBonus)}</div>
)}
{details.verifiedUpvoteBonus > 0 && (
<div>Verified Upvotes: +{formatScore(details.verifiedUpvoteBonus)}</div>
)}
{details.verifiedCommenterBonus > 0 && (
<div>Verified Commenters: +{formatScore(details.verifiedCommenterBonus)}</div>
)}
<div>Time Decay: ×{details.timeDecayMultiplier.toFixed(2)}</div>
{details.isModerated && (
<div>Moderation: ×{details.moderationPenalty.toFixed(1)}</div>
)}
</div>
);
};
const badge = (
<Badge
variant="secondary"
className={`cursor-pointer ${getScoreColor(score)} text-white ${className}`}
title={showTooltip ? undefined : "Click to see relevance score details"}
>
<TrendingUp className="w-3 h-3 mr-1" />
{formatScore(score)}
</Badge>
);
return (
<>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
{showTooltip ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
{badge}
</DialogTrigger>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
{createTooltipContent()}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<DialogTrigger asChild>
{badge}
</DialogTrigger>
)}
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Relevance Score Details
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Final Score: {formatScore(score)}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground">
{type.charAt(0).toUpperCase() + type.slice(1)} relevance score
</div>
</CardContent>
</Card>
{details && (
<>
<Card>
<CardHeader>
<CardTitle className="text-base">Score Breakdown</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-blue-500 rounded"></div>
<span>Base Score</span>
</div>
<span className="font-mono">{formatScore(details.baseScore)}</span>
</div>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<ThumbsUp className="w-4 h-4 text-green-500" />
<span>Engagement ({details.upvotes} upvotes, {details.comments} comments)</span>
</div>
<span className="font-mono">+{formatScore(details.engagementScore)}</span>
</div>
{details.authorVerificationBonus > 0 && (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<UserCheck className="w-4 h-4 text-purple-500" />
<span>Author Verification Bonus</span>
</div>
<span className="font-mono">+{formatScore(details.authorVerificationBonus)}</span>
</div>
)}
{details.verifiedUpvoteBonus > 0 && (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-indigo-500" />
<span>Verified Upvotes ({details.verifiedUpvotes})</span>
</div>
<span className="font-mono">+{formatScore(details.verifiedUpvoteBonus)}</span>
</div>
)}
{details.verifiedCommenterBonus > 0 && (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-teal-500" />
<span>Verified Commenters ({details.verifiedCommenters})</span>
</div>
<span className="font-mono">+{formatScore(details.verifiedCommenterBonus)}</span>
</div>
)}
<Separator />
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-orange-500" />
<span>Time Decay ({details.daysOld.toFixed(1)} days old)</span>
</div>
<span className="font-mono">×{details.timeDecayMultiplier.toFixed(3)}</span>
</div>
{details.isModerated && (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-red-500" />
<span>Moderation Penalty</span>
</div>
<span className="font-mono">×{details.moderationPenalty.toFixed(1)}</span>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">User Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<UserCheck className={`w-4 h-4 ${details.isVerified ? 'text-green-500' : 'text-gray-400'}`} />
<span className={details.isVerified ? 'text-green-600' : 'text-gray-500'}>
{details.isVerified ? 'Verified User' : 'Unverified User'}
</span>
</div>
</CardContent>
</Card>
</>
)}
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { Cell, Post, Comment, OpchanMessage, User } from '@/types';
import { useToast } from '@/components/ui/use-toast';
import { useAuth } from '@/contexts/useAuth';
import {
@ -17,7 +17,8 @@ import {
initializeNetwork
} from '@/lib/waku/network';
import messageManager from '@/lib/waku';
import { transformCell, transformComment, transformPost } from '@/lib/forum/transformers';
import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator, UserVerificationStatus } from '@/lib/forum/relevance';
import { AuthService } from '@/lib/identity/services/AuthService';
interface ForumContextType {
@ -92,26 +93,45 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
(message: OpchanMessage) => authService.verifyMessage(message) :
undefined;
// Transform cells with verification
setCells(
Object.values(messageManager.messageCache.cells)
.map(cell => transformCell(cell, verifyFn))
.filter(cell => cell !== null) as Cell[]
);
// Build user verification status for relevance calculation
const relevanceCalculator = new RelevanceCalculator();
const allUsers: User[] = [];
// Transform posts with verification
setPosts(
Object.values(messageManager.messageCache.posts)
.map(post => transformPost(post, verifyFn))
.filter(post => post !== null) as Post[]
);
// Collect all unique users from posts, comments, and votes
const userAddresses = new Set<string>();
// Transform comments with verification
setComments(
Object.values(messageManager.messageCache.comments)
.map(comment => transformComment(comment, verifyFn))
.filter(comment => comment !== null) as Comment[]
);
// Add users from posts
Object.values(messageManager.messageCache.posts).forEach(post => {
userAddresses.add(post.author);
});
// Add users from comments
Object.values(messageManager.messageCache.comments).forEach(comment => {
userAddresses.add(comment.author);
});
// Add users from votes
Object.values(messageManager.messageCache.votes).forEach(vote => {
userAddresses.add(vote.author);
});
// Create user objects for verification status
Array.from(userAddresses).forEach(address => {
allUsers.push({
address,
walletType: 'bitcoin', // Default, will be updated if we have more info
verificationStatus: 'unverified'
});
});
const userVerificationStatus = relevanceCalculator.buildUserVerificationStatus(allUsers);
// Transform data with relevance calculation
const { cells, posts, comments } = getDataFromCache(verifyFn, userVerificationStatus);
setCells(cells);
setPosts(posts);
setComments(comments);
}, [authService, isAuthenticated]);
const handleRefreshData = async () => {

59
src/lib/forum/sorting.ts Normal file
View File

@ -0,0 +1,59 @@
import { Post, Comment, Cell } from '@/types';
export type SortOption = 'relevance' | 'time';
/**
* Sort posts by relevance score (highest first)
*/
export const sortByRelevance = (items: Post[] | Comment[] | Cell[]) => {
return items.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
};
/**
* Sort by timestamp (newest first)
*/
export const sortByTime = (items: Post[] | Comment[] | Cell[]) => {
return items.sort((a, b) => b.timestamp - a.timestamp);
};
/**
* Sort posts with a specific option
*/
export const sortPosts = (posts: Post[], option: SortOption): Post[] => {
switch (option) {
case 'relevance':
return sortByRelevance(posts) as Post[];
case 'time':
return sortByTime(posts) as Post[];
default:
return sortByRelevance(posts) as Post[];
}
};
/**
* Sort comments with a specific option
*/
export const sortComments = (comments: Comment[], option: SortOption): Comment[] => {
switch (option) {
case 'relevance':
return sortByRelevance(comments) as Comment[];
case 'time':
return sortByTime(comments) as Comment[];
default:
return sortByRelevance(comments) as Comment[];
}
};
/**
* Sort cells with a specific option
*/
export const sortCells = (cells: Cell[], option: SortOption): Cell[] => {
switch (option) {
case 'relevance':
return sortByRelevance(cells) as Cell[];
case 'time':
return sortByTime(cells) as Cell[];
default:
return sortByRelevance(cells) as Cell[];
}
};

View File

@ -1,19 +1,22 @@
import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { Cell, Post, Comment, OpchanMessage, User } from '@/types';
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
import messageManager from '@/lib/waku';
import { RelevanceCalculator, UserVerificationStatus } from './relevance';
type VerifyFunction = (message: OpchanMessage) => boolean;
export const transformCell = (
cellMessage: CellMessage,
verifyMessage?: VerifyFunction
verifyMessage?: VerifyFunction,
userVerificationStatus?: UserVerificationStatus,
posts?: Post[]
): Cell | null => {
if (verifyMessage && !verifyMessage(cellMessage)) {
console.warn(`Cell message ${cellMessage.id} failed verification`);
return null;
}
return {
const transformedCell = {
id: cellMessage.id,
name: cellMessage.name,
description: cellMessage.description,
@ -21,11 +24,39 @@ export const transformCell = (
signature: cellMessage.signature,
browserPubKey: cellMessage.browserPubKey,
};
// Calculate relevance score if user verification status and posts are provided
if (userVerificationStatus && posts) {
const relevanceCalculator = new RelevanceCalculator();
const relevanceResult = relevanceCalculator.calculateCellScore(
transformedCell,
posts,
userVerificationStatus
);
// Calculate active member count
const cellPosts = posts.filter(post => post.cellId === cellMessage.id);
const activeMembers = new Set<string>();
cellPosts.forEach(post => {
activeMembers.add(post.authorAddress);
});
return {
...transformedCell,
relevanceScore: relevanceResult.score,
activeMemberCount: activeMembers.size,
relevanceDetails: relevanceResult.details
};
}
return transformedCell;
};
export const transformPost = (
postMessage: PostMessage,
verifyMessage?: VerifyFunction
verifyMessage?: VerifyFunction,
userVerificationStatus?: UserVerificationStatus
): Post | null => {
if (verifyMessage && !verifyMessage(postMessage)) {
console.warn(`Post message ${postMessage.id} failed verification`);
@ -48,7 +79,7 @@ export const transformPost = (
);
const isUserModerated = !!userModMsg;
return {
const transformedPost = {
id: postMessage.id,
cellId: postMessage.cellId,
authorAddress: postMessage.author,
@ -64,11 +95,56 @@ export const transformPost = (
moderationReason: isPostModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined,
moderationTimestamp: isPostModerated ? modMsg.timestamp : isUserModerated ? userModMsg!.timestamp : undefined,
};
// Calculate relevance score if user verification status is provided
if (userVerificationStatus) {
const relevanceCalculator = new RelevanceCalculator();
// Get comments for this post
const comments = Object.values(messageManager.messageCache.comments)
.map((comment) => transformComment(comment, verifyMessage, userVerificationStatus))
.filter(Boolean) as Comment[];
const postComments = comments.filter(comment => comment.postId === postMessage.id);
const relevanceResult = relevanceCalculator.calculatePostScore(
transformedPost,
filteredVotes,
postComments,
userVerificationStatus
);
const relevanceScore = relevanceResult.score;
// Calculate verified upvotes and commenters
const verifiedUpvotes = upvotes.filter(vote => {
const voterStatus = userVerificationStatus[vote.author];
return voterStatus?.isVerified;
}).length;
const verifiedCommenters = new Set<string>();
postComments.forEach(comment => {
const commenterStatus = userVerificationStatus[comment.authorAddress];
if (commenterStatus?.isVerified) {
verifiedCommenters.add(comment.authorAddress);
}
});
return {
...transformedPost,
relevanceScore,
verifiedUpvotes,
verifiedCommenters: Array.from(verifiedCommenters),
relevanceDetails: relevanceResult.details
};
}
return transformedPost;
};
export const transformComment = (
commentMessage: CommentMessage,
verifyMessage?: VerifyFunction,
userVerificationStatus?: UserVerificationStatus
): Comment | null => {
if (verifyMessage && !verifyMessage(commentMessage)) {
console.warn(`Comment message ${commentMessage.id} failed verification`);
@ -93,7 +169,7 @@ export const transformComment = (
);
const isUserModerated = !!userModMsg;
return {
const transformedComment = {
id: commentMessage.id,
postId: commentMessage.postId,
authorAddress: commentMessage.author,
@ -108,6 +184,25 @@ export const transformComment = (
moderationReason: isCommentModerated ? modMsg.reason : isUserModerated ? userModMsg!.reason : undefined,
moderationTimestamp: isCommentModerated ? modMsg.timestamp : isUserModerated ? userModMsg!.timestamp : undefined,
};
// Calculate relevance score if user verification status is provided
if (userVerificationStatus) {
const relevanceCalculator = new RelevanceCalculator();
const relevanceResult = relevanceCalculator.calculateCommentScore(
transformedComment,
filteredVotes,
userVerificationStatus
);
return {
...transformedComment,
relevanceScore: relevanceResult.score,
relevanceDetails: relevanceResult.details
};
}
return transformedComment;
};
export const transformVote = (
@ -121,15 +216,23 @@ export const transformVote = (
return voteMessage;
};
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
const cells = Object.values(messageManager.messageCache.cells)
.map((cell) => transformCell(cell, verifyMessage))
.filter(Boolean) as Cell[];
export const getDataFromCache = (
verifyMessage?: VerifyFunction,
userVerificationStatus?: UserVerificationStatus
) => {
// First transform posts and comments to get relevance scores
const posts = Object.values(messageManager.messageCache.posts)
.map((post) => transformPost(post, verifyMessage))
.map((post) => transformPost(post, verifyMessage, userVerificationStatus))
.filter(Boolean) as Post[];
const comments = Object.values(messageManager.messageCache.comments)
.map((c) => transformComment(c, verifyMessage))
.map((c) => transformComment(c, verifyMessage, userVerificationStatus))
.filter(Boolean) as Comment[];
// Then transform cells with posts for relevance calculation
const cells = Object.values(messageManager.messageCache.cells)
.map((cell) => transformCell(cell, verifyMessage, userVerificationStatus, posts))
.filter(Boolean) as Cell[];
return { cells, posts, comments };
};

25
src/lib/forum/types.ts Normal file
View File

@ -0,0 +1,25 @@
export interface RelevanceScoreDetails {
baseScore: number;
engagementScore: number;
authorVerificationBonus: number;
verifiedUpvoteBonus: number;
verifiedCommenterBonus: number;
timeDecayMultiplier: number;
moderationPenalty: number;
finalScore: number;
isVerified: boolean;
upvotes: number;
comments: number;
verifiedUpvotes: number;
verifiedCommenters: number;
daysOld: number;
isModerated: boolean;
}
export interface UserVerificationStatus {
[address: string]: {
isVerified: boolean;
hasENS: boolean;
hasOrdinal: boolean;
};
}

View File

@ -1,11 +1,13 @@
import React, { useMemo } from 'react';
import { RefreshCw, Plus } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { RefreshCw, Plus, TrendingUp, Clock } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import PostCard from '@/components/PostCard';
import FeedSidebar from '@/components/FeedSidebar';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { sortPosts, SortOption } from '@/lib/forum/sorting';
const FeedPage: React.FC = () => {
const {
@ -16,13 +18,13 @@ const FeedPage: React.FC = () => {
refreshData
} = useForum();
const { verificationStatus } = useAuth();
const [sortOption, setSortOption] = useState<SortOption>('relevance');
// Combine posts from all cells and sort by timestamp (newest first)
// Combine posts from all cells and apply sorting
const allPosts = useMemo(() => {
return [...posts]
.sort((a, b) => b.timestamp - a.timestamp)
.filter(post => !post.moderated); // Hide moderated posts from main feed
}, [posts]);
const filteredPosts = posts.filter(post => !post.moderated); // Hide moderated posts from main feed
return sortPosts(filteredPosts, sortOption);
}, [posts, sortOption]);
// Calculate comment counts for each post
const getCommentCount = (postId: string) => {
@ -92,6 +94,26 @@ const FeedPage: React.FC = () => {
</p>
</div>
<div className="flex items-center space-x-2">
<Select value={sortOption} onValueChange={(value: SortOption) => setSortOption(value)}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
<span>Relevance</span>
</div>
</SelectItem>
<SelectItem value="time">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>Newest</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"

View File

@ -1,4 +1,5 @@
import { CellMessage, CommentMessage, PostMessage, VoteMessage, ModerateMessage } from "@/lib/waku/types";
import { RelevanceScoreDetails } from "@/lib/forum/relevance";
export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage;
@ -30,6 +31,9 @@ export interface Cell {
icon?: string;
signature?: string; // Message signature
browserPubKey?: string; // Public key that signed the message
relevanceScore?: number; // Calculated relevance score
activeMemberCount?: number; // Number of active members in the cell
relevanceDetails?: RelevanceScoreDetails; // Detailed breakdown of relevance score calculation
}
export interface Post {
@ -47,6 +51,10 @@ export interface Post {
moderatedBy?: string;
moderationReason?: string;
moderationTimestamp?: number;
relevanceScore?: number; // Calculated relevance score
verifiedUpvotes?: number; // Count of upvotes from verified users
verifiedCommenters?: string[]; // List of verified users who commented
relevanceDetails?: RelevanceScoreDetails; // Detailed breakdown of relevance score calculation
}
export interface Comment {
@ -63,6 +71,8 @@ export interface Comment {
moderatedBy?: string;
moderationReason?: string;
moderationTimestamp?: number;
relevanceScore?: number; // Calculated relevance score
relevanceDetails?: RelevanceScoreDetails; // Detailed breakdown of relevance score calculation
}
// Extended message types for verification