extract reusable loading/unavailable states for chat invites
Add ChatInvite.Loading and ChatInvite.Unavailable presentational parts, and expose a single `status` enum from context instead of loose loading/error booleans. Consumers (JoinRequestEmbed, ChatInviteEmbed, MessageItemInviteEmbed, MessageInputEmbed) now branch on `status` and share the markup. The message embed gains the "no longer available" state it was previously missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,9 +38,9 @@ function ChatInviteEmbedBody({
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const {error} = ChatInvite.useChatInvite()
|
||||
const {status} = ChatInvite.useChatInvite()
|
||||
|
||||
if (error) {
|
||||
if (status === 'error') {
|
||||
return <ExternalEmbed link={link} onOpen={onOpen} style={style} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.p_lg,
|
||||
a.border,
|
||||
a.rounded_lg,
|
||||
t.atoms.border_contrast_high,
|
||||
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
||||
style,
|
||||
]}>
|
||||
<Loader size="md" fill={t.atoms.text.color} />
|
||||
</View>
|
||||
)
|
||||
const box = [
|
||||
a.border,
|
||||
a.rounded_lg,
|
||||
t.atoms.border_contrast_high,
|
||||
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
||||
]
|
||||
|
||||
if (status === 'loading') {
|
||||
return <ChatInvite.Loading style={[box, a.p_lg, style]} />
|
||||
}
|
||||
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||
if (status !== 'available') {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.p_lg,
|
||||
a.gap_xs,
|
||||
a.border,
|
||||
a.rounded_lg,
|
||||
t.atoms.border_contrast_high,
|
||||
t.atoms.bg_contrast_25,
|
||||
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
||||
style,
|
||||
]}>
|
||||
<WarningIcon size="md" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Chat invite link no longer available</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<ChatInvite.Unavailable
|
||||
style={[box, a.p_lg, t.atoms.bg_contrast_25, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.justify_between,
|
||||
a.border,
|
||||
a.rounded_lg,
|
||||
a.p_lg,
|
||||
a.gap_lg,
|
||||
t.atoms.border_contrast_high,
|
||||
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
||||
style,
|
||||
]}>
|
||||
<View style={[a.justify_between, a.p_lg, a.gap_lg, box, style]}>
|
||||
<ChatInvite.Card size="large" />
|
||||
<ChatInvite.JoinButton onPress={onOpen} />
|
||||
</View>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.align_center, a.justify_center, style]}>
|
||||
<Loader size="md" fill={t.atoms.text.color} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<ChatInviteProvider
|
||||
value={{code, loading, error: !!error, preview, action, hasFixedHeight}}>
|
||||
<ChatInviteProvider value={{code, status, preview, action, hasFixedHeight}}>
|
||||
{children}
|
||||
</ChatInviteProvider>
|
||||
)
|
||||
|
||||
@@ -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<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
const {hasFixedHeight} = useChatInvite()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[a.flex_row, a.align_center, a.justify_center, a.gap_xs, style]}>
|
||||
<WarningIcon size="md" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}
|
||||
allowFontScaling={!hasFixedHeight}>
|
||||
<Trans>Chat invite link no longer available</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -82,8 +82,7 @@ let MessageItemInviteEmbed = ({
|
||||
initialPreview={embed.joinLinkPreview}
|
||||
currentConvoId={convo.convo.view.id}
|
||||
hasFixedHeight={false}>
|
||||
<ChatInvite.Card size="small" />
|
||||
<ChatInvite.JoinButton />
|
||||
<MessageItemInviteEmbedBody />
|
||||
</ChatInvite.Root>
|
||||
</View>
|
||||
</View>
|
||||
@@ -92,3 +91,22 @@ let MessageItemInviteEmbed = ({
|
||||
}
|
||||
MessageItemInviteEmbed = memo(MessageItemInviteEmbed)
|
||||
export {MessageItemInviteEmbed}
|
||||
|
||||
function MessageItemInviteEmbedBody() {
|
||||
const {status} = ChatInvite.useChatInvite()
|
||||
|
||||
if (status === 'loading') {
|
||||
return <ChatInvite.Loading style={a.py_lg} />
|
||||
}
|
||||
|
||||
if (status !== 'available') {
|
||||
return <ChatInvite.Unavailable style={a.py_sm} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatInvite.Card size="small" />
|
||||
<ChatInvite.JoinButton />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={[{minHeight: 64}, a.justify_center, a.align_center]}>
|
||||
<Loader />
|
||||
</View>
|
||||
)
|
||||
if (status === 'loading') {
|
||||
return <ChatInvite.Loading style={{minHeight: 64}} />
|
||||
}
|
||||
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{minHeight: 64},
|
||||
a.flex_row,
|
||||
a.gap_xs,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<WarningIcon size="md" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Chat invite link no longer available</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
if (status !== 'available') {
|
||||
return <ChatInvite.Unavailable style={{minHeight: 64}} />
|
||||
}
|
||||
|
||||
return <ChatInvite.Card size="small" />
|
||||
|
||||
Reference in New Issue
Block a user