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

@ -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;
}
}
}

View file

@ -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';

View file

@ -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';

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,
};

View file

@ -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<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
(
| {
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<HTMLImageElement>(new Image());
const [isStickerPopperOpen, setIsStickerPopperOpen] =
useState<boolean>(false);
const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(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<InputApi | undefined>();
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<ImageStateType>(INITIAL_IMAGE_STATE);
const [cropPreset, setCropPreset] = useState<CropPreset>(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,14 +724,17 @@ export function MediaEditor({
return null;
}
let tooling: JSX.Element | undefined;
let toolElement: JSX.Element | undefined;
if (editMode === EditMode.Text) {
tooling = (
toolElement = (
<>
<div className="MediaEditor__tools-row-1" />
<div className="MediaEditor__tools-row-2">
<div className="MediaEditor__toolbar">
<Slider
handleStyle={{ backgroundColor: getHSL(sliderValue) }}
label={i18n('icu:CustomColorEditor__hue')}
moduleClassName="HueSlider MediaEditor__tools__tool"
moduleClassName="HueSlider MediaEditor__toolbar__tool"
onChange={setSliderValue}
value={sliderValue}
/>
@ -665,19 +760,19 @@ export function MediaEditor({
value: TextStyle.Outline,
},
]}
moduleClassName={classNames('MediaEditor__tools__tool', {
'MediaEditor__tools__button--text-regular':
moduleClassName={classNames('MediaEditor__toolbar__tool', {
'MediaEditor__toolbar__button--text-regular':
textStyle === TextStyle.Regular,
'MediaEditor__tools__button--text-highlight':
'MediaEditor__toolbar__button--text-highlight':
textStyle === TextStyle.Highlight,
'MediaEditor__tools__button--text-outline':
'MediaEditor__toolbar__button--text-outline':
textStyle === TextStyle.Outline,
})}
theme={Theme.Dark}
value={textStyle}
/>
<button
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
</div>
<Button
onClick={() => {
setEditMode(undefined);
@ -686,19 +781,24 @@ export function MediaEditor({
activeObject.exitEditing();
}
}}
type="button"
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('icu:done')}
</button>
</Button>
</div>
</>
);
} else if (editMode === EditMode.Draw) {
tooling = (
toolElement = (
<>
<div className="MediaEditor__tools-row-1" />
<div className="MediaEditor__tools-row-2">
<div className="MediaEditor__toolbar">
<Slider
handleStyle={{ backgroundColor: getHSL(sliderValue) }}
label={i18n('icu:CustomColorEditor__hue')}
moduleClassName="HueSlider MediaEditor__tools__tool"
moduleClassName="HueSlider MediaEditor__toolbar__tool"
onChange={setSliderValue}
value={sliderValue}
/>
@ -718,9 +818,10 @@ export function MediaEditor({
value: DrawTool.Highlighter,
},
]}
moduleClassName={classNames('MediaEditor__tools__tool', {
'MediaEditor__tools__button--draw-pen': drawTool === DrawTool.Pen,
'MediaEditor__tools__button--draw-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}
@ -754,26 +855,28 @@ export function MediaEditor({
value: DrawWidth.Heavy,
},
]}
moduleClassName={classNames('MediaEditor__tools__tool', {
'MediaEditor__tools__button--width-thin':
moduleClassName={classNames('MediaEditor__toolbar__tool', {
'MediaEditor__toolbar__button--width-thin':
drawWidth === DrawWidth.Thin,
'MediaEditor__tools__button--width-regular':
'MediaEditor__toolbar__button--width-regular':
drawWidth === DrawWidth.Regular,
'MediaEditor__tools__button--width-medium':
'MediaEditor__toolbar__button--width-medium':
drawWidth === DrawWidth.Medium,
'MediaEditor__tools__button--width-heavy':
'MediaEditor__toolbar__button--width-heavy':
drawWidth === DrawWidth.Heavy,
})}
theme={Theme.Dark}
value={drawWidth}
/>
<button
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
</div>
<Button
onClick={() => setEditMode(undefined)}
type="button"
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('icu:done')}
</button>
</Button>
</div>
</>
);
} else if (editMode === EditMode.Crop) {
@ -784,10 +887,51 @@ export function MediaEditor({
imageState.flipY ||
imageState.angle !== 0;
tooling = (
toolElement = (
<>
<div className="MediaEditor__tools-row-1">
<button
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
className={classNames(
'MediaEditor__crop-preset MediaEditor__crop-preset--free',
{
'MediaEditor__crop-preset--selected':
cropPreset === CropPreset.Freeform,
}
)}
onClick={() => setCropPreset(CropPreset.Freeform)}
type="button"
>
Freeform
</button>
<button
className={classNames(
'MediaEditor__crop-preset MediaEditor__crop-preset--square',
{
'MediaEditor__crop-preset--selected':
cropPreset === CropPreset.Square,
}
)}
onClick={() => setCropPreset(CropPreset.Square)}
type="button"
>
Square
</button>
<button
className={classNames(
'MediaEditor__crop-preset MediaEditor__crop-preset--vertical',
{
'MediaEditor__crop-preset--selected':
cropPreset === CropPreset.Vertical,
}
)}
onClick={() => setCropPreset(CropPreset.Vertical)}
type="button"
>
9:16
</button>
</div>
<div className="MediaEditor__tools-row-2">
<Button
disabled={!canReset}
onClick={async () => {
if (!fabricCanvas) {
@ -800,16 +944,19 @@ export function MediaEditor({
width: image.width,
};
setImageState(newImageState);
setCropPreset(CropPreset.Freeform);
moveFabricObjectsForReset(fabricCanvas, imageState);
takeSnapshot('reset', newImageState);
}}
type="button"
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('icu:MediaEditor__crop--reset')}
</button>
</Button>
<div className="MediaEditor__toolbar">
<button
aria-label={i18n('icu:MediaEditor__crop--rotate')}
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--rotate"
className="MediaEditor__toolbar__tool MediaEditor__toolbar__button MediaEditor__toolbar__button--rotate"
onClick={() => {
if (!fabricCanvas) {
return;
@ -845,7 +992,7 @@ export function MediaEditor({
/>
<button
aria-label={i18n('icu:MediaEditor__crop--flip')}
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--flip"
className="MediaEditor__toolbar__tool MediaEditor__toolbar__button MediaEditor__toolbar__button--flip"
onClick={() => {
if (!fabricCanvas) {
return;
@ -865,8 +1012,8 @@ export function MediaEditor({
<button
aria-label={i18n('icu:MediaEditor__crop--lock')}
className={classNames(
'MediaEditor__tools__button',
`MediaEditor__tools__button--crop-${
'MediaEditor__toolbar__button',
`MediaEditor__toolbar__button--crop-${
cropAspectRatioLock ? '' : 'un'
}locked`
)}
@ -878,10 +1025,14 @@ export function MediaEditor({
}}
type="button"
/>
<button
className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
disabled={!canCrop}
</div>
<Button
onClick={() => {
if (!canCrop) {
setEditMode(undefined);
return;
}
if (!fabricCanvas) {
return;
}
@ -899,17 +1050,52 @@ export function MediaEditor({
moveFabricObjectsForCrop(fabricCanvas, pendingCrop);
takeSnapshot('crop', newImageState);
setEditMode(undefined);
setCropPreset(CropPreset.Freeform);
}}
type="button"
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('icu:done')}
</button>
</Button>
</div>
</>
);
}
return createPortal(
<div className="MediaEditor">
<div className="MediaEditor__history-buttons">
<button
aria-label={i18n('icu:MediaEditor__control--undo')}
className="MediaEditor__control MediaEditor__control--undo"
disabled={!canUndo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
undoIfPossible();
}}
type="button"
/>
<button
aria-label={i18n('icu:MediaEditor__control--redo')}
className="MediaEditor__control MediaEditor__control--redo"
disabled={!canRedo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
redoIfPossible();
}}
type="button"
/>
</div>
<button
aria-label={i18n('icu:close')}
className="MediaEditor__close"
onClick={onTryClose}
type="button"
/>
<div className="MediaEditor__container">
<SizeObserver
onSizeChange={size => {
@ -934,63 +1120,12 @@ export function MediaEditor({
)}
</SizeObserver>
</div>
<div className="MediaEditor__toolbar">
{tooling ? (
<div className="MediaEditor__tools">{tooling}</div>
<div className="MediaEditor__tools">
{toolElement !== undefined ? (
toolElement
) : (
<>
{props.supportsCaption ? (
<div className="MediaEditor__toolbar__caption">
<button
type="button"
className="MediaEditor__toolbar__caption__add-caption-button"
onClick={() => setShowAddCaptionModal(true)}
>
{caption !== '' ? (
<span>
<MessageBody
renderLocation={RenderLocation.MediaEditor}
bodyRanges={captionBodyRanges}
i18n={i18n}
isSpoilerExpanded={{}}
text={caption}
/>
</span>
) : (
i18n('icu:MediaEditor__caption-button')
)}
</button>
{showAddCaptionModal && (
<AddCaptionModal
i18n={i18n}
draftText={caption}
draftBodyRanges={captionBodyRanges}
onSubmit={(messageText, bodyRanges) => {
setCaption(messageText.trim());
setCaptionBodyRanges(bodyRanges);
setShowAddCaptionModal(false);
}}
onClose={() => setShowAddCaptionModal(false)}
RenderCompositionTextArea={props.renderCompositionTextArea}
theme={ThemeType.dark}
/>
)}
</div>
) : (
<div className="MediaEditor__toolbar--space" />
)}
</>
)}
<div className="MediaEditor__toolbar--buttons">
<Button
onClick={onTryClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('icu:discard')}
</Button>
<div className="MediaEditor__controls">
<div className="MediaEditor__tools-row-1">
<button
aria-label={i18n('icu:MediaEditor__control--draw')}
className={classNames({
@ -1025,6 +1160,29 @@ export function MediaEditor({
}}
type="button"
/>
<button
aria-label={i18n('icu:MediaEditor__control--crop')}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--crop': true,
'MediaEditor__control--selected': editMode === EditMode.Crop,
})}
onClick={() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Crop) {
const obj = fabricCanvas.getActiveObject();
if (obj instanceof MediaEditorFabricCropRect) {
fabricCanvas.remove(obj);
}
setEditMode(undefined);
} else {
setEditMode(EditMode.Crop);
}
}}
type="button"
/>
<StickerButton
blessedPacks={[]}
className={classNames({
@ -1058,7 +1216,10 @@ export function MediaEditor({
const sticker = new MediaEditorFabricSticker(src);
sticker.scaleToHeight(size);
sticker.setPositionByOrigin(
new fabric.Point(imageState.width / 2, imageState.height / 2),
new fabric.Point(
imageState.width / 2,
imageState.height / 2
),
'center',
'center'
);
@ -1120,53 +1281,48 @@ export function MediaEditor({
showPickerHint={false}
theme={Theme.Dark}
/>
<button
aria-label={i18n('icu:MediaEditor__control--crop')}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--crop': true,
'MediaEditor__control--selected': editMode === EditMode.Crop,
})}
onClick={() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Crop) {
const obj = fabricCanvas.getActiveObject();
if (obj instanceof MediaEditorFabricCropRect) {
fabricCanvas.remove(obj);
}
setEditMode(undefined);
} else {
setEditMode(EditMode.Crop);
</div>
<div className="MediaEditor__tools-row-2">
<div className="MediaEditor__tools--input">
<CompositionInput
draftText={draftText}
draftBodyRanges={draftBodyRanges}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
inputApi={inputApiRef}
isFormattingEnabled={isFormattingEnabled}
isFormattingFlagEnabled={isFormattingFlagEnabled}
isFormattingSpoilersFlagEnabled={
isFormattingSpoilersFlagEnabled
}
moduleClassName="StoryViewsNRepliesModal__input"
onCloseLinkPreview={noop}
onEditorStateChange={({ bodyRanges, messageText }) => {
setCaptionBodyRanges(bodyRanges);
setCaption(messageText);
}}
type="button"
/>
<button
aria-label={i18n('icu:MediaEditor__control--undo')}
className="MediaEditor__control MediaEditor__control--undo"
disabled={!canUndo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
undoIfPossible();
onPickEmoji={onPickEmoji}
onSubmit={() => {
inputApiRef.current?.reset();
}}
type="button"
/>
<button
aria-label={i18n('icu:MediaEditor__control--redo')}
className="MediaEditor__control MediaEditor__control--redo"
disabled={!canRedo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
redoIfPossible();
}}
type="button"
onTextTooLong={onTextTooLong}
placeholder="Message"
platform={platform}
sendCounter={0}
sortedGroupMembers={sortedGroupMembers}
theme={ThemeType.dark}
>
<EmojiButton
className="StoryViewsNRepliesModal__emoji-button"
i18n={i18n}
onPickEmoji={insertEmoji}
onOpen={() => setEmojiPopperOpen(true)}
onClose={closeEmojiPickerAndFocusComposer}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</CompositionInput>
</div>
<Button
disabled={!image || isSaving || isSending}
@ -1247,6 +1403,8 @@ export function MediaEditor({
)}
</Button>
</div>
</>
)}
</div>
{confirmDiscardModal}
</div>,

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);

View file

@ -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';

View file

@ -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({

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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(
const nextLeft = clamp(
left / zoom,
MediaEditorFabricCropRect.PADDING / zoom,
(canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom
)
);
this.set(
'top',
clamp(
const nextTop = clamp(
top / zoom,
MediaEditorFabricCropRect.PADDING / zoom,
(canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom
)
);
}
this.set('left', nextLeft);
this.set('top', nextTop);
this.setCoords();
}

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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 {

View file

@ -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: (

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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 {

View file

@ -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';

View file

@ -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 = {

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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
) => {

View file

@ -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 (
<StoryCreator
@ -113,6 +127,9 @@ export function SmartStoryCreator(): JSX.Element | null {
i18n={i18n}
imageToBlurHash={imageToBlurHash}
installedPacks={installedPacks}
isFormattingEnabled={isFormattingEnabled}
isFormattingFlagEnabled={isFormattingFlagEnabled}
isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
isSending={isSending}
linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
me={me}
@ -123,19 +140,21 @@ export function SmartStoryCreator(): JSX.Element | null {
onDeleteList={deleteDistributionList}
onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom}
onMediaPlaybackStart={pauseVoiceNotePlayer}
onPickEmoji={onPickEmoji}
onRemoveMembers={removeMembersFromDistributionList}
onRepliesNReactionsChanged={allowsRepliesChanged}
onSelectedStoryList={verifyStoryListMembers}
onSend={sendStoryMessage}
onSetSkinTone={onSetSkinTone}
onTextTooLong={onTextTooLong}
onUseEmoji={onUseEmoji}
onViewersUpdated={updateStoryViewers}
onMediaPlaybackStart={pauseVoiceNotePlayer}
ourConversationId={ourConversationId}
platform={platform}
processAttachment={processAttachment}
recentEmojis={recentEmojis}
recentStickers={recentStickers}
renderCompositionTextArea={SmartCompositionTextArea}
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
signalConnections={signalConnections}

View file

@ -3,7 +3,7 @@
import { assert } from 'chai';
import { normalizeAci } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import {
getCountryCodeValue,
getBucketValue,

View file

@ -3,7 +3,8 @@
import { assert } from 'chai';
import type { ConversationType } from '../../state/ducks/conversations';
import { generateAci, normalizeAci } from '../../types/ServiceId';
import { generateAci } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
import type { ServiceIdString } from '../../types/ServiceId';
import { getDefaultConversationWithServiceId } from '../helpers/getDefaultConversation';

View file

@ -8,7 +8,8 @@ import { generateKeyPair } from '../../Curve';
import type { UploadKeysType } from '../../textsecure/WebAPI';
import AccountManager from '../../textsecure/AccountManager';
import type { PreKeyType } from '../../textsecure/Types.d';
import { ServiceIdKind, normalizeAci } from '../../types/ServiceId';
import { ServiceIdKind } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
const { textsecure } = window;

View file

@ -9,11 +9,8 @@ import sinon from 'sinon';
import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d';
import type { WebAPIType } from '../textsecure/WebAPI';
import {
generateAci,
normalizeAci,
normalizeServiceId,
} from '../types/ServiceId';
import { generateAci, normalizeServiceId } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';

View file

@ -3,7 +3,8 @@
import { assert } from 'chai';
import { generateAci, isAciString } from '../../types/ServiceId';
import { generateAci } from '../../types/ServiceId';
import { isAciString } from '../../util/isAciString';
import type { ConversationType } from '../../state/ducks/conversations';
import { MemberRepository, _toMembers } from '../../quill/memberRepository';
import { getDefaultConversationWithServiceId } from '../../test-both/helpers/getDefaultConversation';

View file

@ -13,7 +13,8 @@ import type {
PniString,
ServiceIdString,
} from '../../types/ServiceId';
import { normalizeAci, normalizePni } from '../../types/ServiceId';
import { normalizePni } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
import type {
KyberPreKeyType,
PreKeyType,

View file

@ -9,7 +9,8 @@ import { range } from 'lodash';
import { getTableData, insertData, updateToVersion } from './helpers';
import type { ServiceIdString } from '../../types/ServiceId';
import { normalizeAci, normalizePni } from '../../types/ServiceId';
import { normalizePni } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
import type { PreKeyType } from '../../sql/Interface';
type TestingPreKey = Omit<

View file

@ -9,7 +9,8 @@ import { range } from 'lodash';
import { insertData, updateToVersion } from './helpers';
import type { ServiceIdString } from '../../types/ServiceId';
import { normalizeAci, normalizePni } from '../../types/ServiceId';
import { normalizePni } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
import type { KyberPreKeyType, SignedPreKeyType } from '../../sql/Interface';
type TestingKyberKey = Omit<

View file

@ -43,11 +43,11 @@ import {
import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
import {
ServiceIdKind,
normalizeAci,
normalizePni,
toTaggedPni,
isUntaggedPniString,
} from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { assertDev, strictAssert } from '../util/assert';

View file

@ -6,7 +6,7 @@
import protobuf from '../protobuf/wrap';
import { SignalService as Proto } from '../protobuf';
import { normalizeAci } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { DurationInSeconds } from '../util/durations';
import * as Errors from '../types/errors';
import * as log from '../logging/log';

View file

@ -56,14 +56,14 @@ import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import type { ServiceIdString } from '../types/ServiceId';
import {
ServiceIdKind,
normalizeAci,
normalizeServiceId,
normalizePni,
isAciString,
isPniString,
isServiceIdString,
fromPniObject,
} from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString';
import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf';

View file

@ -13,7 +13,8 @@ import {
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf';
import type { PniString, AciString } from '../types/ServiceId';
import { normalizeAci, normalizePni } from '../types/ServiceId';
import { normalizePni } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { strictAssert } from '../util/assert';
type ProvisionDecryptResult = {

View file

@ -9,11 +9,8 @@ import Long from 'long';
import type { LoggerType } from '../../types/Logging';
import { strictAssert } from '../../util/assert';
import {
isAciString,
isUntaggedPniString,
toTaggedPni,
} from '../../types/ServiceId';
import { isUntaggedPniString, toTaggedPni } from '../../types/ServiceId';
import { isAciString } from '../../util/isAciString';
import * as Bytes from '../../Bytes';
import { UUID_BYTE_SIZE } from '../../Crypto';
import { uuidToBytes, bytesToUuid } from '../../util/uuidToBytes';

View file

@ -29,7 +29,8 @@ import { SECOND, DurationInSeconds } from '../util/durations';
import type { AnyPaymentEvent } from '../types/Payment';
import { PaymentEventKind } from '../types/Payment';
import { filterAndClean } from '../types/BodyRange';
import { isAciString, normalizeAci } from '../types/ServiceId';
import { isAciString } from '../util/isAciString';
import { normalizeAci } from '../util/normalizeAci';
const FLAGS = Proto.DataMessage.Flags;
export const ATTACHMENT_MAX = 32;

View file

@ -10,7 +10,8 @@ import type {
PniString,
ServiceIdString,
} from '../../types/ServiceId';
import { ServiceIdKind, isAciString, isPniString } from '../../types/ServiceId';
import { ServiceIdKind, isPniString } from '../../types/ServiceId';
import { isAciString } from '../../util/isAciString';
import * as log from '../../logging/log';
import Helpers from '../Helpers';

View file

@ -17,7 +17,7 @@ import {
} from '../util/search';
import { assertDev } from '../util/assert';
import type { AciString } from './ServiceId';
import { normalizeAci } from './ServiceId';
import { normalizeAci } from '../util/normalizeAci';
// Cold storage of body ranges

View file

@ -8,6 +8,7 @@ import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
import { isValidUuid } from '../util/isValidUuid';
import * as log from '../logging/log';
import type { LoggerType } from './Logging';
import { isAciString } from '../util/isAciString';
export enum ServiceIdKind {
ACI = 'ACI',
@ -26,10 +27,6 @@ export function isServiceIdString(
return isAciString(value) || isPniString(value);
}
export function isAciString(value?: string | null): value is AciString {
return isValidUuid(value);
}
export function isPniString(value?: string | null): value is PniString {
if (value == null) {
return false;
@ -87,41 +84,6 @@ export function normalizeServiceId(
return result;
}
export function normalizeAci(
rawAci: string,
context: string,
logger?: Pick<LoggerType, 'warn'>
): AciString;
export function normalizeAci(
rawAci: string | undefined | null,
context: string,
logger?: Pick<LoggerType, 'warn'>
): AciString | undefined;
export function normalizeAci(
rawAci: string | undefined | null,
context: string,
logger: Pick<LoggerType, 'warn'> = 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,

View file

@ -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';

View file

@ -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';

View file

@ -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';

9
ts/util/isAciString.ts Normal file
View file

@ -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);
}

View file

@ -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

View file

@ -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<HTMLDivElement>(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<InputApi | undefined>();",
"reasonCategory": "usageTrusted",
"updated": "2023-09-11T20:19:18.681Z"
},
{
"rule": "React-useRef",
"path": "ts/components/MediaQualitySelector.tsx",

View file

@ -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,

42
ts/util/normalizeAci.ts Normal file
View file

@ -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<LoggerType, 'warn'>
): AciString;
export function normalizeAci(
rawAci: string | undefined | null,
context: string,
logger?: Pick<LoggerType, 'warn'>
): AciString | undefined;
export function normalizeAci(
rawAci: string | undefined | null,
context: string,
logger: Pick<LoggerType, 'warn'> = 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;
}

View file

@ -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;