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