Send text attachment stories
This commit is contained in:
parent
0340f4ee1d
commit
9eff67446f
22 changed files with 1635 additions and 339 deletions
|
@ -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>
|
||||
);
|
||||
|
|
45
ts/components/SendStoryModal.stories.tsx
Normal file
45
ts/components/SendStoryModal.stories.tsx
Normal 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(),
|
||||
};
|
153
ts/components/SendStoryModal.tsx
Normal file
153
ts/components/SendStoryModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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()),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue