Create logged-out view for group chat invites (#10598)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -28,23 +28,29 @@ type Layout = {
|
||||
border?: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
animate?: boolean
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
size?: number
|
||||
moderationOpts?: ModerationOpts
|
||||
}
|
||||
|
||||
export function AvatarBubbles({
|
||||
animate = false,
|
||||
profiles: allProfiles,
|
||||
self = false,
|
||||
size = 120,
|
||||
moderationOpts,
|
||||
}: Props) {
|
||||
}: {
|
||||
animate?: boolean
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
/**
|
||||
* By default, when there are more than 2 profiles, the current user is
|
||||
* filtered out (so you don't see yourself among your own group's members).
|
||||
* Set this to `true` for cases where every passed profile should appear,
|
||||
* e.g. an invite preview where the owner is meaningful regardless of viewer.
|
||||
*/
|
||||
self?: boolean
|
||||
size?: number
|
||||
moderationOpts?: ModerationOpts
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const profiles =
|
||||
allProfiles.length > 2
|
||||
? allProfiles.filter(p => p.did !== currentAccount?.did)
|
||||
!self && allProfiles.length > 2
|
||||
? allProfiles.filter(p => p?.did != null && p.did !== currentAccount?.did)
|
||||
: allProfiles
|
||||
const moderations = useMemo(() => {
|
||||
if (!moderationOpts) return []
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
|
||||
import {useGroupChatJoinIntent} from '#/lib/hooks/useIntentHandler'
|
||||
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types'
|
||||
@@ -19,6 +20,7 @@ import {shareUrl} from '#/lib/sharing'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
createProxiedUrl,
|
||||
getChatInviteCodeFromUrl,
|
||||
isBskyDownloadUrl,
|
||||
isExternalUrl,
|
||||
linkRequiresWarning,
|
||||
@@ -130,6 +132,7 @@ export function useLink({
|
||||
const {closeModal} = useModalControls()
|
||||
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
|
||||
const openLink = useOpenLink()
|
||||
const groupChatJoinIntent = useGroupChatJoinIntent()
|
||||
|
||||
const onPress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
@@ -148,6 +151,12 @@ export function useLink({
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const chatInviteCode = getChatInviteCodeFromUrl(href)
|
||||
if (chatInviteCode) {
|
||||
groupChatJoinIntent(chatInviteCode, href)
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresWarning) {
|
||||
linkWarningDialogControl.open({
|
||||
displayText,
|
||||
@@ -228,6 +237,7 @@ export function useLink({
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
linkWarningDialogControl,
|
||||
groupChatJoinIntent,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+28
-4
@@ -1,22 +1,45 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import * as Linking from 'expo-linking'
|
||||
|
||||
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
|
||||
import {
|
||||
createStarterPackLinkFromAndroidReferrer,
|
||||
httpStarterPackUriToAtUri,
|
||||
} from '#/lib/strings/starter-pack'
|
||||
import {CHAT_INVITE_CODE_REGEX} from '#/lib/strings/url-helpers'
|
||||
import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
|
||||
import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
|
||||
import {
|
||||
useSetActiveLanding,
|
||||
useSetActiveStarterPack,
|
||||
} from '#/state/shell/landing'
|
||||
import {IS_ANDROID} from '#/env'
|
||||
import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
export function useStarterPackEntry() {
|
||||
export function useLandingEntry() {
|
||||
const [ready, setReady] = useState(false)
|
||||
const setActiveStarterPack = useSetActiveStarterPack()
|
||||
const setActiveLanding = useSetActiveLanding()
|
||||
const hasCheckedForStarterPack = useHasCheckedForStarterPack()
|
||||
|
||||
useEffect(() => {
|
||||
if (ready) return
|
||||
|
||||
// Check for group chat invite link from the initial deep link URL
|
||||
const linkingUrl = Linking.getLinkingURL()
|
||||
if (linkingUrl) {
|
||||
const urlp = parseLinkingUrl(linkingUrl)
|
||||
const chatInviteMatch = urlp.pathname.match(CHAT_INVITE_CODE_REGEX)
|
||||
if (chatInviteMatch) {
|
||||
setActiveLanding({
|
||||
type: 'groupchat',
|
||||
uri: linkingUrl,
|
||||
code: chatInviteMatch[1],
|
||||
})
|
||||
setReady(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So,
|
||||
// let's just ensure we never check again after the first time.
|
||||
if (hasCheckedForStarterPack) {
|
||||
@@ -29,7 +52,8 @@ export function useStarterPackEntry() {
|
||||
setReady(true)
|
||||
}, 500)
|
||||
|
||||
;(async () => {
|
||||
void (async () => {
|
||||
// Check for starter pack
|
||||
let uri: string | null | undefined
|
||||
|
||||
if (IS_ANDROID) {
|
||||
@@ -58,7 +82,7 @@ export function useStarterPackEntry() {
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [ready, setActiveStarterPack, hasCheckedForStarterPack])
|
||||
}, [ready, setActiveStarterPack, setActiveLanding, hasCheckedForStarterPack])
|
||||
|
||||
return ready
|
||||
}
|
||||
+7
-5
@@ -1,25 +1,27 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
import {httpStarterPackUriToAtUri} from '#/lib/strings/starter-pack'
|
||||
import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
|
||||
import {useSetActiveStarterPack} from '#/state/shell/landing'
|
||||
|
||||
export function useStarterPackEntry() {
|
||||
export function useLandingEntry() {
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
const setActiveStarterPack = useSetActiveStarterPack()
|
||||
|
||||
useEffect(() => {
|
||||
const href = window.location.href
|
||||
const atUri = httpStarterPackUriToAtUri(href)
|
||||
const url = new URL(href)
|
||||
|
||||
// Check for starter pack
|
||||
const atUri = httpStarterPackUriToAtUri(href)
|
||||
if (atUri) {
|
||||
const url = new URL(href)
|
||||
// Determines if an App Clip is loading this landing page
|
||||
const isClip = url.searchParams.get('clip') === 'true'
|
||||
setActiveStarterPack({
|
||||
uri: atUri,
|
||||
isClip,
|
||||
})
|
||||
setReady(true)
|
||||
return
|
||||
}
|
||||
|
||||
setReady(true)
|
||||
@@ -0,0 +1,432 @@
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
ChatBskyGroupRequestJoin,
|
||||
ChatBskyGroupWithdrawJoinRequest,
|
||||
moderateProfile,
|
||||
} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
|
||||
import {useRequestJoinGroupChat} from '#/state/queries/messages/request-join-group-chat'
|
||||
import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {
|
||||
Button,
|
||||
type ButtonColor,
|
||||
ButtonIcon,
|
||||
ButtonText,
|
||||
} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||
import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight'
|
||||
import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink'
|
||||
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
|
||||
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ProfileBadges} from '../ProfileBadges'
|
||||
|
||||
export function GroupChatJoinDialog() {
|
||||
const {groupChatJoinDialogControl, groupChatJoinState} = useIntentDialogs()
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={groupChatJoinDialogControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<GroupChatJoinDialogInner code={groupChatJoinState?.code} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupChatJoinDialogInner({code}: {code?: string}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Join group chat`}
|
||||
style={[web({maxWidth: 400, borderRadius: 36})]}>
|
||||
<View style={[a.gap_2xl, a.align_center]}>
|
||||
<GroupChatJoinDialogContent code={code} />
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupChatJoinDialogContent({code}: {code?: string}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {groupChatJoinDialogControl: control} = useIntentDialogs()
|
||||
const {hasSession} = useSession()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const {data, error, isLoading} = useJoinLinkPreviewsQuery({
|
||||
codes: code ? [code] : undefined,
|
||||
hasSession,
|
||||
})
|
||||
|
||||
const {mutate: joinGroupChat, isPending: isJoinPending} =
|
||||
useRequestJoinGroupChat({
|
||||
onSuccess: data => {
|
||||
switch (data.status) {
|
||||
case 'pending':
|
||||
control.close(() => {
|
||||
Toast.show(
|
||||
l`Access requested! The group owner will review your request.`,
|
||||
)
|
||||
})
|
||||
break
|
||||
case 'joined': {
|
||||
if (data.convo && data.convo.id) {
|
||||
control.close(() => {
|
||||
Toast.show(l`Successfully joined the group chat!`)
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: data.convo!.id,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
logger.warn('Request to join group chat returned no convo ID', {
|
||||
status: data.status,
|
||||
convoId: data.convo?.id,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
let errorMessage = l`Failed to join the group chat. Please try again.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`There was a problem with your internet connection, please try again`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.ConvoLockedError) {
|
||||
errorMessage = l`This conversation is locked.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.FollowRequiredError
|
||||
) {
|
||||
errorMessage = l`Only followers can join this group chat.`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.InvalidCodeError) {
|
||||
errorMessage = l`Invalid group chat code.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.LinkDisabledError
|
||||
) {
|
||||
errorMessage = l`This invite link has been disabled.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.MemberLimitReachedError
|
||||
) {
|
||||
errorMessage = l`The member limit has been reached.`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.UserKickedError) {
|
||||
errorMessage = l`You have been removed from this group.`
|
||||
}
|
||||
Toast.show(errorMessage)
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: withdrawRequest, isPending: isWithdrawPending} =
|
||||
useWithdrawJoinGroupChatRequest({
|
||||
onSuccess: () => {
|
||||
control.close(() => {
|
||||
Toast.show(l`Join request rescinded.`)
|
||||
})
|
||||
},
|
||||
onError: error => {
|
||||
let errorMessage = l`Failed to rescind your request. Please try again.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`There was a problem with your internet connection, please try again`
|
||||
} else if (
|
||||
error instanceof
|
||||
ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError
|
||||
) {
|
||||
errorMessage = l`Invalid rescind request.`
|
||||
}
|
||||
Toast.show(errorMessage)
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
state: interacted,
|
||||
onIn: onInteract,
|
||||
onOut: onInteractOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const handleJoin = () => {
|
||||
if (!code) return
|
||||
joinGroupChat({code})
|
||||
}
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (!convoId) return
|
||||
withdrawRequest({convoId})
|
||||
}
|
||||
|
||||
// Fallback if the prefetch exceeds the timeout
|
||||
if (isLoading || !data || !moderationOpts) {
|
||||
return (
|
||||
<View style={[a.p_2xl]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
|
||||
<Text
|
||||
style={[a.text_center, a.text_lg, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>This invite link is invalid</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={l`Close this dialog`}
|
||||
accessibilityHint={l`Close this dialog`}
|
||||
onPress={() => control.close()}
|
||||
color="primary"
|
||||
size="large"
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const joinLinkPreview = data.joinLinkPreviews[0]
|
||||
|
||||
if (!joinLinkPreview) {
|
||||
return (
|
||||
<>
|
||||
<View style={[a.py_lg, a.align_center]}>
|
||||
<View style={[a.gap_sm, a.align_center, a.mt_lg]}>
|
||||
<WarningIcon size="3xl" fill={t.atoms.text_contrast_high.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Chat invite link no longer available</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
testID="joinButton"
|
||||
onPress={() => control.close()}
|
||||
label={l`Close this dialog`}
|
||||
accessibilityHint={l`Close this dialog`}
|
||||
size="large"
|
||||
color="secondary"
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>{l`Close`}</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const convoId = joinLinkPreview.convo?.id
|
||||
const isFollowing = joinLinkPreview.owner.viewer?.following ?? false
|
||||
const hasRequested = !convoId && joinLinkPreview.viewer?.requestedAt != null
|
||||
|
||||
let canJoin = true
|
||||
let ButtonIconImage = isJoinPending || isWithdrawPending ? Loader : JoinIcon
|
||||
let buttonText = joinLinkPreview.requireApproval
|
||||
? l`Request to join`
|
||||
: l`Join`
|
||||
let buttonColor: ButtonColor = 'primary'
|
||||
if (joinLinkPreview.enabledStatus !== 'enabled') {
|
||||
canJoin = false
|
||||
ButtonIconImage = WarningIcon
|
||||
buttonText = l`Chat invite link no longer available`
|
||||
buttonColor = 'secondary'
|
||||
} else if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) {
|
||||
canJoin = false
|
||||
ButtonIconImage = HandIcon
|
||||
buttonText = l`This chat is full`
|
||||
buttonColor = 'secondary'
|
||||
} else if (joinLinkPreview.joinRule === 'followedByOwner' && !isFollowing) {
|
||||
canJoin = false
|
||||
ButtonIconImage = HandIcon
|
||||
buttonText = l`Only people the chat owner follows can join`
|
||||
buttonColor = 'secondary'
|
||||
} else if (hasRequested) {
|
||||
ButtonIconImage = XIcon
|
||||
buttonText = l`Rescind request`
|
||||
buttonColor = 'secondary'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[a.py_lg, a.align_center]}>
|
||||
<AvatarBubbles
|
||||
profiles={[
|
||||
joinLinkPreview.owner,
|
||||
...Array(joinLinkPreview.memberCount - 1).fill(undefined),
|
||||
]}
|
||||
self
|
||||
size={135}
|
||||
/>
|
||||
<View style={[a.gap_sm, a.align_center, a.mt_lg]}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Group chat</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_center, a.text_3xl, a.font_bold, t.atoms.text]}>
|
||||
{joinLinkPreview.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||
<Trans comment="The number of active group chat members out of the total number allowed.">
|
||||
{joinLinkPreview.memberCount}/{joinLinkPreview.memberLimit}{' '}
|
||||
members
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.ml_md]}>
|
||||
<PersonGroupIcon
|
||||
size="xs"
|
||||
style={[a.mr_xs, t.atoms.text, {marginTop: -2}]}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||
{joinLinkPreview.joinRule === 'followedByOwner'
|
||||
? l`Followers can join`
|
||||
: l`Anyone can join`}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<View
|
||||
style={[a.flex_row, a.gap_xs, a.align_center, a.justify_center]}>
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
By{' '}
|
||||
<InlineLinkText
|
||||
label={`@${joinLinkPreview.owner.handle}`}
|
||||
to={makeProfileLink(joinLinkPreview.owner)}
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_sm,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
interacted && {
|
||||
...web({
|
||||
outline: 0,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: t.palette.contrast_1000,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
{...web({
|
||||
onMouseEnter: () => {
|
||||
onInteract()
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
onInteractOut()
|
||||
},
|
||||
})}>
|
||||
{createSanitizedDisplayName(
|
||||
joinLinkPreview.owner,
|
||||
true,
|
||||
moderateProfile(joinLinkPreview.owner, moderationOpts).ui(
|
||||
'displayName',
|
||||
),
|
||||
)}
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
<ProfileBadges
|
||||
profile={data.joinLinkPreviews[0].owner}
|
||||
size="sm"
|
||||
style={{marginTop: -3}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
{sanitizeHandle(joinLinkPreview.owner.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{convoId ? (
|
||||
<Button
|
||||
testID="openButton"
|
||||
onPress={() => {
|
||||
control.close(() => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: convoId,
|
||||
})
|
||||
})
|
||||
}}
|
||||
label={l`Open group chat`}
|
||||
accessibilityHint={l`Open this group chat`}
|
||||
size="large"
|
||||
color="primary"
|
||||
disabled={!code}
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>Open chat</ButtonText>
|
||||
<ButtonIcon icon={ArrowRightIcon} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
testID="joinButton"
|
||||
onPress={hasRequested ? handleWithdraw : handleJoin}
|
||||
label={
|
||||
joinLinkPreview.requireApproval
|
||||
? l`Request access to group chat`
|
||||
: l`Join group chat`
|
||||
}
|
||||
accessibilityHint={
|
||||
joinLinkPreview.requireApproval
|
||||
? l`Request access to join this group chat`
|
||||
: l`Join this group chat`
|
||||
}
|
||||
size="large"
|
||||
color={buttonColor}
|
||||
disabled={isJoinPending || isWithdrawPending || !code || !canJoin}
|
||||
style={[a.w_full]}>
|
||||
<ButtonIcon icon={ButtonIconImage} />
|
||||
<ButtonText>{buttonText}</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,30 @@
|
||||
import {createContext, useContext, useMemo, useState} from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import {usePrefetchJoinLinkPreviews} from '#/state/queries/join-links'
|
||||
import {useSession} from '#/state/session'
|
||||
import {
|
||||
useActiveGroupChatJoinRequest,
|
||||
useSetActiveLanding,
|
||||
} from '#/state/shell/landing'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {GroupChatJoinDialog} from '#/components/intents/GroupChatJoinDialog'
|
||||
import {VerifyEmailIntentDialog} from '#/components/intents/VerifyEmailIntentDialog'
|
||||
|
||||
interface Context {
|
||||
verifyEmailDialogControl: DialogControlProps
|
||||
verifyEmailState: {code: string} | undefined
|
||||
setVerifyEmailState: (state: {code: string} | undefined) => void
|
||||
groupChatJoinDialogControl: DialogControlProps
|
||||
groupChatJoinState: {code: string} | undefined
|
||||
setGroupChatJoinState: (state: {code: string} | undefined) => void
|
||||
}
|
||||
|
||||
const Context = createContext({} as Context)
|
||||
@@ -19,20 +36,70 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [verifyEmailState, setVerifyEmailState] = useState<
|
||||
{code: string} | undefined
|
||||
>()
|
||||
const groupChatJoinDialogControl = Dialog.useDialogControl()
|
||||
const [groupChatJoinState, setGroupChatJoinState] = useState<
|
||||
{code: string} | undefined
|
||||
>()
|
||||
|
||||
const {hasSession} = useSession()
|
||||
const groupChatLanding = useActiveGroupChatJoinRequest()
|
||||
const setActiveLanding = useSetActiveLanding()
|
||||
const prefetchJoinLinkPreviews = usePrefetchJoinLinkPreviews()
|
||||
const landingHandledRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSession && groupChatLanding && !landingHandledRef.current) {
|
||||
landingHandledRef.current = true
|
||||
const code = groupChatLanding.code
|
||||
setActiveLanding(undefined)
|
||||
const prefetch = prefetchJoinLinkPreviews({
|
||||
codes: [code],
|
||||
hasSession: true,
|
||||
})
|
||||
void Promise.race([
|
||||
prefetch,
|
||||
new Promise(res => setTimeout(res, 200)),
|
||||
]).finally(() => {
|
||||
setGroupChatJoinState({code})
|
||||
groupChatJoinDialogControl.open()
|
||||
})
|
||||
}
|
||||
if (!groupChatLanding) {
|
||||
landingHandledRef.current = false
|
||||
}
|
||||
}, [
|
||||
hasSession,
|
||||
groupChatLanding,
|
||||
setActiveLanding,
|
||||
setGroupChatJoinState,
|
||||
prefetchJoinLinkPreviews,
|
||||
groupChatJoinDialogControl,
|
||||
])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
verifyEmailDialogControl,
|
||||
verifyEmailState,
|
||||
setVerifyEmailState,
|
||||
groupChatJoinDialogControl,
|
||||
groupChatJoinState,
|
||||
setGroupChatJoinState,
|
||||
}),
|
||||
[verifyEmailDialogControl, verifyEmailState, setVerifyEmailState],
|
||||
[
|
||||
verifyEmailDialogControl,
|
||||
verifyEmailState,
|
||||
setVerifyEmailState,
|
||||
groupChatJoinDialogControl,
|
||||
groupChatJoinState,
|
||||
setGroupChatJoinState,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider value={value}>
|
||||
{children}
|
||||
<VerifyEmailIntentDialog />
|
||||
<GroupChatJoinDialog />
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user