diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 91ad8361..90668d1e 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -15,7 +15,6 @@ import { MoonIcon, SunIcon, ComputerDesktopIcon, - ExclamationTriangleIcon, UserGroupIcon, UserPlusIcon, LockClosedIcon, @@ -31,7 +30,6 @@ import * as RadioGroup from '@radix-ui/react-radio-group' import { SettingsSwitch } from '@/components/settings/settings-switch' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' -import toast from 'react-hot-toast' import { KeyBackupSettings } from '@/components/settings/key-backup-settings' import { BlockedUsersSettings } from '@/components/settings/blocked-users' import { PrivateFeedSettings } from '@/components/settings/private-feed-settings' @@ -133,12 +131,6 @@ function SettingsPage() { } } - // Privacy settings - const [privacySettings, setPrivacySettings] = useState({ - publicProfile: true, - showActivity: true, - }) - // Account creation date from profile const [accountCreatedAt, setAccountCreatedAt] = useState(null) @@ -206,13 +198,6 @@ function SettingsPage() { router.back() } - // TODO: Implement account deletion - const handleDeleteAccount = () => { - if (confirm('Are you sure you want to delete your account? This action cannot be undone.')) { - toast.error('Account deletion is not yet implemented') - } - } - const renderMainSettings = () => (
{visibleSections.map((section) => ( @@ -313,14 +298,6 @@ function SettingsPage() { Log Out -
@@ -381,32 +358,6 @@ function SettingsPage() {

Privacy

-
-
-

Public Profile

-

Allow anyone to view your profile

-
- - setPrivacySettings(prev => ({ ...prev, publicProfile: checked })) - } - /> -
- -
-
-

Show Activity Status

-

Let others see when you're active

-
- - setPrivacySettings(prev => ({ ...prev, showActivity: checked })) - } - /> -
-

Link Previews

diff --git a/hooks/use-avatar.ts b/hooks/use-avatar.ts index 560262e7..b997e041 100644 --- a/hooks/use-avatar.ts +++ b/hooks/use-avatar.ts @@ -1,7 +1,7 @@ 'use client' import { logger } from '@/lib/logger'; -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import type { DiceBearStyle } from '@/lib/services/unified-profile-service' export interface AvatarSettings { @@ -38,8 +38,11 @@ export function useAvatarSettings(userId: string): UseAvatarSettingsResult { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) + // Bumped per load so a slow response for a previous user cannot overwrite a newer one. + const requestRef = useRef(0) const loadSettings = useCallback(async () => { + const request = ++requestRef.current if (!userId) { setLoading(false) return @@ -53,6 +56,7 @@ export function useAvatarSettings(userId: string): UseAvatarSettingsResult { // Get profile to extract avatar settings const profile = await unifiedProfileService.getProfile(userId) + if (request !== requestRef.current) return if (profile?.avatar) { // Parse the avatar field to extract settings @@ -119,10 +123,11 @@ export function useAvatarSettings(userId: string): UseAvatarSettingsResult { }) } } catch (err) { + if (request !== requestRef.current) return logger.error('useAvatarSettings: Error loading settings:', err) setError('Failed to load avatar settings') } finally { - setLoading(false) + if (request === requestRef.current) setLoading(false) } }, [userId]) diff --git a/hooks/use-crypto-price.ts b/hooks/use-crypto-price.ts index 4ea51362..6ccf4e74 100644 --- a/hooks/use-crypto-price.ts +++ b/hooks/use-crypto-price.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { cryptoPriceService } from '@/lib/services/crypto-price-service' export interface UseCryptoPriceResult { @@ -30,14 +30,19 @@ export function useCryptoPrice( const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [fetchTrigger, setFetchTrigger] = useState(0) - const [skipCache, setSkipCache] = useState(false) + // A ref, not state: the effect reads it once per run and it must not itself re-run the effect. + const skipCacheRef = useRef(false) const refetch = useCallback(() => { - setSkipCache(true) + skipCacheRef.current = true setFetchTrigger((prev) => prev + 1) }, []) useEffect(() => { + // Read once per run; a refetch requested while inputs were invalid must not carry over. + const skipCache = skipCacheRef.current + skipCacheRef.current = false + // Reset state if inputs are invalid if (!fiatAmount || !fiatCurrency || !scheme || fiatAmount <= 0) { setCryptoAmount(null) @@ -87,10 +92,7 @@ export function useCryptoPrice( setPriceSources([]) setError(err instanceof Error ? err.message : 'Failed to fetch price') } finally { - if (!cancelled) { - setIsLoading(false) - setSkipCache(false) - } + if (!cancelled) setIsLoading(false) } } @@ -101,7 +103,7 @@ export function useCryptoPrice( return () => { cancelled = true } - }, [fiatAmount, fiatCurrency, scheme, fetchTrigger, skipCache]) + }, [fiatAmount, fiatCurrency, scheme, fetchTrigger]) return { cryptoAmount, diff --git a/lib/services/direct-message-service.ts b/lib/services/direct-message-service.ts index dd99849b..beb57db5 100644 --- a/lib/services/direct-message-service.ts +++ b/lib/services/direct-message-service.ts @@ -73,10 +73,12 @@ class DirectMessageService { } } - // 4. Check if we need to create a conversation invite + // 4. Create the conversation invite if this is the first message that way. + // An unknown answer (lookup failed) skips creation: a missing invite + // costs one inbox hint, a duplicate costs credits every time. const existingInvite = await this.getMyInviteToRecipient(senderId, recipientId) - if (!existingInvite) { + if (existingInvite === null) { // Create conversation invite const senderPubKey = getPublicKeyFromPrivate(privateKey) @@ -599,13 +601,11 @@ class DirectMessageService { } } - /** - * Get my invite to a recipient - */ + /** The sender's invite to the recipient; `null` when there is none, `undefined` when the lookup failed. */ private async getMyInviteToRecipient( senderId: string, recipientId: string - ): Promise | null> { + ): Promise | null | undefined> { try { const sdk = await getEvoSdk() @@ -622,8 +622,9 @@ class DirectMessageService { const docs = this.extractDocuments(response) return docs[0] || null - } catch { - return null + } catch (error) { + logger.warn('Could not check for an existing conversation invite:', error) + return undefined } } @@ -634,7 +635,7 @@ class DirectMessageService { senderId: string, recipientId: string ): Promise | null> { - return this.getMyInviteToRecipient(senderId, recipientId) + return (await this.getMyInviteToRecipient(senderId, recipientId)) ?? null } /**