Refactor and auto-accept chats

alex-chats
Justin 2 years ago
parent ea5525d02c
commit d2d64e4ec4

@ -1,22 +1,39 @@
import React, { createContext, useContext, useState } from 'react';
import { useDispatch } from 'react-redux';
import { toggleMainWindow } from 'soapbox/actions/chats';
import { useSettings } from 'soapbox/hooks';
import type { IChat } from 'soapbox/queries/chats';
type WindowState = 'open' | 'minimized';
const ChatContext = createContext<any>({
chat: null,
isOpen: false,
});
const ChatProvider: React.FC = ({ children }) => {
const [chat, setChat] = useState<IChat>();
const dispatch = useDispatch();
const settings = useSettings();
const [chat, setChat] = useState<IChat | null>();
const mainWindowState = settings.getIn(['chats', 'mainWindow']) as WindowState;
const isOpen = mainWindowState === 'open';
const toggleChatPane = () => dispatch(toggleMainWindow());
return (
<ChatContext.Provider value={{ chat, setChat }}>{children}</ChatContext.Provider>
<ChatContext.Provider value={{ chat, setChat, isOpen, toggleChatPane }}>{children}</ChatContext.Provider>
);
};
interface IChatContext {
chat: IChat | null
isOpen: boolean
setChat: React.Dispatch<React.SetStateAction<IChat | null>>
toggleChatPane(): void
}
const useChatContext = (): IChatContext => useContext(ChatContext);

