0
2
<script lang="ts">
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
import GalleryCard from '$lib/components/molecules/GalleryCard.svelte'
import GalleryCardSkeleton from '$lib/components/molecules/GalleryCardSkeleton.svelte'
import CommentSheet from '$lib/components/organisms/CommentSheet.svelte'
import FeedTabs from '$lib/components/molecules/FeedTabs.svelte'
import PullToRefresh from '$lib/components/molecules/PullToRefresh.svelte'
import OGMeta from '$lib/components/atoms/OGMeta.svelte'
import { poolFeedQuery, spaceSupportQuery } from '$lib/queries'
import SpacesRequired from '$lib/components/molecules/SpacesRequired.svelte'
import { viewer } from '$lib/stores'
import { Lock } from 'lucide-svelte'
import type { GalleryView, GroupView } from '$hatk/client'
import { spaceBlobViews } from '$lib/spaceBlob'
// The pools you are in, in one column. Not a feed in grain's usual sense:
// every other one is SQL over the index with a cursor, and none of this is
// indexed — a pool is a permissioned space, so it is read live, per viewer,
// with a credential each group's host issues. Which means no cursor, no
// infinite scroll, and nothing here that could be served to anyone else.
const queryClient = useQueryClient()
const feed = createQuery(() => ({ ...poolFeedQuery(), enabled: !!$viewer }))
// The tab is hidden without spaces, but the URL is still a URL.
const spaces = createQuery(() => ({ ...spaceSupportQuery(), enabled: !!$viewer }))
const blocked = $derived(spaces.isSuccess && spaces.data?.supported !== true)
async function refresh() {
await queryClient.invalidateQueries({ queryKey: ['poolFeed'] })
}
const groups = $derived(new Map((feed.data?.groups ?? []).map((g: GroupView) => [g.did, g])))
// The sheet a card's comment button opens, carrying which pool to read the
// thread from — it is not in any index to be looked up by subject alone.
let thread = $state<{ uri: string; group: string } | null>(null)
const openThread = (i: number) => {
const g = feed.data?.galleries?.[i]
if (g) thread = { uri: `at://${g.did}/social.grain.gallery/${g.rkey}`, group: g.group }
}
const cards = $derived(
(feed.data?.galleries ?? []).map(
(g) =>
({
uri: `at://${g.did}/social.grain.gallery/${g.rkey}`,
cid: '',
title: g.title,
description: g.description,
createdAt: g.createdAt,
creator: { did: g.did, handle: g.handle, displayName: g.displayName, avatar: g.avatar },
// The card already knows how to say which pool a gallery is in — the
// "in <group>" line under the author. No `item`: that is the accept
// record a public pool has and this one does not, which is also what
// keeps the card from offering to remove it.
group: groups.get(g.group)
? {
did: g.group,
handle: groups.get(g.group)!.handle,
displayName: groups.get(g.group)!.displayName,
}
: undefined,
favCount: g.favCount ?? 0,
commentCount: g.commentCount ?? 0,
viewer: g.viewerFav ? { fav: g.viewerFav } : undefined,
items: (g.items ?? []).map((item) => ({
uri: item.uri,
cid: item.cid,
...spaceBlobViews(g.space, item.did, item.cid),
alt: item.alt,
aspectRatio: item.aspectRatio,
})),
}) as unknown as GalleryView,
),
)
</script>
<OGMeta title="Groups - grain" noindex />
<FeedTabs />
<PullToRefresh onRefresh={refresh}>
{#if !$viewer}
<div class="empty">
<Lock size={15} />
<p>These pools are their groups', not the network's. Sign in to see the ones you are in.</p>
</div>
{:else if blocked}
<SpacesRequired />
{:else if feed.isLoading}
<GalleryCardSkeleton />
<GalleryCardSkeleton />
{:else if feed.isError}
<div class="empty">
<Lock size={15} />
<p>
Your groups' pools couldn't be loaded. Signing in again usually fixes this.
</p>
</div>
{:else if cards.length === 0}
<div class="empty">
<Lock size={15} />
<p>Nothing in your groups' pools yet. Join a group, or add the first gallery to one.</p>
</div>
{:else}
{#each cards as card, i (card.uri)}
<GalleryCard
gallery={card}
privateGallery
pool={feed.data?.galleries?.[i]?.group}
onCommentClick={() => openThread(i)}
/>
{/each}
{#if thread}
<CommentSheet
open={true}
subjectUri={thread.uri}
pool={thread.group}
onClose={() => (thread = null)}
/>
{/if}
{/if}
</PullToRefresh>
<style>
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 56px 24px;
color: var(--text-muted);
text-align: center;
}
.empty p { margin: 0; font-size: 14px; line-height: 1.5; max-width: 34ch; }
</style>