import { useEffect, useState } from 'react';
import type { AnyProfileView } from '@atcute/bluesky';
import type { ModerationOptions } from '@atcute/bluesky-moderation';
import type { Did } from '@atcute/lexicons';
import { clsx } from 'clsx';
import { profileTarget } from '#/lib/routes/targets';
import { useModerationOpts } from '#/state/moderation/moderation-opts';
import type { FeedDescriptor } from '#/state/queries/feed-descriptor';
import { useSuggestedFollowsByActorWithDismiss } from '#/state/queries/suggested-follows';
import { useGetSuggestedUsersForDiscoverQuery } from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery';
import { useSession } from '#/state/session';
import { BlockLink } from '#/components/BlockLink';
import * as Dialog from '#/components/Dialog';
import * as css from '#/components/FeedInterstitials.css';
import { SuggestedFollowsDialog } from '#/components/SuggestedFollowsDialog';
import { Text } from '#/components/Text';
import * as ProfileCard from '#/components/web/ProfileCard';
import * as Skeleton from '#/components/web/Skeleton';
import ArrowRightIcon from '#/icons/central/ArrowRight_round_outlined_radius1_stroke2.svg';
import XIcon from '#/icons/central/CrossLarge_round_outlined_radius1_stroke2.svg';
import { m } from '#/paraglide/messages';
function SuggestedFollowPlaceholder() {
return (
<div className={css.cardBase}>
<div className={css.body}>
<ProfileCard.AvatarPlaceholder size={88} />
<div className={css.identity}>
<Skeleton.Text size="md" width="60%" />
<div className={css.description}>
<Skeleton.Lines color="contrast_50" count={2} lastWidth={60} size="sm" />
</div>
</div>
</div>
<div className={css.followPlaceholder} />
</div>
);
}
function SuggestedFollowCard({
moderationOpts,
onDismiss,
profile,
}: {
moderationOpts: ModerationOptions;
onDismiss?: (did: string) => void;
profile: AnyProfileView;
}) {
return (
<BlockLink
className={clsx(css.cardBase, css.cardLink)}
label={m['common.profile.a11y.viewNamed']({
name: profile.handle,
})}
to={profileTarget(profile.did)}
>
<div>
{onDismiss && (
<button
aria-label={m['components.feedInterstitials.action.dismiss']()}
className={css.dismiss}
onClick={() => onDismiss(profile.did)}
type="button"
>
<XIcon className={css.xIcon} />
</button>
)}
<div className={css.body}>
<ProfileCard.Avatar disabledPreview moderationOpts={moderationOpts} profile={profile} size={88} />
<div className={css.identity}>
<ProfileCard.Handle profile={profile} />
<div className={css.description}>
<ProfileCard.Description
align="center"
color="textContrastMedium"
numberOfLines={2}
profile={profile}
size="sm"
/>
</div>
</div>
</div>
<ProfileCard.FollowButton profile={profile} variant="text-only" />
</div>
</BlockLink>
);
}
export function SuggestedFollows({ feed }: { feed: FeedDescriptor }) {
const { currentAccount } = useSession();
if (feed.type === 'author') {
if (currentAccount?.did === feed.did) {
return null;
}
return <SuggestedFollowsProfile did={feed.did} />;
}
return <SuggestedFollowsHome />;
}
export function SuggestedFollowsProfile({ did }: { did: Did }) {
const { profiles, onDismiss, isLoading, error } = useSuggestedFollowsByActorWithDismiss({ did });
return (
<ProfileGrid
error={error}
isSuggestionsLoading={isLoading}
onDismiss={onDismiss}
profiles={profiles}
viewContext="profile"
/>
);
}
export function SuggestedFollowsHome() {
const { isLoading, data, error } = useGetSuggestedUsersForDiscoverQuery();
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set());
const onDismiss = (did: string) => {
setDismissedDids((prev) => new Set(prev).add(did));
};
const allProfiles = data?.actors ?? [];
const filteredProfiles = allProfiles.filter((p) => !dismissedDids.has(p.did));
return (
<ProfileGrid
error={error}
isSuggestionsLoading={isLoading}
onDismiss={onDismiss}
profiles={filteredProfiles}
totalProfileCount={allProfiles.length}
viewContext="feed"
/>
);
}
export function ProfileGrid({
error,
isSuggestionsLoading,
onDismiss,
onRequestHide,
profiles,
totalProfileCount,
viewContext,
}: {
error: Error | null;
isSuggestionsLoading: boolean;
onDismiss?: (did: string) => void;
onRequestHide?: () => void;
profiles: AnyProfileView[];
totalProfileCount?: number;
viewContext: 'feed' | 'profile' | 'profileHeader';
}) {
const moderationOpts = useModerationOpts();
const followDialogHandle = Dialog.useDialogHandle();
const isLoading = isSuggestionsLoading || !moderationOpts;
const isProfileHeaderContext = viewContext === 'profileHeader';
// the wide grid caps at a single row of 3; the phone scroller and the taller profile-header carousel show more.
const maxLength = isProfileHeaderContext ? 12 : 6;
const minLength = 3;
const content = isLoading
? Array(maxLength)
.fill(0)
// oxlint-disable-next-line react/no-array-index-key -- static skeleton
.map((_, i) => <SuggestedFollowPlaceholder key={i} />)
: error || !profiles.length
? null
: profiles
.slice(0, maxLength)
.map((profile) =>
moderationOpts ? (
<SuggestedFollowCard
key={profile.did}
moderationOpts={moderationOpts}
onDismiss={onDismiss}
profile={profile}
/>
) : null,
);
// use totalProfileCount (before dismissals) for the minLength check on initial render.
const profileCountForMinCheck = totalProfileCount ?? profiles.length;
useEffect(() => {
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
onRequestHide?.();
}
}, [error, isLoading, onRequestHide, profileCountForMinCheck, minLength]);
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
return null;
}
return (
<div className={css.section({ topBorder: !isProfileHeaderContext })}>
<div className={css.header}>
<Text size="md_sub" weight="semiBold">
{m['components.feedInterstitials.title']()}
</Text>
<Dialog.Trigger
aria-label={m['components.feedInterstitials.seeMoreA11y']()}
className={css.seeMoreTrigger}
handle={followDialogHandle}
>
<Text className={css.seeMoreText} color="primary_600" weight="semiBold" size="md_sub">
{m['components.feedInterstitials.action.seeMore']()}
</Text>
</Dialog.Trigger>
</div>
<SuggestedFollowsDialog handle={followDialogHandle} />
<div className={css.grid}>
{content}
<Dialog.Trigger
aria-label={m['components.feedInterstitials.action.browseMore']()}
className={css.seeMoreCard}
handle={followDialogHandle}
>
<ArrowRightIcon className={css.arrowRightIcon} />
<Text align="center" size="md" weight="medium">
{m['components.feedInterstitials.action.seeMore']()}
</Text>
</Dialog.Trigger>
</div>
</div>
);
}