pl-fe: fix chats in mobile view and clean some of the chats routing fuckery up

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-01-27 15:19:25 +01:00
parent 545040e94b
commit 368b6d6282
15 changed files with 186 additions and 174 deletions

View File

@ -272,7 +272,7 @@ const SidebarNavigation: React.FC<ISidebarNavigation> = React.memo(({ shrink })
{features.chats && (
<SidebarNavigationLink
to='/chats/{-$chatId}'
to='/chats'
icon={require('@phosphor-icons/core/regular/chats-teardrop.svg')}
activeIcon={require('@phosphor-icons/core/fill/chats-teardrop-fill.svg')}
count={unreadChatsCount}

View File

@ -675,7 +675,7 @@ const MenuButton: React.FC<IMenuButton> = ({
const account = status.account;
getOrCreateChatByAccountId(account.id)
.then((chat) => navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } }))
.then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } }))
.catch(() => {});
};

View File

@ -120,7 +120,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => {
src={require('@phosphor-icons/core/regular/chats-teardrop.svg')}
activeSrc={require('@phosphor-icons/core/fill/chats-teardrop-fill.svg')}
text={intl.formatMessage(messages.chats)}
to='/chats/{-$chatId}'
to='/chats'
exact
count={unreadChatsCount}
countMax={9}

View File

@ -166,7 +166,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
toast.error(data?.error);
},
onSuccess: (response) => {
navigate({ to: '/chats/{-$chatId}', params: { chatId: response.id } });
navigate({ to: '/chats/$chatId', params: { chatId: response.id } });
queryClient.invalidateQueries({
queryKey: ['chats', 'search'],
});

View File

@ -59,7 +59,7 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
deleteChat.mutate(undefined, {
onSuccess() {
if (isUsingMainChatPage) {
navigate({ to: '/chats/{-$chatId}' });
navigate({ to: '/chats' });
}
},
});

View File

@ -1,17 +1,17 @@
import { Outlet, useMatches } from '@tanstack/react-router';
import { Outlet, useMatch } from '@tanstack/react-router';
import clsx from 'clsx';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import Stack from 'pl-fe/components/ui/stack';
import { chatsEmptyRoute } from 'pl-fe/features/ui/router';
import { useChats } from 'pl-fe/queries/chats';
import ChatPageSidebar from './components/chat-page-sidebar';
const SIDEBAR_HIDDEN_PATHS = ['/chats/settings', '/chats/new', '/chats/:chatId', '/chats/shoutbox'];
const ChatPage: React.FC = () => {
const isSidebarHidden = useMatches({
select: (matches) => SIDEBAR_HIDDEN_PATHS.some((path) => matches.some(match => match.pathname === path)),
});
const { chatsQuery: { data: chats } } = useChats();
const isSidebarHidden = !useMatch({ from: chatsEmptyRoute.id, shouldThrow: false }) || chats?.length === 0;
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<string | number>('100%');

View File

@ -0,0 +1,156 @@
import { Link, useNavigate } from '@tanstack/react-router';
import React, { useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DropdownMenu, { type Menu } from 'pl-fe/components/dropdown-menu';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
import IconButton from 'pl-fe/components/ui/icon-button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { chatRoute } from 'pl-fe/features/ui/router';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useChat, useChatActions } from 'pl-fe/queries/chats';
import { useModalsActions } from 'pl-fe/stores/modals';
import Chat from '../../chat';
const messages = defineMessages({
blockMessage: { id: 'chat_settings.block.message', defaultMessage: 'Blocking will prevent this profile from direct messaging you and viewing your content. You can unblock later.' },
blockHeading: { id: 'chat_settings.block.heading', defaultMessage: 'Block @{acct}' },
blockConfirm: { id: 'chat_settings.block.confirm', defaultMessage: 'Block' },
unblockMessage: { id: 'chat_settings.unblock.message', defaultMessage: 'Unblocking will allow this profile to direct message you and view your content.' },
unblockHeading: { id: 'chat_settings.unblock.heading', defaultMessage: 'Unblock @{acct}' },
unblockConfirm: { id: 'chat_settings.unblock.confirm', defaultMessage: 'Unblock' },
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
leaveHeading: { id: 'chat_settings.leave.heading', defaultMessage: 'Leave chat' },
leaveConfirm: { id: 'chat_settings.leave.confirm', defaultMessage: 'Leave chat' },
blockUser: { id: 'chat_settings.options.block_user', defaultMessage: 'Block @{acct}' },
unblockUser: { id: 'chat_settings.options.unblock_user', defaultMessage: 'Unblock @{acct}' },
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave chat' },
});
const ChatPageMain = () => {
const intl = useIntl();
const features = useFeatures();
const navigate = useNavigate();
const { chatId } = chatRoute.useParams();
const { openModal } = useModalsActions();
const { data: chat } = useChat(chatId);
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const { deleteChat } = useChatActions(chat?.id as string);
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;
const handleBlockUser = () => {
openModal('BLOCK_MUTE', {
accountId: chat!.account.id,
action: 'BLOCK',
});
};
const handleUnblockUser = () => {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.unblockHeading, { acct: chat?.account.acct }),
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
onConfirm: () => unblockAccount(),
});
};
const handleLeaveChat = () => {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.leaveHeading),
message: intl.formatMessage(messages.leaveMessage),
confirm: intl.formatMessage(messages.leaveConfirm),
onConfirm: () => {
deleteChat.mutate(undefined, {
onSuccess() {
navigate({ to: '/chats' });
},
});
},
});
};
if (!chat) {
return null;
}
const menuItems: Menu = [
{
icon: require('@phosphor-icons/core/regular/prohibit.svg'),
text: intl.formatMessage(isBlocked ? messages.unblockUser : messages.blockUser, { acct: chat.account.acct }),
action: isBlocked ? handleUnblockUser : handleBlockUser,
},
];
if (features.chatsDelete) menuItems.push({
icon: require('@phosphor-icons/core/regular/sign-out.svg'),
text: intl.formatMessage(messages.leaveChat),
action: handleLeaveChat,
});
return (
<Stack className='h-full overflow-hidden'>
<HStack alignItems='center' justifyContent='between' space={2} className='w-full p-4'>
<HStack alignItems='center' space={2}>
<HStack alignItems='center'>
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/$chatId' })}
/>
<Link to='/@{$username}' params={{ username: chat.account.acct }}>
<Avatar src={chat.account.avatar} alt={chat.account.avatar_description} size={40} className='flex-none' isCat={chat.account.is_cat} username={chat.account.username} />
</Link>
</HStack>
<Stack alignItems='start' className='h-11 overflow-hidden'>
<div className='flex w-full grow items-center space-x-1'>
<Link to='/@{$username}' params={{ username: chat.account.acct }}>
<Text weight='bold' size='sm' align='left' truncate>
{chat.account.display_name || `@${chat.account.username}`}
</Text>
</Link>
{chat.account.verified && <VerificationBadge />}
</div>
</Stack>
</HStack>
<DropdownMenu
src={require('@phosphor-icons/core/regular/info.svg')}
component={() => (
<HStack className='px-4 py-2' alignItems='center' space={3}>
<Avatar src={chat.account.avatar} staticSrc={chat.account.avatar_static} alt={chat.account.avatar_description} size={50} isCat={chat.account.is_cat} username={chat.account.username} />
<Stack>
<Text weight='semibold'>{chat.account.display_name}</Text>
<Text size='sm' theme='primary'>@{chat.account.acct}</Text>
</Stack>
</HStack>
)}
items={menuItems}
/>
</HStack>
<div className='h-full overflow-hidden'>
<Chat
className='h-full'
chat={chat}
inputRef={inputRef}
/>
</div>
</Stack>
);
};
export { ChatPageMain as default };

