Add ability to delete message

This commit is contained in:
Justin 2022-08-16 08:39:58 -04:00
parent 7557445a3e
commit 1ed1f3fd2e
4 changed files with 191 additions and 169 deletions

View File

@ -1,3 +1,4 @@
import { useMutation } from '@tanstack/react-query';
import classNames from 'clsx'; import classNames from 'clsx';
import { import {
Map as ImmutableMap, Map as ImmutableMap,
@ -16,10 +17,12 @@ import { initReportById } from 'soapbox/actions/reports';
import { Avatar, HStack, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui'; import { Avatar, HStack, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import emojify from 'soapbox/features/emoji/emoji'; import emojify from 'soapbox/features/emoji/emoji';
import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
import Bundle from 'soapbox/features/ui/components/bundle'; import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useAppDispatch, useRefEventHandler, useOwnAccount } from 'soapbox/hooks'; import { useAppSelector, useAppDispatch, useRefEventHandler, useOwnAccount } from 'soapbox/hooks';
import { IChat, IChatMessage, useChatMessages } from 'soapbox/queries/chats'; import { IChat, IChatMessage, useChat, useChatMessages } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
import { onlyEmoji } from 'soapbox/utils/rich_content'; import { onlyEmoji } from 'soapbox/utils/rich_content';
import type { Menu } from 'soapbox/components/dropdown_menu'; import type { Menu } from 'soapbox/components/dropdown_menu';
@ -80,10 +83,10 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
const [initialLoad, setInitialLoad] = useState(true); const [initialLoad, setInitialLoad] = useState(true);
const [scrollPosition, setScrollPosition] = useState(0); const [scrollPosition, setScrollPosition] = useState(0);
const { data: chatMessages, isFetching, isFetched, fetchNextPage, isFetchingNextPage, isPlaceholderData } = useChatMessages(chat.id); const { deleteChatMessage } = useChat(chat.id);
const { data: chatMessages, isLoading, isFetching, isFetched, fetchNextPage, isFetchingNextPage, isPlaceholderData } = useChatMessages(chat.id);
const formattedChatMessages = chatMessages || []; const formattedChatMessages = chatMessages || [];
const me = useAppSelector(state => state.me); const me = useAppSelector(state => state.me);
@ -94,6 +97,12 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
const initialCount = useMemo(() => formattedChatMessages.length, []); const initialCount = useMemo(() => formattedChatMessages.length, []);
const handleDeleteMessage = useMutation((chatMessageId: string) => deleteChatMessage(chatMessageId), {
onSettled: () => {
queryClient.invalidateQueries(['chats', 'messages', chat.id]);
},
});
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEnd.current?.scrollIntoView(false); messagesEnd.current?.scrollIntoView(false);
}; };
@ -224,12 +233,6 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
</div> </div>
); );
const handleDeleteMessage = (chatId: string, messageId: string) => {
return () => {
dispatch(deleteChatMessage(chatId, messageId));
};
};
const handleReportUser = (userId: string) => { const handleReportUser = (userId: string) => {
return () => { return () => {
dispatch(initReportById(userId)); dispatch(initReportById(userId));
@ -242,7 +245,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
const menu: Menu = [ const menu: Menu = [
{ {
text: intl.formatMessage(messages.delete), text: intl.formatMessage(messages.delete),
action: handleDeleteMessage(chatMessage.chat_id, chatMessage.id), action: () => handleDeleteMessage.mutate(chatMessage.id),
icon: require('@tabler/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
destructive: true, destructive: true,
}, },
@ -257,88 +260,88 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
} }
return ( return (
<Stack <div key={chatMessage.id} className='group'>
key={chatMessage.id} <Stack
space={1} space={1}
className={classNames({
'group max-w-[85%]': true,
'ml-auto': isMyMessage,
})}
>
<HStack
alignItems='center'
justifyContent={isMyMessage ? 'end' : 'start'}
className={classNames({
'opacity-50': chatMessage.pending,
})}
>
{isMyMessage ? (
// <IconButton
// src={require('@tabler/icons/dots.svg')}
// className='hidden group-hover:block text-gray-600 mr-2'
// iconClassName='w-5 h-5'
// />
<div className='hidden group-hover:block mr-2 text-gray-500'>
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title={intl.formatMessage(messages.more)}
/>
</div>
) : null}
<div
title={getFormattedTimestamp(chatMessage)}
className={
classNames({
'text-ellipsis break-words relative rounded-md p-2': true,
'bg-primary-500 text-white mr-2': isMyMessage,
'bg-gray-200 text-gray-900 order-2 ml-2': !isMyMessage,
})
}
ref={setBubbleRef}
tabIndex={0}
>
{maybeRenderMedia(chatMessage)}
<Text size='sm' theme='inherit' dangerouslySetInnerHTML={{ __html: parseContent(chatMessage) }} />
<div className='chat-message__menu'>
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
<div className={classNames({ 'order-1': !isMyMessage })}>
<Avatar src={isMyMessage ? account?.avatar : chat.account.avatar} size={34} />
</div>
</HStack>
<HStack
alignItems='center'
space={2}
className={classNames({ className={classNames({
'ml-auto': isMyMessage, 'ml-auto': isMyMessage,
})} })}
> >
<Text <HStack
theme='muted' alignItems='center'
size='xs' justifyContent={isMyMessage ? 'end' : 'start'}
className={classNames({ className={classNames({
'text-right': isMyMessage, 'opacity-50': chatMessage.pending,
'order-2': !isMyMessage,
})} })}
> >
{intl.formatTime(chatMessage.created_at)} {isMyMessage ? (
</Text> <div className='hidden group-hover:block mr-2 text-gray-500'>
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title={intl.formatMessage(messages.more)}
/>
</div>
) : null}
<div className={classNames({ 'order-1': !isMyMessage })}> <HStack
<div className='w-[34px]' /> alignItems='center'
</div> className='max-w-[85%]'
</HStack> justifyContent={isMyMessage ? 'end' : 'start'}
</Stack> >
<div
title={getFormattedTimestamp(chatMessage)}
className={
classNames({
'text-ellipsis break-words relative rounded-md p-2': true,
'bg-primary-500 text-white mr-2': isMyMessage,
'bg-gray-200 text-gray-900 order-2 ml-2': !isMyMessage,
})
}
ref={setBubbleRef}
tabIndex={0}
>
{maybeRenderMedia(chatMessage)}
<Text size='sm' theme='inherit' dangerouslySetInnerHTML={{ __html: parseContent(chatMessage) }} />
<div className='chat-message__menu'>
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
<div className={classNames({ 'order-1': !isMyMessage })}>
<Avatar src={isMyMessage ? account?.avatar : chat.account.avatar} size={34} />
</div>
</HStack>
</HStack>
<HStack
alignItems='center'
space={2}
className={classNames({
'ml-auto': isMyMessage,
})}
>
<Text
theme='muted'
size='xs'
className={classNames({
'text-right': isMyMessage,
'order-2': !isMyMessage,
})}
>
{intl.formatTime(chatMessage.created_at)}
</Text>
<div className={classNames({ 'order-1': !isMyMessage })}>
<div className='w-[34px]' />
</div>
</HStack>
</Stack>
</div>
); );
}; };
@ -358,18 +361,18 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
}); });
// Stick scrollbar to bottom. // Stick scrollbar to bottom.
// useEffect(() => { useEffect(() => {
// if (isNearBottom()) { if (isNearBottom()) {
// scrollToBottom(); scrollToBottom();
// } }
// // First load. // First load.
// // if (chatMessages.count() !== initialCount) { // if (chatMessages.count() !== initialCount) {
// // setInitialLoad(false); // setInitialLoad(false);
// // setIsLoading(false); // setIsLoading(false);
// // scrollToBottom(); // scrollToBottom();
// // } // }
// }, [formattedChatMessages.length]); }, [formattedChatMessages.length]);
// useEffect(() => { // useEffect(() => {
// scrollToBottom(); // scrollToBottom();
@ -381,9 +384,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
useEffect(() => { useEffect(() => {
// Restore scroll bar position when loading old messages. // Restore scroll bar position when loading old messages.
console.log('hii');
if (!initialLoad) { if (!initialLoad) {
restoreScrollPosition(); restoreScrollPosition();
} }
}, [formattedChatMessages.length, initialLoad]); }, [formattedChatMessages.length, initialLoad]);
@ -398,26 +399,39 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, aut
} }
return ( return (
<div className='h-full flex flex-col space-y-4 px-4 flex-grow overflow-y-scroll' onScroll={handleScroll} ref={node}> {/* style={{ height: autosize ? 'calc(100vh - 16rem)' : undefined }} */} <div className='h-full flex flex-col px-4 flex-grow overflow-y-scroll' onScroll={handleScroll} ref={node}> {/* style={{ height: autosize ? 'calc(100vh - 16rem)' : undefined }} */}
{formattedChatMessages.reduce((acc: any, curr: any, idx: number) => { <div className='flex-grow flex flex-col justify-end space-y-4'>
const lastMessage = formattedChatMessages[idx - 1]; {isLoading ? (
<>
<PlaceholderChat isMyMessage />
<PlaceholderChat />
<PlaceholderChat isMyMessage />
<PlaceholderChat isMyMessage />
<PlaceholderChat />
</>
) : (
formattedChatMessages.reduce((acc: any, curr: any, idx: number) => {
const lastMessage = formattedChatMessages[idx - 1];
if (lastMessage) { if (lastMessage) {
const key = `${curr.id}_divider`; const key = `${curr.id}_divider`;
switch (timeChange(lastMessage, curr)) { switch (timeChange(lastMessage, curr)) {
case 'today': case 'today':
acc.push(renderDivider(key, intl.formatMessage(messages.today))); acc.push(renderDivider(key, intl.formatMessage(messages.today)));
break; break;
case 'date': case 'date':
acc.push(renderDivider(key, intl.formatDate(new Date(curr.created_at), { weekday: 'short', hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric' }))); acc.push(renderDivider(key, intl.formatDate(new Date(curr.created_at), { weekday: 'short', hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric' })));
break; break;
} }
} }
acc.push(renderMessage(curr)); acc.push(renderMessage(curr));
return acc; return acc;
}, [] as React.ReactNode[])} }, [] as React.ReactNode[])
<div style={{ float: 'left', clear: 'both' }} ref={messagesEnd} /> )}
</div>
<div className='float-left clear-both mt-4' style={{ float: 'left', clear: 'both' }} ref={messagesEnd} />
</div> </div>
); );
}; };

