Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import React 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] = React.useState<EmojiPickerState>({
45 isOpen: false,
46 pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
47 })
48
49 const onOpenPicker = React.useCallback(
50 (pos: EmojiPickerPosition | undefined) => {
51 if (!pos) return
52 setPickerState({
53 isOpen: true,
54 pos,
55 })
56 },
57 [],
58 )
59
60 const onClosePicker = React.useCallback(() => {
61 setPickerState(prev => ({
62 ...prev,
63 isOpen: false,
64 }))
65 }, [])
66
67 FocusGuards.useFocusGuards()
68
69 return (
70 <FocusScope.FocusScope loop trapped asChild>
71 <DismissableLayer.DismissableLayer
72 role="dialog"
73 aria-modal
74 style={flatten([
75 {position: 'fixed'},
76 a.inset_0,
77 {backgroundColor: '#000c'},
78 a.flex,
79 a.flex_col,
80 a.align_center,
81 !reduceMotionEnabled && a.fade_in,
82 ])}
83 onFocusOutside={evt => evt.preventDefault()}
84 onInteractOutside={evt => evt.preventDefault()}
85 onDismiss={() => {
86 // TEMP: remove when all modals are ALF'd -sfn
87 if (!isModalActive) {
88 ref.current?.onPressCancel()
89 }
90 }}>
91 <View
92 style={[
93 styles.container,
94 !gtMobile && styles.containerMobile,
95 t.atoms.bg,
96 t.atoms.border_contrast_medium,
97 !reduceMotionEnabled && [
98 a.zoom_fade_in,
99 {animationDelay: 0.1},
100 {animationFillMode: 'backwards'},
101 ],
102 ]}>
103 <ComposePost
104 cancelRef={ref}
105 replyTo={state.replyTo}
106 quote={state.quote}
107 onPost={state.onPost}
108 onPostSuccess={state.onPostSuccess}
109 mention={state.mention}
110 openEmojiPicker={onOpenPicker}
111 text={state.text}
112 imageUris={state.imageUris}
113 videoUri={state.videoUri}
114 openGallery={state.openGallery}
115 />
116 </View>
117 <EmojiPicker state={pickerState} close={onClosePicker} />
118 </DismissableLayer.DismissableLayer>
119 </FocusScope.FocusScope>
120 )
121}
122
123const styles = StyleSheet.create({
124 container: {
125 marginTop: 50,
126 maxWidth: 600,
127 width: '100%',
128 paddingVertical: 0,
129 borderRadius: 8,
130 marginBottom: 0,
131 borderWidth: 1,
132 // @ts-expect-error web only
133 maxHeight: 'calc(100% - (40px * 2))',
134 overflow: 'hidden',
135 },
136 containerMobile: {
137 borderRadius: 0,
138 marginBottom: BOTTOM_BAR_HEIGHT,
139 // @ts-expect-error web only
140 maxHeight: `calc(100% - ${BOTTOM_BAR_HEIGHT}px)`,
141 },
142})