diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4ce9e9f89..c3e692b9a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -15,6 +15,18 @@ } ] }, + "AddCaptionModal__title": { + "message": "Add a message", + "description": "Shown as the title of the dialog that allows you to add a caption to a story" + }, + "AddCaptionModal__placeholder": { + "message": "Message", + "description": "Placeholder text for textarea when adding a caption/message (we don't know which yet so we default to message)" + }, + "AddCaptionModal__submit-button": { + "message": "Done", + "description": "Label on the button that submits changes to a story's caption in the add-caption dialog" + }, "AddUserToAnotherGroupModal__title": { "message": "Add to a group", "description": "Shown as the title of the dialog that allows you to add a contact to an group" @@ -5335,6 +5347,10 @@ "message": "Crop", "description": "Performs the crop" }, + "MediaEditor__caption-button": { + "message": "Add a message", + "description": "Label of the button on the bottom of the media editor that trigger the add-caption dialog" + }, "MyStories__title": { "message": "My Stories", "description": "Title for the my stories list" diff --git a/stylesheets/components/CompositionTextArea.scss b/stylesheets/components/CompositionTextArea.scss new file mode 100644 index 000000000..9ea0730a4 --- /dev/null +++ b/stylesheets/components/CompositionTextArea.scss @@ -0,0 +1,65 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.CompositionTextArea { + position: relative; + + &__input { + &__input { + background: inherit; + border: none; + border-radius: 0; + height: 100%; + + &:focus-within { + border: none; + } + + @include dark-theme() { + border: none; + + &:focus-within { + border: none; + } + } + + @include keyboard-mode { + &:focus-within { + border: solid 1px $color-ultramarine; + } + } + } + + &__input__scroller { + max-height: 300px; + min-height: 300px; + padding: 16px; + // Need more padding on the right to make room for floating emoji button + padding-right: 36px; + } + } + + &__emoji { + position: absolute; + right: 8px; + top: 8px; + + button::after { + background-color: $color-black; + } + } + + &__remaining-character-count { + @include font-subtitle; + color: $color-gray-45; + position: absolute; + bottom: 0; + left: 0; + padding: 12px 12px 12px 24px; + } + + // remove background, should be seamless with modal + .module-composition-input__input { + background: transparent; + } +} diff --git a/stylesheets/components/ForwardMessageModal.scss b/stylesheets/components/ForwardMessageModal.scss index 9bfeed6a1..775e9f665 100644 --- a/stylesheets/components/ForwardMessageModal.scss +++ b/stylesheets/components/ForwardMessageModal.scss @@ -31,41 +31,6 @@ } } - &__input { - &__input { - background: inherit; - border: none; - border-radius: 0; - height: 100%; - - &:focus-within { - border: none; - } - - @include dark-theme() { - border: none; - - &:focus-within { - border: none; - } - } - - @include keyboard-mode { - &:focus-within { - border: solid 1px $color-ultramarine; - } - } - } - - &__input__scroller { - max-height: 300px; - min-height: 300px; - padding: 16px; - // Need more padding on the right to make room for floating emoji button - padding-right: 36px; - } - } - &__header { align-items: center; display: flex; @@ -160,11 +125,6 @@ min-height: 300px; } - &__text-edit-area { - height: 100%; - position: relative; - } - &__no-candidate-contacts { flex-grow: 1; display: flex; @@ -206,16 +166,6 @@ } } - &__emoji { - position: absolute; - right: 8px; - top: 8px; - - button::after { - background-color: $color-black; - } - } - &__footer { @include font-body-2; align-items: center; diff --git a/stylesheets/components/MediaEditor.scss b/stylesheets/components/MediaEditor.scss index 65d700f20..d0dd1a4f7 100644 --- a/stylesheets/components/MediaEditor.scss +++ b/stylesheets/components/MediaEditor.scss @@ -131,6 +131,27 @@ height: $tools-height; margin-bottom: 22px; } + + &__caption { + height: $tools-height; + margin-bottom: 22px; + + &__add-caption-button { + @include button-reset; + border-radius: 9999px; + background: $color-gray-90; + color: $color-gray-15; + padding: 8px 15px; + border: none; + + & > span { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; + } + } + } } &__controls { diff --git a/stylesheets/components/Modal.scss b/stylesheets/components/Modal.scss index 1b0102bdd..0375dd304 100644 --- a/stylesheets/components/Modal.scss +++ b/stylesheets/components/Modal.scss @@ -133,6 +133,7 @@ margin: 0; overflow-y: overlay; overflow-x: auto; + transition: border-color 150ms ease-in-out; } &--padded { @@ -141,6 +142,17 @@ } } + &--has-header#{&}--header-divider { + .module-Modal__body { + @include light-theme() { + border-top-color: $color-gray-15; + } + @include dark-theme() { + border-top-color: $color-gray-60; + } + } + } + &--has-header { .module-Modal__body { padding-top: 0; @@ -158,6 +170,23 @@ } } + &--has-footer#{&}--footer-divider { + .module-Modal__body { + @include light-theme() { + border-bottom-color: $color-gray-15; + } + @include dark-theme() { + border-bottom-color: $color-gray-60; + } + } + } + + &--has-footer { + .module-Modal__body { + border-bottom: 1px solid transparent; + } + } + &__button-footer { display: flex; flex-wrap: wrap; diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 3d41641d3..50392e5c0 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -50,6 +50,7 @@ @import './components/ChatColorPicker.scss'; @import './components/Checkbox.scss'; @import './components/CompositionArea.scss'; +@import './components/CompositionTextArea.scss'; @import './components/ContactModal.scss'; @import './components/ContactName.scss'; @import './components/ContactPill.scss'; diff --git a/ts/components/AddCaptionModal.stories.tsx b/ts/components/AddCaptionModal.stories.tsx new file mode 100644 index 000000000..3cc8e95cb --- /dev/null +++ b/ts/components/AddCaptionModal.stories.tsx @@ -0,0 +1,47 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import type { Meta, Story } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import type { Props } from './AddCaptionModal'; +import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; +import { AddCaptionModal } from './AddCaptionModal'; +import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; +import enMessages from '../../_locales/en/messages.json'; +import { setupI18n } from '../util/setupI18n'; +import { CompositionTextArea } from './CompositionTextArea'; + +const i18n = setupI18n('en', enMessages); + +export default { + title: 'Components/AddCaptionModal', + component: AddCaptionModal, + argTypes: { + i18n: { + defaultValue: i18n, + }, + RenderCompositionTextArea: { + defaultValue: (props: SmartCompositionTextAreaProps) => ( + undefined} + /> + ), + }, + }, +} as Meta; + +const Template: Story = args => ( + +); + +export const Modal = Template.bind({}); +Modal.args = { + draftText: 'Some caption text', +}; diff --git a/ts/components/AddCaptionModal.tsx b/ts/components/AddCaptionModal.tsx new file mode 100644 index 000000000..ce8819b3b --- /dev/null +++ b/ts/components/AddCaptionModal.tsx @@ -0,0 +1,87 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useEffect } from 'react'; +import { noop } from 'lodash'; +import { Button } from './Button'; +import { Modal } from './Modal'; +import type { LocalizerType, ThemeType } from '../types/Util'; +import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; + +export type Props = { + i18n: LocalizerType; + onClose: () => void; + onSubmit: (text: string) => void; + draftText: string; + theme: ThemeType; + RenderCompositionTextArea: ( + props: SmartCompositionTextAreaProps + ) => JSX.Element; +}; + +export const AddCaptionModal = ({ + i18n, + onClose, + onSubmit, + draftText, + RenderCompositionTextArea, + theme, +}: Props): JSX.Element => { + const [messageText, setMessageText] = React.useState(''); + + const [isScrolledTop, setIsScrolledTop] = React.useState(true); + const [isScrolledBottom, setIsScrolledBottom] = React.useState(true); + + const scrollerRef = React.useRef(null); + + // add footer/header dividers depending on the state of scroll + const updateScrollState = React.useCallback(() => { + const scrollerEl = scrollerRef.current; + if (scrollerEl) { + setIsScrolledTop(scrollerEl.scrollTop === 0); + setIsScrolledBottom( + scrollerEl.scrollHeight - scrollerEl.scrollTop === + scrollerEl.clientHeight + ); + } + }, [scrollerRef]); + + useEffect(() => { + updateScrollState(); + }, [updateScrollState]); + + const handleSubmit = React.useCallback(() => { + onSubmit(messageText); + }, [messageText, onSubmit]); + + return ( + + {i18n('AddCaptionModal__submit-button')} + + } + > + + + ); +}; diff --git a/ts/components/AddUserToAnotherGroupModal.stories.tsx b/ts/components/AddUserToAnotherGroupModal.stories.tsx index 1947406ac..7cf0c0887 100644 --- a/ts/components/AddUserToAnotherGroupModal.stories.tsx +++ b/ts/components/AddUserToAnotherGroupModal.stories.tsx @@ -1,8 +1,8 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Meta, Story } from '@storybook/react'; import React from 'react'; +import type { Meta, Story } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import type { Props } from './AddUserToAnotherGroupModal'; diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index dac513388..505d6efe6 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -39,6 +39,7 @@ import { SignalClipboard } from '../quill/signal-clipboard'; import { DirectionalBlot } from '../quill/block/blot'; import { getClassNamesFor } from '../util/getClassNamesFor'; import * as log from '../logging/log'; +import { useRefMerger } from '../hooks/useRefMerger'; Quill.register('formats/emoji', EmojiBlot); Quill.register('formats/mention', MentionBlot); @@ -55,6 +56,7 @@ type HistoryStatic = { export type InputApi = { focus: () => void; insertEmoji: (e: EmojiPickDataType) => void; + setText: (text: string, cursorToEnd?: boolean) => void; reset: () => void; resetEmojiResults: () => void; submit: () => void; @@ -74,6 +76,7 @@ export type Props = { readonly theme: ThemeType; readonly placeholder?: string; sortedGroupMembers?: Array; + scrollerRef?: React.RefObject; onDirtyChange?(dirty: boolean): unknown; onEditorStateChange?( messageText: string, @@ -87,6 +90,7 @@ export type Props = { mentions: Array, timestamp: number ): unknown; + onScroll?: (ev: React.UIEvent) => void; getQuotedMessage?(): unknown; clearQuotedMessage?(): unknown; }; @@ -104,6 +108,7 @@ export function CompositionInput(props: Props): React.ReactElement { moduleClassName, onPickEmoji, onSubmit, + onScroll, placeholder, skinTone, draftText, @@ -115,6 +120,8 @@ export function CompositionInput(props: Props): React.ReactElement { theme, } = props; + const refMerger = useRefMerger(); + const [emojiCompletionElement, setEmojiCompletionElement] = React.useState(); const [lastSelectionRange, setLastSelectionRange] = @@ -125,7 +132,9 @@ export function CompositionInput(props: Props): React.ReactElement { const emojiCompletionRef = React.useRef(); const mentionCompletionRef = React.useRef(); const quillRef = React.useRef(); - const scrollerRef = React.useRef(null); + + const scrollerRefInner = React.useRef(null); + const propsRef = React.useRef(props); const canSendRef = React.useRef(false); const memberRepositoryRef = React.useRef( @@ -219,6 +228,20 @@ export function CompositionInput(props: Props): React.ReactElement { historyModule.clear(); }; + const setText = (text: string, cursorToEnd?: boolean) => { + const quill = quillRef.current; + + if (quill === undefined) { + return; + } + + canSendRef.current = true; + quill.setText(text); + if (cursorToEnd) { + quill.setSelection(quill.getLength(), 0); + } + }; + const resetEmojiResults = () => { const emojiCompletion = emojiCompletionRef.current; @@ -257,6 +280,7 @@ export function CompositionInput(props: Props): React.ReactElement { inputApi.current = { focus, insertEmoji, + setText, reset, resetEmojiResults, submit, @@ -597,7 +621,7 @@ export function CompositionInput(props: Props): React.ReactElement { // When loading a multi-line message out of a draft, the cursor // position needs to be pushed to the end of the input manually. quill.once('editor-change', () => { - const scroller = scrollerRef.current; + const scroller = scrollerRefInner.current; if (scroller != null) { quill.scrollingContainer = scroller; @@ -648,8 +672,13 @@ export function CompositionInput(props: Props): React.ReactElement { {({ ref }) => (
; + onScroll?: (ev: React.UIEvent) => void; + onPickEmoji: (e: EmojiPickDataType) => void; + onChange: ( + messageText: string, + bodyRanges: Array, + caretLocation?: number | undefined + ) => void; + onSetSkinTone: (tone: number) => void; + onSubmit: ( + message: string, + mentions: Array, + timestamp: number + ) => void; + onTextTooLong: () => void; + getPreferredBadge: PreferredBadgeSelectorType; + draftText: string; + theme: ThemeType; +} & Pick; + +/** + * Essentially an HTML textarea but with support for emoji picker and + * at-mentions autocomplete. + * + * Meant for modals that need to collect a message or caption. It is + * basically a rectangle input with an emoji selector floating at the top-right + */ +export const CompositionTextArea = ({ + i18n, + placeholder, + maxLength, + whenToShowRemainingCount = Infinity, + scrollerRef, + onScroll, + onPickEmoji, + onChange, + onSetSkinTone, + onSubmit, + onTextTooLong, + getPreferredBadge, + draftText, + theme, + recentEmojis, + skinTone, +}: CompositionTextAreaProps): JSX.Element => { + const inputApiRef = React.useRef(); + const [characterCount, setCharacterCount] = React.useState( + grapheme.count(draftText) + ); + + const insertEmoji = React.useCallback( + (e: EmojiPickDataType) => { + if (inputApiRef.current) { + inputApiRef.current.insertEmoji(e); + onPickEmoji(e); + } + }, + [inputApiRef, onPickEmoji] + ); + + const focusTextEditInput = React.useCallback(() => { + if (inputApiRef.current) { + inputApiRef.current.focus(); + } + }, [inputApiRef]); + + const handleChange = React.useCallback( + ( + newValue: string, + bodyRanges: Array, + caretLocation?: number | undefined + ) => { + const inputEl = inputApiRef.current; + if (!inputEl) { + return; + } + + const [newValueSized, newCharacterCount] = grapheme.truncateAndSize( + newValue, + maxLength + ); + + if (maxLength !== undefined) { + // if we had to truncate + if (newValueSized.length < newValue.length) { + // reset quill to the value before the change that pushed it over the max + // and push the cursor to the end + // + // this is not perfect as it pushes the cursor to the end, even if the user + // was modifying text in the middle of the editor + // a better solution would be to prevent the change to begin with, but + // quill makes this VERY difficult + inputEl.setText(newValueSized, true); + } + } + setCharacterCount(newCharacterCount); + onChange(newValue, bodyRanges, caretLocation); + }, + [maxLength, onChange] + ); + + return ( +
+ +
+ +
+ {maxLength !== undefined && + characterCount >= whenToShowRemainingCount && ( +
+ {maxLength - characterCount} +
+ )} +
+ ); +}; diff --git a/ts/components/ForwardMessageModal.stories.tsx b/ts/components/ForwardMessageModal.stories.tsx index 85dbadd20..959669ca8 100644 --- a/ts/components/ForwardMessageModal.stories.tsx +++ b/ts/components/ForwardMessageModal.stories.tsx @@ -14,6 +14,7 @@ import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME'; import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { setupI18n } from '../util/setupI18n'; import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; +import { CompositionTextArea } from './CompositionTextArea'; const createAttachment = ( props: Partial = {} @@ -55,12 +56,18 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ messageBody: text('messageBody', overrideProps.messageBody || ''), onClose: action('onClose'), onEditorStateChange: action('onEditorStateChange'), - onPickEmoji: action('onPickEmoji'), - onTextTooLong: action('onTextTooLong'), - onSetSkinTone: action('onSetSkinTone'), - recentEmojis: [], removeLinkPreview: action('removeLinkPreview'), - skinTone: 0, + RenderCompositionTextArea: props => ( + undefined} + /> + ), theme: React.useContext(StorybookThemeContext), regionCode: 'US', }); diff --git a/ts/components/ForwardMessageModal.tsx b/ts/components/ForwardMessageModal.tsx index 62da3055e..64c97eeba 100644 --- a/ts/components/ForwardMessageModal.tsx +++ b/ts/components/ForwardMessageModal.tsx @@ -11,26 +11,21 @@ import React, { } from 'react'; import type { MeasuredComponentProps } from 'react-measure'; import Measure from 'react-measure'; -import { noop } from 'lodash'; import { animated } from '@react-spring/web'; import classNames from 'classnames'; import { AttachmentList } from './conversation/AttachmentList'; import type { AttachmentType } from '../types/Attachment'; import { Button } from './Button'; -import type { InputApi } from './CompositionInput'; -import { CompositionInput } from './CompositionInput'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; import type { Row } from './ConversationList'; import { ConversationList, RowType } from './ConversationList'; import type { ConversationType } from '../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; -import type { Props as EmojiButtonProps } from './emoji/EmojiButton'; -import { EmojiButton } from './emoji/EmojiButton'; -import type { EmojiPickDataType } from './emoji/EmojiPicker'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; import type { BodyRangeType, LocalizerType, ThemeType } from '../types/Util'; +import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; import { ModalHost } from './ModalHost'; import { SearchInput } from './SearchInput'; import { StagedLinkPreview } from './conversation/StagedLinkPreview'; @@ -62,15 +57,14 @@ export type DataPropsType = { bodyRanges: Array, caretLocation?: number ) => unknown; - onTextTooLong: () => void; theme: ThemeType; regionCode: string | undefined; -} & Pick; + RenderCompositionTextArea: ( + props: SmartCompositionTextAreaProps + ) => JSX.Element; +}; -type ActionPropsType = Pick< - EmojiButtonProps, - 'onPickEmoji' | 'onSetSkinTone' -> & { +type ActionPropsType = { removeLinkPreview: () => void; }; @@ -90,17 +84,12 @@ export const ForwardMessageModal: FunctionComponent = ({ messageBody, onClose, onEditorStateChange, - onPickEmoji, - onSetSkinTone, - onTextTooLong, - recentEmojis, removeLinkPreview, - skinTone, + RenderCompositionTextArea, theme, regionCode, }) => { const inputRef = useRef(null); - const inputApiRef = React.useRef(); const [selectedContacts, setSelectedContacts] = useState< Array >([]); @@ -125,22 +114,6 @@ export const ForwardMessageModal: FunctionComponent = ({ [selectedContacts] ); - const focusTextEditInput = React.useCallback(() => { - if (inputApiRef.current) { - inputApiRef.current.focus(); - } - }, [inputApiRef]); - - const insertEmoji = React.useCallback( - (e: EmojiPickDataType) => { - if (inputApiRef.current) { - inputApiRef.current.insertEmoji(e); - onPickEmoji(e); - } - }, - [inputApiRef, onPickEmoji] - ); - const hasContactsSelected = Boolean(selectedContacts.length); const canForwardMessage = @@ -351,40 +324,16 @@ export const ForwardMessageModal: FunctionComponent = ({ }} /> ) : null} -
- { - setMessageBodyText(messageText); - onEditorStateChange(messageText, bodyRanges, caretLocation); - }} - onPickEmoji={onPickEmoji} - onSubmit={forwardMessage} - onTextTooLong={onTextTooLong} - theme={theme} - /> -
- -
-
+ + { + setMessageBodyText(messageText); + onEditorStateChange(messageText, bodyRanges, caretLocation); + }} + onSubmit={forwardMessage} + theme={theme} + />
) : (
diff --git a/ts/components/MediaEditor.stories.tsx b/ts/components/MediaEditor.stories.tsx index e10fdd1ff..f849b4ed0 100644 --- a/ts/components/MediaEditor.stories.tsx +++ b/ts/components/MediaEditor.stories.tsx @@ -9,6 +9,7 @@ import { MediaEditor } from './MediaEditor'; import enMessages from '../../_locales/en/messages.json'; import { setupI18n } from '../util/setupI18n'; import { Stickers, installedPacks } from '../test-both/helpers/getStickerPacks'; +import { CompositionTextArea } from './CompositionTextArea'; const i18n = setupI18n('en', enMessages); @@ -47,3 +48,20 @@ export const Smol = (): JSX.Element => ( export const Portrait = (): JSX.Element => ( ); + +export const WithCaption = (): JSX.Element => ( + ( + undefined} + /> + )} + /> +); diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index 45df98f08..f91f86528 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -9,6 +9,7 @@ import { fabric } from 'fabric'; import { get, has, noop } from 'lodash'; import type { LocalizerType } from '../types/Util'; +import { ThemeType } from '../types/Util'; import type { Props as StickerButtonProps } from './stickers/StickerButton'; import type { ImageStateType } from '../mediaEditor/ImageStateType'; @@ -33,14 +34,30 @@ import { TextStyle, getTextStyleAttributes, } from '../mediaEditor/util/getTextStyleAttributes'; +import { AddCaptionModal } from './AddCaptionModal'; +import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; +import { Emojify } from './conversation/Emojify'; +import { AddNewLines } from './conversation/AddNewLines'; export type PropsType = { doneButtonLabel?: string; i18n: LocalizerType; imageSrc: string; onClose: () => unknown; - onDone: (data: Uint8Array) => unknown; -} & Pick; + onDone: (data: Uint8Array, caption?: string | undefined) => unknown; +} & Pick & + ( + | { + supportsCaption: true; + renderCompositionTextArea: ( + props: SmartCompositionTextAreaProps + ) => JSX.Element; + } + | { + supportsCaption?: false; + renderCompositionTextArea?: undefined; + } + ); const INITIAL_IMAGE_STATE: ImageStateType = { angle: 0, @@ -94,12 +111,17 @@ export const MediaEditor = ({ // StickerButtonProps installedPacks, recentStickers, + ...props }: PropsType): JSX.Element | null => { const [fabricCanvas, setFabricCanvas] = useState(); const [image, setImage] = useState(new Image()); const [isStickerPopperOpen, setIsStickerPopperOpen] = useState(false); + const [caption, setCaption] = useState(''); + + const [showAddCaptionModal, setShowAddCaptionModal] = useState(false); + const canvasId = useUniqueId(); const [imageState, setImageState] = @@ -892,7 +914,46 @@ export const MediaEditor = ({ {tooling ? (
{tooling}
) : ( -
+ <> + {props.supportsCaption ? ( +
+ + + {showAddCaptionModal && ( + { + setCaption(messageText.trim()); + setShowAddCaptionModal(false); + }} + onClose={() => setShowAddCaptionModal(false)} + RenderCompositionTextArea={props.renderCompositionTextArea} + theme={ThemeType.dark} + /> + )} +
+ ) : ( +
+ )} + )}