View File

@ -1,27 +1,11 @@
import React, { useEffect, useRef } from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
closeChat,
toggleChat,
} from 'soapbox/actions/chats';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
import IconButton from 'soapbox/components/icon_button';
import { Avatar, HStack, Counter, Icon, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge'; import VerificationBadge from 'soapbox/components/verification_badge';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { IChat } from 'soapbox/queries/chats'; import { IChat } from 'soapbox/queries/chats';
import { makeGetChat } from 'soapbox/selectors';
import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn as getDisplayFqn } from 'soapbox/utils/state';
import ChatBox from './chat-box'; import ChatBox from './chat-box';
import ChatPaneHeader from './chat-pane-header'; import ChatPaneHeader from './chat-pane-header';
import { Pane, WindowState } from './ui';
import type { Account as AccountEntity } from 'soapbox/types/entities';
const getChat = makeGetChat();
interface IChatWindow { interface IChatWindow {
chat: IChat chat: IChat
@ -67,23 +51,6 @@ const ChatWindow: React.FC<IChatWindow> = ({ chat, closeChat, closePane }) => {
<ChatBox chat={chat} onSetInputRef={() => null} /> <ChatBox chat={chat} onSetInputRef={() => null} />
</Stack> </Stack>
</> </>
// <Pane windowState={windowState} index={idx}>
// <HStack space={2} className='pane__header'>
// {unreadCount > 0 ? unreadIcon : avatar }
// <button className='pane__title' onClick={handleChatToggle(chat.id)}>
// @{getAcct(account, displayFqn)}
// </button>
// <div className='pane__close'>
// <IconButton src={require('@tabler/icons/x.svg')} title='Close chat' onClick={handleChatClose(chat.id)} />
// </div>
// </HStack>
// <div className='pane__content'>
// <ChatBox
// chatId={chat.id}
//
// />
// </div>
// </Pane>
); );
}; };

