import type { MouseEvent } from 'react';
import type { Handle } from '@atcute/lexicons';
import { clsx } from 'clsx';
import { isInvalidHandle } from '#/lib/display-names';
import {
usePreferencesQuery,
useRemoveMutedWordsMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences';
import * as Menu from '#/components/Menu';
import * as Prompt from '#/components/Prompt';
import { atomicSegment } from '#/components/RichText.css';
import { Spinner } from '#/components/Spinner';
import type { TextProps } from '#/components/Text';
import * as textStyles from '#/components/Text.css';
import { type InlineLinkUnderline, useInternalLink } from '#/components/web/Link';
import * as linkStyles from '#/components/web/Link.css';
import Search from '#/icons/central/MagnifyingGlass_round_outlined_radius1_stroke2.svg';
import Mute from '#/icons/central/Mute_round_outlined_radius1_stroke2.svg';
import Person from '#/icons/central/People_round_outlined_radius1_stroke2.svg';
import { m } from '#/paraglide/messages';
import { useRouter } from '#/router';
const preventDefault = (e: MouseEvent) => e.preventDefault();
export type RichTextTagProps = Pick<TextProps, 'color' | 'leading' | 'size'> & {
authorHandle?: Handle;
display: string;
tag: string;
underline?: InlineLinkUnderline;
};
/**
* a link to a hashtag or cashtag feed within {@link RichText} that opens a context menu on click.
*
* @param props the component props
*/
const MenuSpinner = () => <Spinner color="default" label={m['common.status.loading']()} size="lg" />;
export function RichTextTag({
authorHandle,
color = 'textLink',
display,
leading,
size,
tag,
underline = 'hover',
}: RichTextTagProps) {
const router = useRouter();
const { isLoading: isPreferencesLoading, data: preferences } = usePreferencesQuery();
const {
mutateAsync: upsertMutedWord,
variables: optimisticUpsert,
reset: resetUpsert,
} = useUpsertMutedWordsMutation();
const {
mutateAsync: removeMutedWords,
variables: optimisticRemove,
reset: resetRemove,
} = useRemoveMutedWordsMutation();
const { href } = useInternalLink({ to: { name: 'Hashtag', tag } });
const muteConfirmHandle = Prompt.usePromptHandle();
const isCashtag = tag.startsWith('$');
const label = isCashtag
? m['components.richTextTag.a11y.cashtag']({ tag })
: m['components.richTextTag.a11y.hashtag']({ tag });
const prefixedTag = isCashtag ? tag : `#${tag}`;
const isMuted = !!(
(preferences?.moderationPrefs.mutedWords?.find(
(word) => word.value === tag && word.targets.includes('tag'),
) ??
optimisticUpsert?.find((word) => word.value === tag && word.targets.includes('tag'))) &&
!optimisticRemove?.find((word) => word?.value === tag)
);
// mute records that exactly match the tag in question
const removeableMuteWords =
preferences?.moderationPrefs.mutedWords?.filter((word) => word.value === tag) ?? [];
const toggleMute = () => {
if (isMuted) {
resetUpsert();
void removeMutedWords(removeableMuteWords);
} else {
resetRemove();
void upsertMutedWord([{ actorTarget: 'all', targets: ['tag'], value: tag }]);
}
};
return (
<>
<Menu.Root>
<Menu.Trigger
aria-label={label}
className={clsx(
textStyles.text({ color, leading, size }),
linkStyles.inlineLink({ underline }),
atomicSegment,
)}
nativeButton={false}
// the anchor exists only for its href (hover preview, middle/right-click "open in new tab"); a
// plain left-click always opens the menu, so suppress the native navigation it would trigger
onClick={preventDefault}
render={<a href={href} />}
>
{display}
</Menu.Trigger>
<Menu.Popup label={label}>
<Menu.Group>
<Menu.Item
label={m['components.richTextTag.seePosts.prefixed']({ prefixedTag })}
onClick={() => router.navigate({ to: { name: 'Hashtag', tag } })}
>
<Menu.ItemText>
{isCashtag
? m['components.richTextTag.seePosts.tag']({ tag })
: m['components.richTextTag.seePosts.hash']({ tag })}
</Menu.ItemText>
<Menu.ItemIcon icon={Search} />
</Menu.Item>
{authorHandle && !isInvalidHandle(authorHandle) && (
<Menu.Item
label={m['components.richTextTag.seePosts.prefixedByUser']({ prefixedTag })}
onClick={() => router.navigate({ to: { name: 'Hashtag', author: authorHandle, tag } })}
>
<Menu.ItemText>
{isCashtag
? m['components.richTextTag.seePosts.tagByUser']({ tag })
: m['components.richTextTag.seePosts.hashByUser']({ tag })}
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
)}
</Menu.Group>
<Menu.Separator />
<Menu.Item
label={
isMuted
? m['components.richTextTag.unmute']({ prefixedTag })
: m['components.richTextTag.mute']({ prefixedTag })
}
onClick={() => muteConfirmHandle.open(null)}
>
<Menu.ItemText>
{isMuted
? m['components.richTextTag.unmute']({ prefixedTag })
: m['components.richTextTag.mute']({ prefixedTag })}
</Menu.ItemText>
<Menu.ItemIcon icon={isPreferencesLoading ? MenuSpinner : Mute} />
</Menu.Item>
</Menu.Popup>
</Menu.Root>
<Prompt.Basic
handle={muteConfirmHandle}
title={
isMuted
? m['components.richTextTag.unmuteConfirm.title']({ prefixedTag })
: m['components.richTextTag.muteConfirm.title']({ prefixedTag })
}
description={
isMuted
? m['components.richTextTag.unmuteConfirm.description']()
: m['components.richTextTag.muteConfirm.description']()
}
onConfirm={toggleMute}
confirmButtonCta={
isMuted
? m['components.richTextTag.unmute']({ prefixedTag })
: m['components.richTextTag.mute']({ prefixedTag })
}
confirmButtonColor={isMuted ? 'primary' : 'negative'}
/>
</>
);
}