Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface CollectionCardProps {
interactiveActions?: boolean;
/** Show the owner Delete action. Opt in only from the My Collections overview. */
showDeleteAction?: boolean;
unfollowCollectionHandler?: (id: string) => void;
}

/**
Expand Down Expand Up @@ -91,6 +92,7 @@ export function CollectionCard({
presentation = 'landing',
interactiveActions = true,
showDeleteAction = false,
unfollowCollectionHandler,
}: CollectionCardProps) {
const compositeId = buildCompositeId({ pubky: authorPubky, id: postId });
const isMobile = useIsMobile();
Expand Down Expand Up @@ -133,6 +135,7 @@ export function CollectionCard({
isWideLayout={isWideLayout}
interactiveActions={interactiveActions}
showDeleteAction={showDeleteAction}
unfollowCollectionHandler={unfollowCollectionHandler}
/>
);
}
Expand All @@ -149,6 +152,7 @@ interface CollectionCardContentProps {
isWideLayout: boolean;
interactiveActions: boolean;
showDeleteAction: boolean;
unfollowCollectionHandler?: (id: string) => void;
}

function CollectionCardContent({
Expand All @@ -163,6 +167,7 @@ function CollectionCardContent({
isWideLayout,
interactiveActions,
showDeleteAction,
unfollowCollectionHandler,
}: CollectionCardContentProps) {
const isEmbed = presentation === 'embed';
const showTagAddButton = interactiveActions && !isEmbed;
Expand Down Expand Up @@ -222,6 +227,9 @@ function CollectionCardContent({
requireAuth(() => {
void toggle();
});
if (isBookmarked) {
unfollowCollectionHandler?.(compositeId);
}
};

// Collection-specific toast copy so success / failure reads as "Collection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useState } from 'react';
import { Container } from '@/atoms/Container/Container';
import { cn } from '@/libs/utils/utils';
import { DiscoverCollections } from '@/organisms/Collections/DiscoverCollections/DiscoverCollections';
Expand Down Expand Up @@ -28,16 +29,21 @@ export function CollectionsSections({ className }: CollectionsSectionsProps) {
const hasHydrated = useAuthStore((state) => state.hasHydrated);
const currentUserPubky = useAuthStore((state) => state.currentUserPubky);
const showPersonalSections = hasHydrated && Boolean(currentUserPubky);
const [unfollowedId, setUnfollowedId] = useState<string | null>(null);

function unfollowedCollectionIdHandler(id: string) {
setUnfollowedId(id);
}

return (
<Container overrideDefaults className={cn('flex w-full flex-col gap-12', className)}>
{showPersonalSections ? (
<>
<MyCollections />
<FollowedCollections />
<FollowedCollections unfollowHandler={unfollowedCollectionIdHandler} />
</>
) : null}
<DiscoverCollections />
<DiscoverCollections unfollowedId={unfollowedId} />
</Container>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const EMPTY_CURSOR: DiscoverCursor = { lastPostId: undefined, streamTail: 0 };
* raw posts and filtering removed them all), we surface a toast so the
* click gets feedback instead of silently rendering nothing.
*/
export function DiscoverCollections() {
export function DiscoverCollections({ unfollowedId }: { unfollowedId?: string | null }) {
const { toast } = useToast();
// The stream layer filters own collections against the viewer read from the
// auth store, so a viewer switch must reset and refetch (effect dep below).
Expand All @@ -102,6 +102,12 @@ export function DiscoverCollections() {
const visibleIdsRef = useRef<string[]>([]);
visibleIdsRef.current = visibleIds;

useEffect(() => {
if (unfollowedId && !visibleIdsRef.current.includes(unfollowedId)) {
setVisibleIds([unfollowedId, ...visibleIdsRef.current]);
}
}, [unfollowedId]);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

// Cancellation token for the in-flight initial fetch. When the effect
// re-fires (StrictMode double-invoke, or genuine viewer switch), the old
// fetch's closure sees `cancelled.current === true` and skips its state
Expand Down Expand Up @@ -146,7 +152,6 @@ export function DiscoverCollections() {
limit: COLLECTIONS_SECTION_PAGE_SIZE,
});
if (token?.cancelled) return;

// `nextPageIds` is already post-filter; `nextCursor` is the raw skip
// offset the stream layer consumed to produce it. Dedup is defensive
// only — the stream layer's cursor accounting should prevent overlap.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const EMPTY_IDS: string[] = [];
* Live-reactive: a Follow / Unfollow performed anywhere in the app
* pushes a card into / out of this section without a reload.
*/
export function FollowedCollections() {
export function FollowedCollections({ unfollowHandler }: { unfollowHandler?: (id: string) => void }) {
const { toast } = useToast();
// Gate the seed fetch on auth hydration. `StreamPostsController.getOrFetchStreamSlice`
// reads `viewerId` from the auth store synchronously, and the bookmarks-collection
Expand Down Expand Up @@ -196,7 +196,15 @@ export function FollowedCollections() {
// collection. Seed the bookmark hook so the CTA renders as
// "Unfollow" on the first paint instead of briefly flashing
// "Follow" while the async existence check resolves.
return <CollectionCard key={compositeId} authorPubky={pubky} postId={id} initialIsBookmarked />;
return (
<CollectionCard
key={compositeId}
authorPubky={pubky}
postId={id}
initialIsBookmarked
unfollowCollectionHandler={unfollowHandler}
/>
);
})}
</Container>
)}
Expand Down