diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 9ab84711c73..943fab19ef3 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -60,6 +60,8 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({ quotedMessageProps: overrideProps.quotedMessageProps, onClickQuotedMessage: action('onClickQuotedMessage'), setQuotedMessage: action('setQuotedMessage'), + // MediaEditor + imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', // MediaQualitySelector onSelectMediaQuality: action('onSelectMediaQuality'), shouldSendHighQualityAttachments: Boolean( diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index ed4b0efec16..4b11a8bafb9 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -13,6 +13,7 @@ import type { import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder'; import { RecordingState } from '../state/ducks/audioRecorder'; import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing'; +import type { imageToBlurHash } from '../util/imageToBlurHash'; import { Spinner } from './Spinner'; import type { Props as EmojiButtonProps, @@ -56,7 +57,6 @@ import { useKeyboardShortcuts, } from '../hooks/useKeyboardShortcuts'; import { MediaEditor } from './MediaEditor'; -import { IMAGE_PNG } from '../types/MIME'; import { isImageTypeSupported } from '../util/GoogleChrome'; import * as KeyboardLayout from '../services/keyboardLayout'; @@ -98,6 +98,7 @@ export type OwnProps = Readonly<{ groupAdmins: Array<ConversationType>; groupVersion?: 1 | 2; i18n: LocalizerType; + imageToBlurHash: typeof imageToBlurHash; isFetchingUUID?: boolean; isGroupV1AndDisabled?: boolean; isMissingMandatoryProfileSharing?: boolean; @@ -174,6 +175,7 @@ export const CompositionArea = ({ conversationId, i18n, onSendMessage, + imageToBlurHash, processAttachments, removeAttachment, theme, @@ -594,12 +596,14 @@ export const CompositionArea = ({ <MediaEditor i18n={i18n} imageSrc={attachmentToEdit.url} + imageToBlurHash={imageToBlurHash} isSending={false} onClose={() => setAttachmentToEdit(undefined)} - onDone={data => { + onDone={({ data, contentType, blurHash }) => { const newAttachment = { ...attachmentToEdit, - contentType: IMAGE_PNG, + contentType, + blurHash, data, size: data.byteLength, }; diff --git a/ts/components/MediaEditor.stories.tsx b/ts/components/MediaEditor.stories.tsx index f155d4beedb..455bc0dc438 100644 --- a/ts/components/MediaEditor.stories.tsx +++ b/ts/components/MediaEditor.stories.tsx @@ -28,6 +28,7 @@ const getDefaultProps = (): PropsType => ({ onClose: action('onClose'), onDone: action('onDone'), isSending: false, + imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', // StickerButtonProps installedPacks, diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index edd65e2924f..f39fd6c87bb 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -10,6 +10,8 @@ import { get, has, noop } from 'lodash'; import type { LocalizerType } from '../types/Util'; import { ThemeType } from '../types/Util'; +import type { MIMEType } from '../types/MIME'; +import { IMAGE_PNG } from '../types/MIME'; import type { Props as StickerButtonProps } from './stickers/StickerButton'; import type { ImageStateType } from '../mediaEditor/ImageStateType'; @@ -20,6 +22,7 @@ import { Slider } from './Slider'; import { StickerButton } from './stickers/StickerButton'; import { Theme } from '../util/theme'; import { canvasToBytes } from '../util/canvasToBytes'; +import type { imageToBlurHash } from '../util/imageToBlurHash'; import { useFabricHistory } from '../mediaEditor/useFabricHistory'; import { usePortal } from '../hooks/usePortal'; import { useUniqueId } from '../hooks/useUniqueId'; @@ -41,13 +44,21 @@ import { AddNewLines } from './conversation/AddNewLines'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; import { Spinner } from './Spinner'; +export type MediaEditorResultType = Readonly<{ + data: Uint8Array; + contentType: MIMEType; + blurHash: string; + caption?: string; +}>; + export type PropsType = { doneButtonLabel?: string; i18n: LocalizerType; imageSrc: string; isSending: boolean; + imageToBlurHash: typeof imageToBlurHash; onClose: () => unknown; - onDone: (data: Uint8Array, caption?: string | undefined) => unknown; + onDone: (result: MediaEditorResultType) => unknown; } & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> & ( | { @@ -1115,6 +1126,7 @@ export const MediaEditor = ({ setIsSaving(true); let data: Uint8Array; + let blurHash: string; try { const renderFabricCanvas = await cloneFabricCanvas( fabricCanvas @@ -1151,6 +1163,12 @@ export const MediaEditor = ({ const renderedCanvas = renderFabricCanvas.toCanvasElement(); data = await canvasToBytes(renderedCanvas); + + const blob = new Blob([data], { + type: IMAGE_PNG, + }); + + blurHash = await props.imageToBlurHash(blob); } catch (err) { onTryClose(); throw err; @@ -1158,7 +1176,12 @@ export const MediaEditor = ({ setIsSaving(false); } - onDone(data, caption !== '' ? caption : undefined); + onDone({ + contentType: IMAGE_PNG, + data, + caption: caption !== '' ? caption : undefined, + blurHash, + }); }} theme={Theme.Dark} variant={ButtonVariant.Primary} diff --git a/ts/components/StoryCreator.stories.tsx b/ts/components/StoryCreator.stories.tsx index 53cf3953e0c..b89be2728cc 100644 --- a/ts/components/StoryCreator.stories.tsx +++ b/ts/components/StoryCreator.stories.tsx @@ -37,6 +37,7 @@ export default { defaultValue: false, }, i18n: { defaultValue: i18n }, + imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', installedPacks: { defaultValue: [], }, diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index c25fab59a3e..b90a9ce823b 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -14,8 +14,9 @@ import type { LocalizerType } from '../types/Util'; import type { Props as StickerButtonProps } from './stickers/StickerButton'; import type { PropsType as SendStoryModalPropsType } from './SendStoryModal'; import type { UUIDStringType } from '../types/UUID'; +import type { imageToBlurHash } from '../util/imageToBlurHash'; -import { IMAGE_JPEG, TEXT_ATTACHMENT } from '../types/MIME'; +import { TEXT_ATTACHMENT } from '../types/MIME'; import { isVideoAttachment } from '../types/Attachment'; import { SendStoryModal } from './SendStoryModal'; @@ -38,6 +39,7 @@ export type PropsType = { conversationIds: Array<string>, attachment: AttachmentType ) => unknown; + imageToBlurHash: typeof imageToBlurHash; processAttachment: ( file: File ) => Promise<void | InMemoryAttachmentDraftType>; @@ -80,6 +82,7 @@ export const StoryCreator = ({ groupStories, hasFirstStoryPostExperience, i18n, + imageToBlurHash, installedPacks, isSending, linkPreview, @@ -107,6 +110,7 @@ export const StoryCreator = ({ const [draftAttachment, setDraftAttachment] = useState< AttachmentType | undefined >(); + const [isReadyToSend, setIsReadyToSend] = useState(false); const [attachmentUrl, setAttachmentUrl] = useState<string | undefined>(); useEffect(() => { @@ -123,11 +127,16 @@ export const StoryCreator = ({ return; } + setDraftAttachment(attachment); if (isVideoAttachment(attachment)) { - setDraftAttachment(attachment); + setAttachmentUrl(undefined); + setIsReadyToSend(true); } else if (attachment && has(attachment, 'data')) { url = URL.createObjectURL(new Blob([get(attachment, 'data')])); setAttachmentUrl(url); + + // Needs editing in MediaEditor + setIsReadyToSend(false); } } @@ -142,12 +151,17 @@ export const StoryCreator = ({ }, [file, processAttachment]); useEffect(() => { - sendStoryModalOpenStateChanged(Boolean(draftAttachment)); + if (draftAttachment === undefined) { + sendStoryModalOpenStateChanged(false); + setIsReadyToSend(false); + } else { + sendStoryModalOpenStateChanged(true); + } }, [draftAttachment, sendStoryModalOpenStateChanged]); return ( <> - {draftAttachment && ( + {draftAttachment && isReadyToSend && ( <SendStoryModal draftAttachment={draftAttachment} candidateConversations={candidateConversations} @@ -182,7 +196,7 @@ export const StoryCreator = ({ toggleSignalConnectionsModal={toggleSignalConnectionsModal} /> )} - {attachmentUrl && ( + {draftAttachment && !isReadyToSend && attachmentUrl && ( <MediaEditor doneButtonLabel={i18n('next2')} i18n={i18n} @@ -192,13 +206,17 @@ export const StoryCreator = ({ onClose={onClose} supportsCaption renderCompositionTextArea={renderCompositionTextArea} - onDone={(data, caption) => { + imageToBlurHash={imageToBlurHash} + onDone={({ contentType, data, blurHash, caption }) => { setDraftAttachment({ - contentType: IMAGE_JPEG, + ...draftAttachment, + contentType, data, size: data.byteLength, + blurHash, caption, }); + setIsReadyToSend(true); }} recentStickers={recentStickers} /> @@ -216,6 +234,7 @@ export const StoryCreator = ({ textAttachment, size: textAttachment.text?.length || 0, }); + setIsReadyToSend(true); }} /> )} diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index bb3ca43b597..abf0c41afa1 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -9,6 +9,7 @@ import { CompositionArea } from '../../components/CompositionArea'; import type { StateType } from '../reducer'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { dropNull } from '../../util/dropNull'; +import { imageToBlurHash } from '../../util/imageToBlurHash'; import { getPreferredBadgeSelector } from '../selectors/badges'; import { selectRecentEmojis } from '../selectors/emojis'; @@ -89,6 +90,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { recordingState: state.audioRecorder.recordingState, // AttachmentsList draftAttachments, + // MediaEditor + imageToBlurHash, // MediaQualitySelector shouldSendHighQualityAttachments, // StagedLinkPreview diff --git a/ts/state/smart/StoryCreator.tsx b/ts/state/smart/StoryCreator.tsx index 4dfd4d5bf62..34bced922a1 100644 --- a/ts/state/smart/StoryCreator.tsx +++ b/ts/state/smart/StoryCreator.tsx @@ -26,6 +26,7 @@ import { getHasSetMyStoriesPrivacy } from '../selectors/items'; import { getLinkPreview } from '../selectors/linkPreviews'; import { getPreferredBadgeSelector } from '../selectors/badges'; import { processAttachment } from '../../util/processAttachment'; +import { imageToBlurHash } from '../../util/imageToBlurHash'; import { useConversationsActions } from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useLinkPreviewActions } from '../ducks/linkPreviews'; @@ -91,6 +92,7 @@ export function SmartStoryCreator(): JSX.Element | null { groupStories={groupStories} hasFirstStoryPostExperience={!hasSetMyStoriesPrivacy} i18n={i18n} + imageToBlurHash={imageToBlurHash} installedPacks={installedPacks} isSending={isSending} linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}