[Chat] Split view layout for web (#10258)
This commit is contained in:
+248
-194
@@ -2,9 +2,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
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 {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||
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 {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 {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
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 {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
|
||||
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 {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import {Inbox_Stroke2_Corner2_Rounded_Large as InboxLargeIcon} from '#/components/icons/Inbox'
|
||||
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 * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
@@ -41,14 +44,16 @@ import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {ChatListItem} from './components/ChatListItem'
|
||||
import {InboxRequests} from './components/InboxRequests'
|
||||
import {useIsWithinSplitView} from './components/splitView/context'
|
||||
|
||||
type ListItem = {
|
||||
type: 'CONVERSATION'
|
||||
conversation: ChatBskyConvoDefs.ConvoView
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function renderItem({item}: {item: ListItem}) {
|
||||
return <ChatListItem convo={item.conversation} />
|
||||
return <ChatListItem convo={item.conversation} selected={item.selected} />
|
||||
}
|
||||
|
||||
function keyExtractor(item: ListItem) {
|
||||
@@ -58,19 +63,19 @@ function keyExtractor(item: ListItem) {
|
||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
||||
|
||||
export function MessagesScreen(props: Props) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const aaCopy = useAgeAssuranceCopy()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
return (
|
||||
<AgeRestrictedScreen
|
||||
screenTitle={_(msg`Chats`)}
|
||||
screenTitle={l`Chats`}
|
||||
infoText={aaCopy.chatsInfoText}
|
||||
rightHeaderSlot={
|
||||
aa.flags.chatDisabled ? null : (
|
||||
<Link
|
||||
to="/messages/settings"
|
||||
label={_(msg`Chat settings`)}
|
||||
label={l`Chat settings`}
|
||||
size="small"
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
@@ -85,10 +90,10 @@ export function MessagesScreen(props: Props) {
|
||||
}
|
||||
|
||||
export function MessagesScreenInner({navigation, route}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
const newChatControl = useDialogControl()
|
||||
const scrollElRef: ListRef = useAnimatedRef()
|
||||
const pushToConversation = route.params?.pushToConversation
|
||||
|
||||
// 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]),
|
||||
)
|
||||
|
||||
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 [isPTRing, setIsPTRing] = useState(false)
|
||||
|
||||
@@ -134,11 +203,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
||||
refetch,
|
||||
} = useListConvosQuery({status: 'accepted'})
|
||||
|
||||
const {
|
||||
data: inboxData,
|
||||
refetch: refetchInbox,
|
||||
hasNextPage: hasMoreRequests,
|
||||
} = useListConvosQuery({
|
||||
const {refetch: refetchInbox} = useListConvosQuery({
|
||||
status: 'request',
|
||||
})
|
||||
|
||||
@@ -147,16 +212,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
||||
|
||||
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(() => {
|
||||
if (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(convo => !leftConvos.includes(convo.id))
|
||||
|
||||
return [
|
||||
...conversations.map(
|
||||
convo =>
|
||||
({
|
||||
type: 'CONVERSATION',
|
||||
conversation: convo,
|
||||
}) as const,
|
||||
),
|
||||
] satisfies ListItem[]
|
||||
return conversations.map(
|
||||
convo =>
|
||||
({
|
||||
type: 'CONVERSATION',
|
||||
conversation: convo,
|
||||
selected: convo.id === selectedChat,
|
||||
}) as const,
|
||||
) satisfies ListItem[]
|
||||
}
|
||||
return []
|
||||
}, [data, leftConvos])
|
||||
}, [data, leftConvos, selectedChat])
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
@@ -196,12 +250,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const onNewChat = useCallback(
|
||||
(conversation: string) =>
|
||||
navigation.navigate('MessagesConversation', {conversation}),
|
||||
[navigation],
|
||||
)
|
||||
|
||||
const onSoftReset = useCallback(async () => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: IS_NATIVE,
|
||||
@@ -222,141 +270,140 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
||||
return listenSoftReset(() => void onSoftReset())
|
||||
}, [onSoftReset, isScreenFocused])
|
||||
|
||||
// NOTE(APiligrim)
|
||||
// Show empty state only if there are no conversations at all
|
||||
const activeConversations = conversations.filter(
|
||||
item => item.type === 'CONVERSATION',
|
||||
)
|
||||
|
||||
if (activeConversations.length === 0) {
|
||||
if (conversations.length === 0) {
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Header
|
||||
newChatControl={newChatControl}
|
||||
requestsCount={inboxAllConvos.length}
|
||||
hasMoreRequests={hasMoreRequests}
|
||||
/>
|
||||
<Layout.Center>
|
||||
{isLoading ? (
|
||||
<ChatListLoadingPlaceholder />
|
||||
) : (
|
||||
<>
|
||||
{isError ? (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<CircleInfoIcon
|
||||
width={48}
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
/>
|
||||
<Text
|
||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
||||
<Trans>Whoops!</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
{maxWidth: 360},
|
||||
]}>
|
||||
{cleanError(error) ||
|
||||
_(msg`Failed to load conversations`)}
|
||||
</Text>
|
||||
<Layout.Center style={web({minHeight: '100%'})}>
|
||||
{isLoading ? (
|
||||
<ChatListLoadingPlaceholder />
|
||||
) : (
|
||||
<>
|
||||
{isError ? (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<CircleInfoIcon
|
||||
width={48}
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
/>
|
||||
<Text
|
||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
||||
<Trans>Whoops!</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
{maxWidth: 360},
|
||||
]}>
|
||||
{cleanError(error) || l`Failed to load conversations`}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
label={_(msg`Reload conversations`)}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
onPress={() => void refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={RetryIcon} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
||||
<Text
|
||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
||||
<Trans>Nothing here</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>You have no conversations yet. Start one!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Layout.Center>
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||
<Button
|
||||
label={l`Reload conversations`}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
onPress={() => void refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={RetryIcon} />
|
||||
</Button>
|
||||
</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
|
||||
message={l`No chats yet`}
|
||||
icon={InboxLargeIcon}
|
||||
iconSize="4xl"
|
||||
textStyle={t.atoms.text}
|
||||
iconColor={t.atoms.text.color}
|
||||
button={{
|
||||
label: l`New chat`,
|
||||
text: l`New chat`,
|
||||
onPress: wrappedOpenChatControl,
|
||||
size: 'small',
|
||||
color: 'primary',
|
||||
icon: MessagePlusIcon,
|
||||
}}
|
||||
style={web([a.h_full, a.justify_center, {paddingBottom: 120}])}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Layout.Screen>
|
||||
</Layout.Center>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="messagesScreen">
|
||||
<Header
|
||||
newChatControl={newChatControl}
|
||||
requestsCount={inboxAllConvos.length}
|
||||
hasMoreRequests={hasMoreRequests}
|
||||
/>
|
||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||
<List
|
||||
ref={scrollElRef}
|
||||
data={conversations}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={() => void onRefresh()}
|
||||
onEndReached={() => void onEndReached()}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
style={{borderColor: 'transparent'}}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
desktopFixedHeight
|
||||
sideBorders={false}
|
||||
/>
|
||||
</Layout.Screen>
|
||||
<List
|
||||
ref={scrollElRef}
|
||||
data={conversations}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={() => void onRefresh()}
|
||||
onEndReached={() => void onEndReached()}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
style={{borderColor: 'transparent'}}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
desktopFixedHeight
|
||||
sideBorders={false}
|
||||
disableFullWindowScroll={isWithinSplitView}
|
||||
style={
|
||||
isWithinSplitView && [
|
||||
a.w_full,
|
||||
web({
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||
}),
|
||||
]
|
||||
}
|
||||
contentContainerStyle={isWithinSplitView && a.py_sm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({
|
||||
newChatControl,
|
||||
requestsCount,
|
||||
hasMoreRequests,
|
||||
}: {
|
||||
newChatControl: DialogControlProps
|
||||
requestsCount: number
|
||||
hasMoreRequests: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
export function Header({newChatControl}: {newChatControl: DialogControlProps}) {
|
||||
const {t: l} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const requireEmailVerification = useRequireEmailVerification()
|
||||
const leftConvos = useLeftConvos()
|
||||
|
||||
const {data: 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(() => {
|
||||
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 (
|
||||
<Layout.Header.Outer>
|
||||
{gtMobile ? (
|
||||
<>
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.Content align="left">
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Chats</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{requestsLink}
|
||||
{settingsLink}
|
||||
<InboxRequests
|
||||
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
|
||||
label={_(msg`New chat`)}
|
||||
label={l`New chat`}
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="solid"
|
||||
shape="round"
|
||||
onPress={wrappedOpenChatControl}>
|
||||
<ButtonIcon icon={PlusIcon} position="left" />
|
||||
<ButtonText>
|
||||
<Trans>New chat</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={NewChatIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
@@ -420,8 +459,23 @@ function Header({
|
||||
<Trans>Chats</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
{requestsLink}
|
||||
<Layout.Header.Slot>{settingsLink}</Layout.Header.Slot>
|
||||
<InboxRequests
|
||||
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>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
useRoute,
|
||||
} from '@react-navigation/native'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
|
||||
@@ -49,7 +48,7 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
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 {ChatEnded} from './components/ChatEnded'
|
||||
import {ChatLocked} from './components/ChatLocked'
|
||||
@@ -154,8 +153,6 @@ function Inner({convoId}: {convoId: string}) {
|
||||
|
||||
return (
|
||||
<Layout.Center style={[a.flex_1]}>
|
||||
{/* MessagesList does not use the body scroll */}
|
||||
{isFocused && IS_WEB && <RemoveScrollBar />}
|
||||
{!readyToShow && (
|
||||
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
|
||||
<MessagesListHeader convo={convo} />
|
||||
|
||||
@@ -25,10 +25,11 @@ import {useMessagesEventBus} from '#/state/messages/events'
|
||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
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 {List} from '#/view/com/util/List'
|
||||
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 {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
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 {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
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 {ListFooter} from '#/components/Lists'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {RequestListItem} from './components/RequestListItem'
|
||||
import {useIsWithinSplitView} from './components/splitView/context'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
||||
|
||||
@@ -127,6 +129,7 @@ function RequestList({
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
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)
|
||||
// but only when the screen is active
|
||||
@@ -180,7 +183,7 @@ function RequestList({
|
||||
|
||||
if (conversations.length < 1) {
|
||||
return (
|
||||
<Layout.Center>
|
||||
<Layout.Center style={web([a.h_full])}>
|
||||
{isLoading ? (
|
||||
<ChatListLoadingPlaceholder />
|
||||
) : (
|
||||
@@ -212,56 +215,45 @@ function RequestList({
|
||||
label={l`Reload conversations`}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
onPress={() => void refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={RetryIcon} position="right" />
|
||||
<ButtonIcon icon={RetryIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
||||
<Text
|
||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
||||
<Trans comment="Title message shown in chat requests inbox when it's empty">
|
||||
Inbox zero!
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<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()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Messages', {animation: 'pop'})
|
||||
<EmptyState
|
||||
message={l({
|
||||
message: `Inbox zero!`,
|
||||
comment:
|
||||
"Title message shown in chat requests inbox when it's empty",
|
||||
})}
|
||||
icon={InboxLargeIcon}
|
||||
iconSize="4xl"
|
||||
textStyle={t.atoms.text}
|
||||
iconColor={t.atoms.text.color}
|
||||
button={
|
||||
isWithinSplitView
|
||||
? undefined
|
||||
: {
|
||||
label: l`Back to Chats`,
|
||||
text: l`Back`,
|
||||
onPress: () => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Messages', {animation: 'pop'})
|
||||
}
|
||||
},
|
||||
size: 'small',
|
||||
color: 'secondary',
|
||||
icon: ArrowLeftIcon,
|
||||
}
|
||||
}}>
|
||||
<ButtonIcon icon={ArrowLeftIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Back to Chats</Trans>
|
||||
</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`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={() => markAllRead()}>
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
<ButtonText>
|
||||
|
||||
@@ -51,16 +51,19 @@ import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {useIsWithinSplitView} from './splitView/context'
|
||||
|
||||
export const ChatListItemPortal = createPortalGroup()
|
||||
|
||||
export function ChatListItem({
|
||||
convo: convoView,
|
||||
showMenu = true,
|
||||
selected = false,
|
||||
children,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
showMenu?: boolean
|
||||
selected?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
@@ -78,7 +81,8 @@ export function ChatListItem({
|
||||
<DirectChatItem
|
||||
convo={convo}
|
||||
moderationOpts={moderationOpts}
|
||||
showMenu={showMenu}>
|
||||
showMenu={showMenu}
|
||||
selected={selected}>
|
||||
{children}
|
||||
</DirectChatItem>
|
||||
)
|
||||
@@ -88,7 +92,8 @@ export function ChatListItem({
|
||||
<GroupChatItem
|
||||
convo={convo}
|
||||
moderationOpts={moderationOpts}
|
||||
showMenu={showMenu}>
|
||||
showMenu={showMenu}
|
||||
selected={selected}>
|
||||
{children}
|
||||
</GroupChatItem>
|
||||
)
|
||||
@@ -103,15 +108,18 @@ function DirectChatItem({
|
||||
convo,
|
||||
moderationOpts,
|
||||
showMenu,
|
||||
selected,
|
||||
children,
|
||||
}: {
|
||||
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
||||
moderationOpts: ModerationOpts
|
||||
showMenu?: boolean
|
||||
selected?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const profile = useProfileShadow(convo.primaryMember)
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
|
||||
const moderation = useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
@@ -129,7 +137,7 @@ function DirectChatItem({
|
||||
avatar={
|
||||
<PreviewableUserAvatar
|
||||
profile={profile}
|
||||
size={52}
|
||||
size={isWithinSplitView ? 48 : 52}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
}
|
||||
@@ -145,15 +153,18 @@ function DirectChatItem({
|
||||
: l`This conversation is with a deleted or a deactivated account. Press for options`
|
||||
}
|
||||
showMenu={showMenu}
|
||||
selected={selected}
|
||||
isDeletedAccount={isDeletedAccount}
|
||||
isBlockedAccount={moderation.blocked}
|
||||
showProfileBadges
|
||||
postAlerts={
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentList')}
|
||||
size="lg"
|
||||
style={[a.pt_xs]}
|
||||
/>
|
||||
isWithinSplitView ? null : (
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentList')}
|
||||
size="sm"
|
||||
style={[a.pb_2xs, a.max_w_full, a.overflow_hidden]}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
{children}
|
||||
</BaseChatItem>
|
||||
@@ -164,15 +175,18 @@ function GroupChatItem({
|
||||
convo,
|
||||
moderationOpts,
|
||||
showMenu,
|
||||
selected,
|
||||
children,
|
||||
}: {
|
||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||
moderationOpts: ModerationOpts
|
||||
showMenu?: boolean
|
||||
selected?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const groupOwner = useMaybeProfileShadow(convo.primaryMember)
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
|
||||
const moderation = useMemo(
|
||||
() =>
|
||||
@@ -185,7 +199,12 @@ function GroupChatItem({
|
||||
return (
|
||||
<BaseChatItem
|
||||
convo={convo}
|
||||
avatar={<AvatarBubbles profiles={convo.members} size={52} />}
|
||||
avatar={
|
||||
<AvatarBubbles
|
||||
profiles={convo.members}
|
||||
size={isWithinSplitView ? 48 : 52}
|
||||
/>
|
||||
}
|
||||
title={chatName}
|
||||
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
||||
primaryProfile={groupOwner}
|
||||
@@ -193,6 +212,7 @@ function GroupChatItem({
|
||||
isBlockedAccount={false}
|
||||
isDeletedAccount={false}
|
||||
showProfileBadges={false}
|
||||
selected={selected}
|
||||
showMenu={showMenu}>
|
||||
{children}
|
||||
</BaseChatItem>
|
||||
@@ -210,6 +230,7 @@ function BaseChatItem({
|
||||
primaryProfile,
|
||||
primaryProfileModeration,
|
||||
showMenu,
|
||||
selected,
|
||||
showProfileBadges,
|
||||
postAlerts,
|
||||
children,
|
||||
@@ -224,6 +245,7 @@ function BaseChatItem({
|
||||
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
||||
primaryProfileModeration?: ModerationDecision
|
||||
showMenu?: boolean
|
||||
selected?: boolean
|
||||
showProfileBadges: boolean
|
||||
postAlerts?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
@@ -236,6 +258,7 @@ function BaseChatItem({
|
||||
const leaveConvoControl = useDialogControl()
|
||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
|
||||
const playHaptic = useHaptics()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -407,6 +430,8 @@ function BaseChatItem({
|
||||
leftFirst: deleteAction,
|
||||
}
|
||||
|
||||
const avatarSize = isWithinSplitView ? 48 : 52
|
||||
|
||||
return (
|
||||
<ChatListItemPortal.Provider>
|
||||
<GestureActionView actions={actions}>
|
||||
@@ -416,7 +441,7 @@ function BaseChatItem({
|
||||
// @ts-expect-error web only
|
||||
onFocus={onFocus}
|
||||
onBlur={onMouseLeave}
|
||||
style={[a.relative, t.atoms.bg]}>
|
||||
style={[a.relative, t.atoms.bg, isWithinSplitView && a.mx_sm]}>
|
||||
<View
|
||||
style={[
|
||||
a.z_10,
|
||||
@@ -457,13 +482,15 @@ function BaseChatItem({
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
a.gap_md,
|
||||
isWithinSplitView && a.rounded_sm,
|
||||
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
|
||||
selected && t.atoms.bg_contrast_50,
|
||||
]}>
|
||||
{/* Avatar goes here */}
|
||||
<View style={{width: 52, height: 52}} />
|
||||
<View style={{width: avatarSize, height: avatarSize}} />
|
||||
|
||||
<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.flex_shrink]}>
|
||||
<Text
|
||||
@@ -483,7 +510,7 @@ function BaseChatItem({
|
||||
{showProfileBadges && primaryProfile && (
|
||||
<ProfileBadges
|
||||
profile={primaryProfile}
|
||||
size="md"
|
||||
size="sm"
|
||||
style={[a.pl_xs, a.self_center]}
|
||||
/>
|
||||
)}
|
||||
@@ -526,15 +553,14 @@ function BaseChatItem({
|
||||
{subtitle && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_xs,
|
||||
]}>
|
||||
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}
|
||||
emoji>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{postAlerts}
|
||||
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
{LastMessageIcon && (
|
||||
<LastMessageIcon
|
||||
@@ -557,8 +583,6 @@ function BaseChatItem({
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{postAlerts}
|
||||
|
||||
{children}
|
||||
</View>
|
||||
|
||||
|
||||
@@ -3,15 +3,29 @@ import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {UNREAD_LIMIT} from '#/state/queries/messages/list-conversations'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Inbox_Stroke2_Corner2_Rounded as InboxIcon} from '#/components/icons/Inbox'
|
||||
import {Link} from '#/components/Link'
|
||||
|
||||
export function InboxRequests({count, more}: {count: number; more: boolean}) {
|
||||
export function InboxRequests({
|
||||
count,
|
||||
more,
|
||||
variant,
|
||||
}: {
|
||||
count: number
|
||||
more: boolean
|
||||
variant?: 'ghost' | 'solid'
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
if (count < 1) return null
|
||||
const unread = count > 0
|
||||
|
||||
const label =
|
||||
count >= UNREAD_LIMIT && more
|
||||
const label = !unread
|
||||
? l({
|
||||
message: `Requests`,
|
||||
comment: 'Incoming message requests',
|
||||
})
|
||||
: count >= UNREAD_LIMIT && more
|
||||
? l({
|
||||
message: `${count}+ requests`,
|
||||
comment: 'Displayed when the number of requests is greater than 20',
|
||||
@@ -21,12 +35,43 @@ export function InboxRequests({count, more}: {count: number; more: boolean}) {
|
||||
other: '# requests',
|
||||
})
|
||||
|
||||
return (
|
||||
<InlineLinkText
|
||||
label={label}
|
||||
to="/messages/inbox"
|
||||
style={[a.text_md, a.font_medium]}>
|
||||
{label}
|
||||
</InlineLinkText>
|
||||
)
|
||||
switch (variant) {
|
||||
case 'ghost': {
|
||||
return (
|
||||
<Link
|
||||
label={label}
|
||||
to="/messages/inbox"
|
||||
size="small"
|
||||
variant={unread ? 'solid' : 'ghost'}
|
||||
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.pb_lg,
|
||||
// prevent overlap with the scrollbar, which looks ugly
|
||||
a.pr_xs, // xs + md = lg
|
||||
{width: `calc(100% - ${tokens.space.md}px)` as '100%'},
|
||||
a.pr_sm, // sm + sm = lg
|
||||
{width: `calc(100% - ${tokens.space.sm}px)` as '100%'},
|
||||
],
|
||||
})}
|
||||
key={t.name} // android does not update when you change the colors. sigh.
|
||||
|
||||
@@ -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 {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig'
|
||||
import {EditBig_Stroke2_Corner2_Rounded as EditBig} from '#/components/icons/EditBig'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
|
||||
@@ -10,7 +10,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
@@ -40,6 +39,8 @@ import {
|
||||
ProfileFeedHeader,
|
||||
ProfileFeedHeaderSkeleton,
|
||||
} from '#/screens/Profile/components/ProfileFeedHeader'
|
||||
import {useTheme} from '#/alf'
|
||||
import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig'
|
||||
import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
@@ -134,6 +135,7 @@ export function ProfileFeedScreenInner({
|
||||
const {hasSession} = useSession()
|
||||
const {openComposer} = useOpenComposer()
|
||||
const isScreenFocused = useIsFocused()
|
||||
const t = useTheme()
|
||||
|
||||
useSetTitle(feedInfo?.displayName)
|
||||
|
||||
@@ -210,13 +212,7 @@ export function ProfileFeedScreenInner({
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
size={29}
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
}
|
||||
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
|
||||
@@ -15,7 +15,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
@@ -38,6 +37,7 @@ import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
|
||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog'
|
||||
import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
@@ -232,13 +232,7 @@ function ProfileListScreenLoaded({
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
size={29}
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
}
|
||||
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
@@ -281,13 +275,7 @@ function ProfileListScreenLoaded({
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
size={29}
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
}
|
||||
icon={<EditBigIcon size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
|
||||
@@ -197,11 +197,12 @@ export function ItemIcon({
|
||||
'2xs': 8,
|
||||
xs: 12,
|
||||
sm: 16,
|
||||
md: 20,
|
||||
md: 18,
|
||||
lg: 24,
|
||||
xl: 28,
|
||||
'2xl': 32,
|
||||
'3xl': 40,
|
||||
'4xl': 48,
|
||||
}[size]
|
||||
|
||||
const color =
|
||||
|
||||
Reference in New Issue
Block a user