Improvements to the media editor
This commit is contained in:
		
					parent
					
						
							
								e8eb7638c4
							
						
					
				
			
			
				commit
				
					
						d0296ececa
					
				
			
		
					 61 changed files with 1124 additions and 969 deletions
				
			
		| 
						 | 
					@ -2,8 +2,6 @@
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.MediaEditor {
 | 
					.MediaEditor {
 | 
				
			||||||
  $tools-height: 44px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  background: $color-gray-95;
 | 
					  background: $color-gray-95;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
| 
						 | 
					@ -20,7 +18,7 @@
 | 
				
			||||||
  &__container {
 | 
					  &__container {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex: 1;
 | 
					    flex: 1;
 | 
				
			||||||
    padding-block: 22px;
 | 
					    padding-block: 48px;
 | 
				
			||||||
    padding-inline: 60px;
 | 
					    padding-inline: 60px;
 | 
				
			||||||
    padding-bottom: 0;
 | 
					    padding-bottom: 0;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
| 
						 | 
					@ -47,12 +45,12 @@
 | 
				
			||||||
  &__control {
 | 
					  &__control {
 | 
				
			||||||
    @include button-reset;
 | 
					    @include button-reset;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    border-radius: 32px;
 | 
					    border-radius: 20px;
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
    height: 32px;
 | 
					    height: 32px;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    margin-block: 0;
 | 
					    margin-block: 0;
 | 
				
			||||||
    margin-inline: 18px;
 | 
					    margin-inline: 20px;
 | 
				
			||||||
    opacity: 1;
 | 
					    opacity: 1;
 | 
				
			||||||
    width: 32px;
 | 
					    width: 32px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,7 +117,7 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__toolbar {
 | 
					  &__tools {
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
| 
						 | 
					@ -127,6 +125,11 @@
 | 
				
			||||||
    padding: 22px;
 | 
					    padding: 22px;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &--input {
 | 
				
			||||||
 | 
					      margin-inline: 24px;
 | 
				
			||||||
 | 
					      min-width: 410px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &--buttons {
 | 
					    &--buttons {
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
| 
						 | 
					@ -134,18 +137,12 @@
 | 
				
			||||||
      width: 100%;
 | 
					      width: 100%;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &--space {
 | 
					 | 
				
			||||||
      height: $tools-height;
 | 
					 | 
				
			||||||
      margin-bottom: 22px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__caption {
 | 
					    &__caption {
 | 
				
			||||||
      height: $tools-height;
 | 
					      height: 44px;
 | 
				
			||||||
      margin-bottom: 22px;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &__add-caption-button {
 | 
					      &__add-caption-button {
 | 
				
			||||||
        @include button-reset;
 | 
					        @include button-reset;
 | 
				
			||||||
        border-radius: 9999px;
 | 
					        @include rounded-corners;
 | 
				
			||||||
        background: $color-gray-90;
 | 
					        background: $color-gray-90;
 | 
				
			||||||
        color: $color-gray-15;
 | 
					        color: $color-gray-15;
 | 
				
			||||||
        padding-block: 8px;
 | 
					        padding-block: 8px;
 | 
				
			||||||
| 
						 | 
					@ -162,25 +159,36 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__controls {
 | 
					  &__tools-row-1 {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    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;
 | 
					    justify-content: center;
 | 
				
			||||||
    max-width: 596px;
 | 
					    max-width: 596px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__tools {
 | 
					  &__toolbar {
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    background-color: $color-gray-90;
 | 
					    background-color: $color-gray-90;
 | 
				
			||||||
    border-radius: 10px;
 | 
					    border-radius: 10px;
 | 
				
			||||||
    color: $color-white;
 | 
					    color: $color-white;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    height: $tools-height;
 | 
					    height: 36px;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    margin-bottom: 22px;
 | 
					 | 
				
			||||||
    padding-block: 14px;
 | 
					    padding-block: 14px;
 | 
				
			||||||
    padding-inline: 12px;
 | 
					    padding-inline: 12px;
 | 
				
			||||||
 | 
					    margin-inline: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__tool,
 | 
					    &__tool,
 | 
				
			||||||
    &__tool__button {
 | 
					    &__tool__button {
 | 
				
			||||||
| 
						 | 
					@ -206,13 +214,6 @@
 | 
				
			||||||
      margin-inline: 8px;
 | 
					      margin-inline: 8px;
 | 
				
			||||||
      padding: 8px;
 | 
					      padding: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &--words {
 | 
					 | 
				
			||||||
        height: auto;
 | 
					 | 
				
			||||||
        width: auto;
 | 
					 | 
				
			||||||
        padding-block: 0;
 | 
					 | 
				
			||||||
        padding-inline: 6px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &--draw-pen__button {
 | 
					      &--draw-pen__button {
 | 
				
			||||||
        @include icon('v3/brush/brush-pen-compact.svg');
 | 
					        @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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,10 +25,10 @@ import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
 | 
				
			||||||
import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
 | 
					import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  isServiceIdString,
 | 
					  isServiceIdString,
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizePni,
 | 
					  normalizePni,
 | 
				
			||||||
  normalizeServiceId,
 | 
					  normalizeServiceId,
 | 
				
			||||||
} from './types/ServiceId';
 | 
					} from './types/ServiceId';
 | 
				
			||||||
 | 
					import { normalizeAci } from './util/normalizeAci';
 | 
				
			||||||
import { sleep } from './util/sleep';
 | 
					import { sleep } from './util/sleep';
 | 
				
			||||||
import { isNotNil } from './util/isNotNil';
 | 
					import { isNotNil } from './util/isNotNil';
 | 
				
			||||||
import { MINUTE, SECOND } from './util/durations';
 | 
					import { MINUTE, SECOND } from './util/durations';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -142,12 +142,9 @@ import { themeChanged } from './shims/themeChanged';
 | 
				
			||||||
import { createIPCEvents } from './util/createIPCEvents';
 | 
					import { createIPCEvents } from './util/createIPCEvents';
 | 
				
			||||||
import { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
 | 
					import { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
 | 
				
			||||||
import type { ServiceIdString } from './types/ServiceId';
 | 
					import type { ServiceIdString } from './types/ServiceId';
 | 
				
			||||||
import {
 | 
					import { ServiceIdKind, isServiceIdString } from './types/ServiceId';
 | 
				
			||||||
  ServiceIdKind,
 | 
					import { isAciString } from './util/isAciString';
 | 
				
			||||||
  isAciString,
 | 
					import { normalizeAci } from './util/normalizeAci';
 | 
				
			||||||
  isServiceIdString,
 | 
					 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
} from './types/ServiceId';
 | 
					 | 
				
			||||||
import * as log from './logging/log';
 | 
					import * as log from './logging/log';
 | 
				
			||||||
import { loadRecentEmojis } from './util/loadRecentEmojis';
 | 
					import { loadRecentEmojis } from './util/loadRecentEmojis';
 | 
				
			||||||
import { deleteAllLogs } from './util/deleteAllLogs';
 | 
					import { deleteAllLogs } from './util/deleteAllLogs';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@ export default {
 | 
				
			||||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
 | 
					const useProps = (overrideProps: Partial<Props> = {}): Props => ({
 | 
				
			||||||
  addAttachment: action('addAttachment'),
 | 
					  addAttachment: action('addAttachment'),
 | 
				
			||||||
  conversationId: '123',
 | 
					  conversationId: '123',
 | 
				
			||||||
 | 
					  convertDraftBodyRangesIntoHydrated: () => undefined,
 | 
				
			||||||
  discardEditMessage: action('discardEditMessage'),
 | 
					  discardEditMessage: action('discardEditMessage'),
 | 
				
			||||||
  focusCounter: 0,
 | 
					  focusCounter: 0,
 | 
				
			||||||
  sendCounter: 0,
 | 
					  sendCounter: 0,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import type { ReadonlyDeep } from 'type-fest';
 | 
					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 { LocalizerType, ThemeType } from '../types/Util';
 | 
				
			||||||
import type { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
 | 
					import type { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
 | 
				
			||||||
import { RecordingState } from '../types/AudioRecorder';
 | 
					import { RecordingState } from '../types/AudioRecorder';
 | 
				
			||||||
| 
						 | 
					@ -85,6 +88,9 @@ export type OwnProps = Readonly<{
 | 
				
			||||||
    conversationId: string,
 | 
					    conversationId: string,
 | 
				
			||||||
    onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
 | 
					    onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
 | 
				
			||||||
  ) => unknown;
 | 
					  ) => unknown;
 | 
				
			||||||
 | 
					  convertDraftBodyRangesIntoHydrated: (
 | 
				
			||||||
 | 
					    bodyRanges: DraftBodyRanges | undefined
 | 
				
			||||||
 | 
					  ) => HydratedBodyRangesType | undefined;
 | 
				
			||||||
  conversationId: string;
 | 
					  conversationId: string;
 | 
				
			||||||
  discardEditMessage: (id: string) => unknown;
 | 
					  discardEditMessage: (id: string) => unknown;
 | 
				
			||||||
  draftEditMessage?: DraftEditMessageType;
 | 
					  draftEditMessage?: DraftEditMessageType;
 | 
				
			||||||
| 
						 | 
					@ -221,6 +227,7 @@ export function CompositionArea({
 | 
				
			||||||
  // Base props
 | 
					  // Base props
 | 
				
			||||||
  addAttachment,
 | 
					  addAttachment,
 | 
				
			||||||
  conversationId,
 | 
					  conversationId,
 | 
				
			||||||
 | 
					  convertDraftBodyRangesIntoHydrated,
 | 
				
			||||||
  discardEditMessage,
 | 
					  discardEditMessage,
 | 
				
			||||||
  draftEditMessage,
 | 
					  draftEditMessage,
 | 
				
			||||||
  focusCounter,
 | 
					  focusCounter,
 | 
				
			||||||
| 
						 | 
					@ -853,12 +860,25 @@ export function CompositionArea({
 | 
				
			||||||
        'url' in attachmentToEdit &&
 | 
					        'url' in attachmentToEdit &&
 | 
				
			||||||
        attachmentToEdit.url && (
 | 
					        attachmentToEdit.url && (
 | 
				
			||||||
          <MediaEditor
 | 
					          <MediaEditor
 | 
				
			||||||
 | 
					            draftBodyRanges={draftBodyRanges}
 | 
				
			||||||
 | 
					            draftText={draftText}
 | 
				
			||||||
 | 
					            getPreferredBadge={getPreferredBadge}
 | 
				
			||||||
            i18n={i18n}
 | 
					            i18n={i18n}
 | 
				
			||||||
            imageSrc={attachmentToEdit.url}
 | 
					            imageSrc={attachmentToEdit.url}
 | 
				
			||||||
            imageToBlurHash={imageToBlurHash}
 | 
					            imageToBlurHash={imageToBlurHash}
 | 
				
			||||||
 | 
					            installedPacks={installedPacks}
 | 
				
			||||||
 | 
					            isFormattingEnabled={isFormattingEnabled}
 | 
				
			||||||
 | 
					            isFormattingFlagEnabled={isFormattingFlagEnabled}
 | 
				
			||||||
 | 
					            isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
 | 
				
			||||||
            isSending={false}
 | 
					            isSending={false}
 | 
				
			||||||
            onClose={() => setAttachmentToEdit(undefined)}
 | 
					            onClose={() => setAttachmentToEdit(undefined)}
 | 
				
			||||||
            onDone={({ data, contentType, blurHash }) => {
 | 
					            onDone={({
 | 
				
			||||||
 | 
					              caption,
 | 
				
			||||||
 | 
					              captionBodyRanges,
 | 
				
			||||||
 | 
					              data,
 | 
				
			||||||
 | 
					              contentType,
 | 
				
			||||||
 | 
					              blurHash,
 | 
				
			||||||
 | 
					            }) => {
 | 
				
			||||||
              const newAttachment = {
 | 
					              const newAttachment = {
 | 
				
			||||||
                ...attachmentToEdit,
 | 
					                ...attachmentToEdit,
 | 
				
			||||||
                contentType,
 | 
					                contentType,
 | 
				
			||||||
| 
						 | 
					@ -869,9 +889,25 @@ export function CompositionArea({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              addAttachment(conversationId, newAttachment);
 | 
					              addAttachment(conversationId, newAttachment);
 | 
				
			||||||
              setAttachmentToEdit(undefined);
 | 
					              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}
 | 
					            recentStickers={recentStickers}
 | 
				
			||||||
 | 
					            skinTone={skinTone}
 | 
				
			||||||
 | 
					            sortedGroupMembers={sortedGroupMembers}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      <div className="CompositionArea__toggle-large">
 | 
					      <div className="CompositionArea__toggle-large">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ import { BodyRange, collapseRangeTree, insertRange } from '../types/BodyRange';
 | 
				
			||||||
import type { LocalizerType, ThemeType } from '../types/Util';
 | 
					import type { LocalizerType, ThemeType } from '../types/Util';
 | 
				
			||||||
import type { ConversationType } from '../state/ducks/conversations';
 | 
					import type { ConversationType } from '../state/ducks/conversations';
 | 
				
			||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
 | 
					import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
import { MentionBlot } from '../quill/mentions/blot';
 | 
					import { MentionBlot } from '../quill/mentions/blot';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  matchEmojiImage,
 | 
					  matchEmojiImage,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,79 +1,85 @@
 | 
				
			||||||
// Copyright 2021 Signal Messenger, LLC
 | 
					// Copyright 2021 Signal Messenger, LLC
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Meta, Story } from '@storybook/react';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { action } from '@storybook/addon-actions';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { PropsType } from './MediaEditor';
 | 
					import type { PropsType } from './MediaEditor';
 | 
				
			||||||
import { MediaEditor } from './MediaEditor';
 | 
					import { MediaEditor } from './MediaEditor';
 | 
				
			||||||
import enMessages from '../../_locales/en/messages.json';
 | 
					import enMessages from '../../_locales/en/messages.json';
 | 
				
			||||||
import { setupI18n } from '../util/setupI18n';
 | 
					import { setupI18n } from '../util/setupI18n';
 | 
				
			||||||
import { Stickers, installedPacks } from '../test-both/helpers/getStickerPacks';
 | 
					import { Stickers, installedPacks } from '../test-both/helpers/getStickerPacks';
 | 
				
			||||||
import { CompositionTextArea } from './CompositionTextArea';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const i18n = setupI18n('en', enMessages);
 | 
					const i18n = setupI18n('en', enMessages);
 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  title: 'Components/MediaEditor',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const IMAGE_1 = '/fixtures/nathan-anderson-316188-unsplash.jpg';
 | 
					const IMAGE_1 = '/fixtures/nathan-anderson-316188-unsplash.jpg';
 | 
				
			||||||
const IMAGE_2 = '/fixtures/tina-rolf-269345-unsplash.jpg';
 | 
					const IMAGE_2 = '/fixtures/tina-rolf-269345-unsplash.jpg';
 | 
				
			||||||
const IMAGE_3 = '/fixtures/kitten-4-112-112.jpg';
 | 
					const IMAGE_3 = '/fixtures/kitten-4-112-112.jpg';
 | 
				
			||||||
const IMAGE_4 = '/fixtures/snow.jpg';
 | 
					const IMAGE_4 = '/fixtures/snow.jpg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getDefaultProps = (): PropsType => ({
 | 
					export default {
 | 
				
			||||||
  i18n,
 | 
					  title: 'Components/MediaEditor',
 | 
				
			||||||
  imageSrc: IMAGE_2,
 | 
					  component: MediaEditor,
 | 
				
			||||||
  onClose: action('onClose'),
 | 
					  argTypes: {
 | 
				
			||||||
  onDone: action('onDone'),
 | 
					    getPreferredBadge: { action: true },
 | 
				
			||||||
  isSending: false,
 | 
					    i18n: {
 | 
				
			||||||
  imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
 | 
					      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
 | 
					// eslint-disable-next-line react/function-component-definition
 | 
				
			||||||
  installedPacks,
 | 
					const Template: Story<PropsType> = args => <MediaEditor {...args} />;
 | 
				
			||||||
  recentStickers: [Stickers.wide, Stickers.tall, Stickers.abe],
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ExtraLarge(): JSX.Element {
 | 
					export const ExtraLarge = Template.bind({});
 | 
				
			||||||
  return <MediaEditor {...getDefaultProps()} />;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Large(): JSX.Element {
 | 
					export const Large = Template.bind({});
 | 
				
			||||||
  return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_1} />;
 | 
					Large.args = {
 | 
				
			||||||
}
 | 
					  imageSrc: IMAGE_1,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Smol(): JSX.Element {
 | 
					export const Smol = Template.bind({});
 | 
				
			||||||
  return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_3} />;
 | 
					Smol.args = {
 | 
				
			||||||
}
 | 
					  imageSrc: IMAGE_3,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Portrait(): JSX.Element {
 | 
					export const Portrait = Template.bind({});
 | 
				
			||||||
  return <MediaEditor {...getDefaultProps()} imageSrc={IMAGE_4} />;
 | 
					Portrait.args = {
 | 
				
			||||||
}
 | 
					  imageSrc: IMAGE_4,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Sending(): JSX.Element {
 | 
					export const Sending = Template.bind({});
 | 
				
			||||||
  return <MediaEditor {...getDefaultProps()} isSending />;
 | 
					Sending.args = {
 | 
				
			||||||
}
 | 
					  isSending: true,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
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"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,30 +1,26 @@
 | 
				
			||||||
// Copyright 2021 Signal Messenger, LLC
 | 
					// Copyright 2021 Signal Messenger, LLC
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// 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 classNames from 'classnames';
 | 
				
			||||||
import { createPortal } from 'react-dom';
 | 
					import { createPortal } from 'react-dom';
 | 
				
			||||||
import { fabric } from 'fabric';
 | 
					import { fabric } from 'fabric';
 | 
				
			||||||
import { get, has, noop } from 'lodash';
 | 
					import { get, has, noop } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { LocalizerType } from '../types/Util';
 | 
					import type {
 | 
				
			||||||
import { ThemeType } from '../types/Util';
 | 
					  EmojiPickDataType,
 | 
				
			||||||
import type { MIMEType } from '../types/MIME';
 | 
					  Props as EmojiPickerProps,
 | 
				
			||||||
import { IMAGE_PNG } from '../types/MIME';
 | 
					} from './emoji/EmojiPicker';
 | 
				
			||||||
import type { Props as StickerButtonProps } from './stickers/StickerButton';
 | 
					import type { DraftBodyRanges } from '../types/BodyRange';
 | 
				
			||||||
import type { ImageStateType } from '../mediaEditor/ImageStateType';
 | 
					import type { ImageStateType } from '../mediaEditor/ImageStateType';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					  InputApi,
 | 
				
			||||||
import { Button, ButtonVariant } from './Button';
 | 
					  Props as CompositionInputProps,
 | 
				
			||||||
import { ContextMenu } from './ContextMenu';
 | 
					} from './CompositionInput';
 | 
				
			||||||
import { Slider } from './Slider';
 | 
					import type { LocalizerType } from '../types/Util';
 | 
				
			||||||
import { StickerButton } from './stickers/StickerButton';
 | 
					import type { MIMEType } from '../types/MIME';
 | 
				
			||||||
import { Theme } from '../util/theme';
 | 
					import type { Props as StickerButtonProps } from './stickers/StickerButton';
 | 
				
			||||||
import { canvasToBytes } from '../util/canvasToBytes';
 | 
					 | 
				
			||||||
import type { imageToBlurHash } from '../util/imageToBlurHash';
 | 
					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 { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker';
 | 
				
			||||||
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
 | 
					import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
 | 
				
			||||||
| 
						 | 
					@ -35,25 +31,35 @@ import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticke
 | 
				
			||||||
import { fabricEffectListener } from '../mediaEditor/fabricEffectListener';
 | 
					import { fabricEffectListener } from '../mediaEditor/fabricEffectListener';
 | 
				
			||||||
import { getRGBA, getHSL } from '../mediaEditor/util/color';
 | 
					import { getRGBA, getHSL } from '../mediaEditor/util/color';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  TextStyle,
 | 
					 | 
				
			||||||
  getTextStyleAttributes,
 | 
					  getTextStyleAttributes,
 | 
				
			||||||
 | 
					  TextStyle,
 | 
				
			||||||
} from '../mediaEditor/util/getTextStyleAttributes';
 | 
					} from '../mediaEditor/util/getTextStyleAttributes';
 | 
				
			||||||
import { AddCaptionModal } from './AddCaptionModal';
 | 
					
 | 
				
			||||||
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
 | 
					import { Button, ButtonVariant } from './Button';
 | 
				
			||||||
import { Spinner } from './Spinner';
 | 
					import { CompositionInput } from './CompositionInput';
 | 
				
			||||||
import type { HydratedBodyRangesType } from '../types/BodyRange';
 | 
					import { ContextMenu } from './ContextMenu';
 | 
				
			||||||
import { MessageBody } from './conversation/MessageBody';
 | 
					import { EmojiButton } from './emoji/EmojiButton';
 | 
				
			||||||
import { RenderLocation } from './conversation/MessageTextRenderer';
 | 
					import { IMAGE_PNG } from '../types/MIME';
 | 
				
			||||||
import { arrow } from '../util/keyboard';
 | 
					 | 
				
			||||||
import { SizeObserver } from '../hooks/useSizeObserver';
 | 
					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<{
 | 
					export type MediaEditorResultType = Readonly<{
 | 
				
			||||||
  data: Uint8Array;
 | 
					  data: Uint8Array;
 | 
				
			||||||
  contentType: MIMEType;
 | 
					  contentType: MIMEType;
 | 
				
			||||||
  blurHash: string;
 | 
					  blurHash: string;
 | 
				
			||||||
  caption?: string;
 | 
					  caption?: string;
 | 
				
			||||||
  captionBodyRanges?: HydratedBodyRangesType;
 | 
					  captionBodyRanges?: DraftBodyRanges;
 | 
				
			||||||
}>;
 | 
					}>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type PropsType = {
 | 
					export type PropsType = {
 | 
				
			||||||
| 
						 | 
					@ -65,18 +71,20 @@ export type PropsType = {
 | 
				
			||||||
  onClose: () => unknown;
 | 
					  onClose: () => unknown;
 | 
				
			||||||
  onDone: (result: MediaEditorResultType) => unknown;
 | 
					  onDone: (result: MediaEditorResultType) => unknown;
 | 
				
			||||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
 | 
					} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
 | 
				
			||||||
  (
 | 
					  Pick<
 | 
				
			||||||
    | {
 | 
					    CompositionInputProps,
 | 
				
			||||||
        supportsCaption: true;
 | 
					    | 'draftText'
 | 
				
			||||||
        renderCompositionTextArea: (
 | 
					    | 'draftBodyRanges'
 | 
				
			||||||
          props: SmartCompositionTextAreaProps
 | 
					    | 'getPreferredBadge'
 | 
				
			||||||
        ) => JSX.Element;
 | 
					    | 'isFormattingEnabled'
 | 
				
			||||||
      }
 | 
					    | 'isFormattingFlagEnabled'
 | 
				
			||||||
    | {
 | 
					    | 'isFormattingSpoilersFlagEnabled'
 | 
				
			||||||
        supportsCaption?: false;
 | 
					    | 'onPickEmoji'
 | 
				
			||||||
        renderCompositionTextArea?: undefined;
 | 
					    | 'onTextTooLong'
 | 
				
			||||||
      }
 | 
					    | 'platform'
 | 
				
			||||||
  );
 | 
					    | 'sortedGroupMembers'
 | 
				
			||||||
 | 
					  > &
 | 
				
			||||||
 | 
					  EmojiPickerProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const INITIAL_IMAGE_STATE: ImageStateType = {
 | 
					const INITIAL_IMAGE_STATE: ImageStateType = {
 | 
				
			||||||
  angle: 0,
 | 
					  angle: 0,
 | 
				
			||||||
| 
						 | 
					@ -106,6 +114,12 @@ enum DrawTool {
 | 
				
			||||||
  Highlighter = 'Highlighter',
 | 
					  Highlighter = 'Highlighter',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum CropPreset {
 | 
				
			||||||
 | 
					  Freeform = 'Freeform',
 | 
				
			||||||
 | 
					  Square = 'Square',
 | 
				
			||||||
 | 
					  Vertical = 'Vertical',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PendingCropType = {
 | 
					type PendingCropType = {
 | 
				
			||||||
  left: number;
 | 
					  left: number;
 | 
				
			||||||
  top: number;
 | 
					  top: number;
 | 
				
			||||||
| 
						 | 
					@ -128,6 +142,23 @@ export function MediaEditor({
 | 
				
			||||||
  onClose,
 | 
					  onClose,
 | 
				
			||||||
  onDone,
 | 
					  onDone,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // CompositionInput
 | 
				
			||||||
 | 
					  draftText,
 | 
				
			||||||
 | 
					  draftBodyRanges,
 | 
				
			||||||
 | 
					  getPreferredBadge,
 | 
				
			||||||
 | 
					  isFormattingEnabled,
 | 
				
			||||||
 | 
					  isFormattingFlagEnabled,
 | 
				
			||||||
 | 
					  isFormattingSpoilersFlagEnabled,
 | 
				
			||||||
 | 
					  onPickEmoji,
 | 
				
			||||||
 | 
					  onTextTooLong,
 | 
				
			||||||
 | 
					  platform,
 | 
				
			||||||
 | 
					  sortedGroupMembers,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // EmojiPickerProps
 | 
				
			||||||
 | 
					  onSetSkinTone,
 | 
				
			||||||
 | 
					  recentEmojis,
 | 
				
			||||||
 | 
					  skinTone,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // StickerButtonProps
 | 
					  // StickerButtonProps
 | 
				
			||||||
  installedPacks,
 | 
					  installedPacks,
 | 
				
			||||||
  recentStickers,
 | 
					  recentStickers,
 | 
				
			||||||
| 
						 | 
					@ -137,19 +168,39 @@ export function MediaEditor({
 | 
				
			||||||
  const [image, setImage] = useState<HTMLImageElement>(new Image());
 | 
					  const [image, setImage] = useState<HTMLImageElement>(new Image());
 | 
				
			||||||
  const [isStickerPopperOpen, setIsStickerPopperOpen] =
 | 
					  const [isStickerPopperOpen, setIsStickerPopperOpen] =
 | 
				
			||||||
    useState<boolean>(false);
 | 
					    useState<boolean>(false);
 | 
				
			||||||
 | 
					  const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [caption, setCaption] = useState('');
 | 
					  const [caption, setCaption] = useState(draftText ?? '');
 | 
				
			||||||
  const [captionBodyRanges, setCaptionBodyRanges] = useState<
 | 
					  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 canvasId = useUniqueId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [imageState, setImageState] =
 | 
					  const [imageState, setImageState] =
 | 
				
			||||||
    useState<ImageStateType>(INITIAL_IMAGE_STATE);
 | 
					    useState<ImageStateType>(INITIAL_IMAGE_STATE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [cropPreset, setCropPreset] = useState<CropPreset>(CropPreset.Freeform);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // History state
 | 
					  // History state
 | 
				
			||||||
  const { canRedo, canUndo, redoIfPossible, takeSnapshot, undoIfPossible } =
 | 
					  const { canRedo, canUndo, redoIfPossible, takeSnapshot, undoIfPossible } =
 | 
				
			||||||
    useFabricHistory({
 | 
					    useFabricHistory({
 | 
				
			||||||
| 
						 | 
					@ -199,8 +250,8 @@ export function MediaEditor({
 | 
				
			||||||
  const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
 | 
					  const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onTryClose = useCallback(() => {
 | 
					  const onTryClose = useCallback(() => {
 | 
				
			||||||
    confirmDiscardIf(caption !== '' || Boolean(image), onClose);
 | 
					    confirmDiscardIf(canUndo, onClose);
 | 
				
			||||||
  }, [confirmDiscardIf, caption, image, onClose]);
 | 
					  }, [confirmDiscardIf, canUndo, onClose]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Keyboard support
 | 
					  // Keyboard support
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					@ -228,6 +279,12 @@ export function MediaEditor({
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        ev => ev.key === 'Escape',
 | 
					        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
 | 
					          // close window if the user is not in the middle of something
 | 
				
			||||||
          if (editMode === undefined) {
 | 
					          if (editMode === undefined) {
 | 
				
			||||||
            // if the stickers popper is open,
 | 
					            // if the stickers popper is open,
 | 
				
			||||||
| 
						 | 
					@ -377,6 +434,7 @@ export function MediaEditor({
 | 
				
			||||||
  }, [
 | 
					  }, [
 | 
				
			||||||
    fabricCanvas,
 | 
					    fabricCanvas,
 | 
				
			||||||
    editMode,
 | 
					    editMode,
 | 
				
			||||||
 | 
					    isEmojiPopperOpen,
 | 
				
			||||||
    isStickerPopperOpen,
 | 
					    isStickerPopperOpen,
 | 
				
			||||||
    onTryClose,
 | 
					    onTryClose,
 | 
				
			||||||
    redoIfPossible,
 | 
					    redoIfPossible,
 | 
				
			||||||
| 
						 | 
					@ -523,6 +581,40 @@ export function MediaEditor({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [fabricCanvas, sliderValue, textStyle]);
 | 
					  }, [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
 | 
					  // Create the CroppingRect
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (!fabricCanvas) {
 | 
					    if (!fabricCanvas) {
 | 
				
			||||||
| 
						 | 
					@ -632,14 +724,17 @@ export function MediaEditor({
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let tooling: JSX.Element | undefined;
 | 
					  let toolElement: JSX.Element | undefined;
 | 
				
			||||||
  if (editMode === EditMode.Text) {
 | 
					  if (editMode === EditMode.Text) {
 | 
				
			||||||
    tooling = (
 | 
					    toolElement = (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
 | 
					        <div className="MediaEditor__tools-row-1" />
 | 
				
			||||||
 | 
					        <div className="MediaEditor__tools-row-2">
 | 
				
			||||||
 | 
					          <div className="MediaEditor__toolbar">
 | 
				
			||||||
            <Slider
 | 
					            <Slider
 | 
				
			||||||
              handleStyle={{ backgroundColor: getHSL(sliderValue) }}
 | 
					              handleStyle={{ backgroundColor: getHSL(sliderValue) }}
 | 
				
			||||||
              label={i18n('icu:CustomColorEditor__hue')}
 | 
					              label={i18n('icu:CustomColorEditor__hue')}
 | 
				
			||||||
          moduleClassName="HueSlider MediaEditor__tools__tool"
 | 
					              moduleClassName="HueSlider MediaEditor__toolbar__tool"
 | 
				
			||||||
              onChange={setSliderValue}
 | 
					              onChange={setSliderValue}
 | 
				
			||||||
              value={sliderValue}
 | 
					              value={sliderValue}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
| 
						 | 
					@ -665,19 +760,19 @@ export function MediaEditor({
 | 
				
			||||||
                  value: TextStyle.Outline,
 | 
					                  value: TextStyle.Outline,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ]}
 | 
					              ]}
 | 
				
			||||||
          moduleClassName={classNames('MediaEditor__tools__tool', {
 | 
					              moduleClassName={classNames('MediaEditor__toolbar__tool', {
 | 
				
			||||||
            'MediaEditor__tools__button--text-regular':
 | 
					                'MediaEditor__toolbar__button--text-regular':
 | 
				
			||||||
                  textStyle === TextStyle.Regular,
 | 
					                  textStyle === TextStyle.Regular,
 | 
				
			||||||
            'MediaEditor__tools__button--text-highlight':
 | 
					                'MediaEditor__toolbar__button--text-highlight':
 | 
				
			||||||
                  textStyle === TextStyle.Highlight,
 | 
					                  textStyle === TextStyle.Highlight,
 | 
				
			||||||
            'MediaEditor__tools__button--text-outline':
 | 
					                'MediaEditor__toolbar__button--text-outline':
 | 
				
			||||||
                  textStyle === TextStyle.Outline,
 | 
					                  textStyle === TextStyle.Outline,
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
              theme={Theme.Dark}
 | 
					              theme={Theme.Dark}
 | 
				
			||||||
              value={textStyle}
 | 
					              value={textStyle}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        <button
 | 
					          </div>
 | 
				
			||||||
          className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
 | 
					          <Button
 | 
				
			||||||
            onClick={() => {
 | 
					            onClick={() => {
 | 
				
			||||||
              setEditMode(undefined);
 | 
					              setEditMode(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -686,19 +781,24 @@ export function MediaEditor({
 | 
				
			||||||
                activeObject.exitEditing();
 | 
					                activeObject.exitEditing();
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          type="button"
 | 
					            theme={Theme.Dark}
 | 
				
			||||||
 | 
					            variant={ButtonVariant.Secondary}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {i18n('icu:done')}
 | 
					            {i18n('icu:done')}
 | 
				
			||||||
        </button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </>
 | 
					      </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } else if (editMode === EditMode.Draw) {
 | 
					  } else if (editMode === EditMode.Draw) {
 | 
				
			||||||
    tooling = (
 | 
					    toolElement = (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
 | 
					        <div className="MediaEditor__tools-row-1" />
 | 
				
			||||||
 | 
					        <div className="MediaEditor__tools-row-2">
 | 
				
			||||||
 | 
					          <div className="MediaEditor__toolbar">
 | 
				
			||||||
            <Slider
 | 
					            <Slider
 | 
				
			||||||
              handleStyle={{ backgroundColor: getHSL(sliderValue) }}
 | 
					              handleStyle={{ backgroundColor: getHSL(sliderValue) }}
 | 
				
			||||||
              label={i18n('icu:CustomColorEditor__hue')}
 | 
					              label={i18n('icu:CustomColorEditor__hue')}
 | 
				
			||||||
          moduleClassName="HueSlider MediaEditor__tools__tool"
 | 
					              moduleClassName="HueSlider MediaEditor__toolbar__tool"
 | 
				
			||||||
              onChange={setSliderValue}
 | 
					              onChange={setSliderValue}
 | 
				
			||||||
              value={sliderValue}
 | 
					              value={sliderValue}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
| 
						 | 
					@ -718,9 +818,10 @@ export function MediaEditor({
 | 
				
			||||||
                  value: DrawTool.Highlighter,
 | 
					                  value: DrawTool.Highlighter,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ]}
 | 
					              ]}
 | 
				
			||||||
          moduleClassName={classNames('MediaEditor__tools__tool', {
 | 
					              moduleClassName={classNames('MediaEditor__toolbar__tool', {
 | 
				
			||||||
            'MediaEditor__tools__button--draw-pen': drawTool === DrawTool.Pen,
 | 
					                'MediaEditor__toolbar__button--draw-pen':
 | 
				
			||||||
            'MediaEditor__tools__button--draw-highlighter':
 | 
					                  drawTool === DrawTool.Pen,
 | 
				
			||||||
 | 
					                'MediaEditor__toolbar__button--draw-highlighter':
 | 
				
			||||||
                  drawTool === DrawTool.Highlighter,
 | 
					                  drawTool === DrawTool.Highlighter,
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
              theme={Theme.Dark}
 | 
					              theme={Theme.Dark}
 | 
				
			||||||
| 
						 | 
					@ -754,26 +855,28 @@ export function MediaEditor({
 | 
				
			||||||
                  value: DrawWidth.Heavy,
 | 
					                  value: DrawWidth.Heavy,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ]}
 | 
					              ]}
 | 
				
			||||||
          moduleClassName={classNames('MediaEditor__tools__tool', {
 | 
					              moduleClassName={classNames('MediaEditor__toolbar__tool', {
 | 
				
			||||||
            'MediaEditor__tools__button--width-thin':
 | 
					                'MediaEditor__toolbar__button--width-thin':
 | 
				
			||||||
                  drawWidth === DrawWidth.Thin,
 | 
					                  drawWidth === DrawWidth.Thin,
 | 
				
			||||||
            'MediaEditor__tools__button--width-regular':
 | 
					                'MediaEditor__toolbar__button--width-regular':
 | 
				
			||||||
                  drawWidth === DrawWidth.Regular,
 | 
					                  drawWidth === DrawWidth.Regular,
 | 
				
			||||||
            'MediaEditor__tools__button--width-medium':
 | 
					                'MediaEditor__toolbar__button--width-medium':
 | 
				
			||||||
                  drawWidth === DrawWidth.Medium,
 | 
					                  drawWidth === DrawWidth.Medium,
 | 
				
			||||||
            'MediaEditor__tools__button--width-heavy':
 | 
					                'MediaEditor__toolbar__button--width-heavy':
 | 
				
			||||||
                  drawWidth === DrawWidth.Heavy,
 | 
					                  drawWidth === DrawWidth.Heavy,
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
              theme={Theme.Dark}
 | 
					              theme={Theme.Dark}
 | 
				
			||||||
              value={drawWidth}
 | 
					              value={drawWidth}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        <button
 | 
					          </div>
 | 
				
			||||||
          className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
 | 
					          <Button
 | 
				
			||||||
            onClick={() => setEditMode(undefined)}
 | 
					            onClick={() => setEditMode(undefined)}
 | 
				
			||||||
          type="button"
 | 
					            theme={Theme.Dark}
 | 
				
			||||||
 | 
					            variant={ButtonVariant.Secondary}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {i18n('icu:done')}
 | 
					            {i18n('icu:done')}
 | 
				
			||||||
        </button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </>
 | 
					      </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } else if (editMode === EditMode.Crop) {
 | 
					  } else if (editMode === EditMode.Crop) {
 | 
				
			||||||
| 
						 | 
					@ -784,10 +887,51 @@ export function MediaEditor({
 | 
				
			||||||
      imageState.flipY ||
 | 
					      imageState.flipY ||
 | 
				
			||||||
      imageState.angle !== 0;
 | 
					      imageState.angle !== 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tooling = (
 | 
					    toolElement = (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
 | 
					        <div className="MediaEditor__tools-row-1">
 | 
				
			||||||
          <button
 | 
					          <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}
 | 
					            disabled={!canReset}
 | 
				
			||||||
            onClick={async () => {
 | 
					            onClick={async () => {
 | 
				
			||||||
              if (!fabricCanvas) {
 | 
					              if (!fabricCanvas) {
 | 
				
			||||||
| 
						 | 
					@ -800,16 +944,19 @@ export function MediaEditor({
 | 
				
			||||||
                width: image.width,
 | 
					                width: image.width,
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
              setImageState(newImageState);
 | 
					              setImageState(newImageState);
 | 
				
			||||||
 | 
					              setCropPreset(CropPreset.Freeform);
 | 
				
			||||||
              moveFabricObjectsForReset(fabricCanvas, imageState);
 | 
					              moveFabricObjectsForReset(fabricCanvas, imageState);
 | 
				
			||||||
              takeSnapshot('reset', newImageState);
 | 
					              takeSnapshot('reset', newImageState);
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          type="button"
 | 
					            theme={Theme.Dark}
 | 
				
			||||||
 | 
					            variant={ButtonVariant.Secondary}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {i18n('icu:MediaEditor__crop--reset')}
 | 
					            {i18n('icu:MediaEditor__crop--reset')}
 | 
				
			||||||
        </button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <div className="MediaEditor__toolbar">
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              aria-label={i18n('icu:MediaEditor__crop--rotate')}
 | 
					              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={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                if (!fabricCanvas) {
 | 
					                if (!fabricCanvas) {
 | 
				
			||||||
                  return;
 | 
					                  return;
 | 
				
			||||||
| 
						 | 
					@ -845,7 +992,7 @@ export function MediaEditor({
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              aria-label={i18n('icu:MediaEditor__crop--flip')}
 | 
					              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={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                if (!fabricCanvas) {
 | 
					                if (!fabricCanvas) {
 | 
				
			||||||
                  return;
 | 
					                  return;
 | 
				
			||||||
| 
						 | 
					@ -865,8 +1012,8 @@ export function MediaEditor({
 | 
				
			||||||
            <button
 | 
					            <button
 | 
				
			||||||
              aria-label={i18n('icu:MediaEditor__crop--lock')}
 | 
					              aria-label={i18n('icu:MediaEditor__crop--lock')}
 | 
				
			||||||
              className={classNames(
 | 
					              className={classNames(
 | 
				
			||||||
            'MediaEditor__tools__button',
 | 
					                'MediaEditor__toolbar__button',
 | 
				
			||||||
            `MediaEditor__tools__button--crop-${
 | 
					                `MediaEditor__toolbar__button--crop-${
 | 
				
			||||||
                  cropAspectRatioLock ? '' : 'un'
 | 
					                  cropAspectRatioLock ? '' : 'un'
 | 
				
			||||||
                }locked`
 | 
					                }locked`
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
| 
						 | 
					@ -878,10 +1025,14 @@ export function MediaEditor({
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              type="button"
 | 
					              type="button"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        <button
 | 
					          </div>
 | 
				
			||||||
          className="MediaEditor__tools__tool MediaEditor__tools__button MediaEditor__tools__button--words"
 | 
					          <Button
 | 
				
			||||||
          disabled={!canCrop}
 | 
					 | 
				
			||||||
            onClick={() => {
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              if (!canCrop) {
 | 
				
			||||||
 | 
					                setEditMode(undefined);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (!fabricCanvas) {
 | 
					              if (!fabricCanvas) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
| 
						 | 
					@ -899,17 +1050,52 @@ export function MediaEditor({
 | 
				
			||||||
              moveFabricObjectsForCrop(fabricCanvas, pendingCrop);
 | 
					              moveFabricObjectsForCrop(fabricCanvas, pendingCrop);
 | 
				
			||||||
              takeSnapshot('crop', newImageState);
 | 
					              takeSnapshot('crop', newImageState);
 | 
				
			||||||
              setEditMode(undefined);
 | 
					              setEditMode(undefined);
 | 
				
			||||||
 | 
					              setCropPreset(CropPreset.Freeform);
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          type="button"
 | 
					            theme={Theme.Dark}
 | 
				
			||||||
 | 
					            variant={ButtonVariant.Secondary}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {i18n('icu:done')}
 | 
					            {i18n('icu:done')}
 | 
				
			||||||
        </button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </>
 | 
					      </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return createPortal(
 | 
					  return createPortal(
 | 
				
			||||||
    <div className="MediaEditor">
 | 
					    <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">
 | 
					      <div className="MediaEditor__container">
 | 
				
			||||||
        <SizeObserver
 | 
					        <SizeObserver
 | 
				
			||||||
          onSizeChange={size => {
 | 
					          onSizeChange={size => {
 | 
				
			||||||
| 
						 | 
					@ -934,63 +1120,12 @@ export function MediaEditor({
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </SizeObserver>
 | 
					        </SizeObserver>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="MediaEditor__toolbar">
 | 
					      <div className="MediaEditor__tools">
 | 
				
			||||||
        {tooling ? (
 | 
					        {toolElement !== undefined ? (
 | 
				
			||||||
          <div className="MediaEditor__tools">{tooling}</div>
 | 
					          toolElement
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            {props.supportsCaption ? (
 | 
					            <div className="MediaEditor__tools-row-1">
 | 
				
			||||||
              <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">
 | 
					 | 
				
			||||||
              <button
 | 
					              <button
 | 
				
			||||||
                aria-label={i18n('icu:MediaEditor__control--draw')}
 | 
					                aria-label={i18n('icu:MediaEditor__control--draw')}
 | 
				
			||||||
                className={classNames({
 | 
					                className={classNames({
 | 
				
			||||||
| 
						 | 
					@ -1025,6 +1160,29 @@ export function MediaEditor({
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
                type="button"
 | 
					                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
 | 
					              <StickerButton
 | 
				
			||||||
                blessedPacks={[]}
 | 
					                blessedPacks={[]}
 | 
				
			||||||
                className={classNames({
 | 
					                className={classNames({
 | 
				
			||||||
| 
						 | 
					@ -1058,7 +1216,10 @@ export function MediaEditor({
 | 
				
			||||||
                  const sticker = new MediaEditorFabricSticker(src);
 | 
					                  const sticker = new MediaEditorFabricSticker(src);
 | 
				
			||||||
                  sticker.scaleToHeight(size);
 | 
					                  sticker.scaleToHeight(size);
 | 
				
			||||||
                  sticker.setPositionByOrigin(
 | 
					                  sticker.setPositionByOrigin(
 | 
				
			||||||
                  new fabric.Point(imageState.width / 2, imageState.height / 2),
 | 
					                    new fabric.Point(
 | 
				
			||||||
 | 
					                      imageState.width / 2,
 | 
				
			||||||
 | 
					                      imageState.height / 2
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    'center',
 | 
					                    'center',
 | 
				
			||||||
                    'center'
 | 
					                    'center'
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
| 
						 | 
					@ -1120,53 +1281,48 @@ export function MediaEditor({
 | 
				
			||||||
                showPickerHint={false}
 | 
					                showPickerHint={false}
 | 
				
			||||||
                theme={Theme.Dark}
 | 
					                theme={Theme.Dark}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            <button
 | 
					            </div>
 | 
				
			||||||
              aria-label={i18n('icu:MediaEditor__control--crop')}
 | 
					            <div className="MediaEditor__tools-row-2">
 | 
				
			||||||
              className={classNames({
 | 
					              <div className="MediaEditor__tools--input">
 | 
				
			||||||
                MediaEditor__control: true,
 | 
					                <CompositionInput
 | 
				
			||||||
                'MediaEditor__control--crop': true,
 | 
					                  draftText={draftText}
 | 
				
			||||||
                'MediaEditor__control--selected': editMode === EditMode.Crop,
 | 
					                  draftBodyRanges={draftBodyRanges}
 | 
				
			||||||
              })}
 | 
					                  getPreferredBadge={getPreferredBadge}
 | 
				
			||||||
              onClick={() => {
 | 
					                  i18n={i18n}
 | 
				
			||||||
                if (!fabricCanvas) {
 | 
					                  inputApi={inputApiRef}
 | 
				
			||||||
                  return;
 | 
					                  isFormattingEnabled={isFormattingEnabled}
 | 
				
			||||||
                }
 | 
					                  isFormattingFlagEnabled={isFormattingFlagEnabled}
 | 
				
			||||||
                if (editMode === EditMode.Crop) {
 | 
					                  isFormattingSpoilersFlagEnabled={
 | 
				
			||||||
                  const obj = fabricCanvas.getActiveObject();
 | 
					                    isFormattingSpoilersFlagEnabled
 | 
				
			||||||
                  if (obj instanceof MediaEditorFabricCropRect) {
 | 
					 | 
				
			||||||
                    fabricCanvas.remove(obj);
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                  setEditMode(undefined);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                  setEditMode(EditMode.Crop);
 | 
					 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
 | 
					                  moduleClassName="StoryViewsNRepliesModal__input"
 | 
				
			||||||
 | 
					                  onCloseLinkPreview={noop}
 | 
				
			||||||
 | 
					                  onEditorStateChange={({ bodyRanges, messageText }) => {
 | 
				
			||||||
 | 
					                    setCaptionBodyRanges(bodyRanges);
 | 
				
			||||||
 | 
					                    setCaption(messageText);
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
              type="button"
 | 
					                  onPickEmoji={onPickEmoji}
 | 
				
			||||||
            />
 | 
					                  onSubmit={() => {
 | 
				
			||||||
            <button
 | 
					                    inputApiRef.current?.reset();
 | 
				
			||||||
              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"
 | 
					                  onTextTooLong={onTextTooLong}
 | 
				
			||||||
            />
 | 
					                  placeholder="Message"
 | 
				
			||||||
            <button
 | 
					                  platform={platform}
 | 
				
			||||||
              aria-label={i18n('icu:MediaEditor__control--redo')}
 | 
					                  sendCounter={0}
 | 
				
			||||||
              className="MediaEditor__control MediaEditor__control--redo"
 | 
					                  sortedGroupMembers={sortedGroupMembers}
 | 
				
			||||||
              disabled={!canRedo}
 | 
					                  theme={ThemeType.dark}
 | 
				
			||||||
              onClick={() => {
 | 
					                >
 | 
				
			||||||
                if (editMode === EditMode.Crop) {
 | 
					                  <EmojiButton
 | 
				
			||||||
                  setEditMode(undefined);
 | 
					                    className="StoryViewsNRepliesModal__emoji-button"
 | 
				
			||||||
                }
 | 
					                    i18n={i18n}
 | 
				
			||||||
                redoIfPossible();
 | 
					                    onPickEmoji={insertEmoji}
 | 
				
			||||||
              }}
 | 
					                    onOpen={() => setEmojiPopperOpen(true)}
 | 
				
			||||||
              type="button"
 | 
					                    onClose={closeEmojiPickerAndFocusComposer}
 | 
				
			||||||
 | 
					                    recentEmojis={recentEmojis}
 | 
				
			||||||
 | 
					                    skinTone={skinTone}
 | 
				
			||||||
 | 
					                    onSetSkinTone={onSetSkinTone}
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
 | 
					                </CompositionInput>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <Button
 | 
					              <Button
 | 
				
			||||||
                disabled={!image || isSaving || isSending}
 | 
					                disabled={!image || isSaving || isSending}
 | 
				
			||||||
| 
						 | 
					@ -1247,6 +1403,8 @@ export function MediaEditor({
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
              </Button>
 | 
					              </Button>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      {confirmDiscardModal}
 | 
					      {confirmDiscardModal}
 | 
				
			||||||
    </div>,
 | 
					    </div>,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ import type { PropsType as SendStoryModalPropsType } from './SendStoryModal';
 | 
				
			||||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
 | 
					import type { StoryDistributionIdString } from '../types/StoryDistributionId';
 | 
				
			||||||
import type { imageToBlurHash } from '../util/imageToBlurHash';
 | 
					import type { imageToBlurHash } from '../util/imageToBlurHash';
 | 
				
			||||||
import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator';
 | 
					import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator';
 | 
				
			||||||
 | 
					import type { PropsType as MediaEditorPropsType } from './MediaEditor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { TEXT_ATTACHMENT } from '../types/MIME';
 | 
					import { TEXT_ATTACHMENT } from '../types/MIME';
 | 
				
			||||||
import { isVideoAttachment } from '../types/Attachment';
 | 
					import { isVideoAttachment } from '../types/Attachment';
 | 
				
			||||||
| 
						 | 
					@ -24,7 +25,6 @@ import { SendStoryModal } from './SendStoryModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { MediaEditor } from './MediaEditor';
 | 
					import { MediaEditor } from './MediaEditor';
 | 
				
			||||||
import { TextStoryCreator } from './TextStoryCreator';
 | 
					import { TextStoryCreator } from './TextStoryCreator';
 | 
				
			||||||
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
 | 
					 | 
				
			||||||
import type { DraftBodyRanges } from '../types/BodyRange';
 | 
					import type { DraftBodyRanges } from '../types/BodyRange';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function usePortalElement(testid: string): HTMLDivElement | null {
 | 
					function usePortalElement(testid: string): HTMLDivElement | null {
 | 
				
			||||||
| 
						 | 
					@ -63,9 +63,6 @@ export type PropsType = {
 | 
				
			||||||
  processAttachment: (
 | 
					  processAttachment: (
 | 
				
			||||||
    file: File
 | 
					    file: File
 | 
				
			||||||
  ) => Promise<void | InMemoryAttachmentDraftType>;
 | 
					  ) => Promise<void | InMemoryAttachmentDraftType>;
 | 
				
			||||||
  renderCompositionTextArea: (
 | 
					 | 
				
			||||||
    props: SmartCompositionTextAreaProps
 | 
					 | 
				
			||||||
  ) => JSX.Element;
 | 
					 | 
				
			||||||
  sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
 | 
					  sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
 | 
				
			||||||
  theme: ThemeType;
 | 
					  theme: ThemeType;
 | 
				
			||||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
 | 
					} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
 | 
				
			||||||
| 
						 | 
					@ -96,6 +93,15 @@ export type PropsType = {
 | 
				
			||||||
  Pick<
 | 
					  Pick<
 | 
				
			||||||
    TextStoryCreatorPropsType,
 | 
					    TextStoryCreatorPropsType,
 | 
				
			||||||
    'onUseEmoji' | 'skinTone' | 'onSetSkinTone' | 'recentEmojis'
 | 
					    'onUseEmoji' | 'skinTone' | 'onSetSkinTone' | 'recentEmojis'
 | 
				
			||||||
 | 
					  > &
 | 
				
			||||||
 | 
					  Pick<
 | 
				
			||||||
 | 
					    MediaEditorPropsType,
 | 
				
			||||||
 | 
					    | 'isFormattingEnabled'
 | 
				
			||||||
 | 
					    | 'isFormattingFlagEnabled'
 | 
				
			||||||
 | 
					    | 'isFormattingSpoilersFlagEnabled'
 | 
				
			||||||
 | 
					    | 'onPickEmoji'
 | 
				
			||||||
 | 
					    | 'onTextTooLong'
 | 
				
			||||||
 | 
					    | 'platform'
 | 
				
			||||||
  >;
 | 
					  >;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function StoryCreator({
 | 
					export function StoryCreator({
 | 
				
			||||||
| 
						 | 
					@ -110,6 +116,9 @@ export function StoryCreator({
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
  imageToBlurHash,
 | 
					  imageToBlurHash,
 | 
				
			||||||
  installedPacks,
 | 
					  installedPacks,
 | 
				
			||||||
 | 
					  isFormattingEnabled,
 | 
				
			||||||
 | 
					  isFormattingFlagEnabled,
 | 
				
			||||||
 | 
					  isFormattingSpoilersFlagEnabled,
 | 
				
			||||||
  isSending,
 | 
					  isSending,
 | 
				
			||||||
  linkPreview,
 | 
					  linkPreview,
 | 
				
			||||||
  me,
 | 
					  me,
 | 
				
			||||||
| 
						 | 
					@ -118,19 +127,21 @@ export function StoryCreator({
 | 
				
			||||||
  onDeleteList,
 | 
					  onDeleteList,
 | 
				
			||||||
  onDistributionListCreated,
 | 
					  onDistributionListCreated,
 | 
				
			||||||
  onHideMyStoriesFrom,
 | 
					  onHideMyStoriesFrom,
 | 
				
			||||||
 | 
					  onMediaPlaybackStart,
 | 
				
			||||||
 | 
					  onPickEmoji,
 | 
				
			||||||
  onRemoveMembers,
 | 
					  onRemoveMembers,
 | 
				
			||||||
  onRepliesNReactionsChanged,
 | 
					  onRepliesNReactionsChanged,
 | 
				
			||||||
  onSelectedStoryList,
 | 
					  onSelectedStoryList,
 | 
				
			||||||
  onSend,
 | 
					  onSend,
 | 
				
			||||||
  onSetSkinTone,
 | 
					  onSetSkinTone,
 | 
				
			||||||
 | 
					  onTextTooLong,
 | 
				
			||||||
  onUseEmoji,
 | 
					  onUseEmoji,
 | 
				
			||||||
  onViewersUpdated,
 | 
					  onViewersUpdated,
 | 
				
			||||||
  onMediaPlaybackStart,
 | 
					 | 
				
			||||||
  ourConversationId,
 | 
					  ourConversationId,
 | 
				
			||||||
 | 
					  platform,
 | 
				
			||||||
  processAttachment,
 | 
					  processAttachment,
 | 
				
			||||||
  recentEmojis,
 | 
					  recentEmojis,
 | 
				
			||||||
  recentStickers,
 | 
					  recentStickers,
 | 
				
			||||||
  renderCompositionTextArea,
 | 
					 | 
				
			||||||
  sendStoryModalOpenStateChanged,
 | 
					  sendStoryModalOpenStateChanged,
 | 
				
			||||||
  setMyStoriesToAllSignalConnections,
 | 
					  setMyStoriesToAllSignalConnections,
 | 
				
			||||||
  signalConnections,
 | 
					  signalConnections,
 | 
				
			||||||
| 
						 | 
					@ -234,17 +245,19 @@ export function StoryCreator({
 | 
				
			||||||
              toggleSignalConnectionsModal={toggleSignalConnectionsModal}
 | 
					              toggleSignalConnectionsModal={toggleSignalConnectionsModal}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          {draftAttachment && !isReadyToSend && attachmentUrl && (
 | 
					          {draftAttachment && attachmentUrl && (
 | 
				
			||||||
            <MediaEditor
 | 
					            <MediaEditor
 | 
				
			||||||
              doneButtonLabel={i18n('icu:next2')}
 | 
					              doneButtonLabel={i18n('icu:next2')}
 | 
				
			||||||
 | 
					              getPreferredBadge={getPreferredBadge}
 | 
				
			||||||
              i18n={i18n}
 | 
					              i18n={i18n}
 | 
				
			||||||
              imageSrc={attachmentUrl}
 | 
					              imageSrc={attachmentUrl}
 | 
				
			||||||
 | 
					              imageToBlurHash={imageToBlurHash}
 | 
				
			||||||
              installedPacks={installedPacks}
 | 
					              installedPacks={installedPacks}
 | 
				
			||||||
 | 
					              isFormattingEnabled={isFormattingEnabled}
 | 
				
			||||||
 | 
					              isFormattingFlagEnabled={isFormattingFlagEnabled}
 | 
				
			||||||
 | 
					              isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
 | 
				
			||||||
              isSending={isSending}
 | 
					              isSending={isSending}
 | 
				
			||||||
              onClose={onClose}
 | 
					              onClose={onClose}
 | 
				
			||||||
              supportsCaption
 | 
					 | 
				
			||||||
              renderCompositionTextArea={renderCompositionTextArea}
 | 
					 | 
				
			||||||
              imageToBlurHash={imageToBlurHash}
 | 
					 | 
				
			||||||
              onDone={({
 | 
					              onDone={({
 | 
				
			||||||
                contentType,
 | 
					                contentType,
 | 
				
			||||||
                data,
 | 
					                data,
 | 
				
			||||||
| 
						 | 
					@ -263,7 +276,11 @@ export function StoryCreator({
 | 
				
			||||||
                setBodyRanges(captionBodyRanges);
 | 
					                setBodyRanges(captionBodyRanges);
 | 
				
			||||||
                setIsReadyToSend(true);
 | 
					                setIsReadyToSend(true);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
 | 
					              onPickEmoji={onPickEmoji}
 | 
				
			||||||
 | 
					              onTextTooLong={onTextTooLong}
 | 
				
			||||||
 | 
					              platform={platform}
 | 
				
			||||||
              recentStickers={recentStickers}
 | 
					              recentStickers={recentStickers}
 | 
				
			||||||
 | 
					              skinTone={skinTone}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          {!file && (
 | 
					          {!file && (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ export type OwnProps = Readonly<{
 | 
				
			||||||
  emoji?: string;
 | 
					  emoji?: string;
 | 
				
			||||||
  i18n: LocalizerType;
 | 
					  i18n: LocalizerType;
 | 
				
			||||||
  onClose?: () => unknown;
 | 
					  onClose?: () => unknown;
 | 
				
			||||||
 | 
					  onOpen?: () => unknown;
 | 
				
			||||||
  emojiButtonApi?: MutableRefObject<EmojiButtonAPI | undefined>;
 | 
					  emojiButtonApi?: MutableRefObject<EmojiButtonAPI | undefined>;
 | 
				
			||||||
  variant?: EmojiButtonVariant;
 | 
					  variant?: EmojiButtonVariant;
 | 
				
			||||||
}>;
 | 
					}>;
 | 
				
			||||||
| 
						 | 
					@ -47,6 +48,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
  doSend,
 | 
					  doSend,
 | 
				
			||||||
  onClose,
 | 
					  onClose,
 | 
				
			||||||
 | 
					  onOpen,
 | 
				
			||||||
  onPickEmoji,
 | 
					  onPickEmoji,
 | 
				
			||||||
  skinTone,
 | 
					  skinTone,
 | 
				
			||||||
  onSetSkinTone,
 | 
					  onSetSkinTone,
 | 
				
			||||||
| 
						 | 
					@ -58,6 +60,13 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
 | 
				
			||||||
  const popperRef = React.useRef<HTMLDivElement | null>(null);
 | 
					  const popperRef = React.useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
  const refMerger = useRefMerger();
 | 
					  const refMerger = useRefMerger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    if (!open) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    onOpen?.();
 | 
				
			||||||
 | 
					  }, [open, onOpen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClickButton = React.useCallback(() => {
 | 
					  const handleClickButton = React.useCallback(() => {
 | 
				
			||||||
    if (open) {
 | 
					    if (open) {
 | 
				
			||||||
      setOpen(false);
 | 
					      setOpen(false);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,10 +77,10 @@ import type { AvatarDataType } from './types/Avatar';
 | 
				
			||||||
import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
 | 
					import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ServiceIdKind,
 | 
					  ServiceIdKind,
 | 
				
			||||||
  isAciString,
 | 
					 | 
				
			||||||
  isPniString,
 | 
					  isPniString,
 | 
				
			||||||
  isServiceIdString,
 | 
					  isServiceIdString,
 | 
				
			||||||
} from './types/ServiceId';
 | 
					} from './types/ServiceId';
 | 
				
			||||||
 | 
					import { isAciString } from './util/isAciString';
 | 
				
			||||||
import * as Errors from './types/errors';
 | 
					import * as Errors from './types/errors';
 | 
				
			||||||
import { SignalService as Proto } from './protobuf';
 | 
					import { SignalService as Proto } from './protobuf';
 | 
				
			||||||
import { isNotNil } from './util/isNotNil';
 | 
					import { isNotNil } from './util/isNotNil';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import { assertDev } from '../../util/assert';
 | 
				
			||||||
import { isDirectConversation } from '../../util/whatTypeOfConversation';
 | 
					import { isDirectConversation } from '../../util/whatTypeOfConversation';
 | 
				
			||||||
import * as log from '../../logging/log';
 | 
					import * as log from '../../logging/log';
 | 
				
			||||||
import type { ConversationAttributesType } from '../../model-types.d';
 | 
					import type { ConversationAttributesType } from '../../model-types.d';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import type { reportSpamJobQueue } from '../reportSpamJobQueue';
 | 
					import type { reportSpamJobQueue } from '../reportSpamJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function addReportSpamJob({
 | 
					export async function addReportSpamJob({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ import { isConversationAccepted } from '../../util/isConversationAccepted';
 | 
				
			||||||
import { sendToGroup } from '../../util/sendToGroup';
 | 
					import { sendToGroup } from '../../util/sendToGroup';
 | 
				
			||||||
import type { DurationInSeconds } from '../../util/durations';
 | 
					import type { DurationInSeconds } from '../../util/durations';
 | 
				
			||||||
import type { ServiceIdString } from '../../types/ServiceId';
 | 
					import type { ServiceIdString } from '../../types/ServiceId';
 | 
				
			||||||
import { normalizeAci } from '../../types/ServiceId';
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
import * as Bytes from '../../Bytes';
 | 
					import * as Bytes from '../../Bytes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LONG_ATTACHMENT_LIMIT = 2048;
 | 
					const LONG_ATTACHMENT_LIMIT = 2048;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ import { ourProfileKeyService } from '../../services/ourProfileKey';
 | 
				
			||||||
import { canReact, isStory } from '../../state/selectors/message';
 | 
					import { canReact, isStory } from '../../state/selectors/message';
 | 
				
			||||||
import { findAndFormatContact } from '../../util/findAndFormatContact';
 | 
					import { findAndFormatContact } from '../../util/findAndFormatContact';
 | 
				
			||||||
import type { AciString, ServiceIdString } from '../../types/ServiceId';
 | 
					import type { AciString, ServiceIdString } from '../../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import { handleMultipleSendErrors } from './handleMultipleSendErrors';
 | 
					import { handleMultipleSendErrors } from './handleMultipleSendErrors';
 | 
				
			||||||
import { incrementMessageCounter } from '../../util/incrementMessageCounter';
 | 
					import { incrementMessageCounter } from '../../util/incrementMessageCounter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
import { chunk } from 'lodash';
 | 
					import { chunk } from 'lodash';
 | 
				
			||||||
import type { LoggerType } from '../../types/Logging';
 | 
					import type { LoggerType } from '../../types/Logging';
 | 
				
			||||||
import type { AciString } from '../../types/ServiceId';
 | 
					import type { AciString } from '../../types/ServiceId';
 | 
				
			||||||
import { normalizeAci } from '../../types/ServiceId';
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
import { getSendOptions } from '../../util/getSendOptions';
 | 
					import { getSendOptions } from '../../util/getSendOptions';
 | 
				
			||||||
import type { SendTypesType } from '../../util/handleMessageSend';
 | 
					import type { SendTypesType } from '../../util/handleMessageSend';
 | 
				
			||||||
import { handleMessageSend } from '../../util/handleMessageSend';
 | 
					import { handleMessageSend } from '../../util/handleMessageSend';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,26 +29,19 @@ export class MediaEditorFabricCropRect extends fabric.Rect {
 | 
				
			||||||
    const canvasHeight = this.canvas.getHeight();
 | 
					    const canvasHeight = this.canvas.getHeight();
 | 
				
			||||||
    const canvasWidth = this.canvas.getWidth();
 | 
					    const canvasWidth = this.canvas.getWidth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (height > canvasHeight || width > canvasWidth) {
 | 
					    const nextLeft = clamp(
 | 
				
			||||||
      this.canvas.discardActiveObject();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this.set(
 | 
					 | 
				
			||||||
        'left',
 | 
					 | 
				
			||||||
        clamp(
 | 
					 | 
				
			||||||
      left / zoom,
 | 
					      left / zoom,
 | 
				
			||||||
      MediaEditorFabricCropRect.PADDING / zoom,
 | 
					      MediaEditorFabricCropRect.PADDING / zoom,
 | 
				
			||||||
      (canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom
 | 
					      (canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
      this.set(
 | 
					    const nextTop = clamp(
 | 
				
			||||||
        'top',
 | 
					 | 
				
			||||||
        clamp(
 | 
					 | 
				
			||||||
      top / zoom,
 | 
					      top / zoom,
 | 
				
			||||||
      MediaEditorFabricCropRect.PADDING / zoom,
 | 
					      MediaEditorFabricCropRect.PADDING / zoom,
 | 
				
			||||||
      (canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom
 | 
					      (canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
 | 
					    this.set('left', nextLeft);
 | 
				
			||||||
 | 
					    this.set('top', nextTop);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setCoords();
 | 
					    this.setCoords();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,10 +67,10 @@ import { IMAGE_JPEG, IMAGE_WEBP } from '../types/MIME';
 | 
				
			||||||
import type { AciString, PniString, ServiceIdString } from '../types/ServiceId';
 | 
					import type { AciString, PniString, ServiceIdString } from '../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ServiceIdKind,
 | 
					  ServiceIdKind,
 | 
				
			||||||
  isAciString,
 | 
					 | 
				
			||||||
  normalizeServiceId,
 | 
					  normalizeServiceId,
 | 
				
			||||||
  normalizePni,
 | 
					  normalizePni,
 | 
				
			||||||
} from '../types/ServiceId';
 | 
					} from '../types/ServiceId';
 | 
				
			||||||
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  constantTimeEqual,
 | 
					  constantTimeEqual,
 | 
				
			||||||
  decryptProfile,
 | 
					  decryptProfile,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,8 @@ import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { ReactionType } from '../types/Reactions';
 | 
					import type { ReactionType } from '../types/Reactions';
 | 
				
			||||||
import type { ServiceIdString } from '../types/ServiceId';
 | 
					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 reactionUtil from '../reactions/util';
 | 
				
			||||||
import * as Stickers from '../types/Stickers';
 | 
					import * as Stickers from '../types/Stickers';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import { get } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { ConversationType } from '../state/ducks/conversations';
 | 
					import type { ConversationType } from '../state/ducks/conversations';
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import type { AciString } from '../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
import { filter, map } from '../util/iterables';
 | 
					import { filter, map } from '../util/iterables';
 | 
				
			||||||
import { removeDiacritics } from '../util/removeDiacritics';
 | 
					import { removeDiacritics } from '../util/removeDiacritics';
 | 
				
			||||||
import { isNotNil } from '../util/isNotNil';
 | 
					import { isNotNil } from '../util/isNotNil';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ import Parchment from 'parchment';
 | 
				
			||||||
import Quill from 'quill';
 | 
					import Quill from 'quill';
 | 
				
			||||||
import { render } from 'react-dom';
 | 
					import { render } from 'react-dom';
 | 
				
			||||||
import { Emojify } from '../../components/conversation/Emojify';
 | 
					import { Emojify } from '../../components/conversation/Emojify';
 | 
				
			||||||
import { normalizeAci } from '../../types/ServiceId';
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
import type { MentionBlotValue } from '../util';
 | 
					import type { MentionBlotValue } from '../util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare class QuillEmbed extends Parchment.Embed {
 | 
					declare class QuillEmbed extends Parchment.Embed {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import type { RefObject } from 'react';
 | 
				
			||||||
import type { Matcher, AttributeMap } from 'quill';
 | 
					import type { Matcher, AttributeMap } from 'quill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { assertDev } from '../../util/assert';
 | 
					import { assertDev } from '../../util/assert';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import type { MemberRepository } from '../memberRepository';
 | 
					import type { MemberRepository } from '../memberRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const matchMention: (
 | 
					export const matchMention: (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ import { isDirectConversation } from '../util/whatTypeOfConversation';
 | 
				
			||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
 | 
					import { incrementMessageCounter } from '../util/incrementMessageCounter';
 | 
				
			||||||
import { repeat, zipObject } from '../util/iterables';
 | 
					import { repeat, zipObject } from '../util/iterables';
 | 
				
			||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
 | 
					import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
import { SendStatus } from '../messages/MessageSendState';
 | 
					import { SendStatus } from '../messages/MessageSendState';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,7 +70,8 @@ import {
 | 
				
			||||||
  findBestMatchingCameraId,
 | 
					  findBestMatchingCameraId,
 | 
				
			||||||
} from '../calling/findBestMatchingDevice';
 | 
					} from '../calling/findBestMatchingDevice';
 | 
				
			||||||
import type { LocalizerType } from '../types/Util';
 | 
					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 * as Errors from '../types/errors';
 | 
				
			||||||
import type { ConversationModel } from '../models/conversations';
 | 
					import type { ConversationModel } from '../models/conversations';
 | 
				
			||||||
import * as Bytes from '../Bytes';
 | 
					import * as Bytes from '../Bytes';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import PQueue from 'p-queue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { ContactSyncEvent } from '../textsecure/messageReceiverEvents';
 | 
					import type { ContactSyncEvent } from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
import type { ModifiedContactDetails } from '../textsecure/ContactsParser';
 | 
					import type { ModifiedContactDetails } from '../textsecure/ContactsParser';
 | 
				
			||||||
import { normalizeAci } from '../types/ServiceId';
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
import * as Conversation from '../types/Conversation';
 | 
					import * as Conversation from '../types/Conversation';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
import type { ValidateConversationType } from '../model-types.d';
 | 
					import type { ValidateConversationType } from '../model-types.d';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,10 +47,10 @@ import type { StoryDistributionIdString } from '../types/StoryDistributionId';
 | 
				
			||||||
import type { ServiceIdString } from '../types/ServiceId';
 | 
					import type { ServiceIdString } from '../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  normalizeServiceId,
 | 
					  normalizeServiceId,
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizePni,
 | 
					  normalizePni,
 | 
				
			||||||
  ServiceIdKind,
 | 
					  ServiceIdKind,
 | 
				
			||||||
} from '../types/ServiceId';
 | 
					} from '../types/ServiceId';
 | 
				
			||||||
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
import * as Stickers from '../types/Stickers';
 | 
					import * as Stickers from '../types/Stickers';
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  StoryDistributionWithMembersType,
 | 
					  StoryDistributionWithMembersType,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import { omit } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { LoggerType } from '../../types/Logging';
 | 
					import type { LoggerType } from '../../types/Logging';
 | 
				
			||||||
import type { AciString, ServiceIdString } from '../../types/ServiceId';
 | 
					import type { AciString, ServiceIdString } from '../../types/ServiceId';
 | 
				
			||||||
import { normalizeAci } from '../../types/ServiceId';
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
import { isNotNil } from '../../util/isNotNil';
 | 
					import { isNotNil } from '../../util/isNotNil';
 | 
				
			||||||
import { assertDev } from '../../util/assert';
 | 
					import { assertDev } from '../../util/assert';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,11 +10,8 @@ import type {
 | 
				
			||||||
  AciString,
 | 
					  AciString,
 | 
				
			||||||
  PniString,
 | 
					  PniString,
 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					} from '../../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import { normalizeServiceId, normalizePni } from '../../types/ServiceId';
 | 
				
			||||||
  normalizeServiceId,
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizePni,
 | 
					 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					 | 
				
			||||||
import type { JSONWithUnknownFields } from '../../types/Util';
 | 
					import type { JSONWithUnknownFields } from '../../types/Util';
 | 
				
			||||||
import { isNotNil } from '../../util/isNotNil';
 | 
					import { isNotNil } from '../../util/isNotNil';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ import { CallMode } from '../../types/Calling';
 | 
				
			||||||
import type { MessageType, ConversationType } from '../Interface';
 | 
					import type { MessageType, ConversationType } from '../Interface';
 | 
				
			||||||
import { strictAssert } from '../../util/assert';
 | 
					import { strictAssert } from '../../util/assert';
 | 
				
			||||||
import { missingCaseError } from '../../util/missingCaseError';
 | 
					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
 | 
					// Legacy type for calls that never had a call id
 | 
				
			||||||
type DirectCallHistoryDetailsType = {
 | 
					type DirectCallHistoryDetailsType = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,7 +70,7 @@ import type {
 | 
				
			||||||
  AciString,
 | 
					  AciString,
 | 
				
			||||||
  PniString,
 | 
					  PniString,
 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					} from '../../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import { MY_STORY_ID, StorySendMode } from '../../types/Stories';
 | 
					import { MY_STORY_ID, StorySendMode } from '../../types/Stories';
 | 
				
			||||||
import * as Errors from '../../types/errors';
 | 
					import * as Errors from '../../types/errors';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ import type { StoryViewTargetType, StoryViewType } from '../../types/Stories';
 | 
				
			||||||
import type { SyncType } from '../../jobs/helpers/syncHelpers';
 | 
					import type { SyncType } from '../../jobs/helpers/syncHelpers';
 | 
				
			||||||
import type { StoryDistributionIdString } from '../../types/StoryDistributionId';
 | 
					import type { StoryDistributionIdString } from '../../types/StoryDistributionId';
 | 
				
			||||||
import type { ServiceIdString } from '../../types/ServiceId';
 | 
					import type { ServiceIdString } from '../../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import * as log from '../../logging/log';
 | 
					import * as log from '../../logging/log';
 | 
				
			||||||
import { TARGETED_CONVERSATION_CHANGED } from './conversations';
 | 
					import { TARGETED_CONVERSATION_CHANGED } from './conversations';
 | 
				
			||||||
import { SIGNAL_ACI } from '../../types/SignalConversation';
 | 
					import { SIGNAL_ACI } from '../../types/SignalConversation';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import type {
 | 
				
			||||||
  ActiveCallType,
 | 
					  ActiveCallType,
 | 
				
			||||||
  GroupCallRemoteParticipantType,
 | 
					  GroupCallRemoteParticipantType,
 | 
				
			||||||
} from '../../types/Calling';
 | 
					} from '../../types/Calling';
 | 
				
			||||||
import { isAciString } from '../../types/ServiceId';
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
import type { AciString } from '../../types/ServiceId';
 | 
					import type { AciString } from '../../types/ServiceId';
 | 
				
			||||||
import { CallMode, CallState } from '../../types/Calling';
 | 
					import { CallMode, CallState } from '../../types/Calling';
 | 
				
			||||||
import type { StateType } from '../reducer';
 | 
					import type { StateType } from '../reducer';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,10 @@ import { mapDispatchToProps } from '../actions';
 | 
				
			||||||
import type { Props as ComponentPropsType } from '../../components/CompositionArea';
 | 
					import type { Props as ComponentPropsType } from '../../components/CompositionArea';
 | 
				
			||||||
import { CompositionArea } from '../../components/CompositionArea';
 | 
					import { CompositionArea } from '../../components/CompositionArea';
 | 
				
			||||||
import type { StateType } from '../reducer';
 | 
					import type { StateType } from '../reducer';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  DraftBodyRanges,
 | 
				
			||||||
 | 
					  HydratedBodyRangesType,
 | 
				
			||||||
 | 
					} from '../../types/BodyRange';
 | 
				
			||||||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
 | 
					import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
 | 
				
			||||||
import { dropNull } from '../../util/dropNull';
 | 
					import { dropNull } from '../../util/dropNull';
 | 
				
			||||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
 | 
					import { imageToBlurHash } from '../../util/imageToBlurHash';
 | 
				
			||||||
| 
						 | 
					@ -53,7 +57,7 @@ import type { SmartCompositionRecordingProps } from './CompositionRecording';
 | 
				
			||||||
import { SmartCompositionRecording } from './CompositionRecording';
 | 
					import { SmartCompositionRecording } from './CompositionRecording';
 | 
				
			||||||
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
 | 
					import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
 | 
				
			||||||
import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft';
 | 
					import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft';
 | 
				
			||||||
import { BodyRange } from '../../types/BodyRange';
 | 
					import { hydrateRanges } from '../../types/BodyRange';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ExternalProps = {
 | 
					type ExternalProps = {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
| 
						 | 
					@ -133,6 +137,12 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const lastEditableMessageId = getLastEditableMessageId(state);
 | 
					  const lastEditableMessageId = getLastEditableMessageId(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const convertDraftBodyRangesIntoHydrated = (
 | 
				
			||||||
 | 
					    bodyRanges: DraftBodyRanges | undefined
 | 
				
			||||||
 | 
					  ): HydratedBodyRangesType | undefined => {
 | 
				
			||||||
 | 
					    return hydrateRanges(bodyRanges, conversationSelector);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    // Base
 | 
					    // Base
 | 
				
			||||||
    conversationId: id,
 | 
					    conversationId: id,
 | 
				
			||||||
| 
						 | 
					@ -150,6 +160,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
 | 
				
			||||||
    sendCounter,
 | 
					    sendCounter,
 | 
				
			||||||
    shouldHidePopovers,
 | 
					    shouldHidePopovers,
 | 
				
			||||||
    theme: getTheme(state),
 | 
					    theme: getTheme(state),
 | 
				
			||||||
 | 
					    convertDraftBodyRangesIntoHydrated,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // AudioCapture
 | 
					    // AudioCapture
 | 
				
			||||||
    errorDialogAudioRecorderType:
 | 
					    errorDialogAudioRecorderType:
 | 
				
			||||||
| 
						 | 
					@ -204,19 +215,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
 | 
				
			||||||
    groupAdmins: getGroupAdminsSelector(state)(conversation.id),
 | 
					    groupAdmins: getGroupAdminsSelector(state)(conversation.id),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    draftText: dropNull(draftText),
 | 
					    draftText: dropNull(draftText),
 | 
				
			||||||
    draftBodyRanges: draftBodyRanges?.map(bodyRange => {
 | 
					    draftBodyRanges: hydrateRanges(draftBodyRanges, conversationSelector),
 | 
				
			||||||
      if (BodyRange.isMention(bodyRange)) {
 | 
					 | 
				
			||||||
        const mentionConvo = conversationSelector(bodyRange.mentionAci);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          ...bodyRange,
 | 
					 | 
				
			||||||
          conversationID: mentionConvo.id,
 | 
					 | 
				
			||||||
          replacementText: mentionConvo.title,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return bodyRange;
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    renderSmartCompositionRecording: (
 | 
					    renderSmartCompositionRecording: (
 | 
				
			||||||
      recProps: SmartCompositionRecordingProps
 | 
					      recProps: SmartCompositionRecordingProps
 | 
				
			||||||
    ) => {
 | 
					    ) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
 | 
				
			||||||
import { ThemeType, type LocalizerType } from '../../types/Util';
 | 
					import { ThemeType, type LocalizerType } from '../../types/Util';
 | 
				
			||||||
import type { StateType } from '../reducer';
 | 
					import type { StateType } from '../reducer';
 | 
				
			||||||
import { LinkPreviewSourceType } from '../../types/LinkPreview';
 | 
					import { LinkPreviewSourceType } from '../../types/LinkPreview';
 | 
				
			||||||
import { SmartCompositionTextArea } from './CompositionTextArea';
 | 
					 | 
				
			||||||
import { StoryCreator } from '../../components/StoryCreator';
 | 
					import { StoryCreator } from '../../components/StoryCreator';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getAllSignalConnections,
 | 
					  getAllSignalConnections,
 | 
				
			||||||
| 
						 | 
					@ -18,29 +17,35 @@ import {
 | 
				
			||||||
  selectMostRecentActiveStoryTimestampByGroupOrDistributionList,
 | 
					  selectMostRecentActiveStoryTimestampByGroupOrDistributionList,
 | 
				
			||||||
} from '../selectors/conversations';
 | 
					} from '../selectors/conversations';
 | 
				
			||||||
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
 | 
					import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
 | 
				
			||||||
import { getIntl, getUserConversationId } from '../selectors/user';
 | 
					import { getIntl, getPlatform, getUserConversationId } from '../selectors/user';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getInstalledStickerPacks,
 | 
					  getInstalledStickerPacks,
 | 
				
			||||||
  getRecentStickers,
 | 
					  getRecentStickers,
 | 
				
			||||||
} from '../selectors/stickers';
 | 
					} from '../selectors/stickers';
 | 
				
			||||||
import { getAddStoryData } from '../selectors/stories';
 | 
					import { getAddStoryData } from '../selectors/stories';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getEmojiSkinTone,
 | 
					  getIsFormattingFlagEnabled,
 | 
				
			||||||
  getHasSetMyStoriesPrivacy,
 | 
					  getIsFormattingSpoilersFlagEnabled,
 | 
				
			||||||
} from '../selectors/items';
 | 
					} from '../selectors/composer';
 | 
				
			||||||
import { getLinkPreview } from '../selectors/linkPreviews';
 | 
					import { getLinkPreview } from '../selectors/linkPreviews';
 | 
				
			||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
 | 
					import { getPreferredBadgeSelector } from '../selectors/badges';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getEmojiSkinTone,
 | 
				
			||||||
 | 
					  getHasSetMyStoriesPrivacy,
 | 
				
			||||||
 | 
					  getTextFormattingEnabled,
 | 
				
			||||||
 | 
					} from '../selectors/items';
 | 
				
			||||||
import { imageToBlurHash } from '../../util/imageToBlurHash';
 | 
					import { imageToBlurHash } from '../../util/imageToBlurHash';
 | 
				
			||||||
import { processAttachment } from '../../util/processAttachment';
 | 
					import { processAttachment } from '../../util/processAttachment';
 | 
				
			||||||
import { useConversationsActions } from '../ducks/conversations';
 | 
					 | 
				
			||||||
import { useActions as useEmojisActions } from '../ducks/emojis';
 | 
					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 { useGlobalModalActions } from '../ducks/globalModals';
 | 
				
			||||||
import { useItemsActions } from '../ducks/items';
 | 
					import { useItemsActions } from '../ducks/items';
 | 
				
			||||||
import { useLinkPreviewActions } from '../ducks/linkPreviews';
 | 
					import { useLinkPreviewActions } from '../ducks/linkPreviews';
 | 
				
			||||||
import { useRecentEmojis } from '../selectors/emojis';
 | 
					import { useRecentEmojis } from '../selectors/emojis';
 | 
				
			||||||
import { useStoriesActions } from '../ducks/stories';
 | 
					import { useStoriesActions } from '../ducks/stories';
 | 
				
			||||||
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
 | 
					import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
 | 
				
			||||||
import { useAudioPlayerActions } from '../ducks/audioPlayer';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type PropsType = {
 | 
					export type PropsType = {
 | 
				
			||||||
  file?: File;
 | 
					  file?: File;
 | 
				
			||||||
| 
						 | 
					@ -99,6 +104,15 @@ export function SmartStoryCreator(): JSX.Element | null {
 | 
				
			||||||
  const { onSetSkinTone } = useItemsActions();
 | 
					  const { onSetSkinTone } = useItemsActions();
 | 
				
			||||||
  const { onUseEmoji } = useEmojisActions();
 | 
					  const { onUseEmoji } = useEmojisActions();
 | 
				
			||||||
  const { pauseVoiceNotePlayer } = useAudioPlayerActions();
 | 
					  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 (
 | 
					  return (
 | 
				
			||||||
    <StoryCreator
 | 
					    <StoryCreator
 | 
				
			||||||
| 
						 | 
					@ -113,6 +127,9 @@ export function SmartStoryCreator(): JSX.Element | null {
 | 
				
			||||||
      i18n={i18n}
 | 
					      i18n={i18n}
 | 
				
			||||||
      imageToBlurHash={imageToBlurHash}
 | 
					      imageToBlurHash={imageToBlurHash}
 | 
				
			||||||
      installedPacks={installedPacks}
 | 
					      installedPacks={installedPacks}
 | 
				
			||||||
 | 
					      isFormattingEnabled={isFormattingEnabled}
 | 
				
			||||||
 | 
					      isFormattingFlagEnabled={isFormattingFlagEnabled}
 | 
				
			||||||
 | 
					      isFormattingSpoilersFlagEnabled={isFormattingSpoilersFlagEnabled}
 | 
				
			||||||
      isSending={isSending}
 | 
					      isSending={isSending}
 | 
				
			||||||
      linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
 | 
					      linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
 | 
				
			||||||
      me={me}
 | 
					      me={me}
 | 
				
			||||||
| 
						 | 
					@ -123,19 +140,21 @@ export function SmartStoryCreator(): JSX.Element | null {
 | 
				
			||||||
      onDeleteList={deleteDistributionList}
 | 
					      onDeleteList={deleteDistributionList}
 | 
				
			||||||
      onDistributionListCreated={createDistributionList}
 | 
					      onDistributionListCreated={createDistributionList}
 | 
				
			||||||
      onHideMyStoriesFrom={hideMyStoriesFrom}
 | 
					      onHideMyStoriesFrom={hideMyStoriesFrom}
 | 
				
			||||||
 | 
					      onMediaPlaybackStart={pauseVoiceNotePlayer}
 | 
				
			||||||
 | 
					      onPickEmoji={onPickEmoji}
 | 
				
			||||||
      onRemoveMembers={removeMembersFromDistributionList}
 | 
					      onRemoveMembers={removeMembersFromDistributionList}
 | 
				
			||||||
      onRepliesNReactionsChanged={allowsRepliesChanged}
 | 
					      onRepliesNReactionsChanged={allowsRepliesChanged}
 | 
				
			||||||
      onSelectedStoryList={verifyStoryListMembers}
 | 
					      onSelectedStoryList={verifyStoryListMembers}
 | 
				
			||||||
      onSend={sendStoryMessage}
 | 
					      onSend={sendStoryMessage}
 | 
				
			||||||
      onSetSkinTone={onSetSkinTone}
 | 
					      onSetSkinTone={onSetSkinTone}
 | 
				
			||||||
 | 
					      onTextTooLong={onTextTooLong}
 | 
				
			||||||
      onUseEmoji={onUseEmoji}
 | 
					      onUseEmoji={onUseEmoji}
 | 
				
			||||||
      onViewersUpdated={updateStoryViewers}
 | 
					      onViewersUpdated={updateStoryViewers}
 | 
				
			||||||
      onMediaPlaybackStart={pauseVoiceNotePlayer}
 | 
					 | 
				
			||||||
      ourConversationId={ourConversationId}
 | 
					      ourConversationId={ourConversationId}
 | 
				
			||||||
 | 
					      platform={platform}
 | 
				
			||||||
      processAttachment={processAttachment}
 | 
					      processAttachment={processAttachment}
 | 
				
			||||||
      recentEmojis={recentEmojis}
 | 
					      recentEmojis={recentEmojis}
 | 
				
			||||||
      recentStickers={recentStickers}
 | 
					      recentStickers={recentStickers}
 | 
				
			||||||
      renderCompositionTextArea={SmartCompositionTextArea}
 | 
					 | 
				
			||||||
      sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
 | 
					      sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
 | 
				
			||||||
      setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
 | 
					      setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
 | 
				
			||||||
      signalConnections={signalConnections}
 | 
					      signalConnections={signalConnections}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { assert } from 'chai';
 | 
					import { assert } from 'chai';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { normalizeAci } from '../types/ServiceId';
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getCountryCodeValue,
 | 
					  getCountryCodeValue,
 | 
				
			||||||
  getBucketValue,
 | 
					  getBucketValue,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { assert } from 'chai';
 | 
					import { assert } from 'chai';
 | 
				
			||||||
import type { ConversationType } from '../../state/ducks/conversations';
 | 
					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 type { ServiceIdString } from '../../types/ServiceId';
 | 
				
			||||||
import { getDefaultConversationWithServiceId } from '../helpers/getDefaultConversation';
 | 
					import { getDefaultConversationWithServiceId } from '../helpers/getDefaultConversation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@ import { generateKeyPair } from '../../Curve';
 | 
				
			||||||
import type { UploadKeysType } from '../../textsecure/WebAPI';
 | 
					import type { UploadKeysType } from '../../textsecure/WebAPI';
 | 
				
			||||||
import AccountManager from '../../textsecure/AccountManager';
 | 
					import AccountManager from '../../textsecure/AccountManager';
 | 
				
			||||||
import type { PreKeyType } from '../../textsecure/Types.d';
 | 
					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;
 | 
					const { textsecure } = window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,11 +9,8 @@ import sinon from 'sinon';
 | 
				
			||||||
import { ConversationModel } from '../models/conversations';
 | 
					import { ConversationModel } from '../models/conversations';
 | 
				
			||||||
import type { ConversationAttributesType } from '../model-types.d';
 | 
					import type { ConversationAttributesType } from '../model-types.d';
 | 
				
			||||||
import type { WebAPIType } from '../textsecure/WebAPI';
 | 
					import type { WebAPIType } from '../textsecure/WebAPI';
 | 
				
			||||||
import {
 | 
					import { generateAci, normalizeServiceId } from '../types/ServiceId';
 | 
				
			||||||
  generateAci,
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizeServiceId,
 | 
					 | 
				
			||||||
} from '../types/ServiceId';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
 | 
					import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { assert } from 'chai';
 | 
					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 type { ConversationType } from '../../state/ducks/conversations';
 | 
				
			||||||
import { MemberRepository, _toMembers } from '../../quill/memberRepository';
 | 
					import { MemberRepository, _toMembers } from '../../quill/memberRepository';
 | 
				
			||||||
import { getDefaultConversationWithServiceId } from '../../test-both/helpers/getDefaultConversation';
 | 
					import { getDefaultConversationWithServiceId } from '../../test-both/helpers/getDefaultConversation';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,8 @@ import type {
 | 
				
			||||||
  PniString,
 | 
					  PniString,
 | 
				
			||||||
  ServiceIdString,
 | 
					  ServiceIdString,
 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					} from '../../types/ServiceId';
 | 
				
			||||||
import { normalizeAci, normalizePni } from '../../types/ServiceId';
 | 
					import { normalizePni } from '../../types/ServiceId';
 | 
				
			||||||
 | 
					import { normalizeAci } from '../../util/normalizeAci';
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  KyberPreKeyType,
 | 
					  KyberPreKeyType,
 | 
				
			||||||
  PreKeyType,
 | 
					  PreKeyType,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,8 @@ import { range } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { getTableData, insertData, updateToVersion } from './helpers';
 | 
					import { getTableData, insertData, updateToVersion } from './helpers';
 | 
				
			||||||
import type { ServiceIdString } from '../../types/ServiceId';
 | 
					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';
 | 
					import type { PreKeyType } from '../../sql/Interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TestingPreKey = Omit<
 | 
					type TestingPreKey = Omit<
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,8 @@ import { range } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { insertData, updateToVersion } from './helpers';
 | 
					import { insertData, updateToVersion } from './helpers';
 | 
				
			||||||
import type { ServiceIdString } from '../../types/ServiceId';
 | 
					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';
 | 
					import type { KyberPreKeyType, SignedPreKeyType } from '../../sql/Interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TestingKyberKey = Omit<
 | 
					type TestingKyberKey = Omit<
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,11 +43,11 @@ import {
 | 
				
			||||||
import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
 | 
					import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ServiceIdKind,
 | 
					  ServiceIdKind,
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizePni,
 | 
					  normalizePni,
 | 
				
			||||||
  toTaggedPni,
 | 
					  toTaggedPni,
 | 
				
			||||||
  isUntaggedPniString,
 | 
					  isUntaggedPniString,
 | 
				
			||||||
} from '../types/ServiceId';
 | 
					} from '../types/ServiceId';
 | 
				
			||||||
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
 | 
					import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
 | 
				
			||||||
import { ourProfileKeyService } from '../services/ourProfileKey';
 | 
					import { ourProfileKeyService } from '../services/ourProfileKey';
 | 
				
			||||||
import { assertDev, strictAssert } from '../util/assert';
 | 
					import { assertDev, strictAssert } from '../util/assert';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@
 | 
				
			||||||
import protobuf from '../protobuf/wrap';
 | 
					import protobuf from '../protobuf/wrap';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { SignalService as Proto } from '../protobuf';
 | 
					import { SignalService as Proto } from '../protobuf';
 | 
				
			||||||
import { normalizeAci } from '../types/ServiceId';
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
import { DurationInSeconds } from '../util/durations';
 | 
					import { DurationInSeconds } from '../util/durations';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,14 +56,14 @@ import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
 | 
				
			||||||
import type { ServiceIdString } from '../types/ServiceId';
 | 
					import type { ServiceIdString } from '../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ServiceIdKind,
 | 
					  ServiceIdKind,
 | 
				
			||||||
  normalizeAci,
 | 
					 | 
				
			||||||
  normalizeServiceId,
 | 
					  normalizeServiceId,
 | 
				
			||||||
  normalizePni,
 | 
					  normalizePni,
 | 
				
			||||||
  isAciString,
 | 
					 | 
				
			||||||
  isPniString,
 | 
					  isPniString,
 | 
				
			||||||
  isServiceIdString,
 | 
					  isServiceIdString,
 | 
				
			||||||
  fromPniObject,
 | 
					  fromPniObject,
 | 
				
			||||||
} from '../types/ServiceId';
 | 
					} from '../types/ServiceId';
 | 
				
			||||||
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { SignalService as Proto } from '../protobuf';
 | 
					import { SignalService as Proto } from '../protobuf';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,8 @@ import {
 | 
				
			||||||
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
 | 
					import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
 | 
				
			||||||
import { SignalService as Proto } from '../protobuf';
 | 
					import { SignalService as Proto } from '../protobuf';
 | 
				
			||||||
import type { PniString, AciString } from '../types/ServiceId';
 | 
					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';
 | 
					import { strictAssert } from '../util/assert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ProvisionDecryptResult = {
 | 
					type ProvisionDecryptResult = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,11 +9,8 @@ import Long from 'long';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { LoggerType } from '../../types/Logging';
 | 
					import type { LoggerType } from '../../types/Logging';
 | 
				
			||||||
import { strictAssert } from '../../util/assert';
 | 
					import { strictAssert } from '../../util/assert';
 | 
				
			||||||
import {
 | 
					import { isUntaggedPniString, toTaggedPni } from '../../types/ServiceId';
 | 
				
			||||||
  isAciString,
 | 
					import { isAciString } from '../../util/isAciString';
 | 
				
			||||||
  isUntaggedPniString,
 | 
					 | 
				
			||||||
  toTaggedPni,
 | 
					 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					 | 
				
			||||||
import * as Bytes from '../../Bytes';
 | 
					import * as Bytes from '../../Bytes';
 | 
				
			||||||
import { UUID_BYTE_SIZE } from '../../Crypto';
 | 
					import { UUID_BYTE_SIZE } from '../../Crypto';
 | 
				
			||||||
import { uuidToBytes, bytesToUuid } from '../../util/uuidToBytes';
 | 
					import { uuidToBytes, bytesToUuid } from '../../util/uuidToBytes';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,8 @@ import { SECOND, DurationInSeconds } from '../util/durations';
 | 
				
			||||||
import type { AnyPaymentEvent } from '../types/Payment';
 | 
					import type { AnyPaymentEvent } from '../types/Payment';
 | 
				
			||||||
import { PaymentEventKind } from '../types/Payment';
 | 
					import { PaymentEventKind } from '../types/Payment';
 | 
				
			||||||
import { filterAndClean } from '../types/BodyRange';
 | 
					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;
 | 
					const FLAGS = Proto.DataMessage.Flags;
 | 
				
			||||||
export const ATTACHMENT_MAX = 32;
 | 
					export const ATTACHMENT_MAX = 32;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,8 @@ import type {
 | 
				
			||||||
  PniString,
 | 
					  PniString,
 | 
				
			||||||
  ServiceIdString,
 | 
					  ServiceIdString,
 | 
				
			||||||
} from '../../types/ServiceId';
 | 
					} 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 * as log from '../../logging/log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Helpers from '../Helpers';
 | 
					import Helpers from '../Helpers';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import {
 | 
				
			||||||
} from '../util/search';
 | 
					} from '../util/search';
 | 
				
			||||||
import { assertDev } from '../util/assert';
 | 
					import { assertDev } from '../util/assert';
 | 
				
			||||||
import type { AciString } from './ServiceId';
 | 
					import type { AciString } from './ServiceId';
 | 
				
			||||||
import { normalizeAci } from './ServiceId';
 | 
					import { normalizeAci } from '../util/normalizeAci';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Cold storage of body ranges
 | 
					// Cold storage of body ranges
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
 | 
				
			||||||
import { isValidUuid } from '../util/isValidUuid';
 | 
					import { isValidUuid } from '../util/isValidUuid';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import type { LoggerType } from './Logging';
 | 
					import type { LoggerType } from './Logging';
 | 
				
			||||||
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum ServiceIdKind {
 | 
					export enum ServiceIdKind {
 | 
				
			||||||
  ACI = 'ACI',
 | 
					  ACI = 'ACI',
 | 
				
			||||||
| 
						 | 
					@ -26,10 +27,6 @@ export function isServiceIdString(
 | 
				
			||||||
  return isAciString(value) || isPniString(value);
 | 
					  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 {
 | 
					export function isPniString(value?: string | null): value is PniString {
 | 
				
			||||||
  if (value == null) {
 | 
					  if (value == null) {
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
| 
						 | 
					@ -87,41 +84,6 @@ export function normalizeServiceId(
 | 
				
			||||||
  return result;
 | 
					  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(
 | 
					export function normalizePni(
 | 
				
			||||||
  rawPni: string,
 | 
					  rawPni: string,
 | 
				
			||||||
  context: string,
 | 
					  context: string,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ import {
 | 
				
			||||||
  GroupCallJoinState,
 | 
					  GroupCallJoinState,
 | 
				
			||||||
} from '../types/Calling';
 | 
					} from '../types/Calling';
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import type { AciString } from '../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
import { isMe } from './whatTypeOfConversation';
 | 
					import { isMe } from './whatTypeOfConversation';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import type { MessageModel } from '../models/messages';
 | 
				
			||||||
import type { SignalService as Proto } from '../protobuf';
 | 
					import type { SignalService as Proto } from '../protobuf';
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import type { AciString } from '../types/ServiceId';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import { normalizeAci } from '../types/ServiceId';
 | 
					import { normalizeAci } from './normalizeAci';
 | 
				
			||||||
import { filter } from './iterables';
 | 
					import { filter } from './iterables';
 | 
				
			||||||
import { getContactId } from '../messages/helpers';
 | 
					import { getContactId } from '../messages/helpers';
 | 
				
			||||||
import { getTimestampFromLong } from './timestampLongUtils';
 | 
					import { getTimestampFromLong } from './timestampLongUtils';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ import { ReadStatus } from '../messages/MessageReadStatus';
 | 
				
			||||||
import dataInterface from '../sql/Client';
 | 
					import dataInterface from '../sql/Client';
 | 
				
			||||||
import { drop } from './drop';
 | 
					import { drop } from './drop';
 | 
				
			||||||
import { getAttachmentSignature, isVoiceMessage } from '../types/Attachment';
 | 
					import { getAttachmentSignature, isVoiceMessage } from '../types/Attachment';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
import { getMessageIdForLogging } from './idForLogging';
 | 
					import { getMessageIdForLogging } from './idForLogging';
 | 
				
			||||||
import { hasErrors } from '../state/selectors/message';
 | 
					import { hasErrors } from '../state/selectors/message';
 | 
				
			||||||
import { isIncoming, isOutgoing } from '../messages/helpers';
 | 
					import { isIncoming, isOutgoing } from '../messages/helpers';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								ts/util/isAciString.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ts/util/isAciString.ts
									
										
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { ConversationType } from '../state/ducks/conversations';
 | 
					import type { ConversationType } from '../state/ducks/conversations';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isSafetyNumberNotAvailable = (
 | 
					export const isSafetyNumberNotAvailable = (
 | 
				
			||||||
  contact?: ConversationType
 | 
					  contact?: ConversationType
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2004,13 +2004,6 @@
 | 
				
			||||||
    "updated": "2021-12-10T23:24:03.829Z",
 | 
					    "updated": "2021-12-10T23:24:03.829Z",
 | 
				
			||||||
    "reasonDetail": "Doesn't touch the DOM."
 | 
					    "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",
 | 
					    "rule": "React-useRef",
 | 
				
			||||||
    "path": "ts/components/AvatarTextEditor.tsx",
 | 
					    "path": "ts/components/AvatarTextEditor.tsx",
 | 
				
			||||||
| 
						 | 
					@ -2390,6 +2383,13 @@
 | 
				
			||||||
    "reasonCategory": "usageTrusted",
 | 
					    "reasonCategory": "usageTrusted",
 | 
				
			||||||
    "updated": "2022-11-11T17:11:07.659Z"
 | 
					    "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",
 | 
					    "rule": "React-useRef",
 | 
				
			||||||
    "path": "ts/components/MediaQualitySelector.tsx",
 | 
					    "path": "ts/components/MediaQualitySelector.tsx",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ import {
 | 
				
			||||||
} from '../jobs/conversationJobQueue';
 | 
					} from '../jobs/conversationJobQueue';
 | 
				
			||||||
import { ReceiptType } from '../types/Receipt';
 | 
					import { ReceiptType } from '../types/Receipt';
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import type { AciString } from '../types/ServiceId';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function markConversationRead(
 | 
					export async function markConversationRead(
 | 
				
			||||||
  conversationAttrs: ConversationAttributesType,
 | 
					  conversationAttrs: ConversationAttributesType,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										42
									
								
								ts/util/normalizeAci.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								ts/util/normalizeAci.ts
									
										
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ import {
 | 
				
			||||||
  SafetyNumberIdentifierType,
 | 
					  SafetyNumberIdentifierType,
 | 
				
			||||||
  SafetyNumberMode,
 | 
					  SafetyNumberMode,
 | 
				
			||||||
} from '../types/safetyNumber';
 | 
					} from '../types/safetyNumber';
 | 
				
			||||||
import { isAciString } from '../types/ServiceId';
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ITERATION_COUNT = 5200;
 | 
					const ITERATION_COUNT = 5200;
 | 
				
			||||||
const E164_VERSION = 1;
 | 
					const E164_VERSION = 1;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue