Improvements to the media editor

This commit is contained in:
Josh Perez 2023-09-14 13:04:48 -04:00 committed by GitHub
parent e8eb7638c4
commit d0296ececa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1124 additions and 969 deletions

View file

@ -1,52 +0,0 @@
// 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) => (
<CompositionTextArea
{...props}
getPreferredBadge={() => undefined}
i18n={i18n}
isFormattingEnabled
isFormattingFlagEnabled
isFormattingSpoilersFlagEnabled
onPickEmoji={action('onPickEmoji')}
onChange={action('onChange')}
onTextTooLong={action('onTextTooLong')}
onSetSkinTone={action('onSetSkinTone')}
platform="darwin"
/>
),
},
},
} as Meta;
// eslint-disable-next-line react/function-component-definition
const Template: Story<Props> = args => (
<AddCaptionModal {...args} theme={React.useContext(StorybookThemeContext)} />
);
export const Modal = Template.bind({});
Modal.args = {
draftText: 'Some caption text',
};

View file

@ -1,95 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React 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';
import type { HydratedBodyRangesType } from '../types/BodyRange';
import { isScrolled, isScrolledToBottom } from '../hooks/useSizeObserver';
export type Props = {
i18n: LocalizerType;
onClose: () => void;
onSubmit: (
text: string,
bodyRanges: HydratedBodyRangesType | undefined
) => void;
draftText: string;
draftBodyRanges: HydratedBodyRangesType | undefined;
theme: ThemeType;
RenderCompositionTextArea: (
props: SmartCompositionTextAreaProps
) => JSX.Element;
};
export function AddCaptionModal({
i18n,
onClose,
onSubmit,
draftText,
draftBodyRanges,
RenderCompositionTextArea,
theme,
}: Props): JSX.Element {
const [messageText, setMessageText] = React.useState('');
const [bodyRanges, setBodyRanges] = React.useState<
HydratedBodyRangesType | undefined
>();
const [scrolled, setScrolled] = React.useState(false);
// We don't know that this is true, but it most likely is
const [scrolledToBottom, setScrolledToBottom] = React.useState(true);
const scrollerRef = React.useRef<HTMLDivElement>(null);
// add footer/header dividers depending on the state of scroll
const updateScrollState = React.useCallback(() => {
const scrollerEl = scrollerRef.current;
if (scrollerEl) {
setScrolled(isScrolled(scrollerEl));
setScrolledToBottom(isScrolledToBottom(scrollerEl));
}
}, []);
const handleSubmit = React.useCallback(() => {
onSubmit(messageText, bodyRanges);
}, [bodyRanges, messageText, onSubmit]);
return (
<Modal
i18n={i18n}
modalName="AddCaptionModal"
hasXButton
hasHeaderDivider={scrolled}
hasFooterDivider={!scrolledToBottom}
moduleClassName="AddCaptionModal"
padded={false}
title={i18n('icu:AddCaptionModal__title')}
onClose={onClose}
modalFooter={
<Button onClick={handleSubmit}>
{i18n('icu:AddCaptionModal__submit-button')}
</Button>
}
>
<RenderCompositionTextArea
maxLength={1500}
whenToShowRemainingCount={1450}
placeholder={i18n('icu:AddCaptionModal__placeholder')}
onChange={(updatedMessageText, updatedBodyRanges) => {
setMessageText(updatedMessageText);
setBodyRanges(updatedBodyRanges);
}}
scrollerRef={scrollerRef}
draftText={draftText}
bodyRanges={draftBodyRanges}
onSubmit={noop}
onScroll={updateScrollState}
theme={theme}
/>
</Modal>
);
}

View file

