Merge branch 'alex-chats' into chats

This commit is contained in:
Justin 2022-09-12 14:50:02 -04:00
commit 56c617bd32
13 changed files with 100 additions and 61 deletions

View File

@ -113,9 +113,9 @@ const SidebarNavigation = () => {
return ( return (
<SidebarNavigationLink <SidebarNavigationLink
to='/chats' to='/chats'
icon={require('@tabler/icons/messages.svg')} icon={require('@tabler/icons/mail.svg')}
count={chatsCount} count={chatsCount}
text={<FormattedMessage id='tabs_bar.chats' defaultMessage='Chats' />} text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
/> />
); );
} }

View File

@ -17,8 +17,8 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
if (features.chats) { if (features.chats) {
return ( return (
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/messages.svg')} src={require('@tabler/icons/mail.svg')}
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />} text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
to='/chats' to='/chats'
exact exact
count={chatsCount} count={chatsCount}

View File

@ -1,7 +1,7 @@
import classNames from 'clsx'; import classNames from 'clsx';
import React from 'react'; import React from 'react';
type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 10
const spaces = { const spaces = {
0: 'space-y-0', 0: 'space-y-0',
@ -12,6 +12,7 @@ const spaces = {
3: 'space-y-3', 3: 'space-y-3',
4: 'space-y-4', 4: 'space-y-4',
5: 'space-y-5', 5: 'space-y-5',
6: 'space-y-6',
10: 'space-y-10', 10: 'space-y-10',
}; };

View File

@ -1,4 +1,5 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import classNames from 'clsx';
import React, { MutableRefObject, useState } from 'react'; import React, { MutableRefObject, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
@ -6,7 +7,7 @@ import { uploadMedia } from 'soapbox/actions/media';
import { HStack, IconButton, Stack, Text, Textarea } from 'soapbox/components/ui'; import { HStack, IconButton, Stack, Text, Textarea } from 'soapbox/components/ui';
import UploadProgress from 'soapbox/components/upload-progress'; import UploadProgress from 'soapbox/components/upload-progress';
import UploadButton from 'soapbox/features/compose/components/upload_button'; import UploadButton from 'soapbox/features/compose/components/upload_button';
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { IChat, useChat } from 'soapbox/queries/chats'; import { IChat, useChat } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client'; import { queryClient } from 'soapbox/queries/client';
import { truncateFilename } from 'soapbox/utils/media'; import { truncateFilename } from 'soapbox/utils/media';
@ -23,14 +24,15 @@ const fileKeyGen = (): number => Math.floor((Math.random() * 0x10000));
interface IChatBox { interface IChatBox {
chat: IChat, chat: IChat,
autosize?: boolean, autosize?: boolean,
inputRef?: MutableRefObject<HTMLTextAreaElement> inputRef?: MutableRefObject<HTMLTextAreaElement>,
className?: string,
} }
/** /**
* Chat UI with just the messages and textarea. * Chat UI with just the messages and textarea.
* Reused between floating desktop chats and fullscreen/mobile chats. * Reused between floating desktop chats and fullscreen/mobile chats.
*/ */
const ChatBox: React.FC<IChatBox> = ({ chat, autosize, inputRef }) => { const ChatBox: React.FC<IChatBox> = ({ chat, autosize, inputRef, className }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useOwnAccount(); const account = useOwnAccount();
@ -48,7 +50,7 @@ const ChatBox: React.FC<IChatBox> = ({ chat, autosize, inputRef }) => {
const submitMessage = useMutation(({ chatId, content }: any) => createChatMessage(chatId, content), { const submitMessage = useMutation(({ chatId, content }: any) => createChatMessage(chatId, content), {
retry: false, retry: false,
onMutate: async(newMessage: any) => { onMutate: async (newMessage: any) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update) // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['chats', 'messages', chat.id]); await queryClient.cancelQueries(['chats', 'messages', chat.id]);
@ -206,7 +208,7 @@ const ChatBox: React.FC<IChatBox> = ({ chat, autosize, inputRef }) => {
}; };
return ( return (
<Stack className='overflow-hidden flex flex-grow' onMouseOver={handleMouseOver}> <Stack className={classNames('overflow-hidden flex flex-grow', className)} onMouseOver={handleMouseOver}>
<div className='flex-grow h-full overflow-hidden flex justify-center'> <div className='flex-grow h-full overflow-hidden flex justify-center'>
<ChatMessageList chat={chat} autosize /> <ChatMessageList chat={chat} autosize />
</div> </div>

View File

@ -15,10 +15,11 @@ import Blankslate from './chat-pane/blankslate';
interface IChatList { interface IChatList {
onClickChat: (chat: any) => void, onClickChat: (chat: any) => void,
useWindowScroll?: boolean, useWindowScroll?: boolean,
fade?: boolean,
searchValue?: string searchValue?: string
} }
const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, searchValue }) => { const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, searchValue, fade }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const chatListRef = useRef(null); const chatListRef = useRef(null);
@ -75,18 +76,22 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, s
)} )}
</PullToRefresh> </PullToRefresh>
<div {fade && (
className={classNames('inset-x-0 top-0 flex rounded-t-lg justify-center bg-gradient-to-b from-white pb-12 pt-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', { <>
'opacity-0': isNearTop, <div
'opacity-100': !isNearTop, className={classNames('inset-x-0 top-0 flex rounded-t-lg justify-center bg-gradient-to-b from-white pb-12 pt-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
})} 'opacity-0': isNearTop,
/> 'opacity-100': !isNearTop,
<div })}
className={classNames('inset-x-0 bottom-0 flex rounded-b-lg justify-center bg-gradient-to-t from-white pt-12 pb-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', { />
'opacity-0': isNearBottom, <div
'opacity-100': !isNearBottom, className={classNames('inset-x-0 bottom-0 flex rounded-b-lg justify-center bg-gradient-to-t from-white pt-12 pb-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
})} 'opacity-0': isNearBottom,
/> 'opacity-100': !isNearBottom,
})}
/>
</>
)}
</div> </div>
); );
}; };

View File

@ -283,7 +283,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
title={getFormattedTimestamp(chatMessage)} title={getFormattedTimestamp(chatMessage)}
className={ className={
classNames({ classNames({
'text-ellipsis break-words relative rounded-md p-2': true, 'text-ellipsis break-words relative rounded-md p-2 max-w-full': true,
'bg-primary-500 text-white mr-2': isMyMessage, 'bg-primary-500 text-white mr-2': isMyMessage,
'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100 order-2 ml-2': !isMyMessage, 'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100 order-2 ml-2': !isMyMessage,
}) })

View File

@ -84,7 +84,7 @@ const ChatPane = () => {
<ChatList <ChatList
searchValue={debouncedValue} searchValue={debouncedValue}
onClickChat={handleClickChat} onClickChat={handleClickChat}
useWindowScroll={false} fade
/> />
) : ( ) : (
<Text>no results</Text> <Text>no results</Text>

View File

@ -21,12 +21,12 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
data-testid='chat' data-testid='chat'
> >
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'> <HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
<HStack alignItems='center' space={2}> <HStack alignItems='center' space={2} className='overflow-hidden'>
<Avatar src={chat.account?.avatar} size={40} /> <Avatar src={chat.account?.avatar} size={40} className='flex-none' />
<Stack alignItems='start'> <Stack alignItems='start' className='overflow-hidden'>
<div className='flex items-center space-x-1 flex-grow'> <div className='flex items-center space-x-1 flex-grow w-full'>
<Text weight='bold' size='sm' truncate>{chat.account?.display_name || `@${chat.account.username}`}</Text> <Text weight='bold' size='sm' align='left' truncate>{chat.account?.display_name || `@${chat.account.username}`}</Text>
{chat.account?.verified && <VerificationBadge />} {chat.account?.verified && <VerificationBadge />}
</div> </div>
@ -37,7 +37,7 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
weight='medium' weight='medium'
theme='muted' theme='muted'
truncate truncate
className='max-w-[200px]' className='w-full'
data-testid='chat-last-message' data-testid='chat-last-message'
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }} dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
/> />
@ -54,7 +54,12 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
/> />
)} )}
<RelativeTimestamp timestamp={chat.last_message.created_at} size='sm' /> <RelativeTimestamp
timestamp={chat.last_message.created_at}
align='right'
size='xs'
truncate
/>
</HStack> </HStack>
)} )}
</HStack> </HStack>

