feat: give the starter pack page a scroll-away header
12345610 unmodified lines17181920212223242526272829303132333435363738394041424344454630 unmodified lines77787980575859606162636465666768697071727374818283848586878889909192939495969722 unmodified linesimport type { ReactNode } from 'react';
import type { AppBskyActorDefs, AppBskyGraphStarterpack } from '@atcute/bluesky';
import { profileTarget } from '#/lib/routes/targets';
10 unmodified lines
import * as css from './StarterPackHeader.css';
/** * @param creator custom creator node * @param handle fallback creator handle * @param isOwn whether to use the owner byline * @returns the localized byline */export function StarterPackByline({ creator, handle, isOwn,}: { creator?: ReactNode; handle: string; isOwn: boolean;}) { if (isOwn) { return m['common.starterPack.byYou'](); }
return <Trans message={m['view.profile.starterPack.by']} markup={{ t0: () => creator ?? handle }} />;}
/** * profile-subpage header card for a starter pack: the starter-pack glyph, the pack's title, and a "by …" * byline, followed by the description, a sign-in prompt for logged-out viewers, and the join count. * * @param onPressSignIn invoked when a logged-out viewer taps the sign-in prompt30 unmodified lines <Text size="xl" weight="semiBold" numberOfLines={2}> {record.name || ''} </Text> <Text color="textContrastMedium" numberOfLines={1}> {isOwn ? ( m['common.starterPack.byYou']() ) : ( <Trans message={m['view.profile.starterPack.by']} markup={{ t0: () => ( <InlineLinkText to={profileTarget(creator.did)} label={m['screens.profile.avatar.a11y.viewProfile']({ handle: creator.handle })} color="textContrastMedium" > {creator.handle} </InlineLinkText> ), }} /> )} <StarterPackByline creator={ <InlineLinkText to={profileTarget(creator.did)} label={m['screens.profile.avatar.a11y.viewProfile']({ handle: creator.handle })} color="textContrastMedium" > {creator.handle} </InlineLinkText> } handle={creator.handle} isOwn={isOwn} /> </Text> </div> </div> {descriptionRT ? <RichText value={descriptionRT} /> : null}22 unmodified lines1123452 unmodified lines891011121314151625 unmodified lines4243444545464748495051525353545556575820 unmodified lines7980818282838485868777 unmodified lines16516616716816917017117217314 unmodified lines1881891901911901921931941951961971981992002012022033 unmodified lines20720820921021121221321421521621721821922066 unmodified lines28728828929028129129228329329429529629729829930030130226 unmodified lines32933033133233333433531932032132232332432532632732833633733833934034134234334434534634734834935071 unmodified linesimport { useEffect, useState } from 'react';import { type Ref, useEffect, useState } from 'react';
import type { AppBskyGraphDefs } from '@atcute/bluesky';import type { ModerationOptions } from '@atcute/bluesky-moderation';
2 unmodified linesimport { useQueryClient } from '@tanstack/react-query';
import { getStarterPackRecord } from '#/lib/api/record-casts';import { bulkWriteFollows } from '#/lib/bulk-write-follows';import { useElementHeight } from '#/lib/hooks/use-element-height';import { prefetchImage } from '#/lib/media/prefetch';import { isBlockedOrBlocking, isMuted } from '#/lib/moderation/blocked-and-muted';import { targetToShareUrl } from '#/lib/routes/app-links';import { starterPackTarget } from '#/lib/routes/targets';25 unmodified linesimport { m } from '#/paraglide/messages';import { type RouteParams, useParams, useRouter } from '#/router';
import { OverflowMenu } from './OverflowMenu';import { StarterPackHeader } from './StarterPackHeader';import { StarterPackByline, StarterPackHeader } from './StarterPackHeader';import * as css from './StarterPackScreen.css';
type StarterPackTabId = 'feeds' | 'people' | 'posts';
export function StarterPackScreen() { const [params, replaceParams] = useParams('StarterPack'); return ( <Layout.Screen> <Layout.Screen className={Layout.ScrollAway.scope}> <StarterPackScreenInner onTabChange={(next) => replaceParams({ tab: next })} routeParams={params} /> </Layout.Screen> );}20 unmodified lines </Layout.Screen> ); } return ( <Layout.Screen> <Layout.Screen className={Layout.ScrollAway.scope}> <StarterPackScreenInner onTabChange={(next) => replaceParams({ tab: next })} routeParams={{ ...resolvedStarterPack, tab }} />77 unmodified lines }, ]);
const shareDialogHandle = Dialog.useDialogHandle(); const [headerRef, headerHeight] = useElementHeight<HTMLDivElement>();
const link = targetToShareUrl(starterPackTarget(starterPack.creator.did, routeParams.rkey));
const onOpenShareDialog = () => {14 unmodified lines sections={sections} value={routeParams.tab ?? 'people'} onValueChange={onTabChange} header={ <Header starterPack={starterPack} routeParams={routeParams} onOpenShareDialog={onOpenShareDialog} /> <Header starterPack={starterPack} routeParams={routeParams} onOpenShareDialog={onOpenShareDialog} ref={headerRef} /> } headerOffset={headerHeight} />
<ShareDialog handle={shareDialogHandle} link={link} starterPack={starterPack} /> </>3 unmodified linesfunction Header({ starterPack, routeParams, onOpenShareDialog, ref,}: { starterPack: AppBskyGraphDefs.StarterPackView; routeParams: RouteParams<'StarterPack'>; onOpenShareDialog: () => void; ref: Ref<HTMLDivElement>;}) { const { currentAccount, hasSession } = useSession(); const { appview, pds } = getClients(); const queryClient = useQueryClient();66 unmodified lines const canGoBack = router.canGoBack;
return ( <> <Layout.Header.Outer noBottomBorder sticky={false}> <Layout.Header.Outer noBottomBorder ref={ref}> {canGoBack ? <Layout.Header.BackButton /> : <Layout.Header.MenuButton />} <Layout.Header.Content /> <Layout.Header.Content className={Layout.ScrollAway.reveal}> <Layout.Header.TitleText numberOfLines={1}>{record.name}</Layout.Header.TitleText> <Layout.Header.SubtitleText numberOfLines={1}> <StarterPackByline handle={creator.handle} isOwn={isOwn} /> </Layout.Header.SubtitleText> </Layout.Header.Content> <Layout.Header.EndSlot> {hasSession ? ( <> {isOwn ? (26 unmodified lines /> </> ) : null} </Layout.Header.EndSlot> <Layout.ScrollAway.Backdrop /> </Layout.Header.Outer> <Layout.ScrollAway.Region> <StarterPackHeader record={record} creator={creator} isOwn={isOwn} hasSession={hasSession} joinedAllTimeCount={joinedAllTimeCount} onPressSignIn={() => { signinDialogHandle.openWithPayload({}); }} /> <StarterPackHeader record={record} creator={creator} isOwn={isOwn} hasSession={hasSession} joinedAllTimeCount={joinedAllTimeCount} onPressSignIn={() => { signinDialogHandle.openWithPayload({}); }} /> </Layout.ScrollAway.Region> </> );}
71 unmodified lines53 unmodified lines545556575858596060616263646566666768697071727368747576777893 unmodified lines53 unmodified linesexport function EndSlot({ children }: { children?: ReactNode }) { return <div className={styles.endSlot}>{children}</div>;}
export function TitleText({ children }: { children: ReactNode }) {export function TitleText({ children, numberOfLines = 2 }: { children: ReactNode; numberOfLines?: number }) { return ( <Text size="lg" weight="semiBold" numberOfLines={2}> <Text size="lg" weight="semiBold" numberOfLines={numberOfLines}> {children} </Text> );}
export function SubtitleText({ children }: { children: ReactNode }) {export function SubtitleText({ children, numberOfLines = 2,}: { children: ReactNode; numberOfLines?: number;}) { return ( <Text size="sm" color="textContrastMedium" numberOfLines={2}> <Text size="sm" color="textContrastMedium" numberOfLines={numberOfLines}> {children} </Text> );}93 unmodified lines9 unmodified lines10111213141415161788 unmodified lines10610710810911111211311411011111211611311411511611759 unmodified lines9 unmodified lines} from '#/state/queries/preferences';import { useSession } from '#/state/session';
import { Spinner } from '#/components/Spinner';import { Text } from '#/components/Text';import * as Toast from '#/components/Toast';import { Button, ButtonIcon, ButtonText } from '#/components/web/Button';import * as Layout from '#/components/web/Layout';
88 unmodified lines <> <Layout.Header.Outer noBottomBorder ref={ref}> {canGoBack ? <Layout.Header.BackButton /> : <Layout.Header.MenuButton />} <Layout.Header.Content className={Layout.ScrollAway.reveal}> <Text numberOfLines={1} size="lg" weight="semiBold"> {list.name || ''} </Text> <Text color="textContrastMedium" numberOfLines={1} size="sm"> <Layout.Header.TitleText numberOfLines={1}>{list.name}</Layout.Header.TitleText> <Layout.Header.SubtitleText numberOfLines={1}> <ListByline isOwner={isOwner} list={list} /> </Text> </Layout.Header.SubtitleText> </Layout.Header.Content> <Layout.Header.EndSlot> {isCurateList ? ( <Button59 unmodified lines