Files
bsky-social-app/src/view/shell/index.web.tsx
T
Eric Bailey edb19dd6cf [APP-1750] Add the ability to report livestreams (#9654)
* Add report dialog to LiveStatus dialog

* Mr Worldwide™

* Bug fix bug fix

* Copy update

* Simplify parsing

* Bump api sdk

* update dev-env

* Update yarn.lock

* Bump api sdk

* Refactor useActorStatus, add disablement and appeal dialog

* Fix types

* Guard against chat profile views

* Go live (disabled)

* Fix types

* Status reports should only go to bsky

* Ah yeah let's not override types

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2026-01-09 10:19:47 -06:00

206 lines
6.3 KiB
TypeScript

import {useCallback, useEffect, useLayoutEffect, useState} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {type NavigationProp} from '#/lib/routes/types'
import {useSession} from '#/state/session'
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {useCloseAllActiveElements} from '#/state/util'
import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Deactivated} from '#/screens/Deactivated'
import {Takendown} from '#/screens/Takendown'
import {atoms as a, select, useBreakpoints, useTheme} from '#/alf'
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import {EmailDialog} from '#/components/dialogs/EmailDialog'
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {SigninDialog} from '#/components/dialogs/Signin'
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
import {
Outlet as PolicyUpdateOverlayPortalOutlet,
usePolicyUpdateContext,
} from '#/components/PolicyUpdateOverlay'
import {Outlet as PortalOutlet} from '#/components/Portal'
import {WelcomeModal} from '#/components/WelcomeModal'
import {useAgeAssurance} from '#/ageAssurance'
import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
import {FlatNavigator, RoutesContainer} from '#/Navigation'
import {Composer} from './Composer.web'
import {DrawerContent} from './Drawer'
function ShellInner() {
const navigator = useNavigation<NavigationProp>()
const closeAllActiveElements = useCloseAllActiveElements()
const {state: policyUpdateState} = usePolicyUpdateContext()
const welcomeModalControl = useWelcomeModal()
useComposerKeyboardShortcut()
useIntentHandler()
useEffect(() => {
const unsubscribe = navigator.addListener('state', () => {
closeAllActiveElements()
})
return unsubscribe
}, [navigator, closeAllActiveElements])
const drawerLayout = useCallback(
({children}: {children: React.ReactNode}) => (
<DrawerLayout>{children}</DrawerLayout>
),
[],
)
return (
<>
<ErrorBoundary>
<FlatNavigator layout={drawerLayout} />
</ErrorBoundary>
<Composer winHeight={0} />
<ModalsContainer />
<MutedWordsDialog />
<SigninDialog />
<EmailDialog />
<AgeAssuranceRedirectDialog />
<LinkWarningDialog />
<Lightbox />
<NuxDialogs />
<GlobalReportDialog />
{welcomeModalControl.isOpen && (
<WelcomeModal control={welcomeModalControl} />
)}
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
{policyUpdateState.completed && (
<>
<PortalOutlet />
</>
)}
<PolicyUpdateOverlayPortalOutlet />
</>
)
}
function DrawerLayout({children}: {children: React.ReactNode}) {
const t = useTheme()
const isDrawerOpen = useIsDrawerOpen()
const setDrawerOpen = useSetDrawerOpen()
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const showDrawer = !gtTablet && isDrawerOpen
const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
useLayoutEffect(() => {
if (showDrawer !== showDrawerDelayedExit) {
if (showDrawer) {
setShowDrawerDelayedExit(true)
} else {
const timeout = setTimeout(() => {
setShowDrawerDelayedExit(false)
}, 160)
return () => clearTimeout(timeout)
}
}
}, [showDrawer, showDrawerDelayedExit])
return (
<>
{children}
{showDrawerDelayedExit && (
<>
<RemoveScrollBar />
<TouchableWithoutFeedback
onPress={ev => {
// Only close if press happens outside of the drawer
if (ev.target === ev.currentTarget) {
setDrawerOpen(false)
}
}}
accessibilityLabel={_(msg`Close drawer menu`)}
accessibilityHint="">
<View
style={[
styles.drawerMask,
{
backgroundColor: showDrawer
? select(t.name, {
light: 'rgba(0, 57, 117, 0.1)',
dark: 'rgba(1, 82, 168, 0.1)',
dim: 'rgba(10, 13, 16, 0.8)',
})
: 'transparent',
},
a.transition_color,
]}>
<View
style={[
styles.drawerContainer,
showDrawer ? a.slide_in_left : a.slide_out_left,
]}>
<DrawerContent />
</View>
</View>
</TouchableWithoutFeedback>
</>
)}
</>
)
}
export function Shell() {
const t = useTheme()
const aa = useAgeAssurance()
const {currentAccount} = useSession()
return (
<View style={[a.util_screen_outer, t.atoms.bg]}>
{currentAccount?.status === 'takendown' ? (
<Takendown />
) : currentAccount?.status === 'deactivated' ? (
<Deactivated />
) : (
<>
{aa.state.access === aa.Access.None ? (
<NoAccessScreen />
) : (
<RoutesContainer>
<ShellInner />
</RoutesContainer>
)}
<RedirectOverlay />
</>
)}
</View>
)
}
const styles = StyleSheet.create({
drawerMask: {
...a.fixed,
width: '100%',
height: '100%',
top: 0,
left: 0,
},
drawerContainer: {
display: 'flex',
...a.fixed,
top: 0,
left: 0,
height: '100%',
width: 330,
maxWidth: '80%',
},
})