An ATproto social media client -- with an independent Appview.

Add email verification prompts throughout the app (#6174)

authored by hailey.at and committed by

GitHub 427f3a8b dd8d14e1

+265 -46
+25 -2
src/components/StarterPack/ProfileStarterPacks.tsx
··· 14 15 import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' 16 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 17 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 18 import {NavigationProp} from '#/lib/routes/types' 19 import {parseStarterPackUri} from '#/lib/strings/starter-pack' ··· 27 import {Loader} from '#/components/Loader' 28 import * as Prompt from '#/components/Prompt' 29 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 30 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' 31 32 interface SectionRef { ··· 186 const followersDialogControl = useDialogControl() 187 const errorDialogControl = useDialogControl() 188 189 const [isGenerating, setIsGenerating] = React.useState(false) 190 191 const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ ··· 249 color="primary" 250 size="small" 251 disabled={isGenerating} 252 - onPress={confirmDialogControl.open} 253 style={{backgroundColor: 'transparent'}}> 254 <ButtonText style={{color: 'white'}}> 255 <Trans>Make one for me</Trans> ··· 262 color="primary" 263 size="small" 264 disabled={isGenerating} 265 - onPress={() => navigation.navigate('StarterPackWizard')} 266 style={{ 267 backgroundColor: 'white', 268 borderColor: 'white', ··· 317 )} 318 onConfirm={generate} 319 confirmButtonCta={_(msg`Retry`)} 320 /> 321 </LinearGradientBackground> 322 )
··· 14 15 import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' 16 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 17 + import {useEmail} from '#/lib/hooks/useEmail' 18 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 19 import {NavigationProp} from '#/lib/routes/types' 20 import {parseStarterPackUri} from '#/lib/strings/starter-pack' ··· 28 import {Loader} from '#/components/Loader' 29 import * as Prompt from '#/components/Prompt' 30 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 31 + import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' 32 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' 33 34 interface SectionRef { ··· 188 const followersDialogControl = useDialogControl() 189 const errorDialogControl = useDialogControl() 190 191 + const {needsEmailVerification} = useEmail() 192 + const verifyEmailControl = useDialogControl() 193 + 194 const [isGenerating, setIsGenerating] = React.useState(false) 195 196 const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ ··· 254 color="primary" 255 size="small" 256 disabled={isGenerating} 257 + onPress={() => { 258 + if (needsEmailVerification) { 259 + verifyEmailControl.open() 260 + } else { 261 + confirmDialogControl.open() 262 + } 263 + }} 264 style={{backgroundColor: 'transparent'}}> 265 <ButtonText style={{color: 'white'}}> 266 <Trans>Make one for me</Trans> ··· 273 color="primary" 274 size="small" 275 disabled={isGenerating} 276 + onPress={() => { 277 + if (needsEmailVerification) { 278 + verifyEmailControl.open() 279 + } else { 280 + navigation.navigate('StarterPackWizard') 281 + } 282 + }} 283 style={{ 284 backgroundColor: 'white', 285 borderColor: 'white', ··· 334 )} 335 onConfirm={generate} 336 confirmButtonCta={_(msg`Retry`)} 337 + /> 338 + <VerifyEmailDialog 339 + reasonText={_( 340 + msg`Before creating a starter pack, you must first verify your email.`, 341 + )} 342 + control={verifyEmailControl} 343 /> 344 </LinearGradientBackground> 345 )
+41 -21
src/components/dialogs/VerifyEmailDialog.tsx
··· 18 19 export function VerifyEmailDialog({ 20 control, 21 }: { 22 control: Dialog.DialogControlProps 23 }) { 24 const agent = useAgent() 25 ··· 30 control={control} 31 onClose={async () => { 32 if (!didVerify) { 33 return 34 } 35 36 try { 37 await agent.resumeSession(agent.session!) 38 } catch (e: unknown) { 39 logger.error(String(e)) 40 return 41 } 42 }}> 43 <Dialog.Handle /> 44 - <Inner control={control} setDidVerify={setDidVerify} /> 45 </Dialog.Outer> 46 ) 47 } ··· 49 export function Inner({ 50 control, 51 setDidVerify, 52 }: { 53 control: Dialog.DialogControlProps 54 setDidVerify: (value: boolean) => void 55 }) { 56 const {_} = useLingui() 57 const {currentAccount} = useSession() ··· 135 <Text style={[a.text_md, a.leading_snug]}> 136 {currentStep === 'StepOne' ? ( 137 <> 138 - <Trans> 139 - You'll receive an email at{' '} 140 - <Text style={[a.text_md, a.leading_snug, a.font_bold]}> 141 - {currentAccount?.email} 142 - </Text>{' '} 143 - to verify it's you. 144 - </Trans>{' '} 145 - <InlineLinkText 146 - to="#" 147 - label={_(msg`Change email address`)} 148 - style={[a.text_md, a.leading_snug]} 149 - onPress={e => { 150 - e.preventDefault() 151 - control.close(() => { 152 - openModal({name: 'change-email'}) 153 - }) 154 - return false 155 - }}> 156 - <Trans>Need to change it?</Trans> 157 - </InlineLinkText> 158 </> 159 ) : ( 160 uiStrings[currentStep].message
··· 18 19 export function VerifyEmailDialog({ 20 control, 21 + onCloseWithoutVerifying, 22 + onCloseAfterVerifying, 23 + reasonText, 24 }: { 25 control: Dialog.DialogControlProps 26 + onCloseWithoutVerifying?: () => void 27 + onCloseAfterVerifying?: () => void 28 + reasonText?: string 29 }) { 30 const agent = useAgent() 31 ··· 36 control={control} 37 onClose={async () => { 38 if (!didVerify) { 39 + onCloseWithoutVerifying?.() 40 return 41 } 42 43 try { 44 await agent.resumeSession(agent.session!) 45 + onCloseAfterVerifying?.() 46 } catch (e: unknown) { 47 logger.error(String(e)) 48 return 49 } 50 }}> 51 <Dialog.Handle /> 52 + <Inner 53 + control={control} 54 + setDidVerify={setDidVerify} 55 + reasonText={reasonText} 56 + /> 57 </Dialog.Outer> 58 ) 59 } ··· 61 export function Inner({ 62 control, 63 setDidVerify, 64 + reasonText, 65 }: { 66 control: Dialog.DialogControlProps 67 setDidVerify: (value: boolean) => void 68 + reasonText?: string 69 }) { 70 const {_} = useLingui() 71 const {currentAccount} = useSession() ··· 149 <Text style={[a.text_md, a.leading_snug]}> 150 {currentStep === 'StepOne' ? ( 151 <> 152 + {!reasonText ? ( 153 + <> 154 + <Trans> 155 + You'll receive an email at{' '} 156 + <Text style={[a.text_md, a.leading_snug, a.font_bold]}> 157 + {currentAccount?.email} 158 + </Text>{' '} 159 + to verify it's you. 160 + </Trans>{' '} 161 + <InlineLinkText 162 + to="#" 163 + label={_(msg`Change email address`)} 164 + style={[a.text_md, a.leading_snug]} 165 + onPress={e => { 166 + e.preventDefault() 167 + control.close(() => { 168 + openModal({name: 'change-email'}) 169 + }) 170 + return false 171 + }}> 172 + <Trans>Need to change it?</Trans> 173 + </InlineLinkText> 174 + </> 175 + ) : ( 176 + reasonText 177 + )} 178 </> 179 ) : ( 180 uiStrings[currentStep].message
+41 -15
src/components/dms/MessageProfileButton.tsx
··· 3 import {AppBskyActorDefs} from '@atproto/api' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {logEvent} from '#/lib/statsig/statsig' 8 import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' 9 import {atoms as a, useTheme} from '#/alf' 10 - import {ButtonIcon} from '#/components/Button' 11 import {canBeMessaged} from '#/components/dms/util' 12 import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' 13 - import {Link} from '#/components/Link' 14 15 export function MessageProfileButton({ 16 profile, ··· 19 }) { 20 const {_} = useLingui() 21 const t = useTheme() 22 23 const {data: convo, isPending} = useMaybeConvoForUser(profile.did) 24 25 const onPress = React.useCallback(() => { 26 if (convo && !convo.lastMessage) { 27 logEvent('chat:create', {logContext: 'ProfileHeader'}) 28 } 29 logEvent('chat:open', {logContext: 'ProfileHeader'}) 30 - }, [convo]) 31 32 if (isPending) { 33 // show pending state based on declaration ··· 53 54 if (convo) { 55 return ( 56 - <Link 57 - testID="dmBtn" 58 - size="small" 59 - color="secondary" 60 - variant="solid" 61 - shape="round" 62 - label={_(msg`Message ${profile.handle}`)} 63 - to={`/messages/${convo.id}`} 64 - style={[a.justify_center]} 65 - onPress={onPress}> 66 - <ButtonIcon icon={Message} size="md" /> 67 - </Link> 68 ) 69 } else { 70 return null
··· 3 import {AppBskyActorDefs} from '@atproto/api' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 + import {useNavigation} from '@react-navigation/native' 7 8 + import {useEmail} from '#/lib/hooks/useEmail' 9 + import {NavigationProp} from '#/lib/routes/types' 10 import {logEvent} from '#/lib/statsig/statsig' 11 import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' 12 import {atoms as a, useTheme} from '#/alf' 13 + import {Button, ButtonIcon} from '#/components/Button' 14 import {canBeMessaged} from '#/components/dms/util' 15 import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' 16 + import {useDialogControl} from '../Dialog' 17 + import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' 18 19 export function MessageProfileButton({ 20 profile, ··· 23 }) { 24 const {_} = useLingui() 25 const t = useTheme() 26 + const navigation = useNavigation<NavigationProp>() 27 + const {needsEmailVerification} = useEmail() 28 + const verifyEmailControl = useDialogControl() 29 30 const {data: convo, isPending} = useMaybeConvoForUser(profile.did) 31 32 const onPress = React.useCallback(() => { 33 + if (!convo?.id) { 34 + return 35 + } 36 + 37 + if (needsEmailVerification) { 38 + verifyEmailControl.open() 39 + return 40 + } 41 + 42 if (convo && !convo.lastMessage) { 43 logEvent('chat:create', {logContext: 'ProfileHeader'}) 44 } 45 logEvent('chat:open', {logContext: 'ProfileHeader'}) 46 + 47 + navigation.navigate('MessagesConversation', {conversation: convo.id}) 48 + }, [needsEmailVerification, verifyEmailControl, convo, navigation]) 49 50 if (isPending) { 51 // show pending state based on declaration ··· 71 72 if (convo) { 73 return ( 74 + <> 75 + <Button 76 + accessibilityRole="button" 77 + testID="dmBtn" 78 + size="small" 79 + color="secondary" 80 + variant="solid" 81 + shape="round" 82 + label={_(msg`Message ${profile.handle}`)} 83 + style={[a.justify_center]} 84 + onPress={onPress}> 85 + <ButtonIcon icon={Message} size="md" /> 86 + </Button> 87 + <VerifyEmailDialog 88 + reasonText={_( 89 + msg`Before you may message another user, you must first verify your email.`, 90 + )} 91 + control={verifyEmailControl} 92 + /> 93 + </> 94 ) 95 } else { 96 return null
+19 -1
src/components/dms/dialogs/NewChatDialog.tsx
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 import {logEvent} from '#/lib/statsig/statsig' 6 import {logger} from '#/logger' 7 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' ··· 9 import * as Toast from '#/view/com/util/Toast' 10 import {useTheme} from '#/alf' 11 import * as Dialog from '#/components/Dialog' 12 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 13 import {SearchablePeopleList} from './SearchablePeopleList' 14 ··· 21 }) { 22 const t = useTheme() 23 const {_} = useLingui() 24 25 const {mutate: createChat} = useGetConvoForMembers({ 26 onSuccess: data => { ··· 48 <> 49 <FAB 50 testID="newChatFAB" 51 - onPress={control.open} 52 icon={<Plus size="lg" fill={t.palette.white} />} 53 accessibilityRole="button" 54 accessibilityLabel={_(msg`New chat`)} ··· 62 onSelectChat={onCreateChat} 63 /> 64 </Dialog.Outer> 65 </> 66 ) 67 }
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 + import {useEmail} from '#/lib/hooks/useEmail' 6 import {logEvent} from '#/lib/statsig/statsig' 7 import {logger} from '#/logger' 8 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' ··· 10 import * as Toast from '#/view/com/util/Toast' 11 import {useTheme} from '#/alf' 12 import * as Dialog from '#/components/Dialog' 13 + import {useDialogControl} from '#/components/Dialog' 14 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 15 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 16 import {SearchablePeopleList} from './SearchablePeopleList' 17 ··· 24 }) { 25 const t = useTheme() 26 const {_} = useLingui() 27 + const {needsEmailVerification} = useEmail() 28 + const verifyEmailControl = useDialogControl() 29 30 const {mutate: createChat} = useGetConvoForMembers({ 31 onSuccess: data => { ··· 53 <> 54 <FAB 55 testID="newChatFAB" 56 + onPress={() => { 57 + if (needsEmailVerification) { 58 + verifyEmailControl.open() 59 + } else { 60 + control.open() 61 + } 62 + }} 63 icon={<Plus size="lg" fill={t.palette.white} />} 64 accessibilityRole="button" 65 accessibilityLabel={_(msg`New chat`)} ··· 73 onSelectChat={onCreateChat} 74 /> 75 </Dialog.Outer> 76 + 77 + <VerifyEmailDialog 78 + reasonText={_( 79 + msg`Before you may message another user, you must first verify your email.`, 80 + )} 81 + control={verifyEmailControl} 82 + /> 83 </> 84 ) 85 }
+19
src/lib/hooks/useEmail.ts
···
··· 1 + import {useServiceConfigQuery} from '#/state/queries/email-verification-required' 2 + import {useSession} from '#/state/session' 3 + import {BSKY_SERVICE} from '../constants' 4 + import {getHostnameFromUrl} from '../strings/url-helpers' 5 + 6 + export function useEmail() { 7 + const {currentAccount} = useSession() 8 + 9 + const {data: serviceConfig} = useServiceConfigQuery() 10 + 11 + const isSelfHost = 12 + serviceConfig?.checkEmailConfirmed && 13 + currentAccount && 14 + getHostnameFromUrl(currentAccount.service) !== 15 + getHostnameFromUrl(BSKY_SERVICE) 16 + const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed 17 + 18 + return {needsEmailVerification} 19 + }
+24 -2
src/screens/Messages/Conversation.tsx
··· 4 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 - import {useFocusEffect} from '@react-navigation/native' 8 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 10 - import {CommonNavigatorParams} from '#/lib/routes/types' 11 import {isWeb} from '#/platform/detection' 12 import {useProfileShadow} from '#/state/cache/profile-shadow' 13 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' ··· 19 import {CenteredView} from '#/view/com/util/Views' 20 import {MessagesList} from '#/screens/Messages/components/MessagesList' 21 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 23 import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 24 import {Error} from '#/components/Error' ··· 161 hasScrolled: boolean 162 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 163 }) { 164 const convoState = useConvo() 165 const recipient = useProfileShadow(recipientUnshadowed) 166 167 const moderation = React.useMemo(() => { 168 return moderateProfile(recipient, moderationOpts) ··· 178 userBlock, 179 } 180 }, [moderation]) 181 182 return ( 183 <> ··· 201 } 202 /> 203 )} 204 </> 205 ) 206 }
··· 4 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 + import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 10 + import {useEmail} from '#/lib/hooks/useEmail' 11 + import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 12 import {isWeb} from '#/platform/detection' 13 import {useProfileShadow} from '#/state/cache/profile-shadow' 14 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' ··· 20 import {CenteredView} from '#/view/com/util/Views' 21 import {MessagesList} from '#/screens/Messages/components/MessagesList' 22 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 23 + import {useDialogControl} from '#/components/Dialog' 24 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 25 import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 26 import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 27 import {Error} from '#/components/Error' ··· 164 hasScrolled: boolean 165 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 166 }) { 167 + const {_} = useLingui() 168 const convoState = useConvo() 169 + const navigation = useNavigation<NavigationProp>() 170 const recipient = useProfileShadow(recipientUnshadowed) 171 + const verifyEmailControl = useDialogControl() 172 + const {needsEmailVerification} = useEmail() 173 174 const moderation = React.useMemo(() => { 175 return moderateProfile(recipient, moderationOpts) ··· 185 userBlock, 186 } 187 }, [moderation]) 188 + 189 + React.useEffect(() => { 190 + if (needsEmailVerification) { 191 + verifyEmailControl.open() 192 + } 193 + }, [needsEmailVerification, verifyEmailControl]) 194 195 return ( 196 <> ··· 214 } 215 /> 216 )} 217 + <VerifyEmailDialog 218 + reasonText={_( 219 + msg`Before you may message another user, you must first verify your email.`, 220 + )} 221 + control={verifyEmailControl} 222 + onCloseWithoutVerifying={() => { 223 + navigation.navigate('Home') 224 + }} 225 + /> 226 </> 227 ) 228 }
+10 -1
src/screens/Messages/components/MessageInput.tsx
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 import {isIOS} from '#/platform/detection' 22 import { 23 useMessageDraft, ··· 61 const [message, setMessage] = React.useState(getDraft) 62 const inputRef = useAnimatedRef<TextInput>() 63 64 useSaveMessageDraft(message) 65 useExtractEmbedFromFacets(message, setEmbed) 66 67 const onSubmit = React.useCallback(() => { 68 if (!hasEmbed && message.trim() === '') { 69 return 70 } ··· 84 inputRef.current?.focus() 85 }, 100) 86 }, [ 87 hasEmbed, 88 message, 89 clearDraft, ··· 159 ref={inputRef} 160 hitSlop={HITSLOP_10} 161 animatedProps={animatedProps} 162 /> 163 <Pressable 164 accessibilityRole="button" ··· 171 a.justify_center, 172 {height: 30, width: 30, backgroundColor: t.palette.primary_500}, 173 ]} 174 - onPress={onSubmit}> 175 <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} /> 176 </Pressable> 177 </View>
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 + import {useEmail} from '#/lib/hooks/useEmail' 22 import {isIOS} from '#/platform/detection' 23 import { 24 useMessageDraft, ··· 62 const [message, setMessage] = React.useState(getDraft) 63 const inputRef = useAnimatedRef<TextInput>() 64 65 + const {needsEmailVerification} = useEmail() 66 + 67 useSaveMessageDraft(message) 68 useExtractEmbedFromFacets(message, setEmbed) 69 70 const onSubmit = React.useCallback(() => { 71 + if (needsEmailVerification) { 72 + return 73 + } 74 if (!hasEmbed && message.trim() === '') { 75 return 76 } ··· 90 inputRef.current?.focus() 91 }, 100) 92 }, [ 93 + needsEmailVerification, 94 hasEmbed, 95 message, 96 clearDraft, ··· 166 ref={inputRef} 167 hitSlop={HITSLOP_10} 168 animatedProps={animatedProps} 169 + editable={!needsEmailVerification} 170 /> 171 <Pressable 172 accessibilityRole="button" ··· 179 a.justify_center, 180 {height: 30, width: 30, backgroundColor: t.palette.primary_500}, 181 ]} 182 + onPress={onSubmit} 183 + disabled={needsEmailVerification}> 184 <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} /> 185 </Pressable> 186 </View>
+25
src/state/queries/email-verification-required.ts
···
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + interface ServiceConfig { 4 + checkEmailConfirmed: boolean 5 + } 6 + 7 + export function useServiceConfigQuery() { 8 + return useQuery({ 9 + queryKey: ['service-config'], 10 + queryFn: async () => { 11 + const res = await fetch( 12 + 'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig', 13 + ) 14 + if (!res.ok) { 15 + return { 16 + checkEmailConfirmed: false, 17 + } 18 + } 19 + 20 + const json = await res.json() 21 + return json as ServiceConfig 22 + }, 23 + staleTime: 5 * 60 * 1000, 24 + }) 25 + }
+21
src/view/com/composer/Composer.tsx
··· 58 import {until} from '#/lib/async/until' 59 import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' 60 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 61 import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 62 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 63 import {usePalette} from '#/lib/hooks/usePalette' ··· 110 import {UserAvatar} from '#/view/com/util/UserAvatar' 111 import {atoms as a, native, useTheme} from '#/alf' 112 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 113 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 114 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 115 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 296 backHandler.remove() 297 } 298 }, [onPressCancel, closeAllDialogs, closeAllModals]) 299 300 const missingAltError = useMemo(() => { 301 if (!requireAltTextEnabled) { ··· 570 const isWebFooterSticky = !isNative && thread.posts.length > 1 571 return ( 572 <BottomSheetPortalProvider> 573 <KeyboardAvoidingView 574 testID="composePostView" 575 behavior={isIOS ? 'padding' : 'height'}
··· 58 import {until} from '#/lib/async/until' 59 import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' 60 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 61 + import {useEmail} from '#/lib/hooks/useEmail' 62 import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 63 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 64 import {usePalette} from '#/lib/hooks/usePalette' ··· 111 import {UserAvatar} from '#/view/com/util/UserAvatar' 112 import {atoms as a, native, useTheme} from '#/alf' 113 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 114 + import {useDialogControl} from '#/components/Dialog' 115 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 116 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 117 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 118 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 299 backHandler.remove() 300 } 301 }, [onPressCancel, closeAllDialogs, closeAllModals]) 302 + 303 + const {needsEmailVerification} = useEmail() 304 + const emailVerificationControl = useDialogControl() 305 + 306 + useEffect(() => { 307 + if (needsEmailVerification) { 308 + emailVerificationControl.open() 309 + } 310 + }, [needsEmailVerification, emailVerificationControl]) 311 312 const missingAltError = useMemo(() => { 313 if (!requireAltTextEnabled) { ··· 582 const isWebFooterSticky = !isNative && thread.posts.length > 1 583 return ( 584 <BottomSheetPortalProvider> 585 + <VerifyEmailDialog 586 + control={emailVerificationControl} 587 + onCloseWithoutVerifying={() => { 588 + onClose() 589 + }} 590 + reasonText={_( 591 + msg`Before creating a post, you must first verify your email.`, 592 + )} 593 + /> 594 <KeyboardAvoidingView 595 testID="composePostView" 596 behavior={isIOS ? 'padding' : 'height'}
+20 -2
src/view/screens/Lists.tsx
··· 2 import {StyleSheet, View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {Trans} from '@lingui/macro' 6 import {useFocusEffect, useNavigation} from '@react-navigation/native' 7 8 import {usePalette} from '#/lib/hooks/usePalette' 9 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 16 import {Button} from '#/view/com/util/forms/Button' 17 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 18 import {Text} from '#/view/com/util/text/Text' 19 import * as Layout from '#/components/Layout' 20 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 22 export function ListsScreen({}: Props) { 23 const pal = usePalette('default') 24 const setMinimalShellMode = useSetMinimalShellMode() 25 const {isMobile} = useWebMediaQueries() 26 const navigation = useNavigation<NavigationProp>() 27 const {openModal} = useModalControls() 28 29 useFocusEffect( 30 React.useCallback(() => { ··· 33 ) 34 35 const onPressNewList = React.useCallback(() => { 36 openModal({ 37 name: 'create-or-edit-list', 38 purpose: 'app.bsky.graph.defs#curatelist', ··· 46 } catch {} 47 }, 48 }) 49 - }, [openModal, navigation]) 50 51 return ( 52 <Layout.Screen testID="listsScreen"> ··· 87 </View> 88 </SimpleViewHeader> 89 <MyLists filter="curate" style={s.flexGrow1} /> 90 </Layout.Screen> 91 ) 92 }
··· 2 import {StyleSheet, View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 9 + import {useEmail} from '#/lib/hooks/useEmail' 10 import {usePalette} from '#/lib/hooks/usePalette' 11 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 18 import {Button} from '#/view/com/util/forms/Button' 19 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 20 import {Text} from '#/view/com/util/text/Text' 21 + import {useDialogControl} from '#/components/Dialog' 22 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 23 import * as Layout from '#/components/Layout' 24 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 26 export function ListsScreen({}: Props) { 27 + const {_} = useLingui() 28 const pal = usePalette('default') 29 const setMinimalShellMode = useSetMinimalShellMode() 30 const {isMobile} = useWebMediaQueries() 31 const navigation = useNavigation<NavigationProp>() 32 const {openModal} = useModalControls() 33 + const {needsEmailVerification} = useEmail() 34 + const control = useDialogControl() 35 36 useFocusEffect( 37 React.useCallback(() => { ··· 40 ) 41 42 const onPressNewList = React.useCallback(() => { 43 + if (needsEmailVerification) { 44 + control.open() 45 + return 46 + } 47 + 48 openModal({ 49 name: 'create-or-edit-list', 50 purpose: 'app.bsky.graph.defs#curatelist', ··· 58 } catch {} 59 }, 60 }) 61 + }, [needsEmailVerification, control, openModal, navigation]) 62 63 return ( 64 <Layout.Screen testID="listsScreen"> ··· 99 </View> 100 </SimpleViewHeader> 101 <MyLists filter="curate" style={s.flexGrow1} /> 102 + <VerifyEmailDialog 103 + reasonText={_( 104 + msg`Before creating a list, you must first verify your email.`, 105 + )} 106 + control={control} 107 + /> 108 </Layout.Screen> 109 ) 110 }
+20 -2
src/view/screens/ModerationModlists.tsx
··· 2 import {View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {Trans} from '@lingui/macro' 6 import {useFocusEffect, useNavigation} from '@react-navigation/native' 7 8 import {usePalette} from '#/lib/hooks/usePalette' 9 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 16 import {Button} from '#/view/com/util/forms/Button' 17 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 18 import {Text} from '#/view/com/util/text/Text' 19 import * as Layout from '#/components/Layout' 20 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 22 export function ModerationModlistsScreen({}: Props) { 23 const pal = usePalette('default') 24 const setMinimalShellMode = useSetMinimalShellMode() 25 const {isMobile} = useWebMediaQueries() 26 const navigation = useNavigation<NavigationProp>() 27 const {openModal} = useModalControls() 28 29 useFocusEffect( 30 React.useCallback(() => { ··· 33 ) 34 35 const onPressNewList = React.useCallback(() => { 36 openModal({ 37 name: 'create-or-edit-list', 38 purpose: 'app.bsky.graph.defs#modlist', ··· 46 } catch {} 47 }, 48 }) 49 - }, [openModal, navigation]) 50 51 return ( 52 <Layout.Screen testID="moderationModlistsScreen"> ··· 83 </View> 84 </SimpleViewHeader> 85 <MyLists filter="mod" style={s.flexGrow1} /> 86 </Layout.Screen> 87 ) 88 }
··· 2 import {View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 9 + import {useEmail} from '#/lib/hooks/useEmail' 10 import {usePalette} from '#/lib/hooks/usePalette' 11 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 18 import {Button} from '#/view/com/util/forms/Button' 19 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 20 import {Text} from '#/view/com/util/text/Text' 21 + import {useDialogControl} from '#/components/Dialog' 22 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 23 import * as Layout from '#/components/Layout' 24 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 26 export function ModerationModlistsScreen({}: Props) { 27 + const {_} = useLingui() 28 const pal = usePalette('default') 29 const setMinimalShellMode = useSetMinimalShellMode() 30 const {isMobile} = useWebMediaQueries() 31 const navigation = useNavigation<NavigationProp>() 32 const {openModal} = useModalControls() 33 + const {needsEmailVerification} = useEmail() 34 + const control = useDialogControl() 35 36 useFocusEffect( 37 React.useCallback(() => { ··· 40 ) 41 42 const onPressNewList = React.useCallback(() => { 43 + if (needsEmailVerification) { 44 + control.open() 45 + return 46 + } 47 + 48 openModal({ 49 name: 'create-or-edit-list', 50 purpose: 'app.bsky.graph.defs#modlist', ··· 58 } catch {} 59 }, 60 }) 61 + }, [needsEmailVerification, control, openModal, navigation]) 62 63 return ( 64 <Layout.Screen testID="moderationModlistsScreen"> ··· 95 </View> 96 </SimpleViewHeader> 97 <MyLists filter="mod" style={s.flexGrow1} /> 98 + <VerifyEmailDialog 99 + reasonText={_( 100 + msg`Before creating a list, you must first verify your email.`, 101 + )} 102 + control={control} 103 + /> 104 </Layout.Screen> 105 ) 106 }