[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:
@@ -38,9 +38,9 @@ function ChatInviteEmbedBody({
|
|||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const {error} = ChatInvite.useChatInvite()
|
const {status} = ChatInvite.useChatInvite()
|
||||||
|
|
||||||
if (error) {
|
if (status === 'error') {
|
||||||
return <ExternalEmbed link={link} onOpen={onOpen} style={style} />
|
return <ExternalEmbed link={link} onOpen={onOpen} style={style} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {ChatBskyGroupDefs} from '@atproto/api'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ChatInvitePreview,
|
type ChatInvitePreview,
|
||||||
@@ -8,9 +6,6 @@ import {
|
|||||||
} from '#/state/queries/join-links'
|
} from '#/state/queries/join-links'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
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
|
const JOIN_REQUEST_EMBED_HEIGHT = 140
|
||||||
|
|
||||||
@@ -58,62 +53,29 @@ export function JoinRequestEmbedBody({
|
|||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {loading, preview} = ChatInvite.useChatInvite()
|
const {status} = ChatInvite.useChatInvite()
|
||||||
|
|
||||||
if (loading) {
|
const box = [
|
||||||
return (
|
a.border,
|
||||||
<View
|
a.rounded_lg,
|
||||||
style={[
|
t.atoms.border_contrast_high,
|
||||||
a.align_center,
|
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
||||||
a.justify_center,
|
]
|
||||||
a.p_lg,
|
|
||||||
a.border,
|
if (status === 'loading') {
|
||||||
a.rounded_lg,
|
return <ChatInvite.Loading style={[box, a.p_lg, style]} />
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
{height: JOIN_REQUEST_EMBED_HEIGHT},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
<Loader size="md" fill={t.atoms.text.color} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
if (status !== 'available') {
|
||||||
return (
|
return (
|
||||||
<View
|
<ChatInvite.Unavailable
|
||||||
style={[
|
style={[box, a.p_lg, t.atoms.bg_contrast_25, 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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[a.justify_between, a.p_lg, a.gap_lg, box, style]}>
|
||||||
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,
|
|
||||||
]}>
|
|
||||||
<ChatInvite.Card size="large" />
|
<ChatInvite.Card size="large" />
|
||||||
<ChatInvite.JoinButton onPress={onOpen} />
|
<ChatInvite.JoinButton onPress={onOpen} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -22,10 +22,20 @@ export type ChatInviteAction = {
|
|||||||
side: 'left' | 'right'
|
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 = {
|
export type ChatInviteContextValue = {
|
||||||
code: string
|
code: string
|
||||||
loading: boolean
|
status: ChatInviteStatus
|
||||||
error: boolean
|
|
||||||
preview: ChatInvitePreview | undefined
|
preview: ChatInvitePreview | undefined
|
||||||
/**
|
/**
|
||||||
* The derived action descriptor. Undefined while loading or when there's no
|
* 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 {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
||||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||||
import * as Toast from '#/components/Toast'
|
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
|
* 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 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
|
let action: ChatInviteAction | undefined
|
||||||
if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||||
@@ -135,8 +150,7 @@ export function Root({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatInviteProvider
|
<ChatInviteProvider value={{code, status, preview, action, hasFixedHeight}}>
|
||||||
value={{code, loading, error: !!error, preview, action, hasFixedHeight}}>
|
|
||||||
{children}
|
{children}
|
||||||
</ChatInviteProvider>
|
</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 {
|
export {
|
||||||
type ChatInviteAction,
|
type ChatInviteAction,
|
||||||
type ChatInviteContextValue,
|
type ChatInviteContextValue,
|
||||||
|
type ChatInviteStatus,
|
||||||
useChatInvite,
|
useChatInvite,
|
||||||
} from './Context'
|
} from './Context'
|
||||||
export {JoinButton} from './JoinButton'
|
export {JoinButton} from './JoinButton'
|
||||||
|
export {Loading} from './Loading'
|
||||||
export {Root} from './Root'
|
export {Root} from './Root'
|
||||||
|
export {Unavailable} from './Unavailable'
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ let MessageItemInviteEmbed = ({
|
|||||||
initialPreview={embed.joinLinkPreview}
|
initialPreview={embed.joinLinkPreview}
|
||||||
currentConvoId={convo.convo.view.id}
|
currentConvoId={convo.convo.view.id}
|
||||||
hasFixedHeight={false}>
|
hasFixedHeight={false}>
|
||||||
<ChatInvite.Card size="small" />
|
<MessageItemInviteEmbedBody />
|
||||||
<ChatInvite.JoinButton />
|
|
||||||
</ChatInvite.Root>
|
</ChatInvite.Root>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -92,3 +91,22 @@ let MessageItemInviteEmbed = ({
|
|||||||
}
|
}
|
||||||
MessageItemInviteEmbed = memo(MessageItemInviteEmbed)
|
MessageItemInviteEmbed = memo(MessageItemInviteEmbed)
|
||||||
export {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,
|
AppBskyFeedPost,
|
||||||
AppBskyRichtextFacet,
|
AppBskyRichtextFacet,
|
||||||
AtUri,
|
AtUri,
|
||||||
ChatBskyGroupDefs,
|
|
||||||
moderatePost,
|
moderatePost,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
@@ -31,7 +30,6 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
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 {Loader} from '#/components/Loader'
|
||||||
import * as MediaPreview from '#/components/MediaPreview'
|
import * as MediaPreview from '#/components/MediaPreview'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
@@ -305,33 +303,14 @@ function MessageInputInviteEmbed({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MessageInputInviteEmbedBody() {
|
function MessageInputInviteEmbedBody() {
|
||||||
const t = useTheme()
|
const {status} = ChatInvite.useChatInvite()
|
||||||
const {loading, preview} = ChatInvite.useChatInvite()
|
|
||||||
|
|
||||||
if (loading) {
|
if (status === 'loading') {
|
||||||
return (
|
return <ChatInvite.Loading style={{minHeight: 64}} />
|
||||||
<View style={[{minHeight: 64}, a.justify_center, a.align_center]}>
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
if (status !== 'available') {
|
||||||
return (
|
return <ChatInvite.Unavailable style={{minHeight: 64}} />
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ChatInvite.Card size="small" />
|
return <ChatInvite.Card size="small" />
|
||||||
|
|||||||
Reference in New Issue
Block a user