import { weightedIndex } from '@mary/array-fns';
import { triangularRandom } from '#/lib/utils/numbers';
import * as EmbedSkeleton from '#/components/Post/Embed/EmbedSkeleton';
import { PostControlsSkeleton } from '#/components/PostControls';
import * as PostLayout from '#/components/PostLayout';
import * as Skele from '#/components/web/Skeleton';
import RepostIcon from '#/icons/central/ArrowsRepeatRightLeft_round_outlined_radius1_stroke2.svg';
import * as feedCss from './PostFeedItem.css';
import * as reasonCss from './PostFeedReason.css';
// feed avatar size, matching `PostFeedItem`'s `PreviewableUserAvatar size={36}`.
const AVI_SIZE = 36;
/** A frozen randomized config for one post placeholder row. */
export type PostRow = {
embed: EmbedSkeleton.Shape | null;
lastLineWidth: number;
reasonWidth: number | null;
textLines: number;
};
/**
* draws a randomized config for a single post placeholder row. freeze the result (e.g. hold it for the
* component's lifetime) so the row doesn't reshuffle on every re-render.
*
* @param reason whether a row may carry a repost/pin reason line. pass false in contexts that never show one
* (e.g. the notification feed), which forces `reasonWidth` to null. defaults to true.
* @returns a post row config
*/
export function randomPostRow({ reason = true }: { reason?: boolean } = {}): PostRow {
return {
embed: EmbedSkeleton.randomShape(),
lastLineWidth: triangularRandom(35, 90, 5),
// ~30% of rows carry a repost/pin reason line, sized in px to the "Reposted by …" handle's natural
// width; null leaves the row empty (just its reserved space)
reasonWidth: reason && weightedIndex([7, 3]) === 1 ? triangularRandom(110, 210, 10) : null,
// post bodies cluster around a couple of lines; a triangular draw peaks there and tails to 1 and 5
textLines: triangularRandom(1, 5),
};
}
export function PostLoadingPlaceholder({ row, topBorder }: { row: PostRow; topBorder: boolean }) {
const { embed, lastLineWidth, reasonWidth, textLines } = row;
return (
<PostLayout.Frame topBorder={topBorder}>
{/* the real item always renders this reason row; even empty it reserves the post's top spacing, and
a repost/pin reason adds its own line */}
<div className={feedCss.reasonRow}>
<div className={feedCss.spineSlot} />
<div className={feedCss.reason}>
{reasonWidth != null ? (
<div className={reasonCss.includeReason}>
<RepostIcon className={reasonCss.reasonIconPlaceholder} />
{/* fixed px, not a %: the real reason hugs its content, so a percentage has no stable base */}
<Skele.Text color="contrast_25" size="md_sub" width={reasonWidth} />
</div>
) : null}
</div>
</div>
<PostLayout.Row>
<PostLayout.AvatarColumn>
<Skele.Circle size={AVI_SIZE} />
</PostLayout.AvatarColumn>
<PostLayout.ContentColumn>
<div className={feedCss.metaSpacing}>
<Skele.Text size="md" width="25%" />
</div>
<Skele.Lines count={textLines} lastWidth={lastLineWidth} size="md" />
{embed ? <EmbedSkeleton.Reply shape={embed} /> : null}
<PostControlsSkeleton />
</PostLayout.ContentColumn>
</PostLayout.Row>
</PostLayout.Frame>
);
}
export function PostFeedLoadingPlaceholder({ topBorder = false }: { topBorder?: boolean }) {
// freeze the per-row variety for the component's lifetime so it doesn't reshuffle on every re-render.
const rows = Array.from({ length: 9 }, () => randomPostRow());
return (
<>
{rows.map((row, i) => (
// oxlint-disable-next-line react/no-array-index-key -- static skeleton
<PostLoadingPlaceholder key={i} row={row} topBorder={topBorder || i !== 0} />
))}
</>
);
}