[APP-1158] Gate features by email verification state (#8305)

* Use new hook in all locations

* Format

* Seems to work, not great duplication

* Wrap all open composer calls

* Remove unneeded spans

* Missed one

* Fix handler on Conversation

* Gate new chat in header

* Add comment

* Remove whoopsie

* Format

* add hackfix for dialog not showing

* add prompt to accept chat btn

* navigation not necessary

* send back one screen, rather than home

* Update comment

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2025-05-02 15:06:28 -05:00
committed by GitHub
parent 2f9b23c6aa
commit 9fdde1cdf3
30 changed files with 403 additions and 334 deletions
-24
View File
@@ -62,7 +62,6 @@ import {
type SupportedMimeTypes,
} from '#/lib/constants'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useEmail} from '#/lib/hooks/useEmail'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -120,10 +119,6 @@ import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {
EmailDialogScreenID,
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -333,25 +328,6 @@ export const ComposePost = ({
}
}, [onPressCancel, closeAllDialogs, closeAllModals])
const {needsEmailVerification} = useEmail()
const emailDialogControl = useEmailDialogControl()
useEffect(() => {
if (needsEmailVerification) {
emailDialogControl.open({
id: EmailDialogScreenID.Verify,
instructions: [
<Trans key="pre-compose">
Before creating a post, you must first verify your email.
</Trans>,
],
onCloseWithoutVerifying: () => {
onClose()
},
})
}
}, [needsEmailVerification, emailDialogControl, onClose])
const missingAltError = useMemo(() => {
if (!requireAltTextEnabled) {
return
+30 -82
View File
@@ -1,26 +1,19 @@
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {ImagePickerAsset} from 'expo-image-picker'
import {type ImagePickerAsset} from 'expo-image-picker'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
SUPPORTED_MIME_TYPES,
SupportedMimeTypes,
type SupportedMimeTypes,
VIDEO_MAX_DURATION_MS,
} from '#/lib/constants'
import {BSKY_SERVICE} from '#/lib/constants'
import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions'
import {getHostnameFromUrl} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
import * as Prompt from '#/components/Prompt'
import {pickVideo} from './pickVideo'
type Props = {
@@ -33,66 +26,45 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
const {_} = useLingui()
const t = useTheme()
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
const control = Prompt.usePromptControl()
const {currentAccount} = useSession()
const onPressSelectVideo = useCallback(async () => {
if (isNative && !(await requestVideoAccessIfNeeded())) {
return
}
if (
currentAccount &&
!currentAccount.emailConfirmed &&
getHostnameFromUrl(currentAccount.service) ===
getHostnameFromUrl(BSKY_SERVICE)
) {
Keyboard.dismiss()
control.open()
} else {
const response = await pickVideo()
if (response.assets && response.assets.length > 0) {
const asset = response.assets[0]
try {
if (isWeb) {
// asset.duration is null for gifs (see the TODO in pickVideo.web.ts)
if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) {
throw Error(_(msg`Videos must be less than 3 minutes long`))
}
// compression step on native converts to mp4, so no need to check there
if (
!SUPPORTED_MIME_TYPES.includes(
asset.mimeType as SupportedMimeTypes,
)
) {
throw Error(_(msg`Unsupported video type: ${asset.mimeType}`))
}
} else {
if (typeof asset.duration !== 'number') {
throw Error('Asset is not a video')
}
if (asset.duration > VIDEO_MAX_DURATION_MS) {
throw Error(_(msg`Videos must be less than 3 minutes long`))
}
const response = await pickVideo()
if (response.assets && response.assets.length > 0) {
const asset = response.assets[0]
try {
if (isWeb) {
// asset.duration is null for gifs (see the TODO in pickVideo.web.ts)
if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) {
throw Error(_(msg`Videos must be less than 3 minutes long`))
}
onSelectVideo(asset)
} catch (err) {
if (err instanceof Error) {
setError(err.message)
} else {
setError(_(msg`An error occurred while selecting the video`))
// compression step on native converts to mp4, so no need to check there
if (
!SUPPORTED_MIME_TYPES.includes(asset.mimeType as SupportedMimeTypes)
) {
throw Error(_(msg`Unsupported video type: ${asset.mimeType}`))
}
} else {
if (typeof asset.duration !== 'number') {
throw Error('Asset is not a video')
}
if (asset.duration > VIDEO_MAX_DURATION_MS) {
throw Error(_(msg`Videos must be less than 3 minutes long`))
}
}
onSelectVideo(asset)
} catch (err) {
if (err instanceof Error) {
setError(err.message)
} else {
setError(_(msg`An error occurred while selecting the video`))
}
}
}
}, [
requestVideoAccessIfNeeded,
currentAccount,
control,
setError,
_,
onSelectVideo,
])
}, [requestVideoAccessIfNeeded, setError, _, onSelectVideo])
return (
<>
@@ -111,30 +83,6 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
style={disabled && t.atoms.text_contrast_low}
/>
</Button>
<VerifyEmailPrompt control={control} />
</>
)
}
function VerifyEmailPrompt({control}: {control: Prompt.PromptControlProps}) {
const {_} = useLingui()
const verifyEmailDialogControl = useDialogControl()
return (
<>
<Prompt.Basic
control={control}
title={_(msg`Verified email required`)}
description={_(
msg`To upload videos to Bluesky, you must first verify your email.`,
)}
confirmButtonCta={_(msg`Verify now`)}
confirmButtonColor="primary"
onConfirm={() => {
verifyEmailDialogControl.open()
}}
/>
<VerifyEmailDialog control={verifyEmailDialogControl} />
</>
)
}