mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
feat: verify users in the feed / display ENS
This commit is contained in:
parent
808820b4f4
commit
4ae89d69bb
@ -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
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 && (
|
||||
|
||||
59
src/components/ui/author-display.tsx
Normal file
59
src/components/ui/author-display.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -21,5 +21,6 @@ export interface RelevanceScoreDetails {
|
||||
isVerified: boolean;
|
||||
hasENS: boolean;
|
||||
hasOrdinal: boolean;
|
||||
ensName?: string;
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user