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