diff --git a/assets/icons/bubbleSmile_stroke2_corner0_rounded_large.svg b/assets/icons/bubbleSmile_stroke2_corner0_rounded_large.svg new file mode 100644 index 0000000000..5d0f9fddca --- /dev/null +++ b/assets/icons/bubbleSmile_stroke2_corner0_rounded_large.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/inbox_stroke2_corner2_rounded.svg b/assets/icons/inbox_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..a2cbed7ce7 --- /dev/null +++ b/assets/icons/inbox_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/inbox_stroke2_corner2_rounded_large.svg b/assets/icons/inbox_stroke2_corner2_rounded_large.svg new file mode 100644 index 0000000000..0804046fa2 --- /dev/null +++ b/assets/icons/inbox_stroke2_corner2_rounded_large.svg @@ -0,0 +1 @@ + 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 ( ) : ( - <> - - - - - Inbox zero! - - - - - You don't have any chat requests at the moment. - - - - - + } + style={web([a.h_full, a.justify_center, a.pb_5xl])} + /> )} )} @@ -354,7 +346,6 @@ function MarkAsReadHeaderButton() { label={l`Mark all as read`} size="small" color="secondary" - variant="solid" onPress={() => markAllRead()}> diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 721454dd6f..5ac1d0701d 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -51,16 +51,19 @@ import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import type * as bsky from '#/types/bsky' +import {useIsWithinSplitView} from './splitView/context' 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 +81,8 @@ export function ChatListItem({ + showMenu={showMenu} + selected={selected}> {children} ) @@ -88,7 +92,8 @@ export function ChatListItem({ + showMenu={showMenu} + selected={selected}> {children} ) @@ -103,15 +108,18 @@ function DirectChatItem({ convo, moderationOpts, showMenu, + selected, children, }: { convo: Extract moderationOpts: ModerationOpts showMenu?: boolean + selected?: boolean children?: React.ReactNode }) { const {t: l} = useLingui() const profile = useProfileShadow(convo.primaryMember) + const {isWithinSplitView} = useIsWithinSplitView() const moderation = useMemo( () => moderateProfile(profile, moderationOpts), @@ -129,7 +137,7 @@ function DirectChatItem({ avatar={ } @@ -145,15 +153,18 @@ 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 postAlerts={ - + isWithinSplitView ? null : ( + + ) }> {children} @@ -164,15 +175,18 @@ function GroupChatItem({ convo, moderationOpts, showMenu, + selected, children, }: { convo: Extract moderationOpts: ModerationOpts showMenu?: boolean + selected?: boolean children?: React.ReactNode }) { const {t: l} = useLingui() const groupOwner = useMaybeProfileShadow(convo.primaryMember) + const {isWithinSplitView} = useIsWithinSplitView() const moderation = useMemo( () => @@ -185,7 +199,12 @@ function GroupChatItem({ return ( } + avatar={ + + } title={chatName} accessibilityHint={l`Go to the group chat named "${chatName}"`} primaryProfile={groupOwner} @@ -193,6 +212,7 @@ function GroupChatItem({ isBlockedAccount={false} isDeletedAccount={false} showProfileBadges={false} + selected={selected} showMenu={showMenu}> {children} @@ -210,6 +230,7 @@ function BaseChatItem({ primaryProfile, primaryProfileModeration, showMenu, + selected, showProfileBadges, postAlerts, children, @@ -224,6 +245,7 @@ function BaseChatItem({ primaryProfile?: Shadow primaryProfileModeration?: ModerationDecision showMenu?: boolean + selected?: boolean showProfileBadges: boolean postAlerts?: React.ReactNode children?: React.ReactNode @@ -236,6 +258,7 @@ function BaseChatItem({ const leaveConvoControl = useDialogControl() const {mutate: markAsRead} = useMarkAsReadMutation() const {gtMobile} = useBreakpoints() + const {isWithinSplitView} = useIsWithinSplitView() const playHaptic = useHaptics() const queryClient = useQueryClient() @@ -407,6 +430,8 @@ function BaseChatItem({ leftFirst: deleteAction, } + const avatarSize = isWithinSplitView ? 48 : 52 + return ( @@ -416,7 +441,7 @@ function BaseChatItem({ // @ts-expect-error web only onFocus={onFocus} onBlur={onMouseLeave} - style={[a.relative, t.atoms.bg]}> + style={[a.relative, t.atoms.bg, isWithinSplitView && a.mx_sm]}> {/* Avatar goes here */} - + + style={[a.flex_1, a.justify_center, web({paddingRight: 40})]}> )} @@ -526,15 +553,14 @@ function BaseChatItem({ {subtitle && ( + style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]} + emoji> {subtitle} )} + {postAlerts} + {LastMessageIcon && ( - {postAlerts} - {children} diff --git a/src/screens/Messages/components/InboxRequests.tsx b/src/screens/Messages/components/InboxRequests.tsx index aeec64a924..fcb67601c1 100644 --- a/src/screens/Messages/components/InboxRequests.tsx +++ b/src/screens/Messages/components/InboxRequests.tsx @@ -3,15 +3,29 @@ import {useLingui} from '@lingui/react/macro' import {UNREAD_LIMIT} from '#/state/queries/messages/list-conversations' import {atoms as a} from '#/alf' -import {InlineLinkText} from '#/components/Link' +import {ButtonIcon, ButtonText} from '#/components/Button' +import {Inbox_Stroke2_Corner2_Rounded as InboxIcon} from '#/components/icons/Inbox' +import {Link} from '#/components/Link' -export function InboxRequests({count, more}: {count: number; more: boolean}) { +export function InboxRequests({ + count, + more, + variant, +}: { + count: number + more: boolean + variant?: 'ghost' | 'solid' +}) { const {t: l} = useLingui() - if (count < 1) return null + const unread = count > 0 - const label = - count >= UNREAD_LIMIT && more + const label = !unread + ? l({ + message: `Requests`, + comment: 'Incoming message requests', + }) + : count >= UNREAD_LIMIT && more ? l({ message: `${count}+ requests`, comment: 'Displayed when the number of requests is greater than 20', @@ -21,12 +35,43 @@ export function InboxRequests({count, more}: {count: number; more: boolean}) { other: '# requests', }) - return ( - - {label} - - ) + switch (variant) { + case 'ghost': { + return ( + + + {unread && ( + + {count >= UNREAD_LIMIT && more + ? l({ + message: `${count}+`, + comment: + 'Displayed when the number of requests is greater than 20', + }) + : count} + + )} + + ) + } + case 'solid': { + return ( + + + {label} + + ) + } + } } diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 50e403ca83..e19dbc109e 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -321,8 +321,8 @@ export function ComposerContainer({children}: {children: React.ReactNode}) { a.pl_lg, a.pb_lg, // prevent overlap with the scrollbar, which looks ugly - a.pr_xs, // xs + md = lg - {width: `calc(100% - ${tokens.space.md}px)` as '100%'}, + a.pr_sm, // sm + sm = lg + {width: `calc(100% - ${tokens.space.sm}px)` as '100%'}, ], })} key={t.name} // android does not update when you change the colors. sigh. diff --git a/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx b/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx new file mode 100644 index 0000000000..00e57b4e6a --- /dev/null +++ b/src/screens/Messages/components/splitView/MessagesSplitViewLayout.tsx @@ -0,0 +1,128 @@ +import {View} from 'react-native' +import {type ScreenLayoutArgs, useIsFocused} from '@react-navigation/native' +import {type NativeStackNavigationProp} from '@react-navigation/native-stack' + +import {type FlatNavigatorParams} from '#/lib/routes/types' +import {type NativeStackNavigationOptionsWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth' +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 {LockScroll} from '#/components/LockScroll' +import {useAgeAssurance} from '#/ageAssurance' +import {IS_WEB} from '#/env' +import {ChatList, Header as ChatListHeader} from '../../ChatList' +import {SplitViewProvider} from './context' + +const CENTER_COLUMN_WIDTH = 600 +const LEFT_NAV_FULL_WIDTH = 245 +const LEFT_NAV_MINIMAL_WIDTH = 86 +const RIGHT_NAV_FULL_WIDTH = 330 +const RIGHT_NAV_MINIMAL_WIDTH = 280 + +type MessageScreens = + | 'Messages' + | 'MessagesConversation' + | 'MessagesConversationSettings' + | 'MessagesInbox' + | 'MessagesSettings' + +type LayoutProps = ScreenLayoutArgs< + FlatNavigatorParams, + MessageScreens, + NativeStackNavigationOptionsWithAuth, + NativeStackNavigationProp< + FlatNavigatorParams, + MessageScreens, + string | undefined + > +> +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.name === 'MessagesConversationSettings') && + route.params && + 'conversation' in route.params + ? route.params.conversation + : undefined + + const rightNavWidth = centerColumnOffset + ? RIGHT_NAV_MINIMAL_WIDTH + : RIGHT_NAV_FULL_WIDTH + + const leftNavWidth = centerColumnOffset + ? LEFT_NAV_MINIMAL_WIDTH + : LEFT_NAV_FULL_WIDTH - LEFT_NAV_MINIMAL_WIDTH + + // slight reduce width for smaller breakpoint + const centerColumnWidth = centerColumnOffset + ? CENTER_COLUMN_WIDTH - 50 + : CENTER_COLUMN_WIDTH + + // nasty magic numbers here, sorry :( + const offset = centerColumnOffset + ? LEFT_NAV_MINIMAL_WIDTH - 34 + : LEFT_NAV_MINIMAL_WIDTH + 5 + + const containerWidth = leftNavWidth + centerColumnWidth + rightNavWidth + + 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/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index dc5e6b1ad0..5bd36e3374 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -32,7 +32,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' import {type Props as SVGIconProps} from '#/components/icons/common' -import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' +import {EditBig_Stroke2_Corner2_Rounded as EditBig} from '#/components/icons/EditBig' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx index 634779f9db..6ed5f4b5b6 100644 --- a/src/screens/Profile/ProfileFeed/index.tsx +++ b/src/screens/Profile/ProfileFeed/index.tsx @@ -10,7 +10,6 @@ import {useQueryClient} from '@tanstack/react-query' import {VIDEO_FEED_URIS} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {ComposeIcon2} from '#/lib/icons' import {type CommonNavigatorParams} from '#/lib/routes/types' import {cleanError} from '#/lib/strings/errors' import {makeRecordUri} from '#/lib/strings/url-helpers' @@ -40,6 +39,8 @@ import { ProfileFeedHeader, ProfileFeedHeaderSkeleton, } from '#/screens/Profile/components/ProfileFeedHeader' +import {useTheme} from '#/alf' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' import * as Layout from '#/components/Layout' import {IS_NATIVE} from '#/env' @@ -134,6 +135,7 @@ export function ProfileFeedScreenInner({ const {hasSession} = useSession() const {openComposer} = useOpenComposer() const isScreenFocused = useIsFocused() + const t = useTheme() useSetTitle(feedInfo?.displayName) @@ -210,13 +212,7 @@ export function ProfileFeedScreenInner({ openComposer({logContext: 'Fab'})} - icon={ - - } + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" diff --git a/src/screens/ProfileList/index.tsx b/src/screens/ProfileList/index.tsx index a090675eb5..77af470cd5 100644 --- a/src/screens/ProfileList/index.tsx +++ b/src/screens/ProfileList/index.tsx @@ -15,7 +15,6 @@ import {useQueryClient} from '@tanstack/react-query' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {ComposeIcon2} from '#/lib/icons' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -38,6 +37,7 @@ import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' import {atoms as a, native, platform, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Hider from '#/components/moderation/Hider' @@ -232,13 +232,7 @@ function ProfileListScreenLoaded({ openComposer({logContext: 'Fab'})} - icon={ - - } + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" @@ -281,13 +275,7 @@ function ProfileListScreenLoaded({ openComposer({logContext: 'Fab'})} - icon={ - - } + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx index 27cc29096a..b23eab85f4 100644 --- a/src/screens/Settings/components/SettingsList.tsx +++ b/src/screens/Settings/components/SettingsList.tsx @@ -197,11 +197,12 @@ export function ItemIcon({ '2xs': 8, xs: 12, sm: 16, - md: 20, + md: 18, lg: 24, xl: 28, '2xl': 32, '3xl': 40, + '4xl': 48, }[size] const color = diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index faf17e7168..331c62e0d1 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -15,10 +15,8 @@ import {useQueryClient} from '@tanstack/react-query' import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {ComposeIcon2} from '#/lib/icons' import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' import {type AllNavigatorParams} from '#/lib/routes/types' -import {s} from '#/lib/styles' import {listenSoftReset} from '#/state/events' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {useSetHomeBadge} from '#/state/home-badge' @@ -30,14 +28,16 @@ import { } from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' import {useSession} from '#/state/session' +import {PostFeed} from '#/view/com/posts/PostFeed' +import {FAB} from '#/view/com/util/fab/FAB' +import {type ListMethods} from '#/view/com/util/List' +import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' +import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' +import {useTheme} from '#/alf' import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' -import {PostFeed} from '../posts/PostFeed' -import {FAB} from '../util/fab/FAB' -import {type ListMethods} from '../util/List' -import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' -import {MainScrollProvider} from '../util/MainScrollProvider' const POLL_FREQ = 60e3 // 60sec @@ -81,6 +81,7 @@ export function FeedPage({ const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode return IS_NATIVE && _isVideoFeed }, [feedInfo]) + const t = useTheme() useEffect(() => { if (isPageFocused) { @@ -173,7 +174,7 @@ export function FeedPage({ } + icon={} accessibilityRole="button" accessibilityLabel={_(msg({message: `New post`, context: 'action'}))} accessibilityHint="" diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index 232212f8f4..59799cf949 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -2,45 +2,53 @@ 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 { + Button, + ButtonIcon, + type ButtonProps, + ButtonText, +} from '#/components/Button' import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' import {Text} from '#/components/Typography' export type EmptyStateButtonProps = Omit & { label: string text: string + icon?: React.ComponentProps['icon'] } export function EmptyState({ testID, icon, iconSize = '3xl', + iconColor, message, style, textStyle, button, }: { testID?: string - icon?: React.ComponentType | React.ReactElement - iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' + icon?: React.ComponentType | React.ReactElement | null + iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' + iconColor?: string 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 } @@ -57,8 +65,7 @@ export function EmptyState({ return ( ) } @@ -79,16 +86,14 @@ export function EmptyState({ {height: 64, width: 64}, isValidElement(icon) ? a.bg_transparent - : [isTabletOrDesktop && {marginTop: 50}], + : [gtTablet && {marginTop: 50}], ]}> {renderIcon()} {button && ( - + diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 86ad61aa4a..eaea3c47a9 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -22,6 +22,8 @@ import {batchedUpdates} from '#/lib/batchedUpdates' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useScrollHandlers} from '#/lib/ScrollContext' import {addStyle} from '#/lib/styles' +import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context' +import {useTheme, web} from '#/alf' import * as Layout from '#/components/Layout' export type ListMethods = { @@ -60,7 +62,7 @@ function ListImpl( ListHeaderComponent, ListFooterComponent, ListEmptyComponent, - disableFullWindowScroll, + disableFullWindowScroll: disableFullWindowScrollProp, contentContainerStyle, data, desktopFixedHeight, @@ -83,6 +85,12 @@ function ListImpl( ref: React.Ref, ) { const contextScrollHandlers = useScrollHandlers() + const {isWithinSplitView} = useIsWithinSplitView() + const t = useTheme() + + // automatically disable full window scroll when within split view + const disableFullWindowScroll = + disableFullWindowScrollProp ?? isWithinSplitView const isEmpty = !data || data.length === 0 @@ -329,11 +337,15 @@ function ListImpl( }> diff --git a/src/view/com/util/fab/FAB.web.tsx b/src/view/com/util/fab/FAB.web.tsx index 976c19e7df..ac5b08701e 100644 --- a/src/view/com/util/fab/FAB.web.tsx +++ b/src/view/com/util/fab/FAB.web.tsx @@ -1,13 +1,13 @@ import {View} from 'react-native' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useBreakpoints} from '#/alf' import {FABInner, type FABProps} from './FABInner' -export const FAB = (_opts: FABProps) => { - const {isDesktop} = useWebMediaQueries() +export const FAB = (props: FABProps) => { + const {gtMobile} = useBreakpoints() - if (!isDesktop) { - return + if (!gtMobile) { + return } return diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 5552e88490..971c63734d 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -9,7 +9,6 @@ import debounce from 'lodash.debounce' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {ComposeIcon2} from '#/lib/icons' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -37,6 +36,7 @@ import * as FeedCard from '#/components/FeedCard' import {SearchInput} from '#/components/forms/SearchInput' import {IconCircle} from '#/components/IconCircle' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' @@ -104,6 +104,7 @@ type FlatlistSlice = export function FeedsScreen(_props: Props) { const pal = usePalette('default') + const t = useTheme() const {openComposer} = useOpenComposer() const {isMobile} = useWebMediaQueries() const [query, setQuery] = useState('') @@ -539,7 +540,7 @@ export function FeedsScreen(_props: Props) { } + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 3423d689a7..d35d0274e7 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -8,12 +8,10 @@ import {useQueryClient} from '@tanstack/react-query' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {ComposeIcon2} from '#/lib/icons' import { type NativeStackScreenProps, type NotificationsTabNavigatorParams, } from '#/lib/routes/types' -import {s} from '#/lib/styles' import {logger} from '#/logger' import {emitSoftReset, listenSoftReset} from '#/state/events' import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' @@ -33,6 +31,7 @@ import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' import {atoms as a, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {ButtonIcon} from '#/components/Button' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' import * as Layout from '#/components/Layout' import {InlineLinkText, Link} from '#/components/Link' @@ -50,6 +49,7 @@ type Props = NativeStackScreenProps< > export function NotificationsScreen({}: Props) { const {_} = useLingui() + const t = useTheme() const {openComposer} = useOpenComposer() const unreadNotifs = useUnreadNotifications() const hasNew = !!unreadNotifs @@ -161,7 +161,7 @@ export function NotificationsScreen({}: Props) { openComposer({logContext: 'Fab'})} - icon={} + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index a8e07cb59b..35718b9589 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -16,7 +16,6 @@ import {useQueryClient} from '@tanstack/react-query' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {ComposeIcon2} from '#/lib/icons' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -25,7 +24,7 @@ import { import {combinedDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {isInvalidHandle} from '#/lib/strings/handles' -import {colors, s} from '#/lib/styles' +import {colors} from '#/lib/styles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {listenSoftReset} from '#/state/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -43,8 +42,9 @@ import {type ListRef} from '#/view/com/util/List' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as CircleAndSquareIcon} from '#/components/icons/CircleAndSquare' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Message_Stroke1_Corner0_Rounded_Filled as MessageIcon} from '#/components/icons/Message' @@ -172,6 +172,7 @@ function ProfileScreenLoaded({ hideBackButton: boolean isPlaceholderProfile: boolean }) { + const t = useTheme() const profile = useProfileShadow(profileUnshadowed) const {hasSession, currentAccount} = useSession() const {openComposer} = useOpenComposer() @@ -587,7 +588,7 @@ function ProfileScreenLoaded({ } + icon={} accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 4df4a7ffd6..4191f94176 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -46,9 +46,10 @@ import {DesktopRightNav} from './desktop/RightNav' // Older screens are unmounted to prevent memory growth during long sessions. const WEB_MAX_CACHED_SCREENS = 5 -type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { - requireAuth?: boolean -} +export type NativeStackNavigationOptionsWithAuth = + NativeStackNavigationOptions & { + requireAuth?: boolean + } function NativeStackNavigator({ id, @@ -200,7 +201,11 @@ function NativeStackNavigator({ {IS_WEB && ( <> - {showBottomBar ? : } + {showBottomBar ? ( + + ) : ( + + )} {!isMobile && } )} diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 8ca25a8d5e..37c32f231a 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -8,8 +8,6 @@ import {useNavigation, useNavigationState} from '@react-navigation/native' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import { @@ -30,7 +28,14 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {UserAvatar} from '#/view/com/util/UserAvatar' import {NavSignupCard} from '#/view/shell/NavSignupCard' -import {atoms as a, tokens, useLayoutBreakpoints, useTheme, web} from '#/alf' +import { + atoms as a, + tokens, + useBreakpoints, + useLayoutBreakpoints, + useTheme, + web, +} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {type DialogControlProps} from '#/components/Dialog' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' @@ -44,7 +49,7 @@ import { BulletList_Stroke2_Corner0_Rounded as List, } from '#/components/icons/BulletList' import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' -import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' +import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig' import { Hashtag_Filled_Corner0_Rounded as HashtagFilled, Hashtag_Stroke2_Corner0_Rounded as Hashtag, @@ -76,12 +81,12 @@ import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' import {useActorStatus} from '#/features/liveNow' +import {router} from '#/routes' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' -import {router} from '../../../routes' const NAV_ICON_WIDTH = 28 -function ProfileCard() { +function ProfileCard({minimal}: {minimal: boolean}) { const {currentAccount, accounts} = useSession() const {logoutEveryAccount} = useSessionApi() const {isLoading, data} = useProfilesQuery({ @@ -89,7 +94,6 @@ function ProfileCard() { }) const profiles = data?.profiles const signOutPromptControl = Prompt.usePromptControl() - const {leftNavMinimal} = useLayoutBreakpoints() const {_} = useLingui() const t = useTheme() @@ -106,7 +110,7 @@ function ProfileCard() { const {isActive: live} = useActorStatus(profile) return ( - + {!isLoading && profile ? ( @@ -125,7 +129,7 @@ function ProfileCard() { a.align_center, a.flex_row, {gap: 6}, - !leftNavMinimal && [a.pl_lg, a.pr_md], + !minimal && [a.pl_lg, a.pr_md], ]}> @@ -150,7 +154,7 @@ function ProfileCard() { live={live} /> - {!leftNavMinimal && ( + {!minimal && ( <> )} { + if (currentRouteInfo.name === 'Profile') { + return ( + isTab(currentRouteInfo.name, pathName) && (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === currentAccount?.handle - : isTab(currentRouteInfo.name, pathName) + ) + } else { + return isTab(currentRouteInfo.name, pathName) + } + }, [currentAccount?.handle, currentRouteInfo, pathName]) + const onProfilePress = useCallback( (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey || e.altKey) { @@ -374,12 +384,21 @@ interface NavItemProps { icon: JSX.Element iconFilled: JSX.Element label: string + minimal: boolean } -function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { +function NavItem({ + count, + hasNew, + href, + icon, + iconFilled, + label, + minimal, +}: NavItemProps) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() - const {leftNavMinimal} = useLayoutBreakpoints() + const [pathName] = useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { if (!state) { @@ -393,6 +412,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === currentAccount?.handle : isTab(currentRouteInfo.name, pathName) + const isRelated = currentRouteInfo.name.startsWith(pathName) const navigation = useNavigation() const onPressWrapped = useCallback( (e: React.MouseEvent) => { @@ -417,7 +437,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { a.flex_row, a.align_center, a.p_md, - a.rounded_sm, + a.rounded_full, a.gap_sm, a.outline_inset_1, a.transition_color, @@ -438,12 +458,8 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { width: 24, height: 24, }, - leftNavMinimal && { - width: 40, - height: 40, - }, ]}> - {isCurrent ? iconFilled : icon} + {isCurrent || isRelated ? iconFilled : icon} {typeof count === 'string' && count ? ( {count} @@ -502,15 +512,11 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { right: -2, top: -4, }, - leftNavMinimal && { - right: 4, - top: 2, - }, ]} /> ) : null} - {!leftNavMinimal && ( + {!minimal && ( {label} @@ -519,12 +525,11 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { ) } -function ComposeBtn() { +function ComposeBtn({minimal}: {minimal: boolean}) { const {currentAccount} = useSession() const {getState} = useNavigation() const {openComposer} = useOpenComposer() const {_} = useLingui() - const {leftNavMinimal} = useLayoutBreakpoints() const [isFetchingHandle, setIsFetchingHandle] = useState(false) const fetchHandle = useFetchHandle() @@ -564,31 +569,28 @@ function ComposeBtn() { const onPressCompose = async () => openComposer({mention: await getProfileHandle(), logContext: 'Fab'}) - if (leftNavMinimal) { - return null - } - return ( - + ) } -function ChatNavItem() { - const pal = usePalette('default') +function ChatNavItem({minimal}: {minimal: boolean}) { + const t = useTheme() const {_} = useLingui() const numUnreadMessages = useUnreadMessageCount() const aa = useAgeAssurance() @@ -596,14 +598,19 @@ function ChatNavItem() { return ( + } iconFilled={ @@ -613,15 +620,24 @@ function ChatNavItem() { ) } -export function DesktopLeftNav() { +export function DesktopLeftNav({routeName}: {routeName: string}) { const {hasSession, currentAccount} = useSession() - const pal = usePalette('default') const {_} = useLingui() - const {isDesktop} = useWebMediaQueries() - const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() + const t = useTheme() + const {gtMobile} = useBreakpoints() + + const aa = useAgeAssurance() + // splitview uses the minimal variant of the leftnav. unfortunately there's no easy + // way to thread this data through because of the view hierarchy, so just check the route name + const isMessagesRelatedScreen = + routeName.startsWith('Messages') && aa.state.access === aa.Access.Full + const {leftNavMinimal: leftNavMinimalBreakpoint, centerColumnOffset} = + useLayoutBreakpoints() const numUnreadNotifications = useUnreadNotifications() - if (!hasSession && !isDesktop) { + const leftNavMinimal = isMessagesRelatedScreen || leftNavMinimalBreakpoint + + if (!hasSession && !gtMobile) { return null } @@ -637,7 +653,11 @@ export function DesktopLeftNav() { transform: [ { translateX: - -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), + -300 + + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0) + + (isMessagesRelatedScreen && !leftNavMinimalBreakpoint + ? -153 + : 0), }, {translateX: '-100%'}, ...a.scrollbar_offset.transform, @@ -645,7 +665,7 @@ export function DesktopLeftNav() { }, ]}> {hasSession ? ( - + ) : !leftNavMinimal ? ( @@ -656,34 +676,36 @@ export function DesktopLeftNav() { <> } iconFilled={ } label={_(msg`Home`)} /> } iconFilled={ @@ -692,36 +714,38 @@ export function DesktopLeftNav() { /> } iconFilled={ } label={_(msg`Notifications`)} /> - + } iconFilled={ @@ -730,16 +754,17 @@ export function DesktopLeftNav() { /> } iconFilled={ @@ -748,16 +773,17 @@ export function DesktopLeftNav() { /> } iconFilled={ @@ -771,42 +797,44 @@ export function DesktopLeftNav() { /> } iconFilled={ } label={_(msg`Profile`)} /> } iconFilled={ } label={_(msg`Settings`)} /> - + )} @@ -824,15 +852,12 @@ const styles = StyleSheet.create({ // @ts-expect-error web only maxHeight: '100vh', overflowY: 'auto', + scrollbarWidth: 'thin', }, leftNavWide: { width: 245, }, leftNavMinimal: { - paddingTop: 0, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, height: '100%', width: 86, alignItems: 'center', diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 07ea25632a..405386ae10 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -49,6 +49,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const kawaii = useKawaiiMode() const gutters = useGutters(['base', 0, 'base', 'wide']) const isSearchScreen = routeName === 'Search' + const isMessagesRelatedScreen = routeName.startsWith('Messages') const webqueryParams = useWebQueryParams() const searchQuery = webqueryParams?.q const showExploreScreenDuplicatedContent = @@ -56,7 +57,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const {rightNavVisible, centerColumnOffset, leftNavMinimal} = useLayoutBreakpoints() - if (!rightNavVisible) { + if (!rightNavVisible || isMessagesRelatedScreen) { return null }