diff --git a/stylesheets/components/MediaEditor.scss b/stylesheets/components/MediaEditor.scss index 13b843f5c351..f9ed8e2db9cd 100644 --- a/stylesheets/components/MediaEditor.scss +++ b/stylesheets/components/MediaEditor.scss @@ -2,8 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only .MediaEditor { - $tools-height: 44px; - background: $color-gray-95; display: flex; flex-direction: column; @@ -20,7 +18,7 @@ &__container { display: flex; flex: 1; - padding-block: 22px; + padding-block: 48px; padding-inline: 60px; padding-bottom: 0; overflow: hidden; @@ -47,12 +45,12 @@ &__control { @include button-reset; align-items: center; - border-radius: 32px; + border-radius: 20px; display: inline-flex; height: 32px; justify-content: center; margin-block: 0; - margin-inline: 18px; + margin-inline: 20px; opacity: 1; width: 32px; @@ -119,7 +117,7 @@ } } - &__toolbar { + &__tools { align-items: center; display: flex; flex-direction: column; @@ -127,6 +125,11 @@ padding: 22px; width: 100%; + &--input { + margin-inline: 24px; + min-width: 410px; + } + &--buttons { align-items: center; display: flex; @@ -134,18 +137,12 @@ width: 100%; } - &--space { - height: $tools-height; - margin-bottom: 22px; - } - &__caption { - height: $tools-height; - margin-bottom: 22px; + height: 44px; &__add-caption-button { @include button-reset; - border-radius: 9999px; + @include rounded-corners; background: $color-gray-90; color: $color-gray-15; padding-block: 8px; @@ -162,25 +159,36 @@ } } - &__controls { + &__tools-row-1 { display: flex; flex-grow: 1; flex-wrap: wrap; + height: 20px; + justify-content: center; + margin-bottom: 24px; + max-width: 596px; + } + + &__tools-row-2 { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + height: 36px; justify-content: center; max-width: 596px; } - &__tools { + &__toolbar { align-items: center; background-color: $color-gray-90; border-radius: 10px; color: $color-white; display: flex; - height: $tools-height; + height: 36px; justify-content: center; - margin-bottom: 22px; padding-block: 14px; padding-inline: 12px; + margin-inline: 16px; &__tool, &__tool__button { @@ -206,13 +214,6 @@ margin-inline: 8px; padding: 8px; - &--words { - height: auto; - width: auto; - padding-block: 0; - padding-inline: 6px; - } - &--draw-pen__button { @include icon('v3/brush/brush-pen-compact.svg'); } @@ -319,4 +320,55 @@ ); } } + + &__history-buttons { + inset-inline-start: 24px; + position: absolute; + top: 24px; + } + + &__close { + @include button-reset; + + border-radius: 4px; + height: 20px; + position: absolute; + inset-inline-end: 24px; + top: 24px; + width: 20px; + + &::before { + content: ''; + display: block; + width: 100%; + height: 100%; + + @include light-theme { + @include color-svg('../images/icons/v3/x/x.svg', $color-gray-75); + } + + @include dark-theme { + @include color-svg('../images/icons/v3/x/x.svg', $color-gray-15); + } + } + + &:hover, + &:focus { + box-shadow: 0 0 0 2px $color-ultramarine; + } + } + + &__crop-preset { + @include button-reset; + color: $color-white; + height: 28px; + margin-inline: 12px; + padding-block: 5px; + padding-inline: 12px; + + &--selected { + @include rounded-corners; + background: $color-gray-80; + } + } } diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 1c44e8e040d3..276ec4f4b2a2 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -25,10 +25,10 @@ import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; import type { ServiceIdString, AciString, PniString } from './types/ServiceId'; import { isServiceIdString, - normalizeAci, normalizePni, normalizeServiceId, } from './types/ServiceId'; +import { normalizeAci } from './util/normalizeAci'; import { sleep } from './util/sleep'; import { isNotNil } from './util/isNotNil'; import { MINUTE, SECOND } from './util/durations'; diff --git a/ts/background.ts b/ts/background.ts index ddbc0bafc299..b3c64e51cf8c 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -142,12 +142,9 @@ import { themeChanged } from './shims/themeChanged'; import { createIPCEvents } from './util/createIPCEvents'; import { RemoveAllConfiguration } from './types/RemoveAllConfiguration'; import type { ServiceIdString } from './types/ServiceId'; -import { - ServiceIdKind, - isAciString, - isServiceIdString, - normalizeAci, -} from './types/ServiceId'; +import { ServiceIdKind, isServiceIdString } from './types/ServiceId'; +import { isAciString } from './util/isAciString'; +import { normalizeAci } from './util/normalizeAci'; import * as log from './logging/log'; import { loadRecentEmojis } from './util/loadRecentEmojis'; import { deleteAllLogs } from './util/deleteAllLogs'; diff --git a/ts/components/AddCaptionModal.stories.tsx b/ts/components/AddCaptionModal.stories.tsx deleted file mode 100644 index a98a7686fea3..000000000000 --- a/ts/components/AddCaptionModal.stories.tsx +++ /dev/null @@ -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) => ( - 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 = args => ( - -); - -export const Modal = Template.bind({}); -Modal.args = { - draftText: 'Some caption text', -}; diff --git a/ts/components/AddCaptionModal.tsx b/ts/components/AddCaptionModal.tsx deleted file mode 100644 index 8eed5bc4b133..000000000000 --- a/ts/components/AddCaptionModal.tsx +++ /dev/null @@ -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(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 ( - - {i18n('icu:AddCaptionModal__submit-button')} - - } - > - { - setMessageText(updatedMessageText); - setBodyRanges(updatedBodyRanges); - }} - scrollerRef={scrollerRef} - draftText={draftText} - bodyRanges={draftBodyRanges} - onSubmit={noop} - onScroll={updateScrollState} - theme={theme} - /> - - ); -} diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 97d689345133..58d414ce2850 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -34,6 +34,7 @@ export default { const useProps = (overrideProps: Partial = {}): Props => ({ addAttachment: action('addAttachment'), conversationId: '123', + convertDraftBodyRangesIntoHydrated: () => undefined, discardEditMessage: action('discardEditMessage'), focusCounter: 0, sendCounter: 0, diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 1a7661fd369e..4737a488682a 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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 && ( 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} /> )}
diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index d41eb93b2d2b..181661df7db2 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -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, diff --git a/ts/components/MediaEditor.stories.tsx b/ts/components/MediaEditor.stories.tsx index 0e8adf1778fb..53ec34c3f030 100644 --- a/ts/components/MediaEditor.stories.tsx +++ b/ts/components/MediaEditor.stories.tsx @@ -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 = args => ; -export function ExtraLarge(): JSX.Element { - return ; -} +export const ExtraLarge = Template.bind({}); -export function Large(): JSX.Element { - return ; -} +export const Large = Template.bind({}); +Large.args = { + imageSrc: IMAGE_1, +}; -export function Smol(): JSX.Element { - return ; -} +export const Smol = Template.bind({}); +Smol.args = { + imageSrc: IMAGE_3, +}; -export function Portrait(): JSX.Element { - return ; -} +export const Portrait = Template.bind({}); +Portrait.args = { + imageSrc: IMAGE_4, +}; -export function Sending(): JSX.Element { - return ; -} - -export function WithCaption(): JSX.Element { - return ( - ( - 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, +}; diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index a97495493d41..209446368434 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -1,30 +1,26 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import { createPortal } from 'react-dom'; import { fabric } from 'fabric'; 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 { + EmojiPickDataType, + Props as EmojiPickerProps, +} from './emoji/EmojiPicker'; +import type { DraftBodyRanges } from '../types/BodyRange'; import type { ImageStateType } from '../mediaEditor/ImageStateType'; - -import * as log from '../logging/log'; -import { Button, ButtonVariant } from './Button'; -import { ContextMenu } from './ContextMenu'; -import { Slider } from './Slider'; -import { StickerButton } from './stickers/StickerButton'; -import { Theme } from '../util/theme'; -import { canvasToBytes } from '../util/canvasToBytes'; +import type { + InputApi, + Props as CompositionInputProps, +} from './CompositionInput'; +import type { LocalizerType } from '../types/Util'; +import type { MIMEType } from '../types/MIME'; +import type { Props as StickerButtonProps } from './stickers/StickerButton'; import type { imageToBlurHash } from '../util/imageToBlurHash'; -import { useFabricHistory } from '../mediaEditor/useFabricHistory'; -import { usePortal } from '../hooks/usePortal'; -import { useUniqueId } from '../hooks/useUniqueId'; import { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker'; import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect'; @@ -35,25 +31,35 @@ import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticke import { fabricEffectListener } from '../mediaEditor/fabricEffectListener'; import { getRGBA, getHSL } from '../mediaEditor/util/color'; import { - TextStyle, getTextStyleAttributes, + TextStyle, } from '../mediaEditor/util/getTextStyleAttributes'; -import { AddCaptionModal } from './AddCaptionModal'; -import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; -import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; -import { Spinner } from './Spinner'; -import type { HydratedBodyRangesType } from '../types/BodyRange'; -import { MessageBody } from './conversation/MessageBody'; -import { RenderLocation } from './conversation/MessageTextRenderer'; -import { arrow } from '../util/keyboard'; + +import * as log from '../logging/log'; +import { Button, ButtonVariant } from './Button'; +import { CompositionInput } from './CompositionInput'; +import { ContextMenu } from './ContextMenu'; +import { EmojiButton } from './emoji/EmojiButton'; +import { IMAGE_PNG } from '../types/MIME'; import { SizeObserver } from '../hooks/useSizeObserver'; +import { Slider } from './Slider'; +import { Spinner } from './Spinner'; +import { StickerButton } from './stickers/StickerButton'; +import { Theme } from '../util/theme'; +import { ThemeType } from '../types/Util'; +import { arrow } from '../util/keyboard'; +import { canvasToBytes } from '../util/canvasToBytes'; +import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; +import { useFabricHistory } from '../mediaEditor/useFabricHistory'; +import { usePortal } from '../hooks/usePortal'; +import { useUniqueId } from '../hooks/useUniqueId'; export type MediaEditorResultType = Readonly<{ data: Uint8Array; contentType: MIMEType; blurHash: string; caption?: string; - captionBodyRanges?: HydratedBodyRangesType; + captionBodyRanges?: DraftBodyRanges; }>; export type PropsType = { @@ -65,18 +71,20 @@ export type PropsType = { onClose: () => unknown; onDone: (result: MediaEditorResultType) => unknown; } & Pick & - ( - | { - supportsCaption: true; - renderCompositionTextArea: ( - props: SmartCompositionTextAreaProps - ) => JSX.Element; - } - | { - supportsCaption?: false; - renderCompositionTextArea?: undefined; - } - ); + Pick< + CompositionInputProps, + | 'draftText' + | 'draftBodyRanges' + | 'getPreferredBadge' + | 'isFormattingEnabled' + | 'isFormattingFlagEnabled' + | 'isFormattingSpoilersFlagEnabled' + | 'onPickEmoji' + | 'onTextTooLong' + | 'platform' + | 'sortedGroupMembers' + > & + EmojiPickerProps; const INITIAL_IMAGE_STATE: ImageStateType = { angle: 0, @@ -106,6 +114,12 @@ enum DrawTool { Highlighter = 'Highlighter', } +enum CropPreset { + Freeform = 'Freeform', + Square = 'Square', + Vertical = 'Vertical', +} + type PendingCropType = { left: number; top: number; @@ -128,6 +142,23 @@ export function MediaEditor({ onClose, onDone, + // CompositionInput + draftText, + draftBodyRanges, + getPreferredBadge, + isFormattingEnabled, + isFormattingFlagEnabled, + isFormattingSpoilersFlagEnabled, + onPickEmoji, + onTextTooLong, + platform, + sortedGroupMembers, + + // EmojiPickerProps + onSetSkinTone, + recentEmojis, + skinTone, + // StickerButtonProps installedPacks, recentStickers, @@ -137,19 +168,39 @@ export function MediaEditor({ const [image, setImage] = useState(new Image()); const [isStickerPopperOpen, setIsStickerPopperOpen] = useState(false); + const [isEmojiPopperOpen, setEmojiPopperOpen] = useState(false); - const [caption, setCaption] = useState(''); + const [caption, setCaption] = useState(draftText ?? ''); const [captionBodyRanges, setCaptionBodyRanges] = useState< - HydratedBodyRangesType | undefined - >(); + DraftBodyRanges | undefined + >(draftBodyRanges); - const [showAddCaptionModal, setShowAddCaptionModal] = useState(false); + const inputApiRef = useRef(); + + const closeEmojiPickerAndFocusComposer = useCallback(() => { + if (inputApiRef.current) { + inputApiRef.current.focus(); + } + setEmojiPopperOpen(false); + }, [inputApiRef]); + + const insertEmoji = useCallback( + (e: EmojiPickDataType) => { + if (inputApiRef.current) { + inputApiRef.current.insertEmoji(e); + onPickEmoji(e); + } + }, + [inputApiRef, onPickEmoji] + ); const canvasId = useUniqueId(); const [imageState, setImageState] = useState(INITIAL_IMAGE_STATE); + const [cropPreset, setCropPreset] = useState(CropPreset.Freeform); + // History state const { canRedo, canUndo, redoIfPossible, takeSnapshot, undoIfPossible } = useFabricHistory({ @@ -199,8 +250,8 @@ export function MediaEditor({ const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); const onTryClose = useCallback(() => { - confirmDiscardIf(caption !== '' || Boolean(image), onClose); - }, [confirmDiscardIf, caption, image, onClose]); + confirmDiscardIf(canUndo, onClose); + }, [confirmDiscardIf, canUndo, onClose]); // Keyboard support useEffect(() => { @@ -228,6 +279,12 @@ export function MediaEditor({ [ ev => ev.key === 'Escape', () => { + // if the emoji popper is open, + // it will use the escape key to close itself + if (isEmojiPopperOpen) { + return; + } + // close window if the user is not in the middle of something if (editMode === undefined) { // if the stickers popper is open, @@ -377,6 +434,7 @@ export function MediaEditor({ }, [ fabricCanvas, editMode, + isEmojiPopperOpen, isStickerPopperOpen, onTryClose, redoIfPossible, @@ -523,6 +581,40 @@ export function MediaEditor({ } }, [fabricCanvas, sliderValue, textStyle]); + useEffect(() => { + if (!fabricCanvas) { + return; + } + + const rect = fabricCanvas.getObjects().find(obj => { + return obj instanceof MediaEditorFabricCropRect; + }); + + if (!rect) { + return; + } + + const PADDING = MediaEditorFabricCropRect.PADDING / zoom; + let height = + imageState.height - PADDING * Math.max(440 / imageState.height, 2); + let width = + imageState.width - PADDING * Math.max(440 / imageState.width, 2); + + if (cropPreset === CropPreset.Square) { + const size = Math.min(height, width); + height = size; + width = size; + } else if (cropPreset === CropPreset.Vertical) { + width = height * 0.5625; + } + + rect.set({ height, width, scaleX: 1, scaleY: 1 }); + fabricCanvas.viewportCenterObject(rect); + rect.setCoords(); + + setCanCrop(true); + }, [cropPreset, fabricCanvas, imageState.height, imageState.width, zoom]); + // Create the CroppingRect useEffect(() => { if (!fabricCanvas) { @@ -632,148 +724,159 @@ export function MediaEditor({ return null; } - let tooling: JSX.Element | undefined; + let toolElement: JSX.Element | undefined; if (editMode === EditMode.Text) { - tooling = ( + toolElement = ( <> - - setTextStyle(TextStyle.Regular), - value: TextStyle.Regular, - }, - { - icon: 'MediaEditor__icon--text-highlight', - label: i18n('icu:MediaEditor__text--highlight'), - onClick: () => setTextStyle(TextStyle.Highlight), - value: TextStyle.Highlight, - }, - { - icon: 'MediaEditor__icon--text-outline', - label: i18n('icu:MediaEditor__text--outline'), - onClick: () => setTextStyle(TextStyle.Outline), - value: TextStyle.Outline, - }, - ]} - moduleClassName={classNames('MediaEditor__tools__tool', { - 'MediaEditor__tools__button--text-regular': - textStyle === TextStyle.Regular, - 'MediaEditor__tools__button--text-highlight': - textStyle === TextStyle.Highlight, - 'MediaEditor__tools__button--text-outline': - textStyle === TextStyle.Outline, - })} - theme={Theme.Dark} - value={textStyle} - /> - + const activeObject = fabricCanvas?.getActiveObject(); + if (activeObject instanceof MediaEditorFabricIText) { + activeObject.exitEditing(); + } + }} + theme={Theme.Dark} + variant={ButtonVariant.Secondary} + > + {i18n('icu:done')} + +
); } else if (editMode === EditMode.Draw) { - tooling = ( + toolElement = ( <> - - setDrawTool(DrawTool.Pen), - value: DrawTool.Pen, - }, - { - icon: 'MediaEditor__icon--draw-highlighter', - label: i18n('icu:MediaEditor__draw--highlighter'), - onClick: () => setDrawTool(DrawTool.Highlighter), - value: DrawTool.Highlighter, - }, - ]} - moduleClassName={classNames('MediaEditor__tools__tool', { - 'MediaEditor__tools__button--draw-pen': drawTool === DrawTool.Pen, - 'MediaEditor__tools__button--draw-highlighter': - drawTool === DrawTool.Highlighter, - })} - theme={Theme.Dark} - value={drawTool} - /> - setDrawWidth(DrawWidth.Thin), - value: DrawWidth.Thin, - }, - { - icon: 'MediaEditor__icon--width-regular', - label: i18n('icu:MediaEditor__draw--regular'), - onClick: () => setDrawWidth(DrawWidth.Regular), - value: DrawWidth.Regular, - }, - { - icon: 'MediaEditor__icon--width-medium', - label: i18n('icu:MediaEditor__draw--medium'), - onClick: () => setDrawWidth(DrawWidth.Medium), - value: DrawWidth.Medium, - }, - { - icon: 'MediaEditor__icon--width-heavy', - label: i18n('icu:MediaEditor__draw--heavy'), - onClick: () => setDrawWidth(DrawWidth.Heavy), - value: DrawWidth.Heavy, - }, - ]} - moduleClassName={classNames('MediaEditor__tools__tool', { - 'MediaEditor__tools__button--width-thin': - drawWidth === DrawWidth.Thin, - 'MediaEditor__tools__button--width-regular': - drawWidth === DrawWidth.Regular, - 'MediaEditor__tools__button--width-medium': - drawWidth === DrawWidth.Medium, - 'MediaEditor__tools__button--width-heavy': - drawWidth === DrawWidth.Heavy, - })} - theme={Theme.Dark} - value={drawWidth} - /> - +
+
+
+ + setDrawTool(DrawTool.Pen), + value: DrawTool.Pen, + }, + { + icon: 'MediaEditor__icon--draw-highlighter', + label: i18n('icu:MediaEditor__draw--highlighter'), + onClick: () => setDrawTool(DrawTool.Highlighter), + value: DrawTool.Highlighter, + }, + ]} + moduleClassName={classNames('MediaEditor__toolbar__tool', { + 'MediaEditor__toolbar__button--draw-pen': + drawTool === DrawTool.Pen, + 'MediaEditor__toolbar__button--draw-highlighter': + drawTool === DrawTool.Highlighter, + })} + theme={Theme.Dark} + value={drawTool} + /> + setDrawWidth(DrawWidth.Thin), + value: DrawWidth.Thin, + }, + { + icon: 'MediaEditor__icon--width-regular', + label: i18n('icu:MediaEditor__draw--regular'), + onClick: () => setDrawWidth(DrawWidth.Regular), + value: DrawWidth.Regular, + }, + { + icon: 'MediaEditor__icon--width-medium', + label: i18n('icu:MediaEditor__draw--medium'), + onClick: () => setDrawWidth(DrawWidth.Medium), + value: DrawWidth.Medium, + }, + { + icon: 'MediaEditor__icon--width-heavy', + label: i18n('icu:MediaEditor__draw--heavy'), + onClick: () => setDrawWidth(DrawWidth.Heavy), + value: DrawWidth.Heavy, + }, + ]} + moduleClassName={classNames('MediaEditor__toolbar__tool', { + 'MediaEditor__toolbar__button--width-thin': + drawWidth === DrawWidth.Thin, + 'MediaEditor__toolbar__button--width-regular': + drawWidth === DrawWidth.Regular, + 'MediaEditor__toolbar__button--width-medium': + drawWidth === DrawWidth.Medium, + 'MediaEditor__toolbar__button--width-heavy': + drawWidth === DrawWidth.Heavy, + })} + theme={Theme.Dark} + value={drawWidth} + /> +
+ +
); } else if (editMode === EditMode.Crop) { @@ -784,132 +887,215 @@ export function MediaEditor({ imageState.flipY || imageState.angle !== 0; - tooling = ( + toolElement = ( <> - - + + +
+
+ +
+
+ + setImageState(newImageState); + moveFabricObjectsForCrop(fabricCanvas, pendingCrop); + takeSnapshot('crop', newImageState); + setEditMode(undefined); + setCropPreset(CropPreset.Freeform); + }} + theme={Theme.Dark} + variant={ButtonVariant.Secondary} + > + {i18n('icu:done')} + +
); } return createPortal(
+
+
+ - - {showAddCaptionModal && ( - { - setCaption(messageText.trim()); - setCaptionBodyRanges(bodyRanges); - setShowAddCaptionModal(false); - }} - onClose={() => setShowAddCaptionModal(false)} - RenderCompositionTextArea={props.renderCompositionTextArea} - theme={ThemeType.dark} - /> - )} -
- ) : ( -
- )} - - )} -
- -
-
+
+
+ { + setCaptionBodyRanges(bodyRanges); + setCaption(messageText); + }} + onPickEmoji={onPickEmoji} + onSubmit={() => { + inputApiRef.current?.reset(); + }} + onTextTooLong={onTextTooLong} + placeholder="Message" + platform={platform} + sendCounter={0} + sortedGroupMembers={sortedGroupMembers} + theme={ThemeType.dark} + > + setEmojiPopperOpen(true)} + onClose={closeEmojiPickerAndFocusComposer} + recentEmojis={recentEmojis} + skinTone={skinTone} + onSetSkinTone={onSetSkinTone} + /> + +
+
- -
+ }} + theme={Theme.Dark} + variant={ButtonVariant.Primary} + > + {isSending ? ( + + ) : ( + doneButtonLabel || i18n('icu:save') + )} + +
+ + )} {confirmDiscardModal} , diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index 238ca8f3637b..98410a58a807 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -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; - renderCompositionTextArea: ( - props: SmartCompositionTextAreaProps - ) => JSX.Element; sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown; theme: ThemeType; } & Pick & @@ -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 && ( )} {!file && ( diff --git a/ts/components/emoji/EmojiButton.tsx b/ts/components/emoji/EmojiButton.tsx index 9563aca17acb..f6a71a294f8e 100644 --- a/ts/components/emoji/EmojiButton.tsx +++ b/ts/components/emoji/EmojiButton.tsx @@ -25,6 +25,7 @@ export type OwnProps = Readonly<{ emoji?: string; i18n: LocalizerType; onClose?: () => unknown; + onOpen?: () => unknown; emojiButtonApi?: MutableRefObject; 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(null); const refMerger = useRefMerger(); + React.useEffect(() => { + if (!open) { + return; + } + onOpen?.(); + }, [open, onOpen]); + const handleClickButton = React.useCallback(() => { if (open) { setOpen(false); diff --git a/ts/groups.ts b/ts/groups.ts index c14f0b52d431..24dc55b585d5 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -77,10 +77,10 @@ import type { AvatarDataType } from './types/Avatar'; import type { ServiceIdString, AciString, PniString } from './types/ServiceId'; import { ServiceIdKind, - isAciString, isPniString, isServiceIdString, } from './types/ServiceId'; +import { isAciString } from './util/isAciString'; import * as Errors from './types/errors'; import { SignalService as Proto } from './protobuf'; import { isNotNil } from './util/isNotNil'; diff --git a/ts/jobs/helpers/addReportSpamJob.ts b/ts/jobs/helpers/addReportSpamJob.ts index e52ca94e1597..8dfd8de19fbb 100644 --- a/ts/jobs/helpers/addReportSpamJob.ts +++ b/ts/jobs/helpers/addReportSpamJob.ts @@ -5,7 +5,7 @@ import { assertDev } from '../../util/assert'; import { isDirectConversation } from '../../util/whatTypeOfConversation'; import * as log from '../../logging/log'; import type { ConversationAttributesType } from '../../model-types.d'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import type { reportSpamJobQueue } from '../reportSpamJobQueue'; export async function addReportSpamJob({ diff --git a/ts/jobs/helpers/sendNormalMessage.ts b/ts/jobs/helpers/sendNormalMessage.ts index 29191eb77cc6..f455e93cbb38 100644 --- a/ts/jobs/helpers/sendNormalMessage.ts +++ b/ts/jobs/helpers/sendNormalMessage.ts @@ -51,7 +51,7 @@ import { isConversationAccepted } from '../../util/isConversationAccepted'; import { sendToGroup } from '../../util/sendToGroup'; import type { DurationInSeconds } from '../../util/durations'; import type { ServiceIdString } from '../../types/ServiceId'; -import { normalizeAci } from '../../types/ServiceId'; +import { normalizeAci } from '../../util/normalizeAci'; import * as Bytes from '../../Bytes'; const LONG_ATTACHMENT_LIMIT = 2048; diff --git a/ts/jobs/helpers/sendReaction.ts b/ts/jobs/helpers/sendReaction.ts index 0d5adb6a80cf..3bc8ec257b66 100644 --- a/ts/jobs/helpers/sendReaction.ts +++ b/ts/jobs/helpers/sendReaction.ts @@ -28,7 +28,7 @@ import { ourProfileKeyService } from '../../services/ourProfileKey'; import { canReact, isStory } from '../../state/selectors/message'; import { findAndFormatContact } from '../../util/findAndFormatContact'; import type { AciString, ServiceIdString } from '../../types/ServiceId'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import { handleMultipleSendErrors } from './handleMultipleSendErrors'; import { incrementMessageCounter } from '../../util/incrementMessageCounter'; diff --git a/ts/jobs/helpers/syncHelpers.ts b/ts/jobs/helpers/syncHelpers.ts index 71b7a485afdd..d2ad61ec7131 100644 --- a/ts/jobs/helpers/syncHelpers.ts +++ b/ts/jobs/helpers/syncHelpers.ts @@ -4,7 +4,7 @@ import { chunk } from 'lodash'; import type { LoggerType } from '../../types/Logging'; import type { AciString } from '../../types/ServiceId'; -import { normalizeAci } from '../../types/ServiceId'; +import { normalizeAci } from '../../util/normalizeAci'; import { getSendOptions } from '../../util/getSendOptions'; import type { SendTypesType } from '../../util/handleMessageSend'; import { handleMessageSend } from '../../util/handleMessageSend'; diff --git a/ts/mediaEditor/MediaEditorFabricCropRect.ts b/ts/mediaEditor/MediaEditorFabricCropRect.ts index bb180cc860af..5ab255e049c9 100644 --- a/ts/mediaEditor/MediaEditorFabricCropRect.ts +++ b/ts/mediaEditor/MediaEditorFabricCropRect.ts @@ -29,26 +29,19 @@ export class MediaEditorFabricCropRect extends fabric.Rect { const canvasHeight = this.canvas.getHeight(); const canvasWidth = this.canvas.getWidth(); - if (height > canvasHeight || width > canvasWidth) { - this.canvas.discardActiveObject(); - } else { - this.set( - 'left', - clamp( - left / zoom, - MediaEditorFabricCropRect.PADDING / zoom, - (canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom - ) - ); - this.set( - 'top', - clamp( - top / zoom, - MediaEditorFabricCropRect.PADDING / zoom, - (canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom - ) - ); - } + const nextLeft = clamp( + left / zoom, + MediaEditorFabricCropRect.PADDING / zoom, + (canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom + ); + const nextTop = clamp( + top / zoom, + MediaEditorFabricCropRect.PADDING / zoom, + (canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom + ); + + this.set('left', nextLeft); + this.set('top', nextTop); this.setCoords(); } diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 7c965390b7b4..443015dbeb21 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -67,10 +67,10 @@ import { IMAGE_JPEG, IMAGE_WEBP } from '../types/MIME'; import type { AciString, PniString, ServiceIdString } from '../types/ServiceId'; import { ServiceIdKind, - isAciString, normalizeServiceId, normalizePni, } from '../types/ServiceId'; +import { isAciString } from '../util/isAciString'; import { constantTimeEqual, decryptProfile, diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 89812fb0937f..7f1d1ac69c8c 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -45,7 +45,8 @@ import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp'; import type { ReactionType } from '../types/Reactions'; import type { ServiceIdString } from '../types/ServiceId'; -import { isAciString, normalizeServiceId } from '../types/ServiceId'; +import { normalizeServiceId } from '../types/ServiceId'; +import { isAciString } from '../util/isAciString'; import * as reactionUtil from '../reactions/util'; import * as Stickers from '../types/Stickers'; import * as Errors from '../types/errors'; diff --git a/ts/quill/memberRepository.ts b/ts/quill/memberRepository.ts index 29ad861074c7..e7d0ab4f0e92 100644 --- a/ts/quill/memberRepository.ts +++ b/ts/quill/memberRepository.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import type { ConversationType } from '../state/ducks/conversations'; import type { AciString } from '../types/ServiceId'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from '../util/isAciString'; import { filter, map } from '../util/iterables'; import { removeDiacritics } from '../util/removeDiacritics'; import { isNotNil } from '../util/isNotNil'; diff --git a/ts/quill/mentions/blot.tsx b/ts/quill/mentions/blot.tsx index b320690f7b44..4d39f30acbb4 100644 --- a/ts/quill/mentions/blot.tsx +++ b/ts/quill/mentions/blot.tsx @@ -8,7 +8,7 @@ import Parchment from 'parchment'; import Quill from 'quill'; import { render } from 'react-dom'; import { Emojify } from '../../components/conversation/Emojify'; -import { normalizeAci } from '../../types/ServiceId'; +import { normalizeAci } from '../../util/normalizeAci'; import type { MentionBlotValue } from '../util'; declare class QuillEmbed extends Parchment.Embed { diff --git a/ts/quill/mentions/matchers.ts b/ts/quill/mentions/matchers.ts index 853a76f4a168..8973a1b46aba 100644 --- a/ts/quill/mentions/matchers.ts +++ b/ts/quill/mentions/matchers.ts @@ -6,7 +6,7 @@ import type { RefObject } from 'react'; import type { Matcher, AttributeMap } from 'quill'; import { assertDev } from '../../util/assert'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import type { MemberRepository } from '../memberRepository'; export const matchMention: ( diff --git a/ts/reactions/enqueueReactionForSend.ts b/ts/reactions/enqueueReactionForSend.ts index c4b42bd08f0e..f88f0d9ecdcd 100644 --- a/ts/reactions/enqueueReactionForSend.ts +++ b/ts/reactions/enqueueReactionForSend.ts @@ -13,7 +13,7 @@ import { isDirectConversation } from '../util/whatTypeOfConversation'; import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { repeat, zipObject } from '../util/iterables'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from '../util/isAciString'; import { SendStatus } from '../messages/MessageSendState'; import * as log from '../logging/log'; diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 7cd6b9334766..06d6accc0f41 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -70,7 +70,8 @@ import { findBestMatchingCameraId, } from '../calling/findBestMatchingDevice'; import type { LocalizerType } from '../types/Util'; -import { normalizeAci, isAciString } from '../types/ServiceId'; +import { normalizeAci } from '../util/normalizeAci'; +import { isAciString } from '../util/isAciString'; import * as Errors from '../types/errors'; import type { ConversationModel } from '../models/conversations'; import * as Bytes from '../Bytes'; diff --git a/ts/services/contactSync.ts b/ts/services/contactSync.ts index 2713b8b53161..0d5890a71a34 100644 --- a/ts/services/contactSync.ts +++ b/ts/services/contactSync.ts @@ -5,7 +5,7 @@ import PQueue from 'p-queue'; import type { ContactSyncEvent } from '../textsecure/messageReceiverEvents'; import type { ModifiedContactDetails } from '../textsecure/ContactsParser'; -import { normalizeAci } from '../types/ServiceId'; +import { normalizeAci } from '../util/normalizeAci'; import * as Conversation from '../types/Conversation'; import * as Errors from '../types/errors'; import type { ValidateConversationType } from '../model-types.d'; diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 53a19145d115..42c6e22cb32e 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -47,10 +47,10 @@ import type { StoryDistributionIdString } from '../types/StoryDistributionId'; import type { ServiceIdString } from '../types/ServiceId'; import { normalizeServiceId, - normalizeAci, normalizePni, ServiceIdKind, } from '../types/ServiceId'; +import { normalizeAci } from '../util/normalizeAci'; import * as Stickers from '../types/Stickers'; import type { StoryDistributionWithMembersType, diff --git a/ts/sql/migrations/43-gv2-uuid.ts b/ts/sql/migrations/43-gv2-uuid.ts index 8163f76733ce..0a3fdfde7b70 100644 --- a/ts/sql/migrations/43-gv2-uuid.ts +++ b/ts/sql/migrations/43-gv2-uuid.ts @@ -6,7 +6,7 @@ import { omit } from 'lodash'; import type { LoggerType } from '../../types/Logging'; import type { AciString, ServiceIdString } from '../../types/ServiceId'; -import { normalizeAci } from '../../types/ServiceId'; +import { normalizeAci } from '../../util/normalizeAci'; import { isNotNil } from '../../util/isNotNil'; import { assertDev } from '../../util/assert'; import { diff --git a/ts/sql/migrations/88-service-ids.ts b/ts/sql/migrations/88-service-ids.ts index 6bf60da40297..852e293094f1 100644 --- a/ts/sql/migrations/88-service-ids.ts +++ b/ts/sql/migrations/88-service-ids.ts @@ -10,11 +10,8 @@ import type { AciString, PniString, } from '../../types/ServiceId'; -import { - normalizeServiceId, - normalizeAci, - normalizePni, -} from '../../types/ServiceId'; +import { normalizeServiceId, normalizePni } from '../../types/ServiceId'; +import { normalizeAci } from '../../util/normalizeAci'; import type { JSONWithUnknownFields } from '../../types/Util'; import { isNotNil } from '../../util/isNotNil'; diff --git a/ts/sql/migrations/89-call-history.ts b/ts/sql/migrations/89-call-history.ts index e6ffe9f9ae42..0791f84baa91 100644 --- a/ts/sql/migrations/89-call-history.ts +++ b/ts/sql/migrations/89-call-history.ts @@ -23,7 +23,7 @@ import { CallMode } from '../../types/Calling'; import type { MessageType, ConversationType } from '../Interface'; import { strictAssert } from '../../util/assert'; import { missingCaseError } from '../../util/missingCaseError'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; // Legacy type for calls that never had a call id type DirectCallHistoryDetailsType = { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index ca03bbdeed16..0ca91a37c8d2 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -70,7 +70,7 @@ import type { AciString, PniString, } from '../../types/ServiceId'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import { MY_STORY_ID, StorySendMode } from '../../types/Stories'; import * as Errors from '../../types/errors'; import { diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index cfc554b17d7d..fc5ed78fcadf 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -21,7 +21,7 @@ import type { StoryViewTargetType, StoryViewType } from '../../types/Stories'; import type { SyncType } from '../../jobs/helpers/syncHelpers'; import type { StoryDistributionIdString } from '../../types/StoryDistributionId'; import type { ServiceIdString } from '../../types/ServiceId'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import * as log from '../../logging/log'; import { TARGETED_CONVERSATION_CHANGED } from './conversations'; import { SIGNAL_ACI } from '../../types/SignalConversation'; diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index df12e220a4f6..508736568c7e 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -17,7 +17,7 @@ import type { ActiveCallType, GroupCallRemoteParticipantType, } from '../../types/Calling'; -import { isAciString } from '../../types/ServiceId'; +import { isAciString } from '../../util/isAciString'; import type { AciString } from '../../types/ServiceId'; import { CallMode, CallState } from '../../types/Calling'; import type { StateType } from '../reducer'; diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index fe30bfa35269..d1002e21e69c 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -9,6 +9,10 @@ import { mapDispatchToProps } from '../actions'; import type { Props as ComponentPropsType } from '../../components/CompositionArea'; import { CompositionArea } from '../../components/CompositionArea'; import type { StateType } from '../reducer'; +import type { + DraftBodyRanges, + HydratedBodyRangesType, +} from '../../types/BodyRange'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { dropNull } from '../../util/dropNull'; import { imageToBlurHash } from '../../util/imageToBlurHash'; @@ -53,7 +57,7 @@ import type { SmartCompositionRecordingProps } from './CompositionRecording'; import { SmartCompositionRecording } from './CompositionRecording'; import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft'; import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft'; -import { BodyRange } from '../../types/BodyRange'; +import { hydrateRanges } from '../../types/BodyRange'; type ExternalProps = { id: string; @@ -133,6 +137,12 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { const lastEditableMessageId = getLastEditableMessageId(state); + const convertDraftBodyRangesIntoHydrated = ( + bodyRanges: DraftBodyRanges | undefined + ): HydratedBodyRangesType | undefined => { + return hydrateRanges(bodyRanges, conversationSelector); + }; + return { // Base conversationId: id, @@ -150,6 +160,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { sendCounter, shouldHidePopovers, theme: getTheme(state), + convertDraftBodyRangesIntoHydrated, // AudioCapture errorDialogAudioRecorderType: @@ -204,19 +215,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { groupAdmins: getGroupAdminsSelector(state)(conversation.id), draftText: dropNull(draftText), - draftBodyRanges: draftBodyRanges?.map(bodyRange => { - if (BodyRange.isMention(bodyRange)) { - const mentionConvo = conversationSelector(bodyRange.mentionAci); - - return { - ...bodyRange, - conversationID: mentionConvo.id, - replacementText: mentionConvo.title, - }; - } - - return bodyRange; - }), + draftBodyRanges: hydrateRanges(draftBodyRanges, conversationSelector), renderSmartCompositionRecording: ( recProps: SmartCompositionRecordingProps ) => { diff --git a/ts/state/smart/StoryCreator.tsx b/ts/state/smart/StoryCreator.tsx index 08caa52e8e8f..8addda128aa7 100644 --- a/ts/state/smart/StoryCreator.tsx +++ b/ts/state/smart/StoryCreator.tsx @@ -7,7 +7,6 @@ import { useSelector } from 'react-redux'; import { ThemeType, type LocalizerType } from '../../types/Util'; import type { StateType } from '../reducer'; import { LinkPreviewSourceType } from '../../types/LinkPreview'; -import { SmartCompositionTextArea } from './CompositionTextArea'; import { StoryCreator } from '../../components/StoryCreator'; import { getAllSignalConnections, @@ -18,29 +17,35 @@ import { selectMostRecentActiveStoryTimestampByGroupOrDistributionList, } from '../selectors/conversations'; import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists'; -import { getIntl, getUserConversationId } from '../selectors/user'; +import { getIntl, getPlatform, getUserConversationId } from '../selectors/user'; import { getInstalledStickerPacks, getRecentStickers, } from '../selectors/stickers'; import { getAddStoryData } from '../selectors/stories'; import { - getEmojiSkinTone, - getHasSetMyStoriesPrivacy, -} from '../selectors/items'; + getIsFormattingFlagEnabled, + getIsFormattingSpoilersFlagEnabled, +} from '../selectors/composer'; import { getLinkPreview } from '../selectors/linkPreviews'; import { getPreferredBadgeSelector } from '../selectors/badges'; +import { + getEmojiSkinTone, + getHasSetMyStoriesPrivacy, + getTextFormattingEnabled, +} from '../selectors/items'; import { imageToBlurHash } from '../../util/imageToBlurHash'; import { processAttachment } from '../../util/processAttachment'; -import { useConversationsActions } from '../ducks/conversations'; import { useActions as useEmojisActions } from '../ducks/emojis'; +import { useAudioPlayerActions } from '../ducks/audioPlayer'; +import { useComposerActions } from '../ducks/composer'; +import { useConversationsActions } from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useItemsActions } from '../ducks/items'; import { useLinkPreviewActions } from '../ducks/linkPreviews'; import { useRecentEmojis } from '../selectors/emojis'; import { useStoriesActions } from '../ducks/stories'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; -import { useAudioPlayerActions } from '../ducks/audioPlayer'; export type PropsType = { file?: File; @@ -99,6 +104,15 @@ export function SmartStoryCreator(): JSX.Element | null { const { onSetSkinTone } = useItemsActions(); const { onUseEmoji } = useEmojisActions(); const { pauseVoiceNotePlayer } = useAudioPlayerActions(); + const { onTextTooLong } = useComposerActions(); + const { onUseEmoji: onPickEmoji } = useEmojisActions(); + + const isFormattingEnabled = useSelector(getTextFormattingEnabled); + const isFormattingFlagEnabled = useSelector(getIsFormattingFlagEnabled); + const isFormattingSpoilersFlagEnabled = useSelector( + getIsFormattingSpoilersFlagEnabled + ); + const platform = useSelector(getPlatform); return ( -): AciString; - -export function normalizeAci( - rawAci: string | undefined | null, - context: string, - logger?: Pick -): AciString | undefined; - -export function normalizeAci( - rawAci: string | undefined | null, - context: string, - logger: Pick = log -): AciString | undefined { - if (rawAci == null) { - return undefined; - } - - const result = rawAci.toLowerCase(); - - if (!isAciString(result)) { - logger.warn( - `Normalizing invalid serviceId: ${rawAci} to ${result} in context "${context}"` - ); - - // Cast anyway we don't want to throw here - return result as AciString; - } - - return result; -} - export function normalizePni( rawPni: string, context: string, diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index 30a524a9bc8d..56e7e614f98b 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -22,7 +22,7 @@ import { GroupCallJoinState, } from '../types/Calling'; import type { AciString } from '../types/ServiceId'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from './isAciString'; import { isMe } from './whatTypeOfConversation'; import * as log from '../logging/log'; import * as Errors from '../types/errors'; diff --git a/ts/util/findStoryMessage.ts b/ts/util/findStoryMessage.ts index a230ed17747f..b7485b09affb 100644 --- a/ts/util/findStoryMessage.ts +++ b/ts/util/findStoryMessage.ts @@ -6,7 +6,7 @@ import type { MessageModel } from '../models/messages'; import type { SignalService as Proto } from '../protobuf'; import type { AciString } from '../types/ServiceId'; import * as log from '../logging/log'; -import { normalizeAci } from '../types/ServiceId'; +import { normalizeAci } from './normalizeAci'; import { filter } from './iterables'; import { getContactId } from '../messages/helpers'; import { getTimestampFromLong } from './timestampLongUtils'; diff --git a/ts/util/handleEditMessage.ts b/ts/util/handleEditMessage.ts index bee99aa80c58..d1cde5b8546c 100644 --- a/ts/util/handleEditMessage.ts +++ b/ts/util/handleEditMessage.ts @@ -15,7 +15,7 @@ import { ReadStatus } from '../messages/MessageReadStatus'; import dataInterface from '../sql/Client'; import { drop } from './drop'; import { getAttachmentSignature, isVoiceMessage } from '../types/Attachment'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from './isAciString'; import { getMessageIdForLogging } from './idForLogging'; import { hasErrors } from '../state/selectors/message'; import { isIncoming, isOutgoing } from '../messages/helpers'; diff --git a/ts/util/isAciString.ts b/ts/util/isAciString.ts new file mode 100644 index 000000000000..4ba88a976011 --- /dev/null +++ b/ts/util/isAciString.ts @@ -0,0 +1,9 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AciString } from '../types/ServiceId'; +import { isValidUuid } from './isValidUuid'; + +export function isAciString(value?: string | null): value is AciString { + return isValidUuid(value); +} diff --git a/ts/util/isSafetyNumberNotAvailable.ts b/ts/util/isSafetyNumberNotAvailable.ts index b7bfa16a1412..0a9dcfe32177 100644 --- a/ts/util/isSafetyNumberNotAvailable.ts +++ b/ts/util/isSafetyNumberNotAvailable.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationType } from '../state/ducks/conversations'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from './isAciString'; export const isSafetyNumberNotAvailable = ( contact?: ConversationType diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 264bc309b871..8dd84fd52118 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -2004,13 +2004,6 @@ "updated": "2021-12-10T23:24:03.829Z", "reasonDetail": "Doesn't touch the DOM." }, - { - "rule": "React-useRef", - "path": "ts/components/AddCaptionModal.tsx", - "line": " const scrollerRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2022-10-03T16:06:12.837Z" - }, { "rule": "React-useRef", "path": "ts/components/AvatarTextEditor.tsx", @@ -2390,6 +2383,13 @@ "reasonCategory": "usageTrusted", "updated": "2022-11-11T17:11:07.659Z" }, + { + "rule": "React-useRef", + "path": "ts/components/MediaEditor.tsx", + "line": " const inputApiRef = useRef();", + "reasonCategory": "usageTrusted", + "updated": "2023-09-11T20:19:18.681Z" + }, { "rule": "React-useRef", "path": "ts/components/MediaQualitySelector.tsx", diff --git a/ts/util/markConversationRead.ts b/ts/util/markConversationRead.ts index 31af5095670b..4b7bcd210df8 100644 --- a/ts/util/markConversationRead.ts +++ b/ts/util/markConversationRead.ts @@ -23,7 +23,7 @@ import { } from '../jobs/conversationJobQueue'; import { ReceiptType } from '../types/Receipt'; import type { AciString } from '../types/ServiceId'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from './isAciString'; export async function markConversationRead( conversationAttrs: ConversationAttributesType, diff --git a/ts/util/normalizeAci.ts b/ts/util/normalizeAci.ts new file mode 100644 index 000000000000..11aa96f58590 --- /dev/null +++ b/ts/util/normalizeAci.ts @@ -0,0 +1,42 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AciString } from '../types/ServiceId'; +import type { LoggerType } from '../types/Logging'; +import * as log from '../logging/log'; +import { isAciString } from './isAciString'; + +export function normalizeAci( + rawAci: string, + context: string, + logger?: Pick +): AciString; + +export function normalizeAci( + rawAci: string | undefined | null, + context: string, + logger?: Pick +): AciString | undefined; + +export function normalizeAci( + rawAci: string | undefined | null, + context: string, + logger: Pick = log +): AciString | undefined { + if (rawAci == null) { + return undefined; + } + + const result = rawAci.toLowerCase(); + + if (!isAciString(result)) { + logger.warn( + `Normalizing invalid serviceId: ${rawAci} to ${result} in context "${context}"` + ); + + // Cast anyway we don't want to throw here + return result as AciString; + } + + return result; +} diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts index f798736191fb..15d57da0a2ed 100644 --- a/ts/util/safetyNumber.ts +++ b/ts/util/safetyNumber.ts @@ -15,7 +15,7 @@ import { SafetyNumberIdentifierType, SafetyNumberMode, } from '../types/safetyNumber'; -import { isAciString } from '../types/ServiceId'; +import { isAciString } from './isAciString'; const ITERATION_COUNT = 5200; const E164_VERSION = 1;