View File

@ -1,30 +1,69 @@
import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { randomIntFromInterval, generateText } from '../utils'; import { randomIntFromInterval, generateText } from '../utils';
import PlaceholderAvatar from './placeholder_avatar'; import PlaceholderAvatar from './placeholder_avatar';
import PlaceholderDisplayName from './placeholder_display_name'; import PlaceholderDisplayName from './placeholder_display_name';
/** Fake chat to display while data is loading. */ /** Fake chat to display while data is loading. */
const PlaceholderChat: React.FC = () => { const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) => {
const messageLength = randomIntFromInterval(5, 75); const messageLength = randomIntFromInterval(160, 220);
return ( return (
<div className='chat-list-item chat-list-item--placeholder'> <Stack
<div className='account'> space={1}
<div className='account__wrapper'> className={classNames({
<div className='account__display-name'> 'max-w-[85%] animate-pulse': true,
<div className='account__avatar-wrapper'> 'ml-auto': isMyMessage,
<PlaceholderAvatar size={36} /> })}
</div> >
<PlaceholderDisplayName minLength={3} maxLength={25} /> <HStack
<span className='chat__last-message'> alignItems='center'
{generateText(messageLength)} justifyContent={isMyMessage ? 'end' : 'start'}
</span> >
</div> <div
className={
classNames({
'text-ellipsis break-words relative rounded-md p-2': true,
'mr-2': isMyMessage,
'order-2 ml-2': !isMyMessage,
})
}
>
<div style={{ width: messageLength, height: 20 }} className='rounded-full bg-primary-50 dark:bg-primary-800' />
</div> </div>
</div>
</div> <div className={classNames({ 'order-1': !isMyMessage })}>
<PlaceholderAvatar size={34} />
</div>
</HStack>
<HStack
alignItems='center'
space={2}
className={classNames({
'ml-auto': isMyMessage,
})}
>
<Text
theme='muted'
size='xs'
className={classNames({
'text-right': isMyMessage,
'order-2': !isMyMessage,
})}
>
<div style={{ width: 50, height: 12 }} className='rounded-full bg-primary-50 dark:bg-primary-800' />
</Text>
<div className={classNames({ 'order-1': !isMyMessage })}>
<div className='w-[34px] ml-2' />
</div>
</HStack>
</Stack>
); );
}; };

