fix: show only selected post from favorites

This commit is contained in:
TerryM
2026-06-05 18:52:25 +08:00
parent 2d003c6fef
commit 9f5367ae12
3 changed files with 51 additions and 27 deletions

View File

@@ -89,6 +89,7 @@ export function PopularRankRow({
categories, categories,
browseSort = "popular", browseSort = "popular",
showRank = true, showRank = true,
singlePostLink = false,
onFavoriteChange, onFavoriteChange,
}: { }: {
post: Post; post: Post;
@@ -96,6 +97,7 @@ export function PopularRankRow({
categories: Category[]; categories: Category[];
browseSort?: string; browseSort?: string;
showRank?: boolean; showRank?: boolean;
singlePostLink?: boolean;
onFavoriteChange?: (postId: string, favorited: boolean) => void; onFavoriteChange?: (postId: string, favorited: boolean) => void;
}) { }) {
const { t, lang } = useI18n(); const { t, lang } = useI18n();
@@ -142,6 +144,7 @@ export function PopularRankRow({
const params = new URLSearchParams(); const params = new URLSearchParams();
if (browseSort) params.set("sort", browseSort); if (browseSort) params.set("sort", browseSort);
params.set("post", post.id); params.set("post", post.id);
if (singlePostLink) params.set("single", "1");
navigate(lp(`/browse?${params.toString()}`)); navigate(lp(`/browse?${params.toString()}`));
}} }}
aria-label={r.title} aria-label={r.title}

View File