View File

@ -1,18 +1,19 @@
import React from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { launchChat } from 'soapbox/actions/chats'; import { launchChat } from 'soapbox/actions/chats';
import AccountSearch from 'soapbox/components/account_search'; import AccountSearch from 'soapbox/components/account_search';
import AudioToggle from 'soapbox/features/chats/components/audio-toggle';
import { Column } from '../../components/ui'; import { Card, CardTitle, Stack } from '../../components/ui';
import Chat from './components/chat';
import ChatBox from './components/chat-box';
import ChatList from './components/chat-list'; import ChatList from './components/chat-list';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.chats', defaultMessage: 'Chats' }, title: { id: 'column.chats', defaultMessage: 'Messages' },
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' }, searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' },
}); });
@ -21,30 +22,44 @@ const ChatIndex: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const [chat, setChat] = useState<any>(null);
const handleSuggestion = (accountId: string) => { const handleSuggestion = (accountId: string) => {
dispatch(launchChat(accountId, history, true)); dispatch(launchChat(accountId, history, true));
}; };
const handleClickChat = (chat: { id: string }) => { const handleClickChat = (chat: any) => {
history.push(`/chats/${chat.id}`); // history.push(`/chats/${chat.id}`);
setChat(chat);
}; };
return ( return (
<Column label={intl.formatMessage(messages.title)}> <Card className='p-0 h-[calc(100vh-176px)] overflow-hidden' variant='rounded'>
<div className='column__switch'> <div className='grid grid-cols-9 overflow-hidden h-full'>
<AudioToggle /> <Stack className='col-span-3 p-6 bg-gradient-to-r from-white to-gray-100 overflow-hidden' space={6}>
<CardTitle title={intl.formatMessage(messages.title)} />
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={handleSuggestion}
/>
<Stack className='-mx-3 flex-grow h-full'>
<ChatList onClickChat={handleClickChat} />
</Stack>
</Stack>
<Stack className='col-span-6 h-full overflow-hidden'>
{chat && (
<Stack className='h-full overflow-hidden'>
<Chat chat={chat} onClick={() => {}} />
<div className='h-full overflow-hidden'>
<ChatBox className='h-full overflow-hidden' chat={chat} onSetInputRef={() => {}} />
</div>
</Stack>
)}
</Stack>
</div> </div>
</Card>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={handleSuggestion}
/>
<ChatList
onClickChat={handleClickChat}
useWindowScroll
/>
</Column>
); );
}; };

