Refactor and auto-accept chats
This commit is contained in:
@ -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,11 +44,21 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
|
||||
)}
|
||||
</ButtonComp>
|
||||
|
||||
<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 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}>
|
||||
<button onClick={closeChat}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/arrow-left.svg')}
|
||||
className='h-6 w-6 text-gray-600 dark:text-gray-400'
|
||||
/>
|
||||
</button>
|
||||
{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}>
|
||||
<Avatar src={chat.account.avatar} size={40} />
|
||||
{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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user