wip: user info

This commit is contained in:
Danish Arora 2025-09-05 12:53:15 +05:30
parent 60fe855779
commit 30e888bae4
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
26 changed files with 814 additions and 330 deletions

22
TODO.md
View File

@ -5,6 +5,7 @@ This document outlines the features and improvements that still need to be imple
## 🚨 High Priority (1-2 weeks)
### 1. Bookmarking System
- **Requirement**: "Users can bookmark posts and topics; local only"
- **Status**: ❌ Not implemented
- **Missing**:
@ -16,6 +17,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 2-3 days
### 2. Call Sign Setup & Display
- **Requirement**: "Users can setup a call sign; bitcoin identity operator unique name - remains - ordinal used as avatar"
- **Status**: ⚠️ Partially implemented
- **Missing**:
@ -27,6 +29,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 3-4 days
### 3. Cell Icon System
- **Requirement**: "Cell can be created with a name, description, icon; icon size will be restricted"
- **Status**: ❌ Not implemented
- **Missing**:
@ -40,6 +43,7 @@ This document outlines the features and improvements that still need to be imple
## 🔶 Medium Priority (2-3 weeks)
### 4. Enhanced Sorting Options
- **Requirement**: "Users can sort topics per new or top"
- **Status**: ⚠️ Basic implementation exists
- **Missing**:
@ -51,6 +55,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 1-2 days
### 5. Active Member Count Display
- **Requirement**: "A user can see the number of active members per cell; deduced from retrievable activity"
- **Status**: ⚠️ Calculated in backend but not shown
- **Missing**:
@ -61,6 +66,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 1 day
### 6. IndexedDB Integration
- **Requirement**: "store message cache in indexedDB -- make app local-first"
- **Status**: ❌ In-memory caching only
- **Missing**:
@ -72,6 +78,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 3-4 days
### 7. Enhanced Moderation UI
- **Requirement**: "Cell admin can mark posts and comments as moderated"
- **Status**: ⚠️ Backend logic exists, basic UI
- **Missing**:
@ -86,6 +93,7 @@ This document outlines the features and improvements that still need to be imple
## 🔵 Low Priority (3-4 weeks)
### 8. Anonymous User Experience
- **Requirement**: "Anonymous users can upvote, comments and post"
- **Status**: ⚠️ Basic support but limited UX
- **Missing**:
@ -97,6 +105,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 2-3 days
### 9. Relevance Score Visibility
- **Requirement**: "The relevance index is used to push most relevant posts and comments on top"
- **Status**: ⚠️ Calculated but limited visibility
- **Missing**:
@ -108,6 +117,7 @@ This document outlines the features and improvements that still need to be imple
- **Estimated Effort**: 1-2 days
### 10. Mobile Responsiveness
- **Requirement**: "Users do not need any software beyond a browser to use the forum"
- **Status**: ❌ Basic responsive design
- **Missing**:
@ -121,18 +131,21 @@ This document outlines the features and improvements that still need to be imple
## 🛠️ Technical Debt & Infrastructure
### 11. Performance Optimizations
- [ ] Implement virtual scrolling for large lists
- [ ] Add message pagination
- [ ] Optimize relevance calculations
- [ ] Implement lazy loading for images
### 12. Testing & Quality
- [ ] Add comprehensive unit tests
- [ ] Implement integration tests
- [ ] Add end-to-end testing
- [ ] Performance testing and monitoring
### 13. Documentation
- [ ] API documentation
- [ ] User guide
- [ ] Developer setup guide
@ -141,11 +154,13 @@ This document outlines the features and improvements that still need to be imple
## 📋 Implementation Notes
### Dependencies
- Bookmarking system depends on IndexedDB integration
- Call sign setup depends on user profile system completion
- Enhanced moderation depends on existing moderation backend
### Technical Considerations
- Use React Query for state management
- Implement proper error boundaries
- Add loading states for all async operations
@ -153,6 +168,7 @@ This document outlines the features and improvements that still need to be imple
- Follow existing code patterns and conventions
### Testing Strategy
- Unit tests for utility functions
- Integration tests for hooks and contexts
- Component tests for UI elements
@ -169,11 +185,11 @@ This document outlines the features and improvements that still need to be imple
## 📅 Timeline Estimate
- **Phase 1 (High Priority)**: 1-2 weeks
- **Phase 2 (Medium Priority)**: 2-3 weeks
- **Phase 2 (Medium Priority)**: 2-3 weeks
- **Phase 3 (Low Priority)**: 3-4 weeks
- **Total Estimated Time**: 6-9 weeks
---
*Last updated: [Current Date]*
*Based on FURPS requirements analysis and codebase review*
_Last updated: [Current Date]_
_Based on FURPS requirements analysis and codebase review_

View File

@ -23,6 +23,7 @@ import PostPage from './pages/PostPage';
import NotFound from './pages/NotFound';
import Dashboard from './pages/Dashboard';
import Index from './pages/Index';
import ProfilePage from './pages/ProfilePage';
import { appkitConfig } from './lib/wallet/config';
import { WagmiProvider } from 'wagmi';
import { config } from './lib/wallet/config';
@ -46,6 +47,7 @@ const App = () => (
<Route path="/cells" element={<Index />} />
<Route path="/cell/:cellId" element={<CellPage />} />
<Route path="/post/:postId" element={<PostPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</TooltipProvider>

View File

@ -21,8 +21,71 @@ import {
import { CypherImage } from './ui/CypherImage';
import { RelevanceIndicator } from './ui/relevance-indicator';
import { sortCells, SortOption } from '@/lib/utils/sorting';
import { Cell } from '@/types/forum';
import { usePending } from '@/hooks/usePending';
// Separate component to properly use hooks
const CellItem: React.FC<{ cell: Cell }> = ({ cell }) => {
const pending = usePending(cell.id);
return (
<Link
to={`/cell/${cell.id}`}
className="group block p-4 border border-cyber-muted rounded-sm bg-cyber-muted/10 hover:bg-cyber-muted/20 hover:border-cyber-accent/50 transition-all duration-200"
>
<div className="flex items-start gap-4">
<CypherImage
src={cell.icon}
alt={cell.name}
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted flex-shrink-0"
generateUniqueFallback={true}
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<h2 className="text-lg font-bold text-glow group-hover:text-cyber-accent transition-colors line-clamp-1">
{cell.name}
</h2>
{cell.relevanceScore !== undefined && (
<RelevanceIndicator
score={cell.relevanceScore}
details={cell.relevanceDetails}
type="cell"
className="ml-2 flex-shrink-0"
showTooltip={true}
/>
)}
</div>
{pending.isPending && (
<div className="mb-2">
<span className="px-2 py-0.5 rounded-sm bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 text-xs">
syncing
</span>
</div>
)}
<p className="text-cyber-neutral text-sm mb-3 line-clamp-2">
{cell.description}
</p>
<div className="flex items-center justify-between text-xs text-cyber-neutral">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" />
{cell.postCount || 0} posts
</span>
<span className="flex items-center gap-1">
<Layout className="w-3 h-3" />
{cell.activeMemberCount || 0} members
</span>
</div>
</div>
</div>
</div>
</Link>
);
};
const CellList = () => {
const { cellsWithStats, isInitialLoading } = useForumData();
const { refreshData } = useForumActions();
@ -109,63 +172,7 @@ const CellList = () => {
</div>
</div>
) : (
sortedCells.map(cell => (
<Link
key={cell.id}
to={`/cell/${cell.id}`}
className="group block p-4 border border-cyber-muted rounded-sm bg-cyber-muted/10 hover:bg-cyber-muted/20 hover:border-cyber-accent/50 transition-all duration-200"
>
<div className="flex items-start gap-4">
<CypherImage
src={cell.icon}
alt={cell.name}
className="w-12 h-12 object-cover rounded-sm border border-cyber-muted flex-shrink-0"
generateUniqueFallback={true}
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<h2 className="text-lg font-bold text-glow group-hover:text-cyber-accent transition-colors line-clamp-1">
{cell.name}
</h2>
{cell.relevanceScore !== undefined && (
<RelevanceIndicator
score={cell.relevanceScore}
details={cell.relevanceDetails}
type="cell"
className="ml-2 flex-shrink-0"
showTooltip={true}
/>
)}
</div>
{usePending(cell.id).isPending && (
<div className="mb-2">
<span className="px-2 py-0.5 rounded-sm bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 text-xs">
syncing
</span>
</div>
)}
<p className="text-cyber-neutral text-sm mb-3 line-clamp-2">
{cell.description}
</p>
<div className="flex items-center justify-between text-xs text-cyber-neutral">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" />
{cell.postCount || 0} posts
</span>
<span className="flex items-center gap-1">
<Layout className="w-3 h-3" />
{cell.activeMemberCount || 0} members
</span>
</div>
</div>
</div>
</div>
</Link>
))
sortedCells.map(cell => <CellItem key={cell.id} cell={cell} />)
)}
</div>

View File

@ -1,17 +1,11 @@
import React, { useState } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { Plus, TrendingUp, Users, Eye } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { TrendingUp, Users, Eye } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
useForumData,
useForumSelectors,
useAuth,
usePermissions,
} from '@/hooks';
import { useForumData, useForumSelectors, useAuth } from '@/hooks';
import { CypherImage } from '@/components/ui/CypherImage';
import { CreateCellDialog } from '@/components/CreateCellDialog';
import { useUserDisplay } from '@/hooks';
const FeedSidebar: React.FC = () => {
@ -19,11 +13,9 @@ const FeedSidebar: React.FC = () => {
const forumData = useForumData();
const selectors = useForumSelectors(forumData);
const { currentUser, verificationStatus } = useAuth();
const { canCreateCell } = usePermissions();
const [showCreateCell, setShowCreateCell] = useState(false);
// Get user display information using the hook
const { displayName, hasENS, hasOrdinal } = useUserDisplay(
const { displayName, ensName, ordinalDetails } = useUserDisplay(
currentUser?.address || ''
);
@ -41,9 +33,9 @@ const FeedSidebar: React.FC = () => {
return { text: 'Verified Owner', color: 'bg-green-500' };
} else if (verificationStatus.level === 'verified-basic') {
return { text: 'Verified', color: 'bg-blue-500' };
} else if (hasENS) {
} else if (ensName) {
return { text: 'ENS User', color: 'bg-purple-500' };
} else if (hasOrdinal) {
} else if (ordinalDetails) {
return { text: 'Ordinal User', color: 'bg-orange-500' };
}
return { text: 'Unverified', color: 'bg-gray-500' };
@ -82,12 +74,13 @@ const FeedSidebar: React.FC = () => {
</div>
)}
{verificationStatus.level === 'verified-basic' && !hasOrdinal && (
<div className="text-xs text-muted-foreground">
<Eye className="w-3 h-3 inline mr-1" />
Read-only mode. Acquire Ordinals to post.
</div>
)}
{verificationStatus.level === 'verified-basic' &&
!ordinalDetails && (
<div className="text-xs text-muted-foreground">
<Eye className="w-3 h-3 inline mr-1" />
Read-only mode. Acquire Ordinals to post.
</div>
)}
</CardContent>
</Card>
)}
@ -158,31 +151,6 @@ const FeedSidebar: React.FC = () => {
)}
</CardContent>
</Card>
{/* Quick Actions */}
{canCreateCell && (
<Card className="bg-cyber-muted/20 border-cyber-muted">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Quick Actions</CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={() => setShowCreateCell(true)}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80"
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Create Cell
</Button>
</CardContent>
</Card>
)}
{/* Create Cell Dialog */}
<CreateCellDialog
open={showCreateCell}
onOpenChange={setShowCreateCell}
/>
</div>
);
};

