Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 123 lines 3.3 kB view raw
1import {createContext, useContext, useEffect, useMemo, useState} from 'react' 2import { 3 measure, 4 type MeasuredDimensions, 5 runOnJS, 6 runOnUI, 7} from 'react-native-reanimated' 8import {nanoid} from 'nanoid/non-secure' 9 10import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 11import {useHotkeysContext} from '#/lib/hotkeys' 12import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types' 13 14export type Lightbox = { 15 id: string 16 images: ImageSource[] 17 index: number 18} 19 20const LightboxContext = createContext<{ 21 activeLightbox: Lightbox | null 22}>({ 23 activeLightbox: null, 24}) 25LightboxContext.displayName = 'LightboxContext' 26 27const LightboxControlContext = createContext<{ 28 openLightbox: (lightbox: Omit<Lightbox, 'id'>) => void 29 closeLightbox: () => boolean 30}>({ 31 openLightbox: () => {}, 32 closeLightbox: () => false, 33}) 34LightboxControlContext.displayName = 'LightboxControlContext' 35 36export function Provider({children}: React.PropsWithChildren<{}>) { 37 const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null) 38 const {disableScope, enableScope} = useHotkeysContext() 39 40 useEffect(() => { 41 if (activeLightbox) { 42 disableScope('global') 43 } else { 44 enableScope('global') 45 } 46 }, [activeLightbox, disableScope, enableScope]) 47 48 const doOpen = useNonReactiveCallback((lightbox: Omit<Lightbox, 'id'>) => { 49 setActiveLightbox(prevLightbox => { 50 if (prevLightbox) { 51 // Ignore duplicate open requests. If it's already open, 52 // the user has to explicitly close the previous one first. 53 return prevLightbox 54 } else { 55 return {...lightbox, id: nanoid()} 56 } 57 }) 58 }) 59 60 const openLightbox = useNonReactiveCallback( 61 (lightbox: Omit<Lightbox, 'id'>) => { 62 const thumbRef = lightbox.images[lightbox.index]?.thumbRef 63 if (thumbRef) { 64 // Measure the tapped image on the UI thread, then open with 65 // the rect baked in so it's available from the first render. 66 // Only the rect (plain data) goes through runOnJS — AnimatedRef 67 // objects can't survive serialization across threads. 68 const openWithRect = (rect: MeasuredDimensions | null) => { 69 doOpen({ 70 ...lightbox, 71 images: lightbox.images.map((img, i) => 72 i === lightbox.index ? {...img, thumbRect: rect} : img, 73 ), 74 }) 75 } 76 runOnUI(() => { 77 'worklet' 78 const rect = measure(thumbRef) 79 runOnJS(openWithRect)(rect) 80 })() 81 } else { 82 doOpen(lightbox) 83 } 84 }, 85 ) 86 87 const closeLightbox = useNonReactiveCallback(() => { 88 let wasActive = !!activeLightbox 89 setActiveLightbox(null) 90 return wasActive 91 }) 92 93 const state = useMemo( 94 () => ({ 95 activeLightbox, 96 }), 97 [activeLightbox], 98 ) 99 100 const methods = useMemo( 101 () => ({ 102 openLightbox, 103 closeLightbox, 104 }), 105 [openLightbox, closeLightbox], 106 ) 107 108 return ( 109 <LightboxContext.Provider value={state}> 110 <LightboxControlContext.Provider value={methods}> 111 {children} 112 </LightboxControlContext.Provider> 113 </LightboxContext.Provider> 114 ) 115} 116 117export function useLightbox() { 118 return useContext(LightboxContext) 119} 120 121export function useLightboxControls() { 122 return useContext(LightboxControlContext) 123}