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