// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import FocusTrap from 'focus-trap-react'; import React, { useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import { get, has, noop } from 'lodash'; import { usePopper } from 'react-popper'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; import type { LocalizerType } from '../types/Util'; import type { TextAttachmentType } from '../types/Attachment'; import { Button, ButtonVariant } from './Button'; import { ContextMenu } from './ContextMenu'; import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview'; import { Input } from './Input'; import { Slider } from './Slider'; import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import { TextAttachment } from './TextAttachment'; import { Theme, themeClassName } from '../util/theme'; import { getRGBA, getRGBANumber } from '../mediaEditor/util/color'; import { COLOR_BLACK_INT, COLOR_WHITE_INT, getBackgroundColor, } from '../util/getStoryBackground'; import { objectMap } from '../util/objectMap'; import { handleOutsideClick } from '../util/handleOutsideClick'; export type PropsType = { debouncedMaybeGrabLinkPreview: ( message: string, source: LinkPreviewSourceType ) => unknown; i18n: LocalizerType; linkPreview?: LinkPreviewType; onClose: () => unknown; onDone: (textAttachment: TextAttachmentType) => unknown; }; enum LinkPreviewApplied { None = 'None', Automatic = 'Automatic', Manual = 'Manual', } enum TextStyle { Default, Regular, Bold, Serif, Script, Condensed, } enum TextBackground { None, Background, Inverse, } const BackgroundStyle = { BG1: { color: 4285041620 }, BG2: { color: 4287006657 }, BG3: { color: 4290019212 }, BG4: { color: 4287205768 }, BG5: { color: 4283667331 }, BG6: { angle: 180, startColor: 4279871994, endColor: 4294951785, }, BG7: { angle: 180, startColor: 4282660824, endColor: 4294938254, }, BG8: { angle: 180, startColor: 4278206532, endColor: 4287871076, }, }; type BackgroundStyleType = typeof BackgroundStyle[keyof typeof BackgroundStyle]; function getBackground( bgStyle: BackgroundStyleType ): Pick { if (has(bgStyle, 'color')) { return { color: get(bgStyle, 'color') }; } const angle = get(bgStyle, 'angle'); const startColor = get(bgStyle, 'startColor'); const endColor = get(bgStyle, 'endColor'); return { gradient: { angle, startColor, endColor }, }; } function getBgButtonAriaLabel( i18n: LocalizerType, textBackground: TextBackground ): string { if (textBackground === TextBackground.Background) { return i18n('StoryCreator__text-bg--background'); } if (textBackground === TextBackground.Inverse) { return i18n('StoryCreator__text-bg--inverse'); } return i18n('StoryCreator__text-bg--none'); } export const TextStoryCreator = ({ debouncedMaybeGrabLinkPreview, i18n, linkPreview, onClose, onDone, }: PropsType): JSX.Element => { const [isEditingText, setIsEditingText] = useState(false); const [selectedBackground, setSelectedBackground] = useState(BackgroundStyle.BG1); const [textStyle, setTextStyle] = useState(TextStyle.Regular); const [textBackground, setTextBackground] = useState( TextBackground.None ); const [sliderValue, setSliderValue] = useState(100); const [text, setText] = useState(''); const textEditorRef = useRef(null); useEffect(() => { if (isEditingText) { textEditorRef.current?.focus(); } else { textEditorRef.current?.blur(); } }, [isEditingText]); const [isColorPickerShowing, setIsColorPickerShowing] = useState(false); const [colorPickerPopperButtonRef, setColorPickerPopperButtonRef] = useState(null); const [colorPickerPopperRef, setColorPickerPopperRef] = useState(null); const colorPickerPopper = usePopper( colorPickerPopperButtonRef, colorPickerPopperRef, { modifiers: [ { name: 'arrow', }, ], placement: 'top', strategy: 'fixed', } ); const [linkPreviewApplied, setLinkPreviewApplied] = useState( LinkPreviewApplied.None ); const hasLinkPreviewApplied = linkPreviewApplied !== LinkPreviewApplied.None; const [linkPreviewInputValue, setLinkPreviewInputValue] = useState(''); useEffect(() => { if (!linkPreviewInputValue) { return; } if (linkPreviewApplied === LinkPreviewApplied.Manual) { return; } debouncedMaybeGrabLinkPreview( linkPreviewInputValue, LinkPreviewSourceType.StoryCreator ); }, [ debouncedMaybeGrabLinkPreview, linkPreviewApplied, linkPreviewInputValue, ]); useEffect(() => { if (!text) { return; } if (linkPreviewApplied === LinkPreviewApplied.Manual) { return; } debouncedMaybeGrabLinkPreview(text, LinkPreviewSourceType.StoryCreator); }, [debouncedMaybeGrabLinkPreview, linkPreviewApplied, text]); useEffect(() => { if (!linkPreview || !text) { return; } const links = findLinks(text); const shouldApplyLinkPreview = links.includes(linkPreview.url); setLinkPreviewApplied(oldValue => { if (oldValue === LinkPreviewApplied.Manual) { return oldValue; } if (shouldApplyLinkPreview) { return LinkPreviewApplied.Automatic; } return LinkPreviewApplied.None; }); }, [linkPreview, text]); const [isLinkPreviewInputShowing, setIsLinkPreviewInputShowing] = useState(false); const [linkPreviewInputPopperButtonRef, setLinkPreviewInputPopperButtonRef] = useState(null); const [linkPreviewInputPopperRef, setLinkPreviewInputPopperRef] = useState(null); const linkPreviewInputPopper = usePopper( linkPreviewInputPopperButtonRef, linkPreviewInputPopperRef, { modifiers: [ { name: 'arrow', }, ], placement: 'top', strategy: 'fixed', } ); useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { if ( isColorPickerShowing || isEditingText || isLinkPreviewInputShowing ) { setIsColorPickerShowing(false); setIsEditingText(false); setIsLinkPreviewInputShowing(false); event.preventDefault(); event.stopPropagation(); } else { onClose(); } } }; const useCapture = true; document.addEventListener('keydown', handleEscape, useCapture); return () => { document.removeEventListener('keydown', handleEscape, useCapture); }; }, [ isColorPickerShowing, isEditingText, isLinkPreviewInputShowing, colorPickerPopperButtonRef, onClose, ]); useEffect(() => { if (!isColorPickerShowing) { return noop; } return handleOutsideClick( () => { setIsColorPickerShowing(false); return true; }, { containerElements: [colorPickerPopperRef, colorPickerPopperButtonRef], name: 'TextStoryCreator.colorPicker', } ); }, [isColorPickerShowing, colorPickerPopperRef, colorPickerPopperButtonRef]); const sliderColorNumber = getRGBANumber(sliderValue); let textForegroundColor = sliderColorNumber; let textBackgroundColor: number | undefined; if (textBackground === TextBackground.Background) { textBackgroundColor = COLOR_WHITE_INT; textForegroundColor = sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber; } else if (textBackground === TextBackground.Inverse) { textBackgroundColor = sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber; textForegroundColor = COLOR_WHITE_INT; } const textAttachment: TextAttachmentType = { ...getBackground(selectedBackground), text, textStyle, textForegroundColor, textBackgroundColor, preview: hasLinkPreviewApplied ? linkPreview : undefined, }; const hasChanges = Boolean(text || hasLinkPreviewApplied); return (
{ if (!isEditingText) { setIsEditingText(true); } }} onRemoveLinkPreview={() => { setLinkPreviewApplied(LinkPreviewApplied.None); }} textAttachment={textAttachment} />
{isEditingText ? (
setTextStyle(TextStyle.Regular), value: TextStyle.Regular, }, { icon: 'StoryCreator__icon--font-bold', label: i18n('StoryCreator__text--bold'), onClick: () => setTextStyle(TextStyle.Bold), value: TextStyle.Bold, }, { icon: 'StoryCreator__icon--font-serif', label: i18n('StoryCreator__text--serif'), onClick: () => setTextStyle(TextStyle.Serif), value: TextStyle.Serif, }, { icon: 'StoryCreator__icon--font-script', label: i18n('StoryCreator__text--script'), onClick: () => setTextStyle(TextStyle.Script), value: TextStyle.Script, }, { icon: 'StoryCreator__icon--font-condensed', label: i18n('StoryCreator__text--condensed'), onClick: () => setTextStyle(TextStyle.Condensed), value: TextStyle.Condensed, }, ]} moduleClassName={classNames('StoryCreator__tools__tool', { 'StoryCreator__tools__button--font-regular': textStyle === TextStyle.Regular, 'StoryCreator__tools__button--font-bold': textStyle === TextStyle.Bold, 'StoryCreator__tools__button--font-serif': textStyle === TextStyle.Serif, 'StoryCreator__tools__button--font-script': textStyle === TextStyle.Script, 'StoryCreator__tools__button--font-condensed': textStyle === TextStyle.Condensed, })} theme={Theme.Dark} value={textStyle} />
) : (
)}
)} ) : (
{i18n('StoryCreator__link-preview-empty')}
)}
)}
); };