mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 506 lines 14 kB view raw
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})