signal-desktop/ts/components/CompositionArea.tsx

1013 lines
30 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2019 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useRef, useState } from 'react';
2019-08-06 19:18:37 +00:00
import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest';
import type { DraftBodyRanges } from '../types/BodyRange';
import type { LocalizerType, ThemeType } from '../types/Util';
2023-01-21 00:31:30 +00:00
import type { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
import { RecordingState } from '../types/AudioRecorder';
2022-11-16 21:41:38 +00:00
import type { imageToBlurHash } from '../util/imageToBlurHash';
import { Spinner } from './Spinner';
import type {
Props as EmojiButtonProps,
EmojiButtonAPI,
} 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 {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../types/Attachment';
2023-03-02 20:55:40 +00:00
import { isImageAttachment, isVoiceMessage } from '../types/Attachment';
2021-09-29 20:23:06 +00:00
import { AudioCapture } from './conversation/AudioCapture';
import { CompositionUpload } from './CompositionUpload';
import type {
ConversationType,
PushPanelForConversationActionType,
ShowConversationType,
} from '../state/ducks/conversations';
import type { EmojiPickDataType } from './emoji/EmojiPicker';
2022-06-17 00:48:57 +00:00
import type { LinkPreviewType } from '../types/message/LinkPreviews';
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-09-29 20:23:06 +00:00
import { countStickers } from './stickers/lib';
import {
useAttachFileShortcut,
useEditLastMessageSent,
useKeyboardShortcutsConditionally,
} from '../hooks/useKeyboardShortcuts';
2021-12-01 02:14:25 +00:00
import { MediaEditor } from './MediaEditor';
import { isImageTypeSupported } from '../util/GoogleChrome';
import * as KeyboardLayout from '../services/keyboardLayout';
import { usePrevious } from '../hooks/usePrevious';
import { PanelType } from '../types/Panels';
2023-03-02 20:55:40 +00:00
import type { SmartCompositionRecordingDraftProps } from '../state/smart/CompositionRecordingDraft';
2023-01-19 22:59:58 +00:00
import { useEscapeHandling } from '../hooks/useEscapeHandling';
2023-03-02 20:55:40 +00:00
import type { SmartCompositionRecordingProps } from '../state/smart/CompositionRecording';
2023-03-20 22:23:53 +00:00
import SelectModeActions from './conversation/SelectModeActions';
import type { ShowToastAction } from '../state/ducks/toast';
import type { DraftEditMessageType } from '../model-types.d';
export type OwnProps = Readonly<{
2021-09-24 20:02:30 +00:00
acceptedMessageRequest?: boolean;
2023-04-05 20:48:00 +00:00
removalStage?: 'justNotification' | 'messageRequest';
2021-09-24 20:02:30 +00:00
addAttachment: (
conversationId: string,
attachment: InMemoryAttachmentDraftType
2021-09-24 20:02:30 +00:00
) => 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,
2023-03-02 20:55:40 +00:00
onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
2021-09-29 20:23:06 +00:00
) => unknown;
2021-09-24 20:02:30 +00:00
conversationId: string;
discardEditMessage: (id: string) => unknown;
draftEditMessage?: DraftEditMessageType;
2022-11-09 02:38:19 +00:00
uuid?: string;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
2021-09-29 20:23:06 +00:00
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
focusCounter: number;
groupAdmins: Array<ConversationType>;
groupVersion?: 1 | 2;
2021-09-24 20:02:30 +00:00
i18n: LocalizerType;
2022-11-16 21:41:38 +00:00
imageToBlurHash: typeof imageToBlurHash;
2022-12-08 07:43:48 +00:00
isDisabled: boolean;
2021-09-24 20:02:30 +00:00
isFetchingUUID?: boolean;
isFormattingEnabled: boolean;
isFormattingFlagEnabled: boolean;
isFormattingSpoilersFlagEnabled: boolean;
isGroupV1AndDisabled?: boolean;
isMissingMandatoryProfileSharing?: boolean;
2022-11-09 02:38:19 +00:00
isSignalConversation?: boolean;
lastEditableMessageId?: string;
recordingState: RecordingState;
2022-12-08 07:43:48 +00:00
messageCompositionId: string;
2023-05-10 01:23:56 +00:00
shouldHidePopovers?: boolean;
isSMSOnly?: boolean;
left?: boolean;
2021-09-24 20:02:30 +00:00
linkPreviewLoading: boolean;
2022-06-17 00:48:57 +00:00
linkPreviewResult?: LinkPreviewType;
messageRequestsEnabled?: boolean;
onClearAttachments(conversationId: string): unknown;
onCloseLinkPreview(conversationId: string): unknown;
platform: string;
2023-03-20 22:23:53 +00:00
showToast: ShowToastAction;
processAttachments: (options: {
conversationId: string;
files: ReadonlyArray<File>;
}) => unknown;
setMediaQualitySetting(conversationId: string, isHQ: boolean): unknown;
2022-12-08 07:43:48 +00:00
sendStickerMessage(
id: string,
opts: { packId: string; stickerId: number }
): unknown;
sendEditedMessage(
conversationId: string,
options: {
bodyRanges?: DraftBodyRanges;
message?: string;
quoteAuthorUuid?: string;
quoteSentAt?: number;
targetMessageId: string;
}
): unknown;
2022-12-08 07:43:48 +00:00
sendMultiMediaMessage(
conversationId: string,
options: {
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
bodyRanges?: DraftBodyRanges;
2022-12-08 07:43:48 +00:00
message?: string;
timestamp?: number;
voiceNoteAttachment?: InMemoryAttachmentDraftType;
}
): unknown;
quotedMessageId?: string;
quotedMessageProps?: ReadonlyDeep<
Omit<
QuoteProps,
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose'
>
>;
quotedMessageAuthorUuid?: string;
quotedMessageSentAt?: number;
2021-09-24 20:02:30 +00:00
removeAttachment: (conversationId: string, filePath: string) => unknown;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
setComposerFocus: (conversationId: string) => unknown;
setMessageToEdit(conversationId: string, messageId: string): unknown;
setQuoteByMessageId(
conversationId: string,
messageId: string | undefined
): unknown;
2021-09-24 20:02:30 +00:00
shouldSendHighQualityAttachments: boolean;
showConversation: ShowConversationType;
startRecording: (id: string) => unknown;
2021-11-02 23:01:13 +00:00
theme: ThemeType;
2023-03-02 20:55:40 +00:00
renderSmartCompositionRecording: (
props: SmartCompositionRecordingProps
) => JSX.Element;
renderSmartCompositionRecordingDraft: (
props: SmartCompositionRecordingDraftProps
) => JSX.Element | null;
2023-03-20 22:23:53 +00:00
selectedMessageIds: ReadonlyArray<string> | undefined;
toggleSelectMode: (on: boolean) => void;
toggleForwardMessagesModal: (
messageIds: ReadonlyArray<string>,
onForward: () => void
) => void;
}>;
2019-08-06 19:18:37 +00:00
export type Props = Pick<
CompositionInputProps,
| 'clearQuotedMessage'
2020-11-03 01:19:52 +00:00
| 'draftText'
| 'draftBodyRanges'
2021-11-17 18:38:52 +00:00
| 'getPreferredBadge'
2020-07-01 18:05:41 +00:00
| 'getQuotedMessage'
| 'onEditorStateChange'
| 'onTextTooLong'
| 'sendCounter'
| 'sortedGroupMembers'
2019-08-06 19:18:37 +00:00
> &
Pick<
EmojiButtonProps,
'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
> &
Pick<
StickerButtonProps,
| 'knownPacks'
| 'receivedPacks'
| 'installedPack'
| 'installedPacks'
| 'blessedPacks'
| 'recentStickers'
| 'clearInstalledStickerPack'
| 'clearShowIntroduction'
| 'showPickerHint'
| 'clearShowPickerHint'
> &
2020-05-27 21:37:06 +00:00
MessageRequestActionsProps &
2022-12-08 06:41:37 +00:00
Pick<GroupV1DisabledActionsPropsType, 'showGV2MigrationDialog'> &
Pick<GroupV2PendingApprovalActionsPropsType, 'cancelJoinRequest'> & {
pushPanelForConversation: PushPanelForConversationActionType;
} & OwnProps;
2022-11-18 00:45:19 +00:00
export function CompositionArea({
2021-09-24 20:02:30 +00:00
// Base props
addAttachment,
conversationId,
discardEditMessage,
draftEditMessage,
focusCounter,
i18n,
2022-11-16 21:41:38 +00:00
imageToBlurHash,
2022-12-08 07:43:48 +00:00
isDisabled,
isSignalConversation,
lastEditableMessageId,
messageCompositionId,
pushPanelForConversation,
platform,
2021-09-24 20:02:30 +00:00
processAttachments,
removeAttachment,
sendEditedMessage,
2022-12-08 07:43:48 +00:00
sendMultiMediaMessage,
setComposerFocus,
setMessageToEdit,
setQuoteByMessageId,
2023-05-10 01:23:56 +00:00
shouldHidePopovers,
showToast,
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
recordingState,
2021-09-29 20:23:06 +00:00
startRecording,
2021-06-25 16:08:16 +00:00
// StagedLinkPreview
linkPreviewLoading,
linkPreviewResult,
onCloseLinkPreview,
// Quote
quotedMessageId,
2021-06-25 16:08:16 +00:00
quotedMessageProps,
quotedMessageAuthorUuid,
quotedMessageSentAt,
scrollToMessage,
2021-06-25 16:08:16 +00:00
// MediaQualitySelector
setMediaQualitySetting,
2021-06-25 16:08:16 +00:00
shouldSendHighQualityAttachments,
// CompositionInput
clearQuotedMessage,
2020-11-03 01:19:52 +00:00
draftBodyRanges,
draftText,
2021-11-17 18:38:52 +00:00
getPreferredBadge,
2020-05-27 21:37:06 +00:00
getQuotedMessage,
isFormattingEnabled,
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
onEditorStateChange,
onTextTooLong,
sendCounter,
sortedGroupMembers,
// EmojiButton
onPickEmoji,
onSetSkinTone,
recentEmojis,
skinTone,
// StickerButton
knownPacks,
receivedPacks,
installedPack,
installedPacks,
blessedPacks,
recentStickers,
clearInstalledStickerPack,
2022-12-08 07:43:48 +00:00
sendStickerMessage,
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,
2023-04-05 20:48:00 +00:00
removalStage,
2022-12-06 19:03:09 +00:00
acceptConversation,
blockConversation,
blockAndReportSpam,
deleteConversation,
2020-07-24 01:35:32 +00:00
title,
// GroupV1 Disabled Actions
isGroupV1AndDisabled,
2022-12-08 06:41:37 +00:00
showGV2MigrationDialog,
2021-07-20 20:18:35 +00:00
// GroupV2
announcementsOnly,
areWeAdmin,
groupAdmins,
cancelJoinRequest,
showConversation,
// SMS-only contacts
isSMSOnly,
isFetchingUUID,
2023-03-02 20:55:40 +00:00
renderSmartCompositionRecording,
renderSmartCompositionRecordingDraft,
2023-03-20 22:23:53 +00:00
// Selected messages
selectedMessageIds,
toggleSelectMode,
toggleForwardMessagesModal,
2023-03-02 20:55:40 +00:00
}: Props): JSX.Element | null {
2021-09-24 20:02:30 +00:00
const [dirty, setDirty] = useState(false);
const [large, setLarge] = useState(false);
2021-12-01 02:14:25 +00:00
const [attachmentToEdit, setAttachmentToEdit] = useState<
AttachmentDraftType | undefined
>();
2021-09-24 20:02:30 +00:00
const inputApiRef = useRef<InputApi | undefined>();
const emojiButtonRef = useRef<EmojiButtonAPI | undefined>();
2021-09-24 20:02:30 +00:00
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
const draftEditMessageBody = draftEditMessage?.body;
const editedMessageId = draftEditMessage?.targetMessageId;
2021-09-29 20:23:06 +00:00
const handleSubmit = useCallback(
(message: string, bodyRanges: DraftBodyRanges, timestamp: number) => {
emojiButtonRef.current?.close();
if (editedMessageId) {
sendEditedMessage(conversationId, {
bodyRanges,
message,
// sent timestamp for the quote
quoteSentAt: quotedMessageSentAt,
quoteAuthorUuid: quotedMessageAuthorUuid,
targetMessageId: editedMessageId,
});
} else {
sendMultiMediaMessage(conversationId, {
draftAttachments,
bodyRanges,
message,
timestamp,
});
}
setLarge(false);
2019-08-06 19:18:37 +00:00
},
[
conversationId,
draftAttachments,
editedMessageId,
quotedMessageSentAt,
quotedMessageAuthorUuid,
sendEditedMessage,
sendMultiMediaMessage,
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();
}
}, []);
2021-12-01 02:14:25 +00:00
function maybeEditAttachment(attachment: AttachmentDraftType) {
2021-12-15 00:53:15 +00:00
if (!isImageTypeSupported(attachment.contentType)) {
2021-12-01 02:14:25 +00:00
return;
}
setAttachmentToEdit(attachment);
}
const isComposerEmpty =
!draftAttachments.length && !draftText && !draftEditMessage;
const maybeEditMessage = useCallback(() => {
if (!isComposerEmpty || !lastEditableMessageId) {
return false;
}
setMessageToEdit(conversationId, lastEditableMessageId);
return true;
}, [
conversationId,
isComposerEmpty,
lastEditableMessageId,
setMessageToEdit,
]);
const [hasFocus, setHasFocus] = useState(false);
const attachFileShortcut = useAttachFileShortcut(launchAttachmentPicker);
const editLastMessageSent = useEditLastMessageSent(maybeEditMessage);
useKeyboardShortcutsConditionally(
hasFocus,
attachFileShortcut,
editLastMessageSent
);
2021-09-24 20:02:30 +00:00
// Focus input on first mount
const previousFocusCounter = usePrevious<number | undefined>(
focusCounter,
focusCounter
);
useEffect(() => {
if (inputApiRef.current) {
inputApiRef.current.focus();
2020-01-08 17:44:54 +00:00
}
2022-12-09 18:03:32 +00:00
}, []);
// Focus input whenever explicitly requested
useEffect(() => {
if (focusCounter !== previousFocusCounter && inputApiRef.current) {
inputApiRef.current.focus();
}
}, [inputApiRef, focusCounter, previousFocusCounter]);
const withStickers =
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) > 0;
const previousMessageCompositionId = usePrevious(
messageCompositionId,
messageCompositionId
);
const previousSendCounter = usePrevious(sendCounter, sendCounter);
2022-12-08 07:43:48 +00:00
useEffect(() => {
if (!inputApiRef.current) {
return;
}
if (
previousMessageCompositionId !== messageCompositionId ||
previousSendCounter !== sendCounter
) {
inputApiRef.current.reset();
}
}, [
messageCompositionId,
sendCounter,
previousMessageCompositionId,
previousSendCounter,
]);
2022-12-08 07:43:48 +00:00
2021-09-24 20:02:30 +00:00
const insertEmoji = useCallback(
(e: EmojiPickDataType) => {
if (inputApiRef.current) {
inputApiRef.current.insertEmoji(e);
onPickEmoji(e);
}
},
[inputApiRef, onPickEmoji]
);
const previousConversationId = usePrevious(conversationId, conversationId);
useEffect(() => {
if (conversationId === previousConversationId) {
return;
}
if (!draftText) {
inputApiRef.current?.setContents('');
return;
}
2023-01-19 00:59:47 +00:00
inputApiRef.current?.setContents(draftText, draftBodyRanges, true);
}, [conversationId, draftBodyRanges, draftText, previousConversationId]);
// We want to reset the state of Quill only if:
//
// - Our other device edits the message (edit history length would change)
// - User begins editing another message.
const editHistoryLength = draftEditMessage?.editHistoryLength;
const hasEditHistoryChanged =
usePrevious(editHistoryLength, editHistoryLength) !== editHistoryLength;
const hasEditedMessageChanged =
usePrevious(editedMessageId, editedMessageId) !== editedMessageId;
const hasEditDraftChanged = hasEditHistoryChanged || hasEditedMessageChanged;
useEffect(() => {
if (!hasEditDraftChanged) {
return;
}
inputApiRef.current?.setContents(
draftEditMessageBody ?? '',
draftBodyRanges,
true
);
}, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]);
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
const shouldShowMicrophone = !large && isComposerEmpty;
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
emojiButtonApi={emojiButtonRef}
2021-06-25 16:08:16 +00:00
i18n={i18n}
doSend={handleForceSend}
onPickEmoji={insertEmoji}
onClose={() => setComposerFocus(conversationId)}
2021-06-25 16:08:16 +00:00
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
conversationId={conversationId}
2021-06-25 16:08:16 +00:00
i18n={i18n}
isHighQuality={shouldSendHighQualityAttachments}
onSelectQuality={setMediaQualitySetting}
2021-06-25 16:08:16 +00:00
/>
</div>
) : null}
</>
2019-08-06 19:18:37 +00:00
);
2021-09-24 20:02:30 +00:00
const micButtonFragment = shouldShowMicrophone ? (
2022-06-13 23:37:29 +00:00
<div className="CompositionArea__button-cell">
<AudioCapture
conversationId={conversationId}
draftAttachments={draftAttachments}
i18n={i18n}
startRecording={startRecording}
/>
</div>
2019-08-06 19:18:37 +00:00
) : null;
const editMessageFragment = draftEditMessage ? (
<>
{large && <div className="CompositionArea__placeholder" />}
2023-07-28 02:36:14 +00:00
<div className="CompositionArea__button-cell CompositionArea__button-edit">
<button
aria-label={i18n('icu:CompositionArea__edit-action--discard')}
className="CompositionArea__edit-button CompositionArea__edit-button--discard"
onClick={() => discardEditMessage(conversationId)}
type="button"
/>
<button
aria-label={i18n('icu:CompositionArea__edit-action--send')}
className="CompositionArea__edit-button CompositionArea__edit-button--accept"
onClick={() => inputApiRef.current?.submit()}
type="button"
/>
</div>
</>
) : null;
const isRecording = recordingState === RecordingState.Recording;
const attButton =
draftEditMessage || linkPreviewResult || isRecording ? undefined : (
<div className="CompositionArea__button-cell">
<button
type="button"
className="CompositionArea__attach-file"
onClick={launchAttachmentPicker}
2023-03-30 00:03:25 +00:00
aria-label={i18n('icu:CompositionArea--attach-file')}
/>
</div>
);
2019-08-06 19:18:37 +00:00
const sendButtonFragment = !draftEditMessage ? (
2022-06-13 23:37:29 +00:00
<>
<div className="CompositionArea__placeholder" />
<div className="CompositionArea__button-cell">
<button
type="button"
className="CompositionArea__send-button"
onClick={handleForceSend}
2023-03-30 00:03:25 +00:00
aria-label={i18n('icu:sendMessageToContact')}
2022-06-13 23:37:29 +00:00
/>
</div>
</>
) : null;
2019-08-06 19:18:37 +00:00
const stickerButtonPlacement = large ? 'top-start' : 'top-end';
const stickerButtonFragment =
!draftEditMessage && withStickers ? (
<div className="CompositionArea__button-cell">
<StickerButton
i18n={i18n}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
installedPack={installedPack}
installedPacks={installedPacks}
blessedPacks={blessedPacks}
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={() =>
pushPanelForConversation({
type: PanelType.StickerManager,
})
}
onPickSticker={(packId, stickerId) =>
sendStickerMessage(conversationId, { packId, stickerId })
}
clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint}
position={stickerButtonPlacement}
/>
</div>
) : null;
2019-08-06 19:18:37 +00:00
// 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 { shiftKey, ctrlKey, metaKey } = e;
const key = KeyboardLayout.lookup(e);
// When using the ctrl key, `key` is `'K'`. When using the cmd key, `key` is `'k'`
const targetKey = key === 'k' || key === 'K';
const commandKey = platform === 'darwin' && metaKey;
const controlKey = platform !== 'darwin' && ctrlKey;
2020-01-08 17:44:54 +00:00
const commandOrCtrl = commandKey || controlKey;
// cmd/ctrl-shift-k
if (targetKey && shiftKey && commandOrCtrl) {
2020-01-08 17:44:54 +00:00
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);
};
}, [platform, setLarge]);
2023-03-02 20:55:40 +00:00
const handleRecordingBeforeSend = useCallback(() => {
emojiButtonRef.current?.close();
}, [emojiButtonRef]);
const handleEscape = useCallback(() => {
if (linkPreviewResult) {
onCloseLinkPreview(conversationId);
} else if (draftEditMessage) {
discardEditMessage(conversationId);
} else if (quotedMessageId) {
setQuoteByMessageId(conversationId, undefined);
}
}, [
conversationId,
discardEditMessage,
draftEditMessage,
linkPreviewResult,
onCloseLinkPreview,
quotedMessageId,
setQuoteByMessageId,
]);
2023-01-19 22:59:58 +00:00
useEscapeHandling(handleEscape);
2023-01-19 22:59:58 +00:00
2022-11-09 02:38:19 +00:00
if (isSignalConversation) {
// TODO DESKTOP-4547
return <div />;
}
2023-03-20 22:23:53 +00:00
if (selectedMessageIds != null) {
return (
<SelectModeActions
i18n={i18n}
selectedMessageIds={selectedMessageIds}
onExitSelectMode={() => {
toggleSelectMode(false);
}}
onDeleteMessages={() => {
window.reduxActions.globalModals.toggleDeleteMessagesModal({
2023-03-20 22:23:53 +00:00
conversationId,
messageIds: selectedMessageIds,
onDelete() {
toggleSelectMode(false);
},
2023-03-20 22:23:53 +00:00
});
}}
onForwardMessages={() => {
if (selectedMessageIds.length > 0) {
toggleForwardMessagesModal(selectedMessageIds, () => {
toggleSelectMode(false);
});
}
}}
showToast={showToast}
/>
);
}
if (
2020-11-20 17:30:45 +00:00
isBlocked ||
areWePending ||
2023-04-05 20:48:00 +00:00
(messageRequestsEnabled &&
!acceptedMessageRequest &&
removalStage !== 'justNotification')
) {
2020-05-27 21:37:06 +00:00
return (
<MessageRequestActions
2022-12-06 19:03:09 +00:00
acceptConversation={acceptConversation}
blockAndReportSpam={blockAndReportSpam}
blockConversation={blockConversation}
conversationId={conversationId}
2020-05-27 21:37:06 +00:00
conversationType={conversationType}
2022-12-06 19:03:09 +00:00
deleteConversation={deleteConversation}
i18n={i18n}
2020-05-27 21:37:06 +00:00
isBlocked={isBlocked}
2023-04-05 20:48:00 +00:00
isHidden={removalStage !== undefined}
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
2023-03-30 00:03:25 +00:00
ariaLabel={i18n('icu: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">
2023-03-30 00:03:25 +00:00
{i18n('icu:CompositionArea--sms-only__title')}
</h2>
2021-07-20 20:18:35 +00:00
<p className="CompositionArea--sms-only__body">
2023-03-30 00:03:25 +00:00
{i18n('icu:CompositionArea--sms-only__body')}
</p>
</>
)}
</div>
);
}
// If no message request, but we haven't shared profile yet, we show profile-sharing UI
if (
!left &&
2023-04-05 20:48:00 +00:00
((conversationType === 'direct' && removalStage !== 'justNotification') ||
(conversationType === 'group' && groupVersion === 1)) &&
isMissingMandatoryProfileSharing
) {
return (
<MandatoryProfileSharingActions
2022-12-06 19:03:09 +00:00
acceptConversation={acceptConversation}
blockAndReportSpam={blockAndReportSpam}
blockConversation={blockConversation}
conversationId={conversationId}
conversationType={conversationType}
2022-12-06 19:03:09 +00:00
deleteConversation={deleteConversation}
i18n={i18n}
title={title}
/>
);
}
// If this is a V1 group, now disabled entirely, we show UI to help them upgrade
if (!left && isGroupV1AndDisabled) {
return (
<GroupV1DisabledActions
2022-12-08 06:41:37 +00:00
conversationId={conversationId}
i18n={i18n}
2022-12-08 06:41:37 +00:00
showGV2MigrationDialog={showGV2MigrationDialog}
/>
);
}
if (areWePendingApproval) {
return (
<GroupV2PendingApprovalActions
cancelJoinRequest={cancelJoinRequest}
conversationId={conversationId}
i18n={i18n}
/>
);
}
2021-07-20 20:18:35 +00:00
if (announcementsOnly && !areWeAdmin) {
return (
<AnnouncementsOnlyGroupBanner
groupAdmins={groupAdmins}
i18n={i18n}
showConversation={showConversation}
2021-11-02 23:01:13 +00:00
theme={theme}
2021-07-20 20:18:35 +00:00
/>
);
}
2023-03-02 20:55:40 +00:00
if (isRecording) {
return renderSmartCompositionRecording({
onBeforeSend: handleRecordingBeforeSend,
});
}
if (draftAttachments.length === 1 && isVoiceMessage(draftAttachments[0])) {
const voiceNoteAttachment = draftAttachments[0];
if (!voiceNoteAttachment.pending && voiceNoteAttachment.url) {
return renderSmartCompositionRecordingDraft({ voiceNoteAttachment });
}
}
return (
2021-07-20 20:18:35 +00:00
<div className="CompositionArea">
{attachmentToEdit &&
'url' in attachmentToEdit &&
attachmentToEdit.url && (
<MediaEditor
i18n={i18n}
imageSrc={attachmentToEdit.url}
imageToBlurHash={imageToBlurHash}
isSending={false}
onClose={() => setAttachmentToEdit(undefined)}
onDone={({ data, contentType, blurHash }) => {
const newAttachment = {
...attachmentToEdit,
contentType,
blurHash,
data,
size: data.byteLength,
};
addAttachment(conversationId, newAttachment);
setAttachmentToEdit(undefined);
}}
installedPacks={installedPacks}
recentStickers={recentStickers}
/>
)}
2021-07-20 20:18:35 +00:00
<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}
2023-03-30 00:03:25 +00:00
aria-label={i18n('icu: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 && (
2021-06-25 16:08:16 +00:00
<div className="quote-wrapper">
<Quote
2022-06-15 17:53:08 +00:00
isCompose
2021-06-25 16:08:16 +00:00
{...quotedMessageProps}
i18n={i18n}
onClick={
quotedMessageId
? () => scrollToMessage(conversationId, quotedMessageId)
: undefined
}
2023-06-13 16:46:06 +00:00
onClose={
draftEditMessage
? undefined
: () => {
setQuoteByMessageId(conversationId, undefined);
}
}
2021-06-25 16:08:16 +00:00
/>
</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}
canEditImages
2021-06-25 16:08:16 +00:00
i18n={i18n}
2021-09-24 20:02:30 +00:00
onAddAttachment={launchAttachmentPicker}
2021-12-01 02:14:25 +00:00
onClickAttachment={maybeEditAttachment}
onClose={() => onClearAttachments(conversationId)}
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}
2022-06-13 23:37:29 +00:00
<div
className={classNames(
'CompositionArea__input',
large ? 'CompositionArea__input--padded' : null
)}
>
2019-08-06 19:18:37 +00:00
<CompositionInput
clearQuotedMessage={clearQuotedMessage}
2023-01-30 20:16:09 +00:00
conversationId={conversationId}
2022-12-08 07:43:48 +00:00
disabled={isDisabled}
draftBodyRanges={draftBodyRanges}
draftEditMessage={draftEditMessage}
draftText={draftText}
2021-11-17 18:38:52 +00:00
getPreferredBadge={getPreferredBadge}
getQuotedMessage={getQuotedMessage}
i18n={i18n}
2019-08-06 19:18:37 +00:00
inputApi={inputApiRef}
isFormattingEnabled={isFormattingEnabled}
isFormattingFlagEnabled={isFormattingFlagEnabled}
isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
large={large}
2023-01-30 20:16:09 +00:00
linkPreviewLoading={linkPreviewLoading}
linkPreviewResult={linkPreviewResult}
onBlur={() => setHasFocus(false)}
onFocus={() => setHasFocus(true)}
2023-01-30 20:16:09 +00:00
onCloseLinkPreview={onCloseLinkPreview}
onDirtyChange={setDirty}
onEditorStateChange={onEditorStateChange}
2019-08-06 19:18:37 +00:00
onPickEmoji={onPickEmoji}
onSubmit={handleSubmit}
onTextTooLong={onTextTooLong}
platform={platform}
sendCounter={sendCounter}
2023-05-10 01:23:56 +00:00
shouldHidePopovers={shouldHidePopovers}
2019-08-06 19:18:37 +00:00
skinTone={skinTone}
sortedGroupMembers={sortedGroupMembers}
2021-11-17 18:38:52 +00:00
theme={theme}
/>
</div>
2019-08-06 19:18:37 +00:00
{!large ? (
<>
{stickerButtonFragment}
{!dirty ? micButtonFragment : null}
{editMessageFragment}
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}
{editMessageFragment}
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
conversationId={conversationId}
draftAttachments={draftAttachments}
i18n={i18n}
processAttachments={processAttachments}
ref={fileInputRef}
/>
</div>
);
2022-11-18 00:45:19 +00:00
}