fix: call sign

This commit is contained in:
Danish Arora 2025-09-05 16:06:30 +05:30
parent cbe93afe7a
commit d2a512211f
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 395 additions and 315 deletions

195
TODO.md
View File

@ -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_

View File

@ -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">

View 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}`}
/>
);
}

View File

@ -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();
}
});
}
};

View File

@ -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',

View File

@ -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 => {

View File

@ -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(() => {

View File

@ -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
View 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(),
};
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
/**