forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useState} from 'react'
2import {Keyboard, Pressable, View} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
7import {
8 useCameraPermission,
9 usePhotoLibraryPermission,
10 useVideoLibraryPermission,
11} from '#/lib/hooks/usePermissions'
12import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
13import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
14import {MAX_IMAGES} from '#/view/com/composer/state/composer'
15import {UserAvatar} from '#/view/com/util/UserAvatar'
16import {atoms as a, native, useTheme, web} from '#/alf'
17import {Button} from '#/components/Button'
18import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
19import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera'
20import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
21import {SubtleHover} from '#/components/SubtleHover'
22import {Text} from '#/components/Typography'
23import {useAnalytics} from '#/analytics'
24import {IS_NATIVE} from '#/env'
25
26export function ComposerPrompt() {
27 const t = useTheme()
28 const ax = useAnalytics()
29 const {_} = useLingui()
30 const {openComposer} = useOpenComposer()
31 const profile = useCurrentAccountProfile()
32 const [hover, setHover] = useState(false)
33 const {requestCameraAccessIfNeeded} = useCameraPermission()
34 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
35 const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
36 const sheetWrapper = useSheetWrapper()
37
38 const onPress = useCallback(() => {
39 ax.metric('composerPrompt:press', {})
40 openComposer({})
41 }, [ax, openComposer])
42
43 const onPressImage = useCallback(async () => {
44 ax.metric('composerPrompt:gallery:press', {})
45
46 // On web, open the composer with the gallery picker auto-opening
47 if (!IS_NATIVE) {
48 openComposer({openGallery: true})
49 return
50 }
51
52 try {
53 const [photoAccess, videoAccess] = await Promise.all([
54 requestPhotoAccessIfNeeded(),
55 requestVideoAccessIfNeeded(),
56 ])
57
58 if (!photoAccess && !videoAccess) {
59 return
60 }
61
62 if (Keyboard.isVisible()) {
63 Keyboard.dismiss()
64 }
65
66 const selectionCountRemaining = MAX_IMAGES
67 const {assets, canceled} = await sheetWrapper(
68 openUnifiedPicker({selectionCountRemaining}),
69 )
70
71 if (canceled) {
72 return
73 }
74
75 if (assets.length > 0) {
76 const imageUris = assets
77 .filter(asset => asset.mimeType?.startsWith('image/'))
78 .slice(0, MAX_IMAGES)
79 .map(asset => ({
80 uri: asset.uri,
81 width: asset.width,
82 height: asset.height,
83 }))
84
85 if (imageUris.length > 0) {
86 openComposer({imageUris})
87 }
88 }
89 } catch (err: any) {
90 if (!String(err).toLowerCase().includes('cancel')) {
91 ax.logger.error('Error opening image picker', {error: err})
92 }
93 }
94 }, [
95 ax,
96 openComposer,
97 requestPhotoAccessIfNeeded,
98 requestVideoAccessIfNeeded,
99 sheetWrapper,
100 ])
101
102 const onPressCamera = useCallback(async () => {
103 ax.metric('composerPrompt:camera:press', {})
104
105 try {
106 if (!(await requestCameraAccessIfNeeded())) {
107 return
108 }
109
110 if (IS_NATIVE && Keyboard.isVisible()) {
111 Keyboard.dismiss()
112 }
113
114 const image = await openCamera({
115 mediaTypes: 'images',
116 })
117
118 const imageUris = [
119 {
120 uri: image.path,
121 width: image.width,
122 height: image.height,
123 },
124 ]
125
126 openComposer({
127 imageUris: IS_NATIVE ? imageUris : undefined,
128 })
129 } catch (err: any) {
130 if (!String(err).toLowerCase().includes('cancel')) {
131 ax.logger.error('Error opening camera', {error: err})
132 }
133 }
134 }, [ax, openComposer, requestCameraAccessIfNeeded])
135
136 if (!profile) {
137 return null
138 }
139
140 return (
141 <Pressable
142 onPress={onPress}
143 android_ripple={null}
144 accessibilityRole="button"
145 accessibilityLabel={_(msg`Compose new post`)}
146 accessibilityHint={_(msg`Opens the post composer`)}
147 onPointerEnter={() => setHover(true)}
148 onPointerLeave={() => setHover(false)}
149 style={({pressed}) => [
150 a.relative,
151 a.flex_row,
152 a.align_start,
153 {
154 paddingLeft: 18,
155 paddingRight: 15,
156 },
157 a.py_md,
158 native({
159 paddingTop: 10,
160 paddingBottom: 10,
161 }),
162 web({
163 cursor: 'pointer',
164 outline: 'none',
165 }),
166 pressed && web({outline: 'none'}),
167 ]}>
168 <SubtleHover hover={hover} />
169 <UserAvatar
170 avatar={profile.avatar}
171 size={42}
172 type={profile.associated?.labeler ? 'labeler' : 'user'}
173 />
174 <View
175 style={[
176 a.flex_1,
177 a.ml_md,
178 a.flex_row,
179 a.align_center,
180 a.justify_between,
181 {
182 height: 40,
183 },
184 ]}>
185 <Text
186 style={[
187 t.atoms.text_contrast_medium,
188 a.text_md,
189 {includeFontPadding: false},
190 ]}>
191 <Trans>What's up?</Trans>
192 </Text>
193 <View style={[a.flex_row, a.gap_md]}>
194 {IS_NATIVE && (
195 <Button
196 onPress={e => {
197 e.stopPropagation()
198 onPressCamera()
199 }}
200 label={_(msg`Open camera`)}
201 accessibilityHint={_(msg`Opens device camera`)}
202 variant="ghost"
203 shape="round">
204 {({hovered, pressed, focused}) => (
205 <CameraIcon
206 size="lg"
207 style={{
208 color:
209 hovered || pressed || focused
210 ? t.palette.primary_500
211 : t.palette.contrast_300,
212 }}
213 />
214 )}
215 </Button>
216 )}
217 <Button
218 onPress={e => {
219 e.stopPropagation()
220 onPressImage()
221 }}
222 label={_(msg`Add image`)}
223 accessibilityHint={_(msg`Opens image picker`)}
224 variant="ghost"
225 shape="round">
226 {({hovered, pressed, focused}) => (
227 <ImageIcon
228 size="lg"
229 style={{
230 color:
231 hovered || pressed || focused
232 ? t.palette.primary_500
233 : t.palette.contrast_300,
234 }}
235 />
236 )}
237 </Button>
238 </View>
239 </View>
240 </Pressable>
241 )
242}