@@ -24,6 +24,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
const type = sp.get("type") || "all"; const type = sp.get("type") || "all";
const q = (sp.get("q") || "").trim(); const q = (sp.get("q") || "").trim();
const sort = sp.get("sort") || ""; const sort = sp.get("sort") || "";
const singlePostMode = sp.get("single") === "1" && !!sp.get("post");
const params = useMemo( const params = useMemo(
() => ({ scope, type, q, sort, lang }), () => ({ scope, type, q, sort, lang }),
@@ -55,6 +56,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
}, [q]); }, [q]);
useEffect(() => { useEffect(() => {
if (singlePostMode) return;
const el = sentinelRef.current; const el = sentinelRef.current;
if (!el) return; if (!el) return;
const io = new IntersectionObserver( const io = new IntersectionObserver(
@@ -75,7 +77,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
); );
io.observe(el); io.observe(el);
return () => io.disconnect(); return () => io.disconnect();
}, [loadMore]); }, [loadMore, singlePostMode]);
// When arriving with a `?post=<id>` query (or legacy `#post-<id>` hash), // When arriving with a `?post=<id>` query (or legacy `#post-<id>` hash),
// scroll to that bubble — loading more pages until it shows up — then give // scroll to that bubble — loading more pages until it shows up — then give
@@ -90,13 +92,19 @@ export function MessageStream({ scope }: MessageStreamProps) {
); );
const [isFetchingTargetPost, setIsFetchingTargetPost] = useState(false); const [isFetchingTargetPost, setIsFetchingTargetPost] = useState(false);
const [targetPostFetchFailed, setTargetPostFetchFailed] = useState(false); const [targetPostFetchFailed, setTargetPostFetchFailed] = useState(false);
const targetAlreadyInBaseItems = useMemo( const baseTargetPost = useMemo(
() => () =>
!!queryTargetPostId && queryTargetPostId
items.some((post) => post.id === queryTargetPostId), ? (items.find((post) => post.id === queryTargetPostId) ?? null)
: null,
[items, queryTargetPostId], [items, queryTargetPostId],
); );
const targetAlreadyInBaseItems = !!baseTargetPost;
const streamItems = useMemo(() => { const streamItems = useMemo(() => {
if (singlePostMode) {
if (baseTargetPost) return [baseTargetPost];
return resolvedTargetPost ? [resolvedTargetPost] : [];
}
if ( if (
resolvedTargetPost && resolvedTargetPost &&
!items.some((post) => post.id === resolvedTargetPost.id) !items.some((post) => post.id === resolvedTargetPost.id)
@@ -104,7 +112,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
return [resolvedTargetPost, ...items]; return [resolvedTargetPost, ...items];
} }
return items; return items;
}, [items, resolvedTargetPost]); }, [baseTargetPost, items, resolvedTargetPost, singlePostMode]);
const groups = useGroupedByDay(streamItems, lang); const groups = useGroupedByDay(streamItems, lang);
// Lock only engages while we are actively running the smooth-scroll animation // Lock only engages while we are actively running the smooth-scroll animation
// — not during the wait/pagination phase — so the page never feels frozen // — not during the wait/pagination phase — so the page never feels frozen
@@ -355,13 +363,18 @@ export function MessageStream({ scope }: MessageStreamProps) {
// their specific post, not just lazily loading the feed. // their specific post, not just lazily loading the feed.
const targetInLoadedItems = const targetInLoadedItems =
!!queryTargetPostId && streamItems.some((p) => p.id === queryTargetPostId); !!queryTargetPostId && streamItems.some((p) => p.id === queryTargetPostId);
const isSearchingDeepTarget = const isSearchingDeepTarget = singlePostMode
!!queryTargetPostId && ? !!queryTargetPostId && !targetInLoadedItems && isFetchingTargetPost
: !!queryTargetPostId &&
!targetInLoadedItems && !targetInLoadedItems &&
!error && !error &&
(isFetchingTargetPost || hasMore || isLoading); (isFetchingTargetPost || hasMore || isLoading);
const targetNotFoundInStream = const targetNotFoundInStream = singlePostMode
!!queryTargetPostId && ? !!queryTargetPostId &&
!targetInLoadedItems &&
targetPostFetchFailed &&
!isFetchingTargetPost
: !!queryTargetPostId &&
!targetInLoadedItems && !targetInLoadedItems &&
!error && !error &&
targetPostFetchFailed && targetPostFetchFailed &&
@@ -373,12 +386,17 @@ export function MessageStream({ scope }: MessageStreamProps) {
<div className="mx-auto max-w-full md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]"> <div className="mx-auto max-w-full md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
{/* Filters stay pinned below the global header (which shows the page {/* Filters stay pinned below the global header (which shows the page
name) so users can switch filters while scrolling. */} name) so users can switch filters while scrolling. */}
{!singlePostMode ? (
<div <div
ref={filterBarRef} ref={filterBarRef}
className="sticky top-[64px] z-30 bg-ark-bg md:top-[70px]" className="sticky top-[64px] z-30 bg-ark-bg md:top-[70px]"
> >
<FilterChips type={type} onTypeChange={(v) => updateParam("type", v)} /> <FilterChips
type={type}
onTypeChange={(v) => updateParam("type", v)}
/>
</div> </div>
) : null}
<div className="flex flex-col gap-3 px-4 pt-4 md:px-0 md:pt-2"> <div className="flex flex-col gap-3 px-4 pt-4 md:px-0 md:pt-2">
{isSearchingDeepTarget ? ( {isSearchingDeepTarget ? (
@@ -432,7 +450,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
</p> </p>
) : null} ) : null}
{error ? ( {!singlePostMode && error ? (
<div <div
role="alert" role="alert"
className="my-4 flex flex-col gap-3 rounded-xl border border-red-900 bg-red-950/40 px-4 py-3 text-sm text-red-200 sm:flex-row sm:items-center sm:justify-between" className="my-4 flex flex-col gap-3 rounded-xl border border-red-900 bg-red-950/40 px-4 py-3 text-sm text-red-200 sm:flex-row sm:items-center sm:justify-between"
@@ -450,7 +468,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
</div> </div>
) : null} ) : null}
{isLoading && !error ? ( {!singlePostMode && isLoading && !error ? (
<div <div
aria-live="polite" aria-live="polite"
aria-label={t("loading")} aria-label={t("loading")}
@@ -462,7 +480,9 @@ export function MessageStream({ scope }: MessageStreamProps) {
</> </>
)} )}
{!singlePostMode ? (
<div ref={sentinelRef} aria-hidden className="h-1" /> <div ref={sentinelRef} aria-hidden className="h-1" />
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -169,6 +169,7 @@ export default function Favorites() {
categories={categories} categories={categories}
browseSort="" browseSort=""
showRank={false} showRank={false}
singlePostLink
onFavoriteChange={(_, favorited) => { onFavoriteChange={(_, favorited) => {
if (!favorited) setReloadKey((value) => value + 1); if (!favorited) setReloadKey((value) => value + 1);
}} }}