diff --git a/src/script/components/ConversationListCell/components/CellDescription/CellDescription.tsx b/src/script/components/ConversationListCell/components/CellDescription/CellDescription.tsx index 95638d9d69c..cc6243b076f 100644 --- a/src/script/components/ConversationListCell/components/CellDescription/CellDescription.tsx +++ b/src/script/components/ConversationListCell/components/CellDescription/CellDescription.tsx @@ -22,7 +22,7 @@ import {useMemo} from 'react'; import cx from 'classnames'; import * as Icon from 'Components/Icon'; -import {DraftState, generateConversationInputStorageKey} from 'Components/InputBar/util/DraftStateUtil'; +import {DraftState, generateConversationInputStorageKey} from 'Components/InputBar/common/draftState/draftState'; import {useLocalStorage} from 'Hooks/useLocalStorage'; import {iconStyle} from './CellDescription.style'; diff --git a/src/script/components/InputBar/components/MessageTimerButton/index.ts b/src/script/components/InputBar/FileInput/FileInput.tsx similarity index 57% rename from src/script/components/InputBar/components/MessageTimerButton/index.ts rename to src/script/components/InputBar/FileInput/FileInput.tsx index 5363ab400f7..bf8225a9cc2 100644 --- a/src/script/components/InputBar/components/MessageTimerButton/index.ts +++ b/src/script/components/InputBar/FileInput/FileInput.tsx @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2022 Wire Swiss GmbH + * Copyright (C) 2024 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,4 +17,18 @@ * */ -export * from './MessageTimerButton'; +import {PastedFileControls} from '../PastedFileControls/PastedFileControls'; + +interface FileInputProps { + pastedFile: File | null; + onClearPastedFile: () => void; + onSendPastedFile: () => void; +} + +export const FileInput = ({pastedFile, onClearPastedFile, onSendPastedFile}: FileInputProps) => { + if (!pastedFile) { + return null; + } + + return ; +}; diff --git a/src/script/components/InputBar/InputBar.tsx b/src/script/components/InputBar/InputBar.tsx index 4d6a013d48b..fb9c9ea8976 100644 --- a/src/script/components/InputBar/InputBar.tsx +++ b/src/script/components/InputBar/InputBar.tsx @@ -17,62 +17,48 @@ * */ -import {useCallback, useEffect, useRef, useState} from 'react'; +import {useCallback, useRef, useState} from 'react'; -import {$convertToMarkdownString} from '@lexical/markdown'; import {amplify} from 'amplify'; import cx from 'classnames'; -import {LexicalEditor, $createTextNode, $insertNodes, CLEAR_EDITOR_COMMAND} from 'lexical'; +import {LexicalEditor, $createTextNode, $insertNodes} from 'lexical'; import {container} from 'tsyringe'; import {WebAppEvents} from '@wireapp/webapp-events'; import {Avatar, AVATAR_SIZE} from 'Components/Avatar'; import {ConversationClassifiedBar} from 'Components/ClassifiedBar/ClassifiedBar'; -import {checkFileSharingPermission} from 'Components/Conversation/utils/checkFileSharingPermission'; import {EmojiPicker} from 'Components/EmojiPicker/EmojiPicker'; -import {markdownTransformers} from 'Components/InputBar/components/RichTextEditor/utils/markdownTransformers'; -import {transformMessage} from 'Components/InputBar/components/RichTextEditor/utils/transformMessage'; -import {PrimaryModal} from 'Components/Modals/PrimaryModal'; -import {showWarningModal} from 'Components/Modals/utils/showWarningModal'; -import {ConversationRepository} from 'src/script/conversation/ConversationRepository'; import {useUserPropertyValue} from 'src/script/hooks/useUserProperty'; -import {PropertiesRepository} from 'src/script/properties/PropertiesRepository'; import {PROPERTIES_TYPE} from 'src/script/properties/PropertiesType'; import {EventName} from 'src/script/tracking/EventName'; import {CONVERSATION_TYPING_INDICATOR_MODE} from 'src/script/user/TypingIndicatorMode'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; -import {KEY} from 'Util/KeyboardUtil'; import {t} from 'Util/LocalizerUtil'; -import {sanitizeMarkdown} from 'Util/MarkdownUtil'; -import {formatLocale, TIME_IN_MILLIS} from 'Util/TimeUtil'; -import {getFileExtension} from 'Util/util'; - -import {ControlButtons} from './components/InputBarControls/ControlButtons'; -import {PastedFileControls} from './components/PastedFileControls/PastedFileControls'; -import {ReplyBar} from './components/ReplyBar/ReplyBar'; -import {RichTextContent, RichTextEditor} from './components/RichTextEditor'; -import {SendMessageButton} from './components/RichTextEditor/components/SendMessageButton'; -import {TypingIndicator} from './components/TypingIndicator/TypingIndicator'; -import {useEmojiPicker} from './hooks/useEmojiPicker/useEmojiPicker'; -import {useFilePaste} from './hooks/useFilePaste/useFilePaste'; -import {useFormatToolbar} from './hooks/useFormatToolbar/useFormatToolbar'; -import {useTypingIndicator} from './hooks/useTypingIndicator/useTypingIndicator'; -import {handleClickOutsideOfInputBar, IgnoreOutsideClickWrapper} from './util/clickHandlers'; -import {loadDraftState, saveDraftState} from './util/DraftStateUtil'; +import {TIME_IN_MILLIS} from 'Util/TimeUtil'; + +import {MessageContent} from './common/messageContent/messageContent'; +import {InputBarContainer} from './InputBarContainer/InputBarContainer'; +import {InputBarControls} from './InputBarControls/InputBarControls'; +import {InputBarEditor} from './InputBarEditor/InputBarEditor'; +import {PastedFileControls} from './PastedFileControls/PastedFileControls'; +import {ReplyBar} from './ReplyBar/ReplyBar'; +import {TypingIndicator} from './TypingIndicator'; +import {useEmojiPicker} from './useEmojiPicker/useEmojiPicker'; +import {useFileHandling} from './useFileHandling/useFileHandling'; +import {useFormatToolbar} from './useFormatToolbar/useFormatToolbar'; +import {useGiphy} from './useGiphy/useGiphy'; +import {useMessageHandling} from './useMessageHandling/useMessageHandling'; +import {usePing} from './usePing/usePing'; +import {useTypingIndicator} from './useTypingIndicator/useTypingIndicator'; import {Config} from '../../Config'; -import {ConversationVerificationState} from '../../conversation/ConversationVerificationState'; -import {MessageRepository, OutgoingQuote} from '../../conversation/MessageRepository'; +import {ConversationRepository} from '../../conversation/ConversationRepository'; +import {MessageRepository} from '../../conversation/MessageRepository'; import {Conversation} from '../../entity/Conversation'; -import {ContentMessage} from '../../entity/message/ContentMessage'; import {User} from '../../entity/User'; -import {ConversationError} from '../../error/ConversationError'; import {EventRepository} from '../../event/EventRepository'; -import {MentionEntity} from '../../message/MentionEntity'; -import {MessageHasher} from '../../message/MessageHasher'; -import {QuoteEntity} from '../../message/QuoteEntity'; -import {useAppMainState} from '../../page/state'; +import {PropertiesRepository} from '../../properties/PropertiesRepository'; import {SearchRepository} from '../../search/SearchRepository'; import {StorageRepository} from '../../storage'; import {TeamState} from '../../team/TeamState'; @@ -100,8 +86,6 @@ interface InputBarProps { uploadFiles: (files: File[]) => void; } -const conversationInputBarClassName = 'conversation-input-bar'; - export const InputBar = ({ conversation, conversationRepository, @@ -138,19 +122,15 @@ export const InputBar = ({ const wrapperRef = useRef(null); - // Lexical const editorRef = useRef(null); - // Typing indicator const {typingIndicatorMode} = useKoSubscribableChildren(propertiesRepository, ['typingIndicatorMode']); const isTypingIndicatorEnabled = typingIndicatorMode === CONVERSATION_TYPING_INDICATOR_MODE.ON; - // Message - /** the messageContent represents the message being edited. It's directly derived from the editor state */ - const [messageContent, setMessageContent] = useState({text: ''}); - const [editedMessage, setEditedMessage] = useState(); - const [replyMessageEntity, setReplyMessageEntity] = useState(null); - const textValue = messageContent.text; + /** The messageContent represents the message being edited. + * It's directly derived from the editor state + */ + const [messageContent, setMessageContent] = useState({text: ''}); const formatToolbar = useFormatToolbar(); @@ -164,46 +144,28 @@ export const InputBar = ({ }, }); - // Files - const [pastedFile, setPastedFile] = useState(null); - - // Common - const [pingDisabled, setIsPingDisabled] = useState(false); - - // Right sidebar - const {rightSidebar} = useAppMainState.getState(); - const lastItem = rightSidebar.history.length - 1; - const currentState = rightSidebar.history[lastItem]; - const isRightSidebarOpen = !!currentState; - const inputPlaceholder = messageTimer ? t('tooltipConversationEphemeral') : t('tooltipConversationInputPlaceholder'); - const isEditing = !!editedMessage; - const isReplying = !!replyMessageEntity; const isConnectionRequest = isOutgoingRequest || isIncomingRequest; const hasLocalEphemeralTimer = isSelfDeletingMessagesEnabled && !!localMessageTimer && !hasGlobalMessageTimer; const isTypingRef = useRef(false); - const isMessageFormatButtonsFlagEnabled = CONFIG.FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS; - - const showGiphyButton = isMessageFormatButtonsFlagEnabled - ? textValue.length > 0 - : textValue.length > 0 && textValue.length <= CONFIG.GIPHY_TEXT_LENGTH; - const shouldReplaceEmoji = useUserPropertyValue( () => propertiesRepository.getPreference(PROPERTIES_TYPE.EMOJI.REPLACE_INLINE), WebAppEvents.PROPERTIES.UPDATE.EMOJI.REPLACE_INLINE, ); - // Mentions - const getMentionCandidates = (search?: string | null) => { - const candidates = conversation.participating_user_ets().filter(userEntity => !userEntity.isService); - return typeof search === 'string' ? searchRepository.searchUserInSet(search, candidates) : candidates; - }; + const getMentionCandidates = useCallback( + (search?: string | null) => { + const candidates = conversation.participating_user_ets().filter(userEntity => !userEntity.isService); + return typeof search === 'string' ? searchRepository.searchUserInSet(search, candidates) : candidates; + }, + [conversation, searchRepository], + ); useTypingIndicator({ isEnabled: isTypingIndicatorEnabled, - text: textValue, + text: messageContent.text, onTypingChange: useCallback( isTyping => { isTypingRef.current = isTyping; @@ -217,406 +179,74 @@ export const InputBar = ({ ), }); - const handleSaveEditorDraft = (replyId = '') => { - const editor = editorRef.current; - - if (!editor) { - return; - } - - editor.getEditorState().read(() => { - const markdown = $convertToMarkdownString(markdownTransformers, undefined, true); - void saveDraft( - JSON.stringify(editor.getEditorState().toJSON()), - transformMessage({replaceEmojis: shouldReplaceEmoji, markdown}), - replyId, - ); - }); - }; - - const resetDraftState = () => { - setReplyMessageEntity(null); - editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); - }; - - const clearPastedFile = () => setPastedFile(null); - - const sendPastedFile = () => { - if (pastedFile) { - uploadDroppedFiles([pastedFile]); - clearPastedFile(); - } - }; - - const cancelMessageReply = (resetDraft = true) => { - setReplyMessageEntity(null); - handleSaveEditorDraft(); - - if (resetDraft) { - resetDraftState(); - } - }; - - useEffect(() => { - amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.EDIT, (messageEntity: ContentMessage) => { - editMessage(messageEntity); - }); - - return () => { - amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.EDIT); - }; + const fileHandling = useFileHandling({ + uploadDroppedFiles, + uploadImages, }); - const cancelMessageEditing = (resetDraft = true) => { - setEditedMessage(undefined); - setReplyMessageEntity(null); - - if (resetDraft) { - resetDraftState(); - } - }; - - const editMessage = (messageEntity?: ContentMessage) => { - if (messageEntity?.isEditable() && messageEntity !== editedMessage) { - cancelMessageReply(); - cancelMessageEditing(true); - setEditedMessage(messageEntity); - - const quote = messageEntity.quote(); - if (quote && conversation) { - void messageRepository - .getMessageInConversationById(conversation, quote.messageId) - .then(quotedMessage => setReplyMessageEntity(quotedMessage)); - } - } - }; - - const replyMessage = (messageEntity: ContentMessage): void => { - if (messageEntity?.isReplyable() && messageEntity !== replyMessageEntity) { - cancelMessageReply(false); - cancelMessageEditing(!!editedMessage); - setReplyMessageEntity(messageEntity); - handleSaveEditorDraft(messageEntity.id); - - editorRef.current?.focus(); - } - }; - - const generateQuote = (): Promise => { - return !replyMessageEntity - ? Promise.resolve(undefined) - : eventRepository.eventService - .loadEvent(replyMessageEntity.conversation_id, replyMessageEntity.id) - .then(MessageHasher.hashEvent) - .then((messageHash: ArrayBuffer) => { - return new QuoteEntity({ - hash: messageHash, - messageId: replyMessageEntity.id, - userId: replyMessageEntity.from, - }) as OutgoingQuote; - }); - }; - - const sendMessageEdit = (messageText: string, mentions: MentionEntity[]): void | Promise => { - const mentionEntities = mentions.slice(0); - cancelMessageEditing(true); - - if (!messageText.length && editedMessage) { - return messageRepository.deleteMessageForEveryone(conversation, editedMessage); - } - - if (editedMessage) { - messageRepository.sendMessageEdit(conversation, messageText, editedMessage, mentionEntities).catch(error => { - if (error.type !== ConversationError.TYPE.NO_MESSAGE_CHANGES) { - throw error; - } - }); - - cancelMessageReply(); - } - }; - - const sendTextMessage = (messageText: string, mentions: MentionEntity[]) => { - if (messageText.length) { - const mentionEntities = mentions.slice(0); - - void generateQuote().then(quoteEntity => { - void messageRepository.sendTextWithLinkPreview(conversation, messageText, mentionEntities, quoteEntity); - cancelMessageReply(); - }); - } - }; - - const sendMessage = (): void => { - if (pastedFile) { - return void sendPastedFile(); - } - - const messageTrimmedStart = textValue.trimStart(); - const text = messageTrimmedStart.trimEnd(); - const isMessageTextTooLong = text.length > CONFIG.MAXIMUM_MESSAGE_LENGTH; - const mentions = messageContent.mentions ?? []; - - if (isMessageTextTooLong) { - showWarningModal( - t('modalConversationMessageTooLongHeadline'), - t('modalConversationMessageTooLongMessage', {number: CONFIG.MAXIMUM_MESSAGE_LENGTH}), - ); - - return; - } - - if (isEditing) { - void sendMessageEdit(text, mentions); - } else { - sendTextMessage(text, mentions); - } - - editorRef.current?.focus(); - resetDraftState(); - }; - - const handleSendMessage = async () => { - await conversationRepository.refreshMLSConversationVerificationState(conversation); - const isE2EIDegraded = conversation.mlsVerificationState() === ConversationVerificationState.DEGRADED; - - if (isE2EIDegraded) { - PrimaryModal.show(PrimaryModal.type.CONFIRM, { - secondaryAction: { - action: () => { - conversation.mlsVerificationState(ConversationVerificationState.UNVERIFIED); - sendMessage(); - }, - text: t('conversation.E2EISendAnyway'), - }, - primaryAction: { - action: () => {}, - text: t('conversation.E2EICancel'), - }, - text: { - message: t('conversation.E2EIDegradedNewMessage'), - title: t('conversation.E2EIConversationNoLongerVerified'), - }, - }); - } else { - sendMessage(); - } - }; - - const onGifClick = () => openGiphy(textValue); - - const pingConversation = () => { - setIsPingDisabled(true); - void messageRepository.sendPing(conversation).then(() => { - window.setTimeout(() => setIsPingDisabled(false), CONFIG.PING_TIMEOUT); - }); - }; - - const onPingClick = () => { - if (pingDisabled) { - return; - } - - const totalConversationUsers = conversation.participating_user_ets().length; - if ( - !CONFIG.FEATURE.ENABLE_PING_CONFIRMATION || - is1to1 || - totalConversationUsers < CONFIG.FEATURE.MAX_USERS_TO_PING_WITHOUT_ALERT - ) { - pingConversation(); - } else { - PrimaryModal.show(PrimaryModal.type.CONFIRM, { - primaryAction: { - action: pingConversation, - text: t('tooltipConversationPing'), - }, - text: { - title: t('conversationPingConfirmTitle', {memberCount: totalConversationUsers.toString()}), - }, - }); - } - }; - - const handlePasteFiles = (files: FileList): void => { - const [pastedFile] = files; - - if (!pastedFile) { - return; - } - const {lastModified} = pastedFile; - - const date = formatLocale(lastModified || new Date(), 'PP, pp'); - const fileName = `${t('conversationSendPastedFile', {date})}.${getFileExtension(pastedFile.name)}`; - - const newFile = new File([pastedFile], fileName, { - type: pastedFile.type, - }); - - setPastedFile(newFile); - }; - - const sendGiphy = (gifUrl: string, tag: string): void => { - void generateQuote().then(quoteEntity => { - void messageRepository.sendGif(conversation, gifUrl, tag, quoteEntity); - cancelMessageEditing(true); - }); - }; - - useEffect(() => { - amplify.subscribe(WebAppEvents.CONVERSATION.IMAGE.SEND, uploadImages); - amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.REPLY, replyMessage); - amplify.subscribe(WebAppEvents.EXTENSIONS.GIPHY.SEND, sendGiphy); - amplify.subscribe(WebAppEvents.SHORTCUT.PING, onPingClick); - conversation.isTextInputReady(true); - - return () => { - amplify.unsubscribeAll(WebAppEvents.SHORTCUT.PING); - amplify.unsubscribeAll(WebAppEvents.CONVERSATION.IMAGE.SEND); - amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.REPLY); - amplify.unsubscribeAll(WebAppEvents.EXTENSIONS.GIPHY.SEND); - conversation.isTextInputReady(false); - }; - }, []); - - const saveDraft = async (editorState: string, plainMessage: string, replyId?: string) => { - await saveDraftState({ - storageRepository, - conversation, - editorState, - plainMessage: sanitizeMarkdown(plainMessage), - replyId: replyId ?? replyMessageEntity?.id, - editedMessageId: editedMessage?.id, - }); - }; - - const loadDraft = async () => { - const draftState = await loadDraftState(conversation, storageRepository, messageRepository); - - const reply = draftState.messageReply; - if (reply?.isReplyable()) { - setReplyMessageEntity(reply); - } - - const editedMessage = draftState.editedMessage; - if (editedMessage) { - setEditedMessage(editedMessage); - } - - return draftState; - }; - - const handleRepliedMessageDeleted = (messageId: string) => { - if (replyMessageEntity?.id === messageId) { - setReplyMessageEntity(null); - } - }; - - const handleRepliedMessageUpdated = (originalMessageId: string, messageEntity: ContentMessage) => { - if (replyMessageEntity?.id === originalMessageId) { - setReplyMessageEntity(messageEntity); - } - }; - - useEffect(() => { - amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.REMOVED, handleRepliedMessageDeleted); - amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.UPDATED, handleRepliedMessageUpdated); - - return () => { - amplify.unsubscribe(WebAppEvents.CONVERSATION.MESSAGE.REMOVED, handleRepliedMessageDeleted); - amplify.unsubscribe(WebAppEvents.CONVERSATION.MESSAGE.UPDATED, handleRepliedMessageUpdated); - }; - }, [replyMessageEntity]); - - useEffect(() => { - const onWindowClick = (event: Event): void => - handleClickOutsideOfInputBar(event, () => { - // We want to add a timeout in case the click happens because the user switched conversation and the component is unmounting. - // In this case we want to keep the edited message for this conversation - setTimeout(() => { - cancelMessageEditing(true); - cancelMessageReply(); - }); - }); - if (isEditing) { - window.addEventListener('click', onWindowClick); - - return () => { - window.removeEventListener('click', onWindowClick); - }; - } - - return () => undefined; - }, [cancelMessageEditing, cancelMessageReply, isEditing]); - - useFilePaste(checkFileSharingPermission(handlePasteFiles)); - - const sendImageOnEnterClick = (event: KeyboardEvent) => { - if (event.key === KEY.ENTER && !event.shiftKey && !event.altKey && !event.metaKey) { - sendPastedFile(); - } - }; - - useEffect(() => { - if (!pastedFile) { - return () => undefined; - } - - window.addEventListener('keydown', sendImageOnEnterClick); - - return () => { - window.removeEventListener('keydown', sendImageOnEnterClick); - }; - }, [pastedFile]); - const showMarkdownPreview = useUserPropertyValue( () => propertiesRepository.getPreference(PROPERTIES_TYPE.INTERFACE.MARKDOWN_PREVIEW), WebAppEvents.PROPERTIES.UPDATE.INTERFACE.MARKDOWN_PREVIEW, ); - const controlButtonsProps = { - conversation: conversation, - disableFilesharing: !isFileSharingSendingEnabled, - disablePing: pingDisabled, - input: textValue, - isEditing: isEditing, - onCancelEditing: () => cancelMessageEditing(true), - onClickPing: onPingClick, - onGifClick: onGifClick, - onSelectFiles: uploadFiles, - onSelectImages: uploadImages, - showGiphyButton: showGiphyButton, - showFormatButton: isMessageFormatButtonsFlagEnabled && showMarkdownPreview, - showEmojiButton: isMessageFormatButtonsFlagEnabled, - isFormatActive: formatToolbar.open, - onFormatClick: formatToolbar.handleClick, - isEmojiActive: emojiPicker.open, - onEmojiClick: emojiPicker.handleToggle, - }; + const { + editedMessage, + replyMessageEntity, + isEditing, + isReplying, + sendMessage, + cancelSending, + cancelMesssageEditing, + cancelMessageReply, + editMessage, + draftState, + generateQuote, + } = useMessageHandling({ + messageContent, + conversation, + conversationRepository, + storageRepository, + eventRepository, + messageRepository, + editorRef, + pastedFile: fileHandling.pastedFile, + sendPastedFile: fileHandling.sendPastedFile, + }); - const enableSending = textValue.length > 0; + const ping = usePing({ + conversation, + messageRepository, + is1to1, + }); + + const giphy = useGiphy({ + text: messageContent.text, + maxLength: CONFIG.GIPHY_TEXT_LENGTH, + openGiphy, + generateQuote, + messageRepository, + conversation, + cancelMesssageEditing, + }); - const showAvatar = !!textValue.length; + const showAvatar = !!messageContent.text.length; return (
- + {isTypingIndicatorEnabled && } {classifiedDomains && !isConnectionRequest && ( )} - {isReplying && !isEditing && ( + {isReplying && !isEditing && replyMessageEntity && ( cancelMessageReply(false)} /> )}
@@ -632,57 +262,63 @@ export const InputBar = ({ /> )}
- {!isSelfUserRemoved && !pastedFile && ( - { - editorRef.current = lexical; - }} + {!isSelfUserRemoved && !fileHandling.pastedFile && ( + { - if (editedMessage) { - cancelMessageEditing(true); - } else if (replyMessageEntity) { - cancelMessageReply(); - } + inputPlaceholder={inputPlaceholder} + hasLocalEphemeralTimer={hasLocalEphemeralTimer} + showMarkdownPreview={showMarkdownPreview} + formatToolbar={formatToolbar} + onSetup={editor => { + editorRef.current = editor; }} + onEscape={cancelSending} onArrowUp={() => { - if (textValue.length === 0) { + if (messageContent.text.length === 0) { editMessage(conversation.getLastEditableMessage()); } }} - getMentionCandidates={getMentionCandidates} - replaceEmojis={shouldReplaceEmoji} - placeholder={inputPlaceholder} - onUpdate={setMessageContent} - hasLocalEphemeralTimer={hasLocalEphemeralTimer} - showFormatToolbar={formatToolbar.open} - showMarkdownPreview={showMarkdownPreview} - saveDraftState={saveDraft} - loadDraftState={loadDraft} onShiftTab={onShiftTab} - onSend={handleSendMessage} onBlur={() => isTypingRef.current && conversationRepository.sendTypingStop(conversation)} + onUpdate={setMessageContent} + onSend={sendMessage} + getMentionCandidates={getMentionCandidates} + saveDraftState={draftState.save} + loadDraftState={draftState.load} + replaceEmojis={shouldReplaceEmoji} > -
-
    - -
- -
-
+ + )} )} - {pastedFile && ( - + {fileHandling.pastedFile && ( + )}
- + {emojiPicker.open ? ( { + return ( +
+ +
+ ); +}; diff --git a/src/script/components/InputBar/InputBarContainer/InputBarContainer.tsx b/src/script/components/InputBar/InputBarContainer/InputBarContainer.tsx new file mode 100644 index 00000000000..78462b0a5b2 --- /dev/null +++ b/src/script/components/InputBar/InputBarContainer/InputBarContainer.tsx @@ -0,0 +1,47 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {ReactNode} from 'react'; + +import cx from 'classnames'; + +import {useAppMainState} from 'src/script/page/state'; + +import {IgnoreOutsideClickWrapper} from '../util/clickHandlers'; + +interface InputBarContainerProps { + children: ReactNode; +} + +export const InputBarContainer = ({children}: InputBarContainerProps) => { + const {rightSidebar} = useAppMainState.getState(); + const lastItem = rightSidebar.history.length - 1; + const currentState = rightSidebar.history[lastItem]; + const isRightSidebarOpen = !!currentState; + + return ( + + {children} + + ); +}; diff --git a/src/script/components/InputBar/components/AssetUploadButton/AssetUploadButton.test.tsx b/src/script/components/InputBar/InputBarControls/AssetUploadButton/AssetUploadButton.test.tsx similarity index 97% rename from src/script/components/InputBar/components/AssetUploadButton/AssetUploadButton.test.tsx rename to src/script/components/InputBar/InputBarControls/AssetUploadButton/AssetUploadButton.test.tsx index cdaeaa049ef..49b115f6fb3 100644 --- a/src/script/components/InputBar/components/AssetUploadButton/AssetUploadButton.test.tsx +++ b/src/script/components/InputBar/InputBarControls/AssetUploadButton/AssetUploadButton.test.tsx @@ -19,7 +19,7 @@ import {render, fireEvent} from '@testing-library/react'; -import {AssetUploadButton} from '.'; +import {AssetUploadButton} from './AssetUploadButton'; const pngFile = new File(['(⌐□_□)'], 'chucknorris.png', {type: 'image/png'}); diff --git a/src/script/components/InputBar/components/AssetUploadButton/AssetUploadButton.tsx b/src/script/components/InputBar/InputBarControls/AssetUploadButton/AssetUploadButton.tsx similarity index 100% rename from src/script/components/InputBar/components/AssetUploadButton/AssetUploadButton.tsx rename to src/script/components/InputBar/InputBarControls/AssetUploadButton/AssetUploadButton.tsx diff --git a/src/script/components/InputBar/components/InputBarControls/CancelEditButton/CancelEditButton.tsx b/src/script/components/InputBar/InputBarControls/CancelEditButton/CancelEditButton.tsx similarity index 100% rename from src/script/components/InputBar/components/InputBarControls/CancelEditButton/CancelEditButton.tsx rename to src/script/components/InputBar/InputBarControls/CancelEditButton/CancelEditButton.tsx diff --git a/src/script/components/InputBar/components/InputBarControls/ControlButtons.test.tsx b/src/script/components/InputBar/InputBarControls/ControlButtons.test.tsx similarity index 96% rename from src/script/components/InputBar/components/InputBarControls/ControlButtons.test.tsx rename to src/script/components/InputBar/InputBarControls/ControlButtons.test.tsx index a3398c43e6a..ad5c3dc6269 100644 --- a/src/script/components/InputBar/components/InputBarControls/ControlButtons.test.tsx +++ b/src/script/components/InputBar/InputBarControls/ControlButtons.test.tsx @@ -20,14 +20,14 @@ import {render} from '@testing-library/react'; import {withTheme} from 'src/script/auth/util/test/TestUtil'; +import {Conversation} from 'src/script/entity/Conversation'; import {ControlButtons} from './ControlButtons'; type PropsType = React.ComponentProps; const defaultParams: PropsType = { - conversation: undefined, + conversation: undefined as unknown as Conversation, input: '', - onCancelEditing: jest.fn(), onClickPing: jest.fn(), onGifClick: jest.fn(), diff --git a/src/script/components/InputBar/components/InputBarControls/ControlButtons.tsx b/src/script/components/InputBar/InputBarControls/ControlButtons.tsx similarity index 91% rename from src/script/components/InputBar/components/InputBarControls/ControlButtons.tsx rename to src/script/components/InputBar/InputBarControls/ControlButtons.tsx index 528cc411381..d06d5b424ca 100644 --- a/src/script/components/InputBar/components/InputBarControls/ControlButtons.tsx +++ b/src/script/components/InputBar/InputBarControls/ControlButtons.tsx @@ -17,22 +17,21 @@ * */ -import React, {MouseEvent} from 'react'; +import {MouseEvent} from 'react'; -import {FormatSeparator} from 'Components/InputBar/components/common/FormatSeparator/FormatSeparator'; +import {FormatSeparator} from 'Components/InputBar/common/FormatSeparator/FormatSeparator'; import {Config} from 'src/script/Config'; import {Conversation} from 'src/script/entity/Conversation'; +import {AssetUploadButton} from './AssetUploadButton/AssetUploadButton'; import {CancelEditButton} from './CancelEditButton/CancelEditButton'; import {EmojiButton} from './EmojiButton/EmojiButton'; import {FormatTextButton} from './FormatTextButton/FormatTextButton'; import {GiphyButton} from './GiphyButton/GiphyButton'; +import {ImageUploadButton} from './ImageUploadButton/ImageUploadButton'; +import {MessageTimerButton} from './MessageTimerButton/MessageTimerButton'; import {PingButton} from './PingButton/PingButton'; -import {AssetUploadButton} from '../AssetUploadButton'; -import {ImageUploadButton} from '../ImageUploadButton'; -import {MessageTimerButton} from '../MessageTimerButton'; - export type ControlButtonsProps = { input: string; conversation: Conversation; @@ -53,7 +52,7 @@ export type ControlButtonsProps = { onEmojiClick: (event: MouseEvent) => void; }; -const ControlButtons: React.FC = ({ +const ControlButtons = ({ conversation, disablePing, disableFilesharing, @@ -71,7 +70,7 @@ const ControlButtons: React.FC = ({ onGifClick, onFormatClick, onEmojiClick, -}) => { +}: ControlButtonsProps) => { if (isEditing) { return ( <> diff --git a/src/script/components/InputBar/components/InputBarControls/EmojiButton/EmojiButton.tsx b/src/script/components/InputBar/InputBarControls/EmojiButton/EmojiButton.tsx similarity index 100% rename from src/script/components/InputBar/components/InputBarControls/EmojiButton/EmojiButton.tsx rename to src/script/components/InputBar/InputBarControls/EmojiButton/EmojiButton.tsx diff --git a/src/script/components/InputBar/components/InputBarControls/FormatTextButton/FormatTextButton.tsx b/src/script/components/InputBar/InputBarControls/FormatTextButton/FormatTextButton.tsx similarity index 100% rename from src/script/components/InputBar/components/InputBarControls/FormatTextButton/FormatTextButton.tsx rename to src/script/components/InputBar/InputBarControls/FormatTextButton/FormatTextButton.tsx diff --git a/src/script/components/InputBar/components/InputBarControls/GiphyButton/GiphyButton.tsx b/src/script/components/InputBar/InputBarControls/GiphyButton/GiphyButton.tsx similarity index 100% rename from src/script/components/InputBar/components/InputBarControls/GiphyButton/GiphyButton.tsx rename to src/script/components/InputBar/InputBarControls/GiphyButton/GiphyButton.tsx diff --git a/src/script/components/InputBar/components/ImageUploadButton/ImageUploadButton.test.tsx b/src/script/components/InputBar/InputBarControls/ImageUploadButton/ImageUploadButton.test.tsx similarity index 97% rename from src/script/components/InputBar/components/ImageUploadButton/ImageUploadButton.test.tsx rename to src/script/components/InputBar/InputBarControls/ImageUploadButton/ImageUploadButton.test.tsx index 76a73835919..d0ca99e55f2 100644 --- a/src/script/components/InputBar/components/ImageUploadButton/ImageUploadButton.test.tsx +++ b/src/script/components/InputBar/InputBarControls/ImageUploadButton/ImageUploadButton.test.tsx @@ -19,7 +19,7 @@ import {render, fireEvent} from '@testing-library/react'; -import {ImageUploadButton} from '.'; +import {ImageUploadButton} from './ImageUploadButton'; const ALLOWED_IMAGE_TYPES = ['image/gif', 'image/avif']; diff --git a/src/script/components/InputBar/components/ImageUploadButton/ImageUploadButton.tsx b/src/script/components/InputBar/InputBarControls/ImageUploadButton/ImageUploadButton.tsx similarity index 100% rename from src/script/components/InputBar/components/ImageUploadButton/ImageUploadButton.tsx rename to src/script/components/InputBar/InputBarControls/ImageUploadButton/ImageUploadButton.tsx diff --git a/src/script/components/InputBar/InputBarControls/InputBarControls.tsx b/src/script/components/InputBar/InputBarControls/InputBarControls.tsx new file mode 100644 index 00000000000..9c8ed2a54a5 --- /dev/null +++ b/src/script/components/InputBar/InputBarControls/InputBarControls.tsx @@ -0,0 +1,113 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useEffect} from 'react'; + +import {amplify} from 'amplify'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {Config} from 'src/script/Config'; +import {Conversation} from 'src/script/entity/Conversation'; + +import {ControlButtons} from './ControlButtons'; +import {SendMessageButton} from './SendMessageButton/SendMessageButton'; + +import {MessageContent} from '../common/messageContent/messageContent'; + +interface InputBarControlsProps { + conversation: Conversation; + isFileSharingSendingEnabled: boolean; + pingDisabled: boolean; + messageContent: MessageContent; + isEditing: boolean; + showMarkdownPreview: boolean; + showGiphyButton: boolean; + formatToolbar: { + open: boolean; + handleClick: () => void; + }; + emojiPicker: { + open: boolean; + handleToggle: (event: React.MouseEvent) => void; + }; + onEscape: () => void; + onClickPing: () => void; + onGifClick: () => void; + onSelectFiles: (files: File[]) => void; + onSelectImages: (files: File[]) => void; + onSend: () => void; +} + +export const InputBarControls = ({ + conversation, + isFileSharingSendingEnabled, + pingDisabled, + messageContent, + isEditing, + showMarkdownPreview, + showGiphyButton, + formatToolbar, + emojiPicker, + onEscape, + onClickPing, + onGifClick, + onSelectFiles, + onSelectImages, + onSend, +}: InputBarControlsProps) => { + const enableSending = messageContent.text.length > 0; + + const isMessageFormatButtonsFlagEnabled = Config.getConfig().FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS; + + useEffect(() => { + amplify.subscribe(WebAppEvents.SHORTCUT.PING, onClickPing); + + return () => { + amplify.unsubscribeAll(WebAppEvents.SHORTCUT.PING); + }; + }, [onClickPing]); + + return ( +
+
    + +
+ +
+ ); +}; diff --git a/src/script/components/InputBar/components/MessageTimerButton/MessageTimerButton.test.tsx b/src/script/components/InputBar/InputBarControls/MessageTimerButton/MessageTimerButton.test.tsx similarity index 100% rename from src/script/components/InputBar/components/MessageTimerButton/MessageTimerButton.test.tsx rename to src/script/components/InputBar/InputBarControls/MessageTimerButton/MessageTimerButton.test.tsx diff --git a/src/script/components/InputBar/components/MessageTimerButton/MessageTimerButton.tsx b/src/script/components/InputBar/InputBarControls/MessageTimerButton/MessageTimerButton.tsx similarity index 100% rename from src/script/components/InputBar/components/MessageTimerButton/MessageTimerButton.tsx rename to src/script/components/InputBar/InputBarControls/MessageTimerButton/MessageTimerButton.tsx diff --git a/src/script/components/InputBar/components/InputBarControls/PingButton/PingButton.tsx b/src/script/components/InputBar/InputBarControls/PingButton/PingButton.tsx similarity index 100% rename from src/script/components/InputBar/components/InputBarControls/PingButton/PingButton.tsx rename to src/script/components/InputBar/InputBarControls/PingButton/PingButton.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/components/SendMessageButton/SendMessageButton.tsx b/src/script/components/InputBar/InputBarControls/SendMessageButton/SendMessageButton.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/SendMessageButton/SendMessageButton.tsx rename to src/script/components/InputBar/InputBarControls/SendMessageButton/SendMessageButton.tsx diff --git a/src/script/components/InputBar/InputBarEditor/InputBarEditor.tsx b/src/script/components/InputBar/InputBarEditor/InputBarEditor.tsx new file mode 100644 index 00000000000..da551cdf4d9 --- /dev/null +++ b/src/script/components/InputBar/InputBarEditor/InputBarEditor.tsx @@ -0,0 +1,101 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {MutableRefObject} from 'react'; + +import {LexicalEditor} from 'lexical'; + +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; +import {User} from 'src/script/entity/User'; + +import {RichTextEditor} from './RichTextEditor'; + +import {DraftState} from '../common/draftState/draftState'; +import {MessageContent} from '../common/messageContent/messageContent'; + +interface InputBarEditorProps { + editorRef: MutableRefObject; + editedMessage: ContentMessage | undefined; + inputPlaceholder: string; + hasLocalEphemeralTimer: boolean; + showMarkdownPreview: boolean; + formatToolbar: { + open: boolean; + handleClick: () => void; + }; + onSetup: (editor: LexicalEditor) => void; + onEscape: () => void; + onArrowUp: () => void; + onShiftTab: () => void; + onBlur: () => void; + onUpdate: (content: MessageContent) => void; + onSend: () => void; + getMentionCandidates: (search?: string | null) => User[]; + saveDraftState: (editorState: string, plainMessage: string, replyId?: string) => void; + loadDraftState: () => Promise; + replaceEmojis: boolean; + children: React.ReactNode; +} + +export const InputBarEditor = ({ + editorRef, + editedMessage, + inputPlaceholder, + hasLocalEphemeralTimer, + showMarkdownPreview, + formatToolbar, + onSetup, + onEscape, + onArrowUp, + onShiftTab, + onBlur, + onUpdate, + onSend, + getMentionCandidates, + saveDraftState, + loadDraftState, + replaceEmojis, + children, +}: InputBarEditorProps) => { + return ( + { + editorRef.current = editor; + onSetup(editor); + }} + editedMessage={editedMessage} + onEscape={onEscape} + onArrowUp={onArrowUp} + getMentionCandidates={getMentionCandidates} + replaceEmojis={replaceEmojis} + placeholder={inputPlaceholder} + onUpdate={onUpdate} + hasLocalEphemeralTimer={hasLocalEphemeralTimer} + showFormatToolbar={formatToolbar.open} + showMarkdownPreview={showMarkdownPreview} + saveDraftState={saveDraftState} + loadDraftState={loadDraftState} + onShiftTab={onShiftTab} + onSend={onSend} + onBlur={onBlur} + > + {children} + + ); +}; diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatButton/FormatButton.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatButton/FormatButton.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatButton/FormatButton.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatButton/FormatButton.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatToolbar.styles.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatToolbar.styles.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatToolbar.styles.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatToolbar.styles.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatToolbar.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatToolbar.tsx similarity index 98% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatToolbar.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatToolbar.tsx index 0fcb20bc947..3d52cca4293 100644 --- a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/FormatToolbar.tsx +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/FormatToolbar.tsx @@ -33,6 +33,7 @@ import { LinkIcon, } from '@wireapp/react-ui-kit'; +import {FormatSeparator} from 'Components/InputBar/common/FormatSeparator/FormatSeparator'; import {t} from 'Util/LocalizerUtil'; import {FormatButton} from './FormatButton/FormatButton'; @@ -45,8 +46,6 @@ import {useLinkState} from './useLinkState/useLinkState'; import {useListState} from './useListState/useListState'; import {useToolbarState} from './useToolbarState/useToolbarState'; -import {FormatSeparator} from '../../../common/FormatSeparator/FormatSeparator'; - interface FormatToolbarProps { isEditing: boolean; } diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/LinkDialog/LinkDialog.styles.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/LinkDialog/LinkDialog.styles.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/LinkDialog/LinkDialog.styles.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/LinkDialog/LinkDialog.styles.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/LinkDialog/LinkDialog.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/LinkDialog/LinkDialog.tsx similarity index 99% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/LinkDialog/LinkDialog.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/LinkDialog/LinkDialog.tsx index db723476678..68424287f20 100644 --- a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/LinkDialog/LinkDialog.tsx +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/LinkDialog/LinkDialog.tsx @@ -34,7 +34,7 @@ import { titleStyles, } from './LinkDialog.styles'; -import {validateUrl} from '../../../utils/url'; +import {validateUrl} from '../../utils/url'; interface LinkDialogProps { isOpen: boolean; diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.test.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.test.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.test.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.test.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isBlockquoteNode/isBlockquoteNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.test.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.test.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.test.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.test.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isCodeBlockNode/isCodeBlockNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isHeadingNode/isHeadingNode.test.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isHeadingNode/isHeadingNode.test.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isHeadingNode/isHeadingNode.test.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isHeadingNode/isHeadingNode.test.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isHeadingNode/isHeadingNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isHeadingNode/isHeadingNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isHeadingNode/isHeadingNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isHeadingNode/isHeadingNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isListNode/isListNode.test.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isListNode/isListNode.test.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isListNode/isListNode.test.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isListNode/isListNode.test.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isListNode/isListNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isListNode/isListNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/common/isListNode/isListNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/common/isListNode/isListNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useBlockquoteState/useBlockquoteState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useBlockquoteState/useBlockquoteState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useBlockquoteState/useBlockquoteState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useBlockquoteState/useBlockquoteState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useCodeBlockState/useCodeBlockState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useCodeBlockState/useCodeBlockState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useCodeBlockState/useCodeBlockState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useCodeBlockState/useCodeBlockState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useHeadingState/headingCommand.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useHeadingState/headingCommand.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useHeadingState/headingCommand.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useHeadingState/headingCommand.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useHeadingState/useHeadingState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useHeadingState/useHeadingState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useHeadingState/useHeadingState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useHeadingState/useHeadingState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/createNewLink/createNewLink.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/createNewLink/createNewLink.ts similarity index 95% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/createNewLink/createNewLink.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/createNewLink/createNewLink.ts index bdf30199a8b..28f54dbc294 100644 --- a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/createNewLink/createNewLink.ts +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/createNewLink/createNewLink.ts @@ -20,7 +20,7 @@ import {$createLinkNode} from '@lexical/link'; import {RangeSelection, $createTextNode} from 'lexical'; -import {sanitizeUrl} from '../../../../utils/url'; +import {sanitizeUrl} from '../../../utils/url'; interface CreateLinkParams { selection: RangeSelection; diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/getSelectedNode/getSelectedNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/getSelectedNode/getSelectedNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/getSelectedNode/getSelectedNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/getSelectedNode/getSelectedNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useLinkEditing/useLinkEditing.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useLinkEditing/useLinkEditing.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useLinkEditing/useLinkEditing.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useLinkEditing/useLinkEditing.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useLinkState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useLinkState.ts similarity index 99% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useLinkState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useLinkState.ts index 2215a9d7621..9625d2c43f8 100644 --- a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useLinkState.ts +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useLinkState.ts @@ -38,7 +38,7 @@ import {getSelectedNode} from './getSelectedNode/getSelectedNode'; import {useLinkEditing} from './useLinkEditing/useLinkEditing'; import {useModalState} from './useModalState/useModalState'; -import {sanitizeUrl} from '../../../utils/url'; +import {sanitizeUrl} from '../../utils/url'; export const FORMAT_LINK_COMMAND = createCommand(); diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useModalState/useModalState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useModalState/useModalState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useLinkState/useModalState/useModalState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useLinkState/useModalState/useModalState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useListState/useListState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useListState/useListState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useListState/useListState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useListState/useListState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useToolbarState/useToolbarState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useToolbarState/useToolbarState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/FormatToolbar/useToolbarState/useToolbarState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/FormatToolbar/useToolbarState/useToolbarState.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/components/Placeholder/Placeholder.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/Placeholder/Placeholder.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/components/Placeholder/Placeholder.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/Placeholder/Placeholder.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/RichTextEditor.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/RichTextEditor.tsx similarity index 93% rename from src/script/components/InputBar/components/RichTextEditor/RichTextEditor.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/RichTextEditor.tsx index 63255599a76..a8f7b054eee 100644 --- a/src/script/components/InputBar/components/RichTextEditor/RichTextEditor.tsx +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/RichTextEditor.tsx @@ -17,7 +17,7 @@ * */ -import {ReactElement, useRef} from 'react'; +import {ReactNode, useRef} from 'react'; import {$convertToMarkdownString} from '@lexical/markdown'; import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin'; @@ -31,13 +31,14 @@ import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin'; import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; import {LexicalEditor, EditorState} from 'lexical'; -import {DraftState} from 'Components/InputBar/util/DraftStateUtil'; +import {DraftState} from 'Components/InputBar/common/draftState/draftState'; +import {MessageContent} from 'Components/InputBar/common/messageContent/messageContent'; import {ContentMessage} from 'src/script/entity/message/ContentMessage'; import {User} from 'src/script/entity/User'; -import {FormatToolbar} from './components/FormatToolbar/FormatToolbar'; -import {Placeholder} from './components/Placeholder/Placeholder'; import {editorConfig} from './editorConfig'; +import {FormatToolbar} from './FormatToolbar/FormatToolbar'; +import {Placeholder} from './Placeholder/Placeholder'; import {AutoFocusPlugin} from './plugins/AutoFocusPlugin/AutoFocusPlugin'; import {AutoLinkPlugin} from './plugins/AutoLinkPlugin/AutoLinkPlugin'; import {BlockquotePlugin} from './plugins/BlockquotePlugin/BlockquotePlugin'; @@ -60,25 +61,18 @@ import {parseMentions} from './utils/parseMentions'; import {transformMessage} from './utils/transformMessage'; import {useEditorDraftState} from './utils/useEditorDraftState'; -import {MentionEntity} from '../../../../message/MentionEntity'; - -export type RichTextContent = { - text: string; - mentions?: MentionEntity[]; -}; - interface RichTextEditorProps { placeholder: string; replaceEmojis: boolean; editedMessage?: ContentMessage; - children: ReactElement; + children: ReactNode; hasLocalEphemeralTimer: boolean; showFormatToolbar: boolean; showMarkdownPreview: boolean; getMentionCandidates: (search?: string | null) => User[]; saveDraftState: (editor: string, plainMessage: string, replyId?: string) => void; loadDraftState: () => Promise; - onUpdate: (content: RichTextContent) => void; + onUpdate: (content: MessageContent) => void; onArrowUp: () => void; onEscape: () => void; onShiftTab: () => void; diff --git a/src/script/components/InputBar/components/RichTextEditor/editorConfig.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/editorConfig.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/editorConfig.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/editorConfig.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/index.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/index.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/index.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/index.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/nodes/EmojiNode.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/EmojiNode.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/nodes/EmojiNode.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/EmojiNode.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/nodes/Mention.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/Mention.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/nodes/Mention.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/Mention.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/nodes/MentionNode.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/MentionNode.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/nodes/MentionNode.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/nodes/MentionNode.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/AutoFocusPlugin/AutoFocusPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/AutoFocusPlugin/AutoFocusPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/AutoFocusPlugin/AutoFocusPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/AutoFocusPlugin/AutoFocusPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/AutoLinkPlugin/AutoLinkPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/AutoLinkPlugin/AutoLinkPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/AutoLinkPlugin/AutoLinkPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/AutoLinkPlugin/AutoLinkPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/BlockquotePlugin/BlockquotePlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/BlockquotePlugin/BlockquotePlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/BlockquotePlugin/BlockquotePlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/BlockquotePlugin/BlockquotePlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/CodeHighlightPlugin/CodeHighlightPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/CodeHighlightPlugin/CodeHighlightPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/CodeHighlightPlugin/CodeHighlightPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/CodeHighlightPlugin/CodeHighlightPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx similarity index 95% rename from src/script/components/InputBar/components/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx index 6ddbcf056ad..7adc147097a 100644 --- a/src/script/components/InputBar/components/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/DraftStatePlugin/DraftStatePlugin.tsx @@ -21,7 +21,7 @@ import {useCallback, useEffect} from 'react'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {DraftState} from 'Components/InputBar/util/DraftStateUtil'; +import {DraftState} from 'Components/InputBar/common/draftState/draftState'; interface DraftStatePluginProps { loadDraftState: () => Promise; diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx similarity index 97% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx index 3bcb0662d51..213144767f1 100644 --- a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/EditedMessagePlugin.tsx @@ -23,7 +23,6 @@ import {$convertFromMarkdownString} from '@lexical/markdown'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {$getRoot, $setSelection} from 'lexical'; -import {markdownTransformers} from 'Components/InputBar/components/RichTextEditor/utils/markdownTransformers'; import {ContentMessage} from 'src/script/entity/message/ContentMessage'; import {getMentionMarkdownTransformer} from './getMentionMarkdownTransformer/getMentionMarkdownTransformer'; @@ -31,6 +30,8 @@ import {getMentionNodesFromMessage} from './getMentionNodesFromMessage/getMentio import {getRawMarkdownNodesWithMentions} from './getRawMarkdownFromMessage/getRawMarkdownFromMessage'; import {wrapMentionsWithTags} from './wrapMentionsWithTags/wrapMentionsWithTags'; +import {markdownTransformers} from '../../utils/markdownTransformers'; + type Props = { message?: ContentMessage; showMarkdownPreview: boolean; diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getMentionMarkdownTransformer/getMentionMarkdownTransformer.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getMentionMarkdownTransformer/getMentionMarkdownTransformer.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getMentionMarkdownTransformer/getMentionMarkdownTransformer.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getMentionMarkdownTransformer/getMentionMarkdownTransformer.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getMentionNodesFromMessage/getMentionNodesFromMessage.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getMentionNodesFromMessage/getMentionNodesFromMessage.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getMentionNodesFromMessage/getMentionNodesFromMessage.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getMentionNodesFromMessage/getMentionNodesFromMessage.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts similarity index 93% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts index 3fb26919b68..d53295a362e 100644 --- a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts +++ b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/getRawMarkdownFromMessage/getRawMarkdownFromMessage.ts @@ -19,10 +19,10 @@ import {$createParagraphNode, $createTextNode} from 'lexical'; -import {$createMentionNode} from 'Components/InputBar/components/RichTextEditor/nodes/MentionNode'; import {ContentMessage} from 'src/script/entity/message/ContentMessage'; import {Text} from 'src/script/entity/message/Text'; +import {$createMentionNode} from '../../../nodes/MentionNode'; import {createNodes} from '../../../utils/generateNodes'; export const getRawMarkdownNodesWithMentions = (message: ContentMessage) => { diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/wrapMentionsWithTags/wrapMentionsWithTags.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/wrapMentionsWithTags/wrapMentionsWithTags.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EditedMessagePlugin/wrapMentionsWithTags/wrapMentionsWithTags.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EditedMessagePlugin/wrapMentionsWithTags/wrapMentionsWithTags.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.styles.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.styles.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.styles.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.styles.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiItem.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiPickerPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiPickerPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiPickerPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/EmojiPickerPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/index.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/index.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/EmojiPickerPlugin/index.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/EmojiPickerPlugin/index.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/GlobalEventsPlugin/GlobalEventsPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/GlobalEventsPlugin/GlobalEventsPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/GlobalEventsPlugin/GlobalEventsPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/GlobalEventsPlugin/GlobalEventsPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/HistoryPlugin/HistoryPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/HistoryPlugin/HistoryPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/HistoryPlugin/HistoryPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/HistoryPlugin/HistoryPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/InlineEmojiReplacementPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/InlineEmojiReplacementPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/InlineEmojiReplacementPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/InlineEmojiReplacementPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/index.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/index.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/index.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/index.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/inlineReplacements.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/inlineReplacements.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/InlineEmojiReplacementPlugin/inlineReplacements.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/InlineEmojiReplacementPlugin/inlineReplacements.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/LinkPlugin/LinkPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/LinkPlugin/LinkPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/LinkPlugin/LinkPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/LinkPlugin/LinkPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/ListIndentationPlugin/ListIndentationPlugin.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ListIndentationPlugin/ListIndentationPlugin.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/ListIndentationPlugin/ListIndentationPlugin.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ListIndentationPlugin/ListIndentationPlugin.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/MentionSuggestionsItem.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/MentionSuggestionsItem.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/MentionSuggestionsItem.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/MentionSuggestionsItem.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/MentionsPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/MentionsPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/MentionsPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/MentionsPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/index.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/index.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/MentionsPlugin/index.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/MentionsPlugin/index.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/PastePlugin/PastePlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/PastePlugin/PastePlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/PastePlugin/PastePlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/PastePlugin/PastePlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/SendPlugin/SendPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/SendPlugin/SendPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/SendPlugin/SendPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/SendPlugin/SendPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/plugins/TypeaheadMenuPlugin/TypeaheadMenuPlugin.tsx b/src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/TypeaheadMenuPlugin/TypeaheadMenuPlugin.tsx similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/plugins/TypeaheadMenuPlugin/TypeaheadMenuPlugin.tsx rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/plugins/TypeaheadMenuPlugin/TypeaheadMenuPlugin.tsx diff --git a/src/script/components/InputBar/components/RichTextEditor/theme.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/theme.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/theme.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/theme.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/generateNodes.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/generateNodes.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/generateNodes.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/generateNodes.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/getDomRangeRect.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/getDomRangeRect.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/getDomRangeRect.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/getDomRangeRect.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/getSelectionInfo.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/getSelectionInfo.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/getSelectionInfo.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/getSelectionInfo.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/markdownTransformers.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/markdownTransformers.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/markdownTransformers.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/markdownTransformers.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/parseMentions.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/parseMentions.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/parseMentions.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/parseMentions.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/transformMessage.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/transformMessage.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/transformMessage.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/transformMessage.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/url.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/url.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/url.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/url.ts diff --git a/src/script/components/InputBar/components/RichTextEditor/utils/useEditorDraftState.ts b/src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/useEditorDraftState.ts similarity index 100% rename from src/script/components/InputBar/components/RichTextEditor/utils/useEditorDraftState.ts rename to src/script/components/InputBar/InputBarEditor/RichTextEditor/utils/useEditorDraftState.ts diff --git a/src/script/components/InputBar/components/PastedFileControls/PastedFileControls.tsx b/src/script/components/InputBar/PastedFileControls/PastedFileControls.tsx similarity index 93% rename from src/script/components/InputBar/components/PastedFileControls/PastedFileControls.tsx rename to src/script/components/InputBar/PastedFileControls/PastedFileControls.tsx index cc276069f55..ec9efcc294c 100644 --- a/src/script/components/InputBar/components/PastedFileControls/PastedFileControls.tsx +++ b/src/script/components/InputBar/PastedFileControls/PastedFileControls.tsx @@ -24,7 +24,7 @@ import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import * as Icon from 'Components/Icon'; import {t} from 'Util/LocalizerUtil'; -import {Config} from '../../../../Config'; +import {Config} from '../../../Config'; interface PastedFileControlsProps { pastedFile: File; @@ -32,7 +32,7 @@ interface PastedFileControlsProps { onSend: () => void; } -const PastedFileControls: FC = ({pastedFile, onClear, onSend}) => { +export const PastedFileControls: FC = ({pastedFile, onClear, onSend}) => { const isSupportedFileType = (Config.getConfig().ALLOWED_IMAGE_TYPES as ReadonlyArray).includes( pastedFile.type, ); @@ -85,5 +85,3 @@ const PastedFileControls: FC = ({pastedFile, onClear, o ); }; - -export {PastedFileControls}; diff --git a/src/script/components/InputBar/components/ReplyBar/ReplyBar.tsx b/src/script/components/InputBar/ReplyBar/ReplyBar.tsx similarity index 94% rename from src/script/components/InputBar/components/ReplyBar/ReplyBar.tsx rename to src/script/components/InputBar/ReplyBar/ReplyBar.tsx index 00cc272866d..3e1185199f4 100644 --- a/src/script/components/InputBar/components/ReplyBar/ReplyBar.tsx +++ b/src/script/components/InputBar/ReplyBar/ReplyBar.tsx @@ -19,8 +19,6 @@ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ -import {FC} from 'react'; - import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import {RestrictedVideo} from 'Components/asset/RestrictedVideo'; @@ -31,14 +29,14 @@ import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; import {renderMessage} from 'Util/messageRenderer'; -import {ContentMessage} from '../../../../entity/message/ContentMessage'; +import {ContentMessage} from '../../../entity/message/ContentMessage'; interface ReplyBarProps { replyMessageEntity: ContentMessage; onCancel: () => void; } -const ReplyBar: FC = ({replyMessageEntity, onCancel}) => { +export const ReplyBar = ({replyMessageEntity, onCancel}: ReplyBarProps) => { const { assets, senderName, @@ -81,7 +79,7 @@ const ReplyBar: FC = ({replyMessageEntity, onCancel}) => {
@@ -131,5 +129,3 @@ const ReplyBar: FC = ({replyMessageEntity, onCancel}) => {
); }; - -export {ReplyBar}; diff --git a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.styles.ts b/src/script/components/InputBar/TypingIndicator/TypingIndicator.styles.ts similarity index 100% rename from src/script/components/InputBar/components/TypingIndicator/TypingIndicator.styles.ts rename to src/script/components/InputBar/TypingIndicator/TypingIndicator.styles.ts diff --git a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.test.tsx b/src/script/components/InputBar/TypingIndicator/TypingIndicator.test.tsx similarity index 96% rename from src/script/components/InputBar/components/TypingIndicator/TypingIndicator.test.tsx rename to src/script/components/InputBar/TypingIndicator/TypingIndicator.test.tsx index 65f4d612911..0ac417e5486 100644 --- a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.test.tsx +++ b/src/script/components/InputBar/TypingIndicator/TypingIndicator.test.tsx @@ -20,10 +20,10 @@ import {render} from '@testing-library/react'; import {act} from 'react-dom/test-utils'; -import {TypingIndicator, TypingIndicatorProps} from './TypingIndicator'; -import {useTypingIndicatorState} from './TypingIndicator.state'; +import {User} from 'src/script/entity/User'; -import {User} from '../../../../entity/User'; +import {TypingIndicator, TypingIndicatorProps} from './TypingIndicator'; +import {useTypingIndicatorState} from './useTypingIndicatorState/useTypingIndicatorState'; function createUser(id: string, name: string): User { const user = new User(id); diff --git a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.tsx b/src/script/components/InputBar/TypingIndicator/TypingIndicator.tsx similarity index 93% rename from src/script/components/InputBar/components/TypingIndicator/TypingIndicator.tsx rename to src/script/components/InputBar/TypingIndicator/TypingIndicator.tsx index 7482e8cf2e7..5360488378a 100644 --- a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.tsx +++ b/src/script/components/InputBar/TypingIndicator/TypingIndicator.tsx @@ -17,13 +17,10 @@ * */ -import {FC} from 'react'; - import {Avatar, AVATAR_SIZE} from 'Components/Avatar'; import * as Icon from 'Components/Icon'; import {t} from 'Util/LocalizerUtil'; -import {useTypingIndicatorState} from './TypingIndicator.state'; import { dotOneStyles, dotThreeStyles, @@ -33,12 +30,13 @@ import { indicatorTitleStyles, wrapperStyles, } from './TypingIndicator.styles'; +import {useTypingIndicatorState} from './useTypingIndicatorState/useTypingIndicatorState'; export interface TypingIndicatorProps { conversationId: string; } -const TypingIndicator: FC = ({conversationId}) => { +export const TypingIndicator = ({conversationId}: TypingIndicatorProps) => { const users = useTypingIndicatorState(state => state.getTypingUsersInConversation(conversationId)); const usersCount = users.length; @@ -93,5 +91,3 @@ const TypingIndicator: FC = ({conversationId}) => { ); }; - -export {TypingIndicator}; diff --git a/src/script/components/InputBar/components/TypingIndicator/index.ts b/src/script/components/InputBar/TypingIndicator/index.ts similarity index 90% rename from src/script/components/InputBar/components/TypingIndicator/index.ts rename to src/script/components/InputBar/TypingIndicator/index.ts index 3154396ef02..eaf30885e62 100644 --- a/src/script/components/InputBar/components/TypingIndicator/index.ts +++ b/src/script/components/InputBar/TypingIndicator/index.ts @@ -21,6 +21,6 @@ import {TIME_IN_MILLIS} from 'Util/TimeUtil'; export * from './TypingIndicator'; -export {useTypingIndicatorState} from './TypingIndicator.state'; +export {useTypingIndicatorState} from './useTypingIndicatorState/useTypingIndicatorState'; export const TYPING_TIMEOUT = TIME_IN_MILLIS.SECOND * 10; diff --git a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.state.tsx b/src/script/components/InputBar/TypingIndicator/useTypingIndicatorState/useTypingIndicatorState.ts similarity index 95% rename from src/script/components/InputBar/components/TypingIndicator/TypingIndicator.state.tsx rename to src/script/components/InputBar/TypingIndicator/useTypingIndicatorState/useTypingIndicatorState.ts index fb9d5a8620e..acf5d3a9929 100644 --- a/src/script/components/InputBar/components/TypingIndicator/TypingIndicator.state.tsx +++ b/src/script/components/InputBar/TypingIndicator/useTypingIndicatorState/useTypingIndicatorState.ts @@ -36,7 +36,7 @@ type TypingIndicatorState = { getTypingUser: (user: User, conversationId: string) => TypingUser | undefined; }; -const useTypingIndicatorState = create((set, get) => ({ +export const useTypingIndicatorState = create((set, get) => ({ typingUsers: [], addTypingUser: ({conversationId, user, timerId}) => set(state => { @@ -65,5 +65,3 @@ const useTypingIndicatorState = create((set, get) => ({ .map(typingUser => typingUser.user), clearTypingUsers: () => set({typingUsers: []}), })); - -export {useTypingIndicatorState}; diff --git a/src/script/components/InputBar/components/common/FormatSeparator/FormatSeparator.tsx b/src/script/components/InputBar/common/FormatSeparator/FormatSeparator.tsx similarity index 100% rename from src/script/components/InputBar/components/common/FormatSeparator/FormatSeparator.tsx rename to src/script/components/InputBar/common/FormatSeparator/FormatSeparator.tsx diff --git a/src/script/components/InputBar/util/DraftStateUtil.ts b/src/script/components/InputBar/common/draftState/draftState.ts similarity index 91% rename from src/script/components/InputBar/util/DraftStateUtil.ts rename to src/script/components/InputBar/common/draftState/draftState.ts index 33ae7c8907f..ff4eb943784 100644 --- a/src/script/components/InputBar/util/DraftStateUtil.ts +++ b/src/script/components/InputBar/common/draftState/draftState.ts @@ -17,10 +17,10 @@ * */ -import {MessageRepository} from '../../../conversation/MessageRepository'; -import {Conversation} from '../../../entity/Conversation'; -import {ContentMessage} from '../../../entity/message/ContentMessage'; -import {StorageKey, StorageRepository} from '../../../storage'; +import {MessageRepository} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; +import {StorageKey, StorageRepository} from 'src/script/storage'; export interface DraftState { editorState: string | null; diff --git a/src/script/components/InputBar/components/AssetUploadButton/index.ts b/src/script/components/InputBar/common/messageContent/messageContent.ts similarity index 78% rename from src/script/components/InputBar/components/AssetUploadButton/index.ts rename to src/script/components/InputBar/common/messageContent/messageContent.ts index 597979b1ea9..eb145137a88 100644 --- a/src/script/components/InputBar/components/AssetUploadButton/index.ts +++ b/src/script/components/InputBar/common/messageContent/messageContent.ts @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2022 Wire Swiss GmbH + * Copyright (C) 2025 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,4 +17,9 @@ * */ -export * from './AssetUploadButton'; +import {MentionEntity} from 'src/script/message/MentionEntity'; + +export interface MessageContent { + text: string; + mentions?: MentionEntity[]; +} diff --git a/src/script/components/InputBar/components/RichTextEditor/components/SendMessageButton/index.ts b/src/script/components/InputBar/components/RichTextEditor/components/SendMessageButton/index.ts deleted file mode 100644 index 131603eb4ff..00000000000 --- a/src/script/components/InputBar/components/RichTextEditor/components/SendMessageButton/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -export * from './SendMessageButton'; diff --git a/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.test.ts b/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.test.ts deleted file mode 100644 index d9fac3b410a..00000000000 --- a/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Wire - * Copyright (C) 2022 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import {renderHook, act, fireEvent} from '@testing-library/react'; - -import {useFilePaste} from './useFilePaste'; - -describe('useFilePaste', () => { - it('Calls the onFilePasted callback when a file is pasted', async () => { - const onFilePasted = jest.fn(); - renderHook(() => useFilePaste(onFilePasted)); - const files = [new File([''], 'test.jpg', {type: 'image/jpeg'})]; - - act(() => { - fireEvent.paste(document, {clipboardData: {types: ['image/jpeg'], files}}); - }); - expect(onFilePasted).toHaveBeenCalledWith(files); - }); - - it('Ignores paste that are plain text', async () => { - const onFilePasted = jest.fn(); - renderHook(() => useFilePaste(onFilePasted)); - - act(() => { - fireEvent.paste(document, {clipboardData: {types: ['text/plain']}}); - }); - expect(onFilePasted).not.toHaveBeenCalled(); - }); -}); diff --git a/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.ts b/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.ts deleted file mode 100644 index 5e55baefb42..00000000000 --- a/src/script/components/InputBar/hooks/useFilePaste/useFilePaste.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Wire - * Copyright (C) 2022 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import {useEffect} from 'react'; - -export const useFilePaste = (onFilePasted: (files: FileList) => void) => { - useEffect(() => { - const handleFilePasting = (event: ClipboardEvent) => { - if (event.clipboardData?.types.includes('text/plain')) { - return; - } - // Avoid copying the filename into the input field - event.preventDefault(); - const files = event.clipboardData?.files; - if (files) { - onFilePasted(files); - } - }; - document.addEventListener('paste', handleFilePasting); - return () => document.removeEventListener('paste', handleFilePasting); - }, [onFilePasted]); -}; diff --git a/src/script/components/InputBar/hooks/useEmojiPicker/useEmojiPicker.ts b/src/script/components/InputBar/useEmojiPicker/useEmojiPicker.ts similarity index 100% rename from src/script/components/InputBar/hooks/useEmojiPicker/useEmojiPicker.ts rename to src/script/components/InputBar/useEmojiPicker/useEmojiPicker.ts diff --git a/src/script/components/InputBar/useFileHandling/useFileHandling.ts b/src/script/components/InputBar/useFileHandling/useFileHandling.ts new file mode 100644 index 00000000000..2c0e1fc795e --- /dev/null +++ b/src/script/components/InputBar/useFileHandling/useFileHandling.ts @@ -0,0 +1,82 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useEffect, useState} from 'react'; + +import {amplify} from 'amplify'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {useFilePaste} from './useFilePaste/useFilePaste'; + +interface UseFileHandlingProps { + uploadDroppedFiles: (files: File[]) => void; + uploadImages: (images: File[]) => void; +} + +export const useFileHandling = ({uploadDroppedFiles, uploadImages}: UseFileHandlingProps) => { + const [pastedFile, setPastedFile] = useState(null); + + useFilePaste({ + onFilePasted: file => { + setPastedFile(file); + }, + }); + + const clearPastedFile = () => setPastedFile(null); + + const sendPastedFile = () => { + if (pastedFile) { + uploadDroppedFiles([pastedFile]); + clearPastedFile(); + } + }; + + const sendImageOnEnterClick = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.metaKey) { + sendPastedFile(); + } + }; + + useEffect(() => { + if (!pastedFile) { + return () => undefined; + } + + window.addEventListener('keydown', sendImageOnEnterClick); + + return () => { + window.removeEventListener('keydown', sendImageOnEnterClick); + }; + }, [pastedFile]); + + useEffect(() => { + amplify.subscribe(WebAppEvents.CONVERSATION.IMAGE.SEND, uploadImages); + + return () => { + amplify.unsubscribeAll(WebAppEvents.CONVERSATION.IMAGE.SEND); + }; + }, []); + + return { + pastedFile, + clearPastedFile, + sendPastedFile, + }; +}; diff --git a/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.test.ts b/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.test.ts new file mode 100644 index 00000000000..04a09dc9891 --- /dev/null +++ b/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.test.ts @@ -0,0 +1,169 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {act, renderHook} from '@testing-library/react'; + +import * as checkFileSharingPermissionModule from 'Components/Conversation/utils/checkFileSharingPermission'; +import * as LocalizerUtil from 'Util/LocalizerUtil'; +import * as TimeUtil from 'Util/TimeUtil'; + +import {useFilePaste} from './useFilePaste'; + +jest.mock('Components/Conversation/utils/checkFileSharingPermission', () => ({ + checkFileSharingPermission: jest.fn(callback => callback), +})); + +jest.mock('Util/LocalizerUtil', () => ({ + t: jest.fn(), +})); + +jest.mock('Util/TimeUtil', () => ({ + formatLocale: jest.fn(), +})); + +describe('useFilePaste', () => { + const mockOnFilePasted = jest.fn(); + const mockDate = new Date('2024-01-01'); + const mockFormattedDate = '1 Jan 2024, 12:00'; + + beforeEach(() => { + jest.clearAllMocks(); + (LocalizerUtil.t as jest.Mock).mockImplementation((key, params) => { + if (key === 'conversationSendPastedFile' && params?.date) { + return `Pasted file from ${params.date}`; + } + return key; + }); + (TimeUtil.formatLocale as jest.Mock).mockReturnValue(mockFormattedDate); + }); + + it('handles file paste event', () => { + renderHook(() => useFilePaste({onFilePasted: mockOnFilePasted})); + + const file = new File(['test content'], 'test.txt', {type: 'text/plain', lastModified: mockDate.getTime()}); + const clipboardEvent = new MockClipboardEvent([file]); + + act(() => { + document.dispatchEvent(clipboardEvent); + }); + + expect(mockOnFilePasted).toHaveBeenCalled(); + const calledWithFile = mockOnFilePasted.mock.calls[0][0]; + expect(calledWithFile instanceof File).toBe(true); + expect(calledWithFile.name).toBe(`Pasted file from ${mockFormattedDate}.txt`); + }); + + it('ignores paste events with text/plain content', () => { + renderHook(() => useFilePaste({onFilePasted: mockOnFilePasted})); + + const clipboardEvent = new MockClipboardEvent([], ['text/plain']); + + act(() => { + document.dispatchEvent(clipboardEvent); + }); + + expect(mockOnFilePasted).not.toHaveBeenCalled(); + }); + + it('does nothing when no files are pasted', () => { + renderHook(() => useFilePaste({onFilePasted: mockOnFilePasted})); + + const clipboardEvent = new MockClipboardEvent([]); + + act(() => { + document.dispatchEvent(clipboardEvent); + }); + + expect(mockOnFilePasted).not.toHaveBeenCalled(); + }); + + it('uses checkFileSharingPermission wrapper', () => { + renderHook(() => useFilePaste({onFilePasted: mockOnFilePasted})); + + expect(checkFileSharingPermissionModule.checkFileSharingPermission).toHaveBeenCalled(); + }); +}); + +/** + * Mock implementation of the browser's DataTransfer API + * + * Why do we need this? + * 1. DataTransfer is a browser API not available in Jest's test environment + * 2. When files are pasted in a real browser, they come as a ClipboardEvent + * containing a DataTransfer object with: + * - files: List of pasted files + * - types: Content types being pasted (e.g., 'text/plain', 'Files') + * 3. Our hook checks these properties to: + * - Filter out text-only pastes + * - Handle pasted files + * + * This mock allows us to simulate file paste events as they would occur in a real browser. + */ +class MockDataTransfer implements Partial { + readonly files: FileList; + readonly types: string[]; + readonly items: DataTransferItemList; + readonly dropEffect: 'none' = 'none'; + readonly effectAllowed: 'none' = 'none'; + + constructor(files: File[] = []) { + this.files = { + ...files, + length: files.length, + item: (index: number) => files[index] || null, + [Symbol.iterator]: function* () { + for (let i = 0; i < files.length; i++) { + yield files[i]; + } + }, + } as unknown as FileList; + this.types = []; + this.items = { + length: 0, + add: jest.fn(), + clear: jest.fn(), + remove: jest.fn(), + } as unknown as DataTransferItemList; + } + + clearData(): void {} + getData(): string { + return ''; + } + setData(): boolean { + return false; + } + setDragImage(): void {} +} + +class MockClipboardEvent extends Event { + readonly clipboardData: DataTransfer; + constructor(files: File[] = [], types: string[] = []) { + super('paste'); + const dataTransfer = new MockDataTransfer(files); + Object.defineProperty(dataTransfer, 'types', { + value: types, + enumerable: true, + }); + this.clipboardData = dataTransfer; + } +} + +global.DataTransfer = MockDataTransfer as unknown as typeof DataTransfer; +global.ClipboardEvent = MockClipboardEvent as unknown as typeof ClipboardEvent; diff --git a/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.ts b/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.ts new file mode 100644 index 00000000000..300425dccb7 --- /dev/null +++ b/src/script/components/InputBar/useFileHandling/useFilePaste/useFilePaste.ts @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2022 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect} from 'react'; + +import {checkFileSharingPermission} from 'Components/Conversation/utils/checkFileSharingPermission'; +import {t} from 'Util/LocalizerUtil'; +import {formatLocale} from 'Util/TimeUtil'; +import {getFileExtension} from 'Util/util'; + +interface UseFilePasteParams { + onFilePasted: (file: File) => void; +} + +export const useFilePaste = ({onFilePasted}: UseFilePasteParams) => { + const processClipboardFiles = useCallback( + (files: FileList): void => { + const [pastedFile] = files; + + if (!pastedFile) { + return; + } + const {lastModified} = pastedFile; + + const date = formatLocale(lastModified || new Date(), 'PP, pp'); + const fileName = `${t('conversationSendPastedFile', {date})}.${getFileExtension(pastedFile.name)}`; + + const newFile = new File([pastedFile], fileName, { + type: pastedFile.type, + }); + + onFilePasted(newFile); + }, + [onFilePasted], + ); + + const handlePasteEvent = useCallback( + (event: ClipboardEvent) => { + if (event.clipboardData?.types.includes('text/plain')) { + return; + } + // Avoid copying the filename into the input field + event.preventDefault(); + const files = event.clipboardData?.files; + + if (files) { + processClipboardFiles(files); + } + }, + [processClipboardFiles], + ); + + useEffect(() => { + document.addEventListener('paste', checkFileSharingPermission(handlePasteEvent)); + return () => document.removeEventListener('paste', checkFileSharingPermission(handlePasteEvent)); + }, [handlePasteEvent]); +}; diff --git a/src/script/components/InputBar/hooks/useFormatToolbar/useFormatToolbar.ts b/src/script/components/InputBar/useFormatToolbar/useFormatToolbar.ts similarity index 100% rename from src/script/components/InputBar/hooks/useFormatToolbar/useFormatToolbar.ts rename to src/script/components/InputBar/useFormatToolbar/useFormatToolbar.ts diff --git a/src/script/components/InputBar/useGiphy/useGiphy.ts b/src/script/components/InputBar/useGiphy/useGiphy.ts new file mode 100644 index 00000000000..ed41909f3ab --- /dev/null +++ b/src/script/components/InputBar/useGiphy/useGiphy.ts @@ -0,0 +1,83 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect, useMemo} from 'react'; + +import {amplify} from 'amplify'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {Config} from 'src/script/Config'; +import {MessageRepository, OutgoingQuote} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; + +interface UseGiphyProps { + text: string; + maxLength: number; + openGiphy: (inputValue: string) => void; + generateQuote: () => Promise; + messageRepository: MessageRepository; + conversation: Conversation; + cancelMesssageEditing: () => void; +} + +export const useGiphy = ({ + text, + maxLength, + openGiphy, + generateQuote, + messageRepository, + conversation, + cancelMesssageEditing, +}: UseGiphyProps) => { + const isMessageFormatButtonsFlagEnabled = Config.getConfig().FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS; + + const showGiphyButton = useMemo(() => { + if (isMessageFormatButtonsFlagEnabled) { + return text.length > 0; + } + return text.length > 0 && text.length <= maxLength; + }, [text.length, maxLength, isMessageFormatButtonsFlagEnabled]); + + const handleGifClick = () => openGiphy(text); + + const sendGiphy = useCallback( + (gifUrl: string, tag: string): void => { + void generateQuote().then(quoteEntity => { + void messageRepository.sendGif(conversation, gifUrl, tag, quoteEntity); + cancelMesssageEditing(); + }); + }, + [cancelMesssageEditing, conversation, generateQuote, messageRepository], + ); + + useEffect(() => { + amplify.subscribe(WebAppEvents.EXTENSIONS.GIPHY.SEND, sendGiphy); + + return () => { + amplify.unsubscribeAll(WebAppEvents.EXTENSIONS.GIPHY.SEND); + }; + }, [sendGiphy, cancelMesssageEditing]); + + return { + showGiphyButton, + handleGifClick, + sendGiphy, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useDraftState/useDraftState.ts b/src/script/components/InputBar/useMessageHandling/useDraftState/useDraftState.ts new file mode 100644 index 00000000000..66c1da720d9 --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useDraftState/useDraftState.ts @@ -0,0 +1,76 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback} from 'react'; + +import {LexicalEditor, CLEAR_EDITOR_COMMAND} from 'lexical'; + +import {MessageRepository} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; +import {StorageRepository} from 'src/script/storage'; +import {sanitizeMarkdown} from 'Util/MarkdownUtil'; + +import {DraftState, loadDraftState, saveDraftState} from '../../common/draftState/draftState'; + +interface UseDraftStateProps { + conversation: Conversation; + storageRepository: StorageRepository; + messageRepository: MessageRepository; + editorRef: React.RefObject; + onLoad?: (draftState: DraftState) => void; + editedMessageId?: string; + replyMessageEntityId?: string; +} + +export const useDraftState = ({ + conversation, + storageRepository, + messageRepository, + editorRef, + onLoad, + editedMessageId, + replyMessageEntityId, +}: UseDraftStateProps) => { + const reset = useCallback(() => { + editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); + }, [editorRef]); + + const load = useCallback(async () => { + const draftState = await loadDraftState(conversation, storageRepository, messageRepository); + onLoad?.(draftState); + return draftState; + }, [conversation, messageRepository, onLoad, storageRepository]); + + const save = async (editorState: string, text: string, replyId = '') => { + void saveDraftState({ + storageRepository, + conversation, + editorState, + plainMessage: sanitizeMarkdown(text), + replyId: replyId ?? replyMessageEntityId, + editedMessageId, + }); + }; + + return { + reset, + load, + save, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useMessageEditing/useMessageEditing.ts b/src/script/components/InputBar/useMessageHandling/useMessageEditing/useMessageEditing.ts new file mode 100644 index 00000000000..cd10a0aa3bc --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useMessageEditing/useMessageEditing.ts @@ -0,0 +1,54 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect, useState} from 'react'; + +import {amplify} from 'amplify'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; + +export const useMessageEditing = () => { + const [editedMessage, setEditedMessage] = useState(); + + const cancelMessageEditing = useCallback((callback?: () => void) => { + setEditedMessage(undefined); + callback?.(); + }, []); + + const editMessage = useCallback((messageEntity: ContentMessage) => { + setEditedMessage(messageEntity); + }, []); + + useEffect(() => { + amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.EDIT, editMessage); + + return () => { + amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.EDIT); + }; + }, [editMessage]); + + return { + editedMessage, + editMessage, + isEditing: !!editedMessage, + cancelMessageEditing, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useMessageHandling.ts b/src/script/components/InputBar/useMessageHandling/useMessageHandling.ts new file mode 100644 index 00000000000..5cb24f45af5 --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useMessageHandling.ts @@ -0,0 +1,234 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect} from 'react'; + +import {amplify} from 'amplify'; +import {LexicalEditor} from 'lexical'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {ConversationRepository} from 'src/script/conversation/ConversationRepository'; +import {MessageRepository} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; +import {EventRepository} from 'src/script/event/EventRepository'; +import {StorageRepository} from 'src/script/storage'; + +import {useDraftState} from './useDraftState/useDraftState'; +import {useMessageEditing} from './useMessageEditing/useMessageEditing'; +import {useMessageReply} from './useMessageReply/useMessageReply'; +import {useMessageSend} from './useMessageSend/useMessageSend'; +import {useOutsideInputClick} from './useOutsideInputClick/useOutsideInputClick'; + +import {MessageContent} from '../common/messageContent/messageContent'; + +interface UseMessageHandlingProps { + messageContent: MessageContent; + conversation: Conversation; + conversationRepository: ConversationRepository; + eventRepository: EventRepository; + messageRepository: MessageRepository; + storageRepository: StorageRepository; + editorRef: React.RefObject; + pastedFile: File | null; + sendPastedFile: () => void; +} + +export const useMessageHandling = ({ + messageContent, + conversation, + conversationRepository, + eventRepository, + messageRepository, + storageRepository, + editorRef, + pastedFile, + sendPastedFile, +}: UseMessageHandlingProps) => { + const {isEditing, editedMessage, editMessage: editMessageCallback, cancelMessageEditing} = useMessageEditing(); + + const {isReplying, replyMessage: replyMessageCallback, replyMessageEntity} = useMessageReply(); + + const draftState = useDraftState({ + conversation, + storageRepository, + messageRepository, + editorRef, + editedMessageId: editedMessage?.id, + replyMessageEntityId: replyMessageEntity?.id, + onLoad: draftState => { + const reply = draftState.messageReply; + + if (reply?.isReplyable()) { + replyMessageCallback(reply); + } + + const editedMessage = draftState.editedMessage; + if (editedMessage) { + editMessageCallback(editedMessage); + } + }, + }); + + const handleSaveDraft = useCallback( + async (replyId?: string) => { + await draftState.save(JSON.stringify(editorRef.current?.getEditorState().toJSON()), messageContent.text, replyId); + }, + [draftState, editorRef, messageContent.text], + ); + + const cancelMessageReply = useCallback( + (resetDraft = true) => { + replyMessageCallback(null); + void handleSaveDraft(); + + if (resetDraft) { + draftState.reset(); + } + }, + [draftState, handleSaveDraft, replyMessageCallback], + ); + + const cancelMesssageEditingWithDraftReset = useCallback(() => { + cancelMessageEditing(() => { + replyMessageCallback(null); + draftState.reset(); + }); + }, [cancelMessageEditing, draftState, replyMessageCallback]); + + const {sendMessage, generateQuote} = useMessageSend({ + replyMessageEntity, + eventRepository, + messageRepository, + conversation, + conversationRepository, + draftState, + cancelMessageEditing, + cancelMessageReply, + editedMessage, + replyMessageCallback, + editorRef, + pastedFile, + sendPastedFile, + messageContent, + }); + + const editMessage = useCallback( + (messageEntity?: ContentMessage) => { + if (messageEntity?.isEditable() && messageEntity !== editedMessage) { + cancelMessageReply(); + cancelMesssageEditingWithDraftReset(); + editMessageCallback(messageEntity); + + const quote = messageEntity.quote(); + if (quote && conversation) { + void messageRepository + .getMessageInConversationById(conversation, quote.messageId) + .then(quotedMessage => replyMessageCallback(quotedMessage)); + } + } + }, + [ + cancelMessageReply, + cancelMesssageEditingWithDraftReset, + conversation, + editMessageCallback, + editedMessage, + messageRepository, + replyMessageCallback, + ], + ); + + const replyMessage = useCallback( + (messageEntity: ContentMessage) => { + if (messageEntity?.isReplyable() && messageEntity !== replyMessageEntity) { + cancelMessageReply(false); + cancelMessageEditing(() => { + if (isEditing) { + draftState.reset(); + } + }); + + replyMessageCallback(messageEntity); + void handleSaveDraft(messageEntity.id); + + editorRef.current?.focus(); + } + }, + [ + cancelMessageEditing, + cancelMessageReply, + draftState, + editorRef, + handleSaveDraft, + isEditing, + replyMessageCallback, + replyMessageEntity, + ], + ); + + const cancelSending = useCallback(() => { + if (editedMessage) { + cancelMesssageEditingWithDraftReset(); + } else if (replyMessageEntity) { + cancelMessageReply(); + } + }, [editedMessage, replyMessageEntity, cancelMesssageEditingWithDraftReset, cancelMessageReply]); + + useEffect(() => { + amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.REPLY, replyMessage); + + return () => { + amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.REPLY); + }; + }, [replyMessage]); + + useOutsideInputClick({ + isEditing, + callback: () => { + cancelMesssageEditingWithDraftReset(); + cancelMessageReply(); + }, + }); + + useEffect(() => { + conversation.isTextInputReady(true); + + return () => { + conversation.isTextInputReady(false); + }; + }, [conversation]); + + return { + draftState, + editedMessage, + replyMessageEntity, + isEditing, + isReplying, + sendMessage, + cancelSending, + cancelMessageEditing, + cancelMessageReply, + cancelMesssageEditing: cancelMesssageEditingWithDraftReset, + editMessage, + replyMessage, + generateQuote, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useMessageReply/useMessageReply.ts b/src/script/components/InputBar/useMessageHandling/useMessageReply/useMessageReply.ts new file mode 100644 index 00000000000..1cdc6a7b86b --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useMessageReply/useMessageReply.ts @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect, useState} from 'react'; + +import {amplify} from 'amplify'; + +import {WebAppEvents} from '@wireapp/webapp-events'; + +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; + +export const useMessageReply = () => { + const [replyMessageEntity, setReplyMessageEntity] = useState(null); + + const replyMessage = useCallback((messageEntity: ContentMessage | null) => { + setReplyMessageEntity(messageEntity); + }, []); + + const handleRepliedMessageDeleted = useCallback( + (messageId: string) => { + if (replyMessageEntity?.id === messageId) { + replyMessage(null); + } + }, + [replyMessageEntity?.id, replyMessage], + ); + + const handleRepliedMessageUpdated = useCallback( + (originalMessageId: string, messageEntity: ContentMessage) => { + if (replyMessageEntity?.id === originalMessageId) { + replyMessage(messageEntity); + } + }, + [replyMessageEntity, replyMessage], + ); + + useEffect(() => { + amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.REMOVED, handleRepliedMessageDeleted); + amplify.subscribe(WebAppEvents.CONVERSATION.MESSAGE.UPDATED, handleRepliedMessageUpdated); + + return () => { + amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.REMOVED); + amplify.unsubscribeAll(WebAppEvents.CONVERSATION.MESSAGE.UPDATED); + }; + }, [handleRepliedMessageDeleted, handleRepliedMessageUpdated, replyMessage]); + + return { + isReplying: !!replyMessageEntity, + replyMessageEntity, + replyMessage, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useMessageSend/useMessageSend.ts b/src/script/components/InputBar/useMessageHandling/useMessageSend/useMessageSend.ts new file mode 100644 index 00000000000..3499016ee25 --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useMessageSend/useMessageSend.ts @@ -0,0 +1,212 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback} from 'react'; + +import {LexicalEditor} from 'lexical'; + +import {MessageContent} from 'Components/InputBar/common/messageContent/messageContent'; +import {PrimaryModal} from 'Components/Modals/PrimaryModal'; +import {showWarningModal} from 'Components/Modals/utils/showWarningModal'; +import {Config} from 'src/script/Config'; +import {ConversationRepository} from 'src/script/conversation/ConversationRepository'; +import {ConversationVerificationState} from 'src/script/conversation/ConversationVerificationState'; +import {MessageRepository, OutgoingQuote} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; +import {ContentMessage} from 'src/script/entity/message/ContentMessage'; +import {ConversationError} from 'src/script/error/ConversationError'; +import {EventRepository} from 'src/script/event/EventRepository'; +import {MentionEntity} from 'src/script/message/MentionEntity'; +import {MessageHasher} from 'src/script/message/MessageHasher'; +import {QuoteEntity} from 'src/script/message/QuoteEntity'; +import {t} from 'Util/LocalizerUtil'; + +interface UseMessageSendProps { + replyMessageEntity: ContentMessage | null; + eventRepository: EventRepository; + messageRepository: MessageRepository; + conversation: Conversation; + conversationRepository: ConversationRepository; + draftState: { + reset: () => void; + }; + cancelMessageEditing: (callback?: () => void) => void; + cancelMessageReply: () => void; + editedMessage: ContentMessage | undefined; + replyMessageCallback: (messageEntity: ContentMessage | null) => void; + editorRef: React.RefObject; + pastedFile: File | null; + sendPastedFile: () => void; + messageContent: MessageContent; +} + +export const useMessageSend = ({ + replyMessageEntity, + eventRepository, + messageRepository, + conversation, + conversationRepository, + draftState, + cancelMessageEditing, + cancelMessageReply, + editedMessage, + replyMessageCallback, + editorRef, + pastedFile, + sendPastedFile, + messageContent, +}: UseMessageSendProps) => { + const generateQuote = useCallback(async (): Promise => { + return !replyMessageEntity + ? Promise.resolve(undefined) + : eventRepository.eventService + .loadEvent(replyMessageEntity.conversation_id, replyMessageEntity.id) + .then(MessageHasher.hashEvent) + .then((messageHash: ArrayBuffer) => { + return new QuoteEntity({ + hash: messageHash, + messageId: replyMessageEntity.id, + userId: replyMessageEntity.from, + }) as OutgoingQuote; + }); + }, [eventRepository.eventService, replyMessageEntity]); + + const sendMessageEdit = useCallback( + (messageText: string, mentions: MentionEntity[]): void | Promise => { + const mentionEntities = mentions.slice(0); + cancelMessageEditing(() => { + replyMessageCallback(null); + draftState.reset(); + }); + + if (!messageText.length && editedMessage) { + return messageRepository.deleteMessageForEveryone(conversation, editedMessage); + } + + if (editedMessage) { + messageRepository.sendMessageEdit(conversation, messageText, editedMessage, mentionEntities).catch(error => { + if (error.type !== ConversationError.TYPE.NO_MESSAGE_CHANGES) { + throw error; + } + }); + + cancelMessageReply(); + } + }, + [ + cancelMessageEditing, + cancelMessageReply, + conversation, + draftState, + editedMessage, + messageRepository, + replyMessageCallback, + ], + ); + + const sendTextMessage = useCallback( + (messageText: string, mentions: MentionEntity[]) => { + if (messageText.length) { + const mentionEntities = mentions.slice(0); + + void generateQuote().then(quoteEntity => { + void messageRepository.sendTextWithLinkPreview(conversation, messageText, mentionEntities, quoteEntity); + cancelMessageReply(); + }); + } + }, + [cancelMessageReply, conversation, generateQuote, messageRepository], + ); + + const sendMessage = useCallback((): void => { + if (pastedFile) { + return void sendPastedFile(); + } + + const text = messageContent.text; + const mentions = messageContent.mentions ?? []; + + const messageTrimmedStart = text.trimStart(); + const messageText = messageTrimmedStart.trimEnd(); + + const config = Config.getConfig(); + + const isMessageTextTooLong = text.length > config.MAXIMUM_MESSAGE_LENGTH; + + if (isMessageTextTooLong) { + showWarningModal( + t('modalConversationMessageTooLongHeadline'), + t('modalConversationMessageTooLongMessage', {number: config.MAXIMUM_MESSAGE_LENGTH}), + ); + + return; + } + + if (editedMessage) { + void sendMessageEdit(messageText, mentions); + } else { + sendTextMessage(messageText, mentions); + } + + editorRef.current?.focus(); + draftState.reset(); + }, [ + pastedFile, + messageContent.text, + messageContent.mentions, + editedMessage, + editorRef, + draftState, + sendPastedFile, + sendMessageEdit, + sendTextMessage, + ]); + + const handleSendMessage = useCallback(async () => { + await conversationRepository.refreshMLSConversationVerificationState(conversation); + const isE2EIDegraded = conversation.mlsVerificationState() === ConversationVerificationState.DEGRADED; + + if (isE2EIDegraded) { + PrimaryModal.show(PrimaryModal.type.CONFIRM, { + secondaryAction: { + action: () => { + conversation.mlsVerificationState(ConversationVerificationState.UNVERIFIED); + sendMessage(); + }, + text: t('conversation.E2EISendAnyway'), + }, + primaryAction: { + action: () => {}, + text: t('conversation.E2EICancel'), + }, + text: { + message: t('conversation.E2EIDegradedNewMessage'), + title: t('conversation.E2EIConversationNoLongerVerified'), + }, + }); + } else { + sendMessage(); + } + }, [conversation, conversationRepository, sendMessage]); + + return { + sendMessage: handleSendMessage, + generateQuote, + }; +}; diff --git a/src/script/components/InputBar/useMessageHandling/useOutsideInputClick/useOutsideInputClick.ts b/src/script/components/InputBar/useMessageHandling/useOutsideInputClick/useOutsideInputClick.ts new file mode 100644 index 00000000000..5c34ec8f363 --- /dev/null +++ b/src/script/components/InputBar/useMessageHandling/useOutsideInputClick/useOutsideInputClick.ts @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useEffect} from 'react'; + +import {handleClickOutsideOfInputBar} from 'Components/InputBar/util/clickHandlers'; + +interface UseOutsideInputClickParams { + isEditing: boolean; + callback: () => void; +} + +export const useOutsideInputClick = ({isEditing, callback}: UseOutsideInputClickParams) => { + useEffect(() => { + const onWindowClick = (event: Event): void => + handleClickOutsideOfInputBar(event, () => { + // We want to add a timeout in case the click happens because the user switched conversation and the component is unmounting. + // In this case we want to keep the edited message for this conversation + setTimeout(() => { + callback(); + }); + }); + if (isEditing) { + window.addEventListener('click', onWindowClick); + + return () => { + window.removeEventListener('click', onWindowClick); + }; + } + + return () => undefined; + }, [callback, isEditing]); +}; diff --git a/src/script/components/InputBar/usePing/usePing.ts b/src/script/components/InputBar/usePing/usePing.ts new file mode 100644 index 00000000000..7fee180f3b4 --- /dev/null +++ b/src/script/components/InputBar/usePing/usePing.ts @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useState} from 'react'; + +import {PrimaryModal} from 'Components/Modals/PrimaryModal'; +import {Config} from 'src/script/Config'; +import {MessageRepository} from 'src/script/conversation/MessageRepository'; +import {Conversation} from 'src/script/entity/Conversation'; +import {t} from 'Util/LocalizerUtil'; +import {TIME_IN_MILLIS} from 'Util/TimeUtil'; + +interface UsePingProps { + conversation: Conversation; + messageRepository: MessageRepository; + is1to1: boolean; +} + +export const usePing = ({conversation, messageRepository, is1to1}: UsePingProps) => { + const [isPingDisabled, setIsPingDisabled] = useState(false); + + const maxUsersWithoutAlert = Config.getConfig().FEATURE.MAX_USERS_TO_PING_WITHOUT_ALERT; + const enablePingConfirmation = Config.getConfig().FEATURE.ENABLE_PING_CONFIRMATION; + + const pingConversation = () => { + setIsPingDisabled(true); + void messageRepository.sendPing(conversation).then(() => { + window.setTimeout(() => setIsPingDisabled(false), TIME_IN_MILLIS.SECOND * 2); + }); + }; + + const handlePing = () => { + if (isPingDisabled) { + return; + } + + const totalConversationUsers = conversation.participating_user_ets().length; + if (!enablePingConfirmation || is1to1 || totalConversationUsers < maxUsersWithoutAlert) { + pingConversation(); + } else { + PrimaryModal.show(PrimaryModal.type.CONFIRM, { + primaryAction: { + action: pingConversation, + text: t('tooltipConversationPing'), + }, + text: { + title: t('conversationPingConfirmTitle', {memberCount: totalConversationUsers.toString()}), + }, + }); + } + }; + + return { + isPingDisabled, + handlePing, + }; +}; diff --git a/src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.test.ts b/src/script/components/InputBar/useTypingIndicator/useTypingIndicator.test.ts similarity index 98% rename from src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.test.ts rename to src/script/components/InputBar/useTypingIndicator/useTypingIndicator.test.ts index 99f728ed871..2f050ef277c 100644 --- a/src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.test.ts +++ b/src/script/components/InputBar/useTypingIndicator/useTypingIndicator.test.ts @@ -21,7 +21,7 @@ import {fireEvent, renderHook} from '@testing-library/react'; import {useTypingIndicator} from './useTypingIndicator'; -import {TYPING_TIMEOUT} from '../../components/TypingIndicator'; +import {TYPING_TIMEOUT} from '../TypingIndicator'; describe('useTypingIndicator', () => { beforeAll(() => { diff --git a/src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.ts b/src/script/components/InputBar/useTypingIndicator/useTypingIndicator.ts similarity index 97% rename from src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.ts rename to src/script/components/InputBar/useTypingIndicator/useTypingIndicator.ts index 4130ff59140..0ead274b45c 100644 --- a/src/script/components/InputBar/hooks/useTypingIndicator/useTypingIndicator.ts +++ b/src/script/components/InputBar/useTypingIndicator/useTypingIndicator.ts @@ -19,7 +19,7 @@ import {useCallback, useEffect, useRef} from 'react'; -import {TYPING_TIMEOUT} from '../../components/TypingIndicator'; +import {TYPING_TIMEOUT} from '../TypingIndicator'; type TypingIndicatorProps = { text: string; diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts index f196f6cdf80..90e5a01e5e6 100644 --- a/src/script/conversation/ConversationRepository.ts +++ b/src/script/conversation/ConversationRepository.ts @@ -58,7 +58,7 @@ import {flatten} from 'underscore'; import {Asset as ProtobufAsset, Confirmation, LegalHoldStatus} from '@wireapp/protocol-messaging'; import {WebAppEvents} from '@wireapp/webapp-events'; -import {TYPING_TIMEOUT, useTypingIndicatorState} from 'Components/InputBar/components/TypingIndicator'; +import {TYPING_TIMEOUT, useTypingIndicatorState} from 'Components/InputBar/TypingIndicator'; import {getNextItem} from 'Util/ArrayUtil'; import {allowsAllFiles, getFileExtensionOrName, isAllowedFile} from 'Util/FileTypeUtil'; import {replaceLink, t} from 'Util/LocalizerUtil';