refactor!: retire ListFooter and ListMaybePlaceholder
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { createVar, fallbackVar, style } from '@vanilla-extract/css';
import { colors } from '#/styles/colors';import { recipe } from '#/styles/recipe';import { borderRadius, space } from '#/styles/tokens.css';
const belowTablet = '(width < 1300px)';
export const heightVar = createVar();
export const footer = recipe( { base: { boxSizing: 'border-box', display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 30, paddingBottom: space.lg, width: '100%', height: fallbackVar(heightVar, '180px'), }, defaultVariants: { border: true }, variants: { border: { false: {}, true: { borderTopWidth: 1, borderTopStyle: 'solid', borderTopColor: colors.borderContrastLow, }, }, }, }, { debugId: 'footer' },);
export const errorOuter = style({ boxSizing: 'border-box', paddingRight: space.lg, paddingLeft: space.lg, width: '100%',});
export const errorRow = style({ display: 'flex', flexDirection: 'row', gap: space.md, alignItems: 'center', borderRadius: borderRadius.sm, backgroundColor: colors.contrast_25, padding: space.md,});
export const errorText = style({ flex: 1, minWidth: 0,});
export const placeholderLoading = recipe( { base: { boxSizing: 'border-box', display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100vh', paddingTop: 175, paddingBottom: 110, }, defaultVariants: { topBorder: false }, variants: { topBorder: { false: {}, true: { '@media': { [belowTablet]: { borderTopWidth: 1, borderTopStyle: 'solid', borderTopColor: colors.borderContrastLow, }, }, }, }, }, }, { debugId: 'placeholderLoading' },);123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136import type { ReactElement, ReactNode } from 'react';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { cleanError } from '#/lib/errors';
import { EmptyState, type EmptyStateButtonProps, type EmptyStateIcon } from '#/components/EmptyState';import { Error } from '#/components/Error';import * as css from '#/components/Lists.css';import { Spinner } from '#/components/Spinner';import { Text } from '#/components/Text';import { Button, ButtonText } from '#/components/web/Button';
import { m } from '#/paraglide/messages';
export function ListFooter({ border = true, endMessageText, error, hasNextPage, height, isFetchingNextPage, onRetry, showEndMessage = false,}: { /** whether to draw the divider above the footer. */ border?: boolean; endMessageText?: string; error?: string; hasNextPage?: boolean; height?: number; isFetchingNextPage?: boolean; onRetry?: () => Promise<unknown>; showEndMessage?: boolean;}) { return ( <div className={css.footer({ border })} style={height != null ? assignInlineVars({ [css.heightVar]: `${height}px` }) : undefined} > {isFetchingNextPage ? ( <Spinner color="default" label={m['common.status.loading']()} size="_2xl" /> ) : error ? ( <ListFooterError error={error} onRetry={onRetry} /> ) : !hasNextPage && showEndMessage ? ( <Text color="textContrastLow" size="sm"> {endMessageText ?? m['common.list.endOfList']()} </Text> ) : null} </div> );}
function ListFooterError({ error, onRetry }: { error: string; onRetry?: () => Promise<unknown> }) { return ( <div className={css.errorOuter}> <div className={css.errorRow}> <Text className={css.errorText} color="textContrastMedium" numberOfLines={2} size="sm"> {cleanError(error)} </Text> <Button label={m['common.a11y.pressToRetry']()} onClick={() => void onRetry?.()} variant="solid"> <ButtonText>{m['common.action.retry']()}</ButtonText> </Button> </div> </div> );}
function ListMaybePlaceholder({ isLoading, isError, emptyTitle, emptyMessage, errorMessage, emptyType = 'page', onRetry, topBorder = false, emptyStateIcon, emptyStateButton, useEmptyState = false,}: { isLoading: boolean; isError?: boolean; emptyTitle?: string; emptyMessage?: string; errorMessage?: string; emptyType?: 'page' | 'results'; onRetry?: () => Promise<unknown>; topBorder?: boolean; emptyStateIcon?: EmptyStateIcon | ReactElement; emptyStateButton?: EmptyStateButtonProps; useEmptyState?: boolean;}): ReactNode { if (isLoading) { return ( <div className={css.placeholderLoading({ topBorder })}> <Spinner color="default" label={m['common.status.loading']()} size="_2xl" /> </div> ); }
if (isError) { return ( <Error title={m['common.error.oops']()} message={errorMessage ?? m['common.error.generic']()} onRetry={onRetry} /> ); }
if (useEmptyState) { return ( <EmptyState icon={emptyStateIcon} message={ emptyMessage ?? (emptyType === 'results' ? m['common.list.noResults']() : m['common.error.pageNotFound']()) } button={emptyStateButton} /> ); }
return ( <Error title={ emptyTitle ?? (emptyType === 'results' ? m['common.list.noResults']() : m['common.error.pageNotFound']()) } message={emptyMessage ?? m['common.error.notFoundDescription']()} onRetry={onRetry} /> );}export { ListMaybePlaceholder };24 unmodified lines25262728292930313233343536373868 unmodified lines107108109110110111112113113114115116117295 unmodified linesExpand all41341441541641741841942042142242342442542642741741841942042143343443543643742242342442573 unmodified lines49950050150251952052152252352452552650350450550650750850951051151251352951451551651751851953552052152252310 unmodified lines24 unmodified linesimport { useSession } from '#/state/session';
import { SuggestedFollows } from '#/components/FeedInterstitials';import { List, type ListRef, type ListRenderItemInfo } from '#/components/List/List';import { ListFooter } from '#/components/Lists';import * as ListTail from '#/components/List/ListTail';import { PostFeedLoadingPlaceholder } from '#/components/PostFeed/PostFeedLoadingPlaceholder';import { RichText } from '#/components/RichText';import { TrendingInterstitial, useShowTrendingInterstitial } from '#/components/TrendingInterstitial';
import { m } from '#/paraglide/messages';import { useFocusEffect, useIsFocused } from '#/router';
import { FeedShutdownMsg } from './FeedShutdownMsg';import * as css from './PostFeed.css';68 unmodified lines}
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY;
// estimate row height from typical post sizes.const FEED_ITEM_HEIGHT_ESTIMATE = 300;
function PostFeed({export function PostFeed({ feed, description, ignoreFilterFor, pollInterval,295 unmodified linesExpand all
return arr; })();
// events // =
const onEndReached = () => { if (isFetching || !hasNextPage || isError) { return; }
void fetchNextPage(); };
const onPressTryAgain = () => { void refetch(); onHasNew?.(false); };
const onPressRetryLoadMore = () => fetchNextPage();
// rendering // =
const renderItem = ({ item: row, index: rowIndex }: ListRenderItemInfo<FeedRow>) => { switch (row.type) { case 'empty': { return renderEmptyState();73 unmodified lines keyExtractor={(item: FeedRow) => item.key} estimateHeight={FEED_ITEM_HEIGHT_ESTIMATE} renderItem={renderItem} ListFooterComponent={ <ListFooter border={!isEmpty} error={isError && !isEmpty ? cleanError(error) : undefined} hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} onRetry={onPressRetryLoadMore} showEndMessage={!isEmpty && !isFetching} /> <ListTail.Frame border={!isEmpty}> {isFetchingNextPage ? ( <ListTail.Pending /> ) : isError && !isEmpty ? ( <ListTail.Error message={cleanError(error)} onRetry={() => void fetchNextPage()} /> ) : !isEmpty && !isFetching && !hasNextPage ? ( <ListTail.End>{m['common.list.endOfList']()}</ListTail.End> ) : null} </ListTail.Frame> } onScrolledDownChange={handleScrolledDownChange} onEndReached={onEndReached} onEndReached={() => void fetchNextPage()} onEndReachedThreshold={2} onItemSeen={onItemSeen} /> );}export { PostFeed };
export function isThreadParentAt(arr: Array<unknown>, i: number) { if (arr.length === 1) { return false;10 unmodified lines25 unmodified lines262728293030313233343534 unmodified lines7071727373747576777874757677787980818283848525 unmodified lines111112113114115116117118119113114115116117120121122123124125126300 unmodified linesExpand all25 unmodified linesimport { useTitle } from '#/state/use-title';
import * as Dialog from '#/components/Dialog';import { signinDialogHandle } from '#/components/dialogs/handles';import { ListMaybePlaceholder } from '#/components/Lists';import { ListError } from '#/components/List/ListError';import { ListLoading } from '#/components/List/ListLoading';import { Spinner } from '#/components/Spinner';import { FeedsList } from '#/components/StarterPack/Main/FeedsList';import { PostsList } from '#/components/StarterPack/Main/PostsList';import { ProfilesList } from '#/components/StarterPack/Main/ProfilesList';34 unmodified lines
if (isLoading || isError || !resolvedStarterPack) { return ( <Layout.Screen> <ListMaybePlaceholder isLoading={isLoading} isError={isError} errorMessage={m['screens.starterPack.error.notFound']()} emptyMessage={m['screens.starterPack.error.notFound']()} /> {isLoading ? ( <ListLoading /> ) : ( <ListError message={m['screens.starterPack.error.notFound']()} title={m['common.error.pageNotFound']()} /> )} </Layout.Screen> ); } return (25 unmodified lines starterPack !== undefined && (starterPack.list !== undefined || starterPack.creator.did === currentAccount?.did);
if (!did || !starterPack || !isValid || !moderationOpts) { if (!isErrorDid && !isErrorStarterPack && (!did || !starterPack || !moderationOpts)) { return <ListLoading />; }
return ( <ListMaybePlaceholder isLoading={!isErrorDid && !isErrorStarterPack && (!did || !starterPack || !moderationOpts)} isError={isErrorDid || isErrorStarterPack || !isValid} errorMessage={m['screens.starterPack.error.notFound']()} emptyMessage={m['screens.starterPack.error.notFound']()} <ListError message={m['screens.starterPack.error.notFound']()} title={m['common.error.pageNotFound']()} /> ); }
300 unmodified linesExpand all29 unmodified lines303132333434353637383924 unmodified lines6465666767686970716869707172737475767778747580818283847677787980818283929394959684858687888990919293949596102103104105979899100101102103435 unmodified linesExpand all29 unmodified linesimport { StepProfiles } from '#/screens/StarterPack/Wizard/StepProfiles';
import * as Dialog from '#/components/Dialog';import { markStarterPackCreated } from '#/components/dialogs/starter-pack-dialog-reopen';import { ListMaybePlaceholder } from '#/components/Lists';import { ListError } from '#/components/List/ListError';import { ListLoading } from '#/components/List/ListLoading';import { Spinner } from '#/components/Spinner';import { WizardEditListDialog } from '#/components/StarterPack/Wizard/WizardEditListDialog';import { Text } from '#/components/Text';import * as Toast from '#/components/Toast';24 unmodified lines const moderationOpts = useModerationOpts(); // Use targetDid if provided (from dialog), otherwise use current account const profileDid = targetDid || currentAccount!.did;
const { data: starterPack, isLoading: isLoadingStarterPack, isError: isErrorStarterPack, } = useStarterPackQuery({ did: currentAccount!.did, rkey }); const { data: starterPack, isLoading: isLoadingStarterPack } = useStarterPackQuery({ did: currentAccount!.did, rkey, }); const listUri = starterPack?.list?.uri;
const { data: listItems, isLoading: isLoadingProfiles, isError: isErrorProfiles, } = useAllListMembersQuery(listUri); const { data: listItems, isLoading: isLoadingProfiles } = useAllListMembersQuery(listUri);
const { data: profile, isLoading: isLoadingProfile, isError: isErrorProfile, } = useProfileQuery({ did: profileDid }); const { data: profile, isLoading: isLoadingProfile } = useProfileQuery({ did: profileDid });
const isEdit = !!rkey; const isReady = (!isEdit || (isEdit && starterPack && listItems)) && profile && moderationOpts;
if (!isReady) { return ( <Layout.Screen> <ListMaybePlaceholder isLoading={isLoadingStarterPack || isLoadingProfiles || isLoadingProfile} isError={isErrorStarterPack || isErrorProfiles || isErrorProfile} errorMessage={m['screens.starterPack.error.notFound']()} /> {isLoadingStarterPack || isLoadingProfiles || isLoadingProfile ? ( <ListLoading /> ) : ( <ListError message={m['screens.starterPack.error.notFound']()} title={m['common.error.pageNotFound']()} /> )} </Layout.Screen> ); } else if (isEdit && starterPack?.creator.did !== currentAccount?.did) { return ( <Layout.Screen> <ListMaybePlaceholder isLoading={false} isError={true} errorMessage={m['screens.starterPack.error.notFound']()} <ListError message={m['screens.starterPack.error.notFound']()} title={m['common.error.pageNotFound']()} /> </Layout.Screen> ); }435 unmodified linesExpand all