From 8129c78e1caef592731d8162f79a16128f9237d1 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 24 Apr 2025 14:31:00 +0530 Subject: [PATCH] chore: ordinal related improvements --- src/components/Header.tsx | 121 ++++++++++++++++++++++++--- src/components/PostDetail.tsx | 152 ++++++++++++++++------------------ src/components/PostList.tsx | 108 +++++++++++++++++------- src/contexts/AuthContext.tsx | 42 ++++++++-- 4 files changed, 291 insertions(+), 132 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 2d197dc..752f512 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,11 +4,11 @@ import { useAuth } from '@/contexts/AuthContext'; import { useForum } from '@/contexts/ForumContext'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff } from 'lucide-react'; +import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; const Header = () => { - const { currentUser, isAuthenticated, connectWallet, disconnectWallet, verifyOrdinal } = useAuth(); + const { currentUser, isAuthenticated, verificationStatus, connectWallet, disconnectWallet, verifyOrdinal } = useAuth(); const { isNetworkConnected, isRefreshing } = useForum(); const handleConnect = async () => { @@ -22,6 +22,111 @@ const Header = () => { const handleVerify = async () => { await verifyOrdinal(); }; + + const renderAccessBadge = () => { + if (verificationStatus === 'unverified') { + return ( + + ); + } + + if (verificationStatus === 'verifying') { + return ( + + + Verifying... + + ); + } + + if (verificationStatus === 'verified-none') { + return ( +
+ + + + + Read-Only Access + + + +

Wallet Verified - No Ordinals Found

+

Your wallet has been verified but does not contain any Ordinal Operators.

+

You can browse content but cannot post, comment, or vote.

+
+
+ + + + + +

Verify again

+
+
+
+ ); + } + + if (verificationStatus === 'verified-owner') { + return ( +
+ + + + + Full Access + + + +

Ordinal Operators Verified!

+

You have full forum access with permission to post, comment, and vote.

+
+
+ + + + + +

Verify again

+
+
+
+ ); + } + + return null; + }; return (
@@ -76,17 +181,7 @@ const Header = () => { ) : ( <> - {!isAuthenticated && ( - - )} + {renderAccessBadge()} {currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)} diff --git a/src/components/PostDetail.tsx b/src/components/PostDetail.tsx index 6d526b4..90081f1 100644 --- a/src/components/PostDetail.tsx +++ b/src/components/PostDetail.tsx @@ -5,10 +5,11 @@ import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Skeleton } from '@/components/ui/skeleton'; -import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw } from 'lucide-react'; +import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send, RefreshCw, Eye } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import { Comment } from '@/types'; import { CypherImage } from './ui/CypherImage'; +import { Badge } from '@/components/ui/badge'; const PostDetail = () => { const { postId } = useParams<{ postId: string }>(); @@ -27,34 +28,21 @@ const PostDetail = () => { isRefreshing, refreshData } = useForum(); - const { currentUser, isAuthenticated } = useAuth(); + const { currentUser, isAuthenticated, verificationStatus } = useAuth(); const [newComment, setNewComment] = useState(''); - if (!postId || isInitialLoading) { + if (!postId) return
Invalid post ID
; + + if (isInitialLoading) { return ( -
-
-
- Loading... -
-
- -
- - - -
- - - +
+ + + +
- {[...Array(2)].map((_, i) => ( -
- - - -
- ))} + +
); @@ -64,25 +52,18 @@ const PostDetail = () => { if (!post) { return ( -
-
- - Back to Cells - -
-
-

Post Not Found

-

The post you're looking for doesn't exist or may have been pruned.

- -
+
+

Post not found

+

The post you're looking for doesn't exist or has been removed.

+
); } const cell = getCellById(post.cellId); - const postComments = getCommentsByPost(postId); + const postComments = getCommentsByPost(post.id); const handleCreateComment = async (e: React.FormEvent) => { e.preventDefault(); @@ -100,12 +81,12 @@ const PostDetail = () => { }; const handleVotePost = async (isUpvote: boolean) => { - if (!isAuthenticated) return; + if (verificationStatus !== 'verified-owner') return; await votePost(post.id, isUpvote); }; const handleVoteComment = async (commentId: string, isUpvote: boolean) => { - if (!isAuthenticated) return; + if (verificationStatus !== 'verified-owner') return; await voteComment(commentId, isUpvote); }; @@ -118,44 +99,39 @@ const PostDetail = () => { return votes.some(vote => vote.author === currentUser.address); }; + const getIdentityImageUrl = (address: string) => { + return `https://api.dicebear.com/7.x/identicon/svg?seed=${address}`; + }; + return ( -
-
- - - {cell ? `Back to ${cell.name}` : 'Back to Cells'} - +
+
-
- -
-
-
+ +
+
- {post.upvotes.length - post.downvotes.length} + {post.upvotes.length - post.downvotes.length} @@ -181,7 +157,7 @@ const PostDetail = () => {
- {isAuthenticated ? ( + {verificationStatus === 'verified-owner' ? (
@@ -202,6 +178,17 @@ const PostDetail = () => {
+ ) : verificationStatus === 'verified-none' ? ( +
+
+ +

Read-Only Mode

+
+

+ Your wallet has been verified but does not contain any Ordinal Operators. + You can browse threads but cannot comment or vote. +

+
) : (

Connect wallet and verify Ordinal ownership to comment

@@ -224,8 +211,8 @@ const PostDetail = () => { @@ -233,24 +220,29 @@ const PostDetail = () => {
- -
-

{comment.content}

-
- - +
+
+
+ + + {comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)} + +
+ {formatDistanceToNow(comment.timestamp, { addSuffix: true })} - - {comment.authorAddress.slice(0, 6)}...{comment.authorAddress.slice(-4)} -
+

{comment.content}

diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx index 1b5920c..98f0143 100644 --- a/src/components/PostList.tsx +++ b/src/components/PostList.tsx @@ -6,9 +6,10 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Skeleton } from '@/components/ui/skeleton'; -import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, RefreshCw } from 'lucide-react'; +import { ArrowLeft, MessageSquare, MessageCircle, ArrowUp, ArrowDown, Clock, RefreshCw, Eye } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import { CypherImage } from './ui/CypherImage'; +import { Badge } from '@/components/ui/badge'; const PostList = () => { const { cellId } = useParams<{ cellId: string }>(); @@ -20,9 +21,12 @@ const PostList = () => { isInitialLoading, isPostingPost, isRefreshing, - refreshData + refreshData, + votePost, + isVoting, + posts } = useForum(); - const { isAuthenticated } = useAuth(); + const { isAuthenticated, currentUser, verificationStatus } = useAuth(); const [newPostTitle, setNewPostTitle] = useState(''); const [newPostContent, setNewPostContent] = useState(''); @@ -54,7 +58,7 @@ const PostList = () => { } const cell = getCellById(cellId); - const posts = getPostsByCell(cellId); + const cellPosts = getPostsByCell(cellId); if (!cell) { return ( @@ -91,6 +95,19 @@ const PostList = () => { } }; + const handleVotePost = async (postId: string, isUpvote: boolean) => { + if (!isAuthenticated) return; + await votePost(postId, isUpvote); + }; + + const isPostVoted = (postId: string, isUpvote: boolean) => { + if (!currentUser) return false; + const post = posts.find(p => p.id === postId); + if (!post) return false; + const votes = isUpvote ? post.upvotes : post.downvotes; + return votes.some(vote => vote.author === currentUser.address); + }; + return (
@@ -123,7 +140,7 @@ const PostList = () => {
- {isAuthenticated && ( + {verificationStatus === 'verified-owner' && (

@@ -158,8 +175,30 @@ const PostList = () => {

)} + {verificationStatus === 'verified-none' && ( +
+
+ +

Read-Only Mode

+
+

+ Your wallet does not contain any Ordinal Operators. You can browse threads but cannot post or interact. +

+ No Ordinals Found +
+ )} + + {!currentUser && ( +
+

Connect wallet and verify Ordinal ownership to post

+ +
+ )} +
- {posts.length === 0 ? ( + {cellPosts.length === 0 ? (

No Threads Yet

@@ -170,32 +209,41 @@ const PostList = () => {

) : ( - posts.map(post => ( - -
-

{post.title}

-

{post.content}

-
- - - {formatDistanceToNow(post.timestamp, { addSuffix: true })} - - - - {getCommentsByPost(post.id).length} comments - -
- - {post.upvotes.length} - - {post.downvotes.length} -
- - {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} - + cellPosts.map(post => ( +
+
+
+ + {post.upvotes.length - post.downvotes.length} + +
+ +
+ +

{post.title}

+

{post.content}

+
+ {formatDistanceToNow(post.timestamp, { addSuffix: true })} + by {post.authorAddress.slice(0, 6)}...{post.authorAddress.slice(-4)} +
+
- +
)) )}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 3be908b..72d057f 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -3,10 +3,13 @@ import { useToast } from '@/components/ui/use-toast'; import { User } from '@/types'; import { OrdinalAPI } from '@/lib/identity/ordinal'; +export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; + interface AuthContextType { currentUser: User | null; isAuthenticated: boolean; isAuthenticating: boolean; + verificationStatus: VerificationStatus; connectWallet: () => Promise; disconnectWallet: () => void; verifyOrdinal: () => Promise; @@ -17,28 +20,34 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [currentUser, setCurrentUser] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); + const [verificationStatus, setVerificationStatus] = useState('unverified'); const { toast } = useToast(); const ordinalApi = new OrdinalAPI(); - // Check for existing session on mount useEffect(() => { const storedUser = localStorage.getItem('opchan-user'); if (storedUser) { try { const user = JSON.parse(storedUser); - // Check if the stored authentication is still valid (not expired) const lastChecked = user.lastChecked || 0; - const expiryTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + const expiryTime = 24 * 60 * 60 * 1000; if (Date.now() - lastChecked < expiryTime) { setCurrentUser(user); + + if ('ordinalOwnership' in user) { + setVerificationStatus(user.ordinalOwnership ? 'verified-owner' : 'verified-none'); + } else { + setVerificationStatus('unverified'); + } } else { - // Clear expired session localStorage.removeItem('opchan-user'); + setVerificationStatus('unverified'); } } catch (e) { console.error("Failed to parse stored user data", e); localStorage.removeItem('opchan-user'); + setVerificationStatus('unverified'); } } }, []); @@ -59,6 +68,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Store user data setCurrentUser(newUser); localStorage.setItem('opchan-user', JSON.stringify(newUser)); + setVerificationStatus('unverified'); toast({ title: "Wallet Connected", @@ -82,6 +92,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const disconnectWallet = () => { setCurrentUser(null); localStorage.removeItem('opchan-user'); + setVerificationStatus('unverified'); toast({ title: "Disconnected", description: "Your wallet has been disconnected.", @@ -99,8 +110,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } setIsAuthenticating(true); + setVerificationStatus('verifying'); + try { - toast({ title: "Verifying Ordinal", description: "Checking your wallet for Ordinal Operators..." }); + toast({ + title: "Verifying Ordinal", + description: "Checking your wallet for Ordinal Operators..." + }); const response = await ordinalApi.getOperatorDetails(currentUser.address); const hasOperators = response.has_operators; @@ -114,31 +130,38 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setCurrentUser(updatedUser); localStorage.setItem('opchan-user', JSON.stringify(updatedUser)); + // Update verification status + setVerificationStatus(hasOperators ? 'verified-owner' : 'verified-none'); + if (hasOperators) { toast({ title: "Ordinal Verified", - description: "You can now post and interact with the forum.", + description: "You now have full access to post and interact with the forum.", }); } else { toast({ - title: "Verification Failed", - description: "No Ordinal Operators found in the connected wallet.", - variant: "destructive", + title: "Read-Only Access", + description: "No Ordinal Operators found. You have read-only access.", + variant: "default", }); } return hasOperators; } catch (error) { console.error("Error verifying Ordinal:", error); + setVerificationStatus('unverified'); + let errorMessage = "Failed to verify Ordinal ownership. Please try again."; if (error instanceof Error) { errorMessage = error.message; } + toast({ title: "Verification Error", description: errorMessage, variant: "destructive", }); + return false; } finally { setIsAuthenticating(false); @@ -151,6 +174,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { currentUser, isAuthenticated: !!currentUser?.ordinalOwnership, isAuthenticating, + verificationStatus, connectWallet, disconnectWallet, verifyOrdinal,