import { FC, useCallback, useEffect, useState } from 'react'; import { Container } from 'components/layout'; import { Visible } from 'components'; import { useProfile, useResponsiveLayout } from 'hooks'; import { debounce, omit, uniqueId } from 'lodash'; import { useReactiveVar } from '@apollo/client'; import { editingMessageIdVar, isEditingMessageVar } from 'state-management/chat'; import { RichTextVariant } from 'components/rich-text/Types'; import { removeDraftId, setDraftId } from 'utils/message'; import { metrics } from 'config'; import { Editor } from 'components/editor'; import { EditorState } from 'lexical'; import { parseLexicalContentToLocal } from 'utils/richText'; import { CreateDraftMessageMutation, MessageFragment, MessageTypeEnum, RichTextContentSegmentString, RichTextContentSegmentTypeEnum, SendMessageMutation, useCreateDraftMessageMutation, useSendMessageMutation, useUpdateMessageMutation, MessageStatusEnum, RichTextContentSegmentStringInput, MultipleMessagesDocument, Block, User, RichTextContentSegment, } from 'expo-schema'; import { ReferenceMessage } from '../rich-text/reference-message'; const { LIMITS } = metrics; const { INPUT_DEBOUNCE } = metrics.RICH_TEXT_EDITOR; export const ChatInput: FC = ({ roomId, message }) => { const { refetchDrafts, drafts, removeDraft, profile } = useProfile(); const { isSmall } = useResponsiveLayout(); const [draft, setDraft] = useState(); const isEditingMessage = useReactiveVar(isEditingMessageVar); const editingMessageId = useReactiveVar(editingMessageIdVar); const draftId = draft?.id; const [richTextContent, setRichTextContent] = useState({ id: uniqueId(), strings: [], }); useEffect(() => { refetchDrafts(); }, [refetchDrafts]); const handleDrafts = useCallback(async () => { const foundDraft = drafts.find((draftItem) => draftItem.room?.id === roomId); if (foundDraft) { const cleanedDraft = omit(foundDraft, '__typename'); setDraft(cleanedDraft); await setDraftId(cleanedDraft.id); } else { setDraft(undefined); await removeDraftId(); } }, [drafts, roomId]); useEffect(() => { handleDrafts(); }, [drafts, handleDrafts, profile, roomId]); useEffect(() => { const contents = draft?.richTextContent; const isDraftContent = contents && contents?.length > 0; const isAlreadyContent = richTextContent.strings.length > 0; const isRichTextContentValid = isDraftContent && !isAlreadyContent; if (isRichTextContentValid) { if (contents) { const strings = contents[0].strings as RichTextContentSegmentString[]; setRichTextContent({ id: draft?.id as string, strings, }); } } }, [draft, richTextContent.strings.length]); useEffect(() => { // This will only execute when draft have been remove from backend and on reload we need to remove from input if (!draft) { setRichTextContent({ id: uniqueId(), strings: [], }); } }, [draft]); const getParsedStrings = useCallback( (preParsedStrings: RichTextContentSegmentString[]): RichTextContentSegmentStringInput[] => { return preParsedStrings.map(({ id, style, __typename, ...string }, index) => { const innerStyle = omit(style || {}, '__typename'); return { value: string.value, onNewLine: string.onNewLine, style: innerStyle, order: index, }; }); }, [] ); const getParsedStringsForOptimistic = useCallback( (preParsedStrings: RichTextContentSegmentString[]): RichTextContentSegmentString[] => { return preParsedStrings.map(({ id, style, __typename, ...string }, index) => { return { __typename: 'RichTextContentSegmentString', id: uniqueId(), mentionedBlock: { id: '' } as Block, mentionedUser: { id: '', name: { first: '', last: '' }, profilePhotoUrl: '' } as User, mentionedDate: new Date(), mentionedUserId: '', value: string.value, onNewLine: string.onNewLine || false, linkUrl: string.linkUrl || '', style: style, order: index, }; }); }, [] ); const createDraftComplete = useCallback( async (draftResponse: CreateDraftMessageMutation) => { refetchDrafts(); const draftRes = draftResponse.createDraftMessage.message; const id = draftResponse.createDraftMessage.message.id; setDraft(draftRes); await setDraftId(id); }, [refetchDrafts] ); const sendMessageComplete = useCallback( (response: SendMessageMutation) => { if (response.sendMessage.message.id) { setDraft(undefined); removeDraft(response.sendMessage.message.id); setRichTextContent({ id: '', strings: [] }); } else { if (draftId) { removeDraft(draftId); } } }, [draftId, removeDraft] ); const [createDraftMessage] = useCreateDraftMessageMutation({ onCompleted: createDraftComplete, }); const [updateMessage] = useUpdateMessageMutation({}); const [sendMessage] = useSendMessageMutation({ onCompleted: sendMessageComplete, update: async (cache, { data }) => { const cacheMessages: any = cache.readQuery({ query: MultipleMessagesDocument, optimistic: true, variables: { input: { status: MessageStatusEnum.Sent, roomId, limit: LIMITS.messages, }, }, }); const newMessage = { ...data?.sendMessage.message, avAssets: [], replyCount: 0, ref: '', tags: [], userName: profile?.name, userProfilePhotoUrl: profile?.profilePhotoUrl, keyValueContent: [], block: { id: '' }, room: { id: roomId }, modifiedAt: new Date(), resolved: true, parent: { id: '', type: '' }, reactionEmoji: '', AvAsset: {}, }; const multipleMessages = [...(cacheMessages?.multipleMessages || []), newMessage]; cache.writeQuery({ query: MultipleMessagesDocument, variables: { input: { status: MessageStatusEnum.Sent, roomId, limit: LIMITS.messages, }, }, data: { multipleMessages, }, }); }, }); const doCreateDraftMessage = useCallback( (strings: RichTextContentSegmentString[]) => { createDraftMessage({ variables: { input: { type: MessageTypeEnum.Text, richTextContent: [ { type: RichTextContentSegmentTypeEnum.Paragraph, strings: getParsedStrings(strings), order: 1, }, ], roomId: roomId, }, }, }); }, [createDraftMessage, getParsedStrings, roomId] ); const doUpdateMessage = useCallback( (strings: RichTextContentSegmentString[]) => { updateMessage({ variables: { input: { id: draftId as string, richTextContent: [ { type: RichTextContentSegmentTypeEnum.Paragraph, strings: getParsedStrings(strings), order: 1, }, ], }, }, }); }, [draftId, getParsedStrings, updateMessage] ); const clearMessageEditing = useCallback(() => { editingMessageIdVar(''); isEditingMessageVar(false); }, []); const doSendMessage = useCallback( (editorState?: EditorState) => { if (isEditingMessage && editorState) { const strings = parseLexicalContentToLocal(editorState); updateMessage({ variables: { input: { id: editingMessageId, richTextContent: [ { type: RichTextContentSegmentTypeEnum.Paragraph, strings: getParsedStrings(strings), order: 1, }, ], }, }, }); clearMessageEditing(); } else { const isValidMessage = richTextContent.strings.length > 0; if (isValidMessage) { if (draftId) { sendMessage({ variables: { id: draftId as string, }, optimisticResponse: { sendMessage: { message: { __typename: 'Message', id: uniqueId(), sentAt: new Date(), createdAt: new Date(), userName: { first: profile?.name.first as string, last: profile?.name.last as string, }, type: MessageTypeEnum.Text, userProfilePhotoUrl: profile?.profilePhotoUrl, status: MessageStatusEnum.Sent, richTextContent: [ { __typename: 'RichTextContentSegment', id: uniqueId(), type: RichTextContentSegmentTypeEnum.Paragraph, strings: getParsedStringsForOptimistic( richTextContent.strings ) as RichTextContentSegmentString[], order: 1, }, ], }, }, }, }); } else { // TODO: need to handle at backend doCreateDraftMessage(richTextContent.strings); } } } }, [ clearMessageEditing, doCreateDraftMessage, draftId, editingMessageId, getParsedStrings, getParsedStringsForOptimistic, isEditingMessage, profile?.name.first, profile?.name.last, profile?.profilePhotoUrl, richTextContent.strings, sendMessage, updateMessage, ] ); const handleRichTextContent = useCallback( (input: RichTextContentSegment) => { setRichTextContent(input); const isValidMessage = input.strings.length > 0; const isTextRemoved = !isValidMessage && richTextContent.strings.length > input.strings.length; if (draftId) { if (isValidMessage || isTextRemoved) { doUpdateMessage(input.strings); } if (isTextRemoved) { setDraft(undefined); } } else { if (isValidMessage) { doCreateDraftMessage(input.strings); } } }, [doCreateDraftMessage, doUpdateMessage, draftId, richTextContent.strings.length] ); const handleChange = (editorState: EditorState) => { const strings = parseLexicalContentToLocal(editorState); if (strings.length > 0) { handleRichTextContent({ strings, id: uniqueId(), type: RichTextContentSegmentTypeEnum.Paragraph, order: 1, }); } }; // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedChangeHandler = useCallback(debounce(handleChange, INPUT_DEBOUNCE), [ INPUT_DEBOUNCE, handleChange, debounce, ]); const borderRadius = isSmall ? 0 : 10; const borderWidth = isSmall ? 0 : 1; return ( ); }; export interface ChatInputProps { variant: RichTextVariant; message?: MessageFragment; roomId: string; } interface RichTextContentProps { id: string; strings: RichTextContentSegmentString[]; }