feat: preview Tangled strings in composer
11234512 unmodified lines1819202122232425262728293031323334353637383953 unmodified lines9394959697989299100101102103104105106107108109101 unmodified linesExpand allimport type { CSSProperties, ReactNode } from 'react';import { lazy, type CSSProperties, type ReactNode, Suspense } from 'react';
import type { AppBskyEmbedExternal } from '@atcute/bluesky';
import { clsx } from 'clsx';12 unmodified linesimport { ModeratedFeedEmbed } from '#/components/Post/Embed/FeedEmbed';import { ModeratedListEmbed } from '#/components/Post/Embed/ListEmbed';import { StandardSiteEmbed } from '#/components/Post/Embed/StandardSiteEmbed';import { isStandardSiteEmbed } from '#/components/Post/Embed/StandardSiteEmbed/utils';import { parseTangledStringUrl } from '#/components/Post/Embed/TangledStringEmbed/detect';import { TangledStringPlaceholder } from '#/components/Post/Embed/TangledStringEmbed/Placeholder';import { Spinner } from '#/components/Spinner';import { Embed as StarterPackEmbed } from '#/components/StarterPack/StarterPackCard';import { Text } from '#/components/Text';
import { m } from '#/paraglide/messages';
import * as styles from './ExternalEmbed.css';
const TangledStringEmbed = lazy(() => import('#/components/Post/Embed/TangledStringEmbed').then((mod) => ({ default: mod.TangledStringEmbed })),);
export const ExternalEmbedGif = ({ onRemove, gif }: { onRemove: () => void; gif: Gif }) => { const { data, error } = useResolveGifQuery(gif); const thumbUrl = getBlobUrl(data?.thumb?.source.blob); let linkInfo: AppBskyEmbedExternal.ViewExternal | undefined;53 unmodified lines onRemove: () => void;}) => { const { data, error } = useResolveLinkQuery(uri); const thumbUrl = getBlobUrl(data?.type === 'external' ? data.thumb?.source.blob : undefined); const tangledTarget = parseTangledStringUrl(uri); let linkComponent: ReactNode; if (data) { if (tangledTarget) { linkComponent = ( <Suspense fallback={<TangledStringPlaceholder />}> <TangledStringEmbed target={tangledTarget} /> </Suspense> ); } else if (data) { if (data.type === 'external') { const external = data.view?.external; if (external && isStandardSiteEmbed(external)) { linkComponent = (101 unmodified linesExpand all37 unmodified lines38394041424344454643444748495046515253545545 unmodified lines1011021031041051061071081091101111121131021141151161171185 unmodified lines37 unmodified linesexport const filenameLink = style({ display: 'flex', flexGrow: 1, minWidth: 0,});
export const filenameLinkInteractive = style([ filenameLink, { ':hover': { textDecoration: 'underline', ':hover': { textDecoration: 'underline', }, },});]);
export const domainLink = style({ display: 'flex', flexShrink: 0,45 unmodified lines gap: space.sm, minWidth: 0,});
export const bylineInteractive = style([ byline, { cursor: 'pointer', },]);
export const bylineHandle = style({ selectors: { [`${byline}:hover &`]: { [`${bylineInteractive}:hover &`]: { textDecoration: 'underline', textDecorationColor: colors.textContrastMedium, }, },5 unmodified lines9 unmodified lines10111213141516171825 unmodified lines4445464748495051522 unmodified lines555657585960616263646566676869707163646566676869707172737475767778798081828384857374757677787980818283868788899091929394959697989910010125 unmodified lines1271281291301311171181191201211221231241251261271281321331341351361371381391401411421431441451461471321481491501511525 unmodified lines1581591601611621631641651664 unmodified lines1711721731741751761771781791801811821831841851861871881891901911921931941951601611621631641651661671681691701961971981992002019 unmodified linesimport { useResolveDidQuery } from '#/state/queries/resolve-uri';import { useTangledStringQuery } from '#/state/queries/tangled-string';
import * as Dialog from '#/components/Dialog';import { useNavigationDisabled } from '#/components/NavigationDisabled';import { ProfileHoverCard } from '#/components/ProfileHoverCard';import { Text } from '#/components/Text';import { UserAvatar } from '#/components/UserAvatar';import { Button, ButtonIcon, ButtonText } from '#/components/web/Button';25 unmodified lines};
export function TangledStringEmbed({ target, className, onOpen }: TangledStringEmbedProps) { const dialog = Dialog.useDialogHandle(); const navigationDisabled = useNavigationDisabled();
const uri = makeRecordUri(target.actor, 'sh.tangled.string', target.rkey); const { data: record, error } = useTangledStringQuery({ uri });
2 unmodified lines filename !== undefined ? m['components.post.tangledString.a11y.open']({ filename }) : m['components.post.tangledString.a11y.openSnippet'](); const viewFileLabel = m['components.post.tangledString.viewFile'](); const filenameContent = record ? ( <Text numberOfLines={1} size="md_sub" weight="semiBold"> {record.filename} </Text> ) : !error ? ( <Skele.Text size="md_sub" width={120} /> ) : null;
return ( <div className={clsx(css.card, className)}> <div className={css.header}> <CodeBrackets aria-hidden className={css.headerIcon} />
<ExternalLink className={css.filenameLink} href={target.href} label={openLabel} onPress={onOpen}> {record ? ( <Text numberOfLines={1} size="md_sub" weight="semiBold"> {record.filename} </Text> ) : !error ? ( <Skele.Text size="md_sub" width={120} /> ) : null} </ExternalLink> {navigationDisabled ? ( <div className={css.filenameLink}>{filenameContent}</div> ) : ( <ExternalLink className={css.filenameLinkInteractive} href={target.href} label={openLabel} onPress={onOpen} > {filenameContent} </ExternalLink> )}
{!navigationDisabled ? ( <ExternalLink className={css.domainLink} href={target.href} label={m['components.post.tangledString.a11y.openSnippet']()} onPress={onOpen} tabIndex={-1} > <Text color="textContrastMedium" size="xs"> tangled.org </Text> </ExternalLink> <ExternalLink className={css.domainLink} href={target.href} label={m['components.post.tangledString.a11y.openSnippet']()} onPress={onOpen} tabIndex={-1} > <Text color="textContrastMedium" size="xs"> tangled.org </Text> </ExternalLink> ) : null} </div>
<div className={css.codeArea}> {record ? (25 unmodified lines })} </Text> ) : null}
{!navigationDisabled ? ( <Button className={css.action} color="secondary" disabled={record === undefined} label={viewFileLabel} onClick={() => dialog.open(null)} size="tiny" variant="ghost" > <ButtonText>{viewFileLabel}</ButtonText> <ButtonIcon icon={ExpandIcon} /> </Button> <Button className={css.action} color="secondary" disabled={record === undefined} label={viewFileLabel} onClick={() => dialog.open(null)} size="tiny" variant="ghost" > <ButtonText>{viewFileLabel}</ButtonText> <ButtonIcon icon={ExpandIcon} /> </Button> ) : null} </div> </div>
{record !== undefined ? ( {record !== undefined && !navigationDisabled ? ( <FullFileDialog contents={record.contents} filename={record.filename} handle={dialog}5 unmodified lines );}
const Byline = ({ actor }: { actor: ActorIdentifier }) => { const navigationDisabled = useNavigationDisabled(); const { data: did } = useResolveDidQuery(actor); const { data: author } = useProfileQuery({ batch: true, did });
if (!did || !author) {4 unmodified lines </Skele.Row> ); }
const content = ( <> <UserAvatar avatar={author.avatar} size={20} type="user" /> <Text className={css.bylineHandle} color="textContrastMedium" numberOfLines={1} size="sm" weight="medium" > {author.handle} </Text> </> );
if (navigationDisabled) { return <div className={css.byline}>{content}</div>; }
return ( <ProfileHoverCard actor={did}> <Link className={css.byline} label={author.handle} to={profileTarget(author.did)}> <UserAvatar avatar={author.avatar} size={20} type="user" /> <Text className={css.bylineHandle} color="textContrastMedium" numberOfLines={1} size="sm" weight="medium" > {author.handle} </Text> <Link className={css.bylineInteractive} label={author.handle} to={profileTarget(author.did)}> {content} </Link> </ProfileHoverCard> );};