[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
@@ -18,7 +18,7 @@ import {useNavigation} from '@react-navigation/native'
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useEmail} from '#/lib/hooks/useEmail' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {parseStarterPackUri} from '#/lib/strings/starter-pack' import {parseStarterPackUri} from '#/lib/strings/starter-pack'
@@ -29,7 +29,6 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {atoms as a, ios, useTheme} from '#/alf' import {atoms as a, ios, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
@@ -196,9 +195,7 @@ function Empty() {
const confirmDialogControl = useDialogControl() const confirmDialogControl = useDialogControl()
const followersDialogControl = useDialogControl() const followersDialogControl = useDialogControl()
const errorDialogControl = useDialogControl() const errorDialogControl = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const [isGenerating, setIsGenerating] = useState(false) const [isGenerating, setIsGenerating] = useState(false)
@@ -229,6 +226,27 @@ function Empty() {
generateStarterPack() generateStarterPack()
} }
const openConfirmDialog = useCallback(() => {
confirmDialogControl.open()
}, [confirmDialogControl])
const wrappedOpenConfirmDialog = requireEmailVerification(openConfirmDialog, {
instructions: [
<Trans key="confirm">
Before creating a starter pack, you must first verify your email.
</Trans>,
],
})
const navToWizard = useCallback(() => {
navigation.navigate('StarterPackWizard')
}, [navigation])
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
instructions: [
<Trans key="nav">
Before creating a starter pack, you must first verify your email.
</Trans>,
],
})
return ( return (
<LinearGradientBackground <LinearGradientBackground
style={[ style={[
@@ -257,13 +275,7 @@ function Empty() {
color="primary" color="primary"
size="small" size="small"
disabled={isGenerating} disabled={isGenerating}
onPress={() => { onPress={wrappedOpenConfirmDialog}
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
confirmDialogControl.open()
}
}}
style={{backgroundColor: 'transparent'}}> style={{backgroundColor: 'transparent'}}>
<ButtonText style={{color: 'white'}}> <ButtonText style={{color: 'white'}}>
<Trans>Make one for me</Trans> <Trans>Make one for me</Trans>
@@ -276,13 +288,7 @@ function Empty() {
color="primary" color="primary"
size="small" size="small"
disabled={isGenerating} disabled={isGenerating}
onPress={() => { onPress={wrappedNavToWizard}
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
navigation.navigate('StarterPackWizard')
}
}}
style={{ style={{
backgroundColor: 'white', backgroundColor: 'white',
borderColor: 'white', borderColor: 'white',
@@ -338,12 +344,6 @@ function Empty() {
onConfirm={generate} onConfirm={generate}
confirmButtonCta={_(msg`Retry`)} confirmButtonCta={_(msg`Retry`)}
/> />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a starter pack, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</LinearGradientBackground> </LinearGradientBackground>
) )
} }
+15 -28
View File
@@ -1,20 +1,18 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {type AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability' import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {canBeMessaged} from '#/components/dms/util' import {canBeMessaged} from '#/components/dms/util'
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
@@ -26,8 +24,7 @@ export function MessageProfileButton({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail() const requireEmailVerification = useRequireEmailVerification()
const verifyEmailControl = useDialogControl()
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did) const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
const {mutate: initiateConvo} = useGetConvoForMembers({ const {mutate: initiateConvo} = useGetConvoForMembers({
@@ -45,11 +42,6 @@ export function MessageProfileButton({
return return
} }
if (needsEmailVerification) {
verifyEmailControl.open()
return
}
if (convoAvailability.convo) { if (convoAvailability.convo) {
logEvent('chat:open', {logContext: 'ProfileHeader'}) logEvent('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', { navigation.navigate('MessagesConversation', {
@@ -59,14 +51,15 @@ export function MessageProfileButton({
logEvent('chat:create', {logContext: 'ProfileHeader'}) logEvent('chat:create', {logContext: 'ProfileHeader'})
initiateConvo([profile.did]) initiateConvo([profile.did])
} }
}, [ }, [navigation, profile.did, initiateConvo, convoAvailability])
needsEmailVerification,
verifyEmailControl, const wrappedOnPress = requireEmailVerification(onPress, {
navigation, instructions: [
profile.did, <Trans key="message">
initiateConvo, Before you may message another user, you must first verify your email.
convoAvailability, </Trans>,
]) ],
})
if (!convoAvailability) { if (!convoAvailability) {
// show pending state based on declaration // show pending state based on declaration
@@ -102,15 +95,9 @@ export function MessageProfileButton({
shape="round" shape="round"
label={_(msg`Message ${profile.handle}`)} label={_(msg`Message ${profile.handle}`)}
style={[a.justify_center]} style={[a.justify_center]}
onPress={onPress}> onPress={wrappedOnPress}>
<ButtonIcon icon={Message} size="md" /> <ButtonIcon icon={Message} size="md" />
</Button> </Button>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</> </>
) )
} else { } else {
+15 -20
View File
@@ -1,8 +1,8 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {msg} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useEmail} from '#/lib/hooks/useEmail' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
@@ -10,9 +10,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
export function NewChat({ export function NewChat({
@@ -24,8 +22,7 @@ export function NewChat({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {needsEmailVerification} = useEmail() const requireEmailVerification = useRequireEmailVerification()
const verifyEmailControl = useDialogControl()
const {mutate: createChat} = useGetConvoForMembers({ const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => { onSuccess: data => {
@@ -49,17 +46,22 @@ export function NewChat({
[control, createChat], [control, createChat],
) )
const onPress = useCallback(() => {
control.open()
}, [control])
const wrappedOnPress = requireEmailVerification(onPress, {
instructions: [
<Trans key="new-chat">
Before you may message another user, you must first verify your email.
</Trans>,
],
})
return ( return (
<> <>
<FAB <FAB
testID="newChatFAB" testID="newChatFAB"
onPress={() => { onPress={wrappedOnPress}
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
control.open()
}
}}
icon={<Plus size="lg" fill={t.palette.white} />} icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)} accessibilityLabel={_(msg`New chat`)}
@@ -74,13 +76,6 @@ export function NewChat({
sortByMessageDeclaration sortByMessageDeclaration
/> />
</Dialog.Outer> </Dialog.Outer>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</> </>
) )
} }
+2 -2
View File
@@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army' import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
@@ -83,7 +83,7 @@ export function useIntentHandler() {
export function useComposeIntent() { export function useComposeIntent() {
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {hasSession} = useSession() const {hasSession} = useSession()
return React.useCallback( return React.useCallback(
+21
View File
@@ -0,0 +1,21 @@
import {useMemo} from 'react'
import {Trans} from '@lingui/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useOpenComposer as rootUseOpenComposer} from '#/state/shell/composer'
export function useOpenComposer() {
const {openComposer} = rootUseOpenComposer()
const requireEmailVerification = useRequireEmailVerification()
return useMemo(() => {
return {
openComposer: requireEmailVerification(openComposer, {
instructions: [
<Trans key="pre-compose">
Before creating a post, you must first verify your email.
</Trans>,
],
}),
}
}, [openComposer, requireEmailVerification])
}
@@ -0,0 +1,53 @@
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {useEmail} from '#/lib/hooks/useEmail'
import {useRequireAuth, useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
EmailDialogScreenID,
type Screen,
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
export function useRequireEmailVerification() {
const {currentAccount} = useSession()
const {needsEmailVerification} = useEmail()
const requireAuth = useRequireAuth()
const emailDialogControl = useEmailDialogControl()
const closeAll = useCloseAllActiveElements()
return useCallback(
<T extends (...args: any[]) => any>(
cb: T,
config: Omit<
Extract<Screen, {id: EmailDialogScreenID.Verify}>,
'id'
> = {},
): ((...args: Parameters<T>) => ReturnType<T>) => {
return (...args: Parameters<T>): ReturnType<T> => {
if (!currentAccount) {
return requireAuth(() => cb(...args)) as ReturnType<T>
}
if (needsEmailVerification) {
Keyboard.dismiss()
closeAll()
emailDialogControl.open({
id: EmailDialogScreenID.Verify,
...config,
})
return undefined as ReturnType<T>
} else {
return cb(...args)
}
}
},
[
needsEmailVerification,
currentAccount,
emailDialogControl,
closeAll,
requireAuth,
],
)
}
+14 -1
View File
@@ -9,6 +9,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useAppState} from '#/lib/hooks/useAppState' import {useAppState} from '#/lib/hooks/useAppState'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type MessagesTabNavigatorParams} from '#/lib/routes/types' import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -321,6 +322,18 @@ export function MessagesScreen({navigation, route}: Props) {
function Header({newChatControl}: {newChatControl: DialogControlProps}) { function Header({newChatControl}: {newChatControl: DialogControlProps}) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const requireEmailVerification = useRequireEmailVerification()
const openChatControl = useCallback(() => {
newChatControl.open()
}, [newChatControl])
const wrappedOpenChatControl = requireEmailVerification(openChatControl, {
instructions: [
<Trans key="new-chat">
Before you may message another user, you must first verify your email.
</Trans>,
],
})
const settingsLink = ( const settingsLink = (
<Link <Link
@@ -352,7 +365,7 @@ function Header({newChatControl}: {newChatControl: DialogControlProps}) {
color="primary" color="primary"
size="small" size="small"
variant="solid" variant="solid"
onPress={newChatControl.open}> onPress={wrappedOpenChatControl}>
<ButtonIcon icon={PlusIcon} position="left" /> <ButtonIcon icon={PlusIcon} position="left" />
<ButtonText> <ButtonText>
<Trans>New chat</Trans> <Trans>New chat</Trans>
+37 -15
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import React, {useCallback, useEffect} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
@@ -17,6 +17,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useEmail} from '#/lib/hooks/useEmail' import {useEmail} from '#/lib/hooks/useEmail'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NavigationProp, type NavigationProp,
@@ -192,22 +193,43 @@ function InnerReady({
const {needsEmailVerification} = useEmail() const {needsEmailVerification} = useEmail()
const emailDialogControl = useEmailDialogControl() const emailDialogControl = useEmailDialogControl()
React.useEffect(() => { /**
* Must be non-reactive, otherwise the update to open the global dialog will
* cause a re-render loop.
*/
const maybeBlockForEmailVerification = useNonReactiveCallback(() => {
if (needsEmailVerification) { if (needsEmailVerification) {
emailDialogControl.open({ /*
id: EmailDialogScreenID.Verify, * HACKFIX
instructions: [ *
<Trans key="pre-compose"> * Load bearing timeout, to bump this state update until the after the
Before you may message another user, you must first verify your * `navigator.addListener('state')` handler closes elements from
email. * `shell/index.*.tsx` - sfn & esb
</Trans>, */
], setTimeout(() =>
onCloseWithoutVerifying: () => { emailDialogControl.open({
navigation.navigate('Home') id: EmailDialogScreenID.Verify,
}, instructions: [
}) <Trans key="pre-compose">
Before you may message another user, you must first verify your
email.
</Trans>,
],
onCloseWithoutVerifying: () => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Messages', {animation: 'pop'})
}
},
}),
)
} }
}, [needsEmailVerification, emailDialogControl, navigation]) })
useEffect(() => {
maybeBlockForEmailVerification()
}, [maybeBlockForEmailVerification])
return ( return (
<> <>
@@ -1,11 +1,12 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {NavigationProp} from '#/lib/routes/types' import {useEmail} from '#/lib/hooks/useEmail'
import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useAcceptConversation} from '#/state/queries/messages/accept-conversation' import {useAcceptConversation} from '#/state/queries/messages/accept-conversation'
import {precacheConvoQuery} from '#/state/queries/messages/conversation' import {precacheConvoQuery} from '#/state/queries/messages/conversation'
@@ -13,8 +14,17 @@ import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useProfileBlockMutationQueue} from '#/state/queries/profile' import {useProfileBlockMutationQueue} from '#/state/queries/profile'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' import {
Button,
ButtonIcon,
type ButtonProps,
ButtonText,
} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {
EmailDialogScreenID,
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
import {ReportDialog} from '#/components/dms/ReportDialog' import {ReportDialog} from '#/components/dms/ReportDialog'
import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX' import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
@@ -186,6 +196,8 @@ export function AcceptChatButton({
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
const emailDialogControl = useEmailDialogControl()
const {mutate: acceptConvo, isPending} = useAcceptConversation(convo.id, { const {mutate: acceptConvo, isPending} = useAcceptConversation(convo.id, {
onMutate: () => { onMutate: () => {
@@ -216,8 +228,20 @@ export function AcceptChatButton({
}) })
const onPressAccept = useCallback(() => { const onPressAccept = useCallback(() => {
acceptConvo() if (needsEmailVerification) {
}, [acceptConvo]) emailDialogControl.open({
id: EmailDialogScreenID.Verify,
instructions: [
<Trans key="request-btn">
Before you can accept this chat request, you must first verify your
email.
</Trans>,
],
})
} else {
acceptConvo()
}
}, [acceptConvo, needsEmailVerification, emailDialogControl])
return ( return (
<Button <Button
@@ -1,5 +1,5 @@
import {View} from 'react-native' import {View} from 'react-native'
import {ChatBskyConvoDefs} from '@atproto/api' import {type ChatBskyConvoDefs} from '@atproto/api'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
+12 -9
View File
@@ -5,36 +5,39 @@ import {AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useIsFocused, useNavigation} from '@react-navigation/native' import {useIsFocused, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URIS} from '#/lib/constants' import {VIDEO_FEED_URIS} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
import {NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers' import {makeRecordUri} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' type FeedSourceFeedInfo,
useFeedSourceInfoQuery,
} from '#/state/queries/feed'
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import { import {
usePreferencesQuery, usePreferencesQuery,
UsePreferencesQueryResponse, type UsePreferencesQueryResponse,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {PostFeed} from '#/view/com/posts/PostFeed' import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState' import {EmptyState} from '#/view/com/util/EmptyState'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {Button} from '#/view/com/util/forms/Button' import {Button} from '#/view/com/util/forms/Button'
import {ListRef} from '#/view/com/util/List' import {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
@@ -156,7 +159,7 @@ export function ProfileFeedScreenInner({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const {hasSession} = useSession() const {hasSession} = useSession()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
useSetTitle(feedInfo?.displayName) useSetTitle(feedInfo?.displayName)
+24 -16
View File
@@ -1,18 +1,18 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import { import {
LayoutAnimation, LayoutAnimation,
ListRenderItem, type ListRenderItem,
Pressable, Pressable,
ScrollView, ScrollView,
View, View,
ViewabilityConfig, type ViewabilityConfig,
ViewToken, type ViewToken,
} from 'react-native' } from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge' import {SystemBars} from 'react-native-edge-to-edge'
import { import {
Gesture, Gesture,
GestureDetector, GestureDetector,
NativeGesture, type NativeGesture,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import Animated, { import Animated, {
useAnimatedStyle, useAnimatedStyle,
@@ -24,38 +24,46 @@ import {
} from 'react-native-safe-area-context' } from 'react-native-safe-area-context'
import {useEvent} from 'expo' import {useEvent} from 'expo'
import {useEventListener} from 'expo' import {useEventListener} from 'expo'
import {Image, ImageStyle} from 'expo-image' import {Image, type ImageStyle} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import {createVideoPlayer, type VideoPlayer, VideoView} from 'expo-video'
import { import {
AppBskyEmbedVideo, AppBskyEmbedVideo,
AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
ModerationDecision, type ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
RouteProp, type RouteProp,
useFocusEffect, useFocusEffect,
useIsFocused, useIsFocused,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native' } from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isAndroid} from '#/platform/detection' import {isAndroid} from '#/platform/detection'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {
POST_TOMBSTONE,
type Shadow,
usePostShadow,
} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import { import {
FeedFeedbackProvider, FeedFeedbackProvider,
@@ -64,13 +72,13 @@ import {
import {useFeedFeedback} from '#/state/feed-feedback' import {useFeedFeedback} from '#/state/feed-feedback'
import {usePostLikeMutationQueue} from '#/state/queries/post' import {usePostLikeMutationQueue} from '#/state/queries/post'
import { import {
AuthorFilter, type AuthorFilter,
FeedPostSliceItem, type FeedPostSliceItem,
usePostFeedQuery, usePostFeedQuery,
} from '#/state/queries/post-feed' } from '#/state/queries/post-feed'
import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls, useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
@@ -685,7 +693,7 @@ function Overlay({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const seekingAnimationSV = useSharedValue(0) const seekingAnimationSV = useSharedValue(0)
+13 -1
View File
@@ -125,5 +125,17 @@ export function useComposerState() {
} }
export function useComposerControls() { export function useComposerControls() {
return React.useContext(controlsContext) const {closeComposer} = React.useContext(controlsContext)
return React.useMemo(() => ({closeComposer}), [closeComposer])
}
/**
* DO NOT USE DIRECTLY. The deprecation notice as a warning only, it's not
* actually deprecated.
*
* @deprecated use `#/lib/hooks/useOpenComposer` instead
*/
export function useOpenComposer() {
const {openComposer} = React.useContext(controlsContext)
return React.useMemo(() => ({openComposer}), [openComposer])
} }
@@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useDialogStateContext} from '#/state/dialogs' import {useDialogStateContext} from '#/state/dialogs'
import {useLightbox} from '#/state/lightbox' import {useLightbox} from '#/state/lightbox'
import {useModals} from '#/state/modals' import {useModals} from '#/state/modals'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useIsDrawerOpen} from '#/state/shell/drawer-open' import {useIsDrawerOpen} from '#/state/shell/drawer-open'
import {useComposerControls} from './'
/** /**
* Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2} * Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2}
@@ -39,7 +39,7 @@ function shouldIgnore(event: KeyboardEvent) {
} }
export function useComposerKeyboardShortcut() { export function useComposerKeyboardShortcut() {
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {openDialogs} = useDialogStateContext() const {openDialogs} = useDialogStateContext()
const {isModalActive} = useModals() const {isModalActive} = useModals()
const {activeLightbox} = useLightbox() const {activeLightbox} = useLightbox()
+1 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import type React from 'react'
import {Provider as ColorModeProvider} from './color-mode' import {Provider as ColorModeProvider} from './color-mode'
import {Provider as DrawerOpenProvider} from './drawer-open' import {Provider as DrawerOpenProvider} from './drawer-open'
@@ -9,7 +9,6 @@ import {Provider as ShellLayoutProvder} from './shell-layout'
import {Provider as TickEveryMinuteProvider} from './tick-every-minute' import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
export {useSetThemePrefs, useThemePrefs} from './color-mode' export {useSetThemePrefs, useThemePrefs} from './color-mode'
export {useComposerControls, useComposerState} from './composer'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export { export {
useIsDrawerSwipeDisabled, useIsDrawerSwipeDisabled,
-24
View File
@@ -62,7 +62,6 @@ import {
type SupportedMimeTypes, type SupportedMimeTypes,
} from '#/lib/constants' } from '#/lib/constants'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useEmail} from '#/lib/hooks/useEmail'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette' 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 {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme} from '#/alf' import {atoms as a, native, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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 {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -333,25 +328,6 @@ export const ComposePost = ({
} }
}, [onPressCancel, closeAllDialogs, closeAllModals]) }, [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(() => { const missingAltError = useMemo(() => {
if (!requireAltTextEnabled) { if (!requireAltTextEnabled) {
return return
+30 -82
View File
@@ -1,26 +1,19 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {Keyboard} from 'react-native' import {type ImagePickerAsset} from 'expo-image-picker'
import {ImagePickerAsset} from 'expo-image-picker'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
SUPPORTED_MIME_TYPES, SUPPORTED_MIME_TYPES,
SupportedMimeTypes, type SupportedMimeTypes,
VIDEO_MAX_DURATION_MS, VIDEO_MAX_DURATION_MS,
} from '#/lib/constants' } from '#/lib/constants'
import {BSKY_SERVICE} from '#/lib/constants'
import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions' import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions'
import {getHostnameFromUrl} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' 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 {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
import * as Prompt from '#/components/Prompt'
import {pickVideo} from './pickVideo' import {pickVideo} from './pickVideo'
type Props = { type Props = {
@@ -33,66 +26,45 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
const control = Prompt.usePromptControl()
const {currentAccount} = useSession()
const onPressSelectVideo = useCallback(async () => { const onPressSelectVideo = useCallback(async () => {
if (isNative && !(await requestVideoAccessIfNeeded())) { if (isNative && !(await requestVideoAccessIfNeeded())) {
return return
} }
if ( const response = await pickVideo()
currentAccount && if (response.assets && response.assets.length > 0) {
!currentAccount.emailConfirmed && const asset = response.assets[0]
getHostnameFromUrl(currentAccount.service) === try {
getHostnameFromUrl(BSKY_SERVICE) if (isWeb) {
) { // asset.duration is null for gifs (see the TODO in pickVideo.web.ts)
Keyboard.dismiss() if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) {
control.open() throw Error(_(msg`Videos must be less than 3 minutes long`))
} 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`))
}
} }
onSelectVideo(asset) // compression step on native converts to mp4, so no need to check there
} catch (err) { if (
if (err instanceof Error) { !SUPPORTED_MIME_TYPES.includes(asset.mimeType as SupportedMimeTypes)
setError(err.message) ) {
} else { throw Error(_(msg`Unsupported video type: ${asset.mimeType}`))
setError(_(msg`An error occurred while selecting the video`))
} }
} 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, setError, _, onSelectVideo])
requestVideoAccessIfNeeded,
currentAccount,
control,
setError,
_,
onSelectVideo,
])
return ( return (
<> <>
@@ -111,30 +83,6 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
style={disabled && t.atoms.text_contrast_low} style={disabled && t.atoms.text_contrast_low}
/> />
</Button> </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} />
</> </>
) )
} }
+8 -8
View File
@@ -1,32 +1,32 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NavigationProp, useNavigation} from '@react-navigation/native' import {type NavigationProp, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URIS} from '#/lib/constants' import {VIDEO_FEED_URIS} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {AllNavigatorParams} from '#/lib/routes/types' import {type AllNavigatorParams} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {useSetHomeBadge} from '#/state/home-badge' import {useSetHomeBadge} from '#/state/home-badge'
import {SavedFeedSourceInfo} from '#/state/queries/feed' import {type SavedFeedSourceInfo} from '#/state/queries/feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import {PostFeed} from '../posts/PostFeed' import {PostFeed} from '../posts/PostFeed'
import {FAB} from '../util/fab/FAB' import {FAB} from '../util/fab/FAB'
import {ListMethods} from '../util/List' import {type ListMethods} from '../util/List'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '../util/MainScrollProvider' import {MainScrollProvider} from '../util/MainScrollProvider'
@@ -57,7 +57,7 @@ export function FeedPage({
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>() const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const headerOffset = useHeaderOffset() const headerOffset = useHeaderOffset()
+9 -9
View File
@@ -5,7 +5,7 @@ import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
moderatePost, moderatePost,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
@@ -14,6 +14,7 @@ import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
@@ -25,19 +26,18 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
fillThreadModerationCache, fillThreadModerationCache,
sortThread, sortThread,
ThreadBlocked, type ThreadBlocked,
ThreadModerationCache, type ThreadModerationCache,
ThreadNode, type ThreadNode,
ThreadNotFound, type ThreadNotFound,
ThreadPost, type ThreadPost,
usePostThreadQuery, usePostThreadQuery,
} from '#/state/queries/post-thread' } from '#/state/queries/post-thread'
import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences' import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {List, ListMethods} from '#/view/com/util/List' import {List, type ListMethods} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
@@ -394,7 +394,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
[refetch], [refetch],
) )
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
if (thread?.type !== 'post') { if (thread?.type !== 'post') {
return return
+2 -2
View File
@@ -17,6 +17,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useOpenLink} from '#/lib/hooks/useOpenLink' import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
@@ -36,7 +37,6 @@ import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {type ThreadPost} from '#/state/queries/post-thread' import {type ThreadPost} from '#/state/queries/post-thread'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
@@ -204,7 +204,7 @@ let PostThreadItemLoaded = ({
const pal = usePalette('default') const pal = usePalette('default')
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
() => countLines(richText?.text) >= MAX_POST_LINES, () => countLines(richText?.text) >= MAX_POST_LINES,
) )
+10 -6
View File
@@ -1,11 +1,11 @@
import React, {useMemo, useState} from 'react' import React, {useMemo, useState} from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
import { import {
AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
moderatePost, moderatePost,
ModerationDecision, type ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -14,15 +14,19 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {colors, s} from '#/lib/styles' import {colors, s} from '#/lib/styles'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {
POST_TOMBSTONE,
type Shadow,
usePostShadow,
} from '#/state/cache/post-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile' import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {AviFollowButton} from '#/view/com/posts/AviFollowButton' import {AviFollowButton} from '#/view/com/posts/AviFollowButton'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -113,7 +117,7 @@ function PostInner({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
() => countLines(richText?.text) >= MAX_POST_LINES, () => countLines(richText?.text) >= MAX_POST_LINES,
) )
+12 -8
View File
@@ -1,35 +1,39 @@
import React, {memo, useMemo, useState} from 'react' import React, {memo, useMemo, useState} from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import { import {
AppBskyActorDefs, type AppBskyActorDefs,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AtUri, AtUri,
ModerationDecision, type ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, type FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types' import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {
POST_TOMBSTONE,
type Shadow,
usePostShadow,
} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {precacheProfile} from '#/state/queries/profile' import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {FeedNameText} from '#/view/com/util/FeedInfoText' import {FeedNameText} from '#/view/com/util/FeedInfoText'
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
@@ -43,7 +47,7 @@ import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/R
import {ContentHider} from '#/components/moderation/ContentHider' import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {AppModerationCause} from '#/components/Pills' import {type AppModerationCause} from '#/components/Pills'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover' import {SubtleWebHover} from '#/components/SubtleWebHover'
@@ -150,7 +154,7 @@ let FeedItemInner = ({
rootPost: AppBskyFeedDefs.PostView rootPost: AppBskyFeedDefs.PostView
}): React.ReactNode => { }): React.ReactNode => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
+7 -7
View File
@@ -8,11 +8,11 @@ import {
} from 'react-native' } from 'react-native'
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import { import {
AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, type AppBskyFeedPost,
AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
RichText as RichTextAPI, type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg, plural} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -22,18 +22,18 @@ import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants'
import {CountWheel} from '#/lib/custom-animations/CountWheel' import {CountWheel} from '#/lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import { import {
usePostLikeMutationQueue, usePostLikeMutationQueue,
usePostRepostMutationQueue, usePostRepostMutationQueue,
} from '#/state/queries/post' } from '#/state/queries/post'
import {useRequireAuth, useSession} from '#/state/session' import {useRequireAuth, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import { import {
ProgressGuideAction, ProgressGuideAction,
useProgressGuideControls, useProgressGuideControls,
@@ -74,7 +74,7 @@ let PostCtrls = ({
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
+9 -6
View File
@@ -1,30 +1,33 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {isNative, isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import { import {
SavedFeedItem, type SavedFeedItem,
useGetPopularFeedsQuery, useGetPopularFeedsQuery,
useSavedFeeds, useSavedFeeds,
useSearchPopularFeedsMutation, useSearchPopularFeedsMutation,
} from '#/state/queries/feed' } from '#/state/queries/feed'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {List, ListMethods} from '#/view/com/util/List' import {List, type ListMethods} from '#/view/com/util/List'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
@@ -102,7 +105,7 @@ type FlatlistSlice =
export function FeedsScreen(_props: Props) { export function FeedsScreen(_props: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [query, setQuery] = React.useState('') const [query, setQuery] = React.useState('')
const [isPTR, setIsPTR] = React.useState(false) const [isPTR, setIsPTR] = React.useState(false)
+17 -20
View File
@@ -4,16 +4,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {
import {NavigationProp} from '#/lib/routes/types' type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {type NavigationProp} from '#/lib/routes/types'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {MyLists} from '#/view/com/lists/MyLists' import {MyLists} from '#/view/com/lists/MyLists'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
@@ -23,8 +24,7 @@ export function ListsScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail() const requireEmailVerification = useRequireEmailVerification()
const control = useDialogControl()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -33,11 +33,6 @@ export function ListsScreen({}: Props) {
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#curatelist', purpose: 'app.bsky.graph.defs#curatelist',
@@ -51,7 +46,15 @@ export function ListsScreen({}: Props) {
} catch {} } catch {}
}, },
}) })
}, [needsEmailVerification, control, openModal, navigation]) }, [openModal, navigation])
const wrappedOnPressNewList = requireEmailVerification(onPressNewList, {
instructions: [
<Trans key="lists">
Before creating a list, you must first verify your email.
</Trans>,
],
})
return ( return (
<Layout.Screen testID="listsScreen"> <Layout.Screen testID="listsScreen">
@@ -68,7 +71,7 @@ export function ListsScreen({}: Props) {
color="secondary" color="secondary"
variant="solid" variant="solid"
size="small" size="small"
onPress={onPressNewList}> onPress={wrappedOnPressNewList}>
<ButtonIcon icon={PlusIcon} /> <ButtonIcon icon={PlusIcon} />
<ButtonText> <ButtonText>
<Trans context="action">New</Trans> <Trans context="action">New</Trans>
@@ -76,12 +79,6 @@ export function ListsScreen({}: Props) {
</Button> </Button>
</Layout.Header.Outer> </Layout.Header.Outer>
<MyLists filter="curate" style={a.flex_grow} /> <MyLists filter="curate" style={a.flex_grow} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen> </Layout.Screen>
) )
} }
+17 -20
View File
@@ -4,16 +4,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {
import {NavigationProp} from '#/lib/routes/types' type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {type NavigationProp} from '#/lib/routes/types'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {MyLists} from '#/view/com/lists/MyLists' import {MyLists} from '#/view/com/lists/MyLists'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
@@ -23,8 +24,7 @@ export function ModerationModlistsScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail() const requireEmailVerification = useRequireEmailVerification()
const control = useDialogControl()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -33,11 +33,6 @@ export function ModerationModlistsScreen({}: Props) {
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#modlist', purpose: 'app.bsky.graph.defs#modlist',
@@ -51,7 +46,15 @@ export function ModerationModlistsScreen({}: Props) {
} catch {} } catch {}
}, },
}) })
}, [needsEmailVerification, control, openModal, navigation]) }, [openModal, navigation])
const wrappedOnPressNewList = requireEmailVerification(onPressNewList, {
instructions: [
<Trans key="modlist">
Before creating a list, you must first verify your email.
</Trans>,
],
})
return ( return (
<Layout.Screen testID="moderationModlistsScreen"> <Layout.Screen testID="moderationModlistsScreen">
@@ -68,7 +71,7 @@ export function ModerationModlistsScreen({}: Props) {
color="secondary" color="secondary"
variant="solid" variant="solid"
size="small" size="small"
onPress={onPressNewList}> onPress={wrappedOnPressNewList}>
<ButtonIcon icon={PlusIcon} /> <ButtonIcon icon={PlusIcon} />
<ButtonText> <ButtonText>
<Trans context="action">New</Trans> <Trans context="action">New</Trans>
@@ -76,12 +79,6 @@ export function ModerationModlistsScreen({}: Props) {
</Button> </Button>
</Layout.Header.Outer> </Layout.Header.Outer>
<MyLists filter="mod" style={a.flex_grow} /> <MyLists filter="mod" style={a.flex_grow} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen> </Layout.Screen>
) )
} }
+5 -5
View File
@@ -6,10 +6,11 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import { import {
NativeStackScreenProps, type NativeStackScreenProps,
NotificationsTabNavigatorParams, type NotificationsTabNavigatorParams,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -22,12 +23,11 @@ import {
} from '#/state/queries/notifications/unread' } from '#/state/queries/notifications/unread'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {NotificationFeed} from '#/view/com/notifications/NotificationFeed' import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
import {Pager} from '#/view/com/pager/Pager' import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar' import {TabBar} from '#/view/com/pager/TabBar'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {ListMethods} from '#/view/com/util/List' import {type ListMethods} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@@ -49,7 +49,7 @@ type Props = NativeStackScreenProps<
> >
export function NotificationsScreen({}: Props) { export function NotificationsScreen({}: Props) {
const {_} = useLingui() const {_} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const unreadNotifs = useUnreadNotifications() const unreadNotifs = useUnreadNotifications()
const hasNew = !!unreadNotifs const hasNew = !!unreadNotifs
const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi() const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi()
+9 -6
View File
@@ -2,9 +2,9 @@ import React, {useCallback, useMemo} from 'react'
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context' import {SafeAreaView} from 'react-native-safe-area-context'
import { import {
AppBskyActorDefs, type AppBskyActorDefs,
moderateProfile, moderateProfile,
ModerationOpts, type ModerationOpts,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@@ -12,9 +12,13 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {combinedDisplayName} from '#/lib/strings/display-names' import {combinedDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {isInvalidHandle} from '#/lib/strings/handles' import {isInvalidHandle} from '#/lib/strings/handles'
@@ -28,13 +32,12 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
import {ProfileLists} from '#/view/com/lists/ProfileLists' import {ProfileLists} from '#/view/com/lists/ProfileLists'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {ListRef} from '#/view/com/util/List' import {type ListRef} from '#/view/com/util/List'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
@@ -165,7 +168,7 @@ function ProfileScreenLoaded({
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const { const {
data: labelerInfo, data: labelerInfo,
error: labelerError, error: labelerError,
+2 -2
View File
@@ -16,6 +16,7 @@ import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -54,7 +55,6 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ListMembers} from '#/view/com/lists/ListMembers' import {ListMembers} from '#/view/com/lists/ListMembers'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {PostFeed} from '#/view/com/posts/PostFeed' import {PostFeed} from '#/view/com/posts/PostFeed'
@@ -155,7 +155,7 @@ function ProfileListScreenLoaded({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {rkey} = route.params const {rkey} = route.params
+2 -2
View File
@@ -10,6 +10,7 @@ import {
} from '@react-navigation/native' } from '@react-navigation/native'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
@@ -25,7 +26,6 @@ import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations
import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfilesQuery} from '#/state/queries/profile' import {useProfilesQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session' import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
@@ -447,7 +447,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
function ComposeBtn() { function ComposeBtn() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {getState} = useNavigation() const {getState} = useNavigation()
const {openComposer} = useComposerControls() const {openComposer} = useOpenComposer()
const {_} = useLingui() const {_} = useLingui()
const {leftNavMinimal} = useLayoutBreakpoints() const {leftNavMinimal} = useLayoutBreakpoints()
const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) const [isFetchingHandle, setIsFetchingHandle] = React.useState(false)