View File

@ -1,174 +1,22 @@
import { Link, useNavigate } from '@tanstack/react-router';
import React, { useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import React from 'react';
import DropdownMenu, { type Menu } from 'pl-fe/components/dropdown-menu';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
import IconButton from 'pl-fe/components/ui/icon-button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { useChatContext } from 'pl-fe/contexts/chat-context';
import { chatRoute } from 'pl-fe/features/ui/router';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useChat, useChatActions, useChats } from 'pl-fe/queries/chats';
import { useModalsActions } from 'pl-fe/stores/modals';
import Chat from '../../chat';
import { useChats } from 'pl-fe/queries/chats';
import BlankslateEmpty from './blankslate-empty';
import BlankslateWithChats from './blankslate-with-chats';
const messages = defineMessages({
blockMessage: { id: 'chat_settings.block.message', defaultMessage: 'Blocking will prevent this profile from direct messaging you and viewing your content. You can unblock later.' },
blockHeading: { id: 'chat_settings.block.heading', defaultMessage: 'Block @{acct}' },
blockConfirm: { id: 'chat_settings.block.confirm', defaultMessage: 'Block' },
unblockMessage: { id: 'chat_settings.unblock.message', defaultMessage: 'Unblocking will allow this profile to direct message you and view your content.' },
unblockHeading: { id: 'chat_settings.unblock.heading', defaultMessage: 'Unblock @{acct}' },
unblockConfirm: { id: 'chat_settings.unblock.confirm', defaultMessage: 'Unblock' },
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
leaveHeading: { id: 'chat_settings.leave.heading', defaultMessage: 'Leave chat' },
leaveConfirm: { id: 'chat_settings.leave.confirm', defaultMessage: 'Leave chat' },
blockUser: { id: 'chat_settings.options.block_user', defaultMessage: 'Block @{acct}' },
unblockUser: { id: 'chat_settings.options.unblock_user', defaultMessage: 'Unblock @{acct}' },
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave chat' },
});
const ChatPageMain = () => {
const intl = useIntl();
const features = useFeatures();
const navigate = useNavigate();
const { chatId } = chatRoute.useParams();
const { openModal } = useModalsActions();
const { data: chat } = useChat(chatId);
const { currentChatId } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats();
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const { deleteChat } = useChatActions(chat?.id as string);
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;
const handleBlockUser = () => {
openModal('BLOCK_MUTE', {
accountId: chat!.account.id,
action: 'BLOCK',
});
};
const handleUnblockUser = () => {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.unblockHeading, { acct: chat?.account.acct }),
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
onConfirm: () => unblockAccount(),
});
};
const handleLeaveChat = () => {
openModal('CONFIRM', {
heading: intl.formatMessage(messages.leaveHeading),
message: intl.formatMessage(messages.leaveMessage),
confirm: intl.formatMessage(messages.leaveConfirm),
onConfirm: () => {
deleteChat.mutate(undefined, {
onSuccess() {
navigate({ to: '/chats/{-$chatId}' });
},
});
},
});
};
if (isLoading) {
return null;
}
if (!currentChatId && chats && chats.length > 0) {
if (chats && chats.length > 0) {
return <BlankslateWithChats />;
}
if (!currentChatId) {
return <BlankslateEmpty />;
}
if (!chat) {
return null;
}
const menuItems: Menu = [
{
icon: require('@phosphor-icons/core/regular/prohibit.svg'),
text: intl.formatMessage(isBlocked ? messages.unblockUser : messages.blockUser, { acct: chat.account.acct }),
action: isBlocked ? handleUnblockUser : handleBlockUser,
},
];
if (features.chatsDelete) menuItems.push({
icon: require('@phosphor-icons/core/regular/sign-out.svg'),
text: intl.formatMessage(messages.leaveChat),
action: handleLeaveChat,
});
return (
<Stack className='h-full overflow-hidden'>
<HStack alignItems='center' justifyContent='between' space={2} className='w-full p-4'>
<HStack alignItems='center' space={2}>
<HStack alignItems='center'>
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
/>
<Link to='/@{$username}' params={{ username: chat.account.acct }}>
<Avatar src={chat.account.avatar} alt={chat.account.avatar_description} size={40} className='flex-none' isCat={chat.account.is_cat} username={chat.account.username} />
</Link>
</HStack>
<Stack alignItems='start' className='h-11 overflow-hidden'>
<div className='flex w-full grow items-center space-x-1'>
<Link to='/@{$username}' params={{ username: chat.account.acct }}>
<Text weight='bold' size='sm' align='left' truncate>
{chat.account.display_name || `@${chat.account.username}`}
</Text>
</Link>
{chat.account.verified && <VerificationBadge />}
</div>
</Stack>
</HStack>
<DropdownMenu
src={require('@phosphor-icons/core/regular/info.svg')}
component={() => (
<HStack className='px-4 py-2' alignItems='center' space={3}>
<Avatar src={chat.account.avatar} staticSrc={chat.account.avatar_static} alt={chat.account.avatar_description} size={50} isCat={chat.account.is_cat} username={chat.account.username} />
<Stack>
<Text weight='semibold'>{chat.account.display_name}</Text>
<Text size='sm' theme='primary'>@{chat.account.acct}</Text>
</Stack>
</HStack>
)}
items={menuItems}
/>
</HStack>
<div className='h-full overflow-hidden'>
<Chat
className='h-full'
chat={chat}
inputRef={inputRef}
/>
</div>
</Stack>
);
return <BlankslateEmpty />;
};
export { ChatPageMain as default };

