Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

[Clipclops] New routes with placeholder screens (#3725)

* add new routes with placeholder screens

* gate content

* add filled envelope style

* swap filled state

* switch to `useAgent`

authored by samuel.fm and committed by

GitHub ce85375c 1af59ca8

+486 -19
+1
assets/icons/envelope_filled_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 11.708 2.654 4.06A.998.998 0 0 1 3 4h18c.122 0 .238.022.346.061L12 11.708ZM2 19V6.11l9.367 7.664a1 1 0 0 0 1.266 0L22 6.11V19a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
+1
assets/icons/settingsSliderVertical_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 3a1 1 0 0 1 1 1v1.126a4 4 0 0 1 0 7.748V20a1 1 0 1 1-2 0v-7.126a4 4 0 0 1 0-7.748V4a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v9.126a4 4 0 1 1-2 0V4a1 1 0 0 1 1-1ZM7 7a2 2 0 1 0 0 4 2 2 0 1 0 0-4Zm10 8a2 2 0 1 0 0 4 2 2 0 1 0 0-4Z" clip-rule="evenodd"/></svg>
+2
bskyweb/cmd/bskyweb/server.go
··· 200 200 e.GET("/support/community-guidelines", server.WebGeneric) 201 201 e.GET("/support/copyright", server.WebGeneric) 202 202 e.GET("/intent/compose", server.WebGeneric) 203 + e.GET("/messages", server.WebGeneric) 204 + e.GET("/messages/:conversation", server.WebGeneric) 203 205 204 206 // profile endpoints; only first populates info 205 207 e.GET("/profile/:handleOrDID", server.WebProfile)
+50
src/Navigation.tsx
··· 25 25 FeedsTabNavigatorParams, 26 26 FlatNavigatorParams, 27 27 HomeTabNavigatorParams, 28 + MessagesTabNavigatorParams, 28 29 MyProfileTabNavigatorParams, 29 30 NotificationsTabNavigatorParams, 30 31 SearchTabNavigatorParams, ··· 46 47 import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' 47 48 import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig' 48 49 import {router} from './routes' 50 + import {MessagesConversationScreen} from './screens/Messages/Conversation' 51 + import {MessagesListScreen} from './screens/Messages/List' 52 + import {MessagesSettingsScreen} from './screens/Messages/Settings' 49 53 import {useModalControls} from './state/modals' 50 54 import {useUnreadNotifications} from './state/queries/notifications/unread' 51 55 import {useSession} from './state/session' ··· 92 96 createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() 93 97 const MyProfileTab = 94 98 createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() 99 + const MessagesTab = 100 + createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>() 95 101 const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() 96 102 const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() 97 103 ··· 290 296 getComponent={() => HashtagScreen} 291 297 options={{title: title(msg`Hashtag`)}} 292 298 /> 299 + <Stack.Screen 300 + name="MessagesConversation" 301 + getComponent={() => MessagesConversationScreen} 302 + options={{title: title(msg`Chat`), requireAuth: true}} 303 + /> 304 + <Stack.Screen 305 + name="MessagesSettings" 306 + getComponent={() => MessagesSettingsScreen} 307 + options={{title: title(msg`Messaging settings`), requireAuth: true}} 308 + /> 293 309 </> 294 310 ) 295 311 } ··· 322 338 <Tab.Screen 323 339 name="MyProfileTab" 324 340 getComponent={() => MyProfileTabNavigator} 341 + /> 342 + <Tab.Screen 343 + name="MessagesTab" 344 + getComponent={() => MessagesTabNavigator} 325 345 /> 326 346 </Tab.Navigator> 327 347 ) ··· 429 449 ) 430 450 } 431 451 452 + function MessagesTabNavigator() { 453 + const pal = usePalette('default') 454 + return ( 455 + <MessagesTab.Navigator 456 + screenOptions={{ 457 + animation: isAndroid ? 'none' : undefined, 458 + gestureEnabled: true, 459 + fullScreenGestureEnabled: true, 460 + headerShown: false, 461 + animationDuration: 250, 462 + contentStyle: pal.view, 463 + }}> 464 + <MessagesTab.Screen 465 + name="MessagesList" 466 + getComponent={() => MessagesListScreen} 467 + options={{requireAuth: true}} 468 + /> 469 + {commonScreens(MessagesTab as typeof HomeTab)} 470 + </MessagesTab.Navigator> 471 + ) 472 + } 473 + 432 474 /** 433 475 * The FlatNavigator is used by Web to represent the routes 434 476 * in a single ("flat") stack. ··· 468 510 name="Notifications" 469 511 getComponent={() => NotificationsScreen} 470 512 options={{title: title(msg`Notifications`), requireAuth: true}} 513 + /> 514 + <Flat.Screen 515 + name="MessagesList" 516 + getComponent={() => MessagesListScreen} 517 + options={{title: title(msg`Messages`), requireAuth: true}} 471 518 /> 472 519 {commonScreens(Flat as typeof HomeTab, numUnread)} 473 520 </Flat.Navigator> ··· 521 568 } 522 569 if (name === 'Home') { 523 570 return buildStateObject('HomeTab', 'Home', params) 571 + } 572 + if (name === 'Messages') { 573 + return buildStateObject('MessagesTab', 'MessagesList', params) 524 574 } 525 575 // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work 526 576 return buildStateObject('HomeTab', name, params, [
+4
src/components/icons/Envelope.tsx
··· 3 3 export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 4 path: 'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z', 5 5 }) 6 + 7 + export const Envelope_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ 8 + path: 'M12 11.708 2.654 4.06A.998.998 0 0 1 3 4h18c.122 0 .238.022.346.061L12 11.708ZM2 19V6.11l9.367 7.664a1 1 0 0 0 1.266 0L22 6.11V19a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z', 9 + })
+6
src/components/icons/SettingsSlider.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const SettingsSliderVertical_Stroke2_Corner0_Rounded = 4 + createSinglePathSVG({ 5 + path: 'M7 3a1 1 0 0 1 1 1v1.126a4 4 0 0 1 0 7.748V20a1 1 0 1 1-2 0v-7.126a4 4 0 0 1 0-7.748V4a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v9.126a4 4 0 1 1-2 0V4a1 1 0 0 1 1-1ZM7 7a2 2 0 1 0 0 4 2 2 0 1 0 0-4Zm10 8a2 2 0 1 0 0 4 2 2 0 1 0 0-4Z', 6 + })
+1 -1
src/components/icons/TEMPLATE.tsx
··· 1 1 import React from 'react' 2 2 import Svg, {Path} from 'react-native-svg' 3 3 4 - import {useCommonSVGProps, Props} from '#/components/icons/common' 4 + import {Props, useCommonSVGProps} from '#/components/icons/common' 5 5 6 6 export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( 7 7 function LogoImpl(props: Props, ref) {
+1
src/lib/analytics/types.ts
··· 76 76 'MobileShell:SearchButtonPressed': {} 77 77 'MobileShell:NotificationsButtonPressed': {} 78 78 'MobileShell:FeedsButtonPressed': {} 79 + 'MobileShell:MessagesButtonPressed': {} 79 80 // NOTIFICATIONS events 80 81 'Notificatons:OpenApp': {} 81 82 // LISTS events
+4 -1
src/lib/hooks/useNavigationTabState.ts
··· 1 1 import {useNavigationState} from '@react-navigation/native' 2 + 2 3 import {getTabState, TabState} from 'lib/routes/helpers' 3 4 4 5 export function useNavigationTabState() { ··· 10 11 isAtNotifications: 11 12 getTabState(state, 'Notifications') !== TabState.Outside, 12 13 isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, 14 + isAtMessages: getTabState(state, 'MessagesList') !== TabState.Outside, 13 15 } 14 16 if ( 15 17 !res.isAtHome && 16 18 !res.isAtSearch && 17 19 !res.isAtFeeds && 18 20 !res.isAtNotifications && 19 - !res.isAtMyProfile 21 + !res.isAtMyProfile && 22 + !res.isAtMessages 20 23 ) { 21 24 // HACK for some reason useNavigationState will give us pre-hydration results 22 25 // and not update after, so we force isAtHome if all came back false
+1 -1
src/lib/routes/router.ts
··· 1 - import {RouteParams, Route} from './types' 1 + import {Route, RouteParams} from './types' 2 2 3 3 export class Router { 4 4 routes: [string, Route][] = []
+10
src/lib/routes/types.ts
··· 38 38 AccessibilitySettings: undefined 39 39 Search: {q?: string} 40 40 Hashtag: {tag: string; author?: string} 41 + MessagesConversation: {conversation: string} 42 + MessagesSettings: undefined 41 43 } 42 44 43 45 export type BottomTabNavigatorParams = CommonNavigatorParams & { ··· 46 48 FeedsTab: undefined 47 49 NotificationsTab: undefined 48 50 MyProfileTab: undefined 51 + MessagesTab: undefined 49 52 } 50 53 51 54 export type HomeTabNavigatorParams = CommonNavigatorParams & { ··· 68 71 MyProfile: undefined 69 72 } 70 73 74 + export type MessagesTabNavigatorParams = CommonNavigatorParams & { 75 + MessagesList: undefined 76 + } 77 + 71 78 export type FlatNavigatorParams = CommonNavigatorParams & { 72 79 Home: undefined 73 80 Search: {q?: string} 74 81 Feeds: undefined 75 82 Notifications: undefined 76 83 Hashtag: {tag: string; author?: string} 84 + MessagesList: undefined 77 85 } 78 86 79 87 export type AllNavigatorParams = CommonNavigatorParams & { ··· 87 95 Notifications: undefined 88 96 MyProfileTab: undefined 89 97 Hashtag: {tag: string; author?: string} 98 + MessagesTab: undefined 99 + MessagesList: undefined 90 100 } 91 101 92 102 // NOTE
+1
src/lib/statsig/gates.ts
··· 3 3 | 'autoexpand_suggestions_on_profile_follow_v2' 4 4 | 'disable_min_shell_on_foregrounding_v2' 5 5 | 'disable_poll_on_discover_v2' 6 + | 'dms' 6 7 | 'hide_vertical_scroll_indicators' 7 8 | 'show_follow_back_label_v2' 8 9 | 'start_session_with_following_v2'
+3
src/routes.ts
··· 37 37 CommunityGuidelines: '/support/community-guidelines', 38 38 CopyrightPolicy: '/support/copyright', 39 39 Hashtag: '/hashtag/:tag', 40 + MessagesList: '/messages', 41 + MessagesSettings: '/messages/settings', 42 + MessagesConversation: '/messages/:conversation', 40 43 })
+32
src/screens/Messages/Conversation/index.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + import {NativeStackScreenProps} from '@react-navigation/native-stack' 6 + 7 + import {CommonNavigatorParams} from '#/lib/routes/types' 8 + import {useGate} from '#/lib/statsig/statsig' 9 + import {ViewHeader} from '#/view/com/util/ViewHeader' 10 + import {ClipClopGate} from '../gate' 11 + 12 + type Props = NativeStackScreenProps< 13 + CommonNavigatorParams, 14 + 'MessagesConversation' 15 + > 16 + export function MessagesConversationScreen({route}: Props) { 17 + const chatId = route.params.conversation 18 + const {_} = useLingui() 19 + 20 + const gate = useGate() 21 + if (!gate('dms')) return <ClipClopGate /> 22 + 23 + return ( 24 + <View> 25 + <ViewHeader 26 + title={_(msg`Chat with ${chatId}`)} 27 + showOnDesktop 28 + showBorder 29 + /> 30 + </View> 31 + ) 32 + }
+234
src/screens/Messages/List/index.tsx
··· 1 + import React, {useCallback, useState} from 'react' 2 + import {View} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + import {NativeStackScreenProps} from '@react-navigation/native-stack' 6 + import {useInfiniteQuery} from '@tanstack/react-query' 7 + 8 + import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 9 + import {MessagesTabNavigatorParams} from '#/lib/routes/types' 10 + import {useGate} from '#/lib/statsig/statsig' 11 + import {cleanError} from '#/lib/strings/errors' 12 + import {logger} from '#/logger' 13 + import {useAgent} from '#/state/session' 14 + import {List} from '#/view/com/util/List' 15 + import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 16 + import {ViewHeader} from '#/view/com/util/ViewHeader' 17 + import {useTheme} from '#/alf' 18 + import {atoms as a} from '#/alf' 19 + import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' 20 + import {Link} from '#/components/Link' 21 + import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 22 + import {Text} from '#/components/Typography' 23 + import {ClipClopGate} from '../gate' 24 + 25 + type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'> 26 + export function MessagesListScreen({}: Props) { 27 + const {_} = useLingui() 28 + const t = useTheme() 29 + 30 + const renderButton = useCallback(() => { 31 + return ( 32 + <Link 33 + to="/messages/settings" 34 + accessibilityLabel={_(msg`Message settings`)} 35 + accessibilityHint={_(msg`Opens the message settings page`)}> 36 + <SettingsSlider size="lg" style={t.atoms.text} /> 37 + </Link> 38 + ) 39 + }, [_, t.atoms.text]) 40 + 41 + const initialNumToRender = useInitialNumToRender() 42 + const [isPTRing, setIsPTRing] = useState(false) 43 + 44 + const { 45 + data, 46 + isLoading, 47 + isFetchingNextPage, 48 + hasNextPage, 49 + fetchNextPage, 50 + error, 51 + refetch, 52 + } = usePlaceholderConversations() 53 + 54 + const isError = !!error 55 + 56 + const conversations = React.useMemo(() => { 57 + if (data?.pages) { 58 + return data.pages.flat() 59 + } 60 + return [] 61 + }, [data]) 62 + 63 + const onRefresh = React.useCallback(async () => { 64 + setIsPTRing(true) 65 + try { 66 + await refetch() 67 + } catch (err) { 68 + logger.error('Failed to refresh conversations', {message: err}) 69 + } 70 + setIsPTRing(false) 71 + }, [refetch, setIsPTRing]) 72 + 73 + const onEndReached = React.useCallback(async () => { 74 + if (isFetchingNextPage || !hasNextPage || isError) return 75 + try { 76 + await fetchNextPage() 77 + } catch (err) { 78 + logger.error('Failed to load more conversations', {message: err}) 79 + } 80 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 81 + 82 + const gate = useGate() 83 + if (!gate('dms')) return <ClipClopGate /> 84 + 85 + if (conversations.length < 1) { 86 + return ( 87 + <ListMaybePlaceholder 88 + isLoading={isLoading} 89 + isError={isError} 90 + emptyType="results" 91 + emptyMessage={_( 92 + msg`You have no messages yet. Start a conversation with someone!`, 93 + )} 94 + errorMessage={cleanError(error)} 95 + onRetry={isError ? refetch : undefined} 96 + /> 97 + ) 98 + } 99 + 100 + return ( 101 + <View> 102 + <ViewHeader 103 + title={_(msg`Messages`)} 104 + showOnDesktop 105 + renderButton={renderButton} 106 + showBorder 107 + canGoBack={false} 108 + /> 109 + <List 110 + data={conversations} 111 + renderItem={({item}) => { 112 + return ( 113 + <Link 114 + to={`/messages/${item.profile.handle}`} 115 + style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}> 116 + <PreviewableUserAvatar 117 + did={item.profile.did} 118 + handle={item.profile.handle} 119 + size={44} 120 + avatar={item.profile.avatar} 121 + /> 122 + <View style={[a.flex_1]}> 123 + <View 124 + style={[ 125 + a.flex_row, 126 + a.align_center, 127 + a.justify_between, 128 + a.gap_lg, 129 + a.flex_1, 130 + ]}> 131 + <Text numberOfLines={1}> 132 + <Text style={item.unread && a.font_bold}> 133 + {item.profile.displayName || item.profile.handle} 134 + </Text>{' '} 135 + <Text style={t.atoms.text_contrast_medium}> 136 + @{item.profile.handle} 137 + </Text> 138 + </Text> 139 + {item.unread && ( 140 + <View 141 + style={[ 142 + a.ml_2xl, 143 + {backgroundColor: t.palette.primary_500}, 144 + a.rounded_full, 145 + {height: 7, width: 7}, 146 + ]} 147 + /> 148 + )} 149 + </View> 150 + <Text 151 + numberOfLines={2} 152 + style={[ 153 + a.text_sm, 154 + item.unread ? a.font_bold : t.atoms.text_contrast_medium, 155 + ]}> 156 + {item.lastMessage} 157 + </Text> 158 + </View> 159 + </Link> 160 + ) 161 + }} 162 + keyExtractor={item => item.profile.did} 163 + refreshing={isPTRing} 164 + onRefresh={onRefresh} 165 + onEndReached={onEndReached} 166 + ListFooterComponent={ 167 + <ListFooter 168 + isFetchingNextPage={isFetchingNextPage} 169 + error={cleanError(error)} 170 + onRetry={fetchNextPage} 171 + style={{borderColor: 'transparent'}} 172 + /> 173 + } 174 + onEndReachedThreshold={3} 175 + initialNumToRender={initialNumToRender} 176 + windowSize={11} 177 + /> 178 + </View> 179 + ) 180 + } 181 + 182 + function usePlaceholderConversations() { 183 + const {getAgent} = useAgent() 184 + 185 + return useInfiniteQuery({ 186 + queryKey: ['messages'], 187 + queryFn: async () => { 188 + const people = await getAgent().getProfiles({actors: PLACEHOLDER_PEOPLE}) 189 + return people.data.profiles.map(profile => ({ 190 + profile, 191 + unread: Math.random() > 0.5, 192 + lastMessage: getRandomPost(), 193 + })) 194 + }, 195 + initialPageParam: undefined, 196 + getNextPageParam: () => undefined, 197 + }) 198 + } 199 + 200 + const PLACEHOLDER_PEOPLE = [ 201 + 'pfrazee.com', 202 + 'haileyok.com', 203 + 'danabra.mov', 204 + 'esb.lol', 205 + 'samuel.bsky.team', 206 + ] 207 + 208 + function getRandomPost() { 209 + const num = Math.floor(Math.random() * 10) 210 + switch (num) { 211 + case 0: 212 + return 'hello' 213 + case 1: 214 + return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' 215 + case 2: 216 + return 'banger post' 217 + case 3: 218 + return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' 219 + case 4: 220 + return 'lol look at this bug' 221 + case 5: 222 + return 'wow' 223 + case 6: 224 + return "that's pretty cool, wow!" 225 + case 7: 226 + return 'I think this is a bug' 227 + case 8: 228 + return 'Hello World!' 229 + case 9: 230 + return 'DMs when???' 231 + default: 232 + return 'this is unlikely' 233 + } 234 + }
+24
src/screens/Messages/Settings/index.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + import {NativeStackScreenProps} from '@react-navigation/native-stack' 6 + 7 + import {CommonNavigatorParams} from '#/lib/routes/types' 8 + import {useGate} from '#/lib/statsig/statsig' 9 + import {ViewHeader} from '#/view/com/util/ViewHeader' 10 + import {ClipClopGate} from '../gate' 11 + 12 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'> 13 + export function MessagesSettingsScreen({}: Props) { 14 + const {_} = useLingui() 15 + 16 + const gate = useGate() 17 + if (!gate('dms')) return <ClipClopGate /> 18 + 19 + return ( 20 + <View> 21 + <ViewHeader title={_(msg`Settings`)} showOnDesktop /> 22 + </View> 23 + ) 24 + }
+17
src/screens/Messages/gate.tsx
··· 1 + import React from 'react' 2 + import {Text, View} from 'react-native' 3 + 4 + export function ClipClopGate() { 5 + return ( 6 + <View 7 + style={{ 8 + flex: 1, 9 + alignItems: 'center', 10 + justifyContent: 'center', 11 + gap: 20, 12 + }}> 13 + <Text style={{fontSize: 50}}>🐴</Text> 14 + <Text style={{textAlign: 'center'}}>Nice try</Text> 15 + </View> 16 + ) 17 + }
+45 -3
src/view/shell/bottom-bar/BottomBar.tsx
··· 24 24 } from '#/lib/icons' 25 25 import {clamp} from '#/lib/numbers' 26 26 import {getTabState, TabState} from '#/lib/routes/helpers' 27 + import {useGate} from '#/lib/statsig/statsig' 27 28 import {s} from '#/lib/styles' 28 29 import {emitSoftReset} from '#/state/events' 29 30 import {useUnreadNotifications} from '#/state/queries/notifications/unread' ··· 39 40 import {Logotype} from '#/view/icons/Logotype' 40 41 import {useDialogControl} from '#/components/Dialog' 41 42 import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 43 + import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 44 + import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' 42 45 import {styles} from './BottomBarStyles' 43 46 44 - type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' 47 + type TabOptions = 48 + | 'Home' 49 + | 'Search' 50 + | 'Notifications' 51 + | 'MyProfile' 52 + | 'Feeds' 53 + | 'Messages' 45 54 46 55 export function BottomBar({navigation}: BottomTabBarProps) { 47 56 const {hasSession, currentAccount} = useSession() ··· 50 59 const safeAreaInsets = useSafeAreaInsets() 51 60 const {track} = useAnalytics() 52 61 const {footerHeight} = useShellLayout() 53 - const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = 54 - useNavigationTabState() 62 + const { 63 + isAtHome, 64 + isAtSearch, 65 + isAtFeeds, 66 + isAtNotifications, 67 + isAtMyProfile, 68 + isAtMessages, 69 + } = useNavigationTabState() 55 70 const numUnreadNotifications = useUnreadNotifications() 56 71 const {footerMinimalShellTransform} = useMinimalShellMode() 57 72 const {data: profile} = useProfileQuery({did: currentAccount?.did}) ··· 60 75 const dedupe = useDedupe() 61 76 const accountSwitchControl = useDialogControl() 62 77 const playHaptic = useHaptics() 78 + const gate = useGate() 63 79 64 80 const showSignIn = React.useCallback(() => { 65 81 closeAllActiveElements() ··· 104 120 onPressTab('MyProfile') 105 121 }, [onPressTab]) 106 122 123 + const onPressMessages = React.useCallback(() => { 124 + onPressTab('Messages') 125 + }, [onPressTab]) 126 + 107 127 const onLongPressProfile = React.useCallback(() => { 108 128 playHaptic() 109 129 accountSwitchControl.open() ··· 220 240 : `${numUnreadNotifications} unread` 221 241 } 222 242 /> 243 + {gate('dms') && ( 244 + <Btn 245 + testID="bottomBarMessagesBtn" 246 + icon={ 247 + isAtMessages ? ( 248 + <EnvelopeFilled 249 + size="lg" 250 + style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} 251 + /> 252 + ) : ( 253 + <Envelope 254 + size="lg" 255 + style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} 256 + /> 257 + ) 258 + } 259 + onPress={onPressMessages} 260 + accessibilityRole="tab" 261 + accessibilityLabel={_(msg`Messages`)} 262 + accessibilityHint="" 263 + /> 264 + )} 223 265 <Btn 224 266 testID="bottomBarProfileBtn" 225 267 icon={
+4
src/view/shell/bottom-bar/BottomBarStyles.tsx
··· 1 1 import {StyleSheet} from 'react-native' 2 + 2 3 import {colors} from 'lib/styles' 3 4 4 5 export const styles = StyleSheet.create({ ··· 64 65 }, 65 66 profileIcon: { 66 67 top: -4, 68 + }, 69 + messagesIcon: { 70 + top: 2, 67 71 }, 68 72 onProfile: { 69 73 borderWidth: 1,
+31 -13
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 1 1 import React from 'react' 2 - import {usePalette} from 'lib/hooks/usePalette' 3 - import {useNavigationState} from '@react-navigation/native' 2 + import {View} from 'react-native' 4 3 import Animated from 'react-native-reanimated' 5 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 - import {View} from 'react-native' 7 5 import {msg, Trans} from '@lingui/macro' 8 6 import {useLingui} from '@lingui/react' 9 - import {getCurrentRoute, isTab} from 'lib/routes/helpers' 10 - import {styles} from './BottomBarStyles' 11 - import {clamp} from 'lib/numbers' 7 + import {useNavigationState} from '@react-navigation/native' 8 + 9 + import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' 10 + import {usePalette} from '#/lib/hooks/usePalette' 12 11 import { 13 12 BellIcon, 14 13 BellIconSolid, 14 + HashtagIcon, 15 15 HomeIcon, 16 16 HomeIconSolid, 17 17 MagnifyingGlassIcon2, 18 18 MagnifyingGlassIcon2Solid, 19 - HashtagIcon, 20 19 UserIcon, 21 20 UserIconSolid, 22 - } from 'lib/icons' 23 - import {Link} from 'view/com/util/Link' 24 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 25 - import {makeProfileLink} from 'lib/routes/links' 26 - import {CommonNavigatorParams} from 'lib/routes/types' 21 + } from '#/lib/icons' 22 + import {clamp} from '#/lib/numbers' 23 + import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 24 + import {makeProfileLink} from '#/lib/routes/links' 25 + import {CommonNavigatorParams} from '#/lib/routes/types' 26 + import {useGate} from '#/lib/statsig/statsig' 27 + import {s} from '#/lib/styles' 27 28 import {useSession} from '#/state/session' 28 29 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 29 30 import {useCloseAllActiveElements} from '#/state/util' 30 31 import {Button} from '#/view/com/util/forms/Button' 31 32 import {Text} from '#/view/com/util/text/Text' 32 - import {s} from 'lib/styles' 33 33 import {Logo} from '#/view/icons/Logo' 34 34 import {Logotype} from '#/view/icons/Logotype' 35 + import {Link} from 'view/com/util/Link' 36 + import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 37 + import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' 38 + import {styles} from './BottomBarStyles' 35 39 36 40 export function BottomBarWeb() { 37 41 const {_} = useLingui() ··· 41 45 const {footerMinimalShellTransform} = useMinimalShellMode() 42 46 const {requestSwitchToAccount} = useLoggedOutViewControls() 43 47 const closeAllActiveElements = useCloseAllActiveElements() 48 + const gate = useGate() 44 49 45 50 const showSignIn = React.useCallback(() => { 46 51 closeAllActiveElements() ··· 117 122 ) 118 123 }} 119 124 </NavItem> 125 + {gate('dms') && ( 126 + <NavItem routeName="Messages" href="/messages"> 127 + {({isActive}) => { 128 + const Icon = isActive ? EnvelopeFilled : Envelope 129 + return ( 130 + <Icon 131 + size="lg" 132 + style={[styles.ctrlIcon, pal.text, styles.messagesIcon]} 133 + /> 134 + ) 135 + }} 136 + </NavItem> 137 + )} 120 138 <NavItem 121 139 routeName="Profile" 122 140 href={
+14
src/view/shell/desktop/LeftNav.tsx
··· 12 12 useNavigationState, 13 13 } from '@react-navigation/native' 14 14 15 + import {useGate} from '#/lib/statsig/statsig' 15 16 import {isInvalidHandle} from '#/lib/strings/handles' 16 17 import {emitSoftReset} from '#/state/events' 17 18 import {useFetchHandle} from '#/state/queries/handle' ··· 46 47 import {PressableWithHover} from 'view/com/util/PressableWithHover' 47 48 import {Text} from 'view/com/util/text/Text' 48 49 import {UserAvatar} from 'view/com/util/UserAvatar' 50 + import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 51 + import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' 49 52 import {router} from '../../../routes' 50 53 51 54 function ProfileCard() { ··· 272 275 const {_} = useLingui() 273 276 const {isDesktop, isTablet} = useWebMediaQueries() 274 277 const numUnread = useUnreadNotifications() 278 + const gate = useGate() 275 279 276 280 if (!hasSession && !isDesktop) { 277 281 return null ··· 346 350 } 347 351 label={_(msg`Notifications`)} 348 352 /> 353 + {gate('dms') && ( 354 + <NavItem 355 + href="/messages" 356 + icon={<Envelope style={pal.text} width={isDesktop ? 26 : 30} />} 357 + iconFilled={ 358 + <EnvelopeFilled style={pal.text} width={isDesktop ? 26 : 30} /> 359 + } 360 + label={_(msg`Messages`)} 361 + /> 362 + )} 349 363 <NavItem 350 364 href="/feeds" 351 365 icon={