diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 6f661e03e9..7889f0ff5f 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -143,6 +143,7 @@ import {setNavigationMetadata} from '#/analytics/metadata' import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {renderMessagesSplitViewLayout} from './screens/Messages/components/splitView/MessagesSplitViewLayout' const navigationRef = createNavigationContainerRef() @@ -564,26 +565,28 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => TopicScreen} options={{title: title(msg`Topic`)}} /> - MessagesConversationScreen} - options={{title: title(msg`Chat`), requireAuth: true}} - /> - MessagesConversationSettingsScreen} - options={{title: title(msg`Group chat settings`), requireAuth: true}} - /> - MessagesSettingsScreen} - options={{title: title(msg`Chat settings`), requireAuth: true}} - /> - MessagesInboxScreen} - options={{title: title(msg`Chat request inbox`), requireAuth: true}} - /> + + MessagesConversationScreen} + options={{title: title(msg`Chat`), requireAuth: true}} + /> + MessagesConversationSettingsScreen} + options={{title: title(msg`Group chat settings`), requireAuth: true}} + /> + MessagesSettingsScreen} + options={{title: title(msg`Chat settings`), requireAuth: true}} + /> + MessagesInboxScreen} + options={{title: title(msg`Chat request inbox`), requireAuth: true}} + /> + NotificationsActivityListScreen} @@ -832,6 +835,7 @@ const FlatNavigator = ({ name="Messages" getComponent={() => MessagesScreen} options={{title: title(msg`Messages`), requireAuth: true}} + layout={renderMessagesSplitViewLayout} /> {children} @@ -108,6 +111,7 @@ export function Slot({children}: {children?: React.ReactNode}) { export function BackButton({onPress, style, ...props}: Partial) { const {_} = useLingui() const navigation = useNavigation() + const {isWithinRightPanel} = useIsWithinSplitView() const onPressBack = useCallback( (evt: GestureResponderEvent) => { @@ -122,6 +126,10 @@ export function BackButton({onPress, style, ...props}: Partial) { [onPress, navigation], ) + if (isWithinRightPanel) { + return null + } + return ( - - - ) : ( - <> - - - - Nothing here - - - You have no conversations yet. Start one! - - - - )} - - )} - - - {!isLoading && !isError && ( - + + + + ) : isWithinSplitView ? ( + + ) : ( + + )} + )} - + ) } return ( - -
- - void onRefresh()} - onEndReached={() => void onEndReached()} - ListFooterComponent={ - - } - onEndReachedThreshold={IS_NATIVE ? 1.5 : 0} - initialNumToRender={initialNumToRender} - windowSize={11} - desktopFixedHeight - sideBorders={false} - /> - + void onRefresh()} + onEndReached={() => void onEndReached()} + ListFooterComponent={ + + } + onEndReachedThreshold={IS_NATIVE ? 1.5 : 0} + initialNumToRender={initialNumToRender} + windowSize={11} + desktopFixedHeight + sideBorders={false} + disableFullWindowScroll={isWithinSplitView} + style={ + isWithinSplitView && [ + a.w_full, + web({ + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_100} transparent`, + }), + ] + } + /> ) } -function Header({ - newChatControl, - requestsCount, - hasMoreRequests, -}: { - newChatControl: DialogControlProps - requestsCount: number - hasMoreRequests: boolean -}) { - const {_} = useLingui() +export function Header({newChatControl}: {newChatControl: DialogControlProps}) { + const {t: l} = useLingui() const {gtMobile} = useBreakpoints() const requireEmailVerification = useRequireEmailVerification() + const leftConvos = useLeftConvos() + + const {data: inboxData, hasNextPage: hasMoreRequests} = useListConvosQuery({ + status: 'request', + }) + + const inboxAllConvos = + inboxData?.pages + .flatMap(page => page.convos) + .filter( + convo => + !leftConvos.includes(convo.id) && + !convo.muted && + convo.members.every(member => member.handle !== 'missing.invalid'), + ) ?? [] const openChatControl = useCallback(() => { newChatControl.open() @@ -370,13 +372,16 @@ function Header({ }) const requestsLink = ( - + ) const settingsLink = ( - + New chat diff --git a/src/screens/Messages/Inbox.tsx b/src/screens/Messages/Inbox.tsx index b00bc3f5b1..b9d0792464 100644 --- a/src/screens/Messages/Inbox.tsx +++ b/src/screens/Messages/Inbox.tsx @@ -44,6 +44,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' import {RequestListItem} from './components/RequestListItem' +import {useIsWithinSplitView} from './components/splitView/context' type Props = NativeStackScreenProps @@ -127,6 +128,7 @@ function RequestList({ const {t: l} = useLingui() const t = useTheme() const navigation = useNavigation() + const {isWithinSplitView} = useIsWithinSplitView() // Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future) // but only when the screen is active @@ -217,7 +219,7 @@ function RequestList({ Retry - + @@ -243,23 +245,25 @@ function RequestList({ You don't have any chat requests at the moment. - + {!isWithinSplitView && ( + + )} )} diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 721454dd6f..01de080936 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -57,10 +57,12 @@ export const ChatListItemPortal = createPortalGroup() export function ChatListItem({ convo: convoView, showMenu = true, + selected = false, children, }: { convo: ChatBskyConvoDefs.ConvoView showMenu?: boolean + selected?: boolean children?: React.ReactNode }) { const {currentAccount} = useSession() @@ -78,7 +80,8 @@ export function ChatListItem({ + showMenu={showMenu} + selected={selected}> {children} ) @@ -88,7 +91,8 @@ export function ChatListItem({ + showMenu={showMenu} + selected={selected}> {children} ) @@ -103,11 +107,13 @@ function DirectChatItem({ convo, moderationOpts, showMenu, + selected, children, }: { convo: Extract moderationOpts: ModerationOpts showMenu?: boolean + selected?: boolean children?: React.ReactNode }) { const {t: l} = useLingui() @@ -145,6 +151,7 @@ function DirectChatItem({ : l`This conversation is with a deleted or a deactivated account. Press for options` } showMenu={showMenu} + selected={selected} isDeletedAccount={isDeletedAccount} isBlockedAccount={moderation.blocked} showProfileBadges @@ -164,11 +171,13 @@ function GroupChatItem({ convo, moderationOpts, showMenu, + selected, children, }: { convo: Extract moderationOpts: ModerationOpts showMenu?: boolean + selected?: boolean children?: React.ReactNode }) { const {t: l} = useLingui() @@ -193,6 +202,7 @@ function GroupChatItem({ isBlockedAccount={false} isDeletedAccount={false} showProfileBadges={false} + selected={selected} showMenu={showMenu}> {children} @@ -210,6 +220,7 @@ function BaseChatItem({ primaryProfile, primaryProfileModeration, showMenu, + selected, showProfileBadges, postAlerts, children, @@ -224,6 +235,7 @@ function BaseChatItem({ primaryProfile?: Shadow primaryProfileModeration?: ModerationDecision showMenu?: boolean + selected?: boolean showProfileBadges: boolean postAlerts?: React.ReactNode children?: React.ReactNode @@ -457,7 +469,8 @@ function BaseChatItem({ a.px_lg, a.py_md, a.gap_md, - (hovered || pressed || focused) && t.atoms.bg_contrast_25, + selected && t.atoms.bg_contrast_25, + (hovered || pressed || focused) && t.atoms.bg_contrast_50, ]}> {/* Avatar goes here */} diff --git a/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx b/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx new file mode 100644 index 0000000000..212ebb5440 --- /dev/null +++ b/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx @@ -0,0 +1,85 @@ +import {View} from 'react-native' +import {type ScreenLayoutArgs, useIsFocused} from '@react-navigation/native' +import {RemoveScrollBar} from 'react-remove-scroll-bar' + +import {type AllNavigatorParams, type NavigationProp} from '#/lib/routes/types' +import {atoms as a, useLayoutBreakpoints, useTheme, web} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {NewChat} from '#/components/dms/dialogs/NewChatDialog' +import {SCROLLBAR_OFFSET} from '#/components/Layout' +import {useAgeAssurance} from '#/ageAssurance' +import {IS_WEB} from '#/env' +import {ChatList, Header as ChatListHeader} from '../../ChatList' +import {SplitViewProvider} from './context' + +const LEFT_NAV_MINIMAL_WIDTH = 86 +const RIGHT_NAV_WIDTH = 330 + 28 + +type LayoutProps = ScreenLayoutArgs< + AllNavigatorParams, + 'MessagesConversation', + {}, + NavigationProp +> +export function renderMessagesSplitViewLayout(props: LayoutProps) { + return +} + +function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) { + const {rightNavVisible, centerColumnOffset} = useLayoutBreakpoints() + const newChatControl = useDialogControl() + const t = useTheme() + const aa = useAgeAssurance() + const isFocused = useIsFocused() + + if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) { + return children + } + + const onNewChat = (conversation: string) => + navigation.navigate('MessagesConversation', {conversation}) + + const selectedChat = + route.name === 'MessagesConversation' + ? route?.params?.conversation + : undefined + + return ( + + {isFocused && } + + + + + + + + + + {children} + + + + ) +} diff --git a/src/screens/Messages/components/splitView/context.tsx b/src/screens/Messages/components/splitView/context.tsx new file mode 100644 index 0000000000..8b896d7dc9 --- /dev/null +++ b/src/screens/Messages/components/splitView/context.tsx @@ -0,0 +1,31 @@ +import {createContext, useContext, useMemo} from 'react' + +const SplitViewContext = createContext<{ + isWithinSplitView: boolean + isWithinLeftPanel: boolean + isWithinRightPanel: boolean +}>({ + isWithinSplitView: false, + isWithinLeftPanel: false, + isWithinRightPanel: false, +}) + +export function SplitViewProvider({ + children, + side, +}: { + children: React.ReactNode + side: 'left' | 'right' +}) { + const value = useMemo( + () => ({ + isWithinSplitView: true, + isWithinLeftPanel: side === 'left', + isWithinRightPanel: side === 'right', + }), + [side], + ) + return {children} +} + +export const useIsWithinSplitView = () => useContext(SplitViewContext) diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index 232212f8f4..8362cf2918 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -2,8 +2,6 @@ import {isValidElement} from 'react' import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native' import {View} from 'react-native' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, type ButtonProps, ButtonText} from '#/components/Button' import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' @@ -24,23 +22,25 @@ export function EmptyState({ button, }: { testID?: string - icon?: React.ComponentType | React.ReactElement + icon?: React.ComponentType | React.ReactElement | null iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' message: string style?: StyleProp textStyle?: StyleProp button?: EmptyStateButtonProps }) { - const pal = usePalette('default') - const {isTabletOrDesktop} = useWebMediaQueries() const t = useTheme() - const {gtMobile} = useBreakpoints() + const {gtMobile, gtTablet} = useBreakpoints() const placeholderIcon = ( ) const renderIcon = () => { + if (icon === null) { + return null + } + if (!icon) { return placeholderIcon } @@ -79,16 +79,14 @@ export function EmptyState({ {height: 64, width: 64}, isValidElement(icon) ? a.bg_transparent - : [isTabletOrDesktop && {marginTop: 50}], + : [gtTablet && {marginTop: 50}], ]}> {renderIcon()}