[Chat] Reusable loading/unavailable states for chat invites (#10819)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-10 21:54:37 +03:00
committed by GitHub
parent 3095bbfbec
commit 3b5ede37f7
9 changed files with 122 additions and 89 deletions
+12 -2
View File
@@ -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
+18
View File
@@ -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 -4
View File
@@ -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>
)
}
+3
View File
@@ -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'