mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
wip: user info
This commit is contained in:
parent
60fe855779
commit
30e888bae4
22
TODO.md
22
TODO.md
@ -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_
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -44,5 +44,3 @@ export function usePendingVote(targetId: string | undefined) {
|
||||
|
||||
return { isPending };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
|
||||
@ -60,5 +60,3 @@ export function openLocalDB(): Promise<IDBDatabase> {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -47,7 +47,6 @@ export const refreshData = async (
|
||||
|
||||
export const initializeNetwork = async (
|
||||
toast: ToastFunction,
|
||||
updateStateFromCache: () => void,
|
||||
setError: (error: string | null) => void
|
||||
): Promise<void> => {
|
||||
try {
|
||||
|
||||
@ -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
482
src/pages/ProfilePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user