import {type ComponentProps, type JSX, memo, useCallback} from 'react' import { Linking, Pressable, ScrollView, TouchableOpacity, View, } from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg, plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' import {StackActions, useNavigation} from '@react-navigation/native' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {type PressableScale} from '#/lib/custom-animations/PressableScale' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import { formatCountsMetricNumber, shouldShowCountsMetricLabelOnly, shouldShowProfileCountsMetric, } from '#/lib/metrics-display' import {getTabState, TabState} from '#/lib/routes/helpers' import {type SharedNavTab, TAB_TO_NAV_ITEM} from '#/lib/routes/tab-to-nav-item' import {type NavigationProp} from '#/lib/routes/types' import {getShapeStyle} from '#/lib/shapes' import {sanitizeHandle} from '#/lib/strings/handles' import {emitSoftReset} from '#/state/events' import {badgeText, useBadgePreference} from '#/state/preferences/badge-text' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import { useFollowersMetricsDisplay, useFollowingMetricsDisplay, useNotificationsTabBadgeDisplay, } from '#/state/preferences/metrics-display-preference' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useProfileQuery} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' import {useSetDrawerOpen, useThemePrefs} from '#/state/shell' import {UserAvatar} from '#/view/com/util/UserAvatar' import {useLogoVariant} from '#/view/icons/useLogoVariant' import {NavSignupCard} from '#/view/shell/NavSignupCard' import {atoms as a, tokens, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {Divider} from '#/components/Divider' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRight} from '#/components/icons/ArrowShareRight' import { Bell_Filled_Corner0_Rounded as BellFilled, Bell_Stroke2_Corner0_Rounded as Bell, } from '#/components/icons/Bell' import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' import {BulletList_Stroke2_Corner0_Rounded as List} from '#/components/icons/BulletList' import { Hashtag_Filled_Corner0_Rounded as HashtagFilled, Hashtag_Stroke2_Corner0_Rounded as Hashtag, } from '#/components/icons/Hashtag' import { HomeOpen_Filled_Corner0_Rounded as HomeFilled, HomeOpen_Stoke2_Corner0_Rounded as Home, } from '#/components/icons/HomeOpen' import { MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled, MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlass, } from '#/components/icons/MagnifyingGlass' import { Message_Stroke2_Corner0_Rounded as Message, Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, } from '#/components/icons/Message' import {SettingsGear2_Stroke2_Corner0_Rounded as Settings} from '#/components/icons/SettingsGear2' import { UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Stroke2_Corner0_Rounded as UserCircle, } from '#/components/icons/UserCircle' import {InlineLinkText} from '#/components/Link' import {OTAChannelNotice} from '#/components/OTAChannelNotice' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE, IS_WEB} from '#/env' import {InviteFriendsDialog} from '#/features/inviteFriends' import {useActorStatus} from '#/features/liveNow' import {accentForeground} from '#/features/themes/accentForeground' import {useActiveThemeUpdate} from '#/features/themes/api' const iconWidth = 26 let DrawerProfileCard = ({ account, onPressProfile, onPressShare, }: { account: SessionAccount onPressProfile: () => void onPressShare?: () => void }): React.ReactNode => { const {_, i18n} = useLingui() const t = useTheme() const {data: profile} = useProfileQuery({did: account.did}) const {isActive: live} = useActorStatus(profile) const followersMetricsDisplay = useFollowersMetricsDisplay() const followingMetricsDisplay = useFollowingMetricsDisplay() const followersCount = profile?.followersCount ?? 0 const followingCount = profile?.followsCount ?? 0 const showFollowers = shouldShowProfileCountsMetric( followersMetricsDisplay, followersCount, ) const showFollowing = shouldShowProfileCountsMetric( followingMetricsDisplay, followingCount, ) return ( {profile?.displayName || account.handle} {profile && } {onPressShare && ( [ a.ml_auto, { width: 32, height: 32, borderRadius: 16, backgroundColor: t.palette.contrast_50, alignItems: 'center', justifyContent: 'center', opacity: pressed ? 0.7 : 1, }, ]}> )} {sanitizeHandle(account.handle, '@')} {!showFollowers && !showFollowing ? null : ( {showFollowers ? ( {!shouldShowCountsMetricLabelOnly( followersMetricsDisplay, followersCount, ) ? ( {formatCountsMetricNumber( i18n, followersMetricsDisplay, followersCount, )} ) : null}{' '} ) : null} {showFollowers && showFollowing ? ( {' '} ·{' '} ) : null} {showFollowing ? ( {!shouldShowCountsMetricLabelOnly( followingMetricsDisplay, followingCount, ) ? ( {formatCountsMetricNumber( i18n, followingMetricsDisplay, followingCount, )} ) : null}{' '} ) : null} )} ) } DrawerProfileCard = memo(DrawerProfileCard) export {DrawerProfileCard} let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { const t = useTheme() const insets = useSafeAreaInsets() const setDrawerOpen = useSetDrawerOpen() const navigation = useNavigation() const ax = useAnalytics() const { isAtHome, isAtSearch, isAtFeeds, isAtBookmarks, isAtNotifications, isAtMyProfile, isAtMessages, } = useNavigationTabState() const {hasSession, currentAccount} = useSession() const inviteFriendsControl = useDialogControl() // events // = const onPressTab = useCallback( (tab: SharedNavTab, surface: 'drawer' | 'drawerHeader' = 'drawer') => { ax.metric('nav:click', { item: TAB_TO_NAV_ITEM[tab], surface, }) const state = navigation.getState() setDrawerOpen(false) if (IS_WEB) { // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh if (tab === 'MyProfile') { navigation.navigate('Profile', {name: currentAccount!.handle}) } else { // @ts-expect-error struggles with string unions, apparently navigation.navigate(tab) } } else { const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { emitSoftReset() } else if (tabState === TabState.Inside) { // find the correct navigator in which to pop-to-top const target = state.routes.find(route => route.name === `${tab}Tab`) ?.state?.key if (target) { // if we found it, trigger pop-to-top navigation.dispatch({ ...StackActions.popToTop(), target, }) } else { // fallback: reset navigation navigation.reset({ index: 0, routes: [{name: `${tab}Tab`}], }) } } else { navigation.navigate(`${tab}Tab`) } } }, [navigation, setDrawerOpen, currentAccount, ax], ) const onPressHome = useCallback(() => onPressTab('Home'), [onPressTab]) const onPressSearch = useCallback(() => onPressTab('Search'), [onPressTab]) const onPressMessages = useCallback( () => onPressTab('Messages'), [onPressTab], ) const onPressNotifications = useCallback( () => onPressTab('Notifications'), [onPressTab], ) const onPressProfile = useCallback(() => { onPressTab('MyProfile') }, [onPressTab]) const onPressDrawerHeaderProfile = useCallback(() => { onPressTab('MyProfile', 'drawerHeader') }, [onPressTab]) const onPressMyFeeds = useCallback(() => { ax.metric('nav:click', {item: 'feeds', surface: 'drawer'}) navigation.navigate('Feeds') setDrawerOpen(false) }, [navigation, setDrawerOpen, ax]) const onPressLists = useCallback(() => { ax.metric('nav:click', {item: 'lists', surface: 'drawer'}) navigation.navigate('Lists') setDrawerOpen(false) }, [navigation, setDrawerOpen, ax]) const onPressBookmarks = useCallback(() => { ax.metric('nav:click', {item: 'saved', surface: 'drawer'}) navigation.navigate('Bookmarks') setDrawerOpen(false) }, [navigation, setDrawerOpen, ax]) const onPressSettings = useCallback(() => { ax.metric('nav:click', {item: 'settings', surface: 'drawer'}) navigation.navigate('Settings') setDrawerOpen(false) }, [navigation, setDrawerOpen, ax]) const onPressFeedback = useCallback(() => { void Linking.openURL( FEEDBACK_FORM_URL({ email: currentAccount?.email, handle: currentAccount?.handle, }), ) }, [currentAccount]) const onPressHelp = useCallback(() => { void Linking.openURL(HELP_DESK_URL) }, []) // rendering // = return ( {hasSession && currentAccount ? ( { ax.metric('invite:dialog:open', {logContext: 'Drawer'}) setDrawerOpen(false) inviteFriendsControl.open() } : undefined } /> ) : ( )} {hasSession ? ( <> ) : ( <> )} ) } DrawerContent = memo(DrawerContent) export {DrawerContent} let DrawerFooter = ({ onPressFeedback, onPressHelp, }: { onPressFeedback: () => void onPressHelp: () => void }): React.ReactNode => { const {_} = useLingui() const insets = useSafeAreaInsets() return ( ) } DrawerFooter = memo(DrawerFooter) interface MenuItemProps extends ComponentProps { icon: JSX.Element label: string count?: string hasNew?: boolean bold?: boolean } let SearchMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg`Explore`)} bold={isActive} onPress={onPress} /> ) } SearchMenuItem = memo(SearchMenuItem) let HomeMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg`Home`)} bold={isActive} onPress={onPress} /> ) } HomeMenuItem = memo(HomeMenuItem) let ChatMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg`Chat`)} bold={isActive} onPress={onPress} /> ) } ChatMenuItem = memo(ChatMenuItem) let NotificationsMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() const numUnreadNotifications = useUnreadNotifications() const [notificationsCustomText] = useBadgePreference('notificationsBadgeText') const notificationsTabBadgeDisplay = useNotificationsTabBadgeDisplay() const notificationsText = notificationsTabBadgeDisplay === 'text' ? notificationsCustomText : undefined return ( ) : ( ) } label={_(msg`Notifications`)} accessibilityHint={ numUnreadNotifications === '' ? '' : _( plural(numUnreadNotifications ?? 0, { one: '# unread item', other: '# unread items', }), ) } count={ notificationsTabBadgeDisplay === 'exact' || notificationsTabBadgeDisplay === 'text' ? badgeText(numUnreadNotifications, notificationsText) : undefined } hasNew={ notificationsTabBadgeDisplay === 'visible' && numUnreadNotifications !== '' } bold={isActive} onPress={onPress} /> ) } NotificationsMenuItem = memo(NotificationsMenuItem) let FeedsMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg`Feeds`)} bold={isActive} onPress={onPress} /> ) } FeedsMenuItem = memo(FeedsMenuItem) let ListsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( } label={_(msg`Lists`)} onPress={onPress} /> ) } ListsMenuItem = memo(ListsMenuItem) let BookmarksMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg({message: 'Saved', context: 'link to bookmarks screen'}))} onPress={onPress} /> ) } BookmarksMenuItem = memo(BookmarksMenuItem) let ProfileMenuItem = ({ isActive, onPress, }: { isActive: boolean onPress: () => void }): React.ReactNode => { const {_} = useLingui() const t = useTheme() return ( ) : ( ) } label={_(msg`Profile`)} onPress={onPress} /> ) } ProfileMenuItem = memo(ProfileMenuItem) let SettingsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => { const {_} = useLingui() const t = useTheme() const {activeTheme} = useThemePrefs() const themeUpdate = useActiveThemeUpdate(activeTheme) return ( } label={_(msg`Settings`)} hasNew={Boolean(themeUpdate)} onPress={onPress} /> ) } SettingsMenuItem = memo(SettingsMenuItem) function MenuItem({icon, label, count, hasNew, bold, onPress}: MenuItemProps) { const t = useTheme() const enableSquareButtons = useEnableSquareButtons() return ( ) } function ExtraLinks() { const {_} = useLingui() const t = useTheme() const logoVariant = useLogoVariant() return ( Terms of Service Privacy Policy {logoVariant === 'kawaii' && ( Kawaii logo by{' '} ovvie )} ) }