Display user's blossom servers in menu (#90)

* feat: Display blossom servers in user menu with caching

Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown.

**Key Changes:**

1. **Database Schema (db.ts)**:
   - Added `CachedBlossomServerList` interface
   - Added `blossomServers` table to Dexie v15
   - Indexed by pubkey and updatedAt for efficient querying

2. **Caching Service (blossom-server-cache.ts)**:
   - Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache
   - 24-hour TTL to reduce network requests
   - Auto-caches kind:10063 events from EventStore
   - Mirrors relay-list-cache pattern for consistency

3. **Sync Hooks**:
   - `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events
   - `useAccountSync`: Extended to fetch and watch active account's blossom servers

4. **State Management**:
   - Added `blossomServers` field to `activeAccount` in GrimoireState
   - Created `setActiveAccountBlossomServers` pure function and callback
   - Reactive updates when user's server list changes

5. **User Menu UI (user-menu.tsx)**:
   - Added blossom servers section with HardDrive icon
   - Shows server count badge (e.g., "3 servers")
   - Clickable links open servers in new tabs
   - Displays only when user has configured servers

**Architecture:**
- Follows existing relay list caching pattern for consistency
- Reactive: UI auto-updates when kind:10063 events arrive
- Incremental sync: Fetches on login, caches for 24h
- Performance: Memory cache for <1ms lookups, Dexie for persistence

**Testing:**
- Build: ✓ No TypeScript errors
- Tests: ✓ All 838 tests passing

* feat: Open blossom server file lists directly from menus

**User Menu & Profile Viewer Improvements:**

1. **Enhanced Click Behavior**:
   - Clicking a blossom server now opens the file list for that server
   - Shows blobs uploaded by the user (user menu) or profile owner (profile viewer)
   - Pre-selects the clicked server in the dropdown

2. **UX Improvements**:
   - Removed server count from user menu label (cleaner UI)
   - Added `cursor-crosshair` to blossom server items (consistent with other clickable items)
   - Removed external link icon (not opening external URL anymore)

3. **Technical Changes**:
   - Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection
   - User menu: Opens `blossom list` with `serverUrl` for active user
   - Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl`

**Flow:**
- User menu → Click server → Opens files for active user on that server
- Profile viewer → Click server → Opens files for viewed user on that server

* fix: Properly fetch blossom servers for any profile view

**Problem:**
Blossom servers were only visible for the logged-in user's profile,
not for other users' profiles being viewed.

**Solution:**
Enhanced ProfileViewer blossom server fetching with multi-layer approach:

1. **Cache-first loading**: Check blossomServerCache for instant display
2. **EventStore check**: Use existing cached event if available
3. **Reactive subscription**: Subscribe to EventStore for real-time updates
4. **Network fetch**: Use addressLoader to fetch latest from relays
5. **Auto-caching**: Update cache when new events arrive

**Benefits:**
- Blossom servers now display for ANY user's profile
- Instant display from cache (< 1ms)
- Reactive updates when data changes
- Proper cache hydration for future visits
- Consistent with relay list fetching pattern

**Technical:**
- Imported and integrated blossomServerCache service
- Added cache check before network fetch
- Separated EventStore subscription from network fetch
- Added cache updates on event arrival

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-14 14:52:16 +01:00
committed by GitHub
parent 998944fdf7
commit 16764e1aca
11 changed files with 510 additions and 32 deletions

View File

@@ -5,12 +5,17 @@ import { useGrimoire } from "@/core/state";
import { addressLoader } from "@/services/loaders";
import type { RelayInfo } from "@/types/app";
import { normalizeRelayURL } from "@/lib/relay-url";
import { getServersFromEvent } from "@/services/blossom";
/**
* Hook that syncs active account with Grimoire state and fetches relay lists
* Hook that syncs active account with Grimoire state and fetches relay lists and blossom servers
*/
export function useAccountSync() {
const { setActiveAccount, setActiveAccountRelays } = useGrimoire();
const {
setActiveAccount,
setActiveAccountRelays,
setActiveAccountBlossomServers,
} = useGrimoire();
const eventStore = useEventStore();
// Watch active account from accounts service
@@ -83,4 +88,41 @@ export function useAccountSync() {
storeSubscription.unsubscribe();
};
}, [activeAccount?.pubkey, eventStore, setActiveAccountRelays]);
// Fetch and watch blossom server list (kind 10063) when account changes
useEffect(() => {
if (!activeAccount?.pubkey) {
return;
}
const pubkey = activeAccount.pubkey;
let lastBlossomEventId: string | undefined;
// Subscribe to kind 10063 (blossom server list)
const subscription = addressLoader({
kind: 10063,
pubkey,
identifier: "",
}).subscribe();
// Watch for blossom server list event in store
const storeSubscription = eventStore
.replaceable(10063, pubkey, "")
.subscribe((blossomListEvent) => {
if (!blossomListEvent) return;
// Only process if this is a new event
if (blossomListEvent.id === lastBlossomEventId) return;
lastBlossomEventId = blossomListEvent.id;
// Parse servers from event
const servers = getServersFromEvent(blossomListEvent);
setActiveAccountBlossomServers(servers);
});
return () => {
subscription.unsubscribe();
storeSubscription.unsubscribe();
};
}, [activeAccount?.pubkey, eventStore, setActiveAccountBlossomServers]);
}

View File

@@ -0,0 +1,24 @@
/**
* Hook to keep blossom server cache in sync with EventStore
*
* Subscribes to kind:10063 events and automatically caches them in Dexie.
* Should be used once at app root level.
*/
import { useEffect } from "react";
import { useEventStore } from "applesauce-react/hooks";
import blossomServerCache from "@/services/blossom-server-cache";
export function useBlossomServerCacheSync() {
const eventStore = useEventStore();
useEffect(() => {
// Subscribe to EventStore for auto-caching
blossomServerCache.subscribeToEventStore(eventStore);
// Cleanup on unmount
return () => {
blossomServerCache.unsubscribe();
};
}, [eventStore]);
}