From d2a512211f91bb907fc21441391a5a89e49cfeb9 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Fri, 5 Sep 2025 16:06:30 +0530 Subject: [PATCH] fix: call sign --- TODO.md | 195 -------------------- src/components/Header.tsx | 15 +- src/components/ui/waku-health-indicator.tsx | 83 +++++++++ src/contexts/ForumContext.tsx | 21 ++- src/hooks/actions/useUserActions.ts | 37 ++-- src/hooks/core/useAuth.ts | 16 +- src/hooks/core/useEnhancedUserDisplay.ts | 34 ++-- src/hooks/index.ts | 7 + src/hooks/useWakuHealth.ts | 127 +++++++++++++ src/lib/database/LocalDatabase.ts | 24 +-- src/lib/services/UserIdentityService.ts | 149 +++++++++++---- src/types/waku.ts | 2 +- 12 files changed, 395 insertions(+), 315 deletions(-) delete mode 100644 TODO.md create mode 100644 src/components/ui/waku-health-indicator.tsx create mode 100644 src/hooks/useWakuHealth.ts diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 9d3e504..0000000 --- a/TODO.md +++ /dev/null @@ -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_ diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 5480831..fef98c6 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -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(null); - const networkStatus = useNetworkStatus(); + const wakuHealth = useWakuHealthStatus(); const location = useLocation(); const { toast } = useToast(); const forum = useForum(); @@ -215,15 +216,9 @@ const Header = () => {
{/* Network Status */}
-
+ - {networkStatus.getStatusMessage()} + {wakuHealth.statusMessage} {forum.lastSync && ( diff --git a/src/components/ui/waku-health-indicator.tsx b/src/components/ui/waku-health-indicator.tsx new file mode 100644 index 0000000..3e93ae7 --- /dev/null +++ b/src/components/ui/waku-health-indicator.tsx @@ -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 ; + case 'connecting': + return ; + case 'disconnected': + return ; + case 'error': + return ; + default: + return ; + } + }; + + 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 ( +
+
{getIcon()}
+ {showText && ( + + {statusMessage} + + )} +
+ ); +} + +/** + * 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 ( +
+ ); +} diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 107f621..9095d61 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -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(); + } }); } }; diff --git a/src/hooks/actions/useUserActions.ts b/src/hooks/actions/useUserActions.ts index ea143cc..c7261fd 100644 --- a/src/hooks/actions/useUserActions.ts +++ b/src/hooks/actions/useUserActions.ts @@ -220,35 +220,20 @@ export function useUserActions(): UserActions { try { let success = true; - const updatePromises: Promise[] = []; - - // 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', diff --git a/src/hooks/core/useAuth.ts b/src/hooks/core/useAuth.ts index c848ffe..4ba7a1c 100644 --- a/src/hooks/core/useAuth.ts +++ b/src/hooks/core/useAuth.ts @@ -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 => { diff --git a/src/hooks/core/useEnhancedUserDisplay.ts b/src/hooks/core/useEnhancedUserDisplay.ts index 9b499da..bdad558 100644 --- a/src/hooks/core/useEnhancedUserDisplay.ts +++ b/src/hooks/core/useEnhancedUserDisplay.ts @@ -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(() => { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d2e21e1..d1b4ffa 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -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'; diff --git a/src/hooks/useWakuHealth.ts b/src/hooks/useWakuHealth.ts new file mode 100644 index 0000000..e763c99 --- /dev/null +++ b/src/hooks/useWakuHealth.ts @@ -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.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(), + }; +} diff --git a/src/lib/database/LocalDatabase.ts b/src/lib/database/LocalDatabase.ts index 14003af..669c1c0 100644 --- a/src/lib/database/LocalDatabase.ts +++ b/src/lib/database/LocalDatabase.ts @@ -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; diff --git a/src/lib/services/UserIdentityService.ts b/src/lib/services/UserIdentityService.ts index 1ba6853..a8b6be5 100644 --- a/src/lib/services/UserIdentityService.ts +++ b/src/lib/services/UserIdentityService.ts @@ -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 { 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; } diff --git a/src/types/waku.ts b/src/types/waku.ts index f5d3c96..3d712f4 100644 --- a/src/types/waku.ts +++ b/src/types/waku.ts @@ -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; } /**