import { Fragment, useRef } from 'react';
import type { AppBskyFeedDefs, AppBskyGraphDefs } from '@atcute/bluesky';
import { parseCanonicalResourceUri } from '@atcute/lexicons/syntax';
import { clsx } from 'clsx';
import { getPostRecord } from '#/lib/api/record-casts';
import { listTarget, profileTarget } from '#/lib/routes/targets';
import { type ThreadgateAllowUISetting, threadgateViewToAllowUISetting } from '#/state/queries/threadgate';
import { Trans } from '#/locale/Trans';
import * as Dialog from '#/components/Dialog';
import {
PostInteractionSettingsDialog,
usePrefetchPostInteractionSettings,
} from '#/components/dialogs/PostInteractionSettingsDialog';
import { Stack } from '#/components/Stack';
import { Text } from '#/components/Text';
import { InlineLinkText } from '#/components/web/Link';
import TinyChevronDownIcon from '#/icons/central/ChevronBottom_round_outlined_radius1_stroke2.svg';
import CircleBanSignIcon from '#/icons/central/CircleBanSign_round_outlined_radius1_stroke2.svg';
import EarthIcon from '#/icons/central/Earth_round_outlined_radius1_stroke2.svg';
import GroupIcon from '#/icons/central/Group3_round_outlined_radius1_stroke2.svg';
import { m } from '#/paraglide/messages';
import * as css from './WhoCanReply.css';
interface WhoCanReplyProps {
post: AppBskyFeedDefs.PostView;
isThreadAuthor: boolean;
}
export function WhoCanReply({ post, isThreadAuthor }: WhoCanReplyProps) {
const infoDialogHandle = Dialog.useDialogHandle();
const editDialogHandle = Dialog.useDialogHandle();
/*
* `WhoCanReply` is only used for root posts atm, in case this changes
* unexpectedly, we should check to make sure it's for sure the root URI.
*/
const record = getPostRecord(post);
const rootUri = record.reply?.root?.uri ?? post.uri;
const settings = threadgateViewToAllowUISetting(post.threadgate);
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
postUri: post.uri,
rootPostUri: rootUri,
});
const prefetchPromise = useRef<Promise<void>>(Promise.resolve());
const prefetch = () => {
prefetchPromise.current = prefetchPostInteractionSettings();
};
const anyoneCanReply = settings.length === 1 && settings[0]!.type === 'everybody';
const noOneCanReply = settings.length === 1 && settings[0]!.type === 'nobody';
const description = anyoneCanReply
? m['components.whoCanReply.summary.everybody.label']()
: noOneCanReply
? m['components.whoCanReply.summary.disabled.label']()
: m['components.whoCanReply.summary.some']();
const onPressOpen = () => {
if (isThreadAuthor) {
// wait briefly for prefetch, then open while showing the spinner.
void Promise.race([prefetchPromise.current, new Promise((res) => setTimeout(res, 200))]).finally(() => {
editDialogHandle.open(null);
});
} else {
infoDialogHandle.open(null);
}
};
return (
<>
<button
type="button"
aria-label={
isThreadAuthor ? m['components.whoCanReply.edit']() : m['common.interaction.whoCanReply']()
}
className={clsx(css.trigger, isThreadAuthor && css.triggerAuthor)}
onClick={onPressOpen}
// prefetch the interaction settings so the edit dialog opens without a spinner
onMouseEnter={isThreadAuthor ? prefetch : undefined}
>
<Icon settings={settings} />
<Text className={css.label} size="md_sub" color={isThreadAuthor ? 'textLink' : 'textContrastMedium'}>
{description}
</Text>
{isThreadAuthor && <TinyChevronDownIcon className={css.tinyChevronDownIcon} />}
</button>
{isThreadAuthor ? (
<PostInteractionSettingsDialog
postUri={post.uri}
rootPostUri={rootUri}
handle={editDialogHandle}
initialThreadgateView={post.threadgate}
/>
) : (
<WhoCanReplyDialog
handle={infoDialogHandle}
post={post}
settings={settings}
embeddingDisabled={!!post.viewer?.embeddingDisabled}
/>
)}
</>
);
}
function Icon({ settings }: { settings: ThreadgateAllowUISetting[] }) {
const isEverybody = settings.every((setting) => setting.type === 'everybody');
const isNobody = !!settings.find((gate) => gate.type === 'nobody');
const IconComponent = isEverybody ? EarthIcon : isNobody ? CircleBanSignIcon : GroupIcon;
return <IconComponent className={css.gateIcon} />;
}
function WhoCanReplyDialog({
handle,
post,
settings,
embeddingDisabled,
}: {
handle: Dialog.DialogHandle;
post: AppBskyFeedDefs.PostView;
settings: ThreadgateAllowUISetting[];
embeddingDisabled: boolean;
}) {
return (
<Dialog.Root handle={handle}>
<Dialog.Popup size="narrow">
<Stack gap="sm">
<Dialog.TitleRow>
<Dialog.Title>{m['components.whoCanReply.title']()}</Dialog.Title>
<Dialog.Close />
</Dialog.TitleRow>
<Rules post={post} settings={settings} embeddingDisabled={embeddingDisabled} />
</Stack>
</Dialog.Popup>
</Dialog.Root>
);
}
function Rules({
post,
settings,
embeddingDisabled,
}: {
post: AppBskyFeedDefs.PostView;
settings: ThreadgateAllowUISetting[];
embeddingDisabled: boolean;
}) {
return (
<>
<Text size="md" color="textContrastMedium">
{settings.length === 0 ? (
m['components.whoCanReply.summary.unknown']()
) : settings[0]!.type === 'everybody' ? (
m['components.whoCanReply.summary.everybody.description']()
) : settings[0]!.type === 'nobody' ? (
m['components.whoCanReply.summary.disabled.description']()
) : (
<Trans
message={m['components.whoCanReply.rules.template']}
markup={{
t0: () => (
<>
{settings.map((rule, i) => (
// oxlint-disable-next-line react/no-array-index-key -- fixed order
<Fragment key={`rule-${i}`}>
<Rule rule={rule} post={post} lists={post.threadgate!.lists} />
<Separator i={i} length={settings.length} />
</Fragment>
))}
</>
),
}}
/>
)}{' '}
</Text>
{embeddingDisabled && (
<Text size="md" color="textContrastMedium">
{m['components.whoCanReply.quote.noOne']()}
</Text>
)}
</>
);
}
function Rule({
rule,
post,
lists,
}: {
rule: ThreadgateAllowUISetting;
post: AppBskyFeedDefs.PostView;
lists: AppBskyGraphDefs.ListViewBasic[] | undefined;
}) {
if (rule.type === 'mention') {
return m['components.whoCanReply.rules.mentioned']();
}
if (rule.type === 'followers') {
return (
<Trans
message={m['components.whoCanReply.rules.following']}
inputs={{ handle: post.author.handle }}
markup={{
t0: ({ children }) => (
<InlineLinkText
label={`@${post.author.handle}`}
size="md_sub"
to={profileTarget(post.author.did)}
>
{children}
</InlineLinkText>
),
}}
/>
);
}
if (rule.type === 'following') {
return (
<Trans
message={m['components.whoCanReply.rules.followedBy']}
inputs={{ handle: post.author.handle }}
markup={{
t0: ({ children }) => (
<InlineLinkText
label={`@${post.author.handle}`}
size="md_sub"
to={profileTarget(post.author.did)}
>
{children}
</InlineLinkText>
),
}}
/>
);
}
if (rule.type === 'list') {
const list = lists?.find((l) => l.uri === rule.list);
if (list) {
const listUrip = parseCanonicalResourceUri(list.uri);
return (
<Trans
message={m['components.whoCanReply.rules.listMembers']}
inputs={{ name: list.name }}
markup={{
t0: ({ children }) => (
<InlineLinkText label={list.name} size="md_sub" to={listTarget(listUrip.repo, listUrip.rkey)}>
{children}
</InlineLinkText>
),
}}
/>
);
}
}
}
function Separator({ i, length }: { i: number; length: number }) {
if (length < 2 || i === length - 1) {
return null;
}
if (i === length - 2) {
return (
<>
{length > 2 ? ',' : ''} {m['components.whoCanReply.rules.and']()}{' '}
</>
);
}
return <>, </>;
}