feat: verify users in the feed / display ENS

This commit is contained in:
Danish Arora 2025-08-11 12:23:08 +05:30
parent 808820b4f4
commit 4ae89d69bb
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 205 additions and 24 deletions

View File

@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton';
import { MessageSquareText, Newspaper } from 'lucide-react';
import { AuthorDisplay } from './ui/author-display';
interface FeedItemBase {
id: string;
@ -33,7 +34,7 @@ interface CommentFeedItem extends FeedItemBase {
type FeedItem = PostFeedItem | CommentFeedItem;
const ActivityFeed: React.FC = () => {
const { posts, comments, cells, getCellById, isInitialLoading } = useForum();
const { posts, comments, cells, getCellById, isInitialLoading, userVerificationStatus } = useForum();
const combinedFeed: FeedItem[] = [
...posts.map((post): PostFeedItem => ({
@ -66,7 +67,6 @@ const ActivityFeed: React.FC = () => {
const renderFeedItem = (item: FeedItem) => {
const cell = item.cellId ? getCellById(item.cellId) : undefined;
const ownerShort = `${item.ownerAddress.slice(0, 5)}...${item.ownerAddress.slice(-4)}`;
const timeAgo = formatDistanceToNow(new Date(item.timestamp), { addSuffix: true });
const linkTarget = item.type === 'post' ? `/post/${item.postId}` : `/post/${item.postId}#comment-${item.id}`;
@ -83,7 +83,12 @@ const ActivityFeed: React.FC = () => {
{item.type === 'post' ? item.title : `Comment on: ${posts.find(p => p.id === item.postId)?.title || 'post'}`}
</span>
by
<span className="font-medium text-foreground/70 mx-1">{ownerShort}</span>
<AuthorDisplay
address={item.ownerAddress}
userVerificationStatus={userVerificationStatus}
className="font-medium text-foreground/70 mx-1"
showBadge={false}
/>
{cell && (
<>
in

View File

@ -6,6 +6,7 @@ import { Post } from '@/types';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { AuthorDisplay } from '@/components/ui/author-display';
interface PostCardProps {
post: Post;
@ -13,7 +14,7 @@ interface PostCardProps {
}
const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
const { getCellById, votePost, isVoting } = useForum();
const { getCellById, votePost, isVoting, userVerificationStatus } = useForum();
const { isAuthenticated, currentUser } = useAuth();
const cell = getCellById(post.cellId);
@ -80,7 +81,13 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
<div className="flex items-center text-xs text-cyber-neutral mb-2 space-x-2">
<span className="font-medium text-cyber-accent">r/{cellName}</span>
<span></span>
<span>Posted by u/{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
<span>Posted by u/</span>
<AuthorDisplay
address={post.authorAddress}
userVerificationStatus={userVerificationStatus}
className="text-xs"
showBadge={false}
/>
<span></span>
<span>{formatDistanceToNow(new Date(post.timestamp), { addSuffix: true })}</span>
{post.relevanceScore !== undefined && (

View File

@ -10,6 +10,7 @@ import { Comment } from '@/types';
import { CypherImage } from './ui/CypherImage';
import { Badge } from '@/components/ui/badge';
import { RelevanceIndicator } from './ui/relevance-indicator';
import { AuthorDisplay } from './ui/author-display';
const PostDetail = () => {
const { postId } = useParams<{ postId: string }>();
@ -28,7 +29,8 @@ const PostDetail = () => {
isRefreshing,
refreshData,
moderateComment,
moderateUser
moderateUser,
userVerificationStatus
} = useForum();
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
const [newComment, setNewComment] = useState('');
@ -163,9 +165,11 @@ const PostDetail = () => {
<MessageCircle className="w-3 h-3 mr-1" />
{postComments.length} {postComments.length === 1 ? 'comment' : 'comments'}
</span>
<span className="truncate max-w-[150px]">
{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}
</span>
<AuthorDisplay
address={post.authorAddress}
userVerificationStatus={userVerificationStatus}
className="truncate max-w-[150px]"
/>
{post.relevanceScore !== undefined && (
<RelevanceIndicator
score={post.relevanceScore}
@ -258,9 +262,11 @@ const PostDetail = () => {
alt={comment.authorAddress.slice(0, 6)}
className="rounded-sm w-5 h-5 bg-secondary"
/>
<span className="text-xs text-muted-foreground">
{comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)}
</span>
<AuthorDisplay
address={comment.authorAddress}
userVerificationStatus={userVerificationStatus}
className="text-xs"
/>
</div>
<div className="flex items-center gap-2">
{comment.relevanceScore !== undefined && (

View File

@ -10,6 +10,7 @@ import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, Ref
import { formatDistanceToNow } from 'date-fns';
import { CypherImage } from './ui/CypherImage';
import { Badge } from '@/components/ui/badge';
import { AuthorDisplay } from './ui/author-display';
const PostList = () => {
const { cellId } = useParams<{ cellId: string }>();
@ -26,7 +27,8 @@ const PostList = () => {
isVoting,
posts,
moderatePost,
moderateUser
moderateUser,
userVerificationStatus
} = useForum();
const { isAuthenticated, currentUser, verificationStatus } = useAuth();
const [newPostTitle, setNewPostTitle] = useState('');
@ -258,7 +260,13 @@ const PostList = () => {
<p className="line-clamp-2 text-sm mb-3">{post.content}</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral">
<span>{formatDistanceToNow(post.timestamp, { addSuffix: true })}</span>
<span>by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)}</span>
<span>by </span>
<AuthorDisplay
address={post.authorAddress}
userVerificationStatus={userVerificationStatus}
className="text-xs"
showBadge={false}
/>
</div>
</Link>
{isCellAdmin && !post.moderated && (

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Badge } from '@/components/ui/badge';
import { Shield, Crown } from 'lucide-react';
import { UserVerificationStatus } from '@/lib/forum/types';
interface AuthorDisplayProps {
address: string;
userVerificationStatus?: UserVerificationStatus;
className?: string;
showBadge?: boolean;
}
export function AuthorDisplay({
address,
userVerificationStatus,
className = "",
showBadge = true
}: AuthorDisplayProps) {
const userStatus = userVerificationStatus?.[address];
const isVerified = userStatus?.isVerified || false;
const hasENS = userStatus?.hasENS || false;
const hasOrdinal = userStatus?.hasOrdinal || false;
// Get ENS name from user verification status if available
const ensName = userStatus?.ensName;
const displayName = ensName || `${address.slice(0, 6)}...${address.slice(-4)}`;
return (
<div className={`flex items-center gap-1.5 ${className}`}>
<span className="text-xs text-muted-foreground">
{displayName}
</span>
{showBadge && isVerified && (
<Badge
variant="secondary"
className="text-xs px-1.5 py-0.5 h-auto bg-green-900/20 border-green-500/30 text-green-400"
>
{hasENS ? (
<>
<Crown className="w-3 h-3 mr-1" />
ENS
</>
) : hasOrdinal ? (
<>
<Shield className="w-3 h-3 mr-1" />
Ordinal
</>
) : (
<>
<Shield className="w-3 h-3 mr-1" />
Verified
</>
)}
</Badge>
)}
</div>
);
}

View File

@ -18,13 +18,16 @@ import {
} from '@/lib/waku/network';
import messageManager from '@/lib/waku';
import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator, UserVerificationStatus } from '@/lib/forum/relevance';
import { RelevanceCalculator } from '@/lib/forum/relevance';
import { UserVerificationStatus } from '@/lib/forum/types';
import { AuthService } from '@/lib/identity/services/AuthService';
interface ForumContextType {
cells: Cell[];
posts: Post[];
comments: Comment[];
// User verification status for display
userVerificationStatus: UserVerificationStatus;
// Granular loading states
isInitialLoading: boolean;
isPostingCell: boolean;
@ -80,6 +83,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const [isRefreshing, setIsRefreshing] = useState(false);
const [isNetworkConnected, setIsNetworkConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const [userVerificationStatus, setUserVerificationStatus] = useState<UserVerificationStatus>({});
const { toast } = useToast();
const { currentUser, isAuthenticated } = useAuth();
@ -117,11 +121,27 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
// 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'
});
// Check if this address matches the current user's address
if (currentUser && currentUser.address === address) {
// Use the current user's actual verification status
allUsers.push({
address,
walletType: currentUser.walletType,
verificationStatus: currentUser.verificationStatus,
ensOwnership: currentUser.ensOwnership,
ensName: currentUser.ensName,
ensAvatar: currentUser.ensAvatar,
ordinalOwnership: currentUser.ordinalOwnership,
lastChecked: currentUser.lastChecked
});
} else {
// Create generic user object for other addresses
allUsers.push({
address,
walletType: 'bitcoin', // Default, will be updated if we have more info
verificationStatus: 'unverified'
});
}
});
const userVerificationStatus = relevanceCalculator.buildUserVerificationStatus(allUsers);
@ -132,7 +152,8 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
setCells(cells);
setPosts(posts);
setComments(comments);
}, [authService, isAuthenticated]);
setUserVerificationStatus(userVerificationStatus);
}, [authService, isAuthenticated, currentUser]);
const handleRefreshData = async () => {
setIsRefreshing(true);
@ -327,6 +348,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
cells,
posts,
comments,
userVerificationStatus,
isInitialLoading,
isPostingCell,
isPostingPost,

View File

@ -1,12 +1,12 @@
import { RelevanceCalculator } from './relevance';
import { Post, Comment, Cell, User } from '@/types';
import { MessageType, VoteMessage } from '@/lib/waku/types';
import { VoteMessage, MessageType } from '@/lib/waku/types';
import { expect, describe, beforeEach, it } from 'vitest';
import { UserVerificationStatus } from './types';
describe('RelevanceCalculator', () => {
let calculator: RelevanceCalculator;
let mockUserVerificationStatus: UserVerificationStatus;
let mockUserVerificationStatus: any;
beforeEach(() => {
calculator = new RelevanceCalculator();
@ -55,6 +55,46 @@ describe('RelevanceCalculator', () => {
expect(result.details.authorVerificationBonus).toBeGreaterThan(0);
});
it('should correctly identify verified users with ENS ownership', () => {
const verifiedUser: User = {
address: 'user1',
walletType: 'ethereum',
verificationStatus: 'verified-owner',
ensOwnership: true,
ensName: 'test.eth',
lastChecked: Date.now()
};
const isVerified = calculator.isUserVerified(verifiedUser);
expect(isVerified).toBe(true);
});
it('should correctly identify verified users with Ordinal ownership', () => {
const verifiedUser: User = {
address: 'user3',
walletType: 'bitcoin',
verificationStatus: 'verified-owner',
ordinalOwnership: true,
lastChecked: Date.now()
};
const isVerified = calculator.isUserVerified(verifiedUser);
expect(isVerified).toBe(true);
});
it('should correctly identify unverified users', () => {
const unverifiedUser: User = {
address: 'user2',
walletType: 'ethereum',
verificationStatus: 'unverified',
ensOwnership: false,
lastChecked: Date.now()
};
const isVerified = calculator.isUserVerified(unverifiedUser);
expect(isVerified).toBe(false);
});
it('should apply moderation penalty', () => {
const post: Post = {
id: '1',
@ -138,4 +178,36 @@ describe('RelevanceCalculator', () => {
expect(recentResult.score).toBeGreaterThan(oldResult.score);
});
});
describe('buildUserVerificationStatus', () => {
it('should correctly build verification status map from users array', () => {
const users: User[] = [
{
address: 'user1',
walletType: 'ethereum',
verificationStatus: 'verified-owner',
ensOwnership: true,
ensName: 'test.eth',
lastChecked: Date.now()
},
{
address: 'user2',
walletType: 'bitcoin',
verificationStatus: 'unverified',
ordinalOwnership: false,
lastChecked: Date.now()
}
];
const status = calculator.buildUserVerificationStatus(users);
expect(status['user1'].isVerified).toBe(true);
expect(status['user1'].hasENS).toBe(true);
expect(status['user1'].hasOrdinal).toBe(false);
expect(status['user2'].isVerified).toBe(false);
expect(status['user2'].hasENS).toBe(false);
expect(status['user2'].hasOrdinal).toBe(false);
});
});
});

View File

@ -217,7 +217,8 @@ export class RelevanceCalculator {
status[user.address] = {
isVerified: this.isUserVerified(user),
hasENS: !!user.ensOwnership,
hasOrdinal: !!user.ordinalOwnership
hasOrdinal: !!user.ordinalOwnership,
ensName: user.ensName
};
});

View File

@ -21,5 +21,6 @@ export interface RelevanceScoreDetails {
isVerified: boolean;
hasENS: boolean;
hasOrdinal: boolean;
ensName?: string;
};
}