signal-desktop/ts/components/CompositionArea.tsx

680 lines
19 KiB
TypeScript
Raw Normal View History

// Copyright 2019-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { MutableRefObject } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
2021-09-29 20:23:06 +00:00
import { get } from 'lodash';
2019-08-06 19:18:37 +00:00
import classNames from 'classnames';
import type {
BodyRangeType,
BodyRangesType,
LocalizerType,
2021-11-02 23:01:13 +00:00
ThemeType,
} from '../types/Util';
import type {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../state/ducks/audioRecorder';
2021-09-29 20:23:06 +00:00
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
import { Spinner } from './Spinner';
import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
import { EmojiButton } from './emoji/EmojiButton';
import type { Props as StickerButtonProps } from './stickers/StickerButton';
import { StickerButton } from './stickers/StickerButton';
import type {
InputApi,
Props as CompositionInputProps,
} from './CompositionInput';
import { CompositionInput } from './CompositionInput';
import type { Props as MessageRequestActionsProps } from './conversation/MessageRequestActions';
import { MessageRequestActions } from './conversation/MessageRequestActions';
import type { PropsType as GroupV1DisabledActionsPropsType } from './conversation/GroupV1DisabledActions';
import { GroupV1DisabledActions } from './conversation/GroupV1DisabledActions';
import type { PropsType as GroupV2PendingApprovalActionsPropsType } from './conversation/GroupV2PendingApprovalActions';
import { GroupV2PendingApprovalActions } from './conversation/GroupV2PendingApprovalActions';
2021-09-29 20:23:06 +00:00
import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner';
2021-06-25 16:08:16 +00:00
import { AttachmentList } from './conversation/AttachmentList';
import type { AttachmentType } from '../types/Attachment';
import { isImageAttachment } from '../types/Attachment';
2021-09-29 20:23:06 +00:00
import { AudioCapture } from './conversation/AudioCapture';
import { CompositionUpload } from './CompositionUpload';
import type { ConversationType } from '../state/ducks/conversations';
import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { LinkPreviewWithDomain } from '../types/LinkPreview';
2021-09-29 20:23:06 +00:00
import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileSharingActions';
2021-06-25 16:08:16 +00:00
import { MediaQualitySelector } from './MediaQualitySelector';
import type { Props as QuoteProps } from './conversation/Quote';
import { Quote } from './conversation/Quote';
2021-06-25 16:08:16 +00:00
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
2021-09-29 20:23:06 +00:00
import { countStickers } from './stickers/lib';
import {
useAttachFileShortcut,
useKeyboardShortcuts,
} from '../hooks/useKeyboardShortcuts';
2021-10-05 16:47:06 +00:00
export type CompositionAPIType =
| {
focusInput: () => void;
isDirty: () => boolean;
setDisabled: (disabled: boolean) => void;
reset: InputApi['reset'];
resetEmojiResults: InputApi['resetEmojiResults'];
}
| undefined;
2021-08-30 21:32:56 +00:00
export type OwnProps = Readonly<{
2021-09-24 20:02:30 +00:00
acceptedMessageRequest?: boolean;
addAttachment: (
conversationId: string,
attachment: AttachmentType
) => unknown;
addPendingAttachment: (
conversationId: string,
pendingAttachment: AttachmentType
) => unknown;
announcementsOnly?: boolean;
areWeAdmin?: boolean;
2021-09-24 20:02:30 +00:00
areWePending?: boolean;
areWePendingApproval?: boolean;
2021-09-29 20:23:06 +00:00
cancelRecording: () => unknown;
completeRecording: (
conversationId: string,
onSendAudioRecording?: (rec: AttachmentType) => unknown
) => unknown;
2021-09-24 20:02:30 +00:00
compositionApi?: MutableRefObject<CompositionAPIType>;
conversationId: string;
draftAttachments: ReadonlyArray<AttachmentType>;
2021-09-29 20:23:06 +00:00
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
groupAdmins: Array<ConversationType>;
groupVersion?: 1 | 2;
2021-09-24 20:02:30 +00:00
i18n: LocalizerType;
isFetchingUUID?: boolean;
isGroupV1AndDisabled?: boolean;
isMissingMandatoryProfileSharing?: boolean;
recordingState: RecordingState;
isSMSOnly?: boolean;
left?: boolean;
2021-09-24 20:02:30 +00:00
linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewWithDomain;
messageRequestsEnabled?: boolean;
2021-06-25 16:08:16 +00:00
onClearAttachments(): unknown;
2021-09-24 20:02:30 +00:00
onClickQuotedMessage(): unknown;
onCloseLinkPreview(): unknown;
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
2021-06-25 16:08:16 +00:00
onSelectMediaQuality(isHQ: boolean): unknown;
2021-09-29 20:23:06 +00:00
onSendMessage(options: {
draftAttachments?: ReadonlyArray<AttachmentType>;
mentions?: BodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
}): unknown;
2021-09-24 20:02:30 +00:00
openConversation(conversationId: string): unknown;
quotedMessageProps?: Omit<
QuoteProps,
'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
>;
2021-09-24 20:02:30 +00:00
removeAttachment: (conversationId: string, filePath: string) => unknown;
2021-06-25 16:08:16 +00:00
setQuotedMessage(message: undefined): unknown;
2021-09-24 20:02:30 +00:00
shouldSendHighQualityAttachments: boolean;
2021-09-29 20:23:06 +00:00
startRecording: () => unknown;
scrollToBottom: (converstionId: string) => unknown;
2021-11-02 23:01:13 +00:00
theme: ThemeType;
}>;
2019-08-06 19:18:37 +00:00
export type Props = Pick<
CompositionInputProps,
| 'sortedGroupMembers'
| 'onEditorStateChange'
| 'onTextTooLong'
2020-11-03 01:19:52 +00:00
| 'draftText'
| 'draftBodyRanges'
2020-07-01 18:05:41 +00:00
| 'clearQuotedMessage'
| 'getQuotedMessage'
2019-08-06 19:18:37 +00:00
> &
Pick<
EmojiButtonProps,
'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
> &
Pick<
StickerButtonProps,
| 'knownPacks'
| 'receivedPacks'
| 'installedPack'
| 'installedPacks'
| 'blessedPacks'
| 'recentStickers'
| 'clearInstalledStickerPack'
| 'onClickAddPack'
| 'onPickSticker'
| 'clearShowIntroduction'
| 'showPickerHint'
| 'clearShowPickerHint'
> &
2020-05-27 21:37:06 +00:00
MessageRequestActionsProps &
Pick<GroupV1DisabledActionsPropsType, 'onStartGroupMigration'> &
Pick<GroupV2PendingApprovalActionsPropsType, 'onCancelJoinRequest'> &
OwnProps;
export const CompositionArea = ({
2021-09-24 20:02:30 +00:00
// Base props
addAttachment,
addPendingAttachment,
conversationId,
i18n,
2021-09-29 20:23:06 +00:00
onSendMessage,
2021-09-24 20:02:30 +00:00
processAttachments,
removeAttachment,
2021-11-02 23:01:13 +00:00
theme,
2021-09-24 20:02:30 +00:00
2021-06-25 16:08:16 +00:00
// AttachmentList
draftAttachments,
onClearAttachments,
2021-09-29 20:23:06 +00:00
// AudioCapture
cancelRecording,
completeRecording,
errorDialogAudioRecorderType,
errorRecording,
recordingState,
2021-09-29 20:23:06 +00:00
startRecording,
2021-06-25 16:08:16 +00:00
// StagedLinkPreview
linkPreviewLoading,
linkPreviewResult,
onCloseLinkPreview,
// Quote
quotedMessageProps,
onClickQuotedMessage,
setQuotedMessage,
// MediaQualitySelector
onSelectMediaQuality,
shouldSendHighQualityAttachments,
// CompositionInput
compositionApi,
onEditorStateChange,
onTextTooLong,
2020-11-03 01:19:52 +00:00
draftText,
draftBodyRanges,
2020-05-27 21:37:06 +00:00
clearQuotedMessage,
getQuotedMessage,
scrollToBottom,
sortedGroupMembers,
// EmojiButton
onPickEmoji,
onSetSkinTone,
recentEmojis,
skinTone,
// StickerButton
knownPacks,
receivedPacks,
installedPack,
installedPacks,
blessedPacks,
recentStickers,
clearInstalledStickerPack,
onClickAddPack,
onPickSticker,
clearShowIntroduction,
showPickerHint,
clearShowPickerHint,
2020-05-27 21:37:06 +00:00
// Message Requests
acceptedMessageRequest,
areWePending,
areWePendingApproval,
2020-05-27 21:37:06 +00:00
conversationType,
groupVersion,
2020-05-27 21:37:06 +00:00
isBlocked,
isMissingMandatoryProfileSharing,
left,
2020-07-24 01:35:32 +00:00
messageRequestsEnabled,
2020-05-27 21:37:06 +00:00
onAccept,
onBlock,
onBlockAndReportSpam,
2020-05-27 21:37:06 +00:00
onDelete,
2020-07-24 01:35:32 +00:00
onUnblock,
title,
// GroupV1 Disabled Actions
isGroupV1AndDisabled,
onStartGroupMigration,
2021-07-20 20:18:35 +00:00
// GroupV2
announcementsOnly,
areWeAdmin,
groupAdmins,
onCancelJoinRequest,
2021-07-20 20:18:35 +00:00
openConversation,
// SMS-only contacts
isSMSOnly,
isFetchingUUID,
2020-09-12 00:46:52 +00:00
}: Props): JSX.Element => {
2021-09-24 20:02:30 +00:00
const [disabled, setDisabled] = useState(false);
const [dirty, setDirty] = useState(false);
const [large, setLarge] = useState(false);
const inputApiRef = useRef<InputApi | undefined>();
const fileInputRef = useRef<null | HTMLInputElement>(null);
const handleForceSend = useCallback(() => {
2020-01-08 17:44:54 +00:00
setLarge(false);
if (inputApiRef.current) {
inputApiRef.current.submit();
}
}, [inputApiRef, setLarge]);
2019-08-06 19:18:37 +00:00
2021-09-29 20:23:06 +00:00
const handleSubmit = useCallback(
(message: string, mentions: Array<BodyRangeType>, timestamp: number) => {
2019-08-06 19:18:37 +00:00
setLarge(false);
2021-09-29 20:23:06 +00:00
onSendMessage({
draftAttachments,
mentions,
message,
timestamp,
});
2019-08-06 19:18:37 +00:00
},
2021-09-29 20:23:06 +00:00
[draftAttachments, onSendMessage, setLarge]
);
const launchAttachmentPicker = useCallback(() => {
2021-09-24 20:02:30 +00:00
const fileInput = fileInputRef.current;
if (fileInput) {
// Setting the value to empty so that onChange always fires in case
// you add multiple photos.
fileInput.value = '';
fileInput.click();
}
}, []);
const attachFileShortcut = useAttachFileShortcut(launchAttachmentPicker);
useKeyboardShortcuts(attachFileShortcut);
2021-09-24 20:02:30 +00:00
const focusInput = useCallback(() => {
if (inputApiRef.current) {
inputApiRef.current.focus();
2020-01-08 17:44:54 +00:00
}
}, [inputApiRef]);
const withStickers =
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) > 0;
if (compositionApi) {
2020-09-12 00:46:52 +00:00
// Using a React.MutableRefObject, so we need to reassign this prop.
// eslint-disable-next-line no-param-reassign
compositionApi.current = {
2019-11-07 21:36:16 +00:00
isDirty: () => dirty,
focusInput,
setDisabled,
reset: () => {
if (inputApiRef.current) {
inputApiRef.current.reset();
}
},
resetEmojiResults: () => {
if (inputApiRef.current) {
inputApiRef.current.resetEmojiResults();
}
},
};
}
2021-09-24 20:02:30 +00:00
const insertEmoji = useCallback(
(e: EmojiPickDataType) => {
if (inputApiRef.current) {
inputApiRef.current.insertEmoji(e);
onPickEmoji(e);
}
},
[inputApiRef, onPickEmoji]
);
2021-09-24 20:02:30 +00:00
const handleToggleLarge = useCallback(() => {
2020-01-08 17:44:54 +00:00
setLarge(l => !l);
}, [setLarge]);
2019-08-06 19:18:37 +00:00
2021-10-05 16:47:06 +00:00
const shouldShowMicrophone = !large && !draftAttachments.length && !draftText;
2021-09-24 20:02:30 +00:00
2021-06-25 16:08:16 +00:00
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
const leftHandSideButtonsFragment = (
<>
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__button-cell">
2021-06-25 16:08:16 +00:00
<EmojiButton
i18n={i18n}
doSend={handleForceSend}
onPickEmoji={insertEmoji}
onClose={focusInput}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</div>
{showMediaQualitySelector ? (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__button-cell">
2021-06-25 16:08:16 +00:00
<MediaQualitySelector
i18n={i18n}
isHighQuality={shouldSendHighQualityAttachments}
onSelectQuality={onSelectMediaQuality}
/>
</div>
) : null}
</>
2019-08-06 19:18:37 +00:00
);
2021-09-24 20:02:30 +00:00
const micButtonFragment = shouldShowMicrophone ? (
2021-09-29 20:23:06 +00:00
<AudioCapture
cancelRecording={cancelRecording}
completeRecording={completeRecording}
conversationId={conversationId}
draftAttachments={draftAttachments}
errorDialogAudioRecorderType={errorDialogAudioRecorderType}
errorRecording={errorRecording}
i18n={i18n}
recordingState={recordingState}
2021-09-29 20:23:06 +00:00
onSendAudioRecording={(voiceNoteAttachment: AttachmentType) => {
onSendMessage({ voiceNoteAttachment });
}}
startRecording={startRecording}
2019-08-06 19:18:37 +00:00
/>
) : null;
2019-08-07 00:40:25 +00:00
const attButton = (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__button-cell">
2021-10-05 16:47:06 +00:00
<button
type="button"
className="CompositionArea__attach-file"
onClick={launchAttachmentPicker}
aria-label={i18n('CompositionArea--attach-file')}
/>
2019-08-07 00:40:25 +00:00
</div>
2019-08-06 19:18:37 +00:00
);
const sendButtonFragment = (
<div
className={classNames(
2021-07-20 20:18:35 +00:00
'CompositionArea__button-cell',
large ? 'CompositionArea__button-cell--large-right' : null
2019-08-06 19:18:37 +00:00
)}
>
<button
2020-09-12 00:46:52 +00:00
type="button"
2021-07-20 20:18:35 +00:00
className="CompositionArea__send-button"
2019-08-06 19:18:37 +00:00
onClick={handleForceSend}
2020-09-12 00:46:52 +00:00
aria-label={i18n('sendMessageToContact')}
2019-08-06 19:18:37 +00:00
/>
</div>
);
const stickerButtonPlacement = large ? 'top-start' : 'top-end';
const stickerButtonFragment = withStickers ? (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__button-cell">
2019-08-06 19:18:37 +00:00
<StickerButton
i18n={i18n}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
installedPack={installedPack}
2019-08-06 19:18:37 +00:00
installedPacks={installedPacks}
blessedPacks={blessedPacks}
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={onClickAddPack}
onPickSticker={onPickSticker}
clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint}
position={stickerButtonPlacement}
/>
</div>
) : null;
// Listen for cmd/ctrl-shift-x to toggle large composition mode
2021-09-24 20:02:30 +00:00
useEffect(() => {
2020-01-08 17:44:54 +00:00
const handler = (e: KeyboardEvent) => {
const { key, shiftKey, ctrlKey, metaKey } = e;
// When using the ctrl key, `key` is `'X'`. When using the cmd key, `key` is `'x'`
const xKey = key === 'x' || key === 'X';
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey;
2020-01-08 17:44:54 +00:00
// cmd/ctrl-shift-x
if (xKey && shiftKey && commandOrCtrl) {
e.preventDefault();
setLarge(x => !x);
}
};
2020-01-08 17:44:54 +00:00
document.addEventListener('keydown', handler);
2020-01-08 17:44:54 +00:00
return () => {
document.removeEventListener('keydown', handler);
};
}, [setLarge]);
if (
2020-11-20 17:30:45 +00:00
isBlocked ||
areWePending ||
(messageRequestsEnabled && !acceptedMessageRequest)
) {
2020-05-27 21:37:06 +00:00
return (
<MessageRequestActions
i18n={i18n}
conversationType={conversationType}
isBlocked={isBlocked}
onBlock={onBlock}
onBlockAndReportSpam={onBlockAndReportSpam}
2020-05-27 21:37:06 +00:00
onUnblock={onUnblock}
onDelete={onDelete}
onAccept={onAccept}
2020-07-24 01:35:32 +00:00
title={title}
2020-05-27 21:37:06 +00:00
/>
);
}
if (conversationType === 'direct' && isSMSOnly) {
return (
<div
className={classNames([
2021-07-20 20:18:35 +00:00
'CompositionArea',
'CompositionArea--sms-only',
isFetchingUUID ? 'CompositionArea--pending' : null,
])}
>
{isFetchingUUID ? (
<Spinner
ariaLabel={i18n('CompositionArea--sms-only__spinner-label')}
role="presentation"
moduleClassName="module-image-spinner"
svgSize="small"
/>
) : (
<>
2021-07-20 20:18:35 +00:00
<h2 className="CompositionArea--sms-only__title">
{i18n('CompositionArea--sms-only__title')}
</h2>
2021-07-20 20:18:35 +00:00
<p className="CompositionArea--sms-only__body">
{i18n('CompositionArea--sms-only__body')}
</p>
</>
)}
</div>
);
}
// If no message request, but we haven't shared profile yet, we show profile-sharing UI
if (
!left &&
(conversationType === 'direct' ||
(conversationType === 'group' && groupVersion === 1)) &&
isMissingMandatoryProfileSharing
) {
return (
<MandatoryProfileSharingActions
i18n={i18n}
conversationType={conversationType}
onBlock={onBlock}
onBlockAndReportSpam={onBlockAndReportSpam}
onDelete={onDelete}
onAccept={onAccept}
title={title}
/>
);
}
// If this is a V1 group, now disabled entirely, we show UI to help them upgrade
if (!left && isGroupV1AndDisabled) {
return (
<GroupV1DisabledActions
i18n={i18n}
onStartGroupMigration={onStartGroupMigration}
/>
);
}
if (areWePendingApproval) {
return (
<GroupV2PendingApprovalActions
i18n={i18n}
onCancelJoinRequest={onCancelJoinRequest}
/>
);
}
2021-07-20 20:18:35 +00:00
if (announcementsOnly && !areWeAdmin) {
return (
<AnnouncementsOnlyGroupBanner
groupAdmins={groupAdmins}
i18n={i18n}
openConversation={openConversation}
2021-11-02 23:01:13 +00:00
theme={theme}
2021-07-20 20:18:35 +00:00
/>
);
}
return (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea">
<div className="CompositionArea__toggle-large">
2019-08-06 19:18:37 +00:00
<button
2020-09-12 00:46:52 +00:00
type="button"
2019-08-06 19:18:37 +00:00
className={classNames(
2021-07-20 20:18:35 +00:00
'CompositionArea__toggle-large__button',
large ? 'CompositionArea__toggle-large__button--large-active' : null
2019-08-06 19:18:37 +00:00
)}
2019-11-07 21:36:16 +00:00
// This prevents the user from tabbing here
tabIndex={-1}
2019-08-06 19:18:37 +00:00
onClick={handleToggleLarge}
2020-09-12 00:46:52 +00:00
aria-label={i18n('CompositionArea--expand')}
/>
</div>
2019-08-06 19:18:37 +00:00
<div
className={classNames(
2021-07-20 20:18:35 +00:00
'CompositionArea__row',
'CompositionArea__row--column'
2019-08-06 19:18:37 +00:00
)}
2021-06-25 16:08:16 +00:00
>
{quotedMessageProps && (
<div className="quote-wrapper">
<Quote
{...quotedMessageProps}
i18n={i18n}
onClick={onClickQuotedMessage}
onClose={() => {
// This one is for redux...
setQuotedMessage(undefined);
// and this is for conversation_view.
clearQuotedMessage();
}}
/>
</div>
)}
{linkPreviewLoading && (
<div className="preview-wrapper">
<StagedLinkPreview
{...(linkPreviewResult || {})}
i18n={i18n}
onClose={onCloseLinkPreview}
/>
</div>
)}
{draftAttachments.length ? (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__attachment-list">
2021-06-25 16:08:16 +00:00
<AttachmentList
attachments={draftAttachments}
i18n={i18n}
2021-09-24 20:02:30 +00:00
onAddAttachment={launchAttachmentPicker}
2021-06-25 16:08:16 +00:00
onClose={onClearAttachments}
2021-09-24 20:02:30 +00:00
onCloseAttachment={attachment => {
if (attachment.path) {
removeAttachment(conversationId, attachment.path);
}
}}
2021-06-25 16:08:16 +00:00
/>
</div>
) : null}
</div>
2019-08-06 19:18:37 +00:00
<div
className={classNames(
2021-07-20 20:18:35 +00:00
'CompositionArea__row',
large ? 'CompositionArea__row--padded' : null
2019-08-06 19:18:37 +00:00
)}
>
2021-06-25 16:08:16 +00:00
{!large ? leftHandSideButtonsFragment : null}
2021-07-20 20:18:35 +00:00
<div className="CompositionArea__input">
2019-08-06 19:18:37 +00:00
<CompositionInput
i18n={i18n}
conversationId={conversationId}
clearQuotedMessage={clearQuotedMessage}
2019-08-06 19:18:37 +00:00
disabled={disabled}
draftBodyRanges={draftBodyRanges}
draftText={draftText}
getQuotedMessage={getQuotedMessage}
2019-08-06 19:18:37 +00:00
inputApi={inputApiRef}
large={large}
onDirtyChange={setDirty}
onEditorStateChange={onEditorStateChange}
2019-08-06 19:18:37 +00:00
onPickEmoji={onPickEmoji}
onSubmit={handleSubmit}
onTextTooLong={onTextTooLong}
scrollToBottom={scrollToBottom}
2019-08-06 19:18:37 +00:00
skinTone={skinTone}
sortedGroupMembers={sortedGroupMembers}
/>
</div>
2019-08-06 19:18:37 +00:00
{!large ? (
<>
{stickerButtonFragment}
{!dirty ? micButtonFragment : null}
2019-08-07 00:40:25 +00:00
{attButton}
2019-08-06 19:18:37 +00:00
</>
) : null}
</div>
{large ? (
<div
className={classNames(
2021-07-20 20:18:35 +00:00
'CompositionArea__row',
'CompositionArea__row--control-row'
2019-08-06 19:18:37 +00:00
)}
>
2021-06-25 16:08:16 +00:00
{leftHandSideButtonsFragment}
2019-08-06 19:18:37 +00:00
{stickerButtonFragment}
2019-08-07 00:40:25 +00:00
{attButton}
2019-08-06 19:18:37 +00:00
{!dirty ? micButtonFragment : null}
2021-09-24 20:02:30 +00:00
{dirty || !shouldShowMicrophone ? sendButtonFragment : null}
2019-08-06 19:18:37 +00:00
</div>
) : null}
2021-09-24 20:02:30 +00:00
<CompositionUpload
addAttachment={addAttachment}
addPendingAttachment={addPendingAttachment}
conversationId={conversationId}
draftAttachments={draftAttachments}
i18n={i18n}
processAttachments={processAttachments}
removeAttachment={removeAttachment}
ref={fileInputRef}
/>
</div>
);
};