mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-04 05:43: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 React, { useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
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 { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||||
import { EVerificationStatus } from '@/types/identity';
|
import { EVerificationStatus } from '@/types/identity';
|
||||||
import { useForum } from '@/contexts/useForum';
|
import { useForum } from '@/contexts/useForum';
|
||||||
@ -29,13 +29,14 @@ import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
|
|||||||
import { WalletWizard } from '@/components/ui/wallet-wizard';
|
import { WalletWizard } from '@/components/ui/wallet-wizard';
|
||||||
|
|
||||||
import { useUserDisplay } from '@/hooks';
|
import { useUserDisplay } from '@/hooks';
|
||||||
|
import { WakuHealthDot } from '@/components/ui/waku-health-indicator';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { verificationStatus } = useAuth();
|
const { verificationStatus } = useAuth();
|
||||||
const { getDelegationStatus } = useAuthContext();
|
const { getDelegationStatus } = useAuthContext();
|
||||||
const [delegationInfo, setDelegationInfo] =
|
const [delegationInfo, setDelegationInfo] =
|
||||||
useState<DelegationFullStatus | null>(null);
|
useState<DelegationFullStatus | null>(null);
|
||||||
const networkStatus = useNetworkStatus();
|
const wakuHealth = useWakuHealthStatus();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const forum = useForum();
|
const forum = useForum();
|
||||||
@ -215,15 +216,9 @@ const Header = () => {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{/* Network Status */}
|
{/* Network Status */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div
|
<WakuHealthDot />
|
||||||
className={`w-2 h-2 rounded-full ${
|
|
||||||
networkStatus.health.isConnected
|
|
||||||
? 'bg-green-400'
|
|
||||||
: 'bg-red-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-cyber-neutral">
|
<span className="text-xs text-cyber-neutral">
|
||||||
{networkStatus.getStatusMessage()}
|
{wakuHealth.statusMessage}
|
||||||
</span>
|
</span>
|
||||||
{forum.lastSync && (
|
{forum.lastSync && (
|
||||||
<span className="text-xs text-cyber-neutral ml-2">
|
<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) {
|
if (hasSeedData) {
|
||||||
setIsInitialLoading(false);
|
setIsInitialLoading(false);
|
||||||
} else {
|
} else {
|
||||||
// Wait for first incoming message before showing UI
|
// Wait for Waku network to be healthy instead of first message
|
||||||
const unsubscribe = messageManager.onMessageReceived(() => {
|
const unsubscribeHealth = messageManager.onHealthChange(isReady => {
|
||||||
setIsInitialLoading(false);
|
if (isReady) {
|
||||||
unsubscribe();
|
setIsInitialLoading(false);
|
||||||
|
unsubscribeHealth();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('LocalDatabase warm-start failed, continuing cold:', 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);
|
await initializeNetwork(toast, setError);
|
||||||
const unsubscribe = messageManager.onMessageReceived(() => {
|
|
||||||
setIsInitialLoading(false);
|
const unsubscribeHealth = messageManager.onHealthChange(isReady => {
|
||||||
unsubscribe();
|
if (isReady) {
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
unsubscribeHealth();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -220,35 +220,20 @@ export function useUserActions(): UserActions {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let success = true;
|
let success = true;
|
||||||
const updatePromises: Promise<boolean>[] = [];
|
if (
|
||||||
|
updates.callSign !== undefined ||
|
||||||
// Update call sign if provided
|
updates.displayPreference !== undefined
|
||||||
if (updates.callSign !== undefined) {
|
) {
|
||||||
updatePromises.push(
|
const callSignToSend = updates.callSign;
|
||||||
userIdentityService.updateUserProfile(
|
const preferenceToSend =
|
||||||
currentUser.address,
|
updates.displayPreference ?? currentUser.displayPreference;
|
||||||
updates.callSign,
|
success = await userIdentityService.updateUserProfile(
|
||||||
currentUser.displayPreference
|
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) {
|
if (success) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Profile Updated',
|
title: 'Profile Updated',
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useAuth as useBaseAuth } from '@/contexts/useAuth';
|
import { useAuth as useBaseAuth } from '@/contexts/useAuth';
|
||||||
|
import { useForum } from '@/contexts/useForum';
|
||||||
import { User, EVerificationStatus } from '@/types/identity';
|
import { User, EVerificationStatus } from '@/types/identity';
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
@ -18,20 +19,17 @@ export interface AuthState {
|
|||||||
export function useAuth(): AuthState {
|
export function useAuth(): AuthState {
|
||||||
const { currentUser, isAuthenticated, isAuthenticating, verificationStatus } =
|
const { currentUser, isAuthenticated, isAuthenticating, verificationStatus } =
|
||||||
useBaseAuth();
|
useBaseAuth();
|
||||||
|
const { userIdentityService } = useForum();
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const getDisplayName = (): string => {
|
const getDisplayName = (): string => {
|
||||||
if (!currentUser) return 'Anonymous';
|
if (!currentUser) return 'Anonymous';
|
||||||
|
// Centralized display logic; fallback to truncated address if service unavailable
|
||||||
if (currentUser.callSign) {
|
if (userIdentityService) {
|
||||||
return currentUser.callSign;
|
return userIdentityService.getDisplayName(currentUser.address);
|
||||||
}
|
}
|
||||||
|
const addr = currentUser.address;
|
||||||
if (currentUser.ensDetails?.ensName) {
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
||||||
return currentUser.ensDetails.ensName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${currentUser.address.slice(0, 6)}...${currentUser.address.slice(-4)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVerificationBadge = (): string | null => {
|
const getVerificationBadge = (): string | null => {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
// Get verification status from forum context for reactive updates
|
// Get verification status from forum context for reactive updates
|
||||||
const verificationInfo = useMemo(() => {
|
const verificationInfo = useMemo(() => {
|
||||||
@ -40,6 +41,21 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
);
|
);
|
||||||
}, [userVerificationStatus, address]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const getUserDisplayInfo = async () => {
|
const getUserDisplayInfo = async () => {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
@ -75,17 +91,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
const identity = await userIdentityService.getUserIdentity(address);
|
const identity = await userIdentityService.getUserIdentity(address);
|
||||||
|
|
||||||
if (identity) {
|
if (identity) {
|
||||||
let displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
const displayName = userIdentityService.getDisplayName(address);
|
||||||
|
|
||||||
// Determine display name based on preferences
|
|
||||||
if (
|
|
||||||
identity.displayPreference === EDisplayPreference.CALL_SIGN &&
|
|
||||||
identity.callSign
|
|
||||||
) {
|
|
||||||
displayName = identity.callSign;
|
|
||||||
} else if (identity.ensName) {
|
|
||||||
displayName = identity.ensName;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName,
|
displayName,
|
||||||
@ -101,9 +107,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setDisplayInfo({
|
setDisplayInfo({
|
||||||
displayName:
|
displayName: userIdentityService.getDisplayName(address),
|
||||||
verificationInfo.ensName ||
|
|
||||||
`${address.slice(0, 6)}...${address.slice(-4)}`,
|
|
||||||
callSign: null,
|
callSign: null,
|
||||||
ensName: verificationInfo.ensName || null,
|
ensName: verificationInfo.ensName || null,
|
||||||
ordinalDetails: null,
|
ordinalDetails: null,
|
||||||
@ -134,7 +138,7 @@ export function useEnhancedUserDisplay(address: string): UserDisplayInfo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getUserDisplayInfo();
|
getUserDisplayInfo();
|
||||||
}, [address, userIdentityService, verificationInfo]);
|
}, [address, userIdentityService, verificationInfo, refreshTrigger]);
|
||||||
|
|
||||||
// Update display info when verification status changes reactively
|
// Update display info when verification status changes reactively
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -63,6 +63,13 @@ export type {
|
|||||||
NetworkStatusData,
|
NetworkStatusData,
|
||||||
} from './utilities/useNetworkStatus';
|
} from './utilities/useNetworkStatus';
|
||||||
|
|
||||||
|
export {
|
||||||
|
useWakuHealth,
|
||||||
|
useWakuReady,
|
||||||
|
useWakuHealthStatus,
|
||||||
|
} from './useWakuHealth';
|
||||||
|
export type { WakuHealthState } from './useWakuHealth';
|
||||||
|
|
||||||
export { useForumSelectors } from './utilities/selectors';
|
export { useForumSelectors } from './utilities/selectors';
|
||||||
export type { ForumSelectors } 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 profileMsg = message as UserProfileUpdateMessage;
|
||||||
const { author, callSign, displayPreference, timestamp } = profileMsg;
|
const { author, callSign, displayPreference, timestamp } = profileMsg;
|
||||||
|
|
||||||
if (
|
const existing = this.cache.userIdentities[author];
|
||||||
!this.cache.userIdentities[author] ||
|
if (!existing || timestamp > existing.lastUpdated) {
|
||||||
this.cache.userIdentities[author]?.lastUpdated !== timestamp
|
const nextRecord = {
|
||||||
) {
|
ensName: existing?.ensName,
|
||||||
this.cache.userIdentities[author] = {
|
ordinalDetails: existing?.ordinalDetails,
|
||||||
ensName: undefined,
|
callSign: callSign !== undefined ? callSign : existing?.callSign,
|
||||||
ordinalDetails: undefined,
|
|
||||||
callSign,
|
|
||||||
displayPreference,
|
displayPreference,
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus:
|
||||||
};
|
existing?.verificationStatus ??
|
||||||
|
EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
|
} as UserIdentityCache[string];
|
||||||
|
|
||||||
|
this.cache.userIdentities[author] = nextRecord;
|
||||||
// Persist with address keyPath
|
// Persist with address keyPath
|
||||||
this.put(STORE.USER_IDENTITIES, {
|
this.put(STORE.USER_IDENTITIES, {
|
||||||
address: author,
|
address: author,
|
||||||
...this.cache.userIdentities[author],
|
...nextRecord,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface UserIdentity {
|
|||||||
export class UserIdentityService {
|
export class UserIdentityService {
|
||||||
private messageService: MessageService;
|
private messageService: MessageService;
|
||||||
private userIdentityCache: UserIdentityCache = {};
|
private userIdentityCache: UserIdentityCache = {};
|
||||||
|
private refreshListeners: Set<(address: string) => void> = new Set();
|
||||||
|
|
||||||
constructor(messageService: MessageService) {
|
constructor(messageService: MessageService) {
|
||||||
this.messageService = messageService;
|
this.messageService = messageService;
|
||||||
@ -40,15 +41,19 @@ export class UserIdentityService {
|
|||||||
if (import.meta.env?.DEV) {
|
if (import.meta.env?.DEV) {
|
||||||
console.debug('UserIdentityService: cache hit (internal)');
|
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 {
|
return {
|
||||||
address,
|
address,
|
||||||
ensName: cached.ensName,
|
ensName: cached.ensName,
|
||||||
ordinalDetails: cached.ordinalDetails,
|
ordinalDetails: cached.ordinalDetails,
|
||||||
callSign: cached.callSign,
|
callSign: cached.callSign,
|
||||||
displayPreference:
|
displayPreference: cached.displayPreference,
|
||||||
cached.displayPreference === 'call-sign'
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: cached.lastUpdated,
|
lastUpdated: cached.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(
|
verificationStatus: this.mapVerificationStatus(
|
||||||
cached.verificationStatus
|
cached.verificationStatus
|
||||||
@ -67,20 +72,26 @@ export class UserIdentityService {
|
|||||||
lastUpdated: persisted.lastUpdated,
|
lastUpdated: persisted.lastUpdated,
|
||||||
verificationStatus: persisted.verificationStatus,
|
verificationStatus: persisted.verificationStatus,
|
||||||
};
|
};
|
||||||
return {
|
const result = {
|
||||||
address,
|
address,
|
||||||
ensName: persisted.ensName,
|
ensName: persisted.ensName,
|
||||||
ordinalDetails: persisted.ordinalDetails,
|
ordinalDetails: persisted.ordinalDetails,
|
||||||
callSign: persisted.callSign,
|
callSign: persisted.callSign,
|
||||||
displayPreference:
|
displayPreference: persisted.displayPreference,
|
||||||
persisted.displayPreference === 'call-sign'
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: persisted.lastUpdated,
|
lastUpdated: persisted.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(
|
verificationStatus: this.mapVerificationStatus(
|
||||||
persisted.verificationStatus
|
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
|
// Fallback: Check Waku message cache
|
||||||
@ -102,20 +113,26 @@ export class UserIdentityService {
|
|||||||
verificationStatus: cacheServiceData.verificationStatus,
|
verificationStatus: cacheServiceData.verificationStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
address,
|
address,
|
||||||
ensName: cacheServiceData.ensName,
|
ensName: cacheServiceData.ensName,
|
||||||
ordinalDetails: cacheServiceData.ordinalDetails,
|
ordinalDetails: cacheServiceData.ordinalDetails,
|
||||||
callSign: cacheServiceData.callSign,
|
callSign: cacheServiceData.callSign,
|
||||||
displayPreference:
|
displayPreference: cacheServiceData.displayPreference,
|
||||||
cacheServiceData.displayPreference === 'call-sign'
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: cacheServiceData.lastUpdated,
|
lastUpdated: cacheServiceData.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(
|
verificationStatus: this.mapVerificationStatus(
|
||||||
cacheServiceData.verificationStatus
|
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) {
|
if (import.meta.env?.DEV) {
|
||||||
@ -129,10 +146,7 @@ export class UserIdentityService {
|
|||||||
ensName: identity.ensName,
|
ensName: identity.ensName,
|
||||||
ordinalDetails: identity.ordinalDetails,
|
ordinalDetails: identity.ordinalDetails,
|
||||||
callSign: identity.callSign,
|
callSign: identity.callSign,
|
||||||
displayPreference:
|
displayPreference: identity.displayPreference,
|
||||||
identity.displayPreference === EDisplayPreference.CALL_SIGN
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: identity.lastUpdated,
|
lastUpdated: identity.lastUpdated,
|
||||||
verificationStatus: identity.verificationStatus,
|
verificationStatus: identity.verificationStatus,
|
||||||
};
|
};
|
||||||
@ -150,10 +164,7 @@ export class UserIdentityService {
|
|||||||
ensName: cached.ensName,
|
ensName: cached.ensName,
|
||||||
ordinalDetails: cached.ordinalDetails,
|
ordinalDetails: cached.ordinalDetails,
|
||||||
callSign: cached.callSign,
|
callSign: cached.callSign,
|
||||||
displayPreference:
|
displayPreference: cached.displayPreference,
|
||||||
cached.displayPreference === 'call-sign'
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: cached.lastUpdated,
|
lastUpdated: cached.lastUpdated,
|
||||||
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
verificationStatus: this.mapVerificationStatus(cached.verificationStatus),
|
||||||
}));
|
}));
|
||||||
@ -164,7 +175,7 @@ export class UserIdentityService {
|
|||||||
*/
|
*/
|
||||||
async updateUserProfile(
|
async updateUserProfile(
|
||||||
address: string,
|
address: string,
|
||||||
callSign: string,
|
callSign: string | undefined,
|
||||||
displayPreference: EDisplayPreference
|
displayPreference: EDisplayPreference
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@ -172,17 +183,18 @@ export class UserIdentityService {
|
|||||||
console.debug('UserIdentityService: updating profile', { address });
|
console.debug('UserIdentityService: updating profile', { address });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
const unsignedMessage: UnsignedUserProfileUpdateMessage = {
|
const unsignedMessage: UnsignedUserProfileUpdateMessage = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
type: MessageType.USER_PROFILE_UPDATE,
|
type: MessageType.USER_PROFILE_UPDATE,
|
||||||
timestamp: Date.now(),
|
timestamp,
|
||||||
author: address,
|
author: address,
|
||||||
callSign,
|
displayPreference,
|
||||||
displayPreference:
|
|
||||||
displayPreference === EDisplayPreference.CALL_SIGN
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
};
|
};
|
||||||
|
// Only include callSign if provided and non-empty
|
||||||
|
if (callSign && callSign.trim()) {
|
||||||
|
unsignedMessage.callSign = callSign.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (import.meta.env?.DEV) {
|
if (import.meta.env?.DEV) {
|
||||||
console.debug('UserIdentityService: created unsigned message');
|
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;
|
return !!signedMessage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update user profile:', error);
|
console.error('Failed to update user profile:', error);
|
||||||
@ -295,10 +349,7 @@ export class UserIdentityService {
|
|||||||
ensName: undefined,
|
ensName: undefined,
|
||||||
ordinalDetails: undefined,
|
ordinalDetails: undefined,
|
||||||
callSign: undefined,
|
callSign: undefined,
|
||||||
displayPreference:
|
displayPreference,
|
||||||
displayPreference === EDisplayPreference.CALL_SIGN
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||||
};
|
};
|
||||||
@ -309,12 +360,12 @@ export class UserIdentityService {
|
|||||||
this.userIdentityCache[author] = {
|
this.userIdentityCache[author] = {
|
||||||
...this.userIdentityCache[author],
|
...this.userIdentityCache[author],
|
||||||
callSign,
|
callSign,
|
||||||
displayPreference:
|
displayPreference,
|
||||||
displayPreference === EDisplayPreference.CALL_SIGN
|
|
||||||
? EDisplayPreference.CALL_SIGN
|
|
||||||
: EDisplayPreference.WALLET_ADDRESS,
|
|
||||||
lastUpdated: timestamp,
|
lastUpdated: timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Notify listeners that the user identity has been updated
|
||||||
|
this.notifyRefreshListeners(author);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +400,21 @@ export class UserIdentityService {
|
|||||||
this.userIdentityCache = {};
|
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
|
* Get display name for user based on their preferences
|
||||||
*/
|
*/
|
||||||
@ -358,7 +424,10 @@ export class UserIdentityService {
|
|||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
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;
|
return identity.callSign;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export interface UnsignedModerateMessage extends UnsignedBaseMessage {
|
|||||||
export interface UnsignedUserProfileUpdateMessage extends UnsignedBaseMessage {
|
export interface UnsignedUserProfileUpdateMessage extends UnsignedBaseMessage {
|
||||||
type: MessageType.USER_PROFILE_UPDATE;
|
type: MessageType.USER_PROFILE_UPDATE;
|
||||||
callSign?: string;
|
callSign?: string;
|
||||||
displayPreference: 'call-sign' | 'wallet-address';
|
displayPreference: EDisplayPreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user