View File

@ -13,6 +13,7 @@ import {
CircleSlash,
Home,
Grid3X3,
User,
} from 'lucide-react';
import {
Tooltip,
@ -177,6 +178,19 @@ const Header = () => {
<Grid3X3 className="w-4 h-4" />
<span>Cells</span>
</Link>
{isConnected && (
<Link
to="/profile"
className={`flex items-center space-x-1 px-3 py-1 rounded-sm text-sm transition-colors ${
location.pathname === '/profile'
? 'bg-cyber-accent/20 text-cyber-accent'
: 'text-cyber-neutral hover:text-cyber-accent hover:bg-cyber-muted/50'
}`}
>
<User className="w-4 h-4" />
<span>Profile</span>
</Link>
)}
</nav>
</div>
@ -196,7 +210,12 @@ const Header = () => {
</span>
{forum.lastSync && (
<span className="text-xs text-cyber-neutral ml-2">
Last updated {new Date(forum.lastSync).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
Last updated{' '}
{new Date(forum.lastSync).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
{forum.isSyncing ? ' • syncing…' : ''}
</span>
)}

View File

@ -185,7 +185,9 @@ const PostDetail = () => {
<ArrowDown className="w-4 h-4" />
</button>
{postVotePending.isPending && (
<span className="mt-1 text-[10px] text-yellow-500">syncing</span>
<span className="mt-1 text-[10px] text-yellow-500">
syncing
</span>
)}
</div>

View File

@ -1,7 +1,6 @@
import {
useForumData,
useAuth,
useUserDisplay,
useUserVotes,
useForumActions,
useUserActions,
@ -23,7 +22,6 @@ export function HookDemoComponent() {
// Core data hooks - reactive and optimized
const forumData = useForumData();
const auth = useAuth();
const userDisplay = useUserDisplay(auth.currentUser?.address || '');
// Derived hooks for specific data
const userVotes = useUserVotes();
@ -142,17 +140,6 @@ export function HookDemoComponent() {
</div>
</div>
{userDisplay.badges.length > 0 && (
<div className="flex gap-2">
<strong>Badges:</strong>
{userDisplay.badges.map((badge, index) => (
<Badge key={index} className={badge.color}>
{badge.icon} {badge.label}
</Badge>
))}
</div>
)}
<div className="flex gap-2">
<Button
onClick={handleDelegateKey}

View File

@ -13,11 +13,11 @@ export function AuthorDisplay({
className = '',
showBadge = true,
}: AuthorDisplayProps) {
const { displayName, hasCallSign, hasENS, hasOrdinal } =
const { displayName, callSign, ensName, ordinalDetails } =
useUserDisplay(address);
// Only show a badge if the author has ENS, Ordinal, or Call Sign
const shouldShowBadge = showBadge && (hasENS || hasOrdinal || hasCallSign);
const shouldShowBadge = showBadge && (ensName || ordinalDetails || callSign);
return (
<div className={`flex items-center gap-1.5 ${className}`}>
@ -28,12 +28,12 @@ export function AuthorDisplay({
variant="secondary"
className="text-xs px-1.5 py-0.5 h-auto bg-green-900/20 border-green-500/30 text-green-400"
>
{hasCallSign ? (
{callSign ? (
<>
<Hash className="w-3 h-3 mr-1" />
Call Sign
</>
) : hasENS ? (
) : ensName ? (
<>
<Crown className="w-3 h-3 mr-1" />
ENS

View File

@ -46,6 +46,42 @@ export function VerificationStep({
details?: OrdinalDetails | EnsDetails;
} | null>(null);
// Watch for changes in user state after verification
React.useEffect(() => {
if (
verificationResult?.success &&
verificationResult.message.includes('Checking ownership')
) {
// Check if actual ownership was verified
const hasOwnership =
walletType === 'bitcoin'
? !!currentUser?.ordinalDetails
: !!currentUser?.ensDetails;
if (hasOwnership) {
setVerificationResult({
success: true,
message:
walletType === 'bitcoin'
? 'Ordinal ownership verified successfully!'
: 'ENS ownership verified successfully!',
details:
walletType === 'bitcoin'
? currentUser?.ordinalDetails
: currentUser?.ensDetails,
});
} else {
setVerificationResult({
success: false,
message:
walletType === 'bitcoin'
? 'No Ordinal ownership found. You can still participate in the forum with your connected wallet!'
: 'No ENS ownership found. You can still participate in the forum with your connected wallet!',
});
}
}
}, [currentUser, verificationResult, walletType]);
const handleVerify = async () => {
if (!currentUser) return;
@ -56,16 +92,15 @@ export function VerificationStep({
const success = await verifyWallet();
if (success) {
// For now, just show success - the actual ownership check will be done
// by the useEffect when the user state updates
setVerificationResult({
success: true,
message:
walletType === 'bitcoin'
? 'Ordinal ownership verified successfully!'
: 'ENS ownership verified successfully!',
details:
walletType === 'bitcoin'
? currentUser.ordinalDetails
: currentUser.ensDetails,
? 'Verification process completed. Checking ownership...'
: 'Verification process completed. Checking ownership...',
details: undefined,
});
} else {
setVerificationResult({

View File

@ -1,7 +1,11 @@
import React, { createContext, useState, useEffect, useMemo } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { OpchanMessage } from '@/types/forum';
import { User, EVerificationStatus, EDisplayPreference } from '@/types/identity';
import {
User,
EVerificationStatus,
EDisplayPreference,
} from '@/types/identity';
import { WalletManager } from '@/lib/wallet';
import {
DelegationManager,

View File

@ -7,7 +7,11 @@ import React, {
useRef,
} from 'react';
import { Cell, Post, Comment } from '@/types/forum';
import { User, EVerificationStatus, EDisplayPreference } from '@/types/identity';
import {
User,
EVerificationStatus,
EDisplayPreference,
} from '@/types/identity';
import { useToast } from '@/components/ui/use-toast';
import { ForumActions } from '@/lib/forum/ForumActions';
@ -122,7 +126,6 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
// Transform message cache data to the expected types
const updateStateFromCache = useCallback(async () => {
// Build user verification status for relevance calculation
const relevanceCalculator = new RelevanceCalculator();
const allUsers: User[] = [];
@ -260,8 +263,14 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
Object.assign(messageManager.messageCache.posts, seeded.posts);
Object.assign(messageManager.messageCache.comments, seeded.comments);
Object.assign(messageManager.messageCache.votes, seeded.votes);
Object.assign(messageManager.messageCache.moderations, seeded.moderations);
Object.assign(messageManager.messageCache.userIdentities, seeded.userIdentities);
Object.assign(
messageManager.messageCache.moderations,
seeded.moderations
);
Object.assign(
messageManager.messageCache.userIdentities,
seeded.userIdentities
);
// Determine if we have any cached content
const hasSeedData =
@ -274,7 +283,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
await updateStateFromCache();
// Initialize network and let incoming messages update LocalDatabase/Cache
await initializeNetwork(toast, updateStateFromCache, setError);
await initializeNetwork(toast, setError);
if (hasSeedData) {
setIsInitialLoading(false);
@ -288,7 +297,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
} catch (e) {
console.warn('LocalDatabase warm-start failed, continuing cold:', e);
// Initialize network even if local DB failed, keep loader until first message
await initializeNetwork(toast, updateStateFromCache, setError);
await initializeNetwork(toast, setError);
const unsubscribe = messageManager.onMessageReceived(() => {
setIsInitialLoading(false);
unsubscribe();

View File

@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
import { useAuth } from '@/hooks/core/useEnhancedAuth';
import { DelegationDuration } from '@/lib/delegation';
import { useToast } from '@/components/ui/use-toast';
import { useAuth as useAuthContext } from '@/contexts/useAuth';
export interface AuthActionStates {
isConnecting: boolean;
@ -38,6 +39,7 @@ export function useAuthActions(): AuthActions {
verificationStatus,
} = useAuth();
const { verifyOwnership, delegateKey: delegateKeyFromContext } = useAuthContext();
const { toast } = useToast();
const [isConnecting, setIsConnecting] = useState(false);
@ -153,23 +155,24 @@ export function useAuthActions(): AuthActions {
setIsVerifying(true);
try {
toast({
title: 'Verifying...',
description: 'Please sign the verification message in your wallet.',
});
// This would trigger the verification process
// The actual implementation would depend on the verification system
// Simulate verification process
await new Promise(resolve => setTimeout(resolve, 3000));
toast({
title: 'Verification Complete',
description: 'Your wallet has been verified successfully.',
});
return true;
try {
// Call the real verification function from AuthContext
const success = await verifyOwnership();
if (success) {
toast({
title: 'Verification Complete',
description: 'Your wallet has been verified successfully.',
});
} else {
toast({
title: 'Verification Failed',
description: 'Failed to verify wallet ownership. Please try again.',
variant: 'destructive',
});
}
return success;
} catch (error) {
console.error('Failed to verify wallet:', error);
toast({
@ -181,7 +184,7 @@ export function useAuthActions(): AuthActions {
} finally {
setIsVerifying(false);
}
}, [isAuthenticated, verificationStatus.level, toast]);
}, [isAuthenticated, verificationStatus.level, verifyOwnership, toast]);
// Delegate key
const delegateKey = useCallback(
@ -207,21 +210,24 @@ export function useAuthActions(): AuthActions {
setIsDelegating(true);
try {
toast({
title: 'Delegating Key...',
description: 'Please sign the delegation message in your wallet.',
});
// This would trigger the key delegation process
// The actual implementation would use the DelegationManager
const durationLabel = duration === '7days' ? '1 week' : '30 days';
toast({
title: 'Key Delegated',
description: `Your signing key has been delegated for ${durationLabel}.`,
});
return true;
// Call the real delegation function from AuthContext
const success = await delegateKeyFromContext(duration);
if (success) {
const durationLabel = duration === '7days' ? '1 week' : '30 days';
toast({
title: 'Key Delegated',
description: `Your signing key has been delegated for ${durationLabel}.`,
});
} else {
toast({
title: 'Delegation Failed',
description: 'Failed to delegate signing key. Please try again.',
variant: 'destructive',
});
}
return success;
} catch (error) {
console.error('Failed to delegate key:', error);
toast({
@ -234,7 +240,7 @@ export function useAuthActions(): AuthActions {
setIsDelegating(false);
}
},
[isAuthenticated, verificationStatus.level, toast]
[isAuthenticated, verificationStatus.level, delegateKeyFromContext, toast]
);
// Clear delegation

View File

@ -2,20 +2,13 @@ import { useState, useEffect, useMemo } from 'react';
import { useForum } from '@/contexts/useForum';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
export interface Badge {
type: 'verification' | 'ens' | 'ordinal' | 'callsign';
label: string;
icon: string;
color: string;
}
export interface UserDisplayInfo {
displayName: string;
hasCallSign: boolean;
hasENS: boolean;
hasOrdinal: boolean;
callSign: string | null;
ensName: string | null;
ordinalDetails: string | null;
verificationLevel: EVerificationStatus;
badges: Badge[];
displayPreference: EDisplayPreference | null;
isLoading: boolean;
error: string | null;
}
@ -27,11 +20,11 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
const { userIdentityService, userVerificationStatus } = useForum();
const [displayInfo, setDisplayInfo] = useState<UserDisplayInfo>({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
hasCallSign: false,
hasENS: false,
hasOrdinal: false,
callSign: null,
ensName: null,
ordinalDetails: null,
verificationLevel: EVerificationStatus.UNVERIFIED,
badges: [],
displayPreference: null,
isLoading: true,
error: null,
});
@ -41,8 +34,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
return (
userVerificationStatus[address] || {
isVerified: false,
hasENS: false,
hasOrdinal: false,
ensName: null,
verificationStatus: EVerificationStatus.UNVERIFIED,
}
);
@ -66,13 +58,13 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
);
setDisplayInfo({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
hasCallSign: false,
hasENS: false,
hasOrdinal: false,
callSign: null,
ensName: verificationInfo.ensName || null,
ordinalDetails: null,
verificationLevel:
verificationInfo.verificationStatus ||
EVerificationStatus.UNVERIFIED,
badges: [],
displayPreference: null,
isLoading: false,
error: null,
});
@ -95,102 +87,30 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
displayName = identity.ensName;
}
// Generate badges
const badges: Badge[] = [];
// Verification badge
if (
identity.verificationStatus === EVerificationStatus.VERIFIED_OWNER
) {
badges.push({
type: 'verification',
label: 'Verified Owner',
icon: '🔑',
color: 'text-cyber-accent',
});
} else if (
identity.verificationStatus === EVerificationStatus.VERIFIED_BASIC
) {
badges.push({
type: 'verification',
label: 'Verified',
icon: '✅',
color: 'text-green-400',
});
}
// ENS badge
if (identity.ensName) {
badges.push({
type: 'ens',
label: 'ENS',
icon: '🏷️',
color: 'text-blue-400',
});
}
// Ordinal badge
if (identity.ordinalDetails) {
badges.push({
type: 'ordinal',
label: 'Ordinal',
icon: '⚡',
color: 'text-orange-400',
});
}
// Call sign badge
if (identity.callSign) {
badges.push({
type: 'callsign',
label: 'Call Sign',
icon: '📻',
color: 'text-purple-400',
});
}
setDisplayInfo({
displayName,
hasCallSign: Boolean(identity.callSign),
hasENS: Boolean(identity.ensName),
hasOrdinal: Boolean(identity.ordinalDetails),
callSign: identity.callSign || null,
ensName: identity.ensName || null,
ordinalDetails: identity.ordinalDetails
? identity.ordinalDetails.ordinalDetails
: null,
verificationLevel: identity.verificationStatus,
badges,
displayPreference: identity.displayPreference || null,
isLoading: false,
error: null,
});
} else {
// Use verification info from forum context
const badges: Badge[] = [];
if (verificationInfo.hasENS) {
badges.push({
type: 'ens',
label: 'ENS',
icon: '🏷️',
color: 'text-blue-400',
});
}
if (verificationInfo.hasOrdinal) {
badges.push({
type: 'ordinal',
label: 'Ordinal',
icon: '⚡',
color: 'text-orange-400',
});
}
setDisplayInfo({
displayName:
verificationInfo.ensName ||
`${address.slice(0, 6)}...${address.slice(-4)}`,
hasCallSign: false,
hasENS: verificationInfo.hasENS,
hasOrdinal: verificationInfo.hasOrdinal,
callSign: null,
ensName: verificationInfo.ensName || null,
ordinalDetails: null,
verificationLevel:
verificationInfo.verificationStatus ||
EVerificationStatus.UNVERIFIED,
badges,
displayPreference: null,
isLoading: false,
error: null,
});
@ -202,11 +122,11 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
);
setDisplayInfo({
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
hasCallSign: false,
hasENS: false,
hasOrdinal: false,
callSign: null,
ensName: null,
ordinalDetails: null,
verificationLevel: EVerificationStatus.UNVERIFIED,
badges: [],
displayPreference: null,
isLoading: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
@ -221,19 +141,16 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
if (!displayInfo.isLoading && verificationInfo) {
setDisplayInfo(prev => ({
...prev,
hasENS: verificationInfo.hasENS || prev.hasENS,
hasOrdinal: verificationInfo.hasOrdinal || prev.hasOrdinal,
ensName: verificationInfo.ensName || prev.ensName,
verificationLevel:
verificationInfo.verificationStatus || prev.verificationLevel,
}));
}
}, [
verificationInfo.ensName,
verificationInfo.hasENS,
verificationInfo.hasOrdinal,
verificationInfo.verificationStatus,
displayInfo.isLoading,
verificationInfo
verificationInfo,
]);
return displayInfo;

View File

@ -1,3 +1,3 @@
// Re-export the enhanced user display hook as the main useUserDisplay
export { useEnhancedUserDisplay as useUserDisplay } from './useEnhancedUserDisplay';
export type { Badge, UserDisplayInfo } from './useEnhancedUserDisplay';
export type { UserDisplayInfo } from './useEnhancedUserDisplay';

View File

@ -18,7 +18,7 @@ export type {
EnhancedAuthState,
} from './core/useEnhancedAuth';
export type { Badge, UserDisplayInfo } from './core/useEnhancedUserDisplay';
export type { UserDisplayInfo } from './core/useEnhancedUserDisplay';
// Derived hooks
export { useCell } from './derived/useCell';

View File

@ -44,5 +44,3 @@ export function usePendingVote(targetId: string | undefined) {
return { isPending };
}

View File

@ -113,26 +113,38 @@ export class LocalDatabase {
private storeMessage(message: OpchanMessage): void {
switch (message.type) {
case MessageType.CELL:
if (!this.cache.cells[message.id] || this.cache.cells[message.id]?.timestamp !== message.timestamp) {
if (
!this.cache.cells[message.id] ||
this.cache.cells[message.id]?.timestamp !== message.timestamp
) {
this.cache.cells[message.id] = message;
this.put(STORE.CELLS, message);
}
break;
case MessageType.POST:
if (!this.cache.posts[message.id] || this.cache.posts[message.id]?.timestamp !== message.timestamp) {
if (
!this.cache.posts[message.id] ||
this.cache.posts[message.id]?.timestamp !== message.timestamp
) {
this.cache.posts[message.id] = message;
this.put(STORE.POSTS, message);
}
break;
case MessageType.COMMENT:
if (!this.cache.comments[message.id] || this.cache.comments[message.id]?.timestamp !== message.timestamp) {
if (
!this.cache.comments[message.id] ||
this.cache.comments[message.id]?.timestamp !== message.timestamp
) {
this.cache.comments[message.id] = message;
this.put(STORE.COMMENTS, message);
}
break;
case MessageType.VOTE: {
const voteKey = `${message.targetId}:${message.author}`;
if (!this.cache.votes[voteKey] || this.cache.votes[voteKey]?.timestamp !== message.timestamp) {
if (
!this.cache.votes[voteKey] ||
this.cache.votes[voteKey]?.timestamp !== message.timestamp
) {
this.cache.votes[voteKey] = message;
this.put(STORE.VOTES, { key: voteKey, ...message });
}
@ -140,7 +152,11 @@ export class LocalDatabase {
}
case MessageType.MODERATE: {
const modMsg = message as ModerateMessage;
if (!this.cache.moderations[modMsg.targetId] || this.cache.moderations[modMsg.targetId]?.timestamp !== modMsg.timestamp) {
if (
!this.cache.moderations[modMsg.targetId] ||
this.cache.moderations[modMsg.targetId]?.timestamp !==
modMsg.timestamp
) {
this.cache.moderations[modMsg.targetId] = modMsg;
this.put(STORE.MODERATIONS, modMsg);
}
@ -150,7 +166,10 @@ export class LocalDatabase {
const profileMsg = message as UserProfileUpdateMessage;
const { author, callSign, displayPreference, timestamp } = profileMsg;
if (!this.cache.userIdentities[author] || this.cache.userIdentities[author]?.lastUpdated !== timestamp) {
if (
!this.cache.userIdentities[author] ||
this.cache.userIdentities[author]?.lastUpdated !== timestamp
) {
this.cache.userIdentities[author] = {
ensName: undefined,
ordinalDetails: undefined,
@ -182,14 +201,7 @@ export class LocalDatabase {
private async hydrateFromIndexedDB(): Promise<void> {
if (!this.db) return;
const [
cells,
posts,
comments,
votes,
moderations,
identities,
]: [
const [cells, posts, comments, votes, moderations, identities]: [
CellMessage[],
PostMessage[],
CommentMessage[],
@ -217,7 +229,9 @@ export class LocalDatabase {
return [key, vote];
})
);
this.cache.moderations = Object.fromEntries(moderations.map(m => [m.targetId, m]));
this.cache.moderations = Object.fromEntries(
moderations.map(m => [m.targetId, m])
);
this.cache.userIdentities = Object.fromEntries(
identities.map(u => {
const { address, ...record } = u;
@ -232,7 +246,10 @@ export class LocalDatabase {
STORE.META
);
meta
.filter(entry => typeof entry.key === 'string' && entry.key.startsWith('pending:'))
.filter(
entry =>
typeof entry.key === 'string' && entry.key.startsWith('pending:')
)
.forEach(entry => {
const id = (entry.key as string).substring('pending:'.length);
this.pendingIds.add(id);
@ -313,5 +330,3 @@ export class LocalDatabase {
}
export const localDatabase = new LocalDatabase();

View File

@ -60,5 +60,3 @@ export function openLocalDB(): Promise<IDBDatabase> {
};
});
}

View File

@ -85,7 +85,10 @@ export class DelegationManager {
*/
signMessage(message: UnsignedMessage): OpchanMessage | null {
const now = Date.now();
if (!this.cachedDelegation || now - this.cachedAt > DelegationManager.CACHE_TTL_MS) {
if (
!this.cachedDelegation ||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS
) {
this.cachedDelegation = DelegationStorage.retrieve();
this.cachedAt = now;
}
@ -164,7 +167,10 @@ export class DelegationManager {
currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationFullStatus {
const now = Date.now();
if (!this.cachedDelegation || now - this.cachedAt > DelegationManager.CACHE_TTL_MS) {
if (
!this.cachedDelegation ||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS
) {
this.cachedDelegation = DelegationStorage.retrieve();
this.cachedAt = now;
}

View File

@ -1,6 +1,10 @@
import { RelevanceCalculator } from '../RelevanceCalculator';
import { Post, Comment, UserVerificationStatus } from '@/types/forum';
import { User, EVerificationStatus, EDisplayPreference } from '@/types/identity';
import {
User,
EVerificationStatus,
EDisplayPreference,
} from '@/types/identity';
import { VoteMessage, MessageType } from '@/types/waku';
import { expect, describe, beforeEach, it } from 'vitest';

View File

@ -84,7 +84,8 @@ export class UserIdentityService {
}
// Fallback: Check Waku message cache
const cacheServiceData = messageManager.messageCache.userIdentities[address];
const cacheServiceData =
messageManager.messageCache.userIdentities[address];
if (cacheServiceData) {
if (import.meta.env?.DEV) {
@ -191,7 +192,10 @@ export class UserIdentityService {
await this.messageService.signAndBroadcastMessage(unsignedMessage);
if (import.meta.env?.DEV) {
console.debug('UserIdentityService: message broadcast result', !!signedMessage);
console.debug(
'UserIdentityService: message broadcast result',
!!signedMessage
);
}
return !!signedMessage;
@ -248,9 +252,15 @@ export class UserIdentityService {
}
try {
// For now, return null - ENS resolution can be added later
// This would typically call an ENS resolver API
return null;
// Import the ENS resolver from wagmi
const { getEnsName } = await import('@wagmi/core');
const { config } = await import('@/lib/wallet/config');
const ensName = await getEnsName(config, {
address: address as `0x${string}`,
});
return ensName || null;
} catch (error) {
console.error('Failed to resolve ENS name:', error);
return null;
@ -263,13 +273,9 @@ export class UserIdentityService {
private async resolveOrdinalDetails(
address: string
): Promise<{ ordinalId: string; ordinalDetails: string } | null> {
if (address.startsWith('0x')) {
return null; // Not a Bitcoin address
}
try {
// For now, return null - Ordinal resolution can be added later
// This would typically call an Ordinal API
//TODO: add Ordinal API call
console.log('resolveOrdinalDetails', address);
return null;
} catch (error) {
console.error('Failed to resolve Ordinal details:', error);
@ -290,7 +296,9 @@ export class UserIdentityService {
ordinalDetails: undefined,
callSign: undefined,
displayPreference:
displayPreference === EDisplayPreference.CALL_SIGN ? EDisplayPreference.CALL_SIGN : EDisplayPreference.WALLET_ADDRESS,
displayPreference === EDisplayPreference.CALL_SIGN
? EDisplayPreference.CALL_SIGN
: EDisplayPreference.WALLET_ADDRESS,
lastUpdated: timestamp,
verificationStatus: EVerificationStatus.UNVERIFIED,
};

View File

@ -15,8 +15,7 @@ class MessageManager {
private messageService: MessageService | null = null;
private reliableMessaging: ReliableMessaging | null = null;
constructor() {
}
constructor() {}
public static async create(): Promise<MessageManager> {
const manager = new MessageManager();

View File

@ -47,7 +47,6 @@ export const refreshData = async (
export const initializeNetwork = async (
toast: ToastFunction,
updateStateFromCache: () => void,
setError: (error: string | null) => void
): Promise<void> => {
try {

View File

@ -207,7 +207,10 @@ export class WalletManager {
const bitcoinMessage = await loadBitcoinMessage();
const result = bitcoinMessage.verify(message, walletAddress, signature);
if (import.meta.env?.DEV) {
console.debug('WalletManager.verifySignature (bitcoin) result', result);
console.debug(
'WalletManager.verifySignature (bitcoin) result',
result
);
}
return result;
}

482
src/pages/ProfilePage.tsx Normal file
View File

@ -0,0 +1,482 @@
import { useState, useEffect } from 'react';
import { useAuth, useUserActions, useForumActions } from '@/hooks';
import { useUserDisplay } from '@/hooks';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { WalletWizard } from '@/components/ui/wallet-wizard';
import {
Loader2,
Wallet,
Hash,
User,
Shield,
CheckCircle,
AlertTriangle,
XCircle,
} from 'lucide-react';
import { EDisplayPreference, EVerificationStatus } from '@/types/identity';
import { useToast } from '@/hooks/use-toast';
export default function ProfilePage() {
const { updateProfile } = useUserActions();
const { refreshData } = useForumActions();
const { toast } = useToast();
// Get current user from auth context for the address
const { currentUser, delegationInfo } = useAuth();
const address = currentUser?.address;
// Get comprehensive user information from the unified hook
const userInfo = useUserDisplay(address || '');
const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [callSign, setCallSign] = useState(currentUser?.callSign || '');
const [displayPreference, setDisplayPreference] = useState(
currentUser?.displayPreference || EDisplayPreference.WALLET_ADDRESS
);
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
// Update local state when user data changes
useEffect(() => {
if (currentUser) {
setCallSign(currentUser.callSign || '');
setDisplayPreference(
currentUser.displayPreference || EDisplayPreference.WALLET_ADDRESS
);
}
}, [currentUser]);
if (!currentUser) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
Please connect your wallet to view your profile.
</div>
</CardContent>
</Card>
</div>
);
}
const handleSave = async () => {
if (!callSign.trim()) {
toast({
title: 'Invalid Input',
description: 'Call sign cannot be empty.',
variant: 'destructive',
});
return;
}
// Basic validation for call sign
if (callSign.length < 3 || callSign.length > 20) {
toast({
title: 'Invalid Call Sign',
description: 'Call sign must be between 3 and 20 characters.',
variant: 'destructive',
});
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(callSign)) {
toast({
title: 'Invalid Call Sign',
description:
'Call sign can only contain letters, numbers, underscores, and hyphens.',
variant: 'destructive',
});
return;
}
setIsSubmitting(true);
try {
const success = await updateProfile({
callSign: callSign.trim(),
displayPreference,
});
if (success) {
await refreshData();
setIsEditing(false);
toast({
title: 'Profile Updated',
description: 'Your profile has been updated successfully.',
});
}
} catch {
toast({
title: 'Update Failed',
description: 'Failed to update profile. Please try again.',
variant: 'destructive',
});
} finally {
setIsSubmitting(false);
}
};
const handleCancel = () => {
setCallSign(currentUser.callSign || '');
setDisplayPreference(currentUser.displayPreference);
setIsEditing(false);
};
const getVerificationIcon = () => {
switch (userInfo.verificationLevel) {
case EVerificationStatus.VERIFIED_OWNER:
return <CheckCircle className="h-4 w-4 text-green-500" />;
case EVerificationStatus.VERIFIED_BASIC:
return <Shield className="h-4 w-4 text-blue-500" />;
case EVerificationStatus.UNVERIFIED:
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
default:
return <XCircle className="h-4 w-4 text-red-500" />;
}
};
const getVerificationText = () => {
switch (userInfo.verificationLevel) {
case EVerificationStatus.VERIFIED_OWNER:
return 'Fully Verified';
case EVerificationStatus.VERIFIED_BASIC:
return 'Basic Verification';
case EVerificationStatus.UNVERIFIED:
return 'Unverified';
default:
return 'Unknown';
}
};
const getVerificationColor = () => {
switch (userInfo.verificationLevel) {
case EVerificationStatus.VERIFIED_OWNER:
return 'bg-green-100 text-green-800 border-green-200';
case EVerificationStatus.VERIFIED_BASIC:
return 'bg-blue-100 text-blue-800 border-blue-200';
case EVerificationStatus.UNVERIFIED:
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Profile
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Wallet Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Wallet className="h-4 w-4" />
Wallet Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Address
</Label>
<div className="mt-1 font-mono text-sm bg-muted px-3 py-2 rounded-md break-all">
{currentUser.address}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Network
</Label>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="capitalize">
{currentUser.walletType}
</Badge>
</div>
</div>
</div>
</div>
<Separator />
{/* Identity Information */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Hash className="h-4 w-4" />
Identity
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
ENS Name
</Label>
<div className="mt-1 text-sm">
{currentUser.ensDetails?.ensName || 'N/A'}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Current Display Name
</Label>
<div className="mt-1 text-sm font-medium">
{userInfo.displayName}
</div>
</div>
</div>
</div>
<Separator />
{/* Editable Profile Fields */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Profile Settings</h3>
<div className="space-y-4">
<div>
<Label htmlFor="callSign" className="text-sm font-medium">
Call Sign
</Label>
{isEditing ? (
<Input
id="callSign"
value={callSign}
onChange={e => setCallSign(e.target.value)}
placeholder="Enter your call sign"
className="mt-1"
disabled={isSubmitting}
/>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{userInfo.callSign || currentUser.callSign || 'Not set'}
</div>
)}
<p className="mt-1 text-xs text-muted-foreground">
3-20 characters, letters, numbers, underscores, and hyphens
only
</p>
</div>
<div>
<Label
htmlFor="displayPreference"
className="text-sm font-medium"
>
Display Preference
</Label>
{isEditing ? (
<Select
value={displayPreference}
onValueChange={value =>
setDisplayPreference(value as EDisplayPreference)
}
disabled={isSubmitting}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={EDisplayPreference.CALL_SIGN}>
Call Sign (when available)
</SelectItem>
<SelectItem value={EDisplayPreference.WALLET_ADDRESS}>
Wallet Address
</SelectItem>
</SelectContent>
</Select>
) : (
<div className="mt-1 text-sm bg-muted px-3 py-2 rounded-md">
{(userInfo.displayPreference || displayPreference) ===
EDisplayPreference.CALL_SIGN
? 'Call Sign (when available)'
: 'Wallet Address'}
</div>
)}
</div>
</div>
</div>
<Separator />
{/* Verification Status */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Shield className="h-4 w-4" />
Verification Status
</h3>
<div className="flex items-center gap-3">
{getVerificationIcon()}
<Badge className={getVerificationColor()}>
{getVerificationText()}
</Badge>
</div>
</div>
<Separator />
{/* Delegation Details */}
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<CheckCircle className="h-4 w-4" />
Key Delegation
</h3>
<div className="space-y-4">
{/* Delegation Status */}
<div className="flex items-center gap-3">
<Badge
variant={delegationInfo.isActive ? "default" : "secondary"}
className={delegationInfo.isActive ? "bg-green-600 hover:bg-green-700" : ""}
>
{delegationInfo.isActive ? 'Active' : 'Inactive'}
</Badge>
{delegationInfo.isActive && delegationInfo.timeRemaining && (
<span className="text-sm text-muted-foreground">
{delegationInfo.timeRemaining} remaining
</span>
)}
{delegationInfo.needsRenewal && !delegationInfo.isExpired && (
<Badge variant="outline" className="text-yellow-600 border-yellow-600">
Renewal Recommended
</Badge>
)}
{delegationInfo.isExpired && (
<Badge variant="destructive">
Expired
</Badge>
)}
</div>
{/* Delegation Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Browser Public Key
</Label>
<div className="mt-1 text-sm font-mono bg-muted px-3 py-2 rounded-md break-all">
{currentUser.browserPubKey ? (
`${currentUser.browserPubKey.slice(0, 12)}...${currentUser.browserPubKey.slice(-8)}`
) : 'Not delegated'}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Delegation Signature
</Label>
<div className="mt-1 text-sm">
{currentUser.delegationSignature === 'valid' ? (
<Badge variant="outline" className="text-green-600 border-green-600">
Valid
</Badge>
) : (
'Not signed'
)}
</div>
</div>
{currentUser.delegationExpiry && (
<div>
<Label className="text-sm font-medium text-muted-foreground">
Expires At
</Label>
<div className="mt-1 text-sm">
{new Date(currentUser.delegationExpiry).toLocaleString()}
</div>
</div>
)}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Last Updated
</Label>
<div className="mt-1 text-sm">
{currentUser.lastChecked ?
new Date(currentUser.lastChecked).toLocaleString() :
'Never'
}
</div>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">
Can Delegate
</Label>
<div className="mt-1 text-sm">
{delegationInfo.canDelegate ? (
<Badge variant="outline" className="text-green-600 border-green-600">
Yes
</Badge>
) : (
<Badge variant="outline" className="text-red-600 border-red-600">
No
</Badge>
)}
</div>
</div>
</div>
{/* Delegation Actions */}
{delegationInfo.canDelegate && (
<div className="pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setWalletWizardOpen(true)}
>
{delegationInfo.isActive ? 'Renew Delegation' : 'Delegate Key'}
</Button>
</div>
)}
</div>
</div>
<Separator />
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">
{isEditing ? (
<>
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSubmitting}>
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Changes
</Button>
</>
) : (
<Button onClick={() => setIsEditing(true)}>Edit Profile</Button>
)}
</div>
</CardContent>
</Card>
{/* Wallet Wizard */}
<WalletWizard
open={walletWizardOpen}
onOpenChange={setWalletWizardOpen}
onComplete={() => setWalletWizardOpen(false)}
/>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { EDisplayPreference, EVerificationStatus } from "./identity";
import { EDisplayPreference, EVerificationStatus } from './identity';
/**
* Message types for Waku communication
@ -162,8 +162,8 @@ export interface UserIdentityCache {
ordinalDetails: string;
};
callSign?: string;
displayPreference: EDisplayPreference
displayPreference: EDisplayPreference;
lastUpdated: number;
verificationStatus: EVerificationStatus
verificationStatus: EVerificationStatus;
};
}