import { type ReactNode, useState } from 'react';
import {
type DisplayRestrictions,
type LabelModerationCause,
ModerationCauseType,
} from '@atcute/bluesky-moderation';
import { unique } from '@mary/array-fns';
import { Collapsible } from '@base-ui/react/collapsible';
import { clsx } from 'clsx';
import { sanitizeDisplayName } from '#/lib/display-names';
import { isJustAMute } from '#/lib/moderation/causes';
import { ADULT_CONTENT_LABELS } from '#/lib/moderation/self-labels';
import { useLabelDefinitions } from '#/state/moderation/label-defs';
import { getDefinition, getLabelStrings } from '#/state/moderation/use-label-info';
import { useModerationCauseDescription } from '#/state/moderation/use-moderation-cause-description';
import { LOCALE } from '#/locale/intl/locale';
import * as Dialog from '#/components/Dialog';
import { ModerationDetailsDialog } from '#/components/moderation/ModerationDetailsDialog';
import { Text } from '#/components/Text';
import { m } from '#/paraglide/messages';
import * as styles from './ContentHider.css';
const isAdultLabel = (cause: LabelModerationCause) =>
ADULT_CONTENT_LABELS.some((label) => label === cause.label.val);
type ContentHiderProps = {
modui: DisplayRestrictions | undefined;
ignoreMute?: boolean;
/** Always applied to the outer wrapper — carries the card chrome (border, radius). */
className?: string;
/** Added to the outer wrapper only when content is blurred. */
activeClassName?: string;
/** Wraps the revealed children once the blur is overridden. */
childContainerClassName?: string;
children?: ReactNode | ((props: { active: boolean }) => ReactNode);
};
/**
* renders children directly when nothing blurs them, otherwise a blur card with a Base UI {@link Collapsible}
* disclosure that reveals the content on demand.
*/
export function ContentHider({
modui,
ignoreMute,
className,
activeClassName,
childContainerClassName,
children,
}: ContentHiderProps) {
const blur = modui?.blurs[0];
if (!modui || !blur || (ignoreMute && isJustAMute(modui))) {
return (
<div className={clsx(styles.outer, className)}>
{typeof children === 'function' ? children({ active: false }) : children}
</div>
);
}
return (
<ContentHiderActive
modui={modui}
className={className}
activeClassName={activeClassName}
childContainerClassName={childContainerClassName}
>
{typeof children === 'function' ? children({ active: true }) : children}
</ContentHiderActive>
);
}
function ContentHiderActive({
modui,
className,
activeClassName,
childContainerClassName,
children,
}: {
modui: DisplayRestrictions;
className?: string;
activeClassName?: string;
childContainerClassName?: string;
children?: ReactNode;
}) {
const [override, setOverride] = useState(false);
const handle = Dialog.useDialogHandle();
const { labelDefs } = useLabelDefinitions();
const blur = modui.blurs[0]!;
const desc = useModerationCauseDescription(blur);
let labelName: string | undefined;
if (!modui?.blurs || !blur) {
labelName = undefined;
} else if (blur.type !== ModerationCauseType.Label || blur.source !== null) {
if (desc.isSubjectAccount) {
labelName = m['components.moderation.label.accountSuffix']({ name: desc.name });
} else {
labelName = desc.name;
}
} else {
const selfBlurCauses = modui.blurs.filter((cause): cause is LabelModerationCause => {
if (cause.type !== ModerationCauseType.Label) {
return false;
}
if (cause.source !== null) {
return false;
}
return true;
});
// keep only the first adult-content self-label; later ones are dropped so the UI doesn't
// stack duplicate "Adult Content" entries.
const firstAdultIdx = selfBlurCauses.findIndex(isAdultLabel);
const selfBlurNames = selfBlurCauses
.filter((cause, i) => {
if (firstAdultIdx !== -1 && isAdultLabel(cause) && i !== firstAdultIdx) {
return false;
}
return true;
})
.slice(0, 2)
.map((cause) => {
if (cause.type !== ModerationCauseType.Label) {
return;
}
const def = cause.labelDef || getDefinition(labelDefs, cause.label);
if (def.identifier === 'porn' || def.identifier === 'sexual') {
return m['common.moderation.adultContent']();
}
return getLabelStrings(LOCALE, def).name;
});
if (selfBlurNames.length === 0) {
labelName = desc.name;
} else {
labelName = unique(selfBlurNames).join(', ');
}
}
const triggerInner = (
<>
<desc.icon className={styles.iconWrap} />
<Text
size="md"
weight="medium"
color="textContrastMedium"
numberOfLines={2}
className={styles.labelText}
>
{labelName}
</Text>
{!modui.noOverride && (
<Text size="md_sub" weight="medium" color="textContrastHigh" className={styles.toggleText}>
{override ? m['common.action.hide']() : m['common.action.show']()}
</Text>
)}
</>
);
return (
<Collapsible.Root
open={override}
onOpenChange={(open) => setOverride(open)}
className={clsx(styles.outer, className, activeClassName)}
>
<ModerationDetailsDialog handle={handle} modcause={blur} />
{modui.noOverride ? (
// noOverride content can never be revealed; its trigger opens the details dialog instead of toggling.
<Dialog.Trigger handle={handle} className={styles.blurButton} aria-label={desc.name}>
{triggerInner}
</Dialog.Trigger>
) : (
<Collapsible.Trigger className={styles.blurButton} aria-label={desc.name}>
{triggerInner}
</Collapsible.Trigger>
)}
{desc.source && blur.type === ModerationCauseType.Label && !override && (
<Dialog.Trigger
handle={handle}
className={styles.learnMoreButton}
aria-label={m['components.moderation.label.learnMore.aboutContent']()}
>
<Text size="md_sub" color="textContrastMedium">
{desc.sourceType === 'user'
? m['components.moderation.label.labeledByAuthor']()
: m['components.moderation.label.labeledBy']({ source: sanitizeDisplayName(desc.source) })}{' '}
<Text size="md_sub" color="textLink" className={styles.learnMoreLink}>
{m['components.moderation.label.learnMore.dot']()}
</Text>
</Text>
</Dialog.Trigger>
)}
<Collapsible.Panel className={clsx(styles.panel, childContainerClassName)}>
{children}
</Collapsible.Panel>
</Collapsible.Root>
);
}