forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useState} from 'react'
2import {StyleSheet, View} from 'react-native'
3import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
4import {RemoveScrollBar} from 'react-remove-scroll-bar'
5
6import {useA11y} from '#/state/a11y'
7import {useModals} from '#/state/modals'
8import {type ComposerOpts, useComposerState} from '#/state/shell/composer'
9import {
10 EmojiPicker,
11 type EmojiPickerPosition,
12 type EmojiPickerState,
13} from '#/view/com/composer/text-input/web/EmojiPicker'
14import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
15import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
16
17const BOTTOM_BAR_HEIGHT = 61
18
19export function Composer({}: {winHeight: number}) {
20 const state = useComposerState()
21 const isActive = !!state
22
23 // rendering
24 // =
25
26 if (!isActive) {
27 return null
28 }
29
30 return (
31 <>
32 <RemoveScrollBar />
33 <Inner state={state} />
34 </>
35 )
36}
37
38function Inner({state}: {state: ComposerOpts}) {
39 const ref = useComposerCancelRef()
40 const {isModalActive} = useModals()
41 const t = useTheme()
42 const {gtMobile} = useBreakpoints()
43 const {reduceMotionEnabled} = useA11y()
44 const [pickerState, setPickerState] = useState<EmojiPickerState>({
45 isOpen: false,
46 pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
47 })
48
49 const onOpenPicker = useCallback((pos: EmojiPickerPosition | undefined) => {
50 if (!pos) return
51 setPickerState({
52 isOpen: true,
53 pos,
54 })
55 }, [])
56
57 const onClosePicker = useCallback(() => {
58 setPickerState(prev => ({
59 ...prev,
60 isOpen: false,
61 }))
62 }, [])
63
64 FocusGuards.useFocusGuards()
65
66 return (
67 <FocusScope.FocusScope loop trapped asChild>
68 <DismissableLayer.DismissableLayer
69 role="dialog"
70 aria-modal
71 style={flatten([
72 {position: 'fixed'},
73 a.inset_0,
74 {backgroundColor: '#000c'},
75 a.flex,
76 a.flex_col,
77 a.align_center,
78 !reduceMotionEnabled && a.fade_in,
79 ])}
80 onFocusOutside={evt => evt.preventDefault()}
81 onInteractOutside={evt => evt.preventDefault()}
82 onDismiss={() => {
83 // TEMP: remove when all modals are ALF'd -sfn
84 if (!isModalActive) {
85 ref.current?.onPressCancel()
86 }
87 }}>
88 <View
89 style={[
90 styles.container,
91 !gtMobile && styles.containerMobile,
92 t.atoms.bg,
93 t.atoms.border_contrast_medium,
94 !reduceMotionEnabled && [
95 a.zoom_fade_in,
96 {animationDelay: 0.1},
97 {animationFillMode: 'backwards'},
98 ],
99 ]}>
100 <ComposePost
101 cancelRef={ref}
102 replyTo={state.replyTo}
103 quote={state.quote}
104 onPost={state.onPost}
105 onPostSuccess={state.onPostSuccess}
106 mention={state.mention}
107 openEmojiPicker={onOpenPicker}
108 text={state.text}
109 imageUris={state.imageUris}
110 videoUri={state.videoUri}
111 openGallery={state.openGallery}
112 />
113 </View>
114 <EmojiPicker state={pickerState} close={onClosePicker} />
115 </DismissableLayer.DismissableLayer>
116 </FocusScope.FocusScope>
117 )
118}
119
120const styles = StyleSheet.create({
121 container: {
122 marginTop: 50,
123 maxWidth: 600,
124 width: '100%',
125 paddingVertical: 0,
126 borderRadius: 8,
127 marginBottom: 0,
128 borderWidth: 1,
129 // @ts-expect-error web only
130 maxHeight: 'calc(100% - (40px * 2))',
131 overflow: 'hidden',
132 },
133 containerMobile: {
134 borderRadius: 0,
135 marginBottom: BOTTOM_BAR_HEIGHT,
136 // @ts-expect-error web only
137 maxHeight: `calc(100% - ${BOTTOM_BAR_HEIGHT}px)`,
138 },
139})