@ -34,6 +34,7 @@ export default {
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'),
conversationId: '123',
convertDraftBodyRangesIntoHydrated: () => undefined,
discardEditMessage: action('discardEditMessage'),
focusCounter: 0,
sendCounter: 0,

View file

@ -5,7 +5,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest';
import type { DraftBodyRanges } from '../types/BodyRange';
import type {
DraftBodyRanges,
HydratedBodyRangesType,
} from '../types/BodyRange';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
import { RecordingState } from '../types/AudioRecorder';
@ -85,6 +88,9 @@ export type OwnProps = Readonly<{
conversationId: string,
onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
) => unknown;
convertDraftBodyRangesIntoHydrated: (
bodyRanges: DraftBodyRanges | undefined
) => HydratedBodyRangesType | undefined;
conversationId: string;
discardEditMessage: (id: string) => unknown;
draftEditMessage?: DraftEditMessageType;
@ -221,6 +227,7 @@ export function CompositionArea({
// Base props
addAttachment,
conversationId,
convertDraftBodyRangesIntoHydrated,
discardEditMessage,
draftEditMessage,
focusCounter,
@ -853,12 +860,25 @@ export function CompositionArea({
'url' in attachmentToEdit &&
attachmentToEdit.url && (
<MediaEditor
draftBodyRanges={draftBodyRanges}
draftText={draftText}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
imageSrc={attachmentToEdit.url}
imageToBlurHash={imageToBlurHash}
installedPacks={installedPacks}
isFormattingEnabled={isFormattingEnabled}
isFormattingFlagEnabled={isFormattingFlagEnabled}
isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
isSending={false}
onClose={() => setAttachmentToEdit(undefined)}
onDone={({ data, contentType, blurHash }) => {
onDone={({
caption,
captionBodyRanges,
data,
contentType,
blurHash,
}) => {
const newAttachment = {
...attachmentToEdit,
contentType,
@ -869,9 +889,25 @@ export function CompositionArea({
addAttachment(conversationId, newAttachment);
setAttachmentToEdit(undefined);
onEditorStateChange?.({
bodyRanges: captionBodyRanges ?? [],
conversationId,
messageText: caption ?? '',
sendCounter,
});
inputApiRef.current?.setContents(
caption ?? '',
convertDraftBodyRangesIntoHydrated(captionBodyRanges),
true
);
}}
installedPacks={installedPacks}
onPickEmoji={onPickEmoji}
onTextTooLong={onTextTooLong}
platform={platform}
recentStickers={recentStickers}
skinTone={skinTone}
sortedGroupMembers={sortedGroupMembers}
/>
)}
<div className="CompositionArea__toggle-large">

View file

@ -26,7 +26,7 @@ import { BodyRange, collapseRangeTree, insertRange } from '../types/BodyRange';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { isAciString } from '../types/ServiceId';
import { isAciString } from '../util/isAciString';
import { MentionBlot } from '../quill/mentions/blot';
import {
matchEmojiImage,

View file

@ -1,79 +1,85 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './MediaEditor';
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);
export default {
title: 'Components/MediaEditor',
};
const IMAGE_1 = '/fixtures/nathan-anderson-316188-unsplash.jpg';
const IMAGE_2 = '/fixtures/tina-rolf-269345-unsplash.jpg';
const IMAGE_3 = '/fixtures/kitten-4-112-112.jpg';
const IMAGE_4 = '/fixtures/snow.jpg';
const getDefaultProps = (): PropsType => ({
i18n,
imageSrc: IMAGE_2,
onClose: action('onClose'),
onDone: action('onDone'),
isSending: false,
imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
export default {
title: 'Components/MediaEditor',
component: MediaEditor,
argTypes: {
getPreferredBadge: { action: true },
i18n: {
defaultValue: i18n,
},
imageToBlurHash: { action: true },
imageSrc: {
defaultValue: IMAGE_2,
},
installedPacks: {
defaultValue: installedPacks,
},
isFormattingEnabled: {
defaultValue: true,
},
isFormattingFlagEnabled: {
defaultValue: true,
},
isFormattingSpoilersFlagEnabled: {
defaultValue: true,
},
isSending: {
defaultValue: false,
},
onClose: { action: true },
onDone: { action: true },
onPickEmoji: { action: true },
onTextTooLong: { action: true },
platform: {
defaultValue: 'darwin',
},
recentStickers: {
defaultValue: [Stickers.wide, Stickers.tall, Stickers.abe],
},
skinTone: {
defaultValue: 0,
},
},
} as Meta;
// StickerButtonProps
installedPacks,
recentStickers: [Stickers.wide, Stickers.tall, Stickers.abe],
});
// eslint-disable-next-line react/function-component-definition
const Template: Story<PropsType> = args => <MediaEditor {...args} />;
export function ExtraLarge(): JSX.Element {
return <MediaEditor {...getDefaultProps()} />;
}
export const ExtraLarge = Template.bind({});
export function Large(): JSX.Element {
return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_1} />;
}
export const Large = Template.bind({});
Large.args = {
imageSrc: IMAGE_1,
};
export function Smol(): JSX.Element {
return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_3} />;
}
export const Smol = Template.bind({});
Smol.args = {
imageSrc: IMAGE_3,
};
export function Portrait(): JSX.Element {
return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_4} />;
}
export const Portrait = Template.bind({});
Portrait.args = {
imageSrc: IMAGE_4,
};
export function Sending(): JSX.Element {
return <MediaEditor {...getDefaultProps()} isSending />;
}
export function WithCaption(): JSX.Element {
return (
<MediaEditor
{...getDefaultProps()}
supportsCaption
renderCompositionTextArea={props => (
<CompositionTextArea
{...props}
getPreferredBadge={() => undefined}
i18n={i18n}
isFormattingEnabled
isFormattingFlagEnabled
isFormattingSpoilersFlagEnabled
onPickEmoji={action('onPickEmoji')}
onSetSkinTone={action('onSetSkinTone')}
onTextTooLong={action('onTextTooLong')}
platform="darwin"
/>
)}
/>
);
}
export const Sending = Template.bind({});
Sending.args = {
isSending: true,
};

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ import type { PropsType as SendStoryModalPropsType } from './SendStoryModal';
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type { imageToBlurHash } from '../util/imageToBlurHash';
import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator';
import type { PropsType as MediaEditorPropsType } from './MediaEditor';
import { TEXT_ATTACHMENT } from '../types/MIME';
import { isVideoAttachment } from '../types/Attachment';
@ -24,7 +25,6 @@ import { SendStoryModal } from './SendStoryModal';
import { MediaEditor } from './MediaEditor';
import { TextStoryCreator } from './TextStoryCreator';
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
import type { DraftBodyRanges } from '../types/BodyRange';
function usePortalElement(testid: string): HTMLDivElement | null {
@ -63,9 +63,6 @@ export type PropsType = {
processAttachment: (
file: File
) => Promise<void | InMemoryAttachmentDraftType>;
renderCompositionTextArea: (
props: SmartCompositionTextAreaProps
) => JSX.Element;
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
theme: ThemeType;
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
@ -96,6 +93,15 @@ export type PropsType = {
Pick<
TextStoryCreatorPropsType,
'onUseEmoji' | 'skinTone' | 'onSetSkinTone' | 'recentEmojis'
> &
Pick<
MediaEditorPropsType,
| 'isFormattingEnabled'
| 'isFormattingFlagEnabled'
| 'isFormattingSpoilersFlagEnabled'
| 'onPickEmoji'
| 'onTextTooLong'
| 'platform'
>;
export function StoryCreator({
@ -110,6 +116,9 @@ export function StoryCreator({
i18n,
imageToBlurHash,
installedPacks,
isFormattingEnabled,
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
isSending,
linkPreview,
me,
@ -118,19 +127,21 @@ export function StoryCreator({
onDeleteList,
onDistributionListCreated,
onHideMyStoriesFrom,
onMediaPlaybackStart,
onPickEmoji,
onRemoveMembers,
onRepliesNReactionsChanged,
onSelectedStoryList,
onSend,
onSetSkinTone,
onTextTooLong,
onUseEmoji,
onViewersUpdated,
onMediaPlaybackStart,
ourConversationId,
platform,
processAttachment,
recentEmojis,
recentStickers,
renderCompositionTextArea,
sendStoryModalOpenStateChanged,
setMyStoriesToAllSignalConnections,
signalConnections,
@ -234,17 +245,19 @@ export function StoryCreator({
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
)}
{draftAttachment && !isReadyToSend && attachmentUrl && (
{draftAttachment && attachmentUrl && (
<MediaEditor
doneButtonLabel={i18n('icu:next2')}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
imageSrc={attachmentUrl}
imageToBlurHash={imageToBlurHash}
installedPacks={installedPacks}
isFormattingEnabled={isFormattingEnabled}
isFormattingFlagEnabled={isFormattingFlagEnabled}
isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
isSending={isSending}
onClose={onClose}
supportsCaption
renderCompositionTextArea={renderCompositionTextArea}
imageToBlurHash={imageToBlurHash}
onDone={({
contentType,
data,
@ -263,7 +276,11 @@ export function StoryCreator({
setBodyRanges(captionBodyRanges);
setIsReadyToSend(true);
}}
onPickEmoji={onPickEmoji}
onTextTooLong={onTextTooLong}
platform={platform}
recentStickers={recentStickers}
skinTone={skinTone}
/>
)}
{!file && (

View file

@ -25,6 +25,7 @@ export type OwnProps = Readonly<{
emoji?: string;
i18n: LocalizerType;
onClose?: () => unknown;
onOpen?: () => unknown;
emojiButtonApi?: MutableRefObject<EmojiButtonAPI | undefined>;
variant?: EmojiButtonVariant;
}>;
@ -47,6 +48,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
i18n,
doSend,
onClose,
onOpen,
onPickEmoji,
skinTone,
onSetSkinTone,
@ -58,6 +60,13 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
const popperRef = React.useRef<HTMLDivElement | null>(null);
const refMerger = useRefMerger();
React.useEffect(() => {
if (!open) {
return;
}
onOpen?.();
}, [open, onOpen]);
const handleClickButton = React.useCallback(() => {
if (open) {
setOpen(false);