diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx index 6fe330e..94a7b46 100644 --- a/src/components/ActivityFeed.tsx +++ b/src/components/ActivityFeed.tsx @@ -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'}`} by - {ownerShort} + {cell && ( <> in diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index b238f70..89964fb 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -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 = ({ 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 = ({ post, commentCount = 0 }) => {
r/{cellName} - Posted by u/{post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} + Posted by u/ + {formatDistanceToNow(new Date(post.timestamp), { addSuffix: true })} {post.relevanceScore !== undefined && ( diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx index 6b956c8..516f877 100644 --- a/src/components/PostDetail.tsx +++ b/src/components/PostDetail.tsx @@ -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 = () => { {postComments.length} {postComments.length === 1 ? 'comment' : 'comments'} - - {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} - + {post.relevanceScore !== undefined && ( { alt={comment.authorAddress.slice(0, 6)} className="rounded-sm w-5 h-5 bg-secondary" /> - - {comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)} - +
{comment.relevanceScore !== undefined && ( diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx index 4f5b424..446a79e 100644 --- a/src/components/PostList.tsx +++ b/src/components/PostList.tsx @@ -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 = () => {

{post.content}

{formatDistanceToNow(post.timestamp, { addSuffix: true })} - by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} + by +
{isCellAdmin && !post.moderated && ( diff --git a/src/components/ui/author-display.tsx b/src/components/ui/author-display.tsx new file mode 100644 index 0000000..1b2731f --- /dev/null +++ b/src/components/ui/author-display.tsx @@ -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 ( +
+ + {displayName} + + + {showBadge && isVerified && ( + + {hasENS ? ( + <> + + ENS + + ) : hasOrdinal ? ( + <> + + Ordinal + + ) : ( + <> + + Verified + + )} + + )} +
+ ); +} diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 227d25d..c62f81a 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -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(null); + const [userVerificationStatus, setUserVerificationStatus] = useState({}); 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, diff --git a/src/lib/forum/relevance.test.ts b/src/lib/forum/relevance.test.ts index 5f89371..81caf21 100644 --- a/src/lib/forum/relevance.test.ts +++ b/src/lib/forum/relevance.test.ts @@ -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); + }); + }); }); diff --git a/src/lib/forum/relevance.ts b/src/lib/forum/relevance.ts index 037bb54..39d7140 100644 --- a/src/lib/forum/relevance.ts +++ b/src/lib/forum/relevance.ts @@ -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 }; }); diff --git a/src/lib/forum/types.ts b/src/lib/forum/types.ts index 4016bef..985c7f3 100644 --- a/src/lib/forum/types.ts +++ b/src/lib/forum/types.ts @@ -21,5 +21,6 @@ export interface RelevanceScoreDetails { isVerified: boolean; hasENS: boolean; hasOrdinal: boolean; + ensName?: string; }; } \ No newline at end of file