forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 createContext,
3 useCallback,
4 useContext,
5 useEffect,
6 useId,
7 useMemo,
8 useRef,
9 useState,
10} from 'react'
11import {useWindowDimensions} from 'react-native'
12
13import {IS_NATIVE, IS_WEB} from '#/env'
14
15const Context = createContext<{
16 activeViewId: string | null
17 setActiveView: (viewId: string) => void
18 sendViewPosition: (viewId: string, y: number) => void
19} | null>(null)
20Context.displayName = 'ActiveVideoWebContext'
21
22export function Provider({children}: {children: React.ReactNode}) {
23 if (!IS_WEB) {
24 throw new Error('ActiveVideoWebContext may only be used on web.')
25 }
26
27 const [activeViewId, setActiveViewId] = useState<string | null>(null)
28 const activeViewLocationRef = useRef(Infinity)
29 const {height: windowHeight} = useWindowDimensions()
30
31 // minimising re-renders by using refs
32 const manuallySetRef = useRef(false)
33 const activeViewIdRef = useRef(activeViewId)
34 useEffect(() => {
35 activeViewIdRef.current = activeViewId
36 }, [activeViewId])
37
38 const setActiveView = useCallback(
39 (viewId: string) => {
40 setActiveViewId(viewId)
41 manuallySetRef.current = true
42 // we don't know the exact position, but it's definitely on screen
43 // so just guess that it's in the middle. Any value is fine
44 // so long as it's not offscreen
45 activeViewLocationRef.current = windowHeight / 2
46 },
47 [windowHeight],
48 )
49
50 const sendViewPosition = useCallback(
51 (viewId: string, y: number) => {
52 if (IS_NATIVE) return
53
54 if (viewId === activeViewIdRef.current) {
55 activeViewLocationRef.current = y
56 } else {
57 if (
58 distanceToIdealPosition(y) <
59 distanceToIdealPosition(activeViewLocationRef.current)
60 ) {
61 // if the old view was manually set, only usurp if the old view is offscreen
62 if (
63 manuallySetRef.current &&
64 withinViewport(activeViewLocationRef.current)
65 ) {
66 return
67 }
68
69 setActiveViewId(viewId)
70 activeViewLocationRef.current = y
71 manuallySetRef.current = false
72 }
73 }
74
75 function distanceToIdealPosition(yPos: number) {
76 return Math.abs(yPos - windowHeight / 2.5)
77 }
78
79 function withinViewport(yPos: number) {
80 return yPos > 0 && yPos < windowHeight
81 }
82 },
83 [windowHeight],
84 )
85
86 const value = useMemo(
87 () => ({
88 activeViewId,
89 setActiveView,
90 sendViewPosition,
91 }),
92 [activeViewId, setActiveView, sendViewPosition],
93 )
94
95 return <Context.Provider value={value}>{children}</Context.Provider>
96}
97
98export function useActiveVideoWeb() {
99 const context = useContext(Context)
100 if (!context) {
101 throw new Error(
102 'useActiveVideoWeb must be used within a ActiveVideoWebProvider',
103 )
104 }
105
106 const {activeViewId, setActiveView, sendViewPosition} = context
107 const id = useId()
108
109 return {
110 active: activeViewId === id,
111 setActive: () => {
112 setActiveView(id)
113 },
114 currentActiveView: activeViewId,
115 sendPosition: (y: number) => sendViewPosition(id, y),
116 }
117}