Send text attachment stories

This commit is contained in:
Josh Perez 2022-08-02 15:31:55 -04:00 committed by GitHub
parent 0340f4ee1d
commit 9eff67446f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1635 additions and 339 deletions

View file

@ -8,6 +8,11 @@ import { getClassNamesFor } from '../util/getClassNamesFor';
export type PropsType = {
checked?: boolean;
children?: (childrenOpts: {
id: string;
checkboxNode: JSX.Element;
labelNode: JSX.Element;
}) => JSX.Element;
description?: string;
disabled?: boolean;
isRadio?: boolean;
@ -20,6 +25,7 @@ export type PropsType = {
export const Checkbox = ({
checked,
children,
description,
disabled,
isRadio,
@ -31,26 +37,41 @@ export const Checkbox = ({
}: PropsType): JSX.Element => {
const getClassName = getClassNamesFor('Checkbox', moduleClassName);
const id = useMemo(() => `${name}::${uuid()}`, [name]);
const checkboxNode = (
<div className={getClassName('__checkbox')}>
<input
checked={Boolean(checked)}
disabled={disabled}
id={id}
name={name}
onChange={ev => onChange(ev.target.checked)}
onClick={onClick}
type={isRadio ? 'radio' : 'checkbox'}
/>
</div>
);
const labelNode = (
<div>
<label htmlFor={id}>
<div>{label}</div>
<div className={getClassName('__description')}>{description}</div>
</label>
</div>
);
return (
<div className={getClassName('')}>
<div className={getClassName('__container')}>
<div className={getClassName('__checkbox')}>
<input
checked={Boolean(checked)}
disabled={disabled}
id={id}
name={name}
onChange={ev => onChange(ev.target.checked)}
onClick={onClick}
type={isRadio ? 'radio' : 'checkbox'}
/>
</div>
<div>
<label htmlFor={id}>
<div>{label}</div>
<div className={getClassName('__description')}>{description}</div>
</label>
</div>
{children ? (
children({ id, checkboxNode, labelNode })
) : (
<>
{checkboxNode}
{labelNode}
</>
)}
</div>
</div>
);

View file

@ -0,0 +1,45 @@
// 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 { PropsType } from './SendStoryModal';
import enMessages from '../../_locales/en/messages.json';
import { SendStoryModal } from './SendStoryModal';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n';
import {
getMyStories,
getFakeDistributionLists,
} from '../test-both/helpers/getFakeDistributionLists';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/SendStoryModal',
component: SendStoryModal,
argTypes: {
distributionLists: {
defaultValue: [getMyStories()],
},
i18n: {
defaultValue: i18n,
},
me: {
defaultValue: getDefaultConversation(),
},
onClose: { action: true },
onSend: { action: true },
signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation),
},
},
} as Meta;
const Template: Story<PropsType> = args => <SendStoryModal {...args} />;
export const Modal = Template.bind({});
Modal.args = {
distributionLists: getFakeDistributionLists(),
};

View file

@ -0,0 +1,153 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo, useState } from 'react';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
import type { UUIDStringType } from '../types/UUID';
import { Avatar, AvatarSize } from './Avatar';
import { Checkbox } from './Checkbox';
import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
import { Modal } from './Modal';
import { StoryDistributionListName } from './StoryDistributionListName';
export type PropsType = {
distributionLists: Array<StoryDistributionListDataType>;
i18n: LocalizerType;
me: ConversationType;
onClose: () => unknown;
onSend: (listIds: Array<UUIDStringType>) => unknown;
signalConnections: Array<ConversationType>;
};
function getListViewers(
list: StoryDistributionListDataType,
i18n: LocalizerType,
signalConnections: Array<ConversationType>
): string {
let memberCount = list.memberUuids.length;
if (list.id === MY_STORIES_ID && list.isBlockList) {
memberCount = list.isBlockList
? signalConnections.length - list.memberUuids.length
: signalConnections.length;
}
return memberCount === 1
? i18n('StoriesSettingsModal__list__viewers--singular', ['1'])
: i18n('StoriesSettings__viewers--plural', [String(memberCount)]);
}
export const SendStoryModal = ({
distributionLists,
i18n,
me,
onClose,
onSend,
signalConnections,
}: PropsType): JSX.Element => {
const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>(
new Set()
);
const selectedListNames = useMemo(
() =>
distributionLists
.filter(list => selectedListIds.has(list.id))
.map(list => list.name),
[distributionLists, selectedListIds]
);
return (
<Modal
hasXButton
i18n={i18n}
onClose={onClose}
title={i18n('SendStoryModal__title')}
>
{distributionLists.map(list => (
<Checkbox
checked={selectedListIds.has(list.id)}
key={list.id}
label={getStoryDistributionListName(i18n, list.id, list.name)}
moduleClassName="SendStoryModal__distribution-list"
name="SendStoryModal__distribution-list"
onChange={(value: boolean) => {
if (value) {
setSelectedListIds(listIds => {
listIds.add(list.id);
return new Set([...listIds]);
});
} else {
setSelectedListIds(listIds => {
listIds.delete(list.id);
return new Set([...listIds]);
});
}
}}
>
{({ id, checkboxNode }) => (
<>
<label
className="SendStoryModal__distribution-list__label"
htmlFor={id}
>
{list.id === MY_STORIES_ID ? (
<Avatar
acceptedMessageRequest={me.acceptedMessageRequest}
avatarPath={me.avatarPath}
badge={undefined}
color={me.color}
conversationType={me.type}
i18n={i18n}
isMe
sharedGroupNames={me.sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
title={me.title}
/>
) : (
<span className="StoriesSettingsModal__list__avatar--private" />
)}
<div className="SendStoryModal__distribution-list__info">
<div className="SendStoryModal__distribution-list__name">
<StoryDistributionListName
i18n={i18n}
id={list.id}
name={list.name}
/>
</div>
<div className="SendStoryModal__distribution-list__description">
{getListViewers(list, i18n, signalConnections)}
</div>
</div>
</label>
{checkboxNode}
</>
)}
</Checkbox>
))}
<Modal.ButtonFooter moduleClassName="SendStoryModal">
<div className="SendStoryModal__selected-lists">
{selectedListNames
.map(listName =>
getStoryDistributionListName(i18n, listName, listName)
)
.join(', ')}
</div>
<button
aria-label="SendStoryModal__send"
className="SendStoryModal__send"
disabled={!selectedListIds.size}
onClick={() => {
onSend(Array.from(selectedListIds));
}}
type="button"
/>
</Modal.ButtonFooter>
</Modal>
);
};

View file

@ -6,11 +6,13 @@ import React from 'react';
import type { PropsType } from './StoriesSettingsModal';
import enMessages from '../../_locales/en/messages.json';
import { MY_STORIES_ID } from '../types/Stories';
import { StoriesSettingsModal } from './StoriesSettingsModal';
import { UUID } from '../types/UUID';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n';
import {
getMyStories,
getFakeDistributionList,
} from '../test-both/helpers/getFakeDistributionLists';
const i18n = setupI18n('en', enMessages);
@ -46,60 +48,59 @@ export default {
const Template: Story<PropsType> = args => <StoriesSettingsModal {...args} />;
export const MyStories = Template.bind({});
MyStories.args = {
distributionLists: [
{
allowsReplies: true,
id: MY_STORIES_ID,
isBlockList: false,
members: [],
name: MY_STORIES_ID,
},
],
};
{
const myStories = getMyStories();
MyStories.args = {
distributionLists: [
{
...myStories,
members: [],
},
],
};
}
export const MyStoriesBlockList = Template.bind({});
MyStoriesBlockList.args = {
distributionLists: [
{
allowsReplies: true,
id: MY_STORIES_ID,
isBlockList: true,
members: Array.from(Array(2), () => getDefaultConversation()),
name: MY_STORIES_ID,
},
],
};
{
const myStories = getMyStories();
MyStoriesBlockList.args = {
distributionLists: [
{
...myStories,
members: Array.from(Array(2), () => getDefaultConversation()),
},
],
};
}
export const MyStoriesExclusive = Template.bind({});
MyStoriesExclusive.args = {
distributionLists: [
{
allowsReplies: false,
id: MY_STORIES_ID,
isBlockList: false,
members: Array.from(Array(11), () => getDefaultConversation()),
name: MY_STORIES_ID,
},
],
};
{
const myStories = getMyStories();
MyStoriesExclusive.args = {
distributionLists: [
{
...myStories,
isBlockList: false,
members: Array.from(Array(11), () => getDefaultConversation()),
},
],
};
}
export const SingleList = Template.bind({});
SingleList.args = {
distributionLists: [
{
allowsReplies: true,
id: MY_STORIES_ID,
isBlockList: false,
members: [],
name: MY_STORIES_ID,
},
{
allowsReplies: true,
id: UUID.generate().toString(),
isBlockList: false,
members: Array.from(Array(4), () => getDefaultConversation()),
name: 'Thailand 2021',
},
],
};
{
const myStories = getMyStories();
const fakeDistroList = getFakeDistributionList();
SingleList.args = {
distributionLists: [
{
...myStories,
members: [],
},
{
...fakeDistroList,
members: fakeDistroList.memberUuids.map(() => getDefaultConversation()),
},
],
};
}

View file

@ -3,12 +3,13 @@
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './StoryCreator';
import enMessages from '../../_locales/en/messages.json';
import { StoryCreator } from './StoryCreator';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { getFakeDistributionLists } from '../test-both/helpers/getFakeDistributionLists';
import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages);
@ -16,26 +17,34 @@ const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/StoryCreator',
component: StoryCreator,
argTypes: {
debouncedMaybeGrabLinkPreview: { action: true },
distributionLists: { defaultValue: getFakeDistributionLists() },
linkPreview: {
defaultValue: undefined,
},
i18n: { defaultValue: i18n },
me: {
defaultValue: getDefaultConversation(),
},
onClose: { action: true },
onSend: { action: true },
signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation),
},
},
} as Meta;
const getDefaultProps = (): PropsType => ({
debouncedMaybeGrabLinkPreview: action('debouncedMaybeGrabLinkPreview'),
i18n,
onClose: action('onClose'),
onNext: action('onNext'),
});
const Template: Story<PropsType> = args => <StoryCreator {...args} />;
export const Default = Template.bind({});
Default.args = getDefaultProps();
Default.args = {};
Default.story = {
name: 'w/o Link Preview available',
};
export const LinkPreview = Template.bind({});
LinkPreview.args = {
...getDefaultProps(),
linkPreview: {
domain: 'www.catsandkittens.lolcats',
image: fakeAttachment({

View file

@ -7,9 +7,12 @@ import classNames from 'classnames';
import { get, has } from 'lodash';
import { usePopper } from 'react-popper';
import type { ConversationType } from '../state/ducks/conversations';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { LocalizerType } from '../types/Util';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
import type { TextAttachmentType } from '../types/Attachment';
import type { UUIDStringType } from '../types/UUID';
import { Button, ButtonVariant } from './Button';
import { ContextMenu } from './ContextMenu';
@ -17,6 +20,7 @@ import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import { Input } from './Input';
import { Slider } from './Slider';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { SendStoryModal } from './SendStoryModal';
import { TextAttachment } from './TextAttachment';
import { Theme, themeClassName } from '../util/theme';
import { getRGBA, getRGBANumber } from '../mediaEditor/util/color';
@ -32,10 +36,16 @@ export type PropsType = {
message: string,
source: LinkPreviewSourceType
) => unknown;
distributionLists: Array<StoryDistributionListDataType>;
i18n: LocalizerType;
linkPreview?: LinkPreviewType;
me: ConversationType;
onClose: () => unknown;
onNext: () => unknown;
onSend: (
listIds: Array<UUIDStringType>,
textAttachment: TextAttachmentType
) => unknown;
signalConnections: Array<ConversationType>;
};
enum TextStyle {
@ -92,10 +102,13 @@ function getBackground(
export const StoryCreator = ({
debouncedMaybeGrabLinkPreview,
distributionLists,
i18n,
linkPreview,
me,
onClose,
onNext,
onSend,
signalConnections,
}: PropsType): JSX.Element => {
const [isEditingText, setIsEditingText] = useState(false);
const [selectedBackground, setSelectedBackground] =
@ -106,6 +119,7 @@ export const StoryCreator = ({
);
const [sliderValue, setSliderValue] = useState<number>(100);
const [text, setText] = useState<string>('');
const [hasSendToModal, setHasSendToModal] = useState(false);
const textEditorRef = useRef<HTMLInputElement | null>(null);
@ -229,266 +243,289 @@ export const StoryCreator = ({
textForegroundColor = COLOR_WHITE_INT;
}
const textAttachment: TextAttachmentType = {
...getBackground(selectedBackground),
text,
textStyle,
textForegroundColor,
textBackgroundColor,
preview: hasLinkPreviewApplied ? linkPreview : undefined,
};
const hasChanges = Boolean(text || hasLinkPreviewApplied);
return (
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="StoryCreator">
<div className="StoryCreator__container">
<TextAttachment
disableLinkPreviewPopup
i18n={i18n}
isEditingText={isEditingText}
onChange={setText}
onClick={() => {
if (!isEditingText) {
setIsEditingText(true);
}
}}
onRemoveLinkPreview={() => {
setHasLinkPreviewApplied(false);
}}
textAttachment={{
...getBackground(selectedBackground),
text,
textStyle,
textForegroundColor,
textBackgroundColor,
preview: hasLinkPreviewApplied ? linkPreview : undefined,
}}
/>
</div>
<div className="StoryCreator__toolbar">
{isEditingText ? (
<div className="StoryCreator__tools">
<Slider
handleStyle={{ backgroundColor: getRGBA(sliderValue) }}
label={i18n('CustomColorEditor__hue')}
moduleClassName="HueSlider StoryCreator__tools__tool"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
i18n={i18n}
menuOptions={[
{
icon: 'StoryCreator__icon--font-regular',
label: i18n('StoryCreator__text--regular'),
onClick: () => 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}
/>
<button
aria-label={i18n('StoryCreator__text-bg')}
className={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--bg-none':
textBackground === TextBackground.None,
'StoryCreator__tools__button--bg':
textBackground === TextBackground.Background,
'StoryCreator__tools__button--bg-inverse':
textBackground === TextBackground.Inverse,
})}
onClick={() => {
if (textBackground === TextBackground.None) {
setTextBackground(TextBackground.Background);
} else if (textBackground === TextBackground.Background) {
setTextBackground(TextBackground.Inverse);
} else {
setTextBackground(TextBackground.None);
}
}}
type="button"
/>
</div>
) : (
<div className="StoryCreator__toolbar--space" />
)}
<div className="StoryCreator__toolbar--buttons">
<Button
onClick={onClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('discard')}
</Button>
<div className="StoryCreator__controls">
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--bg': true,
'StoryCreator__control--bg--selected': isColorPickerShowing,
})}
onClick={() => setIsColorPickerShowing(!isColorPickerShowing)}
ref={setColorPickerPopperButtonRef}
style={{
background: getBackgroundColor(
getBackground(selectedBackground)
),
}}
type="button"
/>
{isColorPickerShowing && (
<div
className="StoryCreator__popper"
ref={setColorPickerPopperRef}
style={colorPickerPopper.styles.popper}
{...colorPickerPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
{objectMap<BackgroundStyleType>(
BackgroundStyle,
(bg, backgroundValue) => (
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__bg: true,
'StoryCreator__bg--selected':
selectedBackground === backgroundValue,
})}
key={String(bg)}
onClick={() => {
setSelectedBackground(backgroundValue);
setIsColorPickerShowing(false);
}}
type="button"
style={{
background: getBackgroundColor(
getBackground(backgroundValue)
),
}}
/>
)
)}
</div>
)}
<button
aria-label={i18n('StoryCreator__control--draw')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--text': true,
'StoryCreator__control--selected': isEditingText,
})}
onClick={() => {
setIsEditingText(!isEditingText);
}}
type="button"
/>
<button
aria-label={i18n('StoryCreator__control--link')}
className="StoryCreator__control StoryCreator__control--link"
onClick={() =>
setIsLinkPreviewInputShowing(!isLinkPreviewInputShowing)
<>
{hasSendToModal && (
<SendStoryModal
distributionLists={distributionLists}
i18n={i18n}
me={me}
onClose={() => setHasSendToModal(false)}
onSend={listIds => {
onSend(listIds, textAttachment);
setHasSendToModal(false);
onClose();
}}
signalConnections={signalConnections}
/>
)}
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="StoryCreator">
<div className="StoryCreator__container">
<TextAttachment
disableLinkPreviewPopup
i18n={i18n}
isEditingText={isEditingText}
onChange={setText}
onClick={() => {
if (!isEditingText) {
setIsEditingText(true);
}
ref={setLinkPreviewInputPopperButtonRef}
type="button"
/>
{isLinkPreviewInputShowing && (
<div
className={classNames(
'StoryCreator__popper StoryCreator__link-preview-input-popper',
themeClassName(Theme.Dark)
)}
ref={setLinkPreviewInputPopperRef}
style={linkPreviewInputPopper.styles.popper}
{...linkPreviewInputPopper.attributes.popper}
>
}}
onRemoveLinkPreview={() => {
setHasLinkPreviewApplied(false);
}}
textAttachment={textAttachment}
/>
</div>
<div className="StoryCreator__toolbar">
{isEditingText ? (
<div className="StoryCreator__tools">
<Slider
handleStyle={{ backgroundColor: getRGBA(sliderValue) }}
label={i18n('CustomColorEditor__hue')}
moduleClassName="HueSlider StoryCreator__tools__tool"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
i18n={i18n}
menuOptions={[
{
icon: 'StoryCreator__icon--font-regular',
label: i18n('StoryCreator__text--regular'),
onClick: () => 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}
/>
<button
aria-label={i18n('StoryCreator__text-bg')}
className={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--bg-none':
textBackground === TextBackground.None,
'StoryCreator__tools__button--bg':
textBackground === TextBackground.Background,
'StoryCreator__tools__button--bg-inverse':
textBackground === TextBackground.Inverse,
})}
onClick={() => {
if (textBackground === TextBackground.None) {
setTextBackground(TextBackground.Background);
} else if (textBackground === TextBackground.Background) {
setTextBackground(TextBackground.Inverse);
} else {
setTextBackground(TextBackground.None);
}
}}
type="button"
/>
</div>
) : (
<div className="StoryCreator__toolbar--space" />
)}
<div className="StoryCreator__toolbar--buttons">
<Button
onClick={onClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('discard')}
</Button>
<div className="StoryCreator__controls">
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--bg': true,
'StoryCreator__control--bg--selected': isColorPickerShowing,
})}
onClick={() => setIsColorPickerShowing(!isColorPickerShowing)}
ref={setColorPickerPopperButtonRef}
style={{
background: getBackgroundColor(
getBackground(selectedBackground)
),
}}
type="button"
/>
{isColorPickerShowing && (
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
<Input
disableSpellcheck
i18n={i18n}
moduleClassName="StoryCreator__link-preview-input"
onChange={setLinkPreviewInputValue}
placeholder={i18n('StoryCreator__link-preview-placeholder')}
ref={el => el?.focus()}
value={linkPreviewInputValue}
/>
<div className="StoryCreator__link-preview-container">
{linkPreview ? (
<>
<StagedLinkPreview
domain={linkPreview.domain}
i18n={i18n}
image={linkPreview.image}
moduleClassName="StoryCreator__link-preview"
title={linkPreview.title}
url={linkPreview.url}
/>
<Button
className="StoryCreator__link-preview-button"
className="StoryCreator__popper"
ref={setColorPickerPopperRef}
style={colorPickerPopper.styles.popper}
{...colorPickerPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
{objectMap<BackgroundStyleType>(
BackgroundStyle,
(bg, backgroundValue) => (
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__bg: true,
'StoryCreator__bg--selected':
selectedBackground === backgroundValue,
})}
key={String(bg)}
onClick={() => {
setHasLinkPreviewApplied(true);
setIsLinkPreviewInputShowing(false);
setSelectedBackground(backgroundValue);
setIsColorPickerShowing(false);
}}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__add-link')}
</Button>
</>
) : (
<div className="StoryCreator__link-preview-empty">
<div className="StoryCreator__link-preview-empty__icon" />
{i18n('StoryCreator__link-preview-empty')}
</div>
type="button"
style={{
background: getBackgroundColor(
getBackground(backgroundValue)
),
}}
/>
)
)}
</div>
</div>
)}
)}
<button
aria-label={i18n('StoryCreator__control--text')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--text': true,
'StoryCreator__control--selected': isEditingText,
})}
onClick={() => {
setIsEditingText(!isEditingText);
}}
type="button"
/>
<button
aria-label={i18n('StoryCreator__control--link')}
className="StoryCreator__control StoryCreator__control--link"
onClick={() =>
setIsLinkPreviewInputShowing(!isLinkPreviewInputShowing)
}
ref={setLinkPreviewInputPopperButtonRef}
type="button"
/>
{isLinkPreviewInputShowing && (
<div
className={classNames(
'StoryCreator__popper StoryCreator__link-preview-input-popper',
themeClassName(Theme.Dark)
)}
ref={setLinkPreviewInputPopperRef}
style={linkPreviewInputPopper.styles.popper}
{...linkPreviewInputPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
<Input
disableSpellcheck
i18n={i18n}
moduleClassName="StoryCreator__link-preview-input"
onChange={setLinkPreviewInputValue}
placeholder={i18n(
'StoryCreator__link-preview-placeholder'
)}
ref={el => el?.focus()}
value={linkPreviewInputValue}
/>
<div className="StoryCreator__link-preview-container">
{linkPreview ? (
<>
<StagedLinkPreview
domain={linkPreview.domain}
i18n={i18n}
image={linkPreview.image}
moduleClassName="StoryCreator__link-preview"
title={linkPreview.title}
url={linkPreview.url}
/>
<Button
className="StoryCreator__link-preview-button"
onClick={() => {
setHasLinkPreviewApplied(true);
setIsLinkPreviewInputShowing(false);
}}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__add-link')}
</Button>
</>
) : (
<div className="StoryCreator__link-preview-empty">
<div className="StoryCreator__link-preview-empty__icon" />
{i18n('StoryCreator__link-preview-empty')}
</div>
)}
</div>
</div>
)}
</div>
<Button
disabled={!hasChanges}
onClick={() => setHasSendToModal(true)}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__next')}
</Button>
</div>
<Button
onClick={onNext}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__next')}
</Button>
</div>
</div>
</div>
</FocusTrap>
</FocusTrap>
</>
);
};