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
}