import {useCallback, useEffect, useMemo, useState} from 'react' import {LayoutAnimation, View} from 'react-native' import {AtUri} from '@atproto/syntax' import {moderatePost} from '@bsky/sdk/moderation' import {RichText as RichTextAPI} from '@bsky/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' import {HITSLOP_20} from '#/lib/constants' import {makeProfileLink} from '#/lib/routes/links' import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, getChatInviteCodeFromUrl, isBskyChatInviteUrl, isBskyPostUrl, makeRecordUri, } from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {usePostQuery} from '#/state/queries/post' import {PostMeta} from '#/view/com/util/PostMeta' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import * as ChatInvite from '#/components/dms/ChatInvite' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import * as MediaPreview from '#/components/MediaPreview' import {ContentHider} from '#/components/moderation/ContentHider' import {PostAlerts} from '#/components/moderation/PostAlerts' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {app} from '#/lexicons' import * as bsky from '#/types/bsky' /** * The embed staged in the message composer. A message can carry at most one * embed: either a quoted post or a group chat invite link. */ export type MessageEmbedState = {type: 'post'; uri: string} | {type: 'invite'; code: string} export function useMessageEmbed() { const route = useRoute>() const navigation = useNavigation() const embedFromParams = route.params.embed const [embed, setEmbed] = useState( embedFromParams ? {type: 'post', uri: embedFromParams} : undefined, ) if (embedFromParams && embed?.type !== 'post') { setEmbed({type: 'post', uri: embedFromParams}) } return { embed, setEmbed: useCallback( (embedUrl: string | undefined) => { if (!embedUrl) { // Only the post embed is reflected in the route param (used by the // share-to-DM intent flow); invites are local-only. navigation.setParams({embed: ''}) setEmbed(undefined) return } if (embedFromParams) return if (isBskyChatInviteUrl(embedUrl)) { const code = getChatInviteCodeFromUrl(embedUrl) if (code) { setEmbed({type: 'invite', code}) } return } if (isBskyPostUrl(embedUrl)) { const url = convertBskyAppUrlIfNeeded(embedUrl) const [_0, user, _1, rkey] = url.split('/').filter(Boolean) const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) setEmbed({type: 'post', uri}) } }, [embedFromParams, navigation], ), } } export function useExtractEmbedFromFacets( message: string, setEmbed: (embedUrl: string | undefined) => void, ) { const rt = new RichTextAPI({text: message}) rt.detectFacetsWithoutResolution() let uriFromFacet: string | undefined for (const facet of rt.facets ?? []) { for (const feature of facet.features) { if ( bsky.isType(app.bsky.richtext.facet.link, feature) && (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) ) { uriFromFacet = feature.uri break } } } useEffect(() => { if (uriFromFacet) { setEmbed(uriFromFacet) } }, [uriFromFacet, setEmbed]) } export function MessageInputEmbed({ embed, setEmbed, }: { embed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void }) { const onRemove = useCallback(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setEmbed(undefined) }, [setEmbed]) if (!embed) { return null } switch (embed.type) { case 'post': return case 'invite': return } } function MessageInputPostEmbed({ uri, onRemove, }: { uri: string onRemove: () => void }) { const t = useTheme() const {t: l} = useLingui() const {data: post, status} = usePostQuery(uri) const moderationOpts = useModerationOpts() const moderation = useMemo( () => moderationOpts && post ? moderatePost(post, moderationOpts) : undefined, [moderationOpts, post], ) const {rt, record} = useMemo(() => { if (post && bsky.isType(app.bsky.feed.post, post.record)) { return { rt: new RichTextAPI({ text: post.record.text, facets: post.record.facets, }), record: post.record, } } return {rt: undefined, record: undefined} }, [post]) switch (status) { case 'pending': { return ( ) } case 'error': { return ( Could not fetch post ) } case 'success': { const itemUrip = new AtUri(post.uri) const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey) if (!post || !moderation || !rt || !record) { return null } return ( {rt.text && ( )} ) } } } function MessageInputInviteEmbed({ code, onRemove, }: { code: string onRemove: () => void }) { const t = useTheme() const {t: l} = useLingui() return ( ) } function MessageInputInviteEmbedBody() { const {status} = ChatInvite.useChatInvite() if (status === 'loading') { return } if (status !== 'available') { return } return } function SimpleContainer({ children, onRemove, }: { children: React.ReactNode onRemove?: () => void }) { const t = useTheme() const {t: l} = useLingui() return ( {children} {onRemove && ( )} ) }