View File

@ -29,6 +29,7 @@ import ThumbNavigation from 'soapbox/components/thumb_navigation';
import { Layout } from 'soapbox/components/ui'; import { Layout } from 'soapbox/components/ui';
import { useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures } from 'soapbox/hooks'; import { useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
import AdminPage from 'soapbox/pages/admin_page'; import AdminPage from 'soapbox/pages/admin_page';
import ChatsPage from 'soapbox/pages/chats-page';
import DefaultPage from 'soapbox/pages/default_page'; import DefaultPage from 'soapbox/pages/default_page';
// import GroupsPage from 'soapbox/pages/groups_page'; // import GroupsPage from 'soapbox/pages/groups_page';
// import GroupPage from 'soapbox/pages/group_page'; // import GroupPage from 'soapbox/pages/group_page';
@ -265,8 +266,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
{features.suggestions && <WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />} {features.suggestions && <WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />}
{features.profileDirectory && <WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />} {features.profileDirectory && <WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />}
{features.chats && <WrappedRoute path='/chats' exact page={DefaultPage} component={ChatIndex} content={children} />} {features.chats && <WrappedRoute path='/chats' exact page={ChatsPage} component={ChatIndex} content={children} />}
{features.chats && <WrappedRoute path='/chats/:chatId' page={DefaultPage} component={ChatRoom} content={children} />} {features.chats && <WrappedRoute path='/chats/:chatId' page={ChatsPage} component={ChatRoom} content={children} />}
<WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} /> <WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} /> <WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />

View File

@ -194,7 +194,7 @@
"column.birthdays": "Birthdays", "column.birthdays": "Birthdays",
"column.blocks": "Blocked users", "column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks", "column.bookmarks": "Bookmarks",
"column.chats": "Chats", "column.chats": "Messages",
"column.community": "Local timeline", "column.community": "Local timeline",
"column.crypto_donate": "Donate Cryptocurrency", "column.crypto_donate": "Donate Cryptocurrency",
"column.developers": "Developers", "column.developers": "Developers",

View File

@ -0,0 +1,12 @@
import React from 'react';
/** Custom layout for chats on desktop. */
const ChatsPage: React.FC = ({ children }) => {
return (
<div className='md:col-span-12 lg:col-span-9 pb-36'>
{children}
</div>
);
};
export default ChatsPage;

View File

@ -3,8 +3,10 @@ import { useEffect, useState } from 'react';
import { fetchRelationships } from 'soapbox/actions/accounts'; import { fetchRelationships } from 'soapbox/actions/accounts';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import compareId from 'soapbox/compare_id';
import { useChatContext } from 'soapbox/contexts/chat-context'; import { useChatContext } from 'soapbox/contexts/chat-context';
import { useApi, useAppDispatch } from 'soapbox/hooks'; import { useApi, useAppDispatch } from 'soapbox/hooks';
import { normalizeChatMessage } from 'soapbox/normalizers';
import { queryClient } from './client'; import { queryClient } from './client';
@ -40,11 +42,7 @@ export interface IChatMessage {
pending?: boolean pending?: boolean
} }
const reverseOrder = (a: IChat, b: IChat): number => { const reverseOrder = (a: IChat, b: IChat): number => compareId(a.id, b.id);
if (Number(a.id) < Number(b.id)) return -1;
if (Number(a.id) > Number(b.id)) return 1;
return 0;
};
const useChatMessages = (chatId: string) => { const useChatMessages = (chatId: string) => {
const api = useApi(); const api = useApi();
@ -57,7 +55,7 @@ const useChatMessages = (chatId: string) => {
}); });
const hasMore = !!headers.link; const hasMore = !!headers.link;
const result = data.sort(reverseOrder); const result = data.sort(reverseOrder).map(normalizeChatMessage);
const nextMaxId = result[0]?.id; const nextMaxId = result[0]?.id;
return { return {