mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {StyleSheet, TouchableOpacity, View} from 'react-native'
3import {
4 FontAwesomeIcon,
5 FontAwesomeIconStyle,
6} from '@fortawesome/react-native-fontawesome'
7import {msg, Trans} from '@lingui/macro'
8import {useLingui} from '@lingui/react'
9import {
10 useLinkProps,
11 useNavigation,
12 useNavigationState,
13} from '@react-navigation/native'
14
15import {isInvalidHandle} from '#/lib/strings/handles'
16import {emitSoftReset} from '#/state/events'
17import {useFetchHandle} from '#/state/queries/handle'
18import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
19import {useUnreadNotifications} from '#/state/queries/notifications/unread'
20import {useProfileQuery} from '#/state/queries/profile'
21import {useSession} from '#/state/session'
22import {useComposerControls} from '#/state/shell/composer'
23import {usePalette} from 'lib/hooks/usePalette'
24import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
25import {getCurrentRoute, isStateAtTabRoot, isTab} from 'lib/routes/helpers'
26import {makeProfileLink} from 'lib/routes/links'
27import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
28import {colors, s} from 'lib/styles'
29import {NavSignupCard} from '#/view/shell/NavSignupCard'
30import {Link} from 'view/com/util/Link'
31import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
32import {PressableWithHover} from 'view/com/util/PressableWithHover'
33import {Text} from 'view/com/util/text/Text'
34import {UserAvatar} from 'view/com/util/UserAvatar'
35import {
36 Bell_Filled_Corner0_Rounded as BellFilled,
37 Bell_Stroke2_Corner0_Rounded as Bell,
38} from '#/components/icons/Bell'
39import {
40 BulletList_Filled_Corner0_Rounded as ListFilled,
41 BulletList_Stroke2_Corner0_Rounded as List,
42} from '#/components/icons/BulletList'
43import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig'
44import {
45 Hashtag_Filled_Corner0_Rounded as HashtagFilled,
46 Hashtag_Stroke2_Corner0_Rounded as Hashtag,
47} from '#/components/icons/Hashtag'
48import {
49 HomeOpen_Filled_Corner0_Rounded as HomeFilled,
50 HomeOpen_Stoke2_Corner0_Rounded as Home,
51} from '#/components/icons/HomeOpen'
52import {MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled} from '#/components/icons/MagnifyingGlass'
53import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
54import {
55 Message_Stroke2_Corner0_Rounded as Message,
56 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
57} from '#/components/icons/Message'
58import {
59 SettingsGear2_Filled_Corner0_Rounded as SettingsFilled,
60 SettingsGear2_Stroke2_Corner0_Rounded as Settings,
61} from '#/components/icons/SettingsGear2'
62import {
63 UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
64 UserCircle_Stroke2_Corner0_Rounded as UserCircle,
65} from '#/components/icons/UserCircle'
66import {router} from '../../../routes'
67
68const NAV_ICON_WIDTH = 28
69
70function ProfileCard() {
71 const {currentAccount} = useSession()
72 const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did})
73 const {isDesktop} = useWebMediaQueries()
74 const {_} = useLingui()
75 const size = 48
76
77 return !isLoading && profile ? (
78 <Link
79 href={makeProfileLink({
80 did: currentAccount!.did,
81 handle: currentAccount!.handle,
82 })}
83 style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
84 title={_(msg`My Profile`)}
85 asAnchor>
86 <UserAvatar
87 avatar={profile.avatar}
88 size={size}
89 type={profile?.associated?.labeler ? 'labeler' : 'user'}
90 />
91 </Link>
92 ) : (
93 <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>
94 <LoadingPlaceholder
95 width={size}
96 height={size}
97 style={{borderRadius: size}}
98 />
99 </View>
100 )
101}
102
103const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
104
105function BackBtn() {
106 const {isTablet} = useWebMediaQueries()
107 const pal = usePalette('default')
108 const navigation = useNavigation<NavigationProp>()
109 const {_} = useLingui()
110 const shouldShow = useNavigationState(
111 state =>
112 !isStateAtTabRoot(state) &&
113 !HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
114 )
115
116 const onPressBack = React.useCallback(() => {
117 if (navigation.canGoBack()) {
118 navigation.goBack()
119 } else {
120 navigation.navigate('Home')
121 }
122 }, [navigation])
123
124 if (!shouldShow || isTablet) {
125 return <></>
126 }
127 return (
128 <TouchableOpacity
129 testID="viewHeaderBackOrMenuBtn"
130 onPress={onPressBack}
131 style={styles.backBtn}
132 accessibilityRole="button"
133 accessibilityLabel={_(msg`Go back`)}
134 accessibilityHint="">
135 <FontAwesomeIcon
136 size={24}
137 icon="angle-left"
138 style={pal.text as FontAwesomeIconStyle}
139 />
140 </TouchableOpacity>
141 )
142}
143
144interface NavItemProps {
145 count?: string
146 href: string
147 icon: JSX.Element
148 iconFilled: JSX.Element
149 label: string
150}
151function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
152 const pal = usePalette('default')
153 const {currentAccount} = useSession()
154 const {isDesktop, isTablet} = useWebMediaQueries()
155 const [pathName] = React.useMemo(() => router.matchPath(href), [href])
156 const currentRouteInfo = useNavigationState(state => {
157 if (!state) {
158 return {name: 'Home'}
159 }
160 return getCurrentRoute(state)
161 })
162 let isCurrent =
163 currentRouteInfo.name === 'Profile'
164 ? isTab(currentRouteInfo.name, pathName) &&
165 (currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
166 currentAccount?.handle
167 : isTab(currentRouteInfo.name, pathName)
168 const {onPress} = useLinkProps({to: href})
169 const onPressWrapped = React.useCallback(
170 (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
171 if (e.ctrlKey || e.metaKey || e.altKey) {
172 return
173 }
174 e.preventDefault()
175 if (isCurrent) {
176 emitSoftReset()
177 } else {
178 onPress()
179 }
180 },
181 [onPress, isCurrent],
182 )
183
184 return (
185 <PressableWithHover
186 style={styles.navItemWrapper}
187 hoverStyle={pal.viewLight}
188 // @ts-ignore the function signature differs on web -prf
189 onPress={onPressWrapped}
190 // @ts-ignore web only -prf
191 href={href}
192 dataSet={{noUnderline: 1}}
193 accessibilityRole="tab"
194 accessibilityLabel={label}
195 accessibilityHint="">
196 <View
197 style={[
198 styles.navItemIconWrapper,
199 isTablet && styles.navItemIconWrapperTablet,
200 ]}>
201 {isCurrent ? iconFilled : icon}
202 {typeof count === 'string' && count ? (
203 <Text
204 type="button"
205 style={[
206 styles.navItemCount,
207 isTablet && styles.navItemCountTablet,
208 ]}>
209 {count}
210 </Text>
211 ) : null}
212 </View>
213 {isDesktop && (
214 <Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
215 {label}
216 </Text>
217 )}
218 </PressableWithHover>
219 )
220}
221
222function ComposeBtn() {
223 const {currentAccount} = useSession()
224 const {getState} = useNavigation()
225 const {openComposer} = useComposerControls()
226 const {_} = useLingui()
227 const {isTablet} = useWebMediaQueries()
228 const [isFetchingHandle, setIsFetchingHandle] = React.useState(false)
229 const fetchHandle = useFetchHandle()
230
231 const getProfileHandle = async () => {
232 const routes = getState()?.routes
233 const currentRoute = routes?.[routes?.length - 1]
234
235 if (currentRoute?.name === 'Profile') {
236 let handle: string | undefined = (
237 currentRoute.params as CommonNavigatorParams['Profile']
238 ).name
239
240 if (handle.startsWith('did:')) {
241 try {
242 setIsFetchingHandle(true)
243 handle = await fetchHandle(handle)
244 } catch (e) {
245 handle = undefined
246 } finally {
247 setIsFetchingHandle(false)
248 }
249 }
250
251 if (
252 !handle ||
253 handle === currentAccount?.handle ||
254 isInvalidHandle(handle)
255 )
256 return undefined
257
258 return handle
259 }
260
261 return undefined
262 }
263
264 const onPressCompose = async () =>
265 openComposer({mention: await getProfileHandle()})
266
267 if (isTablet) {
268 return null
269 }
270 return (
271 <View style={styles.newPostBtnContainer}>
272 <TouchableOpacity
273 disabled={isFetchingHandle}
274 style={styles.newPostBtn}
275 onPress={onPressCompose}
276 accessibilityRole="button"
277 accessibilityLabel={_(msg`New post`)}
278 accessibilityHint="">
279 <View style={styles.newPostBtnIconWrapper}>
280 <EditBig width={19} style={styles.newPostBtnLabel} />
281 </View>
282 <Text type="button" style={styles.newPostBtnLabel}>
283 <Trans context="action">New Post</Trans>
284 </Text>
285 </TouchableOpacity>
286 </View>
287 )
288}
289
290function ChatNavItem() {
291 const pal = usePalette('default')
292 const {_} = useLingui()
293 const numUnreadMessages = useUnreadMessageCount()
294
295 return (
296 <NavItem
297 href="/messages"
298 count={numUnreadMessages.numUnread}
299 icon={<Message style={pal.text} width={NAV_ICON_WIDTH} />}
300 iconFilled={<MessageFilled style={pal.text} width={NAV_ICON_WIDTH} />}
301 label={_(msg`Chat`)}
302 />
303 )
304}
305
306export function DesktopLeftNav() {
307 const {hasSession, currentAccount} = useSession()
308 const pal = usePalette('default')
309 const {_} = useLingui()
310 const {isDesktop, isTablet} = useWebMediaQueries()
311 const numUnreadNotifications = useUnreadNotifications()
312
313 if (!hasSession && !isDesktop) {
314 return null
315 }
316
317 return (
318 <View
319 style={[
320 styles.leftNav,
321 isTablet && styles.leftNavTablet,
322 pal.view,
323 pal.border,
324 ]}>
325 {hasSession ? (
326 <ProfileCard />
327 ) : isDesktop ? (
328 <View style={{paddingHorizontal: 12}}>
329 <NavSignupCard />
330 </View>
331 ) : null}
332
333 {hasSession && (
334 <>
335 <BackBtn />
336
337 <NavItem
338 href="/"
339 icon={<Home width={NAV_ICON_WIDTH} style={pal.text} />}
340 iconFilled={<HomeFilled width={NAV_ICON_WIDTH} style={pal.text} />}
341 label={_(msg`Home`)}
342 />
343 <NavItem
344 href="/search"
345 icon={<MagnifyingGlass style={pal.text} width={NAV_ICON_WIDTH} />}
346 iconFilled={
347 <MagnifyingGlassFilled style={pal.text} width={NAV_ICON_WIDTH} />
348 }
349 label={_(msg`Search`)}
350 />
351 <NavItem
352 href="/notifications"
353 count={numUnreadNotifications}
354 icon={<Bell width={NAV_ICON_WIDTH} style={pal.text} />}
355 iconFilled={<BellFilled width={NAV_ICON_WIDTH} style={pal.text} />}
356 label={_(msg`Notifications`)}
357 />
358 <ChatNavItem />
359 <NavItem
360 href="/feeds"
361 icon={
362 <Hashtag
363 style={pal.text as FontAwesomeIconStyle}
364 width={NAV_ICON_WIDTH}
365 />
366 }
367 iconFilled={
368 <HashtagFilled
369 style={pal.text as FontAwesomeIconStyle}
370 width={NAV_ICON_WIDTH}
371 />
372 }
373 label={_(msg`Feeds`)}
374 />
375 <NavItem
376 href="/lists"
377 icon={<List style={pal.text} width={NAV_ICON_WIDTH} />}
378 iconFilled={<ListFilled style={pal.text} width={NAV_ICON_WIDTH} />}
379 label={_(msg`Lists`)}
380 />
381 <NavItem
382 href={currentAccount ? makeProfileLink(currentAccount) : '/'}
383 icon={<UserCircle width={NAV_ICON_WIDTH} style={pal.text} />}
384 iconFilled={
385 <UserCircleFilled width={NAV_ICON_WIDTH} style={pal.text} />
386 }
387 label={_(msg`Profile`)}
388 />
389 <NavItem
390 href="/settings"
391 icon={<Settings width={NAV_ICON_WIDTH} style={pal.text} />}
392 iconFilled={
393 <SettingsFilled width={NAV_ICON_WIDTH} style={pal.text} />
394 }
395 label={_(msg`Settings`)}
396 />
397
398 <ComposeBtn />
399 </>
400 )}
401 </View>
402 )
403}
404
405const styles = StyleSheet.create({
406 leftNav: {
407 // @ts-ignore web only
408 position: 'fixed',
409 top: 10,
410 // @ts-ignore web only
411 left: 'calc(50vw - 300px - 220px - 20px)',
412 width: 220,
413 // @ts-ignore web only
414 maxHeight: 'calc(100vh - 10px)',
415 overflowY: 'auto',
416 },
417 leftNavTablet: {
418 top: 0,
419 left: 0,
420 right: 'auto',
421 borderRightWidth: 1,
422 height: '100%',
423 width: 76,
424 alignItems: 'center',
425 },
426
427 profileCard: {
428 marginVertical: 10,
429 width: 90,
430 paddingLeft: 12,
431 },
432 profileCardTablet: {
433 width: 70,
434 },
435
436 backBtn: {
437 position: 'absolute',
438 top: 12,
439 right: 12,
440 width: 30,
441 height: 30,
442 },
443
444 navItemWrapper: {
445 flexDirection: 'row',
446 alignItems: 'center',
447 paddingHorizontal: 12,
448 padding: 12,
449 borderRadius: 8,
450 gap: 10,
451 },
452 navItemIconWrapper: {
453 alignItems: 'center',
454 justifyContent: 'center',
455 width: 28,
456 height: 24,
457 marginTop: 2,
458 zIndex: 1,
459 },
460 navItemIconWrapperTablet: {
461 width: 40,
462 height: 40,
463 },
464 navItemCount: {
465 position: 'absolute',
466 top: 0,
467 left: 15,
468 backgroundColor: colors.blue3,
469 color: colors.white,
470 fontSize: 12,
471 fontWeight: 'bold',
472 paddingHorizontal: 4,
473 borderRadius: 6,
474 },
475 navItemCountTablet: {
476 left: 18,
477 fontSize: 14,
478 },
479
480 newPostBtnContainer: {
481 flexDirection: 'row',
482 },
483 newPostBtn: {
484 flexDirection: 'row',
485 alignItems: 'center',
486 justifyContent: 'center',
487 borderRadius: 24,
488 paddingTop: 10,
489 paddingBottom: 12, // visually aligns the text vertically inside the button
490 paddingLeft: 16,
491 paddingRight: 18, // looks nicer like this
492 backgroundColor: colors.blue3,
493 marginLeft: 12,
494 marginTop: 20,
495 marginBottom: 10,
496 gap: 8,
497 },
498 newPostBtnIconWrapper: {
499 marginTop: 2, // aligns the icon visually with the text
500 },
501 newPostBtnLabel: {
502 color: colors.white,
503 fontSize: 16,
504 fontWeight: '600',
505 },
506})