@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useRef, useState } from 'react';
import React, { MutableRefObject, useRef, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import {
@ -31,13 +31,14 @@ interface IChatBox {
chat: IChat,
onSetInputRef: (el: HTMLTextAreaElement) => void,
autosize?: boolean,
inputRef?: MutableRefObject<HTMLTextAreaElement>
}
/**
* Chat UI with just the messages and textarea.
* Reused between floating desktop chats and fullscreen/mobile chats.
*/
const ChatBox: React.FC<IChatBox> = ({ chat, onSetInputRef, autosize }) => {
const ChatBox: React.FC<IChatBox> = ({ chat, onSetInputRef, autosize, inputRef }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const chatMessageIds = useAppSelector(state => state.chat_message_lists.get(chat.id, ImmutableOrderedSet<string>()));
@ -128,6 +129,7 @@ const ChatBox: React.FC<IChatBox> = ({ chat, onSetInputRef, autosize }) => {
};
submitMessage.mutate({ chatId: chat.id, content });
acceptChat.mutate();
}
};
@ -253,7 +255,10 @@ const ChatBox: React.FC<IChatBox> = ({ chat, onSetInputRef, autosize }) => {
<Button
theme='primary'
block
onClick={() => acceptChat.mutate()}
onClick={() => {
acceptChat.mutate();
inputRef?.current?.focus();
}}
disabled={acceptChat.isLoading}
>
Accept
@ -289,6 +294,7 @@ const ChatBox: React.FC<IChatBox> = ({ chat, onSetInputRef, autosize }) => {
<Textarea
rows={1}
autoFocus
ref={inputRef}
placeholder={intl.formatMessage(messages.placeholder)}
onKeyDown={handleKeyDown}
value={content}

@ -8,10 +8,12 @@ interface IChatPaneHeader {
onToggle(): void
title: string | React.ReactNode
unreadCount?: number
secondaryAction?(): void
secondaryActionIcon?: string
}
const ChatPaneHeader = (props: IChatPaneHeader) => {
const { onToggle, isOpen, isToggleable = true, title, unreadCount } = props;
const { onToggle, isOpen, isToggleable = true, title, unreadCount, secondaryAction, secondaryActionIcon } = props;
const ButtonComp = isToggleable ? 'button' : 'div';
const buttonProps: HTMLAttributes<HTMLButtonElement | HTMLDivElement> = {};
@ -42,12 +44,22 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
)}
</ButtonComp>
<HStack space={2} alignItems='center'>
{secondaryAction ? (
<IconButton
onClick={secondaryAction}
src={secondaryActionIcon as string}
iconClassName='w-5 h-5 text-gray-600'
/>
) : null}
<IconButton
onClick={onToggle}
src={isOpen ? require('@tabler/icons/chevron-down.svg') : require('@tabler/icons/chevron-up.svg')}
iconClassName='w-5 h-5 text-gray-600'
/>
</HStack>
</HStack>
);
};

@ -25,7 +25,7 @@ import { Chat } from 'soapbox/types/entities';
import ChatList from './chat-list';
import ChatPaneHeader from './chat-pane-header';
import ChatWindow from './chat-window';
import { Pane, WindowState } from './ui';
import { Pane } from './ui';
const messages = defineMessages({
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Type a name' },
@ -53,7 +53,7 @@ const ChatPane = () => {
const dispatch = useAppDispatch();
const debounce = useDebounce;
const { chat, setChat } = useChatContext();
const { chat, setChat, isOpen, toggleChatPane } = useChatContext();
const [value, setValue] = useState<string>();
const debouncedValue = debounce(value as string, 300);
@ -62,10 +62,8 @@ const ChatPane = () => {
const { data: accounts } = useAccountSearch(debouncedValue);
const panes = useAppSelector((state) => normalizeChatPanes(state));
const mainWindowState = useSettings().getIn(['chats', 'mainWindow']) as WindowState;
const unreadCount = sumBy(chats, (chat) => chat.unread);
const open = mainWindowState === 'open';
const isSearching = accounts && accounts.length > 0;
const hasSearchValue = value && value.length > 0;
@ -88,13 +86,6 @@ const ChatPane = () => {
}
};
const handleMainWindowToggle = () => {
if (mainWindowState === 'open') {
setChat(null);
}
dispatch(toggleMainWindow());
};
const renderBody = () => {
if (isFetching) {
return (
@ -163,14 +154,14 @@ const ChatPane = () => {
return (
<div>
<Pane windowState={mainWindowState} index={0} main>
<Pane isOpen={isOpen} index={0} main>
{chat?.id ? (
<ChatWindow chat={chat} closeChat={() => setChat(null)} closePane={handleMainWindowToggle} />
<ChatWindow />
) : (
<>
<ChatPaneHeader title='Messages' unreadCount={unreadCount} isOpen={open} onToggle={handleMainWindowToggle} />
<ChatPaneHeader title='Messages' unreadCount={unreadCount} isOpen={isOpen} onToggle={toggleChatPane} />
{open ? (
{isOpen ? (
<Stack space={4} className='flex-grow h-full'>
<div className='px-4'>
<Input

@ -1,20 +1,24 @@
import React from 'react';
import React, { useRef } from 'react';
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge';
import { IChat } from 'soapbox/queries/chats';
import { useChatContext } from 'soapbox/contexts/chat-context';
import ChatBox from './chat-box';
import ChatPaneHeader from './chat-pane-header';
interface IChatWindow {
chat: IChat
closeChat(): void
closePane(): void
}
/** Floating desktop chat window. */
const ChatWindow: React.FC<IChatWindow> = ({ chat, closeChat, closePane }) => {
const ChatWindow = () => {
const { chat, setChat, isOpen, toggleChatPane } = useChatContext();
const inputRef = useRef<HTMLTextAreaElement>();
const closeChat = () => setChat(null);
const openAndFocusChat = () => {
toggleChatPane();
inputRef.current?.focus();
};
if (!chat) return null;
return (
@ -22,15 +26,19 @@ const ChatWindow: React.FC<IChatWindow> = ({ chat, closeChat, closePane }) => {
<ChatPaneHeader
title={
<HStack alignItems='center' space={2}>
{isOpen && (
<button onClick={closeChat}>
<Icon
src={require('@tabler/icons/arrow-left.svg')}
className='h-6 w-6 text-gray-600 dark:text-gray-400'
/>
</button>
)}
<HStack alignItems='center' space={3}>
{isOpen && (
<Avatar src={chat.account.avatar} size={40} />
)}
<Stack alignItems='start'>
<div className='flex items-center space-x-1 flex-grow'>
@ -42,13 +50,15 @@ const ChatWindow: React.FC<IChatWindow> = ({ chat, closeChat, closePane }) => {
</HStack>
</HStack>
}
isToggleable={false}
isOpen
onToggle={closePane}
secondaryAction={isOpen ? undefined : openAndFocusChat}
secondaryActionIcon={isOpen ? undefined : require('@tabler/icons/edit.svg')}
isToggleable={!isOpen}
isOpen={isOpen}
onToggle={toggleChatPane}
/>
<Stack className='overflow-hidden flex-grow h-full' space={2}>
<ChatBox chat={chat} onSetInputRef={() => null} />
<ChatBox chat={chat} inputRef={inputRef as any} onSetInputRef={() => null} />
</Stack>
</>
);

@ -1,2 +1 @@
export { Pane } from './pane';
export type { WindowState } from './pane';

@ -1,12 +1,9 @@
import classNames from 'classnames';
import React from 'react';
/** Chat pane state. */
export type WindowState = 'open' | 'minimized';
interface IPane {
/** Whether the pane is open or minimized. */
windowState: WindowState,
isOpen: boolean,
/** Positions the pane on the screen, with 0 at the right. */
index: number,
/** Children to display in the pane. */
@ -16,14 +13,14 @@ interface IPane {
}
/** Chat pane UI component for desktop. */
const Pane: React.FC<IPane> = ({ windowState, index, children, main = false }) => {
const Pane: React.FC<IPane> = ({ isOpen = false, index, children, main = false }) => {
const right = (404 * index) + 20;
return (
<div
className={classNames('flex flex-col shadow-3xl bg-white dark:bg-gray-900 rounded-t-lg fixed bottom-0 right-1 w-96 z-[1000]', {
'h-[550px] max-h-[100vh]': windowState !== 'minimized',
'h-16': windowState === 'minimized',
'h-[550px] max-h-[100vh]': isOpen,
'h-16': !isOpen,
})}
style={{ right: `${right}px` }}
>

@ -3,10 +3,9 @@ import React from 'react';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { randomIntFromInterval, generateText } from '../utils';
import { randomIntFromInterval } from '../utils';
import PlaceholderAvatar from './placeholder_avatar';
import PlaceholderDisplayName from './placeholder_display_name';
/** Fake chat to display while data is loading. */
const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) => {

Loading…
Cancel
Save