Files
2026-01-05 13:43:51 +11:00

868 lines
27 KiB
TypeScript

import _, { debounce, isEmpty, isUndefined } from 'lodash';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { AbortController } from 'abort-controller';
import autoBind from 'auto-bind';
import { Component, createRef, RefObject, KeyboardEvent } from 'react';
import { FrequentlyUsed } from 'emoji-mart';
import * as MIME from '../../../types/MIME';
import { SessionEmojiPanel, StyledEmojiPanel } from '../SessionEmojiPanel';
import { SessionRecording } from '../SessionRecording';
import { SettingsKey } from '../../../data/settings-key';
import { showLinkSharingConfirmationModalDialog } from '../../../interactions/conversationInteractions';
import { ToastUtils } from '../../../session/utils';
import { ReduxConversationType } from '../../../state/ducks/conversations';
import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
import { StateType } from '../../../state/reducer';
import { getQuotedMessage, getSelectedConversation } from '../../../state/selectors/conversations';
import {
getIsSelectedBlocked,
getSelectedCanWrite,
getSelectedConversationKey,
useSelectedConversationKey,
useSelectedIsBlocked,
} from '../../../state/selectors/selectedConversation';
import { AttachmentType } from '../../../types/Attachment';
import { processNewAttachment } from '../../../types/MessageAttachment';
import { AttachmentUtil } from '../../../util';
import {
StagedAttachmentImportedType,
StagedPreviewImportedType,
} from '../../../util/attachment/attachmentsUtil';
import { CaptionEditor } from '../../CaptionEditor';
import { Flex } from '../../basic/Flex';
import { getMediaPermissionsSettings } from '../../settings/SessionSettings';
import { getDraftForConversation, updateDraftForConversation } from '../SessionConversationDrafts';
import { SessionQuotedMessageComposition } from '../SessionQuotedMessageComposition';
import { SessionStagedLinkPreview } from '../SessionStagedLinkPreview';
import { StagedAttachmentList } from '../StagedAttachmentList';
import {
AddStagedAttachmentButton,
SendMessageButton,
StartRecordingButton,
ToggleEmojiButton,
} from './CompositionButtons';
import { CompositionTextArea } from './CompositionTextArea';
import { HTMLDirection } from '../../../util/i18n/rtlSupport';
import type { FixedBaseEmoji } from '../../../types/Reaction';
import { CharacterCount } from './CharacterCount';
import { Constants } from '../../../session';
import type { CompositionInputRef } from './CompositionInput';
import { useShowBlockUnblock } from '../../menuAndSettingsHooks/useShowBlockUnblock';
import { showLocalizedPopupDialog } from '../../dialog/LocalizedPopupDialog';
import { formatNumber } from '../../../util/i18n/formatting/generics';
import { getFeatureFlag } from '../../../state/ducks/types/releasedFeaturesReduxTypes';
import { showSessionCTA } from '../../dialog/SessionCTA';
import { tStripped } from '../../../localization/localeTools';
import type { ProcessedLinkPreviewThumbnailType } from '../../../webworker/workers/node/image_processor/image_processor';
import { CTAVariant } from '../../dialog/cta/types';
import { selectWeAreProUser } from '../../../hooks/useHasPro';
export interface ReplyingToMessageProps {
convoId: string;
id: string; // this is the quoted message timestamp
author: string;
timestamp: number;
text?: string;
attachments?: Array<any>;
}
export type StagedLinkPreviewData = {
title: string | null;
url: string | null;
domain: string | null;
scaledDown: ProcessedLinkPreviewThumbnailType | null;
};
export type StagedAttachmentType = AttachmentType & {
file: File;
path?: string; // a bit hacky, but this is the only way to make our sending audio message be playable, this must be used only for those message
};
export type SendMessageType = {
conversationId: string;
body: string;
attachments: Array<StagedAttachmentImportedType> | undefined;
quote: any | undefined;
preview: any | undefined;
groupInvitation: { url: string | undefined; name: string } | undefined;
};
interface Props {
sendMessage: (msg: SendMessageType) => void;
selectedConversationKey?: string;
weAreProUser: boolean;
selectedConversation: ReduxConversationType | undefined;
typingEnabled: boolean;
isBlocked: boolean;
quotedMessageProps?: ReplyingToMessageProps;
stagedAttachments: Array<StagedAttachmentType>;
onChoseAttachments: (newAttachments: Array<File>) => void;
htmlDirection: HTMLDirection;
}
interface State {
showRecordingView: boolean;
initialDraft: string;
draft: string;
showEmojiPanel: boolean;
lastSelectedLength: number; // used for emoji panel replacement
ignoredLink?: string; // set the ignored url when users closed the link preview
stagedLinkPreview?: StagedLinkPreviewData;
showCaptionEditor?: AttachmentType;
characterCount?: number;
}
function getSendableTextLength(v: string): number {
return v.trim().replace(/^\n+|\n+$/g, '').length;
}
const getDefaultState = (newConvoId?: string) => {
const draft = getDraftForConversation(newConvoId);
return {
draft,
initialDraft: draft,
showRecordingView: false,
showEmojiPanel: false,
lastSelectedLength: 0,
ignoredLink: undefined,
stagedLinkPreview: undefined,
showCaptionEditor: undefined,
characterCount: getSendableTextLength(draft),
};
};
const StyledEmojiPanelContainer = styled.div<{ dir?: HTMLDirection }>`
${StyledEmojiPanel} {
position: absolute;
bottom: 68px;
${props => (props.dir === 'rtl' ? 'left: 0px' : 'right: 0px;')}
}
`;
const StyledSendMessageInput = styled.div<{ dir?: HTMLDirection }>`
position: relative;
cursor: text;
display: flex;
align-self: center;
align-items: center;
flex-grow: 1;
${props => props.dir === 'rtl' && 'margin-inline-start: var(--margins-sm);'}
background-color: inherit;
margin-top: var(--margins-xs);
.mention-container {
border-radius: var(--border-radius);
box-shadow: var(--suggestions-shadow);
background-color: var(--suggestions-background-color);
z-index: 3;
min-width: 100px;
ul {
border-radius: var(--border-radius);
max-height: 70vh;
border: none;
background: transparent;
outline: none;
padding: 0;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
li {
font-size: 14px;
cursor: pointer;
height: auto;
padding-top: var(--margins-xs);
padding-bottom: var(--margins-xs);
background-color: var(--suggestions-background-color);
color: var(--suggestions-text-color);
transition: var(--default-duration);
&:hover,
&.selected-option {
background-color: var(--suggestions-background-hover-color);
}
}
}
}
`;
const StyledRightCompositionBoxButtonContainer = styled.div`
position: absolute;
inset-inline-end: var(--margins-md);
gap: var(--margins-sm);
display: flex;
flex-direction: row;
width: max-content;
z-index: 2;
`;
const StyledCompositionBoxContainer = styled(Flex)`
position: relative;
padding-inline-start: var(--margins-md);
padding-inline-end: 0;
padding-top: var(--margins-sm);
padding-bottom: var(--margins-sm);
`;
class CompositionBoxInner extends Component<Props, State> {
private readonly inputRef: RefObject<CompositionInputRef | null>;
private readonly fileInput: RefObject<HTMLInputElement | null>;
private container: RefObject<HTMLDivElement | null>;
private readonly emojiPanel: RefObject<HTMLDivElement | null>;
private readonly emojiPanelButton: any;
private linkPreviewAbortController?: AbortController;
constructor(props: Props) {
super(props);
this.state = getDefaultState(props.selectedConversationKey);
this.inputRef = createRef();
this.fileInput = createRef();
this.container = createRef();
// Emojis
this.emojiPanel = createRef();
this.emojiPanelButton = createRef();
autoBind(this);
this.toggleEmojiPanel = debounce(this.toggleEmojiPanel.bind(this), 100);
}
public componentDidMount() {
setTimeout(this.focusCompositionBox, 500);
if (this.container.current) {
this.container.current.addEventListener('paste', this.handlePaste);
}
}
public componentWillUnmount() {
this.linkPreviewAbortController?.abort();
this.linkPreviewAbortController = undefined;
if (this.container.current) {
this.container.current.removeEventListener('paste', this.handlePaste);
}
}
public componentDidUpdate(prevProps: Props, _prevState: State) {
// reset the state on new conversation key
if (prevProps.selectedConversationKey !== this.props.selectedConversationKey) {
this.setState(getDefaultState(this.props.selectedConversationKey), this.focusCompositionBox);
} else if (this.props.stagedAttachments?.length !== prevProps.stagedAttachments?.length) {
// if number of staged attachment changed, focus the composition box for a more natural UI
this.focusCompositionBox();
}
// focus the composition box when user clicks start to reply to a message
if (!_.isEqual(prevProps.quotedMessageProps, this.props.quotedMessageProps)) {
this.focusCompositionBox();
}
}
public render() {
const { showRecordingView, draft } = this.state;
const { typingEnabled, isBlocked, stagedAttachments, quotedMessageProps } = this.props;
// we completely hide the composition box when typing is not enabled now.
// Actually not anymore. We want the above, except when we can't write because that user is blocked.
// When that user is blocked, **and only then**, we want to show the composition box, disabled with the placeholder "unblock to send".
if (!typingEnabled && !isBlocked) {
return null;
}
return (
<Flex $flexDirection="column">
<SessionQuotedMessageComposition />
{
// Don't render link previews if quoted message or attachments are already added
stagedAttachments.length !== 0 || quotedMessageProps?.id ? null : (
<SessionStagedLinkPreview
draft={draft}
setStagedLinkPreview={this.setStagedLinkPreview}
/>
)
}
{this.renderAttachmentsStaged()}
<div className="composition-container">
{showRecordingView ? this.renderRecordingView() : this.renderCompositionView()}
<BlockedOverlayOnCompositionBox />
</div>
</Flex>
);
}
private handleClick(e: any) {
if (
(this.emojiPanel?.current && this.emojiPanel.current.contains(e.target)) ||
(this.emojiPanelButton?.current && this.emojiPanelButton.current.contains(e.target))
) {
return;
}
this.hideEmojiPanel();
}
private handlePaste(e: ClipboardEvent) {
if (!e.clipboardData) {
return;
}
const { items } = e.clipboardData;
let imgBlob = null;
// eslint-disable-next-line no-restricted-syntax
for (const item of items as any) {
const pasteType = item.type.split('/')[0];
if (pasteType === 'image') {
imgBlob = item.getAsFile();
}
switch (pasteType) {
case 'image':
imgBlob = item.getAsFile();
break;
case 'text':
void showLinkSharingConfirmationModalDialog(e.clipboardData.getData('text'));
break;
default:
}
}
if (imgBlob !== null) {
const file = imgBlob;
window?.log?.info('Adding attachment from clipboard', file);
this.props.onChoseAttachments([file]);
e.preventDefault();
e.stopPropagation();
}
}
private showEmojiPanel() {
document.addEventListener('mousedown', this.handleClick, false);
this.setState({ lastSelectedLength: window.getSelection()?.toString().length ?? 0 });
this.setState({
showEmojiPanel: true,
});
}
private hideEmojiPanel() {
document.removeEventListener('mousedown', this.handleClick, false);
this.setState({ lastSelectedLength: 0 });
this.setState({
showEmojiPanel: false,
});
this.focusCompositionBox();
}
private setDraft(draft: string) {
this.setState({ draft, characterCount: this.getSendableText().length });
}
private setStagedLinkPreview(linkPreview: StagedLinkPreviewData) {
this.setState({ stagedLinkPreview: linkPreview });
}
private toggleEmojiPanel() {
if (this.state.showEmojiPanel) {
this.hideEmojiPanel();
} else {
this.showEmojiPanel();
}
}
private renderRecordingView() {
return (
<SessionRecording
sendVoiceMessage={this.sendVoiceMessage}
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
onExitVoiceNoteView={this.onExitVoiceNoteView}
/>
);
}
private renderCompositionView() {
const { showEmojiPanel, characterCount } = this.state;
const { typingEnabled, isBlocked } = this.props;
// we can only send a message if the conversation allows writing in it AND
// - we've got a message body OR
// - we've got a staged attachments
const showSendButton =
typingEnabled && (this.isTextSendable() || !isEmpty(this.props.stagedAttachments));
/* eslint-disable @typescript-eslint/no-misused-promises */
// we completely hide the composition box when typing is not enabled now.
// Actually not anymore. We want the above, except when we can't write because that user is blocked.
// When that user is blocked, **and only then**, we want to show the composition box, disabled with the placeholder "unblock to send", and the buttons disabled.
// A click on the composition box should bring the "unblock user" dialog.
if (!typingEnabled && !isBlocked) {
return null;
}
return (
<StyledCompositionBoxContainer
dir={this.props.htmlDirection}
$container={true}
$flexDirection="row"
$alignItems="flex-end"
width="100%"
onClick={this.focusCompositionBox} // used to focus on the textarea when clicking in its container
$flexGap="var(--margins-xs)"
>
{typingEnabled || isBlocked ? (
<AddStagedAttachmentButton onClick={this.onChooseAttachment} />
) : null}
<input
className="hidden"
placeholder="Attachment"
multiple={true}
ref={this.fileInput}
type="file"
onChange={this.onChoseAttachment}
/>
<StyledSendMessageInput role="main" dir={this.props.htmlDirection} ref={this.container}>
<CompositionTextArea
draft={this.state.draft}
initialDraft={this.state.initialDraft}
setDraft={this.setDraft}
container={this.container}
inputRef={this.inputRef}
typingEnabled={this.props.typingEnabled}
onKeyDown={this.onKeyDown}
/>
</StyledSendMessageInput>
<StyledRightCompositionBoxButtonContainer>
{typingEnabled && (
<ToggleEmojiButton ref={this.emojiPanelButton} onClick={this.toggleEmojiPanel} />
)}
{showSendButton ? (
<SendMessageButton onClick={this.onSendMessage} />
) : (
<StartRecordingButton onClick={this.onLoadVoiceNoteView} />
)}
</StyledRightCompositionBoxButtonContainer>
{showEmojiPanel ? (
<StyledEmojiPanelContainer role="button" dir={this.props.htmlDirection}>
<SessionEmojiPanel
ref={this.emojiPanel}
show={showEmojiPanel}
onEmojiClicked={this.onEmojiClick}
onKeyDown={this.onKeyDown}
/>
</StyledEmojiPanelContainer>
) : null}
{!isUndefined(characterCount) ? <CharacterCount count={characterCount} /> : null}
</StyledCompositionBoxContainer>
);
}
private onClickAttachment(attachment: AttachmentType) {
this.setState({ showCaptionEditor: attachment });
}
private renderCaptionEditor(attachment?: AttachmentType) {
if (attachment) {
const onSave = (caption: string) => {
// eslint-disable-next-line no-param-reassign
attachment.caption = caption;
ToastUtils.pushToastInfo('saved', tStripped('saved'));
// close the light box on save
this.setState({
showCaptionEditor: undefined,
});
};
const url = attachment.videoUrl || attachment.url;
return (
<CaptionEditor
attachment={attachment}
url={url}
onSave={onSave}
caption={attachment.caption}
onClose={() => {
this.setState({
showCaptionEditor: undefined,
});
}}
/>
);
}
return null;
}
private renderAttachmentsStaged() {
const { stagedAttachments } = this.props;
const { showCaptionEditor } = this.state;
if (stagedAttachments && stagedAttachments.length) {
return (
<>
<StagedAttachmentList
attachments={stagedAttachments}
onClickAttachment={this.onClickAttachment}
onAddAttachment={this.onChooseAttachment}
/>
{this.renderCaptionEditor(showCaptionEditor)}
</>
);
}
return null;
}
private onChooseAttachment() {
if (
!this.props.selectedConversation?.didApproveMe &&
this.props.selectedConversation?.isPrivate
) {
ToastUtils.pushNoMediaUntilApproved();
return;
}
this.fileInput.current?.click();
}
private async onChoseAttachment() {
// Build attachments list
let attachmentsFileList = null;
// this is terrible, but we have to reset the input value manually.
// otherwise, the user won't be able to select two times the same file for example.
if (this.fileInput.current?.files) {
attachmentsFileList = Array.from(this.fileInput.current.files);
this.fileInput.current.files = null;
this.fileInput.current.value = '';
}
if (!attachmentsFileList || attachmentsFileList.length === 0) {
return;
}
this.props.onChoseAttachments(attachmentsFileList);
}
/**
* This onKeyDown method is called conditionally by the composition input.
* If the input is in mentioning mode, this **will NOT** be called.
* @param event - Keyboard event.
*/
private async onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
const isShiftSendEnabled = !!window.getSettingValue(SettingsKey.hasShiftSendEnabled);
if (
!event.nativeEvent.isComposing &&
event.key === 'Enter' &&
isShiftSendEnabled === event.shiftKey
) {
event.preventDefault();
event.stopPropagation();
await this.onSendMessage();
}
// TODO: Add support for closing the emoji panel, probably should pass an onClose function to it
// TODO: fix the bug with the pageup/down keys popping out the right panel when the box has text in it.
if (this.state.draft.length && (event.key === 'PageUp' || event.key === 'PageDown')) {
event.preventDefault();
event.stopPropagation();
}
if (this.state.draft.length && (event.key === 'Home' || event.key === 'End')) {
event.stopPropagation();
}
}
private getSendableText(): string {
const input = this.inputRef.current;
if (!input) {
return '';
}
return input
.getRawValue(nodeTree =>
nodeTree.querySelectorAll('span[data-user-id]').forEach(span => {
const id = span.getAttribute('data-user-id');
// eslint-disable-next-line no-param-reassign -- intentional mutation of the clone to replace display with id
span.textContent = id ? `@${id}` : span.textContent;
})
)
.trim()
.replace(/^\n+|\n+$/g, '');
}
/**
* This is a significantly cheaper version of calling @see {@link getSendableText} and getting the length
*/
private isTextSendable(): boolean {
return getSendableTextLength(this.state.draft) > 0;
}
private async onSendMessage() {
const {
selectedConversationKey,
selectedConversation,
stagedAttachments,
quotedMessageProps,
weAreProUser: hasPro,
} = this.props;
const { stagedLinkPreview } = this.state;
if (!selectedConversationKey) {
throw new Error('selectedConversationKey is needed');
}
if (!selectedConversation) {
return;
}
this.linkPreviewAbortController?.abort();
const isProAvailable = getFeatureFlag('proAvailable');
const charLimit = hasPro
? Constants.CONVERSATION.MAX_MESSAGE_CHAR_COUNT_PRO
: Constants.CONVERSATION.MAX_MESSAGE_CHAR_COUNT_STANDARD;
const text = this.getSendableText();
if (text.length > charLimit) {
const dispatch = window.inboxStore?.dispatch;
if (dispatch) {
if (isProAvailable && !hasPro) {
showSessionCTA(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT, dispatch);
} else {
showLocalizedPopupDialog(
{
title: {
token: 'modalMessageTooLongTitle',
},
description: {
token: 'modalMessageTooLongDescription',
limit: formatNumber(charLimit),
},
},
dispatch
);
}
}
return;
}
if (selectedConversation.isBlocked) {
ToastUtils.pushUnblockToSend();
return;
}
// Verify message length
const msgLen = text?.length || 0;
if (msgLen === 0 && stagedAttachments?.length === 0) {
return;
}
if (!selectedConversation.isPrivate && selectedConversation.isKickedFromGroup) {
ToastUtils.pushYouLeftTheGroup();
return;
}
// Send message
const extractedQuotedMessageProps = _.pick(
quotedMessageProps,
'id',
'author',
'text',
'attachments'
);
// we consider that a link preview without a title at least is not a preview
const linkPreview =
stagedLinkPreview && stagedLinkPreview.title?.length
? _.pick(stagedLinkPreview, 'url', 'scaledDown', 'title')
: undefined;
try {
const { attachments, previews } = await this.getFiles(linkPreview, stagedAttachments);
this.props.sendMessage({
conversationId: selectedConversationKey,
body: text.trim(),
attachments: attachments || [],
quote: extractedQuotedMessageProps,
preview: previews,
groupInvitation: undefined,
});
window.inboxStore?.dispatch(
removeAllStagedAttachmentsInConversation({
conversationId: selectedConversationKey,
})
);
// Empty composition box and stagedAttachments
this.setState({
showEmojiPanel: false,
stagedLinkPreview: undefined,
ignoredLink: undefined,
draft: '',
characterCount: 0,
});
updateDraftForConversation({
conversationKey: selectedConversationKey,
draft: '',
});
} catch (e) {
// Message sending failed
window?.log?.error(e);
}
}
// This function is called right before sending a message, to gather really the
// files content behind the staged attachments.
private async getFiles(
linkPreview?: Pick<StagedLinkPreviewData, 'url' | 'title' | 'scaledDown'>,
stagedAttachments: Array<StagedAttachmentType> = []
): Promise<{
attachments: Array<StagedAttachmentImportedType>;
previews: Array<StagedPreviewImportedType>;
}> {
let attachments: Array<StagedAttachmentImportedType> = [];
let previews: Array<StagedPreviewImportedType> = [];
if (_.isEmpty(stagedAttachments)) {
attachments = [];
} else {
// scale them down
const files = await Promise.all(stagedAttachments.map(AttachmentUtil.getFileAndStoreLocally));
attachments = _.compact(files);
}
if (!linkPreview || _.isEmpty(linkPreview) || !linkPreview.url || !linkPreview.title) {
previews = [];
} else {
const sharedDetails = { url: linkPreview.url, title: linkPreview.title };
previews = [sharedDetails];
// store the first image preview locally and get the path and details back to include them in the message
if (linkPreview.scaledDown && !isEmpty(linkPreview.scaledDown)) {
const storedLinkPreviewAttachment = await AttachmentUtil.getFileAndStoreLocallyImageBlob(
linkPreview.scaledDown
);
if (storedLinkPreviewAttachment) {
previews = [{ ...sharedDetails, image: storedLinkPreviewAttachment }];
}
}
}
return { attachments, previews };
}
private async sendVoiceMessage(audioBlob: Blob) {
const { selectedConversationKey } = this.props;
if (!this.state.showRecordingView || !selectedConversationKey) {
return;
}
const savedAudioFile = await processNewAttachment({
data: await audioBlob.arrayBuffer(),
isRaw: true,
contentType: MIME.AUDIO_MP3,
});
// { ...savedAudioFile, path: savedAudioFile.path },
const audioAttachment: StagedAttachmentType = {
file: new File([], 'session-audio-message'), // this is just to emulate a file for the staged attachment type of that audio file
contentType: MIME.AUDIO_MP3,
size: savedAudioFile.size,
fileSize: null,
screenshot: null,
fileName: 'session-audio-message',
thumbnail: null,
url: '',
isVoiceMessage: true,
path: savedAudioFile.path,
};
this.props.sendMessage({
conversationId: selectedConversationKey,
body: '',
attachments: [audioAttachment],
preview: undefined,
quote: undefined,
groupInvitation: undefined,
});
this.onExitVoiceNoteView();
}
private onLoadVoiceNoteView() {
if (!getMediaPermissionsSettings()) {
ToastUtils.pushAudioPermissionNeeded();
return;
}
this.setState({
showRecordingView: true,
showEmojiPanel: false,
});
}
private onExitVoiceNoteView() {
this.setState({ showRecordingView: false });
}
private onEmojiClick(emoji: FixedBaseEmoji) {
if (emoji.native) {
FrequentlyUsed.add(emoji);
this.inputRef.current?.typeAtCaret(emoji.native, 0, this.state.lastSelectedLength);
this.setState({ lastSelectedLength: 0 });
}
}
private focusCompositionBox() {
this.inputRef.current?.focus();
}
}
const mapStateToProps = (state: StateType) => {
return {
quotedMessageProps: getQuotedMessage(state),
selectedConversation: getSelectedConversation(state),
weAreProUser: selectWeAreProUser(state),
selectedConversationKey: getSelectedConversationKey(state),
typingEnabled: getSelectedCanWrite(state),
isBlocked: getIsSelectedBlocked(state),
};
};
const smart = connect(mapStateToProps);
export const CompositionBox = smart(CompositionBoxInner);
const StyledBlockedOverlayOnCompositionBox = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
background-color: transparent;
cursor: pointer;
`;
/**
* Note: This component is to be removed once we get the updated composition box PR merged.
* It is only used to hijack the clicks on the composition box and display the "unblock user dialog",
* when that user is currently blocked.
* The reason it was easier to do it this way is because the buttons of the composition box do not react
* to click events when they are disabled.
*
* Note2: Actually having this component offer the unblock user dialog on click right away is convenient as a UX.
* Maybe we should keep it?
*/
function BlockedOverlayOnCompositionBox() {
const selectedConvoKey = useSelectedConversationKey();
const isBlocked = useSelectedIsBlocked();
const blockUnblockCb = useShowBlockUnblock(selectedConvoKey);
if (!isBlocked) {
return null;
}
return <StyledBlockedOverlayOnCompositionBox onClick={blockUnblockCb?.cb} />;
}