View File

@ -28,7 +28,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<CardTitle title={intl.formatMessage(messages.title)} />

View File

@ -58,7 +58,7 @@ const ChatPageSettings = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<CardTitle title={intl.formatMessage(messages.title)} />

View File

@ -25,7 +25,7 @@ const ChatPageShoutbox = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<Avatar src={logo} alt='' size={40} className='flex-none' />

View File

@ -23,7 +23,7 @@ const ChatPageSidebar = () => {
if (chat === 'shoutbox') {
navigate({ to: '/chats/shoutbox' });
} else {
navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } });
navigate({ to: '/chats/$chatId', params: { chatId: chat.id } });
}
};

View File

@ -53,7 +53,7 @@ const ChatSearch: React.FC<IChatSearch> = ({ isMainPage = false }) => {
},
onSuccess: (response) => {
if (isMainPage) {
navigate({ to: '/chats/{-$chatId}', params: { chatId: response.id } });
navigate({ to: '/chats/$chatId', params: { chatId: response.id } });
} else {
changeScreen(ChatWidgetScreens.CHAT, response.id);
}

View File

@ -171,7 +171,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const handleChatClick = () => {
getOrCreateChatByAccountId(account.id)
.then((chat) => navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } }))
.then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } }))
.catch(() => {});
};

View File

@ -39,6 +39,7 @@ import StatusLayout from 'pl-fe/layouts/status-layout';
import { instanceInitialState } from 'pl-fe/reducers/instance';
import { isStandalone } from 'pl-fe/utils/state';
import ChatPageChat from '../../chats/components/chat-page/components/chat-page-chat';
import ChatPageMain from '../../chats/components/chat-page/components/chat-page-main';
import ChatPageNew from '../../chats/components/chat-page/components/chat-page-new';
import ChatPageSettings from '../../chats/components/chat-page/components/chat-page-settings';
@ -583,7 +584,13 @@ export const shoutboxRoute = createRoute({
export const chatRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/{-$chatId}',
path: '/$chatId',
component: ChatPageChat,
});
export const chatsEmptyRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/',
component: ChatPageMain,
});
@ -1311,6 +1318,7 @@ const routeTree = rootRoute.addChildren([
chatsSettingsRoute,
shoutboxRoute,
chatRoute,
chatsEmptyRoute,
]),
]),
layouts.default.addChildren([