pl-fe: add shoutbox list item to chats page

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-04-01 13:10:27 +02:00
parent 953c503ccf
commit a1cd5d204b
6 changed files with 124 additions and 19 deletions

View File

@ -1,4 +1,5 @@
import { verifyCredentials } from './auth';
import { importEntities } from './importer';
import { getMeToken, getMeUrl } from './me';
import type { PlApiClient, ShoutMessage } from 'pl-api';
@ -8,17 +9,25 @@ const SHOUTBOX_MESSAGE_IMPORT = 'SHOUTBOX_MESSAGE_IMPORT' as const;
const SHOUTBOX_MESSAGES_IMPORT = 'SHOUTBOX_MESSAGES_IMPORT' as const;
const SHOUTBOX_CONNECT = 'SHOUTBOX_CONNECT' as const;
const importShoutboxMessages = (messages: ShoutMessage[]) => ({
type: SHOUTBOX_MESSAGES_IMPORT,
messages,
});
const importShoutboxMessages = (messages: ShoutMessage[]) => (dispatch: AppDispatch): ShoutboxAction => {
dispatch(importEntities({ accounts: messages.map((message) => message.author) }));
const importShoutboxMessage = (message: ShoutMessage) => ({
type: SHOUTBOX_MESSAGE_IMPORT,
message,
});
return dispatch({
type: SHOUTBOX_MESSAGES_IMPORT,
messages,
});
};
const connectShoutbox = (dispatch: AppDispatch, getState: () => RootState) => {
const importShoutboxMessage = (message: ShoutMessage) => (dispatch: AppDispatch): ShoutboxAction => {
dispatch(importEntities({ accounts: [message.author] }));
return dispatch({
type: SHOUTBOX_MESSAGE_IMPORT,
message,
});
};
const connectShoutbox = () => (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const token = getMeToken(state);
const accountUrl = getMeUrl(state);
@ -45,8 +54,14 @@ type ShoutboxAction =
type: typeof SHOUTBOX_CONNECT;
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']>;
}
| ReturnType<typeof importShoutboxMessages>
| ReturnType<typeof importShoutboxMessage>;
| {
type: typeof SHOUTBOX_MESSAGE_IMPORT;
message: ShoutMessage;
}
| {
type: typeof SHOUTBOX_MESSAGES_IMPORT;
messages: ShoutMessage[];
}
export {
SHOUTBOX_MESSAGES_IMPORT,

View File

@ -0,0 +1,72 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { ParsedContent } from 'pl-fe/components/parsed-content';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
import type { Chat } from 'pl-api';
interface IChatListShoutboxInterface {
onClick: (chat: Chat | 'shoutbox') => void;
}
const ChatListShoutbox: React.FC<IChatListShoutboxInterface> = ({ onClick }) => {
const { logo } = usePlFeConfig();
const messages = useAppSelector((state) => state.shoutbox.messages);
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
onClick('shoutbox');
}
};
const lastMessage = messages.at(-1);
return (
<div
role='button'
key='shoutbox'
onClick={() => onClick('shoutbox')}
onKeyDown={handleKeyDown}
className='group flex w-full flex-col rounded-lg px-2 py-3 hover:bg-gray-100 focus:shadow-inset-ring dark:hover:bg-gray-800'
data-testid='chat-list-item'
tabIndex={0}
>
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
<HStack alignItems='center' space={2} className='overflow-hidden'>
<Avatar src={logo} alt='' size={40} className='flex-none' />
<Stack alignItems='start' className='overflow-hidden'>
<div className='flex w-full grow items-center space-x-1'>
<Text weight='bold' size='sm' align='left' truncate>
<FormattedMessage id='chat_list_item_shoutbox' defaultMessage='Instance shoutbox' />
</Text>
</div>
{lastMessage && (
<>
<Text
align='left'
size='sm'
weight='medium'
theme='default'
truncate
className='truncate-child pointer-events-none h-5 w-full'
>
<Text weight='bold' size='sm' align='left' truncate tag='span'>{lastMessage.author.display_name || `@${lastMessage.author.username}`}: </Text>
<ParsedContent html={lastMessage.text} />
</Text>
</>
)}
</Stack>
</HStack>
</HStack>
</div>
);
};
export { ChatListShoutbox as default };

