mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {ComponentProps} from 'react'
2import {GestureResponderEvent, TouchableOpacity, View} from 'react-native'
3import Animated from 'react-native-reanimated'
4import {useSafeAreaInsets} from 'react-native-safe-area-context'
5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
8import {StackActions} from '@react-navigation/native'
9
10import {useAnalytics} from '#/lib/analytics/analytics'
11import {useHaptics} from '#/lib/haptics'
12import {useDedupe} from '#/lib/hooks/useDedupe'
13import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode'
14import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
15import {usePalette} from '#/lib/hooks/usePalette'
16import {
17 BellIcon,
18 BellIconSolid,
19 HashtagIcon,
20 HomeIcon,
21 HomeIconSolid,
22 MagnifyingGlassIcon2,
23 MagnifyingGlassIcon2Solid,
24} from '#/lib/icons'
25import {clamp} from '#/lib/numbers'
26import {getTabState, TabState} from '#/lib/routes/helpers'
27import {useGate} from '#/lib/statsig/statsig'
28import {s} from '#/lib/styles'
29import {emitSoftReset} from '#/state/events'
30import {useUnreadNotifications} from '#/state/queries/notifications/unread'
31import {useProfileQuery} from '#/state/queries/profile'
32import {useSession} from '#/state/session'
33import {useLoggedOutViewControls} from '#/state/shell/logged-out'
34import {useShellLayout} from '#/state/shell/shell-layout'
35import {useCloseAllActiveElements} from '#/state/util'
36import {Button} from '#/view/com/util/forms/Button'
37import {Text} from '#/view/com/util/text/Text'
38import {UserAvatar} from '#/view/com/util/UserAvatar'
39import {Logo} from '#/view/icons/Logo'
40import {Logotype} from '#/view/icons/Logotype'
41import {useDialogControl} from '#/components/Dialog'
42import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
43import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
44import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope'
45import {styles} from './BottomBarStyles'
46
47type TabOptions =
48 | 'Home'
49 | 'Search'
50 | 'Notifications'
51 | 'MyProfile'
52 | 'Feeds'
53 | 'Messages'
54
55export function BottomBar({navigation}: BottomTabBarProps) {
56 const {hasSession, currentAccount} = useSession()
57 const pal = usePalette('default')
58 const {_} = useLingui()
59 const safeAreaInsets = useSafeAreaInsets()
60 const {track} = useAnalytics()
61 const {footerHeight} = useShellLayout()
62 const {
63 isAtHome,
64 isAtSearch,
65 isAtFeeds,
66 isAtNotifications,
67 isAtMyProfile,
68 isAtMessages,
69 } = useNavigationTabState()
70 const numUnreadNotifications = useUnreadNotifications()
71 const {footerMinimalShellTransform} = useMinimalShellMode()
72 const {data: profile} = useProfileQuery({did: currentAccount?.did})
73 const {requestSwitchToAccount} = useLoggedOutViewControls()
74 const closeAllActiveElements = useCloseAllActiveElements()
75 const dedupe = useDedupe()
76 const accountSwitchControl = useDialogControl()
77 const playHaptic = useHaptics()
78 const gate = useGate()
79
80 const showSignIn = React.useCallback(() => {
81 closeAllActiveElements()
82 requestSwitchToAccount({requestedAccount: 'none'})
83 }, [requestSwitchToAccount, closeAllActiveElements])
84
85 const showCreateAccount = React.useCallback(() => {
86 closeAllActiveElements()
87 requestSwitchToAccount({requestedAccount: 'new'})
88 // setShowLoggedOut(true)
89 }, [requestSwitchToAccount, closeAllActiveElements])
90
91 const onPressTab = React.useCallback(
92 (tab: TabOptions) => {
93 track(`MobileShell:${tab}ButtonPressed`)
94 const state = navigation.getState()
95 const tabState = getTabState(state, tab)
96 if (tabState === TabState.InsideAtRoot) {
97 emitSoftReset()
98 } else if (tabState === TabState.Inside) {
99 dedupe(() => navigation.dispatch(StackActions.popToTop()))
100 } else {
101 dedupe(() => navigation.navigate(`${tab}Tab`))
102 }
103 },
104 [track, navigation, dedupe],
105 )
106 const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
107 const onPressSearch = React.useCallback(
108 () => onPressTab('Search'),
109 [onPressTab],
110 )
111 const onPressFeeds = React.useCallback(
112 () => onPressTab('Feeds'),
113 [onPressTab],
114 )
115 const onPressNotifications = React.useCallback(
116 () => onPressTab('Notifications'),
117 [onPressTab],
118 )
119 const onPressProfile = React.useCallback(() => {
120 onPressTab('MyProfile')
121 }, [onPressTab])
122
123 const onPressMessages = React.useCallback(() => {
124 onPressTab('Messages')
125 }, [onPressTab])
126
127 const onLongPressProfile = React.useCallback(() => {
128 playHaptic()
129 accountSwitchControl.open()
130 }, [accountSwitchControl, playHaptic])
131
132 return (
133 <>
134 <SwitchAccountDialog control={accountSwitchControl} />
135
136 <Animated.View
137 style={[
138 styles.bottomBar,
139 pal.view,
140 pal.border,
141 {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
142 footerMinimalShellTransform,
143 ]}
144 onLayout={e => {
145 footerHeight.value = e.nativeEvent.layout.height
146 }}>
147 {hasSession ? (
148 <>
149 <Btn
150 testID="bottomBarHomeBtn"
151 icon={
152 isAtHome ? (
153 <HomeIconSolid
154 strokeWidth={4}
155 size={24}
156 style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
157 />
158 ) : (
159 <HomeIcon
160 strokeWidth={4}
161 size={24}
162 style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
163 />
164 )
165 }
166 onPress={onPressHome}
167 accessibilityRole="tab"
168 accessibilityLabel={_(msg`Home`)}
169 accessibilityHint=""
170 />
171 <Btn
172 testID="bottomBarSearchBtn"
173 icon={
174 isAtSearch ? (
175 <MagnifyingGlassIcon2Solid
176 size={25}
177 style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
178 strokeWidth={1.8}
179 />
180 ) : (
181 <MagnifyingGlassIcon2
182 size={25}
183 style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
184 strokeWidth={1.8}
185 />
186 )
187 }
188 onPress={onPressSearch}
189 accessibilityRole="search"
190 accessibilityLabel={_(msg`Search`)}
191 accessibilityHint=""
192 />
193 <Btn
194 testID="bottomBarFeedsBtn"
195 icon={
196 isAtFeeds ? (
197 <HashtagIcon
198 size={24}
199 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
200 strokeWidth={4}
201 />
202 ) : (
203 <HashtagIcon
204 size={24}
205 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
206 strokeWidth={2.25}
207 />
208 )
209 }
210 onPress={onPressFeeds}
211 accessibilityRole="tab"
212 accessibilityLabel={_(msg`Feeds`)}
213 accessibilityHint=""
214 />
215 <Btn
216 testID="bottomBarNotificationsBtn"
217 icon={
218 isAtNotifications ? (
219 <BellIconSolid
220 size={24}
221 strokeWidth={1.9}
222 style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
223 />
224 ) : (
225 <BellIcon
226 size={24}
227 strokeWidth={1.9}
228 style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
229 />
230 )
231 }
232 onPress={onPressNotifications}
233 notificationCount={numUnreadNotifications}
234 accessible={true}
235 accessibilityRole="tab"
236 accessibilityLabel={_(msg`Notifications`)}
237 accessibilityHint={
238 numUnreadNotifications === ''
239 ? ''
240 : `${numUnreadNotifications} unread`
241 }
242 />
243 {gate('dms') && (
244 <Btn
245 testID="bottomBarMessagesBtn"
246 icon={
247 isAtMessages ? (
248 <EnvelopeFilled
249 size="lg"
250 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
251 />
252 ) : (
253 <Envelope
254 size="lg"
255 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
256 />
257 )
258 }
259 onPress={onPressMessages}
260 accessibilityRole="tab"
261 accessibilityLabel={_(msg`Messages`)}
262 accessibilityHint=""
263 />
264 )}
265 <Btn
266 testID="bottomBarProfileBtn"
267 icon={
268 <View style={styles.ctrlIconSizingWrapper}>
269 {isAtMyProfile ? (
270 <View
271 style={[
272 styles.ctrlIcon,
273 pal.text,
274 styles.profileIcon,
275 styles.onProfile,
276 {borderColor: pal.text.color},
277 ]}>
278 <UserAvatar
279 avatar={profile?.avatar}
280 size={27}
281 // See https://github.com/bluesky-social/social-app/pull/1801:
282 usePlainRNImage={true}
283 type={profile?.associated?.labeler ? 'labeler' : 'user'}
284 />
285 </View>
286 ) : (
287 <View
288 style={[styles.ctrlIcon, pal.text, styles.profileIcon]}>
289 <UserAvatar
290 avatar={profile?.avatar}
291 size={28}
292 // See https://github.com/bluesky-social/social-app/pull/1801:
293 usePlainRNImage={true}
294 type={profile?.associated?.labeler ? 'labeler' : 'user'}
295 />
296 </View>
297 )}
298 </View>
299 }
300 onPress={onPressProfile}
301 onLongPress={onLongPressProfile}
302 accessibilityRole="tab"
303 accessibilityLabel={_(msg`Profile`)}
304 accessibilityHint=""
305 />
306 </>
307 ) : (
308 <>
309 <View
310 style={{
311 width: '100%',
312 flexDirection: 'row',
313 alignItems: 'center',
314 justifyContent: 'space-between',
315 paddingTop: 14,
316 paddingBottom: 2,
317 paddingLeft: 14,
318 paddingRight: 6,
319 gap: 8,
320 }}>
321 <View
322 style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
323 <Logo width={28} />
324 <View style={{paddingTop: 4}}>
325 <Logotype width={80} fill={pal.text.color} />
326 </View>
327 </View>
328
329 <View
330 style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
331 <Button
332 onPress={showCreateAccount}
333 accessibilityHint={_(msg`Sign up`)}
334 accessibilityLabel={_(msg`Sign up`)}>
335 <Text type="md" style={[{color: 'white'}, s.bold]}>
336 <Trans>Sign up</Trans>
337 </Text>
338 </Button>
339
340 <Button
341 type="default"
342 onPress={showSignIn}
343 accessibilityHint={_(msg`Sign in`)}
344 accessibilityLabel={_(msg`Sign in`)}>
345 <Text type="md" style={[pal.text, s.bold]}>
346 <Trans>Sign in</Trans>
347 </Text>
348 </Button>
349 </View>
350 </View>
351 </>
352 )}
353 </Animated.View>
354 </>
355 )
356}
357
358interface BtnProps
359 extends Pick<
360 ComponentProps<typeof TouchableOpacity>,
361 | 'accessible'
362 | 'accessibilityRole'
363 | 'accessibilityHint'
364 | 'accessibilityLabel'
365 > {
366 testID?: string
367 icon: JSX.Element
368 notificationCount?: string
369 onPress?: (event: GestureResponderEvent) => void
370 onLongPress?: (event: GestureResponderEvent) => void
371}
372
373function Btn({
374 testID,
375 icon,
376 notificationCount,
377 onPress,
378 onLongPress,
379 accessible,
380 accessibilityHint,
381 accessibilityLabel,
382}: BtnProps) {
383 return (
384 <TouchableOpacity
385 testID={testID}
386 style={styles.ctrl}
387 onPress={onLongPress ? onPress : undefined}
388 onPressIn={onLongPress ? undefined : onPress}
389 onLongPress={onLongPress}
390 accessible={accessible}
391 accessibilityLabel={accessibilityLabel}
392 accessibilityHint={accessibilityHint}>
393 {icon}
394 {notificationCount ? (
395 <View style={[styles.notificationCount]}>
396 <Text style={styles.notificationCountLabel}>{notificationCount}</Text>
397 </View>
398 ) : undefined}
399 </TouchableOpacity>
400 )
401}