forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
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}