Factor lightbox out into hook/context (#1919)

zio/stable
Paul Frazee 2023-11-15 18:17:03 -08:00 committed by GitHub
parent 03b20c70e4
commit e749f2f3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 104 deletions

View File

@ -25,6 +25,7 @@ import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
@ -124,7 +125,9 @@ function App() {
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<LightboxStateProvider>
<InnerApp />
</LightboxStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>

View File

@ -22,6 +22,7 @@ import {I18nProvider} from '@lingui/react'
import {defaultLocale, dynamicActivate} from './locale/i18n'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
@ -111,7 +112,9 @@ function App() {
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<LightboxStateProvider>
<InnerApp />
</LightboxStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>

View File

@ -0,0 +1,86 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
interface Lightbox {
name: string
}
export class ProfileImageLightbox implements Lightbox {
name = 'profile-image'
constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {}
}
interface ImagesLightboxItem {
uri: string
alt?: string
}
export class ImagesLightbox implements Lightbox {
name = 'images'
constructor(public images: ImagesLightboxItem[], public index: number) {}
setIndex(index: number) {
this.index = index
}
}
const LightboxContext = React.createContext<{
activeLightbox: Lightbox | null
}>({
activeLightbox: null,
})
const LightboxControlContext = React.createContext<{
openLightbox: (lightbox: Lightbox) => void
closeLightbox: () => void
}>({
openLightbox: () => {},
closeLightbox: () => {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeLightbox, setActiveLightbox] = React.useState<Lightbox | null>(
null,
)
const openLightbox = React.useCallback(
(lightbox: Lightbox) => {
setActiveLightbox(lightbox)
},
[setActiveLightbox],
)
const closeLightbox = React.useCallback(() => {
setActiveLightbox(null)
}, [setActiveLightbox])
const state = React.useMemo(
() => ({
activeLightbox,
}),
[activeLightbox],
)
const methods = React.useMemo(
() => ({
openLightbox,
closeLightbox,
}),
[openLightbox, closeLightbox],
)
return (
<LightboxContext.Provider value={state}>
<LightboxControlContext.Provider value={methods}>
{children}
</LightboxControlContext.Provider>
</LightboxContext.Provider>
)
}
export function useLightbox() {
return React.useContext(LightboxContext)
}
export function useLightboxControls() {
return React.useContext(LightboxControlContext)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
import {StyleProp, ViewStyle} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {ImageModel} from '#/state/models/media/image'
@ -232,7 +232,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const openModal = React.useCallback(
(modal: Modal) => {
DeviceEventEmitter.emit('navigation')
setActiveModals(activeModals => [...activeModals, modal])
setIsModalActive(true)
},

View File

@ -1,4 +1,3 @@
import {AppBskyActorDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {makeAutoObservable} from 'mobx'
import {
@ -13,34 +12,7 @@ export function isColorMode(v: unknown): v is ColorMode {
return v === 'system' || v === 'light' || v === 'dark'
}
interface LightboxModel {}
export class ProfileImageLightbox implements LightboxModel {
name = 'profile-image'
constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {
makeAutoObservable(this)
}
}
interface ImagesLightboxItem {
uri: string
alt?: string
}
export class ImagesLightbox implements LightboxModel {
name = 'images'
constructor(public images: ImagesLightboxItem[], public index: number) {
makeAutoObservable(this)
}
setIndex(index: number) {
this.index = index
}
}
export class ShellUiModel {
isLightboxActive = false
activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {
rootStore: false,
@ -54,32 +26,13 @@ export class ShellUiModel {
* (used by the android hardware back btn)
*/
closeAnyActiveElement(): boolean {
if (this.isLightboxActive) {
this.closeLightbox()
return true
}
return false
}
/**
* used to clear out any modals, eg for a navigation
*/
closeAllActiveElements() {
if (this.isLightboxActive) {
this.closeLightbox()
}
}
openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
this.rootStore.emitNavigation()
this.isLightboxActive = true
this.activeLightbox = lightbox
}
closeLightbox() {
this.isLightboxActive = false
this.activeLightbox = null
}
closeAllActiveElements() {}
setupLoginModals() {
this.rootStore.onSessionReady(() => {

View File

@ -1,10 +1,7 @@
import React from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ImageView from './ImageViewing'
import {useStores} from 'state/index'
import * as models from 'state/models/ui/shell'
import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
import * as Toast from '../util/Toast'
import {Text} from '../util/text/Text'
@ -12,17 +9,24 @@ import {s, colors} from 'lib/styles'
import {Button} from '../util/forms/Button'
import {isIOS} from 'platform/detection'
import * as MediaLibrary from 'expo-media-library'
import {
useLightbox,
useLightboxControls,
ProfileImageLightbox,
ImagesLightbox,
} from '#/state/lightbox'
export const Lightbox = observer(function Lightbox() {
const store = useStores()
export function Lightbox() {
const {activeLightbox} = useLightbox()
const {closeLightbox} = useLightboxControls()
const onClose = React.useCallback(() => {
store.shell.closeLightbox()
}, [store])
closeLightbox()
}, [closeLightbox])
if (!store.shell.activeLightbox) {
if (!activeLightbox) {
return null
} else if (store.shell.activeLightbox.name === 'profile-image') {
const opts = store.shell.activeLightbox as models.ProfileImageLightbox
} else if (activeLightbox.name === 'profile-image') {
const opts = activeLightbox as ProfileImageLightbox
return (
<ImageView
images={[{uri: opts.profile.avatar || ''}]}
@ -32,8 +36,8 @@ export const Lightbox = observer(function Lightbox() {
FooterComponent={LightboxFooter}
/>
)
} else if (store.shell.activeLightbox.name === 'images') {
const opts = store.shell.activeLightbox as models.ImagesLightbox
} else if (activeLightbox.name === 'images') {
const opts = activeLightbox as ImagesLightbox
return (
<ImageView
images={opts.images.map(img => ({...img}))}
@ -46,14 +50,10 @@ export const Lightbox = observer(function Lightbox() {
} else {
return null
}
})
}
const LightboxFooter = observer(function LightboxFooter({
imageIndex,
}: {
imageIndex: number
}) {
const store = useStores()
function LightboxFooter({imageIndex}: {imageIndex: number}) {
const {activeLightbox} = useLightbox()
const [isAltExpanded, setAltExpanded] = React.useState(false)
const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
@ -81,7 +81,7 @@ const LightboxFooter = observer(function LightboxFooter({
[permissionResponse, requestPermission],
)
const lightbox = store.shell.activeLightbox
const lightbox = activeLightbox
if (!lightbox) {
return null
}
@ -89,11 +89,11 @@ const LightboxFooter = observer(function LightboxFooter({
let altText = ''
let uri = ''
if (lightbox.name === 'images') {
const opts = lightbox as models.ImagesLightbox
const opts = lightbox as ImagesLightbox
uri = opts.images[imageIndex].uri
altText = opts.images[imageIndex].alt || ''
} else if (lightbox.name === 'profile-image') {
const opts = lightbox as models.ProfileImageLightbox
const opts = lightbox as ProfileImageLightbox
uri = opts.profile.avatar || ''
}
@ -132,7 +132,7 @@ const LightboxFooter = observer(function LightboxFooter({
</View>
</View>
)
})
}
const styles = StyleSheet.create({
footer: {

View File

@ -7,41 +7,42 @@ import {
View,
Pressable,
} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useStores} from 'state/index'
import * as models from 'state/models/ui/shell'
import {colors, s} from 'lib/styles'
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
import {Text} from '../util/text/Text'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {
useLightbox,
useLightboxControls,
ImagesLightbox,
ProfileImageLightbox,
} from '#/state/lightbox'
interface Img {
uri: string
alt?: string
}
export const Lightbox = observer(function Lightbox() {
const store = useStores()
export function Lightbox() {
const {activeLightbox} = useLightbox()
const {closeLightbox} = useLightboxControls()
const onClose = useCallback(() => store.shell.closeLightbox(), [store.shell])
if (!store.shell.isLightboxActive) {
if (!activeLightbox) {
return null
}
const activeLightbox = store.shell.activeLightbox
const initialIndex =
activeLightbox instanceof models.ImagesLightbox ? activeLightbox.index : 0
activeLightbox instanceof ImagesLightbox ? activeLightbox.index : 0
let imgs: Img[] | undefined
if (activeLightbox instanceof models.ProfileImageLightbox) {
if (activeLightbox instanceof ProfileImageLightbox) {
const opts = activeLightbox
if (opts.profile.avatar) {
imgs = [{uri: opts.profile.avatar}]
}
} else if (activeLightbox instanceof models.ImagesLightbox) {
} else if (activeLightbox instanceof ImagesLightbox) {
const opts = activeLightbox
imgs = opts.images
}
@ -51,9 +52,13 @@ export const Lightbox = observer(function Lightbox() {
}
return (
<LightboxInner imgs={imgs} initialIndex={initialIndex} onClose={onClose} />
<LightboxInner
imgs={imgs}
initialIndex={initialIndex}
onClose={closeLightbox}
/>
)
})
}
function LightboxInner({
imgs,

View File

@ -17,7 +17,6 @@ import {useLingui} from '@lingui/react'
import {NavigationProp} from 'lib/routes/types'
import {isNative} from 'platform/detection'
import {BlurView} from '../util/BlurView'
import {ProfileImageLightbox} from 'state/models/ui/shell'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
@ -30,8 +29,8 @@ import {formatCount} from '../util/numeric/format'
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
import {Link} from '../util/Link'
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
import {useStores} from 'state/index'
import {useModalControls} from '#/state/modals'
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
import {
useProfileFollowMutation,
useProfileUnfollowMutation,
@ -115,10 +114,10 @@ function ProfileHeaderLoaded({
}: Props) {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const store = useStores()
const {currentAccount} = useSession()
const {_} = useLingui()
const {openModal} = useModalControls()
const {openLightbox} = useLightboxControls()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
const invalidHandle = isInvalidHandle(profile.handle)
@ -151,9 +150,9 @@ function ProfileHeaderLoaded({
profile.avatar &&
!(moderation.avatar.blur && moderation.avatar.noOverride)
) {
store.shell.openLightbox(new ProfileImageLightbox(profile))
openLightbox(new ProfileImageLightbox(profile))
}
}, [store, profile, moderation])
}, [openLightbox, profile, moderation])
const onPressFollow = React.useCallback(async () => {
if (profile.viewer?.following) {

View File

@ -16,7 +16,7 @@ import {useStores} from 'state/index'
import {NavigationProp} from 'lib/routes/types'
import {BACK_HITSLOP} from 'lib/constants'
import {isNative} from 'platform/detection'
import {ImagesLightbox} from 'state/models/ui/shell'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useSetDrawerOpen} from '#/state/shell'
@ -50,6 +50,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {openLightbox} = useLightboxControls()
const pal = usePalette('default')
const canGoBack = navigation.canGoBack()
@ -69,9 +70,9 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
if (
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
) {
store.shell.openLightbox(new ImagesLightbox([{uri: avatar}], 0))
openLightbox(new ImagesLightbox([{uri: avatar}], 0))
}
}, [store, avatar])
}, [openLightbox, avatar])
return (
<CenteredView style={pal.view}>

View File

@ -19,8 +19,7 @@ import {
} from '@atproto/api'
import {Link} from '../Link'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ImagesLightbox} from 'state/models/ui/shell'
import {useStores} from 'state/index'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {YoutubeEmbed} from './YoutubeEmbed'
@ -49,7 +48,7 @@ export function PostEmbeds({
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const store = useStores()
const {openLightbox} = useLightboxControls()
const {isMobile} = useWebMediaQueries()
// quote post with media
@ -104,8 +103,8 @@ export function PostEmbeds({
alt: img.alt,
aspectRatio: img.aspectRatio,
}))
const openLightbox = (index: number) => {
store.shell.openLightbox(new ImagesLightbox(items, index))
const _openLightbox = (index: number) => {
openLightbox(new ImagesLightbox(items, index))
}
const onPressIn = (_: number) => {
InteractionManager.runAfterInteractions(() => {
@ -121,7 +120,7 @@ export function PostEmbeds({
alt={alt}
uri={thumb}
dimensionsHint={aspectRatio}
onPress={() => openLightbox(0)}
onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)}
style={[
styles.singleImage,
@ -143,7 +142,7 @@ export function PostEmbeds({
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
images={embed.images}
onPress={openLightbox}
onPress={_openLightbox}
onPressIn={onPressIn}
style={
embed.images.length === 1