import {
type InterpretedLabelDefinition,
LabelFlags,
type LabelPreference,
} from '@atcute/bluesky-moderation';
import type { Did } from '@atcute/lexicons';
import { resolveGlobalLabelPreference } from '#/lib/moderation/preferences';
import { getLabelStrings } from '#/state/moderation/use-label-info';
import { usePreferencesQuery, usePreferencesSetContentLabelMutation } from '#/state/queries/preferences';
import { LOCALE } from '#/locale/intl/locale';
import * as Toggle from '#/components/forms/Toggle';
import * as Settings from '#/components/SettingsCards';
import { Text } from '#/components/Text';
import CircleInfo from '#/icons/central/CircleInfo_round_outlined_radius1_stroke2.svg';
import { m } from '#/paraglide/messages';
import * as styles from './LabelPreference.css';
// the API uses an absent preference for inheritance
const INHERIT_VALUE = 'inherit';
type LabelOption = {
label: string;
value?: LabelPreference;
};
const toggleValue = (pref: LabelPreference | undefined) => pref ?? INHERIT_VALUE;
/**
* an expandable row for a labeler's label preference.
*
* @param className additional row class
* @param disabled whether the preference can be changed
* @param labelDefinition label definition
* @param labelerDid labeler DID
* @param onOpenChange called when the row opens or closes
* @param open whether the row is open
*/
export function LabelerLabelRow({
className,
disabled,
labelDefinition,
labelerDid,
onOpenChange,
open,
}: {
className?: string;
disabled: boolean;
labelDefinition: InterpretedLabelDefinition;
labelerDid: Did;
onOpenChange: (open: boolean) => void;
open: boolean;
}) {
const { identifier } = labelDefinition;
const { data: preferences } = usePreferencesQuery();
const { isPending, mutate, variables } = usePreferencesSetContentLabelMutation();
const labelStrings = getLabelStrings(LOCALE, labelDefinition);
const adultOnly = !!(labelDefinition.flags & LabelFlags.AdultOnly);
const adultDisabled = adultOnly && !preferences?.moderationPrefs.adultContentEnabled;
// adult-only labels are hidden while adult content is off
const configurable = !disabled && !adultDisabled;
const labelOptions: Record<LabelPreference, string> = {
hide: m['common.action.hide'](),
ignore: m['common.action.show'](),
warn: m['common.moderation.warn'](),
};
// labels without blur or severity cannot warn
const canWarn = !(labelDefinition.blur === 'none' && labelDefinition.severity === 'none');
// map stored warn to ignore when warn is unavailable
const adjust = (pref: LabelPreference): LabelPreference => (!canWarn && pref === 'warn' ? 'ignore' : pref);
const saved = preferences?.moderationPrefs.labelers.find((labeler) => labeler.did === labelerDid)?.labels[
identifier
];
// pending clears must not fall back to the saved value
const current = isPending ? variables?.visibility : saved;
const pref = current ? adjust(current) : undefined;
const inherited = adjust(
preferences
? resolveGlobalLabelPreference(preferences.moderationPrefs, labelDefinition)
: labelDefinition.defaultPref,
);
const effective = adultDisabled ? 'hide' : (pref ?? inherited);
const allOptions: LabelOption[] = [
{ label: m['components.moderation.labelPreference.inherited']({ value: labelOptions[inherited] }) },
{ label: labelOptions.ignore, value: 'ignore' },
{ label: labelOptions.warn, value: 'warn' },
{ label: labelOptions.hide, value: 'hide' },
];
const options = canWarn ? allOptions : allOptions.filter((option) => option.value !== 'warn');
const onChangeVisibility = ([selected]: string[]) => {
const option = options.find((candidate) => toggleValue(candidate.value) === selected);
if (option) {
mutate({ label: identifier, labelerDid, visibility: option.value });
}
};
return (
<Settings.CollapsibleRow
className={className}
label={labelStrings.name}
onOpenChange={onOpenChange}
open={open}
panel="body"
titleText={labelStrings.name}
trailing={!disabled && <Settings.Value text={labelOptions[effective]} />}
>
<Text color="textContrastMedium" size="md_sub">
{labelStrings.description}
</Text>
{adultDisabled && (
<span className={styles.note}>
<CircleInfo className={styles.circleInfoIcon} />
<Text color="textContrastMedium" size="sm" weight="medium">
{m['components.moderation.adultContent.disabled']()}
</Text>
</span>
)}
{configurable && (
<Toggle.Group
className={styles.radioList}
label={m['common.search.filteringFor']({ name: labelStrings.name })}
onChange={onChangeVisibility}
type="radio"
values={[toggleValue(pref)]}
>
{options.map((option) => (
<Toggle.RadioItem
key={toggleValue(option.value)}
label={option.label}
value={toggleValue(option.value)}
>
<Toggle.Panel>
<Toggle.RadioIndicator />
<Toggle.PanelText>{option.label}</Toggle.PanelText>
</Toggle.Panel>
</Toggle.RadioItem>
))}
</Toggle.Group>
)}
</Settings.CollapsibleRow>
);
}