<script lang="ts">
import { resolve } from "$app/paths";
import { navigating, page } from "$app/state";
import GalleryVertical from "$icon/gallery-vertical";
import Globe from "$icon/globe";
import UserRoundGroup from "$icon/user-round-group";
import Button from "#lib/components/ui/Button.svelte";
import ErrorAlert from "#lib/components/ui/Error.svelte";
import RefreshCw from "$icon/refresh-cw";
import Segment from "#lib/components/ui/Segment.svelte";
import SegmentedControl from "#lib/components/ui/SegmentedControl.svelte";
import Announcements from "#lib/components/timeline/Announcements.svelte";
import NewsletterCard from "#lib/components/timeline/NewsletterCard.svelte";
import NotificationsPreview from "#lib/components/timeline/NotificationsPreview.svelte";
import Pending from "#lib/components/ui/Pending.svelte";
import RecentsPreview from "#lib/components/timeline/RecentsPreview.svelte";
import Timeline from "#lib/components/timeline/Timeline.svelte";
import TrendingRepos from "#lib/components/timeline/TrendingRepos.svelte";
import Bones from "#lib/components/ui/Bones.svelte";
import timelineTimelineSkeletonFixture from "#lib/bones/timeline-timeline--skeleton-fixture.bones.json";
import type { RepoCardData } from "#lib/components/profile/types.ts";
import OnboardingNudge from "#lib/components/welcome/OnboardingNudge.svelte";
import type { NotificationSummary } from "#lib/components/notifications/types.ts";
import type { BlogPost } from "#lib/components/timeline/types.ts";
import type * as OrgTangledFeedGetTimeline from "#lib/api/lexicons/types/org/tangled/feed/getTimeline.ts";
import { loadRecents } from "#lib/components/timeline/recents.svelte.ts";
let {
feed,
trending = [],
announcements = [],
notifications,
followingOnly = false,
signedIn = false,
onboardingNudge = false,
basePath,
onRefresh
}: {
feed?:
| OrgTangledFeedGetTimeline.$output["feed"]
| Promise<OrgTangledFeedGetTimeline.$output["feed"]>;
trending?: RepoCardData[] | Promise<RepoCardData[]>;
announcements?: BlogPost[] | Promise<BlogPost[]>;
notifications?: NotificationSummary[] | Promise<NotificationSummary[]>;
followingOnly?: boolean;
signedIn?: boolean;
onboardingNudge?: boolean;
/** route the global/following toggle stays on, e.g. "/" or "/timeline" */
basePath: string;
onRefresh?: () => void;
} = $props();
// loads after hydration, the server can't see this browser's storage
const recents = loadRecents();
const pendingFollowing = $derived(
navigating.to && navigating.to.url.pathname === page.url.pathname
? navigating.to.url.searchParams.get("following") === "true"
: undefined
);
const switching = $derived(
pendingFollowing !== undefined && pendingFollowing !== followingOnly
);
const toggleHref = (following: boolean) =>
resolve((following ? `${basePath}?following=true` : basePath) as "/");
const refreshIfActive = (following: boolean) => (event: MouseEvent) => {
if (following !== followingOnly || !onRefresh) return;
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
return;
event.preventDefault();
onRefresh();
};
</script>
{#snippet findPeople()}
<Button href={toggleHref(false)} icon={Globe}>Find people on the Global timeline</Button>
{/snippet}
<div
class="mx-auto flex w-full max-w-[1920px] flex-col md:grid md:grid-cols-[minmax(0,25rem)_minmax(0,37.5rem)_minmax(0,25rem)] md:items-start md:justify-center"
>
<div
class="flex flex-col md:sticky md:top-0 md:col-start-1 md:row-start-2 md:h-screen md:w-full md:max-w-100 md:gap-md md:overflow-y-auto md:p-md"
>
{#if signedIn && onboardingNudge}
<OnboardingNudge class="max-md:px-xs max-md:pt-md" />
{/if}
<div class="contents max-md:hidden">
<NotificationsPreview {signedIn} {notifications} />
<RecentsPreview items={recents.items} />
</div>
</div>
<div
class="order-2 flex flex-col gap-md px-xs pt-2xl md:order-0 md:col-start-2 md:row-start-1 md:mx-auto md:w-full md:max-w-150 md:px-none"
>
<div class="flex items-center justify-between">
<h1 class="flex items-center gap-xs pl-xs typography-heading-4">
<GalleryVertical class="size-lg" aria-hidden="true" />
<span class="max-[24rem]:sr-only">Timeline</span>
</h1>
{#if signedIn}
<SegmentedControl
value={followingOnly ? "following" : "global"}
href={(value) => toggleHref(value === "following")}
label="Timeline"
>
<Segment value="global" onclick={refreshIfActive(false)} icon={Globe}>
Global
</Segment>
<Segment
value="following"
onclick={refreshIfActive(true)}
icon={UserRoundGroup}
>
Following
</Segment>
</SegmentedControl>
{/if}
</div>
</div>
<div
class="order-3 flex flex-col px-xs pt-md pb-2xl md:order-0 md:col-start-2 md:row-start-2 md:mx-auto md:w-full md:max-w-150 md:px-none"
>
{#if feed === undefined || switching}
<Bones capture={timelineTimelineSkeletonFixture} />
{:else if feed instanceof Promise}
<Pending key={feed}>
{#snippet skeleton()}
<Bones capture={timelineTimelineSkeletonFixture} />
{/snippet}
{#snippet failed()}
<div class="flex flex-col items-start gap-sm" data-testid="timelineError">
<ErrorAlert
class="self-stretch"
label={followingOnly
? "Could not load the timeline of people you follow."
: "Could not load the timeline."}
/>
{#if onRefresh}
<Button icon={RefreshCw} onclick={onRefresh}>Try again</Button>
{/if}
</div>
{/snippet}
{@const resolvedFeed = await feed}
<Timeline
feed={resolvedFeed}
{signedIn}
emptyMessage={followingOnly
? "Activity from people you follow shows up here."
: "Nothing here yet."}
emptyAction={followingOnly ? findPeople : undefined}
/>
</Pending>
{:else}
<Timeline
{feed}
{signedIn}
emptyMessage={followingOnly
? "Activity from people you follow shows up here."
: "Nothing here yet."}
emptyAction={followingOnly ? findPeople : undefined}
/>
{/if}
</div>
<!-- right rail: leads on mobile, where only trending shows -->
<div
class="order-1 flex flex-col gap-md px-xs pt-md md:sticky md:top-0 md:order-0 md:col-start-3 md:row-start-2 md:ml-auto md:h-screen md:w-full md:max-w-100 md:overflow-y-auto md:p-md"
>
<TrendingRepos repos={trending} />
{#if signedIn}
<NewsletterCard />
{/if}
<div class="hidden md:block">
<Announcements posts={announcements} />
</div>
</div>
</div>