View File

@ -32,7 +32,7 @@ const reverseOrder = (a: IChat, b: IChat): number => {
const useChatMessages = (chatId: string) => { const useChatMessages = (chatId: string) => {
const api = useApi(); const api = useApi();
const getChatMessages = async (chatId: string, pageParam?: any): Promise<{ result: IChatMessage[], maxId: string, hasMore: boolean }> => { const getChatMessages = async(chatId: string, pageParam?: any): Promise<{ result: IChatMessage[], maxId: string, hasMore: boolean }> => {
const { data, headers } = await api.get(`/api/v1/pleroma/chats/${chatId}/messages`, { const { data, headers } = await api.get(`/api/v1/pleroma/chats/${chatId}/messages`, {
params: { params: {
max_id: pageParam?.maxId, max_id: pageParam?.maxId,
@ -74,7 +74,7 @@ const useChatMessages = (chatId: string) => {
const useChats = () => { const useChats = () => {
const api = useApi(); const api = useApi();
const getChats = async () => { const getChats = async() => {
const { data } = await api.get('/api/v1/pleroma/chats'); const { data } = await api.get('/api/v1/pleroma/chats');
return data; return data;
}; };
@ -97,7 +97,9 @@ const useChat = (chatId: string) => {
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/messages`, { content }); return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/messages`, { content });
}; };
return { createChatMessage, markChatAsRead }; const deleteChatMessage = (chatMessageId: string) => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}`);
return { createChatMessage, markChatAsRead, deleteChatMessage };
}; };
export { useChat, useChats, useChatMessages }; export { useChat, useChats, useChatMessages };