diff --git a/src/components/Post/Embed/ChatInviteEmbed.tsx b/src/components/Post/Embed/ChatInviteEmbed.tsx index 16f05cbfea..d5f69f49e9 100644 --- a/src/components/Post/Embed/ChatInviteEmbed.tsx +++ b/src/components/Post/Embed/ChatInviteEmbed.tsx @@ -38,9 +38,9 @@ function ChatInviteEmbedBody({ onOpen?: () => void style?: StyleProp }) { - const {error} = ChatInvite.useChatInvite() + const {status} = ChatInvite.useChatInvite() - if (error) { + if (status === 'error') { return } diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx index a3cc08cac6..44b10b2707 100644 --- a/src/components/Post/Embed/JoinRequestEmbed.tsx +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -1,6 +1,4 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import {ChatBskyGroupDefs} from '@atproto/api' -import {Trans} from '@lingui/react/macro' import { type ChatInvitePreview, @@ -8,9 +6,6 @@ import { } from '#/state/queries/join-links' import {atoms as a, useTheme} from '#/alf' import * as ChatInvite from '#/components/dms/ChatInvite' -import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' const JOIN_REQUEST_EMBED_HEIGHT = 140 @@ -58,62 +53,29 @@ export function JoinRequestEmbedBody({ onOpen?: () => void }) { const t = useTheme() - const {loading, preview} = ChatInvite.useChatInvite() + const {status} = ChatInvite.useChatInvite() - if (loading) { - return ( - - - - ) + const box = [ + a.border, + a.rounded_lg, + t.atoms.border_contrast_high, + {height: JOIN_REQUEST_EMBED_HEIGHT}, + ] + + if (status === 'loading') { + return } - if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { + if (status !== 'available') { return ( - - - - Chat invite link no longer available - - + ) } return ( - + diff --git a/src/components/dms/ChatInvite/Context.tsx b/src/components/dms/ChatInvite/Context.tsx index 2e636c3b61..bddbea4cb3 100644 --- a/src/components/dms/ChatInvite/Context.tsx +++ b/src/components/dms/ChatInvite/Context.tsx @@ -22,10 +22,20 @@ export type ChatInviteAction = { side: 'left' | 'right' } +/** + * The resolved state of a chat invite: + * - `loading`: the preview is still being fetched. + * - `error`: the fetch failed (e.g. network). Surfaces may want to fall back to + * a plain link rather than show a chat-specific error. + * - `unavailable`: the preview resolved but the link is disabled, invalid, or an + * unrecognized variant - there's nothing to join. + * - `available`: a usable `JoinLinkPreviewView` is present. + */ +export type ChatInviteStatus = 'loading' | 'error' | 'unavailable' | 'available' + export type ChatInviteContextValue = { code: string - loading: boolean - error: boolean + status: ChatInviteStatus preview: ChatInvitePreview | undefined /** * The derived action descriptor. Undefined while loading or when there's no diff --git a/src/components/dms/ChatInvite/Loading.tsx b/src/components/dms/ChatInvite/Loading.tsx new file mode 100644 index 0000000000..310ed7f1a5 --- /dev/null +++ b/src/components/dms/ChatInvite/Loading.tsx @@ -0,0 +1,18 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' + +/** + * Loading state for a chat invite: a centered spinner. The outer container + * (height, border, etc.) varies per surface, so pass it via `style`. + */ +export function Loading({style}: {style?: StyleProp}) { + const t = useTheme() + + return ( + + + + ) +} diff --git a/src/components/dms/ChatInvite/Root.tsx b/src/components/dms/ChatInvite/Root.tsx index 3577a97c64..016f28d009 100644 --- a/src/components/dms/ChatInvite/Root.tsx +++ b/src/components/dms/ChatInvite/Root.tsx @@ -18,7 +18,11 @@ import {type Props as SVGIconProps} from '#/components/icons/common' import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' import {useIntentDialogs} from '#/components/intents/IntentDialogs' import * as Toast from '#/components/Toast' -import {type ChatInviteAction, ChatInviteProvider} from './Context' +import { + type ChatInviteAction, + ChatInviteProvider, + type ChatInviteStatus, +} from './Context' /** * Headless data + state owner for a chat invite. Fetches the join link preview @@ -61,7 +65,18 @@ export function Root({ }) const preview = data?.joinLinkPreviews[0] - const loading = isPending && !preview + + let status: ChatInviteStatus + if (isPending && !preview) { + status = 'loading' + } else if (error) { + status = 'error' + } else if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { + status = 'available' + } else { + // Resolved to a disabled/invalid/unrecognized preview - nothing to join. + status = 'unavailable' + } let action: ChatInviteAction | undefined if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { @@ -135,8 +150,7 @@ export function Root({ } return ( - + {children} ) diff --git a/src/components/dms/ChatInvite/Unavailable.tsx b/src/components/dms/ChatInvite/Unavailable.tsx new file mode 100644 index 0000000000..cdfb1d951e --- /dev/null +++ b/src/components/dms/ChatInvite/Unavailable.tsx @@ -0,0 +1,29 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {Trans} from '@lingui/react/macro' + +import {atoms as a, useTheme} from '#/alf' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {Text} from '#/components/Typography' +import {useChatInvite} from './Context' + +/** + * "No longer available" state for a chat invite, shown when the link is + * disabled, invalid, or otherwise can't be resolved. The outer container + * (height, border, etc.) varies per surface, so pass it via `style`. + */ +export function Unavailable({style}: {style?: StyleProp}) { + const t = useTheme() + const {hasFixedHeight} = useChatInvite() + + return ( + + + + Chat invite link no longer available + + + ) +} diff --git a/src/components/dms/ChatInvite/index.tsx b/src/components/dms/ChatInvite/index.tsx index bcea3f7651..a37745616a 100644 --- a/src/components/dms/ChatInvite/index.tsx +++ b/src/components/dms/ChatInvite/index.tsx @@ -2,7 +2,10 @@ export {Card} from './Card' export { type ChatInviteAction, type ChatInviteContextValue, + type ChatInviteStatus, useChatInvite, } from './Context' export {JoinButton} from './JoinButton' +export {Loading} from './Loading' export {Root} from './Root' +export {Unavailable} from './Unavailable' diff --git a/src/components/dms/MessageItemInviteEmbed.tsx b/src/components/dms/MessageItemInviteEmbed.tsx index 79416057c9..92dddea35d 100644 --- a/src/components/dms/MessageItemInviteEmbed.tsx +++ b/src/components/dms/MessageItemInviteEmbed.tsx @@ -82,8 +82,7 @@ let MessageItemInviteEmbed = ({ initialPreview={embed.joinLinkPreview} currentConvoId={convo.convo.view.id} hasFixedHeight={false}> - - + @@ -92,3 +91,22 @@ let MessageItemInviteEmbed = ({ } MessageItemInviteEmbed = memo(MessageItemInviteEmbed) export {MessageItemInviteEmbed} + +function MessageItemInviteEmbedBody() { + const {status} = ChatInvite.useChatInvite() + + if (status === 'loading') { + return + } + + if (status !== 'available') { + return + } + + return ( + <> + + + + ) +} diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index c38141ce9d..4b69e1f67d 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -4,7 +4,6 @@ import { AppBskyFeedPost, AppBskyRichtextFacet, AtUri, - ChatBskyGroupDefs, moderatePost, RichText as RichTextAPI, } from '@atproto/api' @@ -31,7 +30,6 @@ 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 {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' import * as MediaPreview from '#/components/MediaPreview' import {ContentHider} from '#/components/moderation/ContentHider' @@ -305,33 +303,14 @@ function MessageInputInviteEmbed({ } function MessageInputInviteEmbedBody() { - const t = useTheme() - const {loading, preview} = ChatInvite.useChatInvite() + const {status} = ChatInvite.useChatInvite() - if (loading) { - return ( - - - - ) + if (status === 'loading') { + return } - if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { - return ( - - - - Chat invite link no longer available - - - ) + if (status !== 'available') { + return } return