[Chat] Split view layout for web (#10258)
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51a1.84 1.84 0 0 0-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z"/><path fill="#111822" d="M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z"/><path fill="#000" d="M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 847 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M19 14h-2.417a5 5 0 0 1-9.166 0H5v4a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4Zm0-8a1 1 0 0 0-.898-.995L18 5H6a1 1 0 0 0-1 1v6h3.126a1 1 0 0 1 .969.751 3.001 3.001 0 0 0 5.81 0l.056-.16a1 1 0 0 1 .913-.591H19V6Zm2 12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12l.154.004A3 3 0 0 1 21 6v12Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001a6.33 6.33 0 0 1 .834 3.142v11.917Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 875 B |
@@ -143,6 +143,7 @@ import {setNavigationMetadata} from '#/analytics/metadata'
|
|||||||
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
import {renderMessagesSplitViewLayout} from './screens/Messages/components/splitView/MessagesSplitViewLayout'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -564,6 +565,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => TopicScreen}
|
getComponent={() => TopicScreen}
|
||||||
options={{title: title(msg`Topic`)}}
|
options={{title: title(msg`Topic`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Group screenLayout={renderMessagesSplitViewLayout}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="MessagesConversation"
|
name="MessagesConversation"
|
||||||
getComponent={() => MessagesConversationScreen}
|
getComponent={() => MessagesConversationScreen}
|
||||||
@@ -584,6 +586,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => MessagesInboxScreen}
|
getComponent={() => MessagesInboxScreen}
|
||||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
</Stack.Group>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="NotificationsActivityList"
|
name="NotificationsActivityList"
|
||||||
getComponent={() => NotificationsActivityListScreen}
|
getComponent={() => NotificationsActivityListScreen}
|
||||||
@@ -832,6 +835,7 @@ const FlatNavigator = ({
|
|||||||
name="Messages"
|
name="Messages"
|
||||||
getComponent={() => MessagesScreen}
|
getComponent={() => MessagesScreen}
|
||||||
options={{title: title(msg`Messages`), requireAuth: true}}
|
options={{title: title(msg`Messages`), requireAuth: true}}
|
||||||
|
layout={renderMessagesSplitViewLayout}
|
||||||
/>
|
/>
|
||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Start"
|
name="Start"
|
||||||
|
|||||||
@@ -811,14 +811,15 @@ export function ButtonIcon({
|
|||||||
* also so that we can calculate transforms.
|
* also so that we can calculate transforms.
|
||||||
*/
|
*/
|
||||||
const iconSize = {
|
const iconSize = {
|
||||||
|
'2xs': 8,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 16,
|
sm: 16,
|
||||||
md: 18,
|
md: 18,
|
||||||
lg: 24,
|
lg: 24,
|
||||||
xl: 28,
|
xl: 28,
|
||||||
'2xs': 8,
|
|
||||||
'2xl': 32,
|
'2xl': 32,
|
||||||
'3xl': 40,
|
'3xl': 40,
|
||||||
|
'4xl': 48,
|
||||||
}[iconSizeShorthand]
|
}[iconSizeShorthand]
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
platform,
|
platform,
|
||||||
@@ -46,6 +47,7 @@ export function Outer({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -64,7 +66,8 @@ export function Outer({
|
|||||||
}),
|
}),
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||||
!isWithinOffsetView && {
|
!isWithinOffsetView &&
|
||||||
|
!isWithinSplitView && {
|
||||||
transform: [
|
transform: [
|
||||||
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
||||||
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
@@ -108,6 +111,7 @@ export function Slot({children}: {children?: React.ReactNode}) {
|
|||||||
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {isWithinRightPanel} = useIsWithinSplitView()
|
||||||
|
|
||||||
const onPressBack = useCallback(
|
const onPressBack = useCallback(
|
||||||
(evt: GestureResponderEvent) => {
|
(evt: GestureResponderEvent) => {
|
||||||
@@ -122,6 +126,10 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
|||||||
[onPress, navigation],
|
[onPress, navigation],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isWithinRightPanel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot>
|
<Slot>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import {forwardRef, memo, useContext, useMemo} from 'react'
|
import {forwardRef, memo, useContext, useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
type StyleProp,
|
type StyleProp,
|
||||||
StyleSheet,
|
|
||||||
View,
|
View,
|
||||||
type ViewProps,
|
type ViewProps,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
|
||||||
KeyboardAwareScrollView,
|
|
||||||
type KeyboardAwareScrollViewProps,
|
|
||||||
} from 'react-native-keyboard-controller'
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
type AnimatedScrollViewProps,
|
type AnimatedScrollViewProps,
|
||||||
useAnimatedProps,
|
useAnimatedProps,
|
||||||
@@ -18,6 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
|
|
||||||
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
@@ -49,14 +45,20 @@ export const Screen = memo(function Screen({
|
|||||||
...props
|
...props
|
||||||
}: ScreenProps) {
|
}: ScreenProps) {
|
||||||
const {top} = useSafeAreaInsets()
|
const {top} = useSafeAreaInsets()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IS_WEB && <WebCenterBorders />}
|
{IS_WEB && !isWithinSplitView && <WebCenterBorders />}
|
||||||
<View
|
<View
|
||||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
style={[
|
||||||
|
a.util_screen_outer,
|
||||||
|
{paddingTop: noInsetTop ? 0 : top},
|
||||||
|
isWithinSplitView && {maxHeight: '100%'},
|
||||||
|
style,
|
||||||
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -85,6 +87,7 @@ export const Content = memo(
|
|||||||
) {
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
const animatedProps = useAnimatedProps(() => {
|
const animatedProps = useAnimatedProps(() => {
|
||||||
return {
|
return {
|
||||||
scrollIndicatorInsets: {
|
scrollIndicatorInsets: {
|
||||||
@@ -103,11 +106,18 @@ export const Content = memo(
|
|||||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||||
// sets the scroll inset to the height of the footer
|
// sets the scroll inset to the height of the footer
|
||||||
animatedProps={animatedProps}
|
animatedProps={animatedProps}
|
||||||
style={[scrollViewStyles.common, style]}
|
style={[
|
||||||
contentContainerStyle={[
|
a.w_full,
|
||||||
scrollViewStyles.contentContainer,
|
isWithinSplitView &&
|
||||||
contentContainerStyle,
|
web({
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'scroll',
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||||
|
}),
|
||||||
|
style,
|
||||||
]}
|
]}
|
||||||
|
contentContainerStyle={[contentContainerStyle]}
|
||||||
{...props}>
|
{...props}>
|
||||||
{IS_WEB ? (
|
{IS_WEB ? (
|
||||||
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||||
@@ -122,45 +132,6 @@ export const Content = memo(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const scrollViewStyles = StyleSheet.create({
|
|
||||||
common: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
paddingBottom: 100,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
|
|
||||||
children: React.ReactNode
|
|
||||||
contentContainerStyle?: StyleProp<ViewStyle>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default scroll view for simple pages.
|
|
||||||
*
|
|
||||||
* BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
|
|
||||||
*/
|
|
||||||
export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
|
|
||||||
children,
|
|
||||||
style,
|
|
||||||
contentContainerStyle,
|
|
||||||
...props
|
|
||||||
}: KeyboardAwareContentProps) {
|
|
||||||
return (
|
|
||||||
<KeyboardAwareScrollView
|
|
||||||
style={[scrollViewStyles.common, style]}
|
|
||||||
contentContainerStyle={[
|
|
||||||
scrollViewStyles.contentContainer,
|
|
||||||
contentContainerStyle,
|
|
||||||
]}
|
|
||||||
keyboardShouldPersistTaps="handled"
|
|
||||||
{...props}>
|
|
||||||
{IS_WEB ? <Center>{children}</Center> : children}
|
|
||||||
</KeyboardAwareScrollView>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility component to center content within the screen
|
* Utility component to center content within the screen
|
||||||
*/
|
*/
|
||||||
@@ -174,16 +145,18 @@ export const Center = memo(function LayoutCenter({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
const {isWithinDialog} = useDialogContext()
|
const {isWithinDialog} = useDialogContext()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.mx_auto,
|
!isWithinSplitView && a.mx_auto,
|
||||||
gtMobile && {
|
gtMobile && {
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
},
|
},
|
||||||
!isWithinOffsetView && {
|
!isWithinOffsetView &&
|
||||||
|
!isWithinSplitView && {
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateX:
|
translateX:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {createSinglePathSVG} from './TEMPLATE'
|
import {createMultiPathSVG, createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
|
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
|
||||||
@@ -15,3 +15,12 @@ export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({
|
|||||||
export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z',
|
path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const BubbleSmile_Stroke2_Corner2_Rounded_Large = createMultiPathSVG({
|
||||||
|
viewBox: '0 0 64 64',
|
||||||
|
paths: [
|
||||||
|
'M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51c.094-.384.046-.809-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z',
|
||||||
|
'M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z',
|
||||||
|
'M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Inbox_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M19 14h-2.417a5 5 0 0 1-9.166 0H5v4a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4Zm0-8a1 1 0 0 0-.898-.995L18 5H6a1 1 0 0 0-1 1v6h3.126a1 1 0 0 1 .969.751 3.001 3.001 0 0 0 5.81 0l.056-.16a1 1 0 0 1 .913-.591H19V6Zm2 12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12l.154.004A3 3 0 0 1 21 6v12Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Inbox_Stroke2_Corner2_Rounded_Large = createSinglePathSVG({
|
||||||
|
viewBox: '0 0 64 64',
|
||||||
|
path: 'M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001c.547.957.834 2.04.834 3.142v11.917Z',
|
||||||
|
})
|
||||||
@@ -51,7 +51,7 @@ export function createSinglePathSVG({
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
viewBox={viewBox || '0 0 24 24'}
|
viewBox={viewBox ?? '0 0 24 24'}
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
style={[style]}>
|
style={[style]}>
|
||||||
@@ -71,7 +71,13 @@ export function createSinglePathSVG({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSinglePathSVG2({path}: {path: string}) {
|
export function createMultiPathSVG({
|
||||||
|
paths,
|
||||||
|
viewBox,
|
||||||
|
}: {
|
||||||
|
paths: string[]
|
||||||
|
viewBox?: string
|
||||||
|
}) {
|
||||||
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||||
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
||||||
|
|
||||||
@@ -80,27 +86,7 @@ export function createSinglePathSVG2({path}: {path: string}) {
|
|||||||
fill="none"
|
fill="none"
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
viewBox="0 0 24 24"
|
viewBox={viewBox ?? '0 0 24 24'}
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
style={style}>
|
|
||||||
{gradient}
|
|
||||||
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMultiPathSVG({paths}: {paths: string[]}) {
|
|
||||||
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
|
||||||
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Svg
|
|
||||||
fill="none"
|
|
||||||
{...rest}
|
|
||||||
ref={ref}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
style={[style]}>
|
style={[style]}>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const sizes = {
|
|||||||
xl: 28,
|
xl: 28,
|
||||||
'2xl': 32,
|
'2xl': 32,
|
||||||
'3xl': 48,
|
'3xl': 48,
|
||||||
|
'4xl': 64,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function useCommonSVGProps(props: Props) {
|
export function useCommonSVGProps(props: Props) {
|
||||||
|
|||||||
@@ -184,47 +184,6 @@ export function RectTallIcon({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComposeIcon2({
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
strokeWidth = 1.5,
|
|
||||||
}: {
|
|
||||||
style?: StyleProp<TextStyle>
|
|
||||||
size?: string | number
|
|
||||||
strokeWidth?: number
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
width={size || 24}
|
|
||||||
height={size || 24}
|
|
||||||
style={style}>
|
|
||||||
<Path
|
|
||||||
d="M 20 9 L 20 16 C 20 18.209 18.209 20 16 20 L 8 20 C 5.791 20 4 18.209 4 16 L 4 8 C 4 5.791 5.791 4 8 4 L 15 4"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
strokeLinecap="round"
|
|
||||||
x1="10"
|
|
||||||
y1="14"
|
|
||||||
x2="18.5"
|
|
||||||
y2="5.5"
|
|
||||||
strokeWidth={strokeWidth * 1.5}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
strokeLinecap="round"
|
|
||||||
x1="20.5"
|
|
||||||
y1="3.5"
|
|
||||||
x2="21"
|
|
||||||
y2="3"
|
|
||||||
strokeWidth={strokeWidth * 1.5}
|
|
||||||
/>
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfoCircleIcon({
|
export function InfoCircleIcon({
|
||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
MyProfileTab: undefined
|
MyProfileTab: undefined
|
||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
|
|||||||
+181
-127
@@ -2,9 +2,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useAnimatedRef} from 'react-native-reanimated'
|
import {useAnimatedRef} from 'react-native-reanimated'
|
||||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
@@ -19,9 +17,10 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
|||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -29,9 +28,13 @@ import {type DialogControlProps, useDialogControl} from '#/components/Dialog'
|
|||||||
import {NewChat} from '#/components/dms/dialogs/NewChatDialog'
|
import {NewChat} from '#/components/dms/dialogs/NewChatDialog'
|
||||||
import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
|
import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
|
||||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
||||||
|
import {BubbleSmile_Stroke2_Corner2_Rounded_Large as BubbleSmileIcon} from '#/components/icons/Bubble'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
||||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
import {Inbox_Stroke2_Corner2_Rounded_Large as InboxLargeIcon} from '#/components/icons/Inbox'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
import {
|
||||||
|
MessagePlus_Stroke2_Corner0_Rounded as MessagePlusIcon,
|
||||||
|
MessagePlus_Stroke2_Corner0_Rounded as NewChatIcon,
|
||||||
|
} from '#/components/icons/Message'
|
||||||
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
@@ -41,14 +44,16 @@ import {useAgeAssurance} from '#/ageAssurance'
|
|||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {ChatListItem} from './components/ChatListItem'
|
import {ChatListItem} from './components/ChatListItem'
|
||||||
import {InboxRequests} from './components/InboxRequests'
|
import {InboxRequests} from './components/InboxRequests'
|
||||||
|
import {useIsWithinSplitView} from './components/splitView/context'
|
||||||
|
|
||||||
type ListItem = {
|
type ListItem = {
|
||||||
type: 'CONVERSATION'
|
type: 'CONVERSATION'
|
||||||
conversation: ChatBskyConvoDefs.ConvoView
|
conversation: ChatBskyConvoDefs.ConvoView
|
||||||
|
selected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({item}: {item: ListItem}) {
|
function renderItem({item}: {item: ListItem}) {
|
||||||
return <ChatListItem convo={item.conversation} />
|
return <ChatListItem convo={item.conversation} selected={item.selected} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyExtractor(item: ListItem) {
|
function keyExtractor(item: ListItem) {
|
||||||
@@ -58,19 +63,19 @@ function keyExtractor(item: ListItem) {
|
|||||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
||||||
|
|
||||||
export function MessagesScreen(props: Props) {
|
export function MessagesScreen(props: Props) {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const aaCopy = useAgeAssuranceCopy()
|
const aaCopy = useAgeAssuranceCopy()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgeRestrictedScreen
|
<AgeRestrictedScreen
|
||||||
screenTitle={_(msg`Chats`)}
|
screenTitle={l`Chats`}
|
||||||
infoText={aaCopy.chatsInfoText}
|
infoText={aaCopy.chatsInfoText}
|
||||||
rightHeaderSlot={
|
rightHeaderSlot={
|
||||||
aa.flags.chatDisabled ? null : (
|
aa.flags.chatDisabled ? null : (
|
||||||
<Link
|
<Link
|
||||||
to="/messages/settings"
|
to="/messages/settings"
|
||||||
label={_(msg`Chat settings`)}
|
label={l`Chat settings`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary">
|
color="secondary">
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
@@ -85,10 +90,10 @@ export function MessagesScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessagesScreenInner({navigation, route}: Props) {
|
export function MessagesScreenInner({navigation, route}: Props) {
|
||||||
const {_} = useLingui()
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
const {t: l} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const newChatControl = useDialogControl()
|
const newChatControl = useDialogControl()
|
||||||
const scrollElRef: ListRef = useAnimatedRef()
|
|
||||||
const pushToConversation = route.params?.pushToConversation
|
const pushToConversation = route.params?.pushToConversation
|
||||||
|
|
||||||
// Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on
|
// Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on
|
||||||
@@ -120,6 +125,70 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
}, [messagesBus, isActive]),
|
}, [messagesBus, isActive]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onNewChat = useCallback(
|
||||||
|
(conversation: string) =>
|
||||||
|
navigation.navigate('MessagesConversation', {conversation}),
|
||||||
|
[navigation],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isWithinSplitView) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EmptyState
|
||||||
|
message={l`Say hi to someone`}
|
||||||
|
icon={BubbleSmileIcon}
|
||||||
|
textStyle={t.atoms.text}
|
||||||
|
iconColor={t.atoms.text.color}
|
||||||
|
iconSize="4xl"
|
||||||
|
button={{
|
||||||
|
label: l`New chat`,
|
||||||
|
text: l`New chat`,
|
||||||
|
onPress: newChatControl.open,
|
||||||
|
size: 'small',
|
||||||
|
color: 'primary',
|
||||||
|
icon: MessagePlusIcon,
|
||||||
|
}}
|
||||||
|
style={[a.h_full, a.justify_center, a.pb_5xl]}
|
||||||
|
/>
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="messagesScreen">
|
||||||
|
<Header newChatControl={newChatControl} />
|
||||||
|
<ChatList newChatControl={newChatControl} />
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatList({
|
||||||
|
selectedChat,
|
||||||
|
newChatControl,
|
||||||
|
}: {
|
||||||
|
selectedChat?: string
|
||||||
|
newChatControl: DialogControlProps
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const scrollElRef: ListRef = useAnimatedRef()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
|
const openChatControl = useCallback(() => {
|
||||||
|
newChatControl.open()
|
||||||
|
}, [newChatControl])
|
||||||
|
|
||||||
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
const wrappedOpenChatControl = requireEmailVerification(openChatControl, {
|
||||||
|
instructions: [
|
||||||
|
<Trans key="new-chat">
|
||||||
|
Before you can message another user, you must first verify your email.
|
||||||
|
</Trans>,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
|
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
|
||||||
@@ -134,11 +203,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useListConvosQuery({status: 'accepted'})
|
} = useListConvosQuery({status: 'accepted'})
|
||||||
|
|
||||||
const {
|
const {refetch: refetchInbox} = useListConvosQuery({
|
||||||
data: inboxData,
|
|
||||||
refetch: refetchInbox,
|
|
||||||
hasNextPage: hasMoreRequests,
|
|
||||||
} = useListConvosQuery({
|
|
||||||
status: 'request',
|
status: 'request',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,16 +212,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
|
|
||||||
const leftConvos = useLeftConvos()
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
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 conversations = useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
const conversations = data.pages
|
const conversations = data.pages
|
||||||
@@ -164,18 +219,17 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
// filter out convos that are actively being left
|
// filter out convos that are actively being left
|
||||||
.filter(convo => !leftConvos.includes(convo.id))
|
.filter(convo => !leftConvos.includes(convo.id))
|
||||||
|
|
||||||
return [
|
return conversations.map(
|
||||||
...conversations.map(
|
|
||||||
convo =>
|
convo =>
|
||||||
({
|
({
|
||||||
type: 'CONVERSATION',
|
type: 'CONVERSATION',
|
||||||
conversation: convo,
|
conversation: convo,
|
||||||
|
selected: convo.id === selectedChat,
|
||||||
}) as const,
|
}) as const,
|
||||||
),
|
) satisfies ListItem[]
|
||||||
] satisfies ListItem[]
|
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data, leftConvos])
|
}, [data, leftConvos, selectedChat])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
@@ -196,12 +250,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
}
|
}
|
||||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||||
|
|
||||||
const onNewChat = useCallback(
|
|
||||||
(conversation: string) =>
|
|
||||||
navigation.navigate('MessagesConversation', {conversation}),
|
|
||||||
[navigation],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSoftReset = useCallback(async () => {
|
const onSoftReset = useCallback(async () => {
|
||||||
scrollElRef.current?.scrollToOffset({
|
scrollElRef.current?.scrollToOffset({
|
||||||
animated: IS_NATIVE,
|
animated: IS_NATIVE,
|
||||||
@@ -222,21 +270,9 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
return listenSoftReset(() => void onSoftReset())
|
return listenSoftReset(() => void onSoftReset())
|
||||||
}, [onSoftReset, isScreenFocused])
|
}, [onSoftReset, isScreenFocused])
|
||||||
|
|
||||||
// NOTE(APiligrim)
|
if (conversations.length === 0) {
|
||||||
// Show empty state only if there are no conversations at all
|
|
||||||
const activeConversations = conversations.filter(
|
|
||||||
item => item.type === 'CONVERSATION',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (activeConversations.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Center style={web({minHeight: '100%'})}>
|
||||||
<Header
|
|
||||||
newChatControl={newChatControl}
|
|
||||||
requestsCount={inboxAllConvos.length}
|
|
||||||
hasMoreRequests={hasMoreRequests}
|
|
||||||
/>
|
|
||||||
<Layout.Center>
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ChatListLoadingPlaceholder />
|
<ChatListLoadingPlaceholder />
|
||||||
) : (
|
) : (
|
||||||
@@ -261,63 +297,55 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
{maxWidth: 360},
|
{maxWidth: 360},
|
||||||
]}>
|
]}>
|
||||||
{cleanError(error) ||
|
{cleanError(error) || l`Failed to load conversations`}
|
||||||
_(msg`Failed to load conversations`)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Reload conversations`)}
|
label={l`Reload conversations`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
variant="solid"
|
|
||||||
onPress={() => void refetch()}>
|
onPress={() => void refetch()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={RetryIcon} position="right" />
|
<ButtonIcon icon={RetryIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
) : isWithinSplitView ? (
|
||||||
|
<EmptyState
|
||||||
|
message={l`Inbox empty`}
|
||||||
|
icon={InboxLargeIcon}
|
||||||
|
iconSize="4xl"
|
||||||
|
textStyle={t.atoms.text}
|
||||||
|
iconColor={t.atoms.text.color}
|
||||||
|
style={web([a.h_full, a.justify_center, {paddingBottom: 120}])}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<EmptyState
|
||||||
<View style={[a.pt_3xl, a.align_center]}>
|
message={l`No chats yet`}
|
||||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
icon={InboxLargeIcon}
|
||||||
<Text
|
iconSize="4xl"
|
||||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
textStyle={t.atoms.text}
|
||||||
<Trans>Nothing here</Trans>
|
iconColor={t.atoms.text.color}
|
||||||
</Text>
|
button={{
|
||||||
<Text
|
label: l`New chat`,
|
||||||
style={[
|
text: l`New chat`,
|
||||||
a.text_md,
|
onPress: wrappedOpenChatControl,
|
||||||
a.pb_xl,
|
size: 'small',
|
||||||
a.text_center,
|
color: 'primary',
|
||||||
a.leading_snug,
|
icon: MessagePlusIcon,
|
||||||
t.atoms.text_contrast_medium,
|
}}
|
||||||
]}>
|
style={web([a.h_full, a.justify_center, {paddingBottom: 120}])}
|
||||||
<Trans>You have no conversations yet. Start one!</Trans>
|
/>
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Center>
|
</Layout.Center>
|
||||||
|
|
||||||
{!isLoading && !isError && (
|
|
||||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
|
||||||
)}
|
|
||||||
</Layout.Screen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="messagesScreen">
|
|
||||||
<Header
|
|
||||||
newChatControl={newChatControl}
|
|
||||||
requestsCount={inboxAllConvos.length}
|
|
||||||
hasMoreRequests={hasMoreRequests}
|
|
||||||
/>
|
|
||||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
|
||||||
<List
|
<List
|
||||||
ref={scrollElRef}
|
ref={scrollElRef}
|
||||||
data={conversations}
|
data={conversations}
|
||||||
@@ -340,23 +368,42 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
windowSize={11}
|
windowSize={11}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
|
disableFullWindowScroll={isWithinSplitView}
|
||||||
|
style={
|
||||||
|
isWithinSplitView && [
|
||||||
|
a.w_full,
|
||||||
|
web({
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
contentContainerStyle={isWithinSplitView && a.py_sm}
|
||||||
/>
|
/>
|
||||||
</Layout.Screen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header({
|
export function Header({newChatControl}: {newChatControl: DialogControlProps}) {
|
||||||
newChatControl,
|
const {t: l} = useLingui()
|
||||||
requestsCount,
|
|
||||||
hasMoreRequests,
|
|
||||||
}: {
|
|
||||||
newChatControl: DialogControlProps
|
|
||||||
requestsCount: number
|
|
||||||
hasMoreRequests: boolean
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
|
const {data: unreadInboxData, hasNextPage: hasMoreRequests} =
|
||||||
|
useListConvosQuery({
|
||||||
|
status: 'request',
|
||||||
|
readState: 'unread',
|
||||||
|
})
|
||||||
|
|
||||||
|
const inboxAllConvos =
|
||||||
|
unreadInboxData?.pages
|
||||||
|
.flatMap(page => page.convos)
|
||||||
|
.filter(
|
||||||
|
convo =>
|
||||||
|
!leftConvos.includes(convo.id) &&
|
||||||
|
!convo.muted &&
|
||||||
|
convo.members.every(member => member.handle !== 'missing.invalid'),
|
||||||
|
) ?? []
|
||||||
|
|
||||||
const openChatControl = useCallback(() => {
|
const openChatControl = useCallback(() => {
|
||||||
newChatControl.open()
|
newChatControl.open()
|
||||||
@@ -369,46 +416,38 @@ function Header({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestsLink = (
|
|
||||||
<InboxRequests count={requestsCount} more={hasMoreRequests} />
|
|
||||||
)
|
|
||||||
|
|
||||||
const settingsLink = (
|
|
||||||
<Link
|
|
||||||
to="/messages/settings"
|
|
||||||
label={_(msg`Chat settings`)}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
style={[a.justify_center]}>
|
|
||||||
<ButtonIcon icon={SettingsIcon} size="lg" />
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header.Outer>
|
<Layout.Header.Outer>
|
||||||
{gtMobile ? (
|
{gtMobile ? (
|
||||||
<>
|
<>
|
||||||
<Layout.Header.Content>
|
<Layout.Header.Content align="left">
|
||||||
<Layout.Header.TitleText>
|
<Layout.Header.TitleText>
|
||||||
<Trans>Chats</Trans>
|
<Trans>Chats</Trans>
|
||||||
</Layout.Header.TitleText>
|
</Layout.Header.TitleText>
|
||||||
</Layout.Header.Content>
|
</Layout.Header.Content>
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
{requestsLink}
|
<InboxRequests
|
||||||
{settingsLink}
|
count={inboxAllConvos.length}
|
||||||
|
more={hasMoreRequests}
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
to="/messages/settings"
|
||||||
|
label={l`Chat settings`}
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
style={[a.justify_center]}>
|
||||||
|
<ButtonIcon icon={SettingsIcon} />
|
||||||
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`New chat`)}
|
label={l`New chat`}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
shape="round"
|
||||||
onPress={wrappedOpenChatControl}>
|
onPress={wrappedOpenChatControl}>
|
||||||
<ButtonIcon icon={PlusIcon} position="left" />
|
<ButtonIcon icon={NewChatIcon} />
|
||||||
<ButtonText>
|
|
||||||
<Trans>New chat</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@@ -420,8 +459,23 @@ function Header({
|
|||||||
<Trans>Chats</Trans>
|
<Trans>Chats</Trans>
|
||||||
</Layout.Header.TitleText>
|
</Layout.Header.TitleText>
|
||||||
</Layout.Header.Content>
|
</Layout.Header.Content>
|
||||||
{requestsLink}
|
<InboxRequests
|
||||||
<Layout.Header.Slot>{settingsLink}</Layout.Header.Slot>
|
count={inboxAllConvos.length}
|
||||||
|
more={hasMoreRequests}
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
<Layout.Header.Slot>
|
||||||
|
<Link
|
||||||
|
to="/messages/settings"
|
||||||
|
label={l`Chat settings`}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
style={[a.justify_center]}>
|
||||||
|
<ButtonIcon icon={SettingsIcon} size="lg" />
|
||||||
|
</Link>
|
||||||
|
</Layout.Header.Slot>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
useRoute,
|
useRoute,
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
|
||||||
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
|
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
|
||||||
@@ -49,7 +48,7 @@ import {Loader} from '#/components/Loader'
|
|||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_INTERNAL, IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
import {IS_INTERNAL, IS_LIQUID_GLASS} from '#/env'
|
||||||
import {ChatDisabled} from './components/ChatDisabled'
|
import {ChatDisabled} from './components/ChatDisabled'
|
||||||
import {ChatEnded} from './components/ChatEnded'
|
import {ChatEnded} from './components/ChatEnded'
|
||||||
import {ChatLocked} from './components/ChatLocked'
|
import {ChatLocked} from './components/ChatLocked'
|
||||||
@@ -154,8 +153,6 @@ function Inner({convoId}: {convoId: string}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Center style={[a.flex_1]}>
|
<Layout.Center style={[a.flex_1]}>
|
||||||
{/* MessagesList does not use the body scroll */}
|
|
||||||
{isFocused && IS_WEB && <RemoveScrollBar />}
|
|
||||||
{!readyToShow && (
|
{!readyToShow && (
|
||||||
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
|
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
|
||||||
<MessagesListHeader convo={convo} />
|
<MessagesListHeader convo={convo} />
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ import {useMessagesEventBus} from '#/state/messages/events'
|
|||||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
||||||
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -37,13 +38,14 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/i
|
|||||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
||||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
||||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
import {Inbox_Stroke2_Corner2_Rounded_Large as InboxLargeIcon} from '#/components/icons/Inbox'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {ListFooter} from '#/components/Lists'
|
import {ListFooter} from '#/components/Lists'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {RequestListItem} from './components/RequestListItem'
|
import {RequestListItem} from './components/RequestListItem'
|
||||||
|
import {useIsWithinSplitView} from './components/splitView/context'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
||||||
|
|
||||||
@@ -127,6 +129,7 @@ function RequestList({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
// Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future)
|
// 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
|
// but only when the screen is active
|
||||||
@@ -180,7 +183,7 @@ function RequestList({
|
|||||||
|
|
||||||
if (conversations.length < 1) {
|
if (conversations.length < 1) {
|
||||||
return (
|
return (
|
||||||
<Layout.Center>
|
<Layout.Center style={web([a.h_full])}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ChatListLoadingPlaceholder />
|
<ChatListLoadingPlaceholder />
|
||||||
) : (
|
) : (
|
||||||
@@ -212,56 +215,45 @@ function RequestList({
|
|||||||
label={l`Reload conversations`}
|
label={l`Reload conversations`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
variant="solid"
|
|
||||||
onPress={() => void refetch()}>
|
onPress={() => void refetch()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={RetryIcon} position="right" />
|
<ButtonIcon icon={RetryIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<EmptyState
|
||||||
<View style={[a.pt_3xl, a.align_center]}>
|
message={l({
|
||||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
message: `Inbox zero!`,
|
||||||
<Text
|
comment:
|
||||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
"Title message shown in chat requests inbox when it's empty",
|
||||||
<Trans comment="Title message shown in chat requests inbox when it's empty">
|
})}
|
||||||
Inbox zero!
|
icon={InboxLargeIcon}
|
||||||
</Trans>
|
iconSize="4xl"
|
||||||
</Text>
|
textStyle={t.atoms.text}
|
||||||
<Text
|
iconColor={t.atoms.text.color}
|
||||||
style={[
|
button={
|
||||||
a.text_md,
|
isWithinSplitView
|
||||||
a.pb_xl,
|
? undefined
|
||||||
a.text_center,
|
: {
|
||||||
a.leading_snug,
|
label: l`Back to Chats`,
|
||||||
t.atoms.text_contrast_medium,
|
text: l`Back`,
|
||||||
]}>
|
onPress: () => {
|
||||||
<Trans>
|
|
||||||
You don't have any chat requests at the moment.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
|
||||||
size="small"
|
|
||||||
label={l`Go back`}
|
|
||||||
onPress={() => {
|
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('Messages', {animation: 'pop'})
|
navigation.navigate('Messages', {animation: 'pop'})
|
||||||
}
|
}
|
||||||
}}>
|
},
|
||||||
<ButtonIcon icon={ArrowLeftIcon} />
|
size: 'small',
|
||||||
<ButtonText>
|
color: 'secondary',
|
||||||
<Trans>Back to Chats</Trans>
|
icon: ArrowLeftIcon,
|
||||||
</ButtonText>
|
}
|
||||||
</Button>
|
}
|
||||||
</View>
|
style={web([a.h_full, a.justify_center, a.pb_5xl])}
|
||||||
</>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -354,7 +346,6 @@ function MarkAsReadHeaderButton() {
|
|||||||
label={l`Mark all as read`}
|
label={l`Mark all as read`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="solid"
|
|
||||||
onPress={() => markAllRead()}>
|
onPress={() => markAllRead()}>
|
||||||
<ButtonIcon icon={CheckIcon} />
|
<ButtonIcon icon={CheckIcon} />
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
|
|||||||
@@ -51,16 +51,19 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {useIsWithinSplitView} from './splitView/context'
|
||||||
|
|
||||||
export const ChatListItemPortal = createPortalGroup()
|
export const ChatListItemPortal = createPortalGroup()
|
||||||
|
|
||||||
export function ChatListItem({
|
export function ChatListItem({
|
||||||
convo: convoView,
|
convo: convoView,
|
||||||
showMenu = true,
|
showMenu = true,
|
||||||
|
selected = false,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -78,7 +81,8 @@ export function ChatListItem({
|
|||||||
<DirectChatItem
|
<DirectChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}
|
||||||
|
selected={selected}>
|
||||||
{children}
|
{children}
|
||||||
</DirectChatItem>
|
</DirectChatItem>
|
||||||
)
|
)
|
||||||
@@ -88,7 +92,8 @@ export function ChatListItem({
|
|||||||
<GroupChatItem
|
<GroupChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}
|
||||||
|
selected={selected}>
|
||||||
{children}
|
{children}
|
||||||
</GroupChatItem>
|
</GroupChatItem>
|
||||||
)
|
)
|
||||||
@@ -103,15 +108,18 @@ function DirectChatItem({
|
|||||||
convo,
|
convo,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const profile = useProfileShadow(convo.primaryMember)
|
const profile = useProfileShadow(convo.primaryMember)
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
@@ -129,7 +137,7 @@ function DirectChatItem({
|
|||||||
avatar={
|
avatar={
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
profile={profile}
|
profile={profile}
|
||||||
size={52}
|
size={isWithinSplitView ? 48 : 52}
|
||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -145,15 +153,18 @@ function DirectChatItem({
|
|||||||
: l`This conversation is with a deleted or a deactivated account. Press for options`
|
: l`This conversation is with a deleted or a deactivated account. Press for options`
|
||||||
}
|
}
|
||||||
showMenu={showMenu}
|
showMenu={showMenu}
|
||||||
|
selected={selected}
|
||||||
isDeletedAccount={isDeletedAccount}
|
isDeletedAccount={isDeletedAccount}
|
||||||
isBlockedAccount={moderation.blocked}
|
isBlockedAccount={moderation.blocked}
|
||||||
showProfileBadges
|
showProfileBadges
|
||||||
postAlerts={
|
postAlerts={
|
||||||
|
isWithinSplitView ? null : (
|
||||||
<PostAlerts
|
<PostAlerts
|
||||||
modui={moderation.ui('contentList')}
|
modui={moderation.ui('contentList')}
|
||||||
size="lg"
|
size="sm"
|
||||||
style={[a.pt_xs]}
|
style={[a.pb_2xs, a.max_w_full, a.overflow_hidden]}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}>
|
}>
|
||||||
{children}
|
{children}
|
||||||
</BaseChatItem>
|
</BaseChatItem>
|
||||||
@@ -164,15 +175,18 @@ function GroupChatItem({
|
|||||||
convo,
|
convo,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const groupOwner = useMaybeProfileShadow(convo.primaryMember)
|
const groupOwner = useMaybeProfileShadow(convo.primaryMember)
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -185,7 +199,12 @@ function GroupChatItem({
|
|||||||
return (
|
return (
|
||||||
<BaseChatItem
|
<BaseChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
avatar={<AvatarBubbles profiles={convo.members} size={52} />}
|
avatar={
|
||||||
|
<AvatarBubbles
|
||||||
|
profiles={convo.members}
|
||||||
|
size={isWithinSplitView ? 48 : 52}
|
||||||
|
/>
|
||||||
|
}
|
||||||
title={chatName}
|
title={chatName}
|
||||||
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
||||||
primaryProfile={groupOwner}
|
primaryProfile={groupOwner}
|
||||||
@@ -193,6 +212,7 @@ function GroupChatItem({
|
|||||||
isBlockedAccount={false}
|
isBlockedAccount={false}
|
||||||
isDeletedAccount={false}
|
isDeletedAccount={false}
|
||||||
showProfileBadges={false}
|
showProfileBadges={false}
|
||||||
|
selected={selected}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}>
|
||||||
{children}
|
{children}
|
||||||
</BaseChatItem>
|
</BaseChatItem>
|
||||||
@@ -210,6 +230,7 @@ function BaseChatItem({
|
|||||||
primaryProfile,
|
primaryProfile,
|
||||||
primaryProfileModeration,
|
primaryProfileModeration,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
showProfileBadges,
|
showProfileBadges,
|
||||||
postAlerts,
|
postAlerts,
|
||||||
children,
|
children,
|
||||||
@@ -224,6 +245,7 @@ function BaseChatItem({
|
|||||||
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
||||||
primaryProfileModeration?: ModerationDecision
|
primaryProfileModeration?: ModerationDecision
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
showProfileBadges: boolean
|
showProfileBadges: boolean
|
||||||
postAlerts?: React.ReactNode
|
postAlerts?: React.ReactNode
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -236,6 +258,7 @@ function BaseChatItem({
|
|||||||
const leaveConvoControl = useDialogControl()
|
const leaveConvoControl = useDialogControl()
|
||||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -407,6 +430,8 @@ function BaseChatItem({
|
|||||||
leftFirst: deleteAction,
|
leftFirst: deleteAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarSize = isWithinSplitView ? 48 : 52
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatListItemPortal.Provider>
|
<ChatListItemPortal.Provider>
|
||||||
<GestureActionView actions={actions}>
|
<GestureActionView actions={actions}>
|
||||||
@@ -416,7 +441,7 @@ function BaseChatItem({
|
|||||||
// @ts-expect-error web only
|
// @ts-expect-error web only
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onMouseLeave}
|
onBlur={onMouseLeave}
|
||||||
style={[a.relative, t.atoms.bg]}>
|
style={[a.relative, t.atoms.bg, isWithinSplitView && a.mx_sm]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.z_10,
|
a.z_10,
|
||||||
@@ -457,13 +482,15 @@ function BaseChatItem({
|
|||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.py_md,
|
a.py_md,
|
||||||
a.gap_md,
|
a.gap_md,
|
||||||
|
isWithinSplitView && a.rounded_sm,
|
||||||
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
|
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
|
||||||
|
selected && t.atoms.bg_contrast_50,
|
||||||
]}>
|
]}>
|
||||||
{/* Avatar goes here */}
|
{/* Avatar goes here */}
|
||||||
<View style={{width: 52, height: 52}} />
|
<View style={{width: avatarSize, height: avatarSize}} />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}>
|
style={[a.flex_1, a.justify_center, web({paddingRight: 40})]}>
|
||||||
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
|
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
|
||||||
<View style={[a.flex_shrink]}>
|
<View style={[a.flex_shrink]}>
|
||||||
<Text
|
<Text
|
||||||
@@ -483,7 +510,7 @@ function BaseChatItem({
|
|||||||
{showProfileBadges && primaryProfile && (
|
{showProfileBadges && primaryProfile && (
|
||||||
<ProfileBadges
|
<ProfileBadges
|
||||||
profile={primaryProfile}
|
profile={primaryProfile}
|
||||||
size="md"
|
size="sm"
|
||||||
style={[a.pl_xs, a.self_center]}
|
style={[a.pl_xs, a.self_center]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -526,15 +553,14 @@ function BaseChatItem({
|
|||||||
{subtitle && (
|
{subtitle && (
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[
|
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}
|
||||||
a.text_sm,
|
emoji>
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.pb_xs,
|
|
||||||
]}>
|
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{postAlerts}
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_center]}>
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
{LastMessageIcon && (
|
{LastMessageIcon && (
|
||||||
<LastMessageIcon
|
<LastMessageIcon
|
||||||
@@ -557,8 +583,6 @@ function BaseChatItem({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{postAlerts}
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,29 @@ import {useLingui} from '@lingui/react/macro'
|
|||||||
|
|
||||||
import {UNREAD_LIMIT} from '#/state/queries/messages/list-conversations'
|
import {UNREAD_LIMIT} from '#/state/queries/messages/list-conversations'
|
||||||
import {atoms as a} from '#/alf'
|
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()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
if (count < 1) return null
|
const unread = count > 0
|
||||||
|
|
||||||
const label =
|
const label = !unread
|
||||||
count >= UNREAD_LIMIT && more
|
? l({
|
||||||
|
message: `Requests`,
|
||||||
|
comment: 'Incoming message requests',
|
||||||
|
})
|
||||||
|
: count >= UNREAD_LIMIT && more
|
||||||
? l({
|
? l({
|
||||||
message: `${count}+ requests`,
|
message: `${count}+ requests`,
|
||||||
comment: 'Displayed when the number of requests is greater than 20',
|
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',
|
other: '# requests',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'ghost': {
|
||||||
return (
|
return (
|
||||||
<InlineLinkText
|
<Link
|
||||||
label={label}
|
label={label}
|
||||||
to="/messages/inbox"
|
to="/messages/inbox"
|
||||||
style={[a.text_md, a.font_medium]}>
|
size="small"
|
||||||
{label}
|
variant={unread ? 'solid' : 'ghost'}
|
||||||
</InlineLinkText>
|
color={unread ? 'primary_subtle' : 'secondary'}
|
||||||
|
shape={unread ? 'default' : 'round'}
|
||||||
|
style={[a.justify_center, unread && [a.gap_sm, a.pl_lg, a.pr_md]]}>
|
||||||
|
<ButtonIcon icon={InboxIcon} size="lg" />
|
||||||
|
{unread && (
|
||||||
|
<ButtonText style={[a.text_md, a.font_bold]}>
|
||||||
|
{count >= UNREAD_LIMIT && more
|
||||||
|
? l({
|
||||||
|
message: `${count}+`,
|
||||||
|
comment:
|
||||||
|
'Displayed when the number of requests is greater than 20',
|
||||||
|
})
|
||||||
|
: count}
|
||||||
|
</ButtonText>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case 'solid': {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
label={label}
|
||||||
|
to="/messages/inbox"
|
||||||
|
color={unread ? 'primary_subtle' : 'secondary'}
|
||||||
|
size="small">
|
||||||
|
<ButtonIcon icon={InboxIcon} />
|
||||||
|
<ButtonText>{label}</ButtonText>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -321,8 +321,8 @@ export function ComposerContainer({children}: {children: React.ReactNode}) {
|
|||||||
a.pl_lg,
|
a.pl_lg,
|
||||||
a.pb_lg,
|
a.pb_lg,
|
||||||
// prevent overlap with the scrollbar, which looks ugly
|
// prevent overlap with the scrollbar, which looks ugly
|
||||||
a.pr_xs, // xs + md = lg
|
a.pr_sm, // sm + sm = lg
|
||||||
{width: `calc(100% - ${tokens.space.md}px)` as '100%'},
|
{width: `calc(100% - ${tokens.space.sm}px)` as '100%'},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
key={t.name} // android does not update when you change the colors. sigh.
|
key={t.name} // android does not update when you change the colors. sigh.
|
||||||
|
|||||||
@@ -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 <MessagesSplitViewLayout {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.mx_auto,
|
||||||
|
{maxWidth: containerWidth},
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{translateX: offset},
|
||||||
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{isFocused && <LockScroll />}
|
||||||
|
<SplitViewProvider side="left">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border_l,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{width: containerWidth - centerColumnWidth},
|
||||||
|
]}>
|
||||||
|
<ChatListHeader newChatControl={newChatControl} />
|
||||||
|
<ChatList
|
||||||
|
newChatControl={newChatControl}
|
||||||
|
selectedChat={selectedChat}
|
||||||
|
/>
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</View>
|
||||||
|
</SplitViewProvider>
|
||||||
|
<SplitViewProvider side="right">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border_x,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{width: centerColumnWidth},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</SplitViewProvider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 <SplitViewContext value={value}>{children}</SplitViewContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useIsWithinSplitView = () => useContext(SplitViewContext)
|
||||||
@@ -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 {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
||||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck'
|
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck'
|
||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
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 {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||||
@@ -40,6 +39,8 @@ import {
|
|||||||
ProfileFeedHeader,
|
ProfileFeedHeader,
|
||||||
ProfileFeedHeaderSkeleton,
|
ProfileFeedHeaderSkeleton,
|
||||||
} from '#/screens/Profile/components/ProfileFeedHeader'
|
} 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 {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
@@ -134,6 +135,7 @@ export function ProfileFeedScreenInner({
|
|||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const isScreenFocused = useIsFocused()
|
const isScreenFocused = useIsFocused()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
useSetTitle(feedInfo?.displayName)
|
useSetTitle(feedInfo?.displayName)
|
||||||
|
|
||||||
@@ -210,13 +212,7 @@ export function ProfileFeedScreenInner({
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={() => openComposer({logContext: 'Fab'})}
|
onPress={() => openComposer({logContext: 'Fab'})}
|
||||||
icon={
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
<ComposeIcon2
|
|
||||||
strokeWidth={1.5}
|
|
||||||
size={29}
|
|
||||||
style={{color: 'white'}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -38,6 +37,7 @@ import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
|
|||||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog'
|
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 * as Layout from '#/components/Layout'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Hider from '#/components/moderation/Hider'
|
import * as Hider from '#/components/moderation/Hider'
|
||||||
@@ -232,13 +232,7 @@ function ProfileListScreenLoaded({
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={() => openComposer({logContext: 'Fab'})}
|
onPress={() => openComposer({logContext: 'Fab'})}
|
||||||
icon={
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
<ComposeIcon2
|
|
||||||
strokeWidth={1.5}
|
|
||||||
size={29}
|
|
||||||
style={{color: 'white'}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
@@ -281,13 +275,7 @@ function ProfileListScreenLoaded({
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={() => openComposer({logContext: 'Fab'})}
|
onPress={() => openComposer({logContext: 'Fab'})}
|
||||||
icon={
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
<ComposeIcon2
|
|
||||||
strokeWidth={1.5}
|
|
||||||
size={29}
|
|
||||||
style={{color: 'white'}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -197,11 +197,12 @@ export function ItemIcon({
|
|||||||
'2xs': 8,
|
'2xs': 8,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 16,
|
sm: 16,
|
||||||
md: 20,
|
md: 18,
|
||||||
lg: 24,
|
lg: 24,
|
||||||
xl: 28,
|
xl: 28,
|
||||||
'2xl': 32,
|
'2xl': 32,
|
||||||
'3xl': 40,
|
'3xl': 40,
|
||||||
|
'4xl': 48,
|
||||||
}[size]
|
}[size]
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
||||||
import {type AllNavigatorParams} from '#/lib/routes/types'
|
import {type AllNavigatorParams} from '#/lib/routes/types'
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {useSetHomeBadge} from '#/state/home-badge'
|
import {useSetHomeBadge} from '#/state/home-badge'
|
||||||
@@ -30,14 +28,16 @@ import {
|
|||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {useSession} from '#/state/session'
|
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 {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
|
||||||
|
import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
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
|
const POLL_FREQ = 60e3 // 60sec
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ export function FeedPage({
|
|||||||
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
|
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
|
||||||
return IS_NATIVE && _isVideoFeed
|
return IS_NATIVE && _isVideoFeed
|
||||||
}, [feedInfo])
|
}, [feedInfo])
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPageFocused) {
|
if (isPageFocused) {
|
||||||
@@ -173,7 +174,7 @@ export function FeedPage({
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg({message: `New post`, context: 'action'}))}
|
accessibilityLabel={_(msg({message: `New post`, context: 'action'}))}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -2,45 +2,53 @@ import {isValidElement} from 'react'
|
|||||||
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
||||||
import {View} 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 {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 {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export type EmptyStateButtonProps = Omit<ButtonProps, 'children' | 'label'> & {
|
export type EmptyStateButtonProps = Omit<ButtonProps, 'children' | 'label'> & {
|
||||||
label: string
|
label: string
|
||||||
text: string
|
text: string
|
||||||
|
icon?: React.ComponentProps<typeof ButtonIcon>['icon']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmptyState({
|
export function EmptyState({
|
||||||
testID,
|
testID,
|
||||||
icon,
|
icon,
|
||||||
iconSize = '3xl',
|
iconSize = '3xl',
|
||||||
|
iconColor,
|
||||||
message,
|
message,
|
||||||
style,
|
style,
|
||||||
textStyle,
|
textStyle,
|
||||||
button,
|
button,
|
||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
icon?: React.ComponentType<any> | React.ReactElement
|
icon?: React.ComponentType<any> | React.ReactElement | null
|
||||||
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
|
||||||
|
iconColor?: string
|
||||||
message: string
|
message: string
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
textStyle?: StyleProp<TextStyle>
|
textStyle?: StyleProp<TextStyle>
|
||||||
button?: EmptyStateButtonProps
|
button?: EmptyStateButtonProps
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile, gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
const placeholderIcon = (
|
const placeholderIcon = (
|
||||||
<EditIcon size="2xl" fill={t.atoms.text_contrast_medium.color} />
|
<EditIcon size="2xl" fill={t.atoms.text_contrast_medium.color} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
|
if (icon === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
return placeholderIcon
|
return placeholderIcon
|
||||||
}
|
}
|
||||||
@@ -57,8 +65,7 @@ export function EmptyState({
|
|||||||
return (
|
return (
|
||||||
<IconComponent
|
<IconComponent
|
||||||
size={iconSize}
|
size={iconSize}
|
||||||
fill={t.atoms.text_contrast_medium.color}
|
style={{color: iconColor ?? t.atoms.text_contrast_low.color}}
|
||||||
style={{color: t.atoms.text_contrast_low.color}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,16 +86,14 @@ export function EmptyState({
|
|||||||
{height: 64, width: 64},
|
{height: 64, width: 64},
|
||||||
isValidElement(icon)
|
isValidElement(icon)
|
||||||
? a.bg_transparent
|
? a.bg_transparent
|
||||||
: [isTabletOrDesktop && {marginTop: 50}],
|
: [gtTablet && {marginTop: 50}],
|
||||||
]}>
|
]}>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
{
|
t.atoms.text_contrast_high,
|
||||||
color: pal.colors.textLight,
|
{maxWidth: gtMobile ? '40%' : '60%'},
|
||||||
maxWidth: gtMobile ? '40%' : '60%',
|
|
||||||
},
|
|
||||||
a.pt_xs,
|
a.pt_xs,
|
||||||
a.font_medium,
|
a.font_medium,
|
||||||
a.text_md,
|
a.text_md,
|
||||||
@@ -101,8 +106,9 @@ export function EmptyState({
|
|||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
{button && (
|
{button && (
|
||||||
<View style={[a.flex_shrink, a.mt_xl, a.self_center, a.mb_5xl]}>
|
<View style={[a.flex_shrink, a.mt_md, a.self_center, a.mb_5xl]}>
|
||||||
<Button {...button}>
|
<Button {...button}>
|
||||||
|
{button.icon && <ButtonIcon icon={button.icon} />}
|
||||||
<ButtonText>{button.text}</ButtonText>
|
<ButtonText>{button.text}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import {batchedUpdates} from '#/lib/batchedUpdates'
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {addStyle} from '#/lib/styles'
|
import {addStyle} from '#/lib/styles'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
|
import {useTheme, web} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
export type ListMethods = {
|
export type ListMethods = {
|
||||||
@@ -60,7 +62,7 @@ function ListImpl<ItemT>(
|
|||||||
ListHeaderComponent,
|
ListHeaderComponent,
|
||||||
ListFooterComponent,
|
ListFooterComponent,
|
||||||
ListEmptyComponent,
|
ListEmptyComponent,
|
||||||
disableFullWindowScroll,
|
disableFullWindowScroll: disableFullWindowScrollProp,
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
data,
|
data,
|
||||||
desktopFixedHeight,
|
desktopFixedHeight,
|
||||||
@@ -83,6 +85,12 @@ function ListImpl<ItemT>(
|
|||||||
ref: React.Ref<ListMethods>,
|
ref: React.Ref<ListMethods>,
|
||||||
) {
|
) {
|
||||||
const contextScrollHandlers = useScrollHandlers()
|
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
|
const isEmpty = !data || data.length === 0
|
||||||
|
|
||||||
@@ -329,11 +337,15 @@ function ListImpl<ItemT>(
|
|||||||
<View
|
<View
|
||||||
{...props}
|
{...props}
|
||||||
style={[
|
style={[
|
||||||
|
isWithinSplitView &&
|
||||||
|
web({
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||||
|
}),
|
||||||
style,
|
style,
|
||||||
disableFullWindowScroll && {
|
disableFullWindowScroll && {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
// @ts-expect-error web only
|
overflowY: 'scroll',
|
||||||
'overflow-y': 'scroll',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
ref={nativeRef as unknown as React.RefObject<View>}>
|
ref={nativeRef as unknown as React.RefObject<View>}>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useBreakpoints} from '#/alf'
|
||||||
import {FABInner, type FABProps} from './FABInner'
|
import {FABInner, type FABProps} from './FABInner'
|
||||||
|
|
||||||
export const FAB = (_opts: FABProps) => {
|
export const FAB = (props: FABProps) => {
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
if (!isDesktop) {
|
if (!gtMobile) {
|
||||||
return <FABInner {..._opts} />
|
return <FABInner {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <View />
|
return <View />
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import debounce from 'lodash.debounce'
|
|||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -37,6 +36,7 @@ import * as FeedCard from '#/components/FeedCard'
|
|||||||
import {SearchInput} from '#/components/forms/SearchInput'
|
import {SearchInput} from '#/components/forms/SearchInput'
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
import {IconCircle} from '#/components/IconCircle'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
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 {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||||
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
|
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
|
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
|
||||||
@@ -104,6 +104,7 @@ type FlatlistSlice =
|
|||||||
|
|
||||||
export function FeedsScreen(_props: Props) {
|
export function FeedsScreen(_props: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const t = useTheme()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
@@ -539,7 +540,7 @@ export function FeedsScreen(_props: Props) {
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {
|
import {
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
type NotificationsTabNavigatorParams,
|
type NotificationsTabNavigatorParams,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {emitSoftReset, listenSoftReset} from '#/state/events'
|
import {emitSoftReset, listenSoftReset} from '#/state/events'
|
||||||
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
|
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 {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {ButtonIcon} from '#/components/Button'
|
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 {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {InlineLinkText, Link} from '#/components/Link'
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
@@ -50,6 +49,7 @@ type Props = NativeStackScreenProps<
|
|||||||
>
|
>
|
||||||
export function NotificationsScreen({}: Props) {
|
export function NotificationsScreen({}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const unreadNotifs = useUnreadNotifications()
|
const unreadNotifs = useUnreadNotifications()
|
||||||
const hasNew = !!unreadNotifs
|
const hasNew = !!unreadNotifs
|
||||||
@@ -161,7 +161,7 @@ export function NotificationsScreen({}: Props) {
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={() => openComposer({logContext: 'Fab'})}
|
onPress={() => openComposer({logContext: 'Fab'})}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -25,7 +24,7 @@ import {
|
|||||||
import {combinedDisplayName} from '#/lib/strings/display-names'
|
import {combinedDisplayName} from '#/lib/strings/display-names'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
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 {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
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 {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
||||||
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||||
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
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 {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 {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2'
|
||||||
import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
import {Message_Stroke1_Corner0_Rounded_Filled as MessageIcon} from '#/components/icons/Message'
|
import {Message_Stroke1_Corner0_Rounded_Filled as MessageIcon} from '#/components/icons/Message'
|
||||||
@@ -172,6 +172,7 @@ function ProfileScreenLoaded({
|
|||||||
hideBackButton: boolean
|
hideBackButton: boolean
|
||||||
isPlaceholderProfile: boolean
|
isPlaceholderProfile: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
@@ -587,7 +588,7 @@ function ProfileScreenLoaded({
|
|||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
testID="composeFAB"
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New post`)}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ import {DesktopRightNav} from './desktop/RightNav'
|
|||||||
// Older screens are unmounted to prevent memory growth during long sessions.
|
// Older screens are unmounted to prevent memory growth during long sessions.
|
||||||
const WEB_MAX_CACHED_SCREENS = 5
|
const WEB_MAX_CACHED_SCREENS = 5
|
||||||
|
|
||||||
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
export type NativeStackNavigationOptionsWithAuth =
|
||||||
|
NativeStackNavigationOptions & {
|
||||||
requireAuth?: boolean
|
requireAuth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +201,11 @@ function NativeStackNavigator({
|
|||||||
</View>
|
</View>
|
||||||
{IS_WEB && (
|
{IS_WEB && (
|
||||||
<>
|
<>
|
||||||
{showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />}
|
{showBottomBar ? (
|
||||||
|
<BottomBarWeb />
|
||||||
|
) : (
|
||||||
|
<DesktopLeftNav routeName={activeRoute.name} />
|
||||||
|
)}
|
||||||
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import {useNavigation, useNavigationState} from '@react-navigation/native'
|
|||||||
|
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
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 {getCurrentRoute, isTab} from '#/lib/routes/helpers'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +28,14 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|||||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {type DialogControlProps} from '#/components/Dialog'
|
import {type DialogControlProps} from '#/components/Dialog'
|
||||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
|
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
|
||||||
@@ -44,7 +49,7 @@ import {
|
|||||||
BulletList_Stroke2_Corner0_Rounded as List,
|
BulletList_Stroke2_Corner0_Rounded as List,
|
||||||
} from '#/components/icons/BulletList'
|
} from '#/components/icons/BulletList'
|
||||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
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 {
|
import {
|
||||||
Hashtag_Filled_Corner0_Rounded as HashtagFilled,
|
Hashtag_Filled_Corner0_Rounded as HashtagFilled,
|
||||||
Hashtag_Stroke2_Corner0_Rounded as Hashtag,
|
Hashtag_Stroke2_Corner0_Rounded as Hashtag,
|
||||||
@@ -76,12 +81,12 @@ import * as Prompt from '#/components/Prompt'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {useActorStatus} from '#/features/liveNow'
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
import {router} from '#/routes'
|
||||||
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {router} from '../../../routes'
|
|
||||||
|
|
||||||
const NAV_ICON_WIDTH = 28
|
const NAV_ICON_WIDTH = 28
|
||||||
|
|
||||||
function ProfileCard() {
|
function ProfileCard({minimal}: {minimal: boolean}) {
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const {logoutEveryAccount} = useSessionApi()
|
const {logoutEveryAccount} = useSessionApi()
|
||||||
const {isLoading, data} = useProfilesQuery({
|
const {isLoading, data} = useProfilesQuery({
|
||||||
@@ -89,7 +94,6 @@ function ProfileCard() {
|
|||||||
})
|
})
|
||||||
const profiles = data?.profiles
|
const profiles = data?.profiles
|
||||||
const signOutPromptControl = Prompt.usePromptControl()
|
const signOutPromptControl = Prompt.usePromptControl()
|
||||||
const {leftNavMinimal} = useLayoutBreakpoints()
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -106,7 +110,7 @@ function ProfileCard() {
|
|||||||
const {isActive: live} = useActorStatus(profile)
|
const {isActive: live} = useActorStatus(profile)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.my_md, !leftNavMinimal && [a.w_full, a.align_start]]}>
|
<View style={[a.my_md, !minimal && [a.w_full, a.align_start]]}>
|
||||||
{!isLoading && profile ? (
|
{!isLoading && profile ? (
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Switch accounts`)}>
|
<Menu.Trigger label={_(msg`Switch accounts`)}>
|
||||||
@@ -125,7 +129,7 @@ function ProfileCard() {
|
|||||||
a.align_center,
|
a.align_center,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
{gap: 6},
|
{gap: 6},
|
||||||
!leftNavMinimal && [a.pl_lg, a.pr_md],
|
!minimal && [a.pl_lg, a.pr_md],
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -138,8 +142,8 @@ function ProfileCard() {
|
|||||||
a.z_10,
|
a.z_10,
|
||||||
active && {
|
active && {
|
||||||
transform: [
|
transform: [
|
||||||
{scale: !leftNavMinimal ? 2 / 3 : 0.8},
|
{scale: !minimal ? 2 / 3 : 0.8},
|
||||||
{translateX: !leftNavMinimal ? -22 : 0},
|
{translateX: !minimal ? -22 : 0},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
@@ -150,7 +154,7 @@ function ProfileCard() {
|
|||||||
live={live}
|
live={live}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{!leftNavMinimal && (
|
{!minimal && (
|
||||||
<>
|
<>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -203,7 +207,7 @@ function ProfileCard() {
|
|||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
style={[{borderRadius: size}, !leftNavMinimal && a.ml_lg]}
|
style={[{borderRadius: size}, !minimal && a.ml_lg]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
@@ -291,12 +295,18 @@ function SwitcherMenuProfileLink() {
|
|||||||
}
|
}
|
||||||
return getCurrentRoute(state)
|
return getCurrentRoute(state)
|
||||||
})
|
})
|
||||||
let isCurrent =
|
const isCurrent = useMemo(() => {
|
||||||
currentRouteInfo.name === 'Profile'
|
if (currentRouteInfo.name === 'Profile') {
|
||||||
? isTab(currentRouteInfo.name, pathName) &&
|
return (
|
||||||
|
isTab(currentRouteInfo.name, pathName) &&
|
||||||
(currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
|
(currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
|
||||||
currentAccount?.handle
|
currentAccount?.handle
|
||||||
: isTab(currentRouteInfo.name, pathName)
|
)
|
||||||
|
} else {
|
||||||
|
return isTab(currentRouteInfo.name, pathName)
|
||||||
|
}
|
||||||
|
}, [currentAccount?.handle, currentRouteInfo, pathName])
|
||||||
|
|
||||||
const onProfilePress = useCallback(
|
const onProfilePress = useCallback(
|
||||||
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||||
@@ -374,12 +384,21 @@ interface NavItemProps {
|
|||||||
icon: JSX.Element
|
icon: JSX.Element
|
||||||
iconFilled: JSX.Element
|
iconFilled: JSX.Element
|
||||||
label: string
|
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 t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {leftNavMinimal} = useLayoutBreakpoints()
|
|
||||||
const [pathName] = useMemo(() => router.matchPath(href), [href])
|
const [pathName] = useMemo(() => router.matchPath(href), [href])
|
||||||
const currentRouteInfo = useNavigationState(state => {
|
const currentRouteInfo = useNavigationState(state => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -393,6 +412,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
(currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
|
(currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
|
||||||
currentAccount?.handle
|
currentAccount?.handle
|
||||||
: isTab(currentRouteInfo.name, pathName)
|
: isTab(currentRouteInfo.name, pathName)
|
||||||
|
const isRelated = currentRouteInfo.name.startsWith(pathName)
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const onPressWrapped = useCallback(
|
const onPressWrapped = useCallback(
|
||||||
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
@@ -417,7 +437,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.p_md,
|
a.p_md,
|
||||||
a.rounded_sm,
|
a.rounded_full,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.outline_inset_1,
|
a.outline_inset_1,
|
||||||
a.transition_color,
|
a.transition_color,
|
||||||
@@ -438,12 +458,8 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
},
|
},
|
||||||
leftNavMinimal && {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
{isCurrent ? iconFilled : icon}
|
{isCurrent || isRelated ? iconFilled : icon}
|
||||||
{typeof count === 'string' && count ? (
|
{typeof count === 'string' && count ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -479,12 +495,6 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
paddingVertical: 1,
|
paddingVertical: 1,
|
||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
},
|
},
|
||||||
leftNavMinimal && [
|
|
||||||
{
|
|
||||||
top: '10%',
|
|
||||||
left: count.length === 1 ? 20 : 16,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]}>
|
]}>
|
||||||
{count}
|
{count}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -502,15 +512,11 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
right: -2,
|
right: -2,
|
||||||
top: -4,
|
top: -4,
|
||||||
},
|
},
|
||||||
leftNavMinimal && {
|
|
||||||
right: 4,
|
|
||||||
top: 2,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{!leftNavMinimal && (
|
{!minimal && (
|
||||||
<Text style={[a.text_xl, isCurrent ? a.font_bold : a.font_normal]}>
|
<Text style={[a.text_xl, isCurrent ? a.font_bold : a.font_normal]}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -519,12 +525,11 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposeBtn() {
|
function ComposeBtn({minimal}: {minimal: boolean}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {getState} = useNavigation()
|
const {getState} = useNavigation()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {leftNavMinimal} = useLayoutBreakpoints()
|
|
||||||
const [isFetchingHandle, setIsFetchingHandle] = useState(false)
|
const [isFetchingHandle, setIsFetchingHandle] = useState(false)
|
||||||
const fetchHandle = useFetchHandle()
|
const fetchHandle = useFetchHandle()
|
||||||
|
|
||||||
@@ -564,31 +569,28 @@ function ComposeBtn() {
|
|||||||
const onPressCompose = async () =>
|
const onPressCompose = async () =>
|
||||||
openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
|
openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
|
||||||
|
|
||||||
if (leftNavMinimal) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.pl_md, a.pt_xl]}>
|
<View style={minimal ? [a.px_sm, a.pt_lg] : [a.flex_row, a.pl_md, a.pt_lg]}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isFetchingHandle}
|
disabled={isFetchingHandle}
|
||||||
label={_(msg`Compose new post`)}
|
label={_(msg`Compose new post`)}
|
||||||
onPress={() => void onPressCompose()}
|
onPress={() => void onPressCompose()}
|
||||||
size="large"
|
size="large"
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
style={[a.rounded_full]}>
|
style={[a.rounded_full, minimal && {width: 48, height: 48}]}>
|
||||||
<ButtonIcon icon={EditBig} position="left" />
|
<ButtonIcon icon={EditBigIcon} size={minimal ? 'lg' : 'sm'} />
|
||||||
|
{!minimal && (
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans context="action">New post</Trans>
|
<Trans context="action">New post</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatNavItem() {
|
function ChatNavItem({minimal}: {minimal: boolean}) {
|
||||||
const pal = usePalette('default')
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const numUnreadMessages = useUnreadMessageCount()
|
const numUnreadMessages = useUnreadMessageCount()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
@@ -596,14 +598,19 @@ function ChatNavItem() {
|
|||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/messages"
|
href="/messages"
|
||||||
|
minimal={minimal}
|
||||||
count={aa.flags.chatDisabled ? undefined : numUnreadMessages.numUnread}
|
count={aa.flags.chatDisabled ? undefined : numUnreadMessages.numUnread}
|
||||||
hasNew={aa.flags.chatDisabled ? false : numUnreadMessages.hasNew}
|
hasNew={aa.flags.chatDisabled ? false : numUnreadMessages.hasNew}
|
||||||
icon={
|
icon={
|
||||||
<Message style={pal.text} aria-hidden={true} width={NAV_ICON_WIDTH} />
|
<Message
|
||||||
|
style={t.atoms.text}
|
||||||
|
aria-hidden={true}
|
||||||
|
width={NAV_ICON_WIDTH}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<MessageFilled
|
<MessageFilled
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
@@ -613,15 +620,24 @@ function ChatNavItem() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopLeftNav() {
|
export function DesktopLeftNav({routeName}: {routeName: string}) {
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const t = useTheme()
|
||||||
const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints()
|
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()
|
const numUnreadNotifications = useUnreadNotifications()
|
||||||
|
|
||||||
if (!hasSession && !isDesktop) {
|
const leftNavMinimal = isMessagesRelatedScreen || leftNavMinimalBreakpoint
|
||||||
|
|
||||||
|
if (!hasSession && !gtMobile) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,7 +653,11 @@ export function DesktopLeftNav() {
|
|||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateX:
|
translateX:
|
||||||
-300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0),
|
-300 +
|
||||||
|
(centerColumnOffset ? CENTER_COLUMN_OFFSET : 0) +
|
||||||
|
(isMessagesRelatedScreen && !leftNavMinimalBreakpoint
|
||||||
|
? -153
|
||||||
|
: 0),
|
||||||
},
|
},
|
||||||
{translateX: '-100%'},
|
{translateX: '-100%'},
|
||||||
...a.scrollbar_offset.transform,
|
...a.scrollbar_offset.transform,
|
||||||
@@ -645,7 +665,7 @@ export function DesktopLeftNav() {
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{hasSession ? (
|
{hasSession ? (
|
||||||
<ProfileCard />
|
<ProfileCard minimal={leftNavMinimal} />
|
||||||
) : !leftNavMinimal ? (
|
) : !leftNavMinimal ? (
|
||||||
<View style={[a.pt_xl]}>
|
<View style={[a.pt_xl]}>
|
||||||
<NavSignupCard />
|
<NavSignupCard />
|
||||||
@@ -656,34 +676,36 @@ export function DesktopLeftNav() {
|
|||||||
<>
|
<>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/"
|
href="/"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<Home
|
<Home
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<HomeFilled
|
<HomeFilled
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={_(msg`Home`)}
|
label={_(msg`Home`)}
|
||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/search"
|
href="/search"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<MagnifyingGlass
|
<MagnifyingGlass
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<MagnifyingGlassFilled
|
<MagnifyingGlassFilled
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
@@ -692,36 +714,38 @@ export function DesktopLeftNav() {
|
|||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/notifications"
|
href="/notifications"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
count={numUnreadNotifications}
|
count={numUnreadNotifications}
|
||||||
icon={
|
icon={
|
||||||
<Bell
|
<Bell
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<BellFilled
|
<BellFilled
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={_(msg`Notifications`)}
|
label={_(msg`Notifications`)}
|
||||||
/>
|
/>
|
||||||
<ChatNavItem />
|
<ChatNavItem minimal={leftNavMinimal} />
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/feeds"
|
href="/feeds"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<Hashtag
|
<Hashtag
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<HashtagFilled
|
<HashtagFilled
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
@@ -730,16 +754,17 @@ export function DesktopLeftNav() {
|
|||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/lists"
|
href="/lists"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<List
|
<List
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<ListFilled
|
<ListFilled
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
@@ -748,16 +773,17 @@ export function DesktopLeftNav() {
|
|||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/saved"
|
href="/saved"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<Bookmark
|
<Bookmark
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<BookmarkFilled
|
<BookmarkFilled
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
/>
|
/>
|
||||||
@@ -771,42 +797,44 @@ export function DesktopLeftNav() {
|
|||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href={currentAccount ? makeProfileLink(currentAccount) : '/'}
|
href={currentAccount ? makeProfileLink(currentAccount) : '/'}
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<UserCircle
|
<UserCircle
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<UserCircleFilled
|
<UserCircleFilled
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={_(msg`Profile`)}
|
label={_(msg`Profile`)}
|
||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/settings"
|
href="/settings"
|
||||||
|
minimal={leftNavMinimal}
|
||||||
icon={
|
icon={
|
||||||
<Settings
|
<Settings
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<SettingsFilled
|
<SettingsFilled
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
width={NAV_ICON_WIDTH}
|
width={NAV_ICON_WIDTH}
|
||||||
style={pal.text}
|
style={t.atoms.text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={_(msg`Settings`)}
|
label={_(msg`Settings`)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ComposeBtn />
|
<ComposeBtn minimal={leftNavMinimal} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -824,15 +852,12 @@ const styles = StyleSheet.create({
|
|||||||
// @ts-expect-error web only
|
// @ts-expect-error web only
|
||||||
maxHeight: '100vh',
|
maxHeight: '100vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
},
|
},
|
||||||
leftNavWide: {
|
leftNavWide: {
|
||||||
width: 245,
|
width: 245,
|
||||||
},
|
},
|
||||||
leftNavMinimal: {
|
leftNavMinimal: {
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
paddingLeft: 0,
|
|
||||||
paddingRight: 0,
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: 86,
|
width: 86,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
const kawaii = useKawaiiMode()
|
const kawaii = useKawaiiMode()
|
||||||
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
||||||
const isSearchScreen = routeName === 'Search'
|
const isSearchScreen = routeName === 'Search'
|
||||||
|
const isMessagesRelatedScreen = routeName.startsWith('Messages')
|
||||||
const webqueryParams = useWebQueryParams()
|
const webqueryParams = useWebQueryParams()
|
||||||
const searchQuery = webqueryParams?.q
|
const searchQuery = webqueryParams?.q
|
||||||
const showExploreScreenDuplicatedContent =
|
const showExploreScreenDuplicatedContent =
|
||||||
@@ -56,7 +57,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
|
const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
|
||||||
useLayoutBreakpoints()
|
useLayoutBreakpoints()
|
||||||
|
|
||||||
if (!rightNavVisible) {
|
if (!rightNavVisible || isMessagesRelatedScreen) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user