signal-desktop/ts/components/TextAttachment.tsx

279 lines
8.8 KiB
TypeScript
Raw Normal View History

2022-04-06 01:18:07 +00:00
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure';
2022-06-17 00:48:57 +00:00
import React, { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
2022-04-06 01:18:07 +00:00
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';
2022-06-17 00:48:57 +00:00
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
2022-04-06 01:18:07 +00:00
import { TextAttachmentStyleType } from '../types/Attachment';
import { count } from '../util/grapheme';
import { getDomain } from '../types/LinkPreview';
import { getFontNameByTextScript } from '../util/getFontNameByTextScript';
2022-04-22 18:36:34 +00:00
import {
2022-06-17 00:48:57 +00:00
COLOR_WHITE_INT,
2022-04-22 18:36:34 +00:00
getHexFromNumber,
getBackgroundColor,
} from '../util/getStoryBackground';
2022-04-06 01:18:07 +00:00
const renderNewLines: RenderTextCallbackType = ({
text: textWithNewLines,
key,
}) => {
return <AddNewLines key={key} text={textWithNewLines} />;
};
const CHAR_LIMIT_TEXT_LARGE = 50;
const CHAR_LIMIT_TEXT_MEDIUM = 200;
2022-08-24 21:35:32 +00:00
const FONT_SIZE_LARGE = 59;
2022-04-06 01:18:07 +00:00
const FONT_SIZE_MEDIUM = 42;
const FONT_SIZE_SMALL = 32;
enum TextSize {
Small,
Medium,
Large,
}
export type PropsType = {
2022-07-08 21:14:01 +00:00
disableLinkPreviewPopup?: boolean;
2022-04-06 01:18:07 +00:00
i18n: LocalizerType;
2022-06-17 00:48:57 +00:00
isEditingText?: boolean;
isThumbnail?: boolean;
2022-06-17 00:48:57 +00:00
onChange?: (text: string) => unknown;
2022-07-08 21:14:01 +00:00
onClick?: () => unknown;
onRemoveLinkPreview?: () => unknown;
2022-04-06 01:18:07 +00:00
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}pt ${fontName}`;
}
2022-06-17 00:48:57 +00:00
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',
};
}
2022-04-06 01:18:07 +00:00
export const TextAttachment = ({
2022-07-08 21:14:01 +00:00
disableLinkPreviewPopup,
2022-04-06 01:18:07 +00:00
i18n,
2022-06-17 00:48:57 +00:00
isEditingText,
isThumbnail,
2022-06-17 00:48:57 +00:00
onChange,
2022-07-08 21:14:01 +00:00
onClick,
onRemoveLinkPreview,
2022-04-06 01:18:07 +00:00
textAttachment,
}: PropsType): JSX.Element | null => {
const linkPreview = useRef<HTMLDivElement | null>(null);
const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState<
number | undefined
>();
2022-06-17 00:48:57 +00:00
const textContent = textAttachment.text || '';
const textEditorRef = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => {
const node = textEditorRef.current;
if (!node) {
return;
}
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}, [isEditingText]);
2022-08-25 00:33:16 +00:00
const storyBackgroundColor = {
background: getBackgroundColor(textAttachment),
};
2022-04-06 01:18:07 +00:00
return (
<Measure bounds>
{({ contentRect, measureRef }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="TextAttachment"
onClick={() => {
if (linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
2022-07-08 21:14:01 +00:00
onClick?.();
2022-04-06 01:18:07 +00:00
}}
onKeyUp={ev => {
if (ev.key === 'Escape' && linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
}}
ref={measureRef}
2022-08-25 00:33:16 +00:00
style={isThumbnail ? storyBackgroundColor : undefined}
2022-04-06 01:18:07 +00:00
>
<div
className="TextAttachment__story"
style={{
2022-08-25 00:33:16 +00:00
...(isThumbnail ? {} : storyBackgroundColor),
2022-04-06 01:18:07 +00:00
transform: `scale(${(contentRect.bounds?.height || 1) / 1280})`,
}}
>
2022-06-17 00:48:57 +00:00
{(textContent || onChange) && (
2022-04-06 01:18:07 +00:00
<div
2022-08-24 21:35:32 +00:00
className={classNames('TextAttachment__text', {
'TextAttachment__text--with-bg': Boolean(
textAttachment.textBackgroundColor
),
})}
2022-04-06 01:18:07 +00:00
style={{
backgroundColor: textAttachment.textBackgroundColor
? getHexFromNumber(textAttachment.textBackgroundColor)
2022-06-17 00:48:57 +00:00
: 'transparent',
2022-04-06 01:18:07 +00:00
}}
>
2022-06-17 00:48:57 +00:00
{onChange ? (
<TextareaAutosize
className="TextAttachment__text__container TextAttachment__text__textarea"
disabled={!isEditingText}
onChange={ev => onChange(ev.currentTarget.value)}
placeholder={i18n('TextAttachment__placeholder')}
ref={textEditorRef}
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
value={textContent}
2022-04-06 01:18:07 +00:00
/>
2022-06-17 00:48:57 +00:00
) : (
<div
className="TextAttachment__text__container"
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
>
<Emojify
text={textContent}
renderNonEmoji={renderNewLines}
/>
</div>
)}
2022-04-06 01:18:07 +00:00
</div>
)}
2022-06-17 00:48:57 +00:00
{textAttachment.preview && textAttachment.preview.url && (
2022-04-06 01:18:07 +00:00
<>
2022-06-17 00:48:57 +00:00
{linkPreviewOffsetTop && !isThumbnail && (
<a
className="TextAttachment__preview__tooltip"
href={textAttachment.preview.url}
rel="noreferrer"
style={{
top: linkPreviewOffsetTop - 150,
}}
target="_blank"
>
<div>
<div>{i18n('TextAttachment__preview__link')}</div>
<div className="TextAttachment__preview__tooltip__url">
{textAttachment.preview.url}
2022-04-06 01:18:07 +00:00
</div>
2022-06-17 00:48:57 +00:00
</div>
<div className="TextAttachment__preview__tooltip__arrow" />
</a>
)}
2022-04-06 01:18:07 +00:00
<div
2022-06-17 00:48:57 +00:00
className={classNames('TextAttachment__preview-container', {
'TextAttachment__preview-container--large': Boolean(
2022-04-06 01:18:07 +00:00
textAttachment.preview.title
),
})}
ref={linkPreview}
2022-07-08 21:14:01 +00:00
onFocus={() => {
if (!disableLinkPreviewPopup) {
setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop);
}
}}
onMouseOver={() => {
if (!disableLinkPreviewPopup) {
setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop);
}
}}
2022-04-06 01:18:07 +00:00
>
2022-07-08 21:14:01 +00:00
{onRemoveLinkPreview && (
<div className="TextAttachment__preview__remove">
<button
aria-label={i18n('Keyboard--remove-draft-link-preview')}
type="button"
onClick={onRemoveLinkPreview}
/>
</div>
)}
2022-06-17 00:48:57 +00:00
<StagedLinkPreview
domain={getDomain(String(textAttachment.preview.url))}
i18n={i18n}
image={textAttachment.preview.image}
2022-07-08 21:14:01 +00:00
imageSize={textAttachment.preview.title ? 144 : 72}
2022-06-17 00:48:57 +00:00
moduleClassName="TextAttachment__preview"
title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url}
/>
2022-04-06 01:18:07 +00:00
</div>
</>
)}
</div>
</div>
)}
</Measure>
);
};