mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
fix: call sign
This commit is contained in:
parent
cbe93afe7a
commit
d2a512211f
195
TODO.md
195
TODO.md
@ -1,195 +0,0 @@
|
||||
# OpChan TODO - Missing Features & Improvements
|
||||
|
||||
This document outlines the features and improvements that still need to be implemented to fully satisfy the FURPS requirements for the Waku Forum.
|
||||
|
||||
## 🚨 High Priority (1-2 weeks)
|
||||
|
||||
### 1. Bookmarking System
|
||||
|
||||
- **Requirement**: "Users can bookmark posts and topics; local only"
|
||||
- **Status**: ❌ Not implemented
|
||||
- **Missing**:
|
||||
- [ ] Local storage implementation for bookmarked posts/topics
|
||||
- [ ] Bookmark UI components (bookmark button, bookmark list)
|
||||
- [ ] Bookmark management interface
|
||||
- [ ] Bookmark persistence across sessions
|
||||
- **Impact**: Users cannot save content for later reference
|
||||
- **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**:
|
||||
- [ ] Complete call sign setup UI integration
|
||||
- [ ] Ordinal avatar display and integration
|
||||
- [ ] User profile settings interface
|
||||
- [ ] Call sign validation and uniqueness checks
|
||||
- **Impact**: Users cannot customize their forum identity
|
||||
- **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**:
|
||||
- [ ] Icon upload/selection interface
|
||||
- [ ] Icon size restrictions and validation
|
||||
- [ ] Icon display in cell listings and details
|
||||
- [ ] Icon storage and management
|
||||
- **Impact**: Cells lack visual identity and branding
|
||||
- **Estimated Effort**: 2-3 days
|
||||
|
||||
## 🔶 Medium Priority (2-3 weeks)
|
||||
|
||||
### 4. Enhanced Sorting Options
|
||||
|
||||
- **Requirement**: "Users can sort topics per new or top"
|
||||
- **Status**: ⚠️ Basic implementation exists
|
||||
- **Missing**:
|
||||
- [ ] "Top" sorting by votes/relevance
|
||||
- [ ] UI controls for sorting preferences
|
||||
- [ ] Persistent sorting preferences
|
||||
- [ ] Sort option indicators in UI
|
||||
- **Impact**: Limited content discovery options
|
||||
- **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**:
|
||||
- [ ] UI components to display active member counts
|
||||
- [ ] Member count updates in real-time
|
||||
- [ ] Member activity indicators
|
||||
- **Impact**: Users cannot gauge cell activity levels
|
||||
- **Estimated Effort**: 1 day
|
||||
|
||||
### 6. IndexedDB Integration
|
||||
|
||||
- **Requirement**: "store message cache in indexedDB -- make app local-first"
|
||||
- **Status**: ❌ In-memory caching only
|
||||
- **Missing**:
|
||||
- [ ] IndexedDB schema design
|
||||
- [ ] Message persistence layer
|
||||
- [ ] Offline-first capabilities
|
||||
- [ ] Cache synchronization logic
|
||||
- **Impact**: No offline support, data lost on refresh
|
||||
- **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**:
|
||||
- [ ] Rich moderation interface
|
||||
- [ ] Moderation history and audit trail
|
||||
- [ ] Bulk moderation actions
|
||||
- [ ] Moderation reason templates
|
||||
- [ ] Moderation statistics dashboard
|
||||
- **Impact**: Limited moderation capabilities for cell admins
|
||||
- **Estimated Effort**: 2-3 days
|
||||
|
||||
## 🔵 Low Priority (3-4 weeks)
|
||||
|
||||
### 8. Anonymous User Experience
|
||||
|
||||
- **Requirement**: "Anonymous users can upvote, comments and post"
|
||||
- **Status**: ⚠️ Basic support but limited UX
|
||||
- **Missing**:
|
||||
- [ ] Better anonymous user flow
|
||||
- [ ] Clear permission indicators
|
||||
- [ ] Anonymous user onboarding
|
||||
- [ ] Anonymous user limitations display
|
||||
- **Impact**: Poor experience for non-authenticated users
|
||||
- **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**:
|
||||
- [ ] Better relevance score indicators
|
||||
- [ ] Relevance-based filtering options
|
||||
- [ ] Relevance score explanations
|
||||
- [ ] Relevance score trends
|
||||
- **Impact**: Users don't understand content ranking
|
||||
- **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**:
|
||||
- [ ] Full mobile-optimized experience
|
||||
- [ ] Touch-friendly interactions
|
||||
- [ ] Mobile-specific navigation
|
||||
- [ ] Responsive image handling
|
||||
- **Impact**: Poor mobile user experience
|
||||
- **Estimated Effort**: 3-4 days
|
||||
|
||||
## 🛠️ 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
|
||||
- [ ] Architecture documentation
|
||||
|
||||
## 📋 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
|
||||
- Ensure accessibility compliance
|
||||
- Follow existing code patterns and conventions
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- Unit tests for utility functions
|
||||
- Integration tests for hooks and contexts
|
||||
- Component tests for UI elements
|
||||
- End-to-end tests for user flows
|
||||
|
||||
## 🎯 Success Metrics
|
||||
|
||||
- [ ] All FURPS requirements satisfied
|
||||
- [ ] 90%+ test coverage
|
||||
- [ ] Lighthouse performance score > 90
|
||||
- [ ] Accessibility score > 95
|
||||
- [ ] Mobile usability score > 90
|
||||
|
||||
## 📅 Timeline Estimate
|
||||
|
||||
- **Phase 1 (High Priority)**: 1-2 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_
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth, useNetworkStatus } from '@/hooks';
|
||||
import { useAuth, useWakuHealthStatus } from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
@ -29,13 +29,14 @@ import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
|
||||
import { WalletWizard } from '@/components/ui/wallet-wizard';
|
||||
|
||||
import { useUserDisplay } from '@/hooks';
|
||||
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
||||
|
||||
const Header = () => {
|
||||
const { verificationStatus } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
useState<DelegationFullStatus | null>(null);
|
||||
const networkStatus = useNetworkStatus();
|
||||
const wakuHealth = useWakuHealthStatus();
|
||||
const location = useLocation();
|
||||
const { toast } = useToast();
|
||||
const forum = useForum();
|
||||
@ -215,15 +216,9 @@ const Header = () => {
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Network Status */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
networkStatus.health.isConnected
|
||||
? 'bg-green-400'
|
||||
: 'bg-red-400'
|
||||
}`}
|
||||
/>
|
||||
<WakuHealthDot />
|
||||
<span className="text-xs text-cyber-neutral">
|
||||
{networkStatus.getStatusMessage()}
|
||||
{wakuHealth.statusMessage}
|
||||
</span>
|
||||
{forum.lastSync && (
|
||||
<span className="text-xs text-cyber-neutral ml-2">
|
||||
|
||||
83
src/components/ui/waku-health-indicator.tsx
Normal file
83
src/components/ui/waku-health-indicator.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Wifi, WifiOff, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import { useWakuHealthStatus } from '@/hooks/useWakuHealth';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface WakuHealthIndicatorProps {
|
||||
className?: string;
|
||||
showText?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function WakuHealthIndicator({
|
||||
className,
|
||||
showText = true,
|
||||
size = 'md',
|
||||
}: WakuHealthIndicatorProps) {
|
||||
const { connectionStatus, statusColor, statusMessage } =
|
||||
useWakuHealthStatus();
|
||||
|
||||
const getIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected':
|
||||
return <CheckCircle className="text-green-500" />;
|
||||
case 'connecting':
|
||||
return <Wifi className="text-yellow-500 animate-pulse" />;
|
||||
case 'disconnected':
|
||||
return <WifiOff className="text-red-500" />;
|
||||
case 'error':
|
||||
return <AlertTriangle className="text-red-500" />;
|
||||
default:
|
||||
return <Wifi className="text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSizeClasses = () => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'w-4 h-4';
|
||||
case 'lg':
|
||||
return 'w-6 h-6';
|
||||
default:
|
||||
return 'w-5 h-5';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
<div className={getSizeClasses()}>{getIcon()}</div>
|
||||
{showText && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm font-medium',
|
||||
statusColor === 'green' && 'text-green-400',
|
||||
statusColor === 'yellow' && 'text-yellow-400',
|
||||
statusColor === 'red' && 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{statusMessage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple dot indicator for Waku health status
|
||||
* Useful for compact displays like headers or status bars
|
||||
*/
|
||||
export function WakuHealthDot({ className }: { className?: string }) {
|
||||
const { statusColor } = useWakuHealthStatus();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full',
|
||||
statusColor === 'green' && 'bg-green-500',
|
||||
statusColor === 'yellow' && 'bg-yellow-500 animate-pulse',
|
||||
statusColor === 'red' && 'bg-red-500',
|
||||
className
|
||||
)}
|
||||
title={`Waku network: ${statusColor}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -288,19 +288,24 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
||||
if (hasSeedData) {
|
||||
setIsInitialLoading(false);
|
||||
} else {
|
||||
// Wait for first incoming message before showing UI
|
||||
const unsubscribe = messageManager.onMessageReceived(() => {
|
||||
setIsInitialLoading(false);
|
||||
unsubscribe();
|
||||
// Wait for Waku network to be healthy instead of first message
|
||||
const unsubscribeHealth = messageManager.onHealthChange(isReady => {
|
||||
if (isReady) {
|
||||
setIsInitialLoading(false);
|
||||
unsubscribeHealth();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('LocalDatabase warm-start failed, continuing cold:', e);
|
||||
// Initialize network even if local DB failed, keep loader until first message
|
||||
// Initialize network even if local DB failed, keep loader until Waku is healthy
|
||||
await initializeNetwork(toast, setError);
|
||||
const unsubscribe = messageManager.onMessageReceived(() => {
|
||||
setIsInitialLoading(false);
|
||||
unsubscribe();
|
||||
|
||||
const unsubscribeHealth = messageManager.onHealthChange(isReady => {
|
||||
if (isReady) {
|
||||
setIsInitialLoading(false);
|
||||
unsubscribeHealth();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -220,35 +220,20 @@ export function useUserActions(): UserActions {
|
||||
|
||||
try {
|
||||
let success = true;
|
||||
const updatePromises: Promise<boolean>[] = [];
|
||||
|
||||
// Update call sign if provided
|
||||
if (updates.callSign !== undefined) {
|
||||
updatePromises.push(
|
||||
userIdentityService.updateUserProfile(
|
||||
currentUser.address,
|
||||
updates.callSign,
|
||||
currentUser.displayPreference
|
||||
)
|
||||
if (
|
||||
updates.callSign !== undefined ||
|
||||
updates.displayPreference !== undefined
|
||||
) {
|
||||
const callSignToSend = updates.callSign;
|
||||
const preferenceToSend =
|
||||
updates.displayPreference ?? currentUser.displayPreference;
|
||||
success = await userIdentityService.updateUserProfile(
|
||||
currentUser.address,
|
||||
callSignToSend,
|
||||
preferenceToSend
|
||||
);
|
||||
}
|
||||
|
||||
// Update display preference if provided
|
||||
if (updates.displayPreference !== undefined) {
|
||||
updatePromises.push(
|
||||
userIdentityService.updateUserProfile(
|
||||
currentUser.address,
|
||||
currentUser.callSign || '',
|
||||
updates.displayPreference
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (updatePromises.length > 0) {
|
||||
const results = await Promise.all(updatePromises);
|
||||
success = results.every(result => result);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
toast({
|
||||
title: 'Profile Updated',
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useAuth as useBaseAuth } from '@/contexts/useAuth';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { User, EVerificationStatus } from '@/types/identity';
|
||||
|
||||
export interface AuthState {
|
||||
@ -18,20 +19,17 @@ export interface AuthState {
|
||||
export function useAuth(): AuthState {
|
||||
const { currentUser, isAuthenticated, isAuthenticating, verificationStatus } =
|
||||
useBaseAuth();
|
||||
const { userIdentityService } = useForum();
|
||||
|
||||
// Helper functions
|
||||
const getDisplayName = (): string => {
|
||||
if (!currentUser) return 'Anonymous';
|
||||
|
||||
if (currentUser.callSign) {
|
||||
return currentUser.callSign;
|
||||
// Centralized display logic; fallback to truncated address if service unavailable
|
||||
if (userIdentityService) {
|
||||
return userIdentityService.getDisplayName(currentUser.address);
|
||||
}
|
||||
|
||||
if (currentUser.ensDetails?.ensName) {
|
||||
return currentUser.ensDetails.ensName;
|
||||
}
|
||||
|
||||
return `${currentUser.address.slice(0, 6)}...${currentUser.address.slice(-4)}`;
|
||||
const addr = currentUser.address;
|
||||
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
||||
};
|
||||
|
||||
const getVerificationBadge = (): string | null => {
|
||||
|
||||
@ -28,6 +28,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
// Get verification status from forum context for reactive updates
|
||||
const verificationInfo = useMemo(() => {
|
||||
@ -40,6 +41,21 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
||||
);
|
||||
}, [userVerificationStatus, address]);
|
||||
|
||||
// Set up refresh listener for user identity changes
|
||||
useEffect(() => {
|
||||
if (!userIdentityService || !address) return;
|
||||
|
||||
const unsubscribe = userIdentityService.addRefreshListener(
|
||||
updatedAddress => {
|
||||
if (updatedAddress === address) {
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return unsubscribe;
|
||||
}, [userIdentityService, address]);
|
||||
|
||||
useEffect(() => {
|
||||
const getUserDisplayInfo = async () => {
|
||||
if (!address) {
|
||||
@ -75,17 +91,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
||||
const identity = await userIdentityService.getUserIdentity(address);
|
||||
|
||||
if (identity) {
|
||||
let displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
|
||||
// Determine display name based on preferences
|
||||
if (
|
||||
identity.displayPreference === EDisplayPreference.CALL_SIGN &&
|
||||
identity.callSign
|
||||
) {
|
||||
displayName = identity.callSign;
|
||||
} else if (identity.ensName) {
|
||||
displayName = identity.ensName;
|
||||
}
|
||||
const displayName = userIdentityService.getDisplayName(address);
|
||||
|
||||
setDisplayInfo({
|
||||
displayName,
|
||||
@ -101,9 +107,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
||||
});
|
||||
} else {
|
||||
setDisplayInfo({
|
||||
displayName:
|
||||
verificationInfo.ensName ||
|
||||
`${address.slice(0, 6)}...${address.slice(-4)}`,
|
||||
displayName: userIdentityService.getDisplayName(address),
|
||||
callSign: null,
|
||||
ensName: verificationInfo.ensName || null,
|
||||
ordinalDetails: null,
|
||||
@ -134,7 +138,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
||||
};
|
||||
|
||||
getUserDisplayInfo();
|
||||
}, [address, userIdentityService, verificationInfo]);
|
||||
}, [address, userIdentityService, verificationInfo, refreshTrigger]);
|
||||
|
||||
// Update display info when verification status changes reactively
|
||||
useEffect(() => {
|
||||
|
||||
@ -63,6 +63,13 @@ export type {
|
||||
NetworkStatusData,
|
||||
} from './utilities/useNetworkStatus';
|
||||
|
||||
export {
|
||||
useWakuHealth,
|
||||
useWakuReady,
|
||||
useWakuHealthStatus,
|
||||
} from './useWakuHealth';
|
||||
export type { WakuHealthState } from './useWakuHealth';
|
||||
|
||||
export { useForumSelectors } from './utilities/selectors';
|
||||
export type { ForumSelectors } from './utilities/selectors';
|
||||
|
||||
|
||||
127
src/hooks/useWakuHealth.ts
Normal file
127
src/hooks/useWakuHealth.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { HealthStatus } from '@waku/sdk';
|
||||
import messageManager from '@/lib/waku';
|
||||
|
||||
export interface WakuHealthState {
|
||||
isReady: boolean;
|
||||
health: HealthStatus;
|
||||
isInitialized: boolean;
|
||||
connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for monitoring Waku network health and connection status
|
||||
* Provides real-time updates on network state and health
|
||||
*/
|
||||
export function useWakuHealth(): WakuHealthState {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [health, setHealth] = useState<HealthStatus>(HealthStatus.Unhealthy);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
'connecting' | 'connected' | 'disconnected' | 'error'
|
||||
>('connecting');
|
||||
|
||||
const updateHealth = useCallback(
|
||||
(ready: boolean, currentHealth: HealthStatus) => {
|
||||
setIsReady(ready);
|
||||
setHealth(currentHealth);
|
||||
|
||||
// Update connection status based on health
|
||||
if (ready) {
|
||||
setConnectionStatus('connected');
|
||||
} else if (currentHealth === HealthStatus.Unhealthy) {
|
||||
setConnectionStatus('disconnected');
|
||||
} else {
|
||||
setConnectionStatus('connecting');
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if messageManager is initialized
|
||||
try {
|
||||
const currentHealth = messageManager.currentHealth;
|
||||
const currentReady = messageManager.isReady;
|
||||
|
||||
setIsInitialized(true);
|
||||
updateHealth(currentReady, currentHealth);
|
||||
|
||||
// Subscribe to health changes
|
||||
const unsubscribe = messageManager.onHealthChange(updateHealth);
|
||||
|
||||
return unsubscribe;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Waku health monitoring:', error);
|
||||
setConnectionStatus('error');
|
||||
setIsInitialized(false);
|
||||
return undefined;
|
||||
}
|
||||
}, [updateHealth]);
|
||||
|
||||
return {
|
||||
isReady,
|
||||
health,
|
||||
isInitialized,
|
||||
connectionStatus,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that provides a simple boolean indicating if Waku is ready for use
|
||||
* Useful for conditional rendering and loading states
|
||||
*/
|
||||
export function useWakuReady(): boolean {
|
||||
const { isReady } = useWakuHealth();
|
||||
return isReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that provides health status with human-readable descriptions
|
||||
*/
|
||||
export function useWakuHealthStatus() {
|
||||
const { isReady, health, connectionStatus } = useWakuHealth();
|
||||
|
||||
const getHealthDescription = useCallback(() => {
|
||||
switch (health) {
|
||||
case HealthStatus.SufficientlyHealthy:
|
||||
return 'Network is healthy and fully operational';
|
||||
case HealthStatus.MinimallyHealthy:
|
||||
return 'Network is minimally healthy and functional';
|
||||
case HealthStatus.Unhealthy:
|
||||
return 'Network is unhealthy or disconnected';
|
||||
default:
|
||||
return 'Network status unknown';
|
||||
}
|
||||
}, [health]);
|
||||
|
||||
const getStatusColor = useCallback(() => {
|
||||
if (isReady) return 'green';
|
||||
if (health === HealthStatus.Unhealthy) return 'red';
|
||||
return 'yellow';
|
||||
}, [isReady, health]);
|
||||
|
||||
const getStatusMessage = useCallback(() => {
|
||||
switch (connectionStatus) {
|
||||
case 'connecting':
|
||||
return 'Connecting to Waku network...';
|
||||
case 'connected':
|
||||
return 'Connected to Waku network';
|
||||
case 'disconnected':
|
||||
return 'Disconnected from Waku network';
|
||||
case 'error':
|
||||
return 'Error connecting to Waku network';
|
||||
default:
|
||||
return 'Unknown connection status';
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
return {
|
||||
isReady,
|
||||
health,
|
||||
connectionStatus,
|
||||
description: getHealthDescription(),
|
||||
statusColor: getStatusColor(),
|
||||
statusMessage: getStatusMessage(),
|
||||
};
|
||||
}
|
||||
@ -167,22 +167,24 @@ 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
|
||||
) {
|
||||
this.cache.userIdentities[author] = {
|
||||
ensName: undefined,
|
||||
ordinalDetails: undefined,
|
||||
callSign,
|
||||
const existing = this.cache.userIdentities[author];
|
||||
if (!existing || timestamp > existing.lastUpdated) {
|
||||
const nextRecord = {
|
||||
ensName: existing?.ensName,
|
||||
ordinalDetails: existing?.ordinalDetails,
|
||||
callSign: callSign !== undefined ? callSign : existing?.callSign,
|
||||
displayPreference,
|
||||
lastUpdated: timestamp,
|
||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||
};
|
||||
verificationStatus:
|
||||
existing?.verificationStatus ??
|
||||
EVerificationStatus.WALLET_UNCONNECTED,
|
||||
} as UserIdentityCache[string];
|
||||
|
||||
this.cache.userIdentities[author] = nextRecord;
|
||||
// Persist with address keyPath
|
||||
this.put(STORE.USER_IDENTITIES, {
|
||||
address: author,
|
||||
...this.cache.userIdentities[author],
|
||||
...nextRecord,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
@ -25,6 +25,7 @@ export interface UserIdentity {
|
||||
export class UserIdentityService {
|
||||
private messageService: MessageService;
|
||||
private userIdentityCache: UserIdentityCache = {};
|
||||
private refreshListeners: Set<(address: string) => void> = new Set();
|
||||
|
||||
constructor(messageService: MessageService) {
|
||||
this.messageService = messageService;
|
||||
@ -40,15 +41,19 @@ export class UserIdentityService {
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug('UserIdentityService: cache hit (internal)');
|
||||
}
|
||||
// Enrich with ENS name if missing and ETH address
|
||||
if (!cached.ensName && address.startsWith('0x')) {
|
||||
const ensName = await this.resolveENSName(address);
|
||||
if (ensName) {
|
||||
cached.ensName = ensName;
|
||||
}
|
||||
}
|
||||
return {
|
||||
address,
|
||||
ensName: cached.ensName,
|
||||
ordinalDetails: cached.ordinalDetails,
|
||||
callSign: cached.callSign,
|
||||
displayPreference:
|
||||
cached.displayPreference === 'call-sign'
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference: cached.displayPreference,
|
||||
lastUpdated: cached.lastUpdated,
|
||||
verificationStatus: this.mapVerificationStatus(
|
||||
cached.verificationStatus
|
||||
@ -67,20 +72,26 @@ export class UserIdentityService {
|
||||
lastUpdated: persisted.lastUpdated,
|
||||
verificationStatus: persisted.verificationStatus,
|
||||
};
|
||||
return {
|
||||
const result = {
|
||||
address,
|
||||
ensName: persisted.ensName,
|
||||
ordinalDetails: persisted.ordinalDetails,
|
||||
callSign: persisted.callSign,
|
||||
displayPreference:
|
||||
persisted.displayPreference === 'call-sign'
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference: persisted.displayPreference,
|
||||
lastUpdated: persisted.lastUpdated,
|
||||
verificationStatus: this.mapVerificationStatus(
|
||||
persisted.verificationStatus
|
||||
),
|
||||
};
|
||||
} as UserIdentity;
|
||||
// Enrich with ENS name if missing and ETH address
|
||||
if (!result.ensName && address.startsWith('0x')) {
|
||||
const ensName = await this.resolveENSName(address);
|
||||
if (ensName) {
|
||||
result.ensName = ensName;
|
||||
this.userIdentityCache[address].ensName = ensName;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback: Check Waku message cache
|
||||
@ -102,20 +113,26 @@ export class UserIdentityService {
|
||||
verificationStatus: cacheServiceData.verificationStatus,
|
||||
};
|
||||
|
||||
return {
|
||||
const result = {
|
||||
address,
|
||||
ensName: cacheServiceData.ensName,
|
||||
ordinalDetails: cacheServiceData.ordinalDetails,
|
||||
callSign: cacheServiceData.callSign,
|
||||
displayPreference:
|
||||
cacheServiceData.displayPreference === 'call-sign'
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference: cacheServiceData.displayPreference,
|
||||
lastUpdated: cacheServiceData.lastUpdated,
|
||||
verificationStatus: this.mapVerificationStatus(
|
||||
cacheServiceData.verificationStatus
|
||||
),
|
||||
};
|
||||
} as UserIdentity;
|
||||
// Enrich with ENS name if missing and ETH address
|
||||
if (!result.ensName && address.startsWith('0x')) {
|
||||
const ensName = await this.resolveENSName(address);
|
||||
if (ensName) {
|
||||
result.ensName = ensName;
|
||||
this.userIdentityCache[address].ensName = ensName;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (import.meta.env?.DEV) {
|
||||
@ -129,10 +146,7 @@ export class UserIdentityService {
|
||||
ensName: identity.ensName,
|
||||
ordinalDetails: identity.ordinalDetails,
|
||||
callSign: identity.callSign,
|
||||
displayPreference:
|
||||
identity.displayPreference === EDisplayPreference.CALL_SIGN
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference: identity.displayPreference,
|
||||
lastUpdated: identity.lastUpdated,
|
||||
verificationStatus: identity.verificationStatus,
|
||||
};
|
||||
@ -150,10 +164,7 @@ export class UserIdentityService {
|
||||
ensName: cached.ensName,
|
||||
ordinalDetails: cached.ordinalDetails,
|
||||
callSign: cached.callSign,
|
||||
displayPreference:
|
||||
cached.displayPreference === 'call-sign'
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference: cached.displayPreference,
|
||||
lastUpdated: cached.lastUpdated,
|
||||
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
||||
}));
|
||||
@ -164,7 +175,7 @@ export class UserIdentityService {
|
||||
*/
|
||||
async updateUserProfile(
|
||||
address: string,
|
||||
callSign: string,
|
||||
callSign: string | undefined,
|
||||
displayPreference: EDisplayPreference
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
@ -172,17 +183,18 @@ export class UserIdentityService {
|
||||
console.debug('UserIdentityService: updating profile', { address });
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const unsignedMessage: UnsignedUserProfileUpdateMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
type: MessageType.USER_PROFILE_UPDATE,
|
||||
timestamp: Date.now(),
|
||||
timestamp,
|
||||
author: address,
|
||||
callSign,
|
||||
displayPreference:
|
||||
displayPreference === EDisplayPreference.CALL_SIGN
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference,
|
||||
};
|
||||
// Only include callSign if provided and non-empty
|
||||
if (callSign && callSign.trim()) {
|
||||
unsignedMessage.callSign = callSign.trim();
|
||||
}
|
||||
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug('UserIdentityService: created unsigned message');
|
||||
@ -198,6 +210,48 @@ export class UserIdentityService {
|
||||
);
|
||||
}
|
||||
|
||||
// If broadcast was successful, immediately update local cache
|
||||
if (signedMessage) {
|
||||
this.updateUserIdentityFromMessage(
|
||||
signedMessage as UserProfileUpdateMessage
|
||||
);
|
||||
|
||||
// Also update the local database cache immediately
|
||||
if (this.userIdentityCache[address]) {
|
||||
const updatedIdentity = {
|
||||
...this.userIdentityCache[address],
|
||||
callSign:
|
||||
callSign && callSign.trim()
|
||||
? callSign.trim()
|
||||
: this.userIdentityCache[address].callSign,
|
||||
displayPreference,
|
||||
lastUpdated: timestamp,
|
||||
};
|
||||
|
||||
localDatabase.cache.userIdentities[address] = updatedIdentity;
|
||||
|
||||
// Persist to IndexedDB using the storeMessage method
|
||||
const profileMessage: UserProfileUpdateMessage = {
|
||||
id: unsignedMessage.id,
|
||||
type: MessageType.USER_PROFILE_UPDATE,
|
||||
timestamp,
|
||||
author: address,
|
||||
displayPreference,
|
||||
signature: signedMessage.signature,
|
||||
browserPubKey: signedMessage.browserPubKey,
|
||||
};
|
||||
if (callSign && callSign.trim()) {
|
||||
profileMessage.callSign = callSign.trim();
|
||||
}
|
||||
|
||||
// Apply the message to update the database
|
||||
await localDatabase.applyMessage(profileMessage);
|
||||
|
||||
// Notify listeners that the user identity has been updated
|
||||
this.notifyRefreshListeners(address);
|
||||
}
|
||||
}
|
||||
|
||||
return !!signedMessage;
|
||||
} catch (error) {
|
||||
console.error('Failed to update user profile:', error);
|
||||
@ -295,10 +349,7 @@ export class UserIdentityService {
|
||||
ensName: undefined,
|
||||
ordinalDetails: undefined,
|
||||
callSign: undefined,
|
||||
displayPreference:
|
||||
displayPreference === EDisplayPreference.CALL_SIGN
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference,
|
||||
lastUpdated: timestamp,
|
||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||
};
|
||||
@ -309,12 +360,12 @@ export class UserIdentityService {
|
||||
this.userIdentityCache[author] = {
|
||||
...this.userIdentityCache[author],
|
||||
callSign,
|
||||
displayPreference:
|
||||
displayPreference === EDisplayPreference.CALL_SIGN
|
||||
? EDisplayPreference.CALL_SIGN
|
||||
: EDisplayPreference.WALLET_ADDRESS,
|
||||
displayPreference,
|
||||
lastUpdated: timestamp,
|
||||
};
|
||||
|
||||
// Notify listeners that the user identity has been updated
|
||||
this.notifyRefreshListeners(author);
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,6 +400,21 @@ export class UserIdentityService {
|
||||
this.userIdentityCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a refresh listener for when user identity data changes
|
||||
*/
|
||||
addRefreshListener(listener: (address: string) => void): () => void {
|
||||
this.refreshListeners.add(listener);
|
||||
return () => this.refreshListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners that user identity data has changed
|
||||
*/
|
||||
private notifyRefreshListeners(address: string): void {
|
||||
this.refreshListeners.forEach(listener => listener(address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for user based on their preferences
|
||||
*/
|
||||
@ -358,7 +424,10 @@ export class UserIdentityService {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
if (identity.displayPreference === 'call-sign' && identity.callSign) {
|
||||
if (
|
||||
identity.displayPreference === EDisplayPreference.CALL_SIGN &&
|
||||
identity.callSign
|
||||
) {
|
||||
return identity.callSign;
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ export interface UnsignedModerateMessage extends UnsignedBaseMessage {
|
||||
export interface UnsignedUserProfileUpdateMessage extends UnsignedBaseMessage {
|
||||
type: MessageType.USER_PROFILE_UPDATE;
|
||||
callSign?: string;
|
||||
displayPreference: 'call-sign' | 'wallet-address';
|
||||
displayPreference: EDisplayPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user