diff --git a/package.json b/package.json
index f687e4e..fde297f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jumble-imwald",
- "version": "11.1",
+ "version": "11.2",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true,
"type": "module",
diff --git a/src/components/CacheRelaysSetting/index.tsx b/src/components/CacheRelaysSetting/index.tsx
index 9c11e28..840fff1 100644
--- a/src/components/CacheRelaysSetting/index.tsx
+++ b/src/components/CacheRelaysSetting/index.tsx
@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay, TMailboxRelayScope } from '@/types'
-import { useEffect, useState, useMemo, useRef } from 'react'
+import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
DndContext,
@@ -28,15 +28,17 @@ import DiscoveredRelays from '../MailboxSetting/DiscoveredRelays'
import { createCacheRelaysDraftEvent } from '@/lib/draft-event'
import { getRelayListFromEvent } from '@/lib/event-metadata'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
-import { CloudUpload, Loader, Trash2, RefreshCw, Database, WrapText, Search, X } from 'lucide-react'
+import { CloudUpload, Loader, Trash2, RefreshCw, Database, WrapText, Search, X, TriangleAlert } from 'lucide-react'
import { Input } from '@/components/ui/input'
import indexedDb from '@/services/indexed-db.service'
import postEditorCache from '@/services/post-editor-cache.service'
import { StorageKey } from '@/constants'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer'
+import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { toast } from 'sonner'
+import { Event } from 'nostr-tools'
export default function CacheRelaysSetting() {
const { t } = useTranslation()
@@ -374,7 +376,20 @@ export default function CacheRelaysSetting() {
setSearchQuery('')
// Update cache info
loadCacheInfo()
- toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}}', { deleted: result.deleted, kept: result.kept }))
+ // Reload items to get accurate count after cleanup
+ const itemsAfterCleanup = await indexedDb.getStoreItems(selectedStore)
+ const actualCount = itemsAfterCleanup.length
+
+ // Show message with actual count
+ if (actualCount !== result.kept) {
+ toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}} (total items after cleanup: {{total}})', {
+ deleted: result.deleted,
+ kept: result.kept,
+ total: actualCount
+ }))
+ } else {
+ toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}}', { deleted: result.deleted, kept: result.kept }))
+ }
} catch (error) {
console.error('Failed to cleanup duplicates:', error)
if (error instanceof Error && error.message === 'Not a replaceable event store') {
@@ -387,6 +402,52 @@ export default function CacheRelaysSetting() {
}
}
+ // Check if an event is invalid
+ const isInvalidEvent = useCallback((item: { key: string; value: any; addedAt: number }): boolean => {
+ if (!item || !item.value) return true
+
+ const event = item.value as Event
+ // Check for required Nostr event fields
+ if (!event.pubkey || !event.kind || typeof event.created_at !== 'number') {
+ return true
+ }
+
+ // Check for tags array (required for Nostr events)
+ if (!event.tags || !Array.isArray(event.tags)) {
+ return true
+ }
+
+ // Check for id and sig (these should be present in valid events)
+ if (!event.id || !event.sig) {
+ return true
+ }
+
+ return false
+ }, [])
+
+ // Get explanation for why an event is invalid
+ const getInvalidEventExplanation = useCallback((item: { key: string; value: any; addedAt: number }): string => {
+ if (!item || !item.value) {
+ return t('Event has no value data')
+ }
+
+ const event = item.value as Event
+ const missing: string[] = []
+
+ if (!event.pubkey) missing.push(t('pubkey'))
+ if (!event.kind) missing.push(t('kind'))
+ if (typeof event.created_at !== 'number') missing.push(t('created_at'))
+ if (!event.tags || !Array.isArray(event.tags)) missing.push(t('tags'))
+ if (!event.id) missing.push(t('id'))
+ if (!event.sig) missing.push(t('sig'))
+
+ if (missing.length > 0) {
+ return t('Event is missing required fields: {{fields}}', { fields: missing.join(', ') })
+ }
+
+ return t('Event appears to be invalid or corrupted')
+ }, [t])
+
const save = async () => {
if (!pubkey) return
@@ -477,10 +538,10 @@ export default function CacheRelaysSetting() {
{t('Clear cached data stored in your browser, including IndexedDB events, localStorage settings, and service worker caches.')}
-
+