158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
import {setStringAsync} from 'expo-clipboard'
|
|
import {ChatBskyGroupDefs} from '@atproto/api'
|
|
import {useLingui} from '@lingui/react/macro'
|
|
import {useNavigation} from '@react-navigation/native'
|
|
|
|
import {type NavigationProp} from '#/lib/routes/types'
|
|
import {
|
|
type ChatInvitePreview,
|
|
useJoinLinkPreviewsQuery,
|
|
} from '#/state/queries/join-links'
|
|
import {useSession} from '#/state/session'
|
|
import {type ButtonColor} from '#/components/Button'
|
|
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
|
import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight'
|
|
import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/ChainLink'
|
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
|
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,
|
|
type ChatInviteStatus,
|
|
} from './Context'
|
|
|
|
/**
|
|
* Headless data + state owner for a chat invite. Fetches the join link preview
|
|
* by code and derives the join/open action, exposing both via context for the
|
|
* composable parts (`Card`, `JoinButton`) or any custom UI to consume.
|
|
*
|
|
* Pass `initialPreview` when the preview is already known (e.g. a DM message
|
|
* embed already carries the resolved view) to avoid a loading flash.
|
|
*/
|
|
export function Root({
|
|
code,
|
|
initialPreview,
|
|
currentConvoId,
|
|
hasFixedHeight,
|
|
children,
|
|
}: {
|
|
code: string
|
|
initialPreview?: ChatInvitePreview
|
|
/**
|
|
* The convo this invite is being viewed within, if any. When the invite
|
|
* links to the same chat, the action becomes "Copy link" instead of
|
|
* open/join (you're already here).
|
|
*/
|
|
currentConvoId?: string
|
|
hasFixedHeight: boolean
|
|
children: React.ReactNode
|
|
}) {
|
|
const {hasSession} = useSession()
|
|
const {t: l} = useLingui()
|
|
const navigation = useNavigation<NavigationProp>()
|
|
const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs()
|
|
|
|
const {data, error, isPending} = useJoinLinkPreviewsQuery({
|
|
codes: [code],
|
|
hasSession,
|
|
// Seed the cache with the already-resolved preview so we don't refetch.
|
|
initialData: initialPreview
|
|
? {joinLinkPreviews: [initialPreview]}
|
|
: undefined,
|
|
})
|
|
|
|
const preview = data?.joinLinkPreviews[0]
|
|
|
|
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)) {
|
|
const convoId = preview.convo?.id
|
|
const isFollowing = preview.owner.viewer?.followedBy ?? false
|
|
const hasRequested = !convoId && preview.viewer?.requestedAt != null
|
|
|
|
if (convoId && convoId === currentConvoId) {
|
|
// You're already in the chat this invite links to - offer to copy the
|
|
// link rather than open/join.
|
|
action = {
|
|
label: l`Copy link`,
|
|
accessibilityHint: l`Tap to copy this invite link`,
|
|
icon: LinkIcon,
|
|
side: 'left',
|
|
color: 'primary',
|
|
disabled: false,
|
|
onPress: () => {
|
|
void setStringAsync(`https://bsky.app/chat/${preview.code}`)
|
|
Toast.show(l`Copied to clipboard`, {type: 'success'})
|
|
},
|
|
}
|
|
} else if (convoId) {
|
|
action = {
|
|
label: l`Open chat`,
|
|
accessibilityHint: l`Tap to open this group chat`,
|
|
icon: ArrowRightIcon,
|
|
side: 'right',
|
|
color: 'primary',
|
|
disabled: false,
|
|
onPress: () => {
|
|
navigation.push('MessagesConversation', {conversation: convoId})
|
|
},
|
|
}
|
|
} else {
|
|
let canJoin = true
|
|
let icon: React.ComponentType<SVGIconProps> = JoinIcon
|
|
let label = preview.requireApproval ? l`Request to join` : l`Join`
|
|
let color: ButtonColor = 'primary'
|
|
if (preview.memberCount >= preview.memberLimit) {
|
|
canJoin = false
|
|
icon = HandIcon
|
|
label = l`This chat is full`
|
|
color = 'secondary'
|
|
} else if (preview.joinRule === 'followedByOwner' && !isFollowing) {
|
|
canJoin = false
|
|
icon = HandIcon
|
|
label = l`Only people the chat owner follows can join`
|
|
color = 'secondary'
|
|
} else if (hasRequested) {
|
|
icon = CheckIcon
|
|
label = l`Requested`
|
|
color = 'secondary'
|
|
}
|
|
|
|
action = {
|
|
label,
|
|
side: 'left',
|
|
accessibilityHint: preview.requireApproval
|
|
? l`Tap to request access to join this group chat`
|
|
: l`Tap to join this group chat immediately`,
|
|
icon,
|
|
color,
|
|
disabled: !canJoin,
|
|
onPress: () => {
|
|
setGroupChatJoinState({code: preview.code})
|
|
groupChatJoinDialogControl.open()
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ChatInviteProvider value={{code, status, preview, action, hasFixedHeight}}>
|
|
{children}
|
|
</ChatInviteProvider>
|
|
)
|
|
}
|