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