soapbox/app/soapbox/features/chats/components/chat-composer.tsx

219 wiersze
7.3 KiB
TypeScript

import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { unblockAccount } from 'soapbox/actions/accounts';
import { openModal } from 'soapbox/actions/modals';
import { Button, Combobox, ComboboxInput, ComboboxList, ComboboxOption, ComboboxPopover, HStack, IconButton, Stack, Text, Textarea } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
const messages = defineMessages({
placeholder: { id: 'chat.input.placeholder', defaultMessage: 'Type a message' },
send: { id: 'chat.actions.send', defaultMessage: 'Send' },
retry: { id: 'chat.retry', defaultMessage: 'Retry?' },
blocked: { id: 'chat_message_list.blocked', defaultMessage: 'You blocked this user' },
unblock: { id: 'chat_composer.unblock', defaultMessage: 'Unblock' },
unblockMessage: { id: 'chat_settings.unblock.message', defaultMessage: 'Unblocking will allow this profile to direct message you and view your content.' },
unblockHeading: { id: 'chat_settings.unblock.heading', defaultMessage: 'Unblock @{acct}' },
unblockConfirm: { id: 'chat_settings.unblock.confirm', defaultMessage: 'Unblock' },
});
const initialSuggestionState = {
list: [],
tokenStart: 0,
token: '',
};
interface Suggestion {
list: { native: string, colons: string }[],
tokenStart: number,
token: string,
}
interface IChatComposer extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onKeyDown' | 'onChange' | 'disabled'> {
value: string
onSubmit: () => void
errorMessage: string | undefined
}
/** Textarea input for chats. */
const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>(({
onKeyDown,
onChange,
value,
onSubmit,
errorMessage = false,
disabled = false,
}, ref) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { chat } = useChatContext();
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const maxCharacterCount = useAppSelector((state) => state.instance.getIn(['configuration', 'chats', 'max_characters']) as number);
const [suggestions, setSuggestions] = useState<Suggestion>(initialSuggestionState);
const isSuggestionsAvailable = suggestions.list.length > 0;
const isOverCharacterLimit = maxCharacterCount && value?.length > maxCharacterCount;
const isSubmitDisabled = disabled || isOverCharacterLimit || value.length === 0;
const overLimitText = maxCharacterCount ? maxCharacterCount - value?.length : '';
const renderSuggestionValue = (emoji: any) => {
return `${(value).slice(0, suggestions.tokenStart)}${emoji.native} ${(value as string).slice(suggestions.tokenStart + suggestions.token.length)}`;
};
const onSelectComboboxOption = (selection: string) => {
const event = { target: { value: selection } } as React.ChangeEvent<HTMLTextAreaElement>;
if (onChange) {
onChange(event);
setSuggestions(initialSuggestionState);
}
};
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const [tokenStart, token] = textAtCursorMatchesToken(
event.target.value,
event.target.selectionStart,
[':'],
);
if (token && tokenStart) {
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
setSuggestions({
list: results,
token,
tokenStart: tokenStart - 1,
});
} else {
setSuggestions(initialSuggestionState);
}
if (onChange) {
onChange(event);
}
};
const handleKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
if (event.key === 'Enter' && !event.shiftKey && isSuggestionsAvailable) {
return;
}
if (onKeyDown) {
onKeyDown(event);
}
};
const handleUnblockUser = () => {
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.unblockHeading, { acct: chat?.account.acct }),
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(unblockAccount(chat?.account.id as string)),
}));
};
if (isBlocking) {
return (
<div className='mt-auto p-6 shadow-3xl dark:border-t-2 dark:border-solid dark:border-gray-800'>
<Stack space={3} alignItems='center'>
<Text align='center' theme='muted'>
{intl.formatMessage(messages.blocked)}
</Text>
<Button theme='secondary' onClick={handleUnblockUser}>
{intl.formatMessage(messages.unblock)}
</Button>
</Stack>
</div>
);
}
if (isBlocked) {
return null;
}
return (
<div className='mt-auto px-4 shadow-3xl'>
<HStack alignItems='stretch' justifyContent='between' space={4}>
<Stack grow>
<Combobox
aria-labelledby='demo'
onSelect={onSelectComboboxOption}
>
<ComboboxInput
as={Textarea}
autoFocus
ref={ref}
placeholder={intl.formatMessage(messages.placeholder)}
onKeyDown={handleKeyDown}
value={value}
onChange={handleChange}
isResizeable={false}
autoGrow
maxRows={5}
disabled={disabled}
autoComplete='off'
/>
{isSuggestionsAvailable ? (
<ComboboxPopover>
<ComboboxList>
{suggestions.list.map((emojiSuggestion) => (
<ComboboxOption
key={emojiSuggestion.colons}
value={renderSuggestionValue(emojiSuggestion)}
>
<span>{emojiSuggestion.native}</span>
<span className='ml-1'>
{emojiSuggestion.colons}
</span>
</ComboboxOption>
))}
</ComboboxList>
</ComboboxPopover>
) : null}
</Combobox>
</Stack>
<Stack space={2} justifyContent='end' alignItems='center' className='w-10 mb-1.5'>
{isOverCharacterLimit ? (
<Text size='sm' theme='danger'>{overLimitText}</Text>
) : null}
<IconButton
src={require('assets/icons/airplane.svg')}
iconClassName='w-5 h-5'
className='text-primary-500'
disabled={isSubmitDisabled}
onClick={onSubmit}
/>
</Stack>
</HStack>
<HStack alignItems='center' className='h-5' space={1}>
{errorMessage && (
<>
<Text theme='danger' size='xs'>
{errorMessage}
</Text>
<button onClick={onSubmit} className='flex hover:underline'>
<Text theme='primary' size='xs' tag='span'>
{intl.formatMessage(messages.retry)}
</Text>
</button>
</>
)}
</HStack>
</div>
);
});
export default ChatComposer;