From ea56806c0c94db8528677122660fa08c2866c544 Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 1 Sep 2026 14:07:36 -0500 Subject: [PATCH] fix: show only fully published events in Most Recent Publication box - Add PublicationStatus type and computePublicationStatus() helper - Add useProfilePublications hook that queries relays via feedGet and determines per-event publication status by comparing served relays against all enabled relays - Rewrite HomeScreen PublicationResult to show only fully published events in the main box; partial events appear only in the expandable Relay results section - Show empty state when no fully published events exist - Add tests: fully published shown, partial hidden, older full shown when newest is partial, all-partial shows empty, relay details expandable, no duplicates - Fix publishFlow integration test to seed profileFeedItems --- frontend/src/lib/publications.ts | 69 +++++++++ frontend/src/lib/types.ts | 3 + frontend/src/screens/HomeScreen.tsx | 131 +++++++++------- frontend/src/state/AppProvider.tsx | 7 +- frontend/src/test/publications.test.tsx | 194 ++++++++++++++++++++++++ frontend/src/test/publishFlow.test.tsx | 15 +- 6 files changed, 360 insertions(+), 59 deletions(-) create mode 100644 frontend/src/lib/publications.ts create mode 100644 frontend/src/test/publications.test.tsx diff --git a/frontend/src/lib/publications.ts b/frontend/src/lib/publications.ts new file mode 100644 index 0000000..4b97ae0 --- /dev/null +++ b/frontend/src/lib/publications.ts @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useState } from 'react'; +import type { FeedItem, PublicationStatus, RelayConfig } from './types'; +import { useApp } from '../state/AppProvider'; + +/** Determine whether a note is fully or partially published. */ +export function computePublicationStatus( + itemRelays: string[], + enabledRelays: RelayConfig[], +): PublicationStatus { + const enabled = enabledRelays.filter((r) => r.enabled).map((r) => r.url); + if (enabled.length === 0) { + return 'fully_published'; + } + const served = new Set(itemRelays); + const allServed = enabled.every((url) => served.has(url)); + return allServed ? 'fully_published' : 'partially_published'; +} + +export interface ProfilePublication extends FeedItem { + publicationStatus: PublicationStatus; +} + +export interface UseProfilePublicationsResult { + publications: ProfilePublication[]; + fullyPublished: ProfilePublication[]; + loading: boolean; + error: string | null; +} + +export function useProfilePublications(): UseProfilePublicationsResult { + const { state, feedGet } = useApp(); + const [publications, setPublications] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const authorNpub = state?.active_profile?.npub ?? null; + + const load = useCallback(async () => { + if (!authorNpub) { + setPublications([]); + return; + } + setLoading(true); + setError(null); + try { + const items = await feedGet(50, false, authorNpub); + const enabledRelays = state?.settings.relays ?? []; + const enriched: ProfilePublication[] = items.map((item) => ({ + ...item, + publicationStatus: computePublicationStatus(item.relays, enabledRelays), + })); + enriched.sort((a, b) => b.created_at - a.created_at); + setPublications(enriched); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + setPublications([]); + } finally { + setLoading(false); + } + }, [authorNpub, feedGet, state?.settings.relays]); + + useEffect(() => { + void load(); + }, [load]); + + const fullyPublished = publications.filter((p) => p.publicationStatus === 'fully_published'); + + return { publications, fullyPublished, loading, error }; +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index fa8d521..5c11d31 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -76,6 +76,9 @@ export interface MetadataPublishReport { failed: RelayFailure[]; } +/** Publication status derived from comparing served relays against all enabled relays. */ +export type PublicationStatus = 'fully_published' | 'partially_published'; + /** A single note shown in the aggregated feed. */ export interface FeedItem { /** Bech32 note id. */ diff --git a/frontend/src/screens/HomeScreen.tsx b/frontend/src/screens/HomeScreen.tsx index a62aff9..54e9cf1 100644 --- a/frontend/src/screens/HomeScreen.tsx +++ b/frontend/src/screens/HomeScreen.tsx @@ -8,6 +8,8 @@ import { EmptyState } from '../components/EmptyState'; import { Icon } from '../components/Icon'; import { shortenNpub } from '../lib/format'; import type { Screen } from '../lib/navigation'; +import type { ProfilePublication } from '../lib/publications'; +import { useProfilePublications } from '../lib/publications'; import { useApp } from '../state/AppProvider'; interface HomeScreenProps { @@ -16,7 +18,8 @@ interface HomeScreenProps { } export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) { - const { state, lastPublish, selectProfile } = useApp(); + const { state, selectProfile } = useApp(); + const { publications, fullyPublished, loading, error } = useProfilePublications(); const [selecting, setSelecting] = useState(null); const onSelect = async (npub: string) => { @@ -182,7 +185,10 @@ export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) {
onNavigate('compose')} />
@@ -193,18 +199,34 @@ export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) { } function PublicationResult({ - lastPublish, + publications, + fullyPublished, + loading, + error, onNavigateCompose, }: { - lastPublish: ReturnType['lastPublish']; + publications: ProfilePublication[]; + fullyPublished: ProfilePublication[]; + loading: boolean; + error: string | null; onNavigateCompose: () => void; }) { - if (!lastPublish) { + if (loading) { + return

Loading publications…

; + } + + if (error) { + return ( + + {error} + + ); + } + + if (publications.length === 0) { return (
-

- You haven't published anything yet. -

+

You haven't published anything yet.