// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { forwardRef, useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import classNames from 'classnames'; import type { LocalizerType, RenderTextCallbackType } from '../types/Util'; import type { TextAttachmentType } from '../types/Attachment'; import { AddNewLines } from './conversation/AddNewLines'; import { Emojify } from './conversation/Emojify'; import { StoryLinkPreview } from './StoryLinkPreview'; import { TextAttachmentStyleType } from '../types/Attachment'; import { count } from '../util/grapheme'; import { getDomain } from '../types/LinkPreview'; import { getFontNameByTextScript } from '../util/getFontNameByTextScript'; import { COLOR_WHITE_INT, getHexFromNumber, getBackgroundColor, } from '../util/getStoryBackground'; import { SECOND } from '../util/durations'; import { useRefMerger } from '../hooks/useRefMerger'; import { useSizeObserver } from '../hooks/useSizeObserver'; const renderNewLines: RenderTextCallbackType = ({ text: textWithNewLines, key, }) => { return ; }; const CHAR_LIMIT_TEXT_LARGE = 50; const CHAR_LIMIT_TEXT_MEDIUM = 200; const FONT_SIZE_LARGE = 59; const FONT_SIZE_MEDIUM = 42; const FONT_SIZE_SMALL = 32; enum TextSize { Small, Medium, Large, } export type PropsType = { disableLinkPreviewPopup?: boolean; i18n: LocalizerType; isEditingText?: boolean; isThumbnail?: boolean; onChange?: (text: string) => unknown; onClick?: () => unknown; onRemoveLinkPreview?: () => unknown; textAttachment: TextAttachmentType; }; function getTextSize(text: string): TextSize { const length = count(text); if (length < CHAR_LIMIT_TEXT_LARGE) { return TextSize.Large; } if (length < CHAR_LIMIT_TEXT_MEDIUM) { return TextSize.Medium; } return TextSize.Small; } function getFont( text: string, textSize: TextSize, textStyle?: TextAttachmentStyleType | null, i18n?: LocalizerType ): string { const textStyleIndex = Number(textStyle) || 0; const fontName = getFontNameByTextScript(text, textStyleIndex, i18n); let fontSize = FONT_SIZE_SMALL; switch (textSize) { case TextSize.Large: fontSize = FONT_SIZE_LARGE; break; case TextSize.Medium: fontSize = FONT_SIZE_MEDIUM; break; default: fontSize = FONT_SIZE_SMALL; } const fontWeight = textStyle === TextAttachmentStyleType.BOLD ? 'bold ' : ''; return `${fontWeight}${fontSize}px ${fontName}`; } function getTextStyles( textContent: string, textForegroundColor?: number | null, textStyle?: TextAttachmentStyleType | null, i18n?: LocalizerType ): { color: string; font: string; textAlign: 'left' | 'center' } { return { color: getHexFromNumber(textForegroundColor || COLOR_WHITE_INT), font: getFont(textContent, getTextSize(textContent), textStyle, i18n), textAlign: getTextSize(textContent) === TextSize.Small ? 'left' : 'center', }; } export const TextAttachment = forwardRef( function TextAttachmentForwarded( { disableLinkPreviewPopup, i18n, isEditingText, isThumbnail, onChange, onClick, onRemoveLinkPreview, textAttachment, }, forwardedTextEditorRef ): JSX.Element | null { const linkPreview = useRef(null); const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState< number | undefined >(); const textContent = textAttachment.text || ''; const textEditorRef = useRef(null); const refMerger = useRefMerger(); useEffect(() => { const node = textEditorRef?.current; if (!node) { return; } node.focus(); node.setSelectionRange(node.value.length, node.value.length); }, [isEditingText]); useEffect(() => { setLinkPreviewOffsetTop(undefined); }, [textAttachment.preview?.url]); const [isHoveringOverTooltip, setIsHoveringOverTooltip] = useState(false); function showTooltip() { if (disableLinkPreviewPopup) { return; } setIsHoveringOverTooltip(true); setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop); } useEffect(() => { const timeout = setTimeout(() => { if (!isHoveringOverTooltip) { setLinkPreviewOffsetTop(undefined); } }, 5 * SECOND); return () => { clearTimeout(timeout); }; }, [isHoveringOverTooltip]); const storyBackgroundColor = { background: getBackgroundColor(textAttachment), }; const ref = useRef(null); const size = useSizeObserver(ref); const scaleFactor = (size?.height || 1) / 1280; return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ if (linkPreviewOffsetTop) { setLinkPreviewOffsetTop(undefined); } onClick?.(); }} onKeyUp={ev => { if (ev.key === 'Escape' && linkPreviewOffsetTop) { setLinkPreviewOffsetTop(undefined); } }} ref={ref} style={isThumbnail ? storyBackgroundColor : undefined} > {/* The tooltip must be outside of the scaled area, as it should not scale with the story, but it must be positioned using the scaled offset */} {textAttachment.preview && textAttachment.preview.url && linkPreviewOffsetTop && !isThumbnail && (
{i18n('icu:TextAttachment__preview__link')}
{textAttachment.preview.url}
)}
{(textContent || onChange) && (
{onChange ? ( onChange(ev.currentTarget.value)} placeholder={i18n('icu:TextAttachment__placeholder')} ref={refMerger(forwardedTextEditorRef, textEditorRef)} style={getTextStyles( textContent, textAttachment.textForegroundColor, textAttachment.textStyle, i18n )} value={textContent} /> ) : (
)}
)} {textAttachment.preview && textAttachment.preview.url && (
setIsHoveringOverTooltip(false)} onFocus={showTooltip} onMouseOut={() => setIsHoveringOverTooltip(false)} onMouseOver={showTooltip} > {onRemoveLinkPreview && (
)}
)}
); } );