View File

@ -6,18 +6,25 @@ import PullToRefresh from 'pl-fe/components/pull-to-refresh';
import Spinner from 'pl-fe/components/ui/spinner';
import Stack from 'pl-fe/components/ui/stack';
import PlaceholderChat from 'pl-fe/features/placeholder/components/placeholder-chat';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useChats } from 'pl-fe/queries/chats';
import ChatListItem from './chat-list-item';
import ChatListShoutbox from './chat-list-shoutbox';
import type { Chat } from 'pl-api';
interface IChatList {
onClickChat: (chat: any) => void;
onClickChat: (chat: Chat | 'shoutbox') => void;
useWindowScroll?: boolean;
}
const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false }) => {
const { shoutbox } = useFeatures();
const { chatsQuery: { data: chats, isFetching, hasNextPage, fetchNextPage, refetch } } = useChats();
const allChats: Array<Chat | 'shoutbox'> | undefined = shoutbox ? ['shoutbox', ...(chats || [])] : chats;
const [isNearBottom, setNearBottom] = useState<boolean>(false);
const [isNearTop, setNearTop] = useState<boolean>(true);
@ -50,11 +57,11 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false })
atTopStateChange={(atTop) => setNearTop(atTop)}
atBottomStateChange={(atBottom) => setNearBottom(atBottom)}
useWindowScroll={useWindowScroll}
data={chats}
data={allChats}
endReached={handleLoadMore}
itemContent={(_index, chat) => (
<div className='px-2'>
<ChatListItem chat={chat} onClick={onClickChat} />
{chat === 'shoutbox' ? <ChatListShoutbox onClick={onClickChat} /> : <ChatListItem chat={chat} onClick={onClickChat} />}
</div>
)}
components={{

View File

@ -19,8 +19,8 @@ const ChatPageSidebar = () => {
const intl = useIntl();
const history = useHistory();
const handleClickChat = (chat: Chat) => {
history.push(`/chats/${chat.id}`);
const handleClickChat = (chat: Chat | 'shoutbox') => {
history.push(`/chats/${chat === 'shoutbox' ? 'shoutbox' : chat.id}`);
};
const handleChatCreate = () => {

View File

@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
import Stack from 'pl-fe/components/ui/stack';
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useChats } from 'pl-fe/queries/chats';
import ChatList from '../chat-list';
@ -19,16 +20,21 @@ import type { Chat } from 'pl-api';
const ChatPane = () => {
const { unreadChatsCount } = useStatContext();
const { shoutbox } = useFeatures();
const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats();
const handleClickChat = (nextChat: Chat) => {
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
const handleClickChat = (nextChat: Chat | 'shoutbox') => {
if (nextChat === 'shoutbox') {
// changeScreen(ChatWidgetScreens.SHOUTBOX);
} else {
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
}
};
const renderBody = () => {
if (Number(chats?.length) > 0 || isLoading) {
if (Number(chats?.length) > 0 || shoutbox || isLoading) {
return (
<Stack space={4} className='h-full grow'>
<ChatList onClickChat={handleClickChat} />

View File

@ -8,6 +8,7 @@ import { fetchFilters } from 'pl-fe/actions/filters';
import { fetchMarker } from 'pl-fe/actions/markers';
import { expandNotifications } from 'pl-fe/actions/notifications';
import { register as registerPushNotifications } from 'pl-fe/actions/push-notifications/registerer';
import { connectShoutbox } from 'pl-fe/actions/shoutbox';
import { fetchHomeTimeline } from 'pl-fe/actions/timelines';
import { useUserStream } from 'pl-fe/api/hooks/streaming/use-user-stream';
import SidebarNavigation from 'pl-fe/components/sidebar-navigation';
@ -421,6 +422,10 @@ const UI: React.FC<IUI> = React.memo(({ children }) => {
setTimeout(() => prefetchFollowRequests(client), 700);
}
if (features.shoutbox) {
dispatch(connectShoutbox());
}
setTimeout(() => {
queryClient.prefetchInfiniteQuery(scheduledStatusesQueryOptions);
}, 900);