[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 {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useEmail} from '#/lib/hooks/useEmail'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type NavigationProp} from '#/lib/routes/types'
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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Loader} from '#/components/Loader'
@@ -196,9 +195,7 @@ function Empty() {
const confirmDialogControl = useDialogControl()
const followersDialogControl = useDialogControl()
const errorDialogControl = useDialogControl()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
const [isGenerating, setIsGenerating] = useState(false)
@@ -229,6 +226,27 @@ function Empty() {
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 (
<LinearGradientBackground
style={[
@@ -257,13 +275,7 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
confirmDialogControl.open()
}
}}
onPress={wrappedOpenConfirmDialog}
style={{backgroundColor: 'transparent'}}>
<ButtonText style={{color: 'white'}}>
<Trans>Make one for me</Trans>
@@ -276,13 +288,7 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
navigation.navigate('StarterPackWizard')
}
}}
onPress={wrappedNavToWizard}
style={{
backgroundColor: 'white',
borderColor: 'white',
@@ -338,12 +344,6 @@ function Empty() {
onConfirm={generate}
confirmButtonCta={_(msg`Retry`)}
/>
<VerifyEmailDialog
reasonText={_(
msg`Before creating a starter pack, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</LinearGradientBackground>
)
}
+15 -28
View File
@@ -1,20 +1,18 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {type AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {NavigationProp} from '#/lib/routes/types'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {canBeMessaged} from '#/components/dms/util'
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
@@ -26,8 +24,7 @@ export function MessageProfileButton({
const {_} = useLingui()
const t = useTheme()
const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
const {mutate: initiateConvo} = useGetConvoForMembers({
@@ -45,11 +42,6 @@ export function MessageProfileButton({
return
}
if (needsEmailVerification) {
verifyEmailControl.open()
return
}
if (convoAvailability.convo) {
logEvent('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', {
@@ -59,14 +51,15 @@ export function MessageProfileButton({
logEvent('chat:create', {logContext: 'ProfileHeader'})
initiateConvo([profile.did])
}
}, [
needsEmailVerification,
verifyEmailControl,
navigation,
profile.did,
initiateConvo,
convoAvailability,
])
}, [navigation, profile.did, initiateConvo, convoAvailability])
const wrappedOnPress = requireEmailVerification(onPress, {
instructions: [
<Trans key="message">
Before you may message another user, you must first verify your email.
</Trans>,
],
})
if (!convoAvailability) {
// show pending state based on declaration
@@ -102,15 +95,9 @@ export function MessageProfileButton({
shape="round"
label={_(msg`Message ${profile.handle}`)}
style={[a.justify_center]}
onPress={onPress}>
onPress={wrappedOnPress}>
<ButtonIcon icon={Message} size="md" />
</Button>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</>
)
} else {
+15 -20
View File
@@ -1,8 +1,8 @@
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useEmail} from '#/lib/hooks/useEmail'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
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 {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
export function NewChat({
@@ -24,8 +22,7 @@ export function NewChat({
}) {
const t = useTheme()
const {_} = useLingui()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
@@ -49,17 +46,22 @@ export function NewChat({
[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 (
<>
<FAB
testID="newChatFAB"
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
control.open()
}
}}
onPress={wrappedOnPress}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
@@ -74,13 +76,6 @@ export function NewChat({
sortByMessageDeclaration
/>
</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 * as Linking from 'expo-linking'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logEvent} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
import {useCloseAllActiveElements} from '#/state/util'
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
@@ -83,7 +83,7 @@ export function useIntentHandler() {
export function useComposeIntent() {
const closeAllActiveElements = useCloseAllActiveElements()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {hasSession} = useSession()
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 {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
@@ -321,6 +322,18 @@ export function MessagesScreen({navigation, route}: Props) {
function Header({newChatControl}: {newChatControl: DialogControlProps}) {
const {_} = useLingui()
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 = (
<Link
@@ -352,7 +365,7 @@ function Header({newChatControl}: {newChatControl: DialogControlProps}) {
color="primary"
size="small"
variant="solid"
onPress={newChatControl.open}>
onPress={wrappedOpenChatControl}>
<ButtonIcon icon={PlusIcon} position="left" />
<ButtonText>
<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 {
type AppBskyActorDefs,
@@ -17,6 +17,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useEmail} from '#/lib/hooks/useEmail'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
type CommonNavigatorParams,
type NavigationProp,
@@ -192,22 +193,43 @@ function InnerReady({
const {needsEmailVerification} = useEmail()
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) {
emailDialogControl.open({
id: EmailDialogScreenID.Verify,
instructions: [
<Trans key="pre-compose">
Before you may message another user, you must first verify your
email.
</Trans>,
],
onCloseWithoutVerifying: () => {
navigation.navigate('Home')
},
})
/*
* HACKFIX
*
* Load bearing timeout, to bump this state update until the after the
* `navigator.addListener('state')` handler closes elements from
* `shell/index.*.tsx` - sfn & esb
*/
setTimeout(() =>
emailDialogControl.open({
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 (
<>
@@ -1,11 +1,12 @@
import {useCallback} from 'react'
import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
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 {useAcceptConversation} from '#/state/queries/messages/accept-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 * as Toast from '#/view/com/util/Toast'
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 {
EmailDialogScreenID,
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
import {ReportDialog} from '#/components/dms/ReportDialog'
import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
@@ -186,6 +196,8 @@ export function AcceptChatButton({
const {_} = useLingui()
const queryClient = useQueryClient()
const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
const emailDialogControl = useEmailDialogControl()
const {mutate: acceptConvo, isPending} = useAcceptConversation(convo.id, {
onMutate: () => {
@@ -216,8 +228,20 @@ export function AcceptChatButton({
})
const onPressAccept = useCallback(() => {
acceptConvo()
}, [acceptConvo])
if (needsEmailVerification) {
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 (
<Button
@@ -1,5 +1,5 @@
import {View} from 'react-native'
import {ChatBskyConvoDefs} from '@atproto/api'
import {type ChatBskyConvoDefs} from '@atproto/api'
import {Trans} from '@lingui/macro'
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 {useLingui} from '@lingui/react'
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 {VIDEO_FEED_URIS} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {NavigationProp} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {type NavigationProp} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {
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 {
usePreferencesQuery,
UsePreferencesQueryResponse,
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {FAB} from '#/view/com/util/fab/FAB'
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 {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text'
@@ -156,7 +159,7 @@ export function ProfileFeedScreenInner({
}) {
const {_} = useLingui()
const {hasSession} = useSession()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const isScreenFocused = useIsFocused()
useSetTitle(feedInfo?.displayName)
+24 -16
View File
@@ -1,18 +1,18 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {
LayoutAnimation,
ListRenderItem,
type ListRenderItem,
Pressable,
ScrollView,
View,
ViewabilityConfig,
ViewToken,
type ViewabilityConfig,
type ViewToken,
} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge'
import {
Gesture,
GestureDetector,
NativeGesture,
type NativeGesture,
} from 'react-native-gesture-handler'
import Animated, {
useAnimatedStyle,
@@ -24,38 +24,46 @@ import {
} from 'react-native-safe-area-context'
import {useEvent} 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 {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video'
import {createVideoPlayer, type VideoPlayer, VideoView} from 'expo-video'
import {
AppBskyEmbedVideo,
AppBskyFeedDefs,
type AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
ModerationDecision,
type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
RouteProp,
type RouteProp,
useFocusEffect,
useIsFocused,
useNavigation,
useRoute,
} 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 {useHaptics} from '#/lib/haptics'
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 {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isAndroid} from '#/platform/detection'
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 {
FeedFeedbackProvider,
@@ -64,13 +72,13 @@ import {
import {useFeedFeedback} from '#/state/feed-feedback'
import {usePostLikeMutationQueue} from '#/state/queries/post'
import {
AuthorFilter,
FeedPostSliceItem,
type AuthorFilter,
type FeedPostSliceItem,
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
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 {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {List} from '#/view/com/util/List'
@@ -685,7 +693,7 @@ function Overlay({
}) {
const {_} = useLingui()
const t = useTheme()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {currentAccount} = useSession()
const navigation = useNavigation<NavigationProp>()
const seekingAnimationSV = useSharedValue(0)
+13 -1
View File
@@ -125,5 +125,17 @@ export function useComposerState() {
}
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 {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useDialogStateContext} from '#/state/dialogs'
import {useLightbox} from '#/state/lightbox'
import {useModals} from '#/state/modals'
import {useSession} from '#/state/session'
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}
@@ -39,7 +39,7 @@ function shouldIgnore(event: KeyboardEvent) {
}
export function useComposerKeyboardShortcut() {
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {openDialogs} = useDialogStateContext()
const {isModalActive} = useModals()
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 DrawerOpenProvider} from './drawer-open'
@@ -9,7 +9,6 @@ import {Provider as ShellLayoutProvder} from './shell-layout'
import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
export {useSetThemePrefs, useThemePrefs} from './color-mode'
export {useComposerControls, useComposerState} from './composer'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export {
useIsDrawerSwipeDisabled,
-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} />
</>
)
}
+8 -8
View File
@@ -1,32 +1,32 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
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 {VIDEO_FEED_URIS} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {ComposeIcon2} from '#/lib/icons'
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 {s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
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 {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import {PostFeed} from '../posts/PostFeed'
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 {MainScrollProvider} from '../util/MainScrollProvider'
@@ -57,7 +57,7 @@ export function FeedPage({
const {_} = useLingui()
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode()
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 {
AppBskyFeedDefs,
AppBskyFeedThreadgate,
type AppBskyFeedThreadgate,
moderatePost,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
@@ -14,6 +14,7 @@ import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers'
@@ -25,19 +26,18 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
fillThreadModerationCache,
sortThread,
ThreadBlocked,
ThreadModerationCache,
ThreadNode,
ThreadNotFound,
ThreadPost,
type ThreadBlocked,
type ThreadModerationCache,
type ThreadNode,
type ThreadNotFound,
type ThreadPost,
usePostThreadQuery,
} from '#/state/queries/post-thread'
import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell'
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 {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
@@ -394,7 +394,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
[refetch],
)
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const onPressReply = React.useCallback(() => {
if (thread?.type !== 'post') {
return
+2 -2
View File
@@ -17,6 +17,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
@@ -36,7 +37,6 @@ import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useLanguagePrefs} from '#/state/preferences'
import {type ThreadPost} from '#/state/queries/post-thread'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
@@ -204,7 +204,7 @@ let PostThreadItemLoaded = ({
const pal = usePalette('default')
const {_, i18n} = useLingui()
const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = React.useState(
() => countLines(richText?.text) >= MAX_POST_LINES,
)
+10 -6
View File
@@ -1,11 +1,11 @@
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 {
AppBskyFeedDefs,
type AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
moderatePost,
ModerationDecision,
type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -14,15 +14,19 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
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 {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {AviFollowButton} from '#/view/com/posts/AviFollowButton'
import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -113,7 +117,7 @@ function PostInner({
const queryClient = useQueryClient()
const pal = usePalette('default')
const {_} = useLingui()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const [limitLines, setLimitLines] = useState(
() => countLines(richText?.text) >= MAX_POST_LINES,
)
+12 -8
View File
@@ -1,35 +1,39 @@
import React, {memo, useMemo, useState} from 'react'
import {StyleSheet, View} from 'react-native'
import {
AppBskyActorDefs,
type AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedThreadgate,
AtUri,
ModerationDecision,
type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
type FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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 {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {countLines} from '#/lib/strings/helpers'
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 {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {FeedNameText} from '#/view/com/util/FeedInfoText'
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 {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {AppModerationCause} from '#/components/Pills'
import {type AppModerationCause} from '#/components/Pills'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
@@ -150,7 +154,7 @@ let FeedItemInner = ({
rootPost: AppBskyFeedDefs.PostView
}): React.ReactNode => {
const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const pal = usePalette('default')
const {_} = useLingui()
+7 -7
View File
@@ -8,11 +8,11 @@ import {
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedThreadgate,
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyFeedThreadgate,
AtUri,
RichText as RichTextAPI,
type RichText as RichTextAPI,
} from '@atproto/api'
import {msg, plural} from '@lingui/macro'
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 {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
import {useGate} from '#/lib/statsig/statsig'
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 {
usePostLikeMutationQueue,
usePostRepostMutationQueue,
} from '#/state/queries/post'
import {useRequireAuth, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {
ProgressGuideAction,
useProgressGuideControls,
@@ -74,7 +74,7 @@ let PostCtrls = ({
}): React.ReactNode => {
const t = useTheme()
const {_, i18n} = useLingui()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {currentAccount} = useSession()
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
+9 -6
View File
@@ -1,30 +1,33 @@
import React from 'react'
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 {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import debounce from 'lodash.debounce'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
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 {s} from '#/lib/styles'
import {isNative, isWeb} from '#/platform/detection'
import {
SavedFeedItem,
type SavedFeedItem,
useGetPopularFeedsQuery,
useSavedFeeds,
useSearchPopularFeedsMutation,
} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
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 {Text} from '#/view/com/util/text/Text'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
@@ -102,7 +105,7 @@ type FlatlistSlice =
export function FeedsScreen(_props: Props) {
const pal = usePalette('default')
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {isMobile} = useWebMediaQueries()
const [query, setQuery] = React.useState('')
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 {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {NavigationProp} from '#/lib/routes/types'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {type NavigationProp} from '#/lib/routes/types'
import {useModalControls} from '#/state/modals'
import {useSetMinimalShellMode} from '#/state/shell'
import {MyLists} from '#/view/com/lists/MyLists'
import {atoms as a} from '#/alf'
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 * as Layout from '#/components/Layout'
@@ -23,8 +24,7 @@ export function ListsScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode()
const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail()
const control = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
useFocusEffect(
React.useCallback(() => {
@@ -33,11 +33,6 @@ export function ListsScreen({}: Props) {
)
const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({
name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#curatelist',
@@ -51,7 +46,15 @@ export function ListsScreen({}: Props) {
} 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 (
<Layout.Screen testID="listsScreen">
@@ -68,7 +71,7 @@ export function ListsScreen({}: Props) {
color="secondary"
variant="solid"
size="small"
onPress={onPressNewList}>
onPress={wrappedOnPressNewList}>
<ButtonIcon icon={PlusIcon} />
<ButtonText>
<Trans context="action">New</Trans>
@@ -76,12 +79,6 @@ export function ListsScreen({}: Props) {
</Button>
</Layout.Header.Outer>
<MyLists filter="curate" style={a.flex_grow} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen>
)
}
+17 -20
View File
@@ -4,16 +4,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {NavigationProp} from '#/lib/routes/types'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {type NavigationProp} from '#/lib/routes/types'
import {useModalControls} from '#/state/modals'
import {useSetMinimalShellMode} from '#/state/shell'
import {MyLists} from '#/view/com/lists/MyLists'
import {atoms as a} from '#/alf'
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 * as Layout from '#/components/Layout'
@@ -23,8 +24,7 @@ export function ModerationModlistsScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode()
const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail()
const control = useDialogControl()
const requireEmailVerification = useRequireEmailVerification()
useFocusEffect(
React.useCallback(() => {
@@ -33,11 +33,6 @@ export function ModerationModlistsScreen({}: Props) {
)
const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({
name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#modlist',
@@ -51,7 +46,15 @@ export function ModerationModlistsScreen({}: Props) {
} 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 (
<Layout.Screen testID="moderationModlistsScreen">
@@ -68,7 +71,7 @@ export function ModerationModlistsScreen({}: Props) {
color="secondary"
variant="solid"
size="small"
onPress={onPressNewList}>
onPress={wrappedOnPressNewList}>
<ButtonIcon icon={PlusIcon} />
<ButtonText>
<Trans context="action">New</Trans>
@@ -76,12 +79,6 @@ export function ModerationModlistsScreen({}: Props) {
</Button>
</Layout.Header.Outer>
<MyLists filter="mod" style={a.flex_grow} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen>
)
}
+5 -5
View File
@@ -6,10 +6,11 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {ComposeIcon2} from '#/lib/icons'
import {
NativeStackScreenProps,
NotificationsTabNavigatorParams,
type NativeStackScreenProps,
type NotificationsTabNavigatorParams,
} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
@@ -22,12 +23,11 @@ import {
} from '#/state/queries/notifications/unread'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
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 {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {atoms as a} from '#/alf'
@@ -49,7 +49,7 @@ type Props = NativeStackScreenProps<
>
export function NotificationsScreen({}: Props) {
const {_} = useLingui()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const unreadNotifs = useUnreadNotifications()
const hasNew = !!unreadNotifs
const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi()
+9 -6
View File
@@ -2,9 +2,9 @@ import React, {useCallback, useMemo} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {
AppBskyActorDefs,
type AppBskyActorDefs,
moderateProfile,
ModerationOpts,
type ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg} from '@lingui/macro'
@@ -12,9 +12,13 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
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 {cleanError} from '#/lib/strings/errors'
import {isInvalidHandle} from '#/lib/strings/handles'
@@ -28,13 +32,12 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useAgent, useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
import {ProfileLists} from '#/view/com/lists/ProfileLists'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
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 {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
@@ -165,7 +168,7 @@ function ProfileScreenLoaded({
const profile = useProfileShadow(profileUnshadowed)
const {hasSession, currentAccount} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {
data: labelerInfo,
error: labelerError,
+2 -2
View File
@@ -16,6 +16,7 @@ import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -54,7 +55,6 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {ListMembers} from '#/view/com/lists/ListMembers'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {PostFeed} from '#/view/com/posts/PostFeed'
@@ -155,7 +155,7 @@ function ProfileListScreenLoaded({
}) {
const {_} = useLingui()
const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession()
const {rkey} = route.params
+2 -2
View File
@@ -10,6 +10,7 @@ import {
} from '@react-navigation/native'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
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 {useProfilesQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
@@ -447,7 +447,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
function ComposeBtn() {
const {currentAccount} = useSession()
const {getState} = useNavigation()
const {openComposer} = useComposerControls()
const {openComposer} = useOpenComposer()
const {_} = useLingui()
const {leftNavMinimal} = useLayoutBreakpoints()
const [isFetchingHandle, setIsFetchingHandle] = React.useState(false)