[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`
zio/stable
Samuel Newman 2024-04-27 05:54:18 +01:00 committed by GitHub
parent 1af59ca8a7
commit ce85375c85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 486 additions and 19 deletions

View File

@ -0,0 +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>

After

Width:  |  Height:  |  Size: 303 B

View File

@ -0,0 +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>

After

Width:  |  Height:  |  Size: 367 B

View File

@ -200,6 +200,8 @@ func serve(cctx *cli.Context) error {
e.GET("/support/community-guidelines", server.WebGeneric) e.GET("/support/community-guidelines", server.WebGeneric)
e.GET("/support/copyright", server.WebGeneric) e.GET("/support/copyright", server.WebGeneric)
e.GET("/intent/compose", server.WebGeneric) e.GET("/intent/compose", server.WebGeneric)
e.GET("/messages", server.WebGeneric)
e.GET("/messages/:conversation", server.WebGeneric)
// profile endpoints; only first populates info // profile endpoints; only first populates info
e.GET("/profile/:handleOrDID", server.WebProfile) e.GET("/profile/:handleOrDID", server.WebProfile)

View File

@ -25,6 +25,7 @@ import {
FeedsTabNavigatorParams, FeedsTabNavigatorParams,
FlatNavigatorParams, FlatNavigatorParams,
HomeTabNavigatorParams, HomeTabNavigatorParams,
MessagesTabNavigatorParams,
MyProfileTabNavigatorParams, MyProfileTabNavigatorParams,
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
SearchTabNavigatorParams, SearchTabNavigatorParams,
@ -46,6 +47,9 @@ import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig' import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
import {router} from './routes' import {router} from './routes'
import {MessagesConversationScreen} from './screens/Messages/Conversation'
import {MessagesListScreen} from './screens/Messages/List'
import {MessagesSettingsScreen} from './screens/Messages/Settings'
import {useModalControls} from './state/modals' import {useModalControls} from './state/modals'
import {useUnreadNotifications} from './state/queries/notifications/unread' import {useUnreadNotifications} from './state/queries/notifications/unread'
import {useSession} from './state/session' import {useSession} from './state/session'
@ -92,6 +96,8 @@ const NotificationsTab =
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab = const MyProfileTab =
createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
const MessagesTab =
createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>()
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
@ -290,6 +296,16 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => HashtagScreen} getComponent={() => HashtagScreen}
options={{title: title(msg`Hashtag`)}} options={{title: title(msg`Hashtag`)}}
/> />
<Stack.Screen
name="MessagesConversation"
getComponent={() => MessagesConversationScreen}
options={{title: title(msg`Chat`), requireAuth: true}}
/>
<Stack.Screen
name="MessagesSettings"
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Messaging settings`), requireAuth: true}}
/>
</> </>
) )
} }
@ -323,6 +339,10 @@ function TabsNavigator() {
name="MyProfileTab" name="MyProfileTab"
getComponent={() => MyProfileTabNavigator} getComponent={() => MyProfileTabNavigator}
/> />
<Tab.Screen
name="MessagesTab"
getComponent={() => MessagesTabNavigator}
/>
</Tab.Navigator> </Tab.Navigator>
) )
} }
@ -429,6 +449,28 @@ function MyProfileTabNavigator() {
) )
} }
function MessagesTabNavigator() {
const pal = usePalette('default')
return (
<MessagesTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<MessagesTab.Screen
name="MessagesList"
getComponent={() => MessagesListScreen}
options={{requireAuth: true}}
/>
{commonScreens(MessagesTab as typeof HomeTab)}
</MessagesTab.Navigator>
)
}
/** /**
* The FlatNavigator is used by Web to represent the routes * The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack. * in a single ("flat") stack.
@ -469,6 +511,11 @@ const FlatNavigator = () => {
getComponent={() => NotificationsScreen} getComponent={() => NotificationsScreen}
options={{title: title(msg`Notifications`), requireAuth: true}} options={{title: title(msg`Notifications`), requireAuth: true}}
/> />
<Flat.Screen
name="MessagesList"
getComponent={() => MessagesListScreen}
options={{title: title(msg`Messages`), requireAuth: true}}
/>
{commonScreens(Flat as typeof HomeTab, numUnread)} {commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator> </Flat.Navigator>
) )
@ -522,6 +569,9 @@ const LINKING = {
if (name === 'Home') { if (name === 'Home') {
return buildStateObject('HomeTab', 'Home', params) return buildStateObject('HomeTab', 'Home', params)
} }
if (name === 'Messages') {
return buildStateObject('MessagesTab', 'MessagesList', params)
}
// 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 // 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
return buildStateObject('HomeTab', name, params, [ return buildStateObject('HomeTab', name, params, [
{ {

View File

@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const Envelope_Stroke2_Corner0_Rounded = createSinglePathSVG({
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', 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',
}) })
export const Envelope_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
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',
})

View File

@ -0,0 +1,6 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SettingsSliderVertical_Stroke2_Corner0_Rounded =
createSinglePathSVG({
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',
})

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import Svg, {Path} from 'react-native-svg' import Svg, {Path} from 'react-native-svg'
import {useCommonSVGProps, Props} from '#/components/icons/common' import {Props, useCommonSVGProps} from '#/components/icons/common'
export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
function LogoImpl(props: Props, ref) { function LogoImpl(props: Props, ref) {

View File

@ -76,6 +76,7 @@ export type TrackPropertiesMap = {
'MobileShell:SearchButtonPressed': {} 'MobileShell:SearchButtonPressed': {}
'MobileShell:NotificationsButtonPressed': {} 'MobileShell:NotificationsButtonPressed': {}
'MobileShell:FeedsButtonPressed': {} 'MobileShell:FeedsButtonPressed': {}
'MobileShell:MessagesButtonPressed': {}
// NOTIFICATIONS events // NOTIFICATIONS events
'Notificatons:OpenApp': {} 'Notificatons:OpenApp': {}
// LISTS events // LISTS events

View File

@ -1,4 +1,5 @@
import {useNavigationState} from '@react-navigation/native' import {useNavigationState} from '@react-navigation/native'
import {getTabState, TabState} from 'lib/routes/helpers' import {getTabState, TabState} from 'lib/routes/helpers'
export function useNavigationTabState() { export function useNavigationTabState() {
@ -10,13 +11,15 @@ export function useNavigationTabState() {
isAtNotifications: isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside, getTabState(state, 'Notifications') !== TabState.Outside,
isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside,
isAtMessages: getTabState(state, 'MessagesList') !== TabState.Outside,
} }
if ( if (
!res.isAtHome && !res.isAtHome &&
!res.isAtSearch && !res.isAtSearch &&
!res.isAtFeeds && !res.isAtFeeds &&
!res.isAtNotifications && !res.isAtNotifications &&
!res.isAtMyProfile !res.isAtMyProfile &&
!res.isAtMessages
) { ) {
// HACK for some reason useNavigationState will give us pre-hydration results // HACK for some reason useNavigationState will give us pre-hydration results
// and not update after, so we force isAtHome if all came back false // and not update after, so we force isAtHome if all came back false

View File

@ -1,4 +1,4 @@
import {RouteParams, Route} from './types' import {Route, RouteParams} from './types'
export class Router { export class Router {
routes: [string, Route][] = [] routes: [string, Route][] = []

View File

@ -38,6 +38,8 @@ export type CommonNavigatorParams = {
AccessibilitySettings: undefined AccessibilitySettings: undefined
Search: {q?: string} Search: {q?: string}
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string}
MessagesSettings: undefined
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {
@ -46,6 +48,7 @@ export type BottomTabNavigatorParams = CommonNavigatorParams & {
FeedsTab: undefined FeedsTab: undefined
NotificationsTab: undefined NotificationsTab: undefined
MyProfileTab: undefined MyProfileTab: undefined
MessagesTab: undefined
} }
export type HomeTabNavigatorParams = CommonNavigatorParams & { export type HomeTabNavigatorParams = CommonNavigatorParams & {
@ -68,12 +71,17 @@ export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
MyProfile: undefined MyProfile: undefined
} }
export type MessagesTabNavigatorParams = CommonNavigatorParams & {
MessagesList: undefined
}
export type FlatNavigatorParams = CommonNavigatorParams & { export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
Notifications: undefined Notifications: undefined
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesList: undefined
} }
export type AllNavigatorParams = CommonNavigatorParams & { export type AllNavigatorParams = CommonNavigatorParams & {
@ -87,6 +95,8 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Notifications: undefined Notifications: undefined
MyProfileTab: undefined MyProfileTab: undefined
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesTab: undefined
MessagesList: undefined
} }
// NOTE // NOTE

View File

@ -3,6 +3,7 @@ export type Gate =
| 'autoexpand_suggestions_on_profile_follow_v2' | 'autoexpand_suggestions_on_profile_follow_v2'
| 'disable_min_shell_on_foregrounding_v2' | 'disable_min_shell_on_foregrounding_v2'
| 'disable_poll_on_discover_v2' | 'disable_poll_on_discover_v2'
| 'dms'
| 'hide_vertical_scroll_indicators' | 'hide_vertical_scroll_indicators'
| 'show_follow_back_label_v2' | 'show_follow_back_label_v2'
| 'start_session_with_following_v2' | 'start_session_with_following_v2'

View File

@ -37,4 +37,7 @@ export const router = new Router({
CommunityGuidelines: '/support/community-guidelines', CommunityGuidelines: '/support/community-guidelines',
CopyrightPolicy: '/support/copyright', CopyrightPolicy: '/support/copyright',
Hashtag: '/hashtag/:tag', Hashtag: '/hashtag/:tag',
MessagesList: '/messages',
MessagesSettings: '/messages/settings',
MessagesConversation: '/messages/:conversation',
}) })

View File

@ -0,0 +1,32 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ClipClopGate} from '../gate'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'MessagesConversation'
>
export function MessagesConversationScreen({route}: Props) {
const chatId = route.params.conversation
const {_} = useLingui()
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
return (
<View>
<ViewHeader
title={_(msg`Chat with ${chatId}`)}
showOnDesktop
showBorder
/>
</View>
)
}

View File

@ -0,0 +1,234 @@
import React, {useCallback, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useInfiniteQuery} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {MessagesTabNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {List} from '#/view/com/util/List'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import {Link} from '#/components/Link'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Text} from '#/components/Typography'
import {ClipClopGate} from '../gate'
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
export function MessagesListScreen({}: Props) {
const {_} = useLingui()
const t = useTheme()
const renderButton = useCallback(() => {
return (
<Link
to="/messages/settings"
accessibilityLabel={_(msg`Message settings`)}
accessibilityHint={_(msg`Opens the message settings page`)}>
<SettingsSlider size="lg" style={t.atoms.text} />
</Link>
)
}, [_, t.atoms.text])
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = useState(false)
const {
data,
isLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
error,
refetch,
} = usePlaceholderConversations()
const isError = !!error
const conversations = React.useMemo(() => {
if (data?.pages) {
return data.pages.flat()
}
return []
}, [data])
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh conversations', {message: err})
}
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more conversations', {message: err})
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
if (conversations.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
emptyType="results"
emptyMessage={_(
msg`You have no messages yet. Start a conversation with someone!`,
)}
errorMessage={cleanError(error)}
onRetry={isError ? refetch : undefined}
/>
)
}
return (
<View>
<ViewHeader
title={_(msg`Messages`)}
showOnDesktop
renderButton={renderButton}
showBorder
canGoBack={false}
/>
<List
data={conversations}
renderItem={({item}) => {
return (
<Link
to={`/messages/${item.profile.handle}`}
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
<PreviewableUserAvatar
did={item.profile.did}
handle={item.profile.handle}
size={44}
avatar={item.profile.avatar}
/>
<View style={[a.flex_1]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.flex_1,
]}>
<Text numberOfLines={1}>
<Text style={item.unread && a.font_bold}>
{item.profile.displayName || item.profile.handle}
</Text>{' '}
<Text style={t.atoms.text_contrast_medium}>
@{item.profile.handle}
</Text>
</Text>
{item.unread && (
<View
style={[
a.ml_2xl,
{backgroundColor: t.palette.primary_500},
a.rounded_full,
{height: 7, width: 7},
]}
/>
)}
</View>
<Text
numberOfLines={2}
style={[
a.text_sm,
item.unread ? a.font_bold : t.atoms.text_contrast_medium,
]}>
{item.lastMessage}
</Text>
</View>
</Link>
)
}}
keyExtractor={item => item.profile.did}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
style={{borderColor: 'transparent'}}
/>
}
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
</View>
)
}
function usePlaceholderConversations() {
const {getAgent} = useAgent()
return useInfiniteQuery({
queryKey: ['messages'],
queryFn: async () => {
const people = await getAgent().getProfiles({actors: PLACEHOLDER_PEOPLE})
return people.data.profiles.map(profile => ({
profile,
unread: Math.random() > 0.5,
lastMessage: getRandomPost(),
}))
},
initialPageParam: undefined,
getNextPageParam: () => undefined,
})
}
const PLACEHOLDER_PEOPLE = [
'pfrazee.com',
'haileyok.com',
'danabra.mov',
'esb.lol',
'samuel.bsky.team',
]
function getRandomPost() {
const num = Math.floor(Math.random() * 10)
switch (num) {
case 0:
return 'hello'
case 1:
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'
case 2:
return 'banger post'
case 3:
return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
case 4:
return 'lol look at this bug'
case 5:
return 'wow'
case 6:
return "that's pretty cool, wow!"
case 7:
return 'I think this is a bug'
case 8:
return 'Hello World!'
case 9:
return 'DMs when???'
default:
return 'this is unlikely'
}
}

View File

@ -0,0 +1,24 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ClipClopGate} from '../gate'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
export function MessagesSettingsScreen({}: Props) {
const {_} = useLingui()
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
return (
<View>
<ViewHeader title={_(msg`Settings`)} showOnDesktop />
</View>
)
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import {Text, View} from 'react-native'
export function ClipClopGate() {
return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 20,
}}>
<Text style={{fontSize: 50}}>🐴</Text>
<Text style={{textAlign: 'center'}}>Nice try</Text>
</View>
)
}

View File

@ -24,6 +24,7 @@ import {
} from '#/lib/icons' } from '#/lib/icons'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {getTabState, TabState} from '#/lib/routes/helpers' import {getTabState, TabState} from '#/lib/routes/helpers'
import {useGate} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useUnreadNotifications} from '#/state/queries/notifications/unread'
@ -39,9 +40,17 @@ import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope'
import {styles} from './BottomBarStyles' import {styles} from './BottomBarStyles'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' type TabOptions =
| 'Home'
| 'Search'
| 'Notifications'
| 'MyProfile'
| 'Feeds'
| 'Messages'
export function BottomBar({navigation}: BottomTabBarProps) { export function BottomBar({navigation}: BottomTabBarProps) {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
@ -50,8 +59,14 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics() const {track} = useAnalytics()
const {footerHeight} = useShellLayout() const {footerHeight} = useShellLayout()
const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = const {
useNavigationTabState() isAtHome,
isAtSearch,
isAtFeeds,
isAtNotifications,
isAtMyProfile,
isAtMessages,
} = useNavigationTabState()
const numUnreadNotifications = useUnreadNotifications() const numUnreadNotifications = useUnreadNotifications()
const {footerMinimalShellTransform} = useMinimalShellMode() const {footerMinimalShellTransform} = useMinimalShellMode()
const {data: profile} = useProfileQuery({did: currentAccount?.did}) const {data: profile} = useProfileQuery({did: currentAccount?.did})
@ -60,6 +75,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const dedupe = useDedupe() const dedupe = useDedupe()
const accountSwitchControl = useDialogControl() const accountSwitchControl = useDialogControl()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const gate = useGate()
const showSignIn = React.useCallback(() => { const showSignIn = React.useCallback(() => {
closeAllActiveElements() closeAllActiveElements()
@ -104,6 +120,10 @@ export function BottomBar({navigation}: BottomTabBarProps) {
onPressTab('MyProfile') onPressTab('MyProfile')
}, [onPressTab]) }, [onPressTab])
const onPressMessages = React.useCallback(() => {
onPressTab('Messages')
}, [onPressTab])
const onLongPressProfile = React.useCallback(() => { const onLongPressProfile = React.useCallback(() => {
playHaptic() playHaptic()
accountSwitchControl.open() accountSwitchControl.open()
@ -220,6 +240,28 @@ export function BottomBar({navigation}: BottomTabBarProps) {
: `${numUnreadNotifications} unread` : `${numUnreadNotifications} unread`
} }
/> />
{gate('dms') && (
<Btn
testID="bottomBarMessagesBtn"
icon={
isAtMessages ? (
<EnvelopeFilled
size="lg"
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
/>
) : (
<Envelope
size="lg"
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
/>
)
}
onPress={onPressMessages}
accessibilityRole="tab"
accessibilityLabel={_(msg`Messages`)}
accessibilityHint=""
/>
)}
<Btn <Btn
testID="bottomBarProfileBtn" testID="bottomBarProfileBtn"
icon={ icon={

View File

@ -1,4 +1,5 @@
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
@ -65,6 +66,9 @@ export const styles = StyleSheet.create({
profileIcon: { profileIcon: {
top: -4, top: -4,
}, },
messagesIcon: {
top: 2,
},
onProfile: { onProfile: {
borderWidth: 1, borderWidth: 1,
borderRadius: 100, borderRadius: 100,

View File

@ -1,37 +1,41 @@
import React from 'react' import React from 'react'
import {usePalette} from 'lib/hooks/usePalette' import {View} from 'react-native'
import {useNavigationState} from '@react-navigation/native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {getCurrentRoute, isTab} from 'lib/routes/helpers' import {useNavigationState} from '@react-navigation/native'
import {styles} from './BottomBarStyles'
import {clamp} from 'lib/numbers' import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode'
import {usePalette} from '#/lib/hooks/usePalette'
import { import {
BellIcon, BellIcon,
BellIconSolid, BellIconSolid,
HashtagIcon,
HomeIcon, HomeIcon,
HomeIconSolid, HomeIconSolid,
MagnifyingGlassIcon2, MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid, MagnifyingGlassIcon2Solid,
HashtagIcon,
UserIcon, UserIcon,
UserIconSolid, UserIconSolid,
} from 'lib/icons' } from '#/lib/icons'
import {Link} from 'view/com/util/Link' import {clamp} from '#/lib/numbers'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button' import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {s} from 'lib/styles'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {Link} from 'view/com/util/Link'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope'
import {styles} from './BottomBarStyles'
export function BottomBarWeb() { export function BottomBarWeb() {
const {_} = useLingui() const {_} = useLingui()
@ -41,6 +45,7 @@ export function BottomBarWeb() {
const {footerMinimalShellTransform} = useMinimalShellMode() const {footerMinimalShellTransform} = useMinimalShellMode()
const {requestSwitchToAccount} = useLoggedOutViewControls() const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const gate = useGate()
const showSignIn = React.useCallback(() => { const showSignIn = React.useCallback(() => {
closeAllActiveElements() closeAllActiveElements()
@ -117,6 +122,19 @@ export function BottomBarWeb() {
) )
}} }}
</NavItem> </NavItem>
{gate('dms') && (
<NavItem routeName="Messages" href="/messages">
{({isActive}) => {
const Icon = isActive ? EnvelopeFilled : Envelope
return (
<Icon
size="lg"
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
/>
)
}}
</NavItem>
)}
<NavItem <NavItem
routeName="Profile" routeName="Profile"
href={ href={

View File

@ -12,6 +12,7 @@ import {
useNavigationState, useNavigationState,
} from '@react-navigation/native' } from '@react-navigation/native'
import {useGate} from '#/lib/statsig/statsig'
import {isInvalidHandle} from '#/lib/strings/handles' import {isInvalidHandle} from '#/lib/strings/handles'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
import {useFetchHandle} from '#/state/queries/handle' import {useFetchHandle} from '#/state/queries/handle'
@ -46,6 +47,8 @@ import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {PressableWithHover} from 'view/com/util/PressableWithHover' import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope'
import {router} from '../../../routes' import {router} from '../../../routes'
function ProfileCard() { function ProfileCard() {
@ -272,6 +275,7 @@ export function DesktopLeftNav() {
const {_} = useLingui() const {_} = useLingui()
const {isDesktop, isTablet} = useWebMediaQueries() const {isDesktop, isTablet} = useWebMediaQueries()
const numUnread = useUnreadNotifications() const numUnread = useUnreadNotifications()
const gate = useGate()
if (!hasSession && !isDesktop) { if (!hasSession && !isDesktop) {
return null return null
@ -346,6 +350,16 @@ export function DesktopLeftNav() {
} }
label={_(msg`Notifications`)} label={_(msg`Notifications`)}
/> />
{gate('dms') && (
<NavItem
href="/messages"
icon={<Envelope style={pal.text} width={isDesktop ? 26 : 30} />}
iconFilled={
<EnvelopeFilled style={pal.text} width={isDesktop ? 26 : 30} />
}
label={_(msg`Messages`)}
/>
)}
<NavItem <NavItem
href="/feeds" href="/feeds"
icon={ icon={