Change design of edit media modal in web UI (#33516)
This commit is contained in:
		
							parent
							
								
									4ebdfed8ea
								
							
						
					
					
						commit
						11786f1114
					
				
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
| 
						 | 
					@ -414,7 +414,7 @@ export function initMediaEditModal(id) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(openModal({
 | 
					    dispatch(openModal({
 | 
				
			||||||
      modalType: 'FOCAL_POINT',
 | 
					      modalType: 'FOCAL_POINT',
 | 
				
			||||||
      modalProps: { id },
 | 
					      modalProps: { mediaId: id },
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,70 @@
 | 
				
			||||||
 | 
					import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { apiUpdateMedia } from 'mastodon/api/compose';
 | 
				
			||||||
 | 
					import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
 | 
				
			||||||
 | 
					import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
				
			||||||
 | 
					import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
 | 
				
			||||||
 | 
					  unattached?: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const simulateModifiedApiResponse = (
 | 
				
			||||||
 | 
					  media: MediaAttachment,
 | 
				
			||||||
 | 
					  params: { description?: string; focus?: string },
 | 
				
			||||||
 | 
					): SimulatedMediaAttachmentJSON => {
 | 
				
			||||||
 | 
					  const [x, y] = (params.focus ?? '').split(',');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = {
 | 
				
			||||||
 | 
					    ...media.toJS(),
 | 
				
			||||||
 | 
					    ...params,
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      focus: {
 | 
				
			||||||
 | 
					        x: parseFloat(x ?? '0'),
 | 
				
			||||||
 | 
					        y: parseFloat(y ?? '0'),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  } as unknown as SimulatedMediaAttachmentJSON;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const changeUploadCompose = createDataLoadingThunk(
 | 
				
			||||||
 | 
					  'compose/changeUpload',
 | 
				
			||||||
 | 
					  async (
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      ...params
 | 
				
			||||||
 | 
					    }: {
 | 
				
			||||||
 | 
					      id: string;
 | 
				
			||||||
 | 
					      description: string;
 | 
				
			||||||
 | 
					      focus: string;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    { getState },
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const media = (
 | 
				
			||||||
 | 
					      (getState().compose as ImmutableMap<string, unknown>).get(
 | 
				
			||||||
 | 
					        'media_attachments',
 | 
				
			||||||
 | 
					      ) as ImmutableList<MediaAttachment>
 | 
				
			||||||
 | 
					    ).find((item) => item.get('id') === id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Editing already-attached media is deferred to editing the post itself.
 | 
				
			||||||
 | 
					    // For simplicity's sake, fake an API reply.
 | 
				
			||||||
 | 
					    if (media && !media.get('unattached')) {
 | 
				
			||||||
 | 
					      return new Promise<SimulatedMediaAttachmentJSON>((resolve) => {
 | 
				
			||||||
 | 
					        resolve(simulateModifiedApiResponse(media, params));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiUpdateMedia(id, params);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  (media: SimulatedMediaAttachmentJSON) => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      media,
 | 
				
			||||||
 | 
					      attached: typeof media.unattached !== 'undefined' && !media.unattached,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    useLoadingBar: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ export type ModalType = keyof typeof MODAL_COMPONENTS;
 | 
				
			||||||
interface OpenModalPayload {
 | 
					interface OpenModalPayload {
 | 
				
			||||||
  modalType: ModalType;
 | 
					  modalType: ModalType;
 | 
				
			||||||
  modalProps: ModalProps;
 | 
					  modalProps: ModalProps;
 | 
				
			||||||
 | 
					  previousModalProps?: ModalProps;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
 | 
					export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { apiRequestPut } from 'mastodon/api';
 | 
				
			||||||
 | 
					import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const apiUpdateMedia = (
 | 
				
			||||||
 | 
					  id: string,
 | 
				
			||||||
 | 
					  params?: { description?: string; focus?: string },
 | 
				
			||||||
 | 
					) => apiRequestPut<ApiMediaAttachmentJSON>(`v1/media/${id}`, params);
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ interface BaseProps
 | 
				
			||||||
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
 | 
					  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
 | 
				
			||||||
  block?: boolean;
 | 
					  block?: boolean;
 | 
				
			||||||
  secondary?: boolean;
 | 
					  secondary?: boolean;
 | 
				
			||||||
 | 
					  compact?: boolean;
 | 
				
			||||||
  dangerous?: boolean;
 | 
					  dangerous?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +28,7 @@ export const Button: React.FC<Props> = ({
 | 
				
			||||||
  disabled,
 | 
					  disabled,
 | 
				
			||||||
  block,
 | 
					  block,
 | 
				
			||||||
  secondary,
 | 
					  secondary,
 | 
				
			||||||
 | 
					  compact,
 | 
				
			||||||
  dangerous,
 | 
					  dangerous,
 | 
				
			||||||
  className,
 | 
					  className,
 | 
				
			||||||
  title,
 | 
					  title,
 | 
				
			||||||
| 
						 | 
					@ -47,6 +49,7 @@ export const Button: React.FC<Props> = ({
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      className={classNames('button', className, {
 | 
					      className={classNames('button', className, {
 | 
				
			||||||
        'button-secondary': secondary,
 | 
					        'button-secondary': secondary,
 | 
				
			||||||
 | 
					        'button--compact': compact,
 | 
				
			||||||
        'button--block': block,
 | 
					        'button--block': block,
 | 
				
			||||||
        'button--dangerous': dangerous,
 | 
					        'button--dangerous': dangerous,
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useIntl, defineMessages } from 'react-intl';
 | 
					import { useIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useIdentity } from '@/mastodon/identity_context';
 | 
					import { useIdentity } from '@/mastodon/identity_context';
 | 
				
			||||||
import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
 | 
					import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
 | 
				
			||||||
import { openModal } from 'mastodon/actions/modal';
 | 
					import { openModal } from 'mastodon/actions/modal';
 | 
				
			||||||
| 
						 | 
					@ -20,7 +22,8 @@ const messages = defineMessages({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const FollowButton: React.FC<{
 | 
					export const FollowButton: React.FC<{
 | 
				
			||||||
  accountId?: string;
 | 
					  accountId?: string;
 | 
				
			||||||
}> = ({ accountId }) => {
 | 
					  compact?: boolean;
 | 
				
			||||||
 | 
					}> = ({ accountId, compact }) => {
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
  const { signedIn } = useIdentity();
 | 
					  const { signedIn } = useIdentity();
 | 
				
			||||||
| 
						 | 
					@ -89,7 +92,9 @@ export const FollowButton: React.FC<{
 | 
				
			||||||
        href='/settings/profile'
 | 
					        href='/settings/profile'
 | 
				
			||||||
        target='_blank'
 | 
					        target='_blank'
 | 
				
			||||||
        rel='noopener'
 | 
					        rel='noopener'
 | 
				
			||||||
        className='button button-secondary'
 | 
					        className={classNames('button button-secondary', {
 | 
				
			||||||
 | 
					          'button--compact': compact,
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {label}
 | 
					        {label}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
| 
						 | 
					@ -106,6 +111,7 @@ export const FollowButton: React.FC<{
 | 
				
			||||||
          (account?.suspended || !!account?.moved))
 | 
					          (account?.suspended || !!account?.moved))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      secondary={following}
 | 
					      secondary={following}
 | 
				
			||||||
 | 
					      compact={compact}
 | 
				
			||||||
      className={following ? 'button--destructive' : undefined}
 | 
					      className={following ? 'button--destructive' : undefined}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {label}
 | 
					      {label}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,46 +1,39 @@
 | 
				
			||||||
import { useCallback, useState } from 'react';
 | 
					import { useCallback, useState, forwardRef } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  src: string;
 | 
					  src: string;
 | 
				
			||||||
  key: string;
 | 
					 | 
				
			||||||
  alt?: string;
 | 
					  alt?: string;
 | 
				
			||||||
  lang?: string;
 | 
					  lang?: string;
 | 
				
			||||||
  width: number;
 | 
					  width?: number;
 | 
				
			||||||
  height: number;
 | 
					  height?: number;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: React.MouseEventHandler;
 | 
				
			||||||
 | 
					  onMouseDown?: React.MouseEventHandler;
 | 
				
			||||||
 | 
					  onTouchStart?: React.TouchEventHandler;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GIFV: React.FC<Props> = ({
 | 
					export const GIFV = forwardRef<HTMLVideoElement, Props>(
 | 
				
			||||||
  src,
 | 
					  (
 | 
				
			||||||
  alt,
 | 
					    { src, alt, lang, width, height, onClick, onMouseDown, onTouchStart },
 | 
				
			||||||
  lang,
 | 
					    ref,
 | 
				
			||||||
  width,
 | 
					  ) => {
 | 
				
			||||||
  height,
 | 
					 | 
				
			||||||
  onClick,
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    const [loading, setLoading] = useState(true);
 | 
					    const [loading, setLoading] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
 | 
					    const handleLoadedData = useCallback(() => {
 | 
				
			||||||
    useCallback(() => {
 | 
					 | 
				
			||||||
      setLoading(false);
 | 
					      setLoading(false);
 | 
				
			||||||
    }, [setLoading]);
 | 
					    }, [setLoading]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClick: React.MouseEventHandler = useCallback(
 | 
					    const handleClick = useCallback(
 | 
				
			||||||
    (e) => {
 | 
					      (e: React.MouseEvent) => {
 | 
				
			||||||
      if (onClick) {
 | 
					 | 
				
			||||||
        e.stopPropagation();
 | 
					        e.stopPropagation();
 | 
				
			||||||
        onClick();
 | 
					        onClick?.(e);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      [onClick],
 | 
					      [onClick],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
    <div className='gifv' style={{ position: 'relative' }}>
 | 
					      <div className='gifv'>
 | 
				
			||||||
        {loading && (
 | 
					        {loading && (
 | 
				
			||||||
          <canvas
 | 
					          <canvas
 | 
				
			||||||
          width={width}
 | 
					 | 
				
			||||||
          height={height}
 | 
					 | 
				
			||||||
            role='button'
 | 
					            role='button'
 | 
				
			||||||
            tabIndex={0}
 | 
					            tabIndex={0}
 | 
				
			||||||
            aria-label={alt}
 | 
					            aria-label={alt}
 | 
				
			||||||
| 
						 | 
					@ -51,20 +44,27 @@ export const GIFV: React.FC<Props> = ({
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <video
 | 
					        <video
 | 
				
			||||||
 | 
					          ref={ref}
 | 
				
			||||||
          src={src}
 | 
					          src={src}
 | 
				
			||||||
          role='button'
 | 
					          role='button'
 | 
				
			||||||
          tabIndex={0}
 | 
					          tabIndex={0}
 | 
				
			||||||
          aria-label={alt}
 | 
					          aria-label={alt}
 | 
				
			||||||
          title={alt}
 | 
					          title={alt}
 | 
				
			||||||
          lang={lang}
 | 
					          lang={lang}
 | 
				
			||||||
 | 
					          width={width}
 | 
				
			||||||
 | 
					          height={height}
 | 
				
			||||||
          muted
 | 
					          muted
 | 
				
			||||||
          loop
 | 
					          loop
 | 
				
			||||||
          autoPlay
 | 
					          autoPlay
 | 
				
			||||||
          playsInline
 | 
					          playsInline
 | 
				
			||||||
          onClick={handleClick}
 | 
					          onClick={handleClick}
 | 
				
			||||||
          onLoadedData={handleLoadedData}
 | 
					          onLoadedData={handleLoadedData}
 | 
				
			||||||
        style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
 | 
					          onMouseDown={onMouseDown}
 | 
				
			||||||
 | 
					          onTouchStart={onTouchStart}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GIFV.displayName = 'GIFV';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,531 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					  useCallback,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useImperativeHandle,
 | 
				
			||||||
 | 
					  forwardRef,
 | 
				
			||||||
 | 
					} from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Textarea from 'react-textarea-autosize';
 | 
				
			||||||
 | 
					import { length } from 'stringz';
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/extensions
 | 
				
			||||||
 | 
					import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-extraneous-dependencies
 | 
				
			||||||
 | 
					import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { showAlertForError } from 'mastodon/actions/alerts';
 | 
				
			||||||
 | 
					import { uploadThumbnail } from 'mastodon/actions/compose';
 | 
				
			||||||
 | 
					import { changeUploadCompose } from 'mastodon/actions/compose_typed';
 | 
				
			||||||
 | 
					import { Button } from 'mastodon/components/button';
 | 
				
			||||||
 | 
					import { GIFV } from 'mastodon/components/gifv';
 | 
				
			||||||
 | 
					import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
				
			||||||
 | 
					import { Skeleton } from 'mastodon/components/skeleton';
 | 
				
			||||||
 | 
					import Audio from 'mastodon/features/audio';
 | 
				
			||||||
 | 
					import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
 | 
				
			||||||
 | 
					import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
 | 
				
			||||||
 | 
					import Video, { getPointerPosition } from 'mastodon/features/video';
 | 
				
			||||||
 | 
					import { me } from 'mastodon/initial_state';
 | 
				
			||||||
 | 
					import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
				
			||||||
 | 
					import { useAppSelector, useAppDispatch } from 'mastodon/store';
 | 
				
			||||||
 | 
					import { assetHost } from 'mastodon/utils/config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  placeholderVisual: {
 | 
				
			||||||
 | 
					    id: 'alt_text_modal.describe_for_people_with_visual_impairments',
 | 
				
			||||||
 | 
					    defaultMessage: 'Describe this for people with visual impairments…',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  placeholderHearing: {
 | 
				
			||||||
 | 
					    id: 'alt_text_modal.describe_for_people_with_hearing_impairments',
 | 
				
			||||||
 | 
					    defaultMessage: 'Describe this for people with hearing impairments…',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  discardMessage: {
 | 
				
			||||||
 | 
					    id: 'confirmations.discard_edit_media.message',
 | 
				
			||||||
 | 
					    defaultMessage:
 | 
				
			||||||
 | 
					      'You have unsaved changes to the media description or preview, discard them anyway?',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  discardConfirm: {
 | 
				
			||||||
 | 
					    id: 'confirmations.discard_edit_media.confirm',
 | 
				
			||||||
 | 
					    defaultMessage: 'Discard',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MAX_LENGTH = 1500;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FocalPoint = [number, number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const UploadButton: React.FC<{
 | 
				
			||||||
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					  onSelectFile: (arg0: File) => void;
 | 
				
			||||||
 | 
					  mimeTypes: string;
 | 
				
			||||||
 | 
					}> = ({ children, onSelectFile, mimeTypes }) => {
 | 
				
			||||||
 | 
					  const fileRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClick = useCallback(() => {
 | 
				
			||||||
 | 
					    fileRef.current?.click();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = useCallback(
 | 
				
			||||||
 | 
					    (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
 | 
					      const file = e.target.files?.[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (file) {
 | 
				
			||||||
 | 
					        onSelectFile(file);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onSelectFile],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <label>
 | 
				
			||||||
 | 
					      <Button onClick={handleClick}>{children}</Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        id='upload-modal__thumbnail'
 | 
				
			||||||
 | 
					        ref={fileRef}
 | 
				
			||||||
 | 
					        type='file'
 | 
				
			||||||
 | 
					        accept={mimeTypes}
 | 
				
			||||||
 | 
					        onChange={handleChange}
 | 
				
			||||||
 | 
					        style={{ display: 'none' }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Preview: React.FC<{
 | 
				
			||||||
 | 
					  mediaId: string;
 | 
				
			||||||
 | 
					  position: FocalPoint;
 | 
				
			||||||
 | 
					  onPositionChange: (arg0: FocalPoint) => void;
 | 
				
			||||||
 | 
					}> = ({ mediaId, position, onPositionChange }) => {
 | 
				
			||||||
 | 
					  const media = useAppSelector((state) =>
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      (state.compose as ImmutableMap<string, unknown>).get(
 | 
				
			||||||
 | 
					        'media_attachments',
 | 
				
			||||||
 | 
					      ) as ImmutableList<MediaAttachment>
 | 
				
			||||||
 | 
					    ).find((x) => x.get('id') === mediaId),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const account = useAppSelector((state) =>
 | 
				
			||||||
 | 
					    me ? state.accounts.get(me) : undefined,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [dragging, setDragging] = useState(false);
 | 
				
			||||||
 | 
					  const [x, y] = position;
 | 
				
			||||||
 | 
					  const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
 | 
				
			||||||
 | 
					  const draggingRef = useRef<boolean>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setRef = useCallback(
 | 
				
			||||||
 | 
					    (e: HTMLImageElement | HTMLVideoElement | null) => {
 | 
				
			||||||
 | 
					      nodeRef.current = e;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMouseDown = useCallback(
 | 
				
			||||||
 | 
					    (e: React.MouseEvent) => {
 | 
				
			||||||
 | 
					      if (e.button !== 0) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { x, y } = getPointerPosition(nodeRef.current, e);
 | 
				
			||||||
 | 
					      setDragging(true);
 | 
				
			||||||
 | 
					      draggingRef.current = true;
 | 
				
			||||||
 | 
					      onPositionChange([x, y]);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setDragging, onPositionChange],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleTouchStart = useCallback(
 | 
				
			||||||
 | 
					    (e: React.TouchEvent) => {
 | 
				
			||||||
 | 
					      const { x, y } = getPointerPosition(nodeRef.current, e);
 | 
				
			||||||
 | 
					      setDragging(true);
 | 
				
			||||||
 | 
					      draggingRef.current = true;
 | 
				
			||||||
 | 
					      onPositionChange([x, y]);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setDragging, onPositionChange],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const handleMouseUp = () => {
 | 
				
			||||||
 | 
					      setDragging(false);
 | 
				
			||||||
 | 
					      draggingRef.current = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleMouseMove = (e: MouseEvent) => {
 | 
				
			||||||
 | 
					      if (draggingRef.current) {
 | 
				
			||||||
 | 
					        const { x, y } = getPointerPosition(nodeRef.current, e);
 | 
				
			||||||
 | 
					        onPositionChange([x, y]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleTouchEnd = () => {
 | 
				
			||||||
 | 
					      setDragging(false);
 | 
				
			||||||
 | 
					      draggingRef.current = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleTouchMove = (e: TouchEvent) => {
 | 
				
			||||||
 | 
					      if (draggingRef.current) {
 | 
				
			||||||
 | 
					        const { x, y } = getPointerPosition(nodeRef.current, e);
 | 
				
			||||||
 | 
					        onPositionChange([x, y]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.addEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					    document.addEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					    document.addEventListener('touchend', handleTouchEnd);
 | 
				
			||||||
 | 
					    document.addEventListener('touchmove', handleTouchMove);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					      document.removeEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					      document.removeEventListener('touchend', handleTouchEnd);
 | 
				
			||||||
 | 
					      document.removeEventListener('touchmove', handleTouchMove);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [setDragging, onPositionChange]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!media) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (media.get('type') === 'image') {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className={classNames('focal-point', { dragging })}>
 | 
				
			||||||
 | 
					        <img
 | 
				
			||||||
 | 
					          ref={setRef}
 | 
				
			||||||
 | 
					          draggable={false}
 | 
				
			||||||
 | 
					          src={media.get('url') as string}
 | 
				
			||||||
 | 
					          alt=''
 | 
				
			||||||
 | 
					          role='presentation'
 | 
				
			||||||
 | 
					          onMouseDown={handleMouseDown}
 | 
				
			||||||
 | 
					          onTouchStart={handleTouchStart}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className='focal-point__reticle'
 | 
				
			||||||
 | 
					          style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (media.get('type') === 'gifv') {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className={classNames('focal-point', { dragging })}>
 | 
				
			||||||
 | 
					        <GIFV
 | 
				
			||||||
 | 
					          ref={setRef}
 | 
				
			||||||
 | 
					          src={media.get('url') as string}
 | 
				
			||||||
 | 
					          alt=''
 | 
				
			||||||
 | 
					          onMouseDown={handleMouseDown}
 | 
				
			||||||
 | 
					          onTouchStart={handleTouchStart}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className='focal-point__reticle'
 | 
				
			||||||
 | 
					          style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (media.get('type') === 'video') {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Video
 | 
				
			||||||
 | 
					        preview={media.get('preview_url') as string}
 | 
				
			||||||
 | 
					        frameRate={media.getIn(['meta', 'original', 'frame_rate']) as string}
 | 
				
			||||||
 | 
					        blurhash={media.get('blurhash') as string}
 | 
				
			||||||
 | 
					        src={media.get('url') as string}
 | 
				
			||||||
 | 
					        detailed
 | 
				
			||||||
 | 
					        inline
 | 
				
			||||||
 | 
					        editable
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (media.get('type') === 'audio') {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Audio
 | 
				
			||||||
 | 
					        src={media.get('url') as string}
 | 
				
			||||||
 | 
					        duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
 | 
				
			||||||
 | 
					        poster={
 | 
				
			||||||
 | 
					          (media.get('preview_url') as string | undefined) ??
 | 
				
			||||||
 | 
					          account?.avatar_static
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        backgroundColor={
 | 
				
			||||||
 | 
					          media.getIn(['meta', 'colors', 'background']) as string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        foregroundColor={
 | 
				
			||||||
 | 
					          media.getIn(['meta', 'colors', 'foreground']) as string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        accentColor={media.getIn(['meta', 'colors', 'accent']) as string}
 | 
				
			||||||
 | 
					        editable
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RestoreProps {
 | 
				
			||||||
 | 
					  previousDescription: string;
 | 
				
			||||||
 | 
					  previousPosition: FocalPoint;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  mediaId: string;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ConfirmationMessage {
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  confirm: string;
 | 
				
			||||||
 | 
					  props?: RestoreProps;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ModalRef {
 | 
				
			||||||
 | 
					  getCloseConfirmationMessage: () => null | ConfirmationMessage;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
 | 
				
			||||||
 | 
					  ({ mediaId, previousDescription, previousPosition, onClose }, ref) => {
 | 
				
			||||||
 | 
					    const intl = useIntl();
 | 
				
			||||||
 | 
					    const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					    const media = useAppSelector((state) =>
 | 
				
			||||||
 | 
					      (
 | 
				
			||||||
 | 
					        (state.compose as ImmutableMap<string, unknown>).get(
 | 
				
			||||||
 | 
					          'media_attachments',
 | 
				
			||||||
 | 
					        ) as ImmutableList<MediaAttachment>
 | 
				
			||||||
 | 
					      ).find((x) => x.get('id') === mediaId),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const lang = useAppSelector(
 | 
				
			||||||
 | 
					      (state) =>
 | 
				
			||||||
 | 
					        (state.compose as ImmutableMap<string, unknown>).get('lang') as string,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const focusX =
 | 
				
			||||||
 | 
					      (media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0;
 | 
				
			||||||
 | 
					    const focusY =
 | 
				
			||||||
 | 
					      (media?.getIn(['meta', 'focus', 'y'], 0) as number | undefined) ?? 0;
 | 
				
			||||||
 | 
					    const [description, setDescription] = useState(
 | 
				
			||||||
 | 
					      previousDescription ??
 | 
				
			||||||
 | 
					        (media?.get('description') as string | undefined) ??
 | 
				
			||||||
 | 
					        '',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [position, setPosition] = useState<FocalPoint>(
 | 
				
			||||||
 | 
					      previousPosition ?? [focusX / 2 + 0.5, focusY / -2 + 0.5],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [isDetecting, setIsDetecting] = useState(false);
 | 
				
			||||||
 | 
					    const [isSaving, setIsSaving] = useState(false);
 | 
				
			||||||
 | 
					    const dirtyRef = useRef(
 | 
				
			||||||
 | 
					      previousDescription || previousPosition ? true : false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const type = media?.get('type') as string;
 | 
				
			||||||
 | 
					    const valid = length(description) <= MAX_LENGTH;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleDescriptionChange = useCallback(
 | 
				
			||||||
 | 
					      (e: React.ChangeEvent<HTMLTextAreaElement>) => {
 | 
				
			||||||
 | 
					        setDescription(e.target.value);
 | 
				
			||||||
 | 
					        dirtyRef.current = true;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [setDescription],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleThumbnailChange = useCallback(
 | 
				
			||||||
 | 
					      (file: File) => {
 | 
				
			||||||
 | 
					        dispatch(uploadThumbnail(mediaId, file));
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [dispatch, mediaId],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handlePositionChange = useCallback(
 | 
				
			||||||
 | 
					      (position: FocalPoint) => {
 | 
				
			||||||
 | 
					        setPosition(position);
 | 
				
			||||||
 | 
					        dirtyRef.current = true;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [setPosition],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleSubmit = useCallback(() => {
 | 
				
			||||||
 | 
					      setIsSaving(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dispatch(
 | 
				
			||||||
 | 
					        changeUploadCompose({
 | 
				
			||||||
 | 
					          id: mediaId,
 | 
				
			||||||
 | 
					          description,
 | 
				
			||||||
 | 
					          focus: `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}`,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          setIsSaving(false);
 | 
				
			||||||
 | 
					          dirtyRef.current = false;
 | 
				
			||||||
 | 
					          onClose();
 | 
				
			||||||
 | 
					          return '';
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((err: unknown) => {
 | 
				
			||||||
 | 
					          setIsSaving(false);
 | 
				
			||||||
 | 
					          dispatch(showAlertForError(err));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }, [dispatch, setIsSaving, mediaId, onClose, position, description]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleKeyUp = useCallback(
 | 
				
			||||||
 | 
					      (e: React.KeyboardEvent) => {
 | 
				
			||||||
 | 
					        if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (valid) {
 | 
				
			||||||
 | 
					            handleSubmit();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [handleSubmit, valid],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleDetectClick = useCallback(() => {
 | 
				
			||||||
 | 
					      setIsDetecting(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fetchTesseract()
 | 
				
			||||||
 | 
					        .then(async ({ createWorker }) => {
 | 
				
			||||||
 | 
					          const worker = await createWorker('eng', 1, {
 | 
				
			||||||
 | 
					            workerPath: tesseractWorkerPath as string,
 | 
				
			||||||
 | 
					            corePath: tesseractCorePath as string,
 | 
				
			||||||
 | 
					            langPath: `${assetHost}/ocr/lang-data`,
 | 
				
			||||||
 | 
					            cacheMethod: 'write',
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const image = URL.createObjectURL(media?.get('file') as File);
 | 
				
			||||||
 | 
					          const result = await worker.recognize(image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          setDescription(result.data.text);
 | 
				
			||||||
 | 
					          setIsDetecting(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          await worker.terminate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return '';
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(() => {
 | 
				
			||||||
 | 
					          setIsDetecting(false);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }, [setDescription, setIsDetecting, media]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useImperativeHandle(
 | 
				
			||||||
 | 
					      ref,
 | 
				
			||||||
 | 
					      () => ({
 | 
				
			||||||
 | 
					        getCloseConfirmationMessage: () => {
 | 
				
			||||||
 | 
					          if (dirtyRef.current) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              message: intl.formatMessage(messages.discardMessage),
 | 
				
			||||||
 | 
					              confirm: intl.formatMessage(messages.discardConfirm),
 | 
				
			||||||
 | 
					              props: {
 | 
				
			||||||
 | 
					                previousDescription: description,
 | 
				
			||||||
 | 
					                previousPosition: position,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      [intl, description, position],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='modal-root__modal dialog-modal'>
 | 
				
			||||||
 | 
					        <div className='dialog-modal__header'>
 | 
				
			||||||
 | 
					          <Button onClick={handleSubmit} disabled={!valid}>
 | 
				
			||||||
 | 
					            {isSaving ? (
 | 
				
			||||||
 | 
					              <LoadingIndicator />
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <FormattedMessage
 | 
				
			||||||
 | 
					                id='alt_text_modal.done'
 | 
				
			||||||
 | 
					                defaultMessage='Done'
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <span className='dialog-modal__header__title'>
 | 
				
			||||||
 | 
					            <FormattedMessage
 | 
				
			||||||
 | 
					              id='alt_text_modal.add_alt_text'
 | 
				
			||||||
 | 
					              defaultMessage='Add alt text'
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Button secondary onClick={onClose}>
 | 
				
			||||||
 | 
					            <FormattedMessage
 | 
				
			||||||
 | 
					              id='alt_text_modal.cancel'
 | 
				
			||||||
 | 
					              defaultMessage='Cancel'
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='dialog-modal__content'>
 | 
				
			||||||
 | 
					          <div className='dialog-modal__content__preview'>
 | 
				
			||||||
 | 
					            <Preview
 | 
				
			||||||
 | 
					              mediaId={mediaId}
 | 
				
			||||||
 | 
					              position={position}
 | 
				
			||||||
 | 
					              onPositionChange={handlePositionChange}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {(type === 'audio' || type === 'video') && (
 | 
				
			||||||
 | 
					              <UploadButton
 | 
				
			||||||
 | 
					                onSelectFile={handleThumbnailChange}
 | 
				
			||||||
 | 
					                mimeTypes='image/jpeg,image/png,image/gif,image/heic,image/heif,image/webp,image/avif'
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <FormattedMessage
 | 
				
			||||||
 | 
					                  id='alt_text_modal.change_thumbnail'
 | 
				
			||||||
 | 
					                  defaultMessage='Change thumbnail'
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </UploadButton>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <form
 | 
				
			||||||
 | 
					            className='dialog-modal__content__form simple_form'
 | 
				
			||||||
 | 
					            onSubmit={handleSubmit}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div className='input'>
 | 
				
			||||||
 | 
					              <div className='label_input'>
 | 
				
			||||||
 | 
					                <Textarea
 | 
				
			||||||
 | 
					                  id='description'
 | 
				
			||||||
 | 
					                  value={isDetecting ? ' ' : description}
 | 
				
			||||||
 | 
					                  onChange={handleDescriptionChange}
 | 
				
			||||||
 | 
					                  onKeyUp={handleKeyUp}
 | 
				
			||||||
 | 
					                  lang={lang}
 | 
				
			||||||
 | 
					                  placeholder={intl.formatMessage(
 | 
				
			||||||
 | 
					                    type === 'audio'
 | 
				
			||||||
 | 
					                      ? messages.placeholderHearing
 | 
				
			||||||
 | 
					                      : messages.placeholderVisual,
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                  minRows={3}
 | 
				
			||||||
 | 
					                  disabled={isDetecting}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {isDetecting && (
 | 
				
			||||||
 | 
					                  <div className='label_input__loading-indicator'>
 | 
				
			||||||
 | 
					                    <Skeleton width='100%' />
 | 
				
			||||||
 | 
					                    <Skeleton width='100%' />
 | 
				
			||||||
 | 
					                    <Skeleton width='61%' />
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className='input__toolbar'>
 | 
				
			||||||
 | 
					                <button
 | 
				
			||||||
 | 
					                  className='link-button'
 | 
				
			||||||
 | 
					                  onClick={handleDetectClick}
 | 
				
			||||||
 | 
					                  disabled={type !== 'image' || isDetecting}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <FormattedMessage
 | 
				
			||||||
 | 
					                    id='alt_text_modal.add_text_from_image'
 | 
				
			||||||
 | 
					                    defaultMessage='Add text from image'
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <CharacterCounter
 | 
				
			||||||
 | 
					                  max={MAX_LENGTH}
 | 
				
			||||||
 | 
					                  text={isDetecting ? '' : description}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AltTextModal.displayName = 'AltTextModal';
 | 
				
			||||||
| 
						 | 
					@ -581,10 +581,14 @@ class Audio extends PureComponent {
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div className='video-player__buttons right'>
 | 
					            <div className='video-player__buttons right'>
 | 
				
			||||||
              {!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>}
 | 
					              {!editable && (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>
 | 
				
			||||||
                  <a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
 | 
					                  <a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
 | 
				
			||||||
                <Icon id={'download'} icon={DownloadIcon} />
 | 
					                    <Icon id='download' icon={DownloadIcon} />
 | 
				
			||||||
                  </a>
 | 
					                  </a>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { length } from 'stringz';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const CharacterCounter = ({ text, max }) => {
 | 
					 | 
				
			||||||
  const diff = max - length(text);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (diff < 0) {
 | 
					 | 
				
			||||||
    return <span className='character-counter character-counter--over'>{diff}</span>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return <span className='character-counter'>{diff}</span>;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CharacterCounter.propTypes = {
 | 
					 | 
				
			||||||
  text: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
  max: PropTypes.number.isRequired,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					import { length } from 'stringz';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CharacterCounter: React.FC<{
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					  max: number;
 | 
				
			||||||
 | 
					}> = ({ text, max }) => {
 | 
				
			||||||
 | 
					  const diff = max - length(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (diff < 0) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <span className='character-counter character-counter--over'>{diff}</span>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <span className='character-counter'>{diff}</span>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -301,6 +301,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
				
			||||||
              <div className='compose-form__submit'>
 | 
					              <div className='compose-form__submit'>
 | 
				
			||||||
                <Button
 | 
					                <Button
 | 
				
			||||||
                  type='submit'
 | 
					                  type='submit'
 | 
				
			||||||
 | 
					                  compact
 | 
				
			||||||
                  text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
 | 
					                  text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
 | 
				
			||||||
                  disabled={!this.canSubmit()}
 | 
					                  disabled={!this.canSubmit()}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,16 +4,16 @@ import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useSortable } from '@dnd-kit/sortable';
 | 
					import { useSortable } from '@dnd-kit/sortable';
 | 
				
			||||||
import { CSS } from '@dnd-kit/utilities';
 | 
					import { CSS } from '@dnd-kit/utilities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
 | 
					import CloseIcon from '@/material-icons/400-20px/close.svg?react';
 | 
				
			||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
 | 
					import EditIcon from '@/material-icons/400-24px/edit.svg?react';
 | 
				
			||||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
 | 
					import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
 | 
				
			||||||
import {
 | 
					import { undoUploadCompose } from 'mastodon/actions/compose';
 | 
				
			||||||
  undoUploadCompose,
 | 
					import { openModal } from 'mastodon/actions/modal';
 | 
				
			||||||
  initMediaEditModal,
 | 
					 | 
				
			||||||
} from 'mastodon/actions/compose';
 | 
					 | 
				
			||||||
import { Blurhash } from 'mastodon/components/blurhash';
 | 
					import { Blurhash } from 'mastodon/components/blurhash';
 | 
				
			||||||
import { Icon } from 'mastodon/components/icon';
 | 
					import { Icon } from 'mastodon/components/icon';
 | 
				
			||||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
					import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
				
			||||||
| 
						 | 
					@ -27,16 +27,15 @@ export const Upload: React.FC<{
 | 
				
			||||||
  wide?: boolean;
 | 
					  wide?: boolean;
 | 
				
			||||||
}> = ({ id, dragging, overlay, tall, wide }) => {
 | 
					}> = ({ id, dragging, overlay, tall, wide }) => {
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
  const media = useAppSelector(
 | 
					  const media = useAppSelector((state) =>
 | 
				
			||||||
    (state) =>
 | 
					    (
 | 
				
			||||||
      state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
 | 
					      (state.compose as ImmutableMap<string, unknown>).get(
 | 
				
			||||||
        .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
					        'media_attachments',
 | 
				
			||||||
        .find((item: MediaAttachment) => item.get('id') === id) as  // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
					      ) as ImmutableList<MediaAttachment>
 | 
				
			||||||
        | MediaAttachment
 | 
					    ).find((item) => item.get('id') === id),
 | 
				
			||||||
        | undefined,
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const sensitive = useAppSelector(
 | 
					  const sensitive = useAppSelector(
 | 
				
			||||||
    (state) => state.compose.get('spoiler') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
					    (state) => state.compose.get('spoiler') as boolean,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleUndoClick = useCallback(() => {
 | 
					  const handleUndoClick = useCallback(() => {
 | 
				
			||||||
| 
						 | 
					@ -44,7 +43,9 @@ export const Upload: React.FC<{
 | 
				
			||||||
  }, [dispatch, id]);
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleFocalPointClick = useCallback(() => {
 | 
					  const handleFocalPointClick = useCallback(() => {
 | 
				
			||||||
    dispatch(initMediaEditModal(id));
 | 
					    dispatch(
 | 
				
			||||||
 | 
					      openModal({ modalType: 'FOCAL_POINT', modalProps: { mediaId: id } }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }, [dispatch, id]);
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { attributes, listeners, setNodeRef, transform, transition } =
 | 
					  const { attributes, listeners, setNodeRef, transform, transition } =
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,11 @@ import { useState, useCallback, useMemo } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useIntl, defineMessages } from 'react-intl';
 | 
					import { useIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { List } from 'immutable';
 | 
					import type {
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  Map as ImmutableMap,
 | 
				
			||||||
 | 
					  List as ImmutableList,
 | 
				
			||||||
 | 
					} from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  DragStartEvent,
 | 
					  DragStartEvent,
 | 
				
			||||||
| 
						 | 
					@ -63,18 +67,20 @@ export const UploadForm: React.FC = () => {
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
  const mediaIds = useAppSelector(
 | 
					  const mediaIds = useAppSelector(
 | 
				
			||||||
    (state) =>
 | 
					    (state) =>
 | 
				
			||||||
      state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
 | 
					      (
 | 
				
			||||||
        .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
					        (state.compose as ImmutableMap<string, unknown>).get(
 | 
				
			||||||
        .map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
					          'media_attachments',
 | 
				
			||||||
 | 
					        ) as ImmutableList<MediaAttachment>
 | 
				
			||||||
 | 
					      ).map((item: MediaAttachment) => item.get('id')) as List<string>,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const active = useAppSelector(
 | 
					  const active = useAppSelector(
 | 
				
			||||||
    (state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
					    (state) => state.compose.get('is_uploading') as boolean,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const progress = useAppSelector(
 | 
					  const progress = useAppSelector(
 | 
				
			||||||
    (state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
					    (state) => state.compose.get('progress') as number,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const isProcessing = useAppSelector(
 | 
					  const isProcessing = useAppSelector(
 | 
				
			||||||
    (state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
					    (state) => state.compose.get('is_processing') as boolean,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
 | 
					  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
 | 
				
			||||||
  const sensors = useSensors(
 | 
					  const sensors = useSensors(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,9 @@ export const NotificationFollow: React.FC<{
 | 
				
			||||||
    const account = notification.sampleAccountIds[0];
 | 
					    const account = notification.sampleAccountIds[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (account) {
 | 
					    if (account) {
 | 
				
			||||||
      actions = <FollowButton accountId={notification.sampleAccountIds[0]} />;
 | 
					      actions = (
 | 
				
			||||||
 | 
					        <FollowButton compact accountId={notification.sampleAccountIds[0]} />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      additionalContent = <FollowerCount accountId={account} />;
 | 
					      additionalContent = <FollowerCount accountId={account} />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,6 @@ export const BoostModal: React.FC<{
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const defaultPrivacy = useAppSelector(
 | 
					  const defaultPrivacy = useAppSelector(
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
					 | 
				
			||||||
    (state) => state.compose.get('default_privacy') as StatusVisibility,
 | 
					    (state) => state.compose.get('default_privacy') as StatusVisibility,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,438 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
import { PureComponent } from 'react';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import classNames from 'classnames';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Textarea from 'react-textarea-autosize';
 | 
					 | 
				
			||||||
import { length } from 'stringz';
 | 
					 | 
				
			||||||
// eslint-disable-next-line import/extensions
 | 
					 | 
				
			||||||
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
 | 
					 | 
				
			||||||
// eslint-disable-next-line import/no-extraneous-dependencies
 | 
					 | 
				
			||||||
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 | 
					 | 
				
			||||||
import { Button } from 'mastodon/components/button';
 | 
					 | 
				
			||||||
import { GIFV } from 'mastodon/components/gifv';
 | 
					 | 
				
			||||||
import { IconButton } from 'mastodon/components/icon_button';
 | 
					 | 
				
			||||||
import Audio from 'mastodon/features/audio';
 | 
					 | 
				
			||||||
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
 | 
					 | 
				
			||||||
import { UploadProgress } from 'mastodon/features/compose/components/upload_progress';
 | 
					 | 
				
			||||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
 | 
					 | 
				
			||||||
import { me } from 'mastodon/initial_state';
 | 
					 | 
				
			||||||
import { assetHost } from 'mastodon/utils/config';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose';
 | 
					 | 
				
			||||||
import Video, { getPointerPosition } from '../../video';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const messages = defineMessages({
 | 
					 | 
				
			||||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
					 | 
				
			||||||
  apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
 | 
					 | 
				
			||||||
  applying: { id: 'upload_modal.applying', defaultMessage: 'Applying…' },
 | 
					 | 
				
			||||||
  placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
 | 
					 | 
				
			||||||
  chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
 | 
					 | 
				
			||||||
  discardMessage: { id: 'confirmations.discard_edit_media.message', defaultMessage: 'You have unsaved changes to the media description or preview, discard them anyway?' },
 | 
					 | 
				
			||||||
  discardConfirm: { id: 'confirmations.discard_edit_media.confirm', defaultMessage: 'Discard' },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapStateToProps = (state, { id }) => ({
 | 
					 | 
				
			||||||
  media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
 | 
					 | 
				
			||||||
  account: state.getIn(['accounts', me]),
 | 
					 | 
				
			||||||
  isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
 | 
					 | 
				
			||||||
  description: state.getIn(['compose', 'media_modal', 'description']),
 | 
					 | 
				
			||||||
  lang: state.getIn(['compose', 'language']),
 | 
					 | 
				
			||||||
  focusX: state.getIn(['compose', 'media_modal', 'focusX']),
 | 
					 | 
				
			||||||
  focusY: state.getIn(['compose', 'media_modal', 'focusY']),
 | 
					 | 
				
			||||||
  dirty: state.getIn(['compose', 'media_modal', 'dirty']),
 | 
					 | 
				
			||||||
  is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapDispatchToProps = (dispatch, { id }) => ({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onSave: (description, x, y) => {
 | 
					 | 
				
			||||||
    dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onChangeDescription: (description) => {
 | 
					 | 
				
			||||||
    dispatch(onChangeMediaDescription(description));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onChangeFocus: (focusX, focusY) => {
 | 
					 | 
				
			||||||
    dispatch(onChangeMediaFocus(focusX, focusY));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onSelectThumbnail: files => {
 | 
					 | 
				
			||||||
    dispatch(uploadThumbnail(id, files[0]));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
 | 
					 | 
				
			||||||
  .replace(/\n/g, ' ')
 | 
					 | 
				
			||||||
  .replace(/\*\*\*\*\*\*/g, '\n\n');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImageLoader extends PureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    src: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    width: PropTypes.number,
 | 
					 | 
				
			||||||
    height: PropTypes.number,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    loading: true,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount() {
 | 
					 | 
				
			||||||
    const image = new Image();
 | 
					 | 
				
			||||||
    image.addEventListener('load', () => this.setState({ loading: false }));
 | 
					 | 
				
			||||||
    image.src = this.props.src;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { loading } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (loading) {
 | 
					 | 
				
			||||||
      return <canvas width={this.props.width} height={this.props.height} />;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return <img {...this.props} alt='' />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class FocalPointModal extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    media: ImmutablePropTypes.map.isRequired,
 | 
					 | 
				
			||||||
    account: ImmutablePropTypes.record.isRequired,
 | 
					 | 
				
			||||||
    isUploadingThumbnail: PropTypes.bool,
 | 
					 | 
				
			||||||
    onSave: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onChangeDescription: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onChangeFocus: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onSelectThumbnail: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onClose: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    dragging: false,
 | 
					 | 
				
			||||||
    dirty: false,
 | 
					 | 
				
			||||||
    progress: 0,
 | 
					 | 
				
			||||||
    loading: true,
 | 
					 | 
				
			||||||
    ocrStatus: '',
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    document.removeEventListener('mousemove', this.handleMouseMove);
 | 
					 | 
				
			||||||
    document.removeEventListener('mouseup', this.handleMouseUp);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleMouseDown = e => {
 | 
					 | 
				
			||||||
    document.addEventListener('mousemove', this.handleMouseMove);
 | 
					 | 
				
			||||||
    document.addEventListener('mouseup', this.handleMouseUp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updatePosition(e);
 | 
					 | 
				
			||||||
    this.setState({ dragging: true });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTouchStart = e => {
 | 
					 | 
				
			||||||
    document.addEventListener('touchmove', this.handleMouseMove);
 | 
					 | 
				
			||||||
    document.addEventListener('touchend', this.handleTouchEnd);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.updatePosition(e);
 | 
					 | 
				
			||||||
    this.setState({ dragging: true });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleMouseMove = e => {
 | 
					 | 
				
			||||||
    this.updatePosition(e);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleMouseUp = () => {
 | 
					 | 
				
			||||||
    document.removeEventListener('mousemove', this.handleMouseMove);
 | 
					 | 
				
			||||||
    document.removeEventListener('mouseup', this.handleMouseUp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ dragging: false });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTouchEnd = () => {
 | 
					 | 
				
			||||||
    document.removeEventListener('touchmove', this.handleMouseMove);
 | 
					 | 
				
			||||||
    document.removeEventListener('touchend', this.handleTouchEnd);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ dragging: false });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  updatePosition = e => {
 | 
					 | 
				
			||||||
    const { x, y } = getPointerPosition(this.node, e);
 | 
					 | 
				
			||||||
    const focusX   = (x - .5) *  2;
 | 
					 | 
				
			||||||
    const focusY   = (y - .5) * -2;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.props.onChangeFocus(focusX, focusY);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleChange = e => {
 | 
					 | 
				
			||||||
    this.props.onChangeDescription(e.target.value);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleKeyDown = (e) => {
 | 
					 | 
				
			||||||
    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
 | 
					 | 
				
			||||||
      this.props.onChangeDescription(e.target.value);
 | 
					 | 
				
			||||||
      this.handleSubmit(e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleSubmit = (e) => {
 | 
					 | 
				
			||||||
    e.preventDefault();
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
    this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getCloseConfirmationMessage = () => {
 | 
					 | 
				
			||||||
    const { intl, dirty } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (dirty) {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        message: intl.formatMessage(messages.discardMessage),
 | 
					 | 
				
			||||||
        confirm: intl.formatMessage(messages.discardConfirm),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.node = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTextDetection = () => {
 | 
					 | 
				
			||||||
    this._detectText();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _detectText = (refreshCache = false) => {
 | 
					 | 
				
			||||||
    const { media } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ detecting: true });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fetchTesseract().then(({ createWorker }) => {
 | 
					 | 
				
			||||||
      const worker = createWorker({
 | 
					 | 
				
			||||||
        workerPath: tesseractWorkerPath,
 | 
					 | 
				
			||||||
        corePath: tesseractCorePath,
 | 
					 | 
				
			||||||
        langPath: `${assetHost}/ocr/lang-data`,
 | 
					 | 
				
			||||||
        logger: ({ status, progress }) => {
 | 
					 | 
				
			||||||
          if (status === 'recognizing text') {
 | 
					 | 
				
			||||||
            this.setState({ ocrStatus: 'detecting', progress });
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            this.setState({ ocrStatus: 'preparing', progress });
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        cacheMethod: refreshCache ? 'refresh' : 'write',
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let media_url = media.get('url');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (window.URL && URL.createObjectURL) {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          media_url = URL.createObjectURL(media.get('file'));
 | 
					 | 
				
			||||||
        } catch (error) {
 | 
					 | 
				
			||||||
          console.error(error);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return (async () => {
 | 
					 | 
				
			||||||
        await worker.load();
 | 
					 | 
				
			||||||
        await worker.loadLanguage('eng');
 | 
					 | 
				
			||||||
        await worker.initialize('eng');
 | 
					 | 
				
			||||||
        const { data: { text } } = await worker.recognize(media_url);
 | 
					 | 
				
			||||||
        this.setState({ detecting: false });
 | 
					 | 
				
			||||||
        this.props.onChangeDescription(removeExtraLineBreaks(text));
 | 
					 | 
				
			||||||
        await worker.terminate();
 | 
					 | 
				
			||||||
      })().catch((e) => {
 | 
					 | 
				
			||||||
        if (refreshCache) {
 | 
					 | 
				
			||||||
          throw e;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this._detectText(true);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }).catch((e) => {
 | 
					 | 
				
			||||||
      console.error(e);
 | 
					 | 
				
			||||||
      this.setState({ detecting: false });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleThumbnailChange = e => {
 | 
					 | 
				
			||||||
    if (e.target.files.length > 0) {
 | 
					 | 
				
			||||||
      this.props.onSelectThumbnail(e.target.files);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setFileInputRef = c => {
 | 
					 | 
				
			||||||
    this.fileInput = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleFileInputClick = () => {
 | 
					 | 
				
			||||||
    this.fileInput.click();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props;
 | 
					 | 
				
			||||||
    const { dragging, detecting, progress, ocrStatus } = this.state;
 | 
					 | 
				
			||||||
    const x = (focusX /  2) + .5;
 | 
					 | 
				
			||||||
    const y = (focusY / -2) + .5;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const width  = media.getIn(['meta', 'original', 'width']) || null;
 | 
					 | 
				
			||||||
    const height = media.getIn(['meta', 'original', 'height']) || null;
 | 
					 | 
				
			||||||
    const focals = ['image', 'gifv'].includes(media.get('type'));
 | 
					 | 
				
			||||||
    const thumbnailable = ['audio', 'video'].includes(media.get('type'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const previewRatio  = 16/9;
 | 
					 | 
				
			||||||
    const previewWidth  = 200;
 | 
					 | 
				
			||||||
    const previewHeight = previewWidth / previewRatio;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let descriptionLabel = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (media.get('type') === 'audio') {
 | 
					 | 
				
			||||||
      descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people who are hard of hearing' />;
 | 
					 | 
				
			||||||
    } else if (media.get('type') === 'video') {
 | 
					 | 
				
			||||||
      descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people who are deaf, hard of hearing, blind or have low vision' />;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for people who are blind or have low vision' />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let ocrMessage = '';
 | 
					 | 
				
			||||||
    if (ocrStatus === 'detecting') {
 | 
					 | 
				
			||||||
      ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
 | 
					 | 
				
			||||||
        <div className='report-modal__target'>
 | 
					 | 
				
			||||||
          <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={20} />
 | 
					 | 
				
			||||||
          <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className='report-modal__container'>
 | 
					 | 
				
			||||||
          <form className='report-modal__comment' onSubmit={this.handleSubmit} >
 | 
					 | 
				
			||||||
            {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {thumbnailable && (
 | 
					 | 
				
			||||||
              <>
 | 
					 | 
				
			||||||
                <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <label>
 | 
					 | 
				
			||||||
                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <input
 | 
					 | 
				
			||||||
                    id='upload-modal__thumbnail'
 | 
					 | 
				
			||||||
                    ref={this.setFileInputRef}
 | 
					 | 
				
			||||||
                    type='file'
 | 
					 | 
				
			||||||
                    accept='image/png,image/jpeg'
 | 
					 | 
				
			||||||
                    onChange={this.handleThumbnailChange}
 | 
					 | 
				
			||||||
                    style={{ display: 'none' }}
 | 
					 | 
				
			||||||
                    disabled={isUploadingThumbnail || is_changing_upload}
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <hr className='setting-divider' />
 | 
					 | 
				
			||||||
              </>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <label className='setting-text-label' htmlFor='upload-modal__description'>
 | 
					 | 
				
			||||||
              {descriptionLabel}
 | 
					 | 
				
			||||||
            </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div className='setting-text__wrapper'>
 | 
					 | 
				
			||||||
              <Textarea
 | 
					 | 
				
			||||||
                id='upload-modal__description'
 | 
					 | 
				
			||||||
                className='setting-text light'
 | 
					 | 
				
			||||||
                value={detecting ? '…' : description}
 | 
					 | 
				
			||||||
                lang={lang}
 | 
					 | 
				
			||||||
                onChange={this.handleChange}
 | 
					 | 
				
			||||||
                onKeyDown={this.handleKeyDown}
 | 
					 | 
				
			||||||
                disabled={detecting || is_changing_upload}
 | 
					 | 
				
			||||||
                autoFocus
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <div className='setting-text__modifiers'>
 | 
					 | 
				
			||||||
                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div className='setting-text__toolbar'>
 | 
					 | 
				
			||||||
              <button
 | 
					 | 
				
			||||||
                type='button'
 | 
					 | 
				
			||||||
                disabled={detecting || media.get('type') !== 'image' || is_changing_upload}
 | 
					 | 
				
			||||||
                className='link-button'
 | 
					 | 
				
			||||||
                onClick={this.handleTextDetection}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' />
 | 
					 | 
				
			||||||
              </button>
 | 
					 | 
				
			||||||
              <CharacterCounter max={1500} text={detecting ? '' : description} />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              type='submit'
 | 
					 | 
				
			||||||
              disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload}
 | 
					 | 
				
			||||||
              text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </form>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className='focal-point-modal__content'>
 | 
					 | 
				
			||||||
            {focals && (
 | 
					 | 
				
			||||||
              <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
 | 
					 | 
				
			||||||
                {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
 | 
					 | 
				
			||||||
                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <div className='focal-point__preview'>
 | 
					 | 
				
			||||||
                  <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
 | 
					 | 
				
			||||||
                  <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
 | 
					 | 
				
			||||||
                <div className='focal-point__overlay' />
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {media.get('type') === 'video' && (
 | 
					 | 
				
			||||||
              <Video
 | 
					 | 
				
			||||||
                preview={media.get('preview_url')}
 | 
					 | 
				
			||||||
                frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
 | 
					 | 
				
			||||||
                blurhash={media.get('blurhash')}
 | 
					 | 
				
			||||||
                src={media.get('url')}
 | 
					 | 
				
			||||||
                detailed
 | 
					 | 
				
			||||||
                inline
 | 
					 | 
				
			||||||
                editable
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {media.get('type') === 'audio' && (
 | 
					 | 
				
			||||||
              <Audio
 | 
					 | 
				
			||||||
                src={media.get('url')}
 | 
					 | 
				
			||||||
                duration={media.getIn(['meta', 'original', 'duration'], 0)}
 | 
					 | 
				
			||||||
                height={150}
 | 
					 | 
				
			||||||
                poster={media.get('preview_url') || account.get('avatar_static')}
 | 
					 | 
				
			||||||
                backgroundColor={media.getIn(['meta', 'colors', 'background'])}
 | 
					 | 
				
			||||||
                foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
 | 
					 | 
				
			||||||
                accentColor={media.getIn(['meta', 'colors', 'accent'])}
 | 
					 | 
				
			||||||
                editable
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default connect(mapStateToProps, mapDispatchToProps, null, {
 | 
					 | 
				
			||||||
  forwardRef: true,
 | 
					 | 
				
			||||||
})(injectIntl(FocalPointModal, { forwardRef: true }));
 | 
					 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import { PureComponent } from 'react';
 | 
				
			||||||
import { Helmet } from 'react-helmet';
 | 
					import { Helmet } from 'react-helmet';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Base from 'mastodon/components/modal_root';
 | 
					import Base from 'mastodon/components/modal_root';
 | 
				
			||||||
 | 
					import { AltTextModal } from 'mastodon/features/alt_text_modal';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  MuteModal,
 | 
					  MuteModal,
 | 
				
			||||||
  BlockModal,
 | 
					  BlockModal,
 | 
				
			||||||
| 
						 | 
					@ -37,7 +38,6 @@ import {
 | 
				
			||||||
  ConfirmLogOutModal,
 | 
					  ConfirmLogOutModal,
 | 
				
			||||||
  ConfirmFollowToListModal,
 | 
					  ConfirmFollowToListModal,
 | 
				
			||||||
} from './confirmation_modals';
 | 
					} from './confirmation_modals';
 | 
				
			||||||
import FocalPointModal from './focal_point_modal';
 | 
					 | 
				
			||||||
import ImageModal from './image_modal';
 | 
					import ImageModal from './image_modal';
 | 
				
			||||||
import MediaModal from './media_modal';
 | 
					import MediaModal from './media_modal';
 | 
				
			||||||
import { ModalPlaceholder } from './modal_placeholder';
 | 
					import { ModalPlaceholder } from './modal_placeholder';
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ export const MODAL_COMPONENTS = {
 | 
				
			||||||
  'REPORT': ReportModal,
 | 
					  'REPORT': ReportModal,
 | 
				
			||||||
  'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
 | 
					  'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
 | 
				
			||||||
  'EMBED': EmbedModal,
 | 
					  'EMBED': EmbedModal,
 | 
				
			||||||
  'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
 | 
					  'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
 | 
				
			||||||
  'LIST_ADDER': ListAdder,
 | 
					  'LIST_ADDER': ListAdder,
 | 
				
			||||||
  'COMPARE_HISTORY': CompareHistoryModal,
 | 
					  'COMPARE_HISTORY': CompareHistoryModal,
 | 
				
			||||||
  'FILTER': FilterModal,
 | 
					  'FILTER': FilterModal,
 | 
				
			||||||
| 
						 | 
					@ -139,8 +139,7 @@ export default class ModalRoot extends PureComponent {
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
 | 
					            <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
 | 
				
			||||||
              {(SpecificComponent) => {
 | 
					              {(SpecificComponent) => {
 | 
				
			||||||
                const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
 | 
					                return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />;
 | 
				
			||||||
                return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
 | 
					 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            </BundleContainer>
 | 
					            </BundleContainer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
    if (confirmationMessage) {
 | 
					    if (confirmationMessage) {
 | 
				
			||||||
      dispatch(
 | 
					      dispatch(
 | 
				
			||||||
        openModal({
 | 
					        openModal({
 | 
				
			||||||
 | 
					          previousModalProps: confirmationMessage.props,
 | 
				
			||||||
          modalType: 'CONFIRM',
 | 
					          modalType: 'CONFIRM',
 | 
				
			||||||
          modalProps: {
 | 
					          modalProps: {
 | 
				
			||||||
            message: confirmationMessage.message,
 | 
					            message: confirmationMessage.message,
 | 
				
			||||||
| 
						 | 
					@ -24,7 +25,8 @@ const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
              modalType: undefined,
 | 
					              modalType: undefined,
 | 
				
			||||||
              ignoreFocus: { ignoreFocus },
 | 
					              ignoreFocus: { ignoreFocus },
 | 
				
			||||||
            })),
 | 
					            })),
 | 
				
			||||||
          } }),
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      dispatch(closeModal({
 | 
					      dispatch(closeModal({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,6 +86,13 @@
 | 
				
			||||||
  "alert.unexpected.message": "An unexpected error occurred.",
 | 
					  "alert.unexpected.message": "An unexpected error occurred.",
 | 
				
			||||||
  "alert.unexpected.title": "Oops!",
 | 
					  "alert.unexpected.title": "Oops!",
 | 
				
			||||||
  "alt_text_badge.title": "Alt text",
 | 
					  "alt_text_badge.title": "Alt text",
 | 
				
			||||||
 | 
					  "alt_text_modal.add_alt_text": "Add alt text",
 | 
				
			||||||
 | 
					  "alt_text_modal.add_text_from_image": "Add text from image",
 | 
				
			||||||
 | 
					  "alt_text_modal.cancel": "Cancel",
 | 
				
			||||||
 | 
					  "alt_text_modal.change_thumbnail": "Change thumbnail",
 | 
				
			||||||
 | 
					  "alt_text_modal.describe_for_people_with_hearing_impairments": "Describe this for people with hearing impairments…",
 | 
				
			||||||
 | 
					  "alt_text_modal.describe_for_people_with_visual_impairments": "Describe this for people with visual impairments…",
 | 
				
			||||||
 | 
					  "alt_text_modal.done": "Done",
 | 
				
			||||||
  "announcement.announcement": "Announcement",
 | 
					  "announcement.announcement": "Announcement",
 | 
				
			||||||
  "annual_report.summary.archetype.booster": "The cool-hunter",
 | 
					  "annual_report.summary.archetype.booster": "The cool-hunter",
 | 
				
			||||||
  "annual_report.summary.archetype.lurker": "The lurker",
 | 
					  "annual_report.summary.archetype.lurker": "The lurker",
 | 
				
			||||||
| 
						 | 
					@ -875,26 +882,12 @@
 | 
				
			||||||
  "upload_button.label": "Add images, a video or an audio file",
 | 
					  "upload_button.label": "Add images, a video or an audio file",
 | 
				
			||||||
  "upload_error.limit": "File upload limit exceeded.",
 | 
					  "upload_error.limit": "File upload limit exceeded.",
 | 
				
			||||||
  "upload_error.poll": "File upload not allowed with polls.",
 | 
					  "upload_error.poll": "File upload not allowed with polls.",
 | 
				
			||||||
  "upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
 | 
					 | 
				
			||||||
  "upload_form.description": "Describe for people who are blind or have low vision",
 | 
					 | 
				
			||||||
  "upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
 | 
					  "upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
 | 
				
			||||||
  "upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
 | 
					  "upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
 | 
				
			||||||
  "upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
 | 
					  "upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
 | 
				
			||||||
  "upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.",
 | 
					  "upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.",
 | 
				
			||||||
  "upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
 | 
					  "upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
 | 
				
			||||||
  "upload_form.edit": "Edit",
 | 
					  "upload_form.edit": "Edit",
 | 
				
			||||||
  "upload_form.thumbnail": "Change thumbnail",
 | 
					 | 
				
			||||||
  "upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
 | 
					 | 
				
			||||||
  "upload_modal.analyzing_picture": "Analyzing picture…",
 | 
					 | 
				
			||||||
  "upload_modal.apply": "Apply",
 | 
					 | 
				
			||||||
  "upload_modal.applying": "Applying…",
 | 
					 | 
				
			||||||
  "upload_modal.choose_image": "Choose image",
 | 
					 | 
				
			||||||
  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
 | 
					 | 
				
			||||||
  "upload_modal.detect_text": "Detect text from picture",
 | 
					 | 
				
			||||||
  "upload_modal.edit_media": "Edit media",
 | 
					 | 
				
			||||||
  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
 | 
					 | 
				
			||||||
  "upload_modal.preparing_ocr": "Preparing OCR…",
 | 
					 | 
				
			||||||
  "upload_modal.preview_label": "Preview ({ratio})",
 | 
					 | 
				
			||||||
  "upload_progress.label": "Uploading...",
 | 
					  "upload_progress.label": "Uploading...",
 | 
				
			||||||
  "upload_progress.processing": "Processing…",
 | 
					  "upload_progress.processing": "Processing…",
 | 
				
			||||||
  "username.taken": "That username is taken. Try another",
 | 
					  "username.taken": "That username is taken. Try another",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 | 
					import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { changeUploadCompose } from 'mastodon/actions/compose_typed';
 | 
				
			||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
 | 
					import { timelineDelete } from 'mastodon/actions/timelines_typed';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -36,17 +37,11 @@ import {
 | 
				
			||||||
  COMPOSE_LANGUAGE_CHANGE,
 | 
					  COMPOSE_LANGUAGE_CHANGE,
 | 
				
			||||||
  COMPOSE_COMPOSING_CHANGE,
 | 
					  COMPOSE_COMPOSING_CHANGE,
 | 
				
			||||||
  COMPOSE_EMOJI_INSERT,
 | 
					  COMPOSE_EMOJI_INSERT,
 | 
				
			||||||
  COMPOSE_UPLOAD_CHANGE_REQUEST,
 | 
					 | 
				
			||||||
  COMPOSE_UPLOAD_CHANGE_SUCCESS,
 | 
					 | 
				
			||||||
  COMPOSE_UPLOAD_CHANGE_FAIL,
 | 
					 | 
				
			||||||
  COMPOSE_RESET,
 | 
					  COMPOSE_RESET,
 | 
				
			||||||
  COMPOSE_POLL_ADD,
 | 
					  COMPOSE_POLL_ADD,
 | 
				
			||||||
  COMPOSE_POLL_REMOVE,
 | 
					  COMPOSE_POLL_REMOVE,
 | 
				
			||||||
  COMPOSE_POLL_OPTION_CHANGE,
 | 
					  COMPOSE_POLL_OPTION_CHANGE,
 | 
				
			||||||
  COMPOSE_POLL_SETTINGS_CHANGE,
 | 
					  COMPOSE_POLL_SETTINGS_CHANGE,
 | 
				
			||||||
  INIT_MEDIA_EDIT_MODAL,
 | 
					 | 
				
			||||||
  COMPOSE_CHANGE_MEDIA_DESCRIPTION,
 | 
					 | 
				
			||||||
  COMPOSE_CHANGE_MEDIA_FOCUS,
 | 
					 | 
				
			||||||
  COMPOSE_CHANGE_MEDIA_ORDER,
 | 
					  COMPOSE_CHANGE_MEDIA_ORDER,
 | 
				
			||||||
  COMPOSE_SET_STATUS,
 | 
					  COMPOSE_SET_STATUS,
 | 
				
			||||||
  COMPOSE_FOCUS,
 | 
					  COMPOSE_FOCUS,
 | 
				
			||||||
| 
						 | 
					@ -87,13 +82,6 @@ const initialState = ImmutableMap({
 | 
				
			||||||
  resetFileKey: Math.floor((Math.random() * 0x10000)),
 | 
					  resetFileKey: Math.floor((Math.random() * 0x10000)),
 | 
				
			||||||
  idempotencyKey: null,
 | 
					  idempotencyKey: null,
 | 
				
			||||||
  tagHistory: ImmutableList(),
 | 
					  tagHistory: ImmutableList(),
 | 
				
			||||||
  media_modal: ImmutableMap({
 | 
					 | 
				
			||||||
    id: null,
 | 
					 | 
				
			||||||
    description: '',
 | 
					 | 
				
			||||||
    focusX: 0,
 | 
					 | 
				
			||||||
    focusY: 0,
 | 
					 | 
				
			||||||
    dirty: false,
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialPoll = ImmutableMap({
 | 
					const initialPoll = ImmutableMap({
 | 
				
			||||||
| 
						 | 
					@ -294,7 +282,24 @@ const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll',
 | 
				
			||||||
  return tmp;
 | 
					  return tmp;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function compose(state = initialState, action) {
 | 
					/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
 | 
				
			||||||
 | 
					export const composeReducer = (state = initialState, action) => {
 | 
				
			||||||
 | 
					  if (changeUploadCompose.fulfilled.match(action)) {
 | 
				
			||||||
 | 
					    return state
 | 
				
			||||||
 | 
					      .set('is_changing_upload', false)
 | 
				
			||||||
 | 
					      .update('media_attachments', list => list.map(item => {
 | 
				
			||||||
 | 
					        if (item.get('id') === action.payload.media.id) {
 | 
				
			||||||
 | 
					          return fromJS(action.payload.media).set('unattached', !action.payload.attached);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return item;
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					  } else if (changeUploadCompose.pending.match(action)) {
 | 
				
			||||||
 | 
					    return state.set('is_changing_upload', true);
 | 
				
			||||||
 | 
					  } else if (changeUploadCompose.rejected.match(action)) {
 | 
				
			||||||
 | 
					    return state.set('is_changing_upload', false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
  case STORE_HYDRATE:
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
    return hydrate(state, action.state.get('compose'));
 | 
					    return hydrate(state, action.state.get('compose'));
 | 
				
			||||||
| 
						 | 
					@ -369,16 +374,13 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  case COMPOSE_SUBMIT_REQUEST:
 | 
					  case COMPOSE_SUBMIT_REQUEST:
 | 
				
			||||||
    return state.set('is_submitting', true);
 | 
					    return state.set('is_submitting', true);
 | 
				
			||||||
  case COMPOSE_UPLOAD_CHANGE_REQUEST:
 | 
					
 | 
				
			||||||
    return state.set('is_changing_upload', true);
 | 
					 | 
				
			||||||
  case COMPOSE_REPLY_CANCEL:
 | 
					  case COMPOSE_REPLY_CANCEL:
 | 
				
			||||||
  case COMPOSE_RESET:
 | 
					  case COMPOSE_RESET:
 | 
				
			||||||
  case COMPOSE_SUBMIT_SUCCESS:
 | 
					  case COMPOSE_SUBMIT_SUCCESS:
 | 
				
			||||||
    return clearAll(state);
 | 
					    return clearAll(state);
 | 
				
			||||||
  case COMPOSE_SUBMIT_FAIL:
 | 
					  case COMPOSE_SUBMIT_FAIL:
 | 
				
			||||||
    return state.set('is_submitting', false);
 | 
					    return state.set('is_submitting', false);
 | 
				
			||||||
  case COMPOSE_UPLOAD_CHANGE_FAIL:
 | 
					 | 
				
			||||||
    return state.set('is_changing_upload', false);
 | 
					 | 
				
			||||||
  case COMPOSE_UPLOAD_REQUEST:
 | 
					  case COMPOSE_UPLOAD_REQUEST:
 | 
				
			||||||
    return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
 | 
					    return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
 | 
				
			||||||
  case COMPOSE_UPLOAD_PROCESSING:
 | 
					  case COMPOSE_UPLOAD_PROCESSING:
 | 
				
			||||||
| 
						 | 
					@ -407,20 +409,6 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return item;
 | 
					        return item;
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
  case INIT_MEDIA_EDIT_MODAL: {
 | 
					 | 
				
			||||||
    const media =  state.get('media_attachments').find(item => item.get('id') === action.id);
 | 
					 | 
				
			||||||
    return state.set('media_modal', ImmutableMap({
 | 
					 | 
				
			||||||
      id: action.id,
 | 
					 | 
				
			||||||
      description: media.get('description') || '',
 | 
					 | 
				
			||||||
      focusX: media.getIn(['meta', 'focus', 'x'], 0),
 | 
					 | 
				
			||||||
      focusY: media.getIn(['meta', 'focus', 'y'], 0),
 | 
					 | 
				
			||||||
      dirty: false,
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  case COMPOSE_CHANGE_MEDIA_DESCRIPTION:
 | 
					 | 
				
			||||||
    return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true);
 | 
					 | 
				
			||||||
  case COMPOSE_CHANGE_MEDIA_FOCUS:
 | 
					 | 
				
			||||||
    return state.setIn(['media_modal', 'focusX'], action.focusX).setIn(['media_modal', 'focusY'], action.focusY).setIn(['media_modal', 'dirty'], true);
 | 
					 | 
				
			||||||
  case COMPOSE_MENTION:
 | 
					  case COMPOSE_MENTION:
 | 
				
			||||||
    return state.withMutations(map => {
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
      map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
 | 
					      map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
 | 
				
			||||||
| 
						 | 
					@ -458,17 +446,6 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  case COMPOSE_EMOJI_INSERT:
 | 
					  case COMPOSE_EMOJI_INSERT:
 | 
				
			||||||
    return insertEmoji(state, action.position, action.emoji, action.needsSpace);
 | 
					    return insertEmoji(state, action.position, action.emoji, action.needsSpace);
 | 
				
			||||||
  case COMPOSE_UPLOAD_CHANGE_SUCCESS:
 | 
					 | 
				
			||||||
    return state
 | 
					 | 
				
			||||||
      .set('is_changing_upload', false)
 | 
					 | 
				
			||||||
      .setIn(['media_modal', 'dirty'], false)
 | 
					 | 
				
			||||||
      .update('media_attachments', list => list.map(item => {
 | 
					 | 
				
			||||||
        if (item.get('id') === action.media.id) {
 | 
					 | 
				
			||||||
          return fromJS(action.media).set('unattached', !action.attached);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return item;
 | 
					 | 
				
			||||||
      }));
 | 
					 | 
				
			||||||
  case REDRAFT:
 | 
					  case REDRAFT:
 | 
				
			||||||
    return state.withMutations(map => {
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
      map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
 | 
					      map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
 | 
				
			||||||
| 
						 | 
					@ -550,4 +527,4 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import { accountsReducer } from './accounts';
 | 
				
			||||||
import accounts_map from './accounts_map';
 | 
					import accounts_map from './accounts_map';
 | 
				
			||||||
import alerts from './alerts';
 | 
					import alerts from './alerts';
 | 
				
			||||||
import announcements from './announcements';
 | 
					import announcements from './announcements';
 | 
				
			||||||
import compose from './compose';
 | 
					import { composeReducer } from './compose';
 | 
				
			||||||
import contexts from './contexts';
 | 
					import contexts from './contexts';
 | 
				
			||||||
import conversations from './conversations';
 | 
					import conversations from './conversations';
 | 
				
			||||||
import custom_emojis from './custom_emojis';
 | 
					import custom_emojis from './custom_emojis';
 | 
				
			||||||
| 
						 | 
					@ -59,7 +59,7 @@ const reducers = {
 | 
				
			||||||
  push_notifications,
 | 
					  push_notifications,
 | 
				
			||||||
  server,
 | 
					  server,
 | 
				
			||||||
  contexts,
 | 
					  contexts,
 | 
				
			||||||
  compose,
 | 
					  compose: composeReducer,
 | 
				
			||||||
  search: searchReducer,
 | 
					  search: searchReducer,
 | 
				
			||||||
  media_attachments,
 | 
					  media_attachments,
 | 
				
			||||||
  notifications,
 | 
					  notifications,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,6 @@ import { Record as ImmutableRecord, Stack } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
 | 
					import { timelineDelete } from 'mastodon/actions/timelines_typed';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
 | 
					 | 
				
			||||||
import type { ModalType } from '../actions/modal';
 | 
					import type { ModalType } from '../actions/modal';
 | 
				
			||||||
import { openModal, closeModal } from '../actions/modal';
 | 
					import { openModal, closeModal } from '../actions/modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,12 +52,36 @@ const pushModal = (
 | 
				
			||||||
  state: State,
 | 
					  state: State,
 | 
				
			||||||
  modalType: ModalType,
 | 
					  modalType: ModalType,
 | 
				
			||||||
  modalProps: ModalProps,
 | 
					  modalProps: ModalProps,
 | 
				
			||||||
 | 
					  previousModalProps?: ModalProps,
 | 
				
			||||||
): State => {
 | 
					): State => {
 | 
				
			||||||
  return state.withMutations((record) => {
 | 
					  return state.withMutations((record) => {
 | 
				
			||||||
    record.set('ignoreFocus', false);
 | 
					    record.set('ignoreFocus', false);
 | 
				
			||||||
    record.update('stack', (stack) =>
 | 
					    record.update('stack', (stack) => {
 | 
				
			||||||
      stack.unshift(Modal({ modalType, modalProps })),
 | 
					      let tmp = stack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // With this option, we update the previously opened modal, so that when the
 | 
				
			||||||
 | 
					      // current (new) modal is closed, the previous modal is re-opened with different
 | 
				
			||||||
 | 
					      // props. Specifically, this is useful for the confirmation modal.
 | 
				
			||||||
 | 
					      if (previousModalProps) {
 | 
				
			||||||
 | 
					        const previousModal = tmp.first() as Modal | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (previousModal) {
 | 
				
			||||||
 | 
					          tmp = tmp.shift().unshift(
 | 
				
			||||||
 | 
					            Modal({
 | 
				
			||||||
 | 
					              modalType: previousModal.modalType,
 | 
				
			||||||
 | 
					              modalProps: {
 | 
				
			||||||
 | 
					                ...previousModal.modalProps,
 | 
				
			||||||
 | 
					                ...previousModalProps,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      tmp = tmp.unshift(Modal({ modalType, modalProps }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return tmp;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,11 +91,10 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
 | 
				
			||||||
      state,
 | 
					      state,
 | 
				
			||||||
      action.payload.modalType,
 | 
					      action.payload.modalType,
 | 
				
			||||||
      action.payload.modalProps,
 | 
					      action.payload.modalProps,
 | 
				
			||||||
 | 
					      action.payload.previousModalProps,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  else if (closeModal.match(action)) return popModal(state, action.payload);
 | 
					  else if (closeModal.match(action)) return popModal(state, action.payload);
 | 
				
			||||||
  // TODO: type those actions
 | 
					  // TODO: type those actions
 | 
				
			||||||
  else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
 | 
					 | 
				
			||||||
    return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
 | 
					 | 
				
			||||||
  else if (timelineDelete.match(action))
 | 
					  else if (timelineDelete.match(action))
 | 
				
			||||||
    return state.update('stack', (stack) =>
 | 
					    return state.update('stack', (stack) =>
 | 
				
			||||||
      stack.filterNot(
 | 
					      stack.filterNot(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -127,9 +127,7 @@
 | 
				
			||||||
.actions-modal ul li:not(:empty) a:focus button,
 | 
					.actions-modal ul li:not(:empty) a:focus button,
 | 
				
			||||||
.actions-modal ul li:not(:empty) a:hover,
 | 
					.actions-modal ul li:not(:empty) a:hover,
 | 
				
			||||||
.actions-modal ul li:not(:empty) a:hover button,
 | 
					.actions-modal ul li:not(:empty) a:hover button,
 | 
				
			||||||
.simple_form .block-button,
 | 
					.simple_form button:not(.button, .link-button) {
 | 
				
			||||||
.simple_form .button,
 | 
					 | 
				
			||||||
.simple_form button {
 | 
					 | 
				
			||||||
  color: $white;
 | 
					  color: $white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -142,6 +140,11 @@
 | 
				
			||||||
  border-top-color: lighten($ui-base-color, 4%);
 | 
					  border-top-color: lighten($ui-base-color, 4%);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dialog-modal__content__preview {
 | 
				
			||||||
 | 
					  background: #fff;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--modal-border-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.reactions-bar__item:hover,
 | 
					.reactions-bar__item:hover,
 | 
				
			||||||
.reactions-bar__item:focus,
 | 
					.reactions-bar__item:focus,
 | 
				
			||||||
.reactions-bar__item:active {
 | 
					.reactions-bar__item:active {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,6 +85,14 @@
 | 
				
			||||||
    outline: $ui-button-icon-focus-outline;
 | 
					    outline: $ui-button-icon-focus-outline;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--compact {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					    line-height: normal;
 | 
				
			||||||
 | 
					    font-weight: 700;
 | 
				
			||||||
 | 
					    padding: 5px 12px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &--dangerous {
 | 
					  &--dangerous {
 | 
				
			||||||
    background-color: var(--error-background-color);
 | 
					    background-color: var(--error-background-color);
 | 
				
			||||||
    color: var(--on-error-color);
 | 
					    color: var(--on-error-color);
 | 
				
			||||||
| 
						 | 
					@ -3734,58 +3742,6 @@ $ui-header-logo-wordmark-width: 99px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.setting-text {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  background: $ui-base-color;
 | 
					 | 
				
			||||||
  padding: 7px 10px;
 | 
					 | 
				
			||||||
  font-family: inherit;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  line-height: 22px;
 | 
					 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
  border: 1px solid var(--background-border-color);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:focus {
 | 
					 | 
				
			||||||
    outline: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &__wrapper {
 | 
					 | 
				
			||||||
    background: $ui-base-color;
 | 
					 | 
				
			||||||
    border: 1px solid var(--background-border-color);
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .setting-text {
 | 
					 | 
				
			||||||
      border: 0;
 | 
					 | 
				
			||||||
      margin-bottom: 0;
 | 
					 | 
				
			||||||
      border-radius: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:focus {
 | 
					 | 
				
			||||||
        border: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__modifiers {
 | 
					 | 
				
			||||||
      color: $inverted-text-color;
 | 
					 | 
				
			||||||
      font-family: inherit;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      background: $white;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &__toolbar {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: space-between;
 | 
					 | 
				
			||||||
    margin-bottom: 20px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (width <= 600px) {
 | 
					 | 
				
			||||||
    font-size: 16px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-card {
 | 
					.status-card {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
| 
						 | 
					@ -6104,6 +6060,35 @@ a.status-card {
 | 
				
			||||||
      gap: 16px;
 | 
					      gap: 16px;
 | 
				
			||||||
      padding: 24px;
 | 
					      padding: 24px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__preview {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 16px;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      padding: 24px;
 | 
				
			||||||
 | 
					      background: #000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img,
 | 
				
			||||||
 | 
					      .gifv video {
 | 
				
			||||||
 | 
					        outline: 1px solid var(--media-outline-color);
 | 
				
			||||||
 | 
					        outline-offset: -1px;
 | 
				
			||||||
 | 
					        border-radius: 8px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img,
 | 
				
			||||||
 | 
					      .gifv video,
 | 
				
			||||||
 | 
					      .video-player,
 | 
				
			||||||
 | 
					      .audio-player {
 | 
				
			||||||
 | 
					        max-width: 360px;
 | 
				
			||||||
 | 
					        max-height: 45vh;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .copy-paste-text {
 | 
					  .copy-paste-text {
 | 
				
			||||||
| 
						 | 
					@ -6450,62 +6435,6 @@ a.status-card {
 | 
				
			||||||
  margin-bottom: 29px;
 | 
					  margin-bottom: 29px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.report-modal__comment {
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  border-inline-end: 1px solid var(--background-border-color);
 | 
					 | 
				
			||||||
  max-width: 320px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  p {
 | 
					 | 
				
			||||||
    font-size: 14px;
 | 
					 | 
				
			||||||
    line-height: 20px;
 | 
					 | 
				
			||||||
    margin-bottom: 20px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .setting-text-label {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    color: $secondary-text-color;
 | 
					 | 
				
			||||||
    font-size: 14px;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .setting-text {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    resize: none;
 | 
					 | 
				
			||||||
    min-height: 100px;
 | 
					 | 
				
			||||||
    max-height: 50vh;
 | 
					 | 
				
			||||||
    border: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @media screen and (height <= 600px) {
 | 
					 | 
				
			||||||
      max-height: 20vh;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @media screen and (max-width: $no-columns-breakpoint) {
 | 
					 | 
				
			||||||
      max-height: 20vh;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .setting-toggle {
 | 
					 | 
				
			||||||
    margin-top: 20px;
 | 
					 | 
				
			||||||
    margin-bottom: 24px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__label {
 | 
					 | 
				
			||||||
      color: $inverted-text-color;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (width <= 480px) {
 | 
					 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
    max-width: 100%;
 | 
					 | 
				
			||||||
    order: 2;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .setting-toggle {
 | 
					 | 
				
			||||||
      margin-bottom: 4px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.actions-modal {
 | 
					.actions-modal {
 | 
				
			||||||
  max-height: 80vh;
 | 
					  max-height: 80vh;
 | 
				
			||||||
  max-width: 80vw;
 | 
					  max-width: 80vw;
 | 
				
			||||||
| 
						 | 
					@ -6998,11 +6927,6 @@ a.status-card {
 | 
				
			||||||
  outline: 1px solid var(--media-outline-color);
 | 
					  outline: 1px solid var(--media-outline-color);
 | 
				
			||||||
  outline-offset: -1px;
 | 
					  outline-offset: -1px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.editable {
 | 
					 | 
				
			||||||
    border-radius: 0;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.inactive {
 | 
					  &.inactive {
 | 
				
			||||||
    audio,
 | 
					    audio,
 | 
				
			||||||
    .video-player__controls {
 | 
					    .video-player__controls {
 | 
				
			||||||
| 
						 | 
					@ -7071,11 +6995,6 @@ a.status-card {
 | 
				
			||||||
  outline-offset: -1px;
 | 
					  outline-offset: -1px;
 | 
				
			||||||
  z-index: 2;
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.editable {
 | 
					 | 
				
			||||||
    border-radius: 0;
 | 
					 | 
				
			||||||
    height: 100% !important;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  video {
 | 
					  video {
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    z-index: -2;
 | 
					    z-index: -2;
 | 
				
			||||||
| 
						 | 
					@ -7381,6 +7300,14 @@ a.status-card {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.gifv {
 | 
					.gifv {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  canvas {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  video {
 | 
					  video {
 | 
				
			||||||
    max-width: 100vw;
 | 
					    max-width: 100vw;
 | 
				
			||||||
    max-height: 80vh;
 | 
					    max-height: 80vh;
 | 
				
			||||||
| 
						 | 
					@ -7686,24 +7613,14 @@ noscript {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.focal-point {
 | 
					.focal-point {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  cursor: move;
 | 
					  cursor: grab;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  background: $base-shadow-color;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  img,
 | 
					  &.dragging {
 | 
				
			||||||
  video,
 | 
					    cursor: grabbing;
 | 
				
			||||||
  canvas {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    max-height: 80vh;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    height: auto;
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    object-fit: contain;
 | 
					 | 
				
			||||||
    background: $base-shadow-color;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__reticle {
 | 
					  &__reticle {
 | 
				
			||||||
| 
						 | 
					@ -7711,54 +7628,10 @@ noscript {
 | 
				
			||||||
    width: 100px;
 | 
					    width: 100px;
 | 
				
			||||||
    height: 100px;
 | 
					    height: 100px;
 | 
				
			||||||
    transform: translate(-50%, -50%);
 | 
					    transform: translate(-50%, -50%);
 | 
				
			||||||
    background: url('../images/reticle.png') no-repeat 0 0;
 | 
					    border: 2px solid #fff;
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
    box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
 | 
					    box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
 | 
				
			||||||
  }
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  &__overlay {
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    inset-inline-start: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &__preview {
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    bottom: 10px;
 | 
					 | 
				
			||||||
    inset-inline-end: 10px;
 | 
					 | 
				
			||||||
    z-index: 2;
 | 
					 | 
				
			||||||
    cursor: move;
 | 
					 | 
				
			||||||
    transition: opacity 0.1s ease;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:hover {
 | 
					 | 
				
			||||||
      opacity: 0.5;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    strong {
 | 
					 | 
				
			||||||
      color: $primary-text-color;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      font-weight: 500;
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      margin-bottom: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    div {
 | 
					 | 
				
			||||||
      border-radius: 4px;
 | 
					 | 
				
			||||||
      box-shadow: 0 0 14px rgba($base-shadow-color, 0.2);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (width <= 480px) {
 | 
					 | 
				
			||||||
    img,
 | 
					 | 
				
			||||||
    video {
 | 
					 | 
				
			||||||
      max-height: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__preview {
 | 
					 | 
				
			||||||
      display: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10468,12 +10341,7 @@ noscript {
 | 
				
			||||||
.compose-form__actions {
 | 
					.compose-form__actions {
 | 
				
			||||||
  .button {
 | 
					  .button {
 | 
				
			||||||
    display: block; // Otherwise text-ellipsis doesn't work
 | 
					    display: block; // Otherwise text-ellipsis doesn't work
 | 
				
			||||||
    font-size: 14px;
 | 
					 | 
				
			||||||
    line-height: normal;
 | 
					 | 
				
			||||||
    font-weight: 700;
 | 
					 | 
				
			||||||
    flex: 1 1 auto;
 | 
					    flex: 1 1 auto;
 | 
				
			||||||
    padding: 5px 12px;
 | 
					 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,6 +76,18 @@ code {
 | 
				
			||||||
    margin-bottom: 16px;
 | 
					    margin-bottom: 16px;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:last-child {
 | 
				
			||||||
 | 
					      margin-bottom: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__toolbar {
 | 
				
			||||||
 | 
					      margin-top: 16px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: space-between;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      gap: 16px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.hidden {
 | 
					    &.hidden {
 | 
				
			||||||
      margin: 0;
 | 
					      margin: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -540,6 +552,7 @@ code {
 | 
				
			||||||
  .actions {
 | 
					  .actions {
 | 
				
			||||||
    margin-top: 30px;
 | 
					    margin-top: 30px;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    gap: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.actions--top {
 | 
					    &.actions--top {
 | 
				
			||||||
      margin-top: 0;
 | 
					      margin-top: 0;
 | 
				
			||||||
| 
						 | 
					@ -552,9 +565,7 @@ code {
 | 
				
			||||||
    margin-bottom: 15px;
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  button,
 | 
					  button:not(.button, .link-button) {
 | 
				
			||||||
  .button,
 | 
					 | 
				
			||||||
  .block-button {
 | 
					 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    border: 0;
 | 
					    border: 0;
 | 
				
			||||||
| 
						 | 
					@ -629,6 +640,18 @@ code {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .label_input {
 | 
					  .label_input {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__loading-indicator {
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      top: 0;
 | 
				
			||||||
 | 
					      inset-inline-start: 0;
 | 
				
			||||||
 | 
					      border: 1px solid transparent;
 | 
				
			||||||
 | 
					      padding: 10px 16px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__wrapper {
 | 
					    &__wrapper {
 | 
				
			||||||
      position: relative;
 | 
					      position: relative;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					declare module 'tesseract.js-core/tesseract-core.wasm.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare module 'tesseract.js/dist/worker.min.js';
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .actions
 | 
					  .actions
 | 
				
			||||||
    %button.button= t('admin.accounts.search')
 | 
					    %button.button= t('admin.accounts.search')
 | 
				
			||||||
    = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
 | 
					    = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button button--dangerous'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%hr.spacer/
 | 
					%hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .actions
 | 
					    .actions
 | 
				
			||||||
      %button.button= t('admin.accounts.search')
 | 
					      %button.button= t('admin.accounts.search')
 | 
				
			||||||
      = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative'
 | 
					      = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button button--dangerous'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= form_with model: @form, url: batch_admin_custom_emojis_path do |f|
 | 
					= form_with model: @form, url: batch_admin_custom_emojis_path do |f|
 | 
				
			||||||
  = hidden_field_tag :page, params[:page] || 1
 | 
					  = hidden_field_tag :page, params[:page] || 1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,4 +19,4 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .actions
 | 
					  .actions
 | 
				
			||||||
    = link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary'
 | 
					    = link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary'
 | 
				
			||||||
    = f.button :submit, t('.confirm'), class: 'button negative', name: :confirm
 | 
					    = f.button :submit, t('.confirm'), class: 'button button--dangerous', name: :confirm
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .actions
 | 
					      .actions
 | 
				
			||||||
        %button.button= t('admin.accounts.search')
 | 
					        %button.button= t('admin.accounts.search')
 | 
				
			||||||
        = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative'
 | 
					        = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button button--dangerous'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%hr.spacer/
 | 
					%hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.simple_form
 | 
					.simple_form
 | 
				
			||||||
  %p.hint= t('admin.relays.description_html')
 | 
					  %p.hint= t('admin.relays.description_html')
 | 
				
			||||||
  = link_to @relays.empty? ? t('admin.relays.setup') : t('admin.relays.add_new'), new_admin_relay_path, class: 'block-button'
 | 
					  = link_to @relays.empty? ? t('admin.relays.setup') : t('admin.relays.add_new'), new_admin_relay_path, class: 'button button--block'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- unless @relays.empty?
 | 
					- unless @relays.empty?
 | 
				
			||||||
  %hr.spacer
 | 
					  %hr.spacer
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .actions
 | 
					    .actions
 | 
				
			||||||
      %button.button= t('admin.accounts.search')
 | 
					      %button.button= t('admin.accounts.search')
 | 
				
			||||||
      = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative'
 | 
					      = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button button--dangerous'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- @reports.group_by(&:target_account_id).each_value do |reports|
 | 
					- @reports.group_by(&:target_account_id).each_value do |reports|
 | 
				
			||||||
  - target_account = reports.first.target_account
 | 
					  - target_account = reports.first.target_account
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .actions
 | 
					  .actions
 | 
				
			||||||
    %button.button= t('admin.tags.search')
 | 
					    %button.button= t('admin.tags.search')
 | 
				
			||||||
    = link_to t('admin.tags.reset'), admin_tags_path, class: 'button negative'
 | 
					    = link_to t('admin.tags.reset'), admin_tags_path, class: 'button button--dangerous'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%hr.spacer/
 | 
					%hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,4 +6,4 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %hr.spacer/
 | 
					  %hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  = link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'block-button'
 | 
					  = link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'button button--block'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,4 +14,4 @@
 | 
				
			||||||
%hr.spacer/
 | 
					%hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.simple_form
 | 
					.simple_form
 | 
				
			||||||
  = link_to t('webauthn_credentials.add'), new_settings_webauthn_credential_path, class: 'block-button'
 | 
					  = link_to t('webauthn_credentials.add'), new_settings_webauthn_credential_path, class: 'button button--block'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,4 +38,4 @@
 | 
				
			||||||
%hr.spacer/
 | 
					%hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.simple_form
 | 
					.simple_form
 | 
				
			||||||
  = link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
 | 
					  = link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'button button--block'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,8 @@
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  test: [
 | 
					  test: [
 | 
				
			||||||
    /tesseract\.js\/dist\/worker\.min\.js$/,
 | 
					    /tesseract\.js\/dist\/worker\.min\.js$/,
 | 
				
			||||||
    /tesseract\.js\/dist\/worker\.min\.js.map$/,
 | 
					    /tesseract\.js\/dist\/worker\.min\.js\.map$/,
 | 
				
			||||||
    /tesseract\.js-core\/tesseract-core\.wasm$/,
 | 
					    /tesseract\.js-core\/tesseract-core\.wasm\.js$/,
 | 
				
			||||||
    /tesseract\.js-core\/tesseract-core\.wasm.js$/,
 | 
					 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  use: {
 | 
					  use: {
 | 
				
			||||||
    loader: 'file-loader',
 | 
					    loader: 'file-loader',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,7 +123,7 @@
 | 
				
			||||||
    "stringz": "^2.1.0",
 | 
					    "stringz": "^2.1.0",
 | 
				
			||||||
    "substring-trie": "^1.0.2",
 | 
					    "substring-trie": "^1.0.2",
 | 
				
			||||||
    "terser-webpack-plugin": "^4.2.3",
 | 
					    "terser-webpack-plugin": "^4.2.3",
 | 
				
			||||||
    "tesseract.js": "^2.1.5",
 | 
					    "tesseract.js": "^6.0.0",
 | 
				
			||||||
    "tiny-queue": "^0.2.1",
 | 
					    "tiny-queue": "^0.2.1",
 | 
				
			||||||
    "twitter-text": "3.1.0",
 | 
					    "twitter-text": "3.1.0",
 | 
				
			||||||
    "use-debounce": "^10.0.0",
 | 
					    "use-debounce": "^10.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,8 @@ RSpec.describe 'OCR', :attachment_processing, :inline_jobs, :js, :streaming do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    click_on('Detect text from picture')
 | 
					    click_on('Add text from image')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(page).to have_css('#upload-modal__description', text: /Hello Mastodon\s*/, wait: 10)
 | 
					    expect(page).to have_css('#description', text: /Hello Mastodon\s*/, wait: 10)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										121
									
								
								yarn.lock
								
								
								
								
							
							
						
						
									
										121
									
								
								yarn.lock
								
								
								
								
							| 
						 | 
					@ -2958,7 +2958,7 @@ __metadata:
 | 
				
			||||||
    stylelint-config-standard-scss: "npm:^14.0.0"
 | 
					    stylelint-config-standard-scss: "npm:^14.0.0"
 | 
				
			||||||
    substring-trie: "npm:^1.0.2"
 | 
					    substring-trie: "npm:^1.0.2"
 | 
				
			||||||
    terser-webpack-plugin: "npm:^4.2.3"
 | 
					    terser-webpack-plugin: "npm:^4.2.3"
 | 
				
			||||||
    tesseract.js: "npm:^2.1.5"
 | 
					    tesseract.js: "npm:^6.0.0"
 | 
				
			||||||
    tiny-queue: "npm:^0.2.1"
 | 
					    tiny-queue: "npm:^0.2.1"
 | 
				
			||||||
    twitter-text: "npm:3.1.0"
 | 
					    twitter-text: "npm:3.1.0"
 | 
				
			||||||
    typescript: "npm:^5.0.4"
 | 
					    typescript: "npm:^5.0.4"
 | 
				
			||||||
| 
						 | 
					@ -5580,13 +5580,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"blueimp-load-image@npm:^3.0.0":
 | 
					 | 
				
			||||||
  version: 3.0.0
 | 
					 | 
				
			||||||
  resolution: "blueimp-load-image@npm:3.0.0"
 | 
					 | 
				
			||||||
  checksum: 10c0/e860da4113afd8e58bc026fb17240007e15dc155287a70fb57b3048fc8f0aa5f7dbd052efed8bff19d1208eeab4d058dc6788684a721c50ccd08b68d836a8d18
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"blurhash@npm:^2.0.5":
 | 
					"blurhash@npm:^2.0.5":
 | 
				
			||||||
  version: 2.0.5
 | 
					  version: 2.0.5
 | 
				
			||||||
  resolution: "blurhash@npm:2.0.5"
 | 
					  resolution: "blurhash@npm:2.0.5"
 | 
				
			||||||
| 
						 | 
					@ -6324,13 +6317,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"colors@npm:^1.4.0":
 | 
					 | 
				
			||||||
  version: 1.4.0
 | 
					 | 
				
			||||||
  resolution: "colors@npm:1.4.0"
 | 
					 | 
				
			||||||
  checksum: 10c0/9af357c019da3c5a098a301cf64e3799d27549d8f185d86f79af23069e4f4303110d115da98483519331f6fb71c8568d5688fa1c6523600044fd4a54e97c4efb
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"combined-stream@npm:^1.0.8":
 | 
					"combined-stream@npm:^1.0.8":
 | 
				
			||||||
  version: 1.0.8
 | 
					  version: 1.0.8
 | 
				
			||||||
  resolution: "combined-stream@npm:1.0.8"
 | 
					  resolution: "combined-stream@npm:1.0.8"
 | 
				
			||||||
| 
						 | 
					@ -8666,13 +8652,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"file-type@npm:^12.4.1":
 | 
					 | 
				
			||||||
  version: 12.4.2
 | 
					 | 
				
			||||||
  resolution: "file-type@npm:12.4.2"
 | 
					 | 
				
			||||||
  checksum: 10c0/26a307262a2a0b41ea83136550fbe83d8b502d080778b6577e0336fbfe9e919e1f871a286a6eb59f668425f60ebb19402fcb6c0443af58446d33c63362554e1d
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"file-uri-to-path@npm:1.0.0":
 | 
					"file-uri-to-path@npm:1.0.0":
 | 
				
			||||||
  version: 1.0.0
 | 
					  version: 1.0.0
 | 
				
			||||||
  resolution: "file-uri-to-path@npm:1.0.0"
 | 
					  resolution: "file-uri-to-path@npm:1.0.0"
 | 
				
			||||||
| 
						 | 
					@ -9704,10 +9683,10 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"idb-keyval@npm:^3.2.0":
 | 
					"idb-keyval@npm:^6.2.0":
 | 
				
			||||||
  version: 3.2.0
 | 
					  version: 6.2.1
 | 
				
			||||||
  resolution: "idb-keyval@npm:3.2.0"
 | 
					  resolution: "idb-keyval@npm:6.2.1"
 | 
				
			||||||
  checksum: 10c0/9b1f65d5f08630ef444a89334370c394175b1543f157621b36a3bc5e5208946f3f0ab5d5e24c74e81f2ef54b55b742b4e5b439c561f62695ffb69a06b0bce8e1
 | 
					  checksum: 10c0/9f0c83703a365e00bd0b4ed6380ce509a06dedfc6ec39b2ba5740085069fd2f2ff5c14ba19356488e3612a2f9c49985971982d836460a982a5d0b4019eeba48a
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10165,13 +10144,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"is-electron@npm:^2.2.0":
 | 
					 | 
				
			||||||
  version: 2.2.2
 | 
					 | 
				
			||||||
  resolution: "is-electron@npm:2.2.2"
 | 
					 | 
				
			||||||
  checksum: 10c0/327bb373f7be01b16cdff3998b5ddaa87d28f576092affaa7fe0659571b3306fdd458afbf0683a66841e7999af13f46ad0e1b51647b469526cd05a4dd736438a
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1":
 | 
					"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1":
 | 
				
			||||||
  version: 0.1.1
 | 
					  version: 0.1.1
 | 
				
			||||||
  resolution: "is-extendable@npm:0.1.1"
 | 
					  resolution: "is-extendable@npm:0.1.1"
 | 
				
			||||||
| 
						 | 
					@ -11145,28 +11117,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"jpeg-autorotate@npm:^7.1.1":
 | 
					 | 
				
			||||||
  version: 7.1.1
 | 
					 | 
				
			||||||
  resolution: "jpeg-autorotate@npm:7.1.1"
 | 
					 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    colors: "npm:^1.4.0"
 | 
					 | 
				
			||||||
    glob: "npm:^7.1.6"
 | 
					 | 
				
			||||||
    jpeg-js: "npm:^0.4.2"
 | 
					 | 
				
			||||||
    piexifjs: "npm:^1.0.6"
 | 
					 | 
				
			||||||
    yargs-parser: "npm:^20.2.1"
 | 
					 | 
				
			||||||
  bin:
 | 
					 | 
				
			||||||
    jpeg-autorotate: src/cli.js
 | 
					 | 
				
			||||||
  checksum: 10c0/75328e15b7abcaf8b36c980495cb0b37ffabeb8921e8576312deac8139a9e8a66f85d9196f314120e4633c76623d1b595e65ca7a87b679511ffb804f880a1644
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"jpeg-js@npm:^0.4.2":
 | 
					 | 
				
			||||||
  version: 0.4.4
 | 
					 | 
				
			||||||
  resolution: "jpeg-js@npm:0.4.4"
 | 
					 | 
				
			||||||
  checksum: 10c0/4d0d5097f8e55d8bbce6f1dc32ffaf3f43f321f6222e4e6490734fdc6d005322e3bd6fb992c2df7f5b587343b1441a1c333281dc3285bc9116e369fd2a2b43a7
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
 | 
					"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
 | 
				
			||||||
  version: 4.0.0
 | 
					  version: 4.0.0
 | 
				
			||||||
  resolution: "js-tokens@npm:4.0.0"
 | 
					  resolution: "js-tokens@npm:4.0.0"
 | 
				
			||||||
| 
						 | 
					@ -12407,9 +12357,9 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"node-fetch@npm:^2.6.0":
 | 
					"node-fetch@npm:^2.6.9":
 | 
				
			||||||
  version: 2.6.11
 | 
					  version: 2.7.0
 | 
				
			||||||
  resolution: "node-fetch@npm:2.6.11"
 | 
					  resolution: "node-fetch@npm:2.7.0"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    whatwg-url: "npm:^5.0.0"
 | 
					    whatwg-url: "npm:^5.0.0"
 | 
				
			||||||
  peerDependencies:
 | 
					  peerDependencies:
 | 
				
			||||||
| 
						 | 
					@ -12417,7 +12367,7 @@ __metadata:
 | 
				
			||||||
  peerDependenciesMeta:
 | 
					  peerDependenciesMeta:
 | 
				
			||||||
    encoding:
 | 
					    encoding:
 | 
				
			||||||
      optional: true
 | 
					      optional: true
 | 
				
			||||||
  checksum: 10c0/3ec847ca43f678d07b80abfd85bdf06523c2554ee9a494c992c5fc61f5d9cde9f9f16aa33ff09a62f19eee9d54813b8850d7f054cdfee8b2daf789c57f8eeaea
 | 
					  checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12786,7 +12736,7 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"opencollective-postinstall@npm:^2.0.2":
 | 
					"opencollective-postinstall@npm:^2.0.3":
 | 
				
			||||||
  version: 2.0.3
 | 
					  version: 2.0.3
 | 
				
			||||||
  resolution: "opencollective-postinstall@npm:2.0.3"
 | 
					  resolution: "opencollective-postinstall@npm:2.0.3"
 | 
				
			||||||
  bin:
 | 
					  bin:
 | 
				
			||||||
| 
						 | 
					@ -13269,13 +13219,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"piexifjs@npm:^1.0.6":
 | 
					 | 
				
			||||||
  version: 1.0.6
 | 
					 | 
				
			||||||
  resolution: "piexifjs@npm:1.0.6"
 | 
					 | 
				
			||||||
  checksum: 10c0/69a10fe09c08f1e67e653844ac79e720324a7fa34689b020359d60d98b3a601c070e1759df8f2d97d022298bd2f5b79eed4c92de86c5f215300c8a63adf947b1
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"pify@npm:^2.0.0":
 | 
					"pify@npm:^2.0.0":
 | 
				
			||||||
  version: 2.3.0
 | 
					  version: 2.3.0
 | 
				
			||||||
  resolution: "pify@npm:2.3.0"
 | 
					  resolution: "pify@npm:2.3.0"
 | 
				
			||||||
| 
						 | 
					@ -17100,31 +17043,27 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"tesseract.js-core@npm:^2.2.0":
 | 
					"tesseract.js-core@npm:^6.0.0":
 | 
				
			||||||
  version: 2.2.0
 | 
					  version: 6.0.0
 | 
				
			||||||
  resolution: "tesseract.js-core@npm:2.2.0"
 | 
					  resolution: "tesseract.js-core@npm:6.0.0"
 | 
				
			||||||
  checksum: 10c0/9ef569529f1ee96f8bf18388ef086d4940d3f02d28b4252df133a9bd36f6a9d140085e77a12ff1963cf9b4cd85bd1c644b61eca266ecfc51bb83adb30a1f11e3
 | 
					  checksum: 10c0/c04be8bbaa296be658664496754f21e857bdffff84113f08adf02f03a1f84596d68b3542ed2fda4a6dc138abb84b09b30ab07c04ee5950879e780876d343955f
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"tesseract.js@npm:^2.1.5":
 | 
					"tesseract.js@npm:^6.0.0":
 | 
				
			||||||
  version: 2.1.5
 | 
					  version: 6.0.0
 | 
				
			||||||
  resolution: "tesseract.js@npm:2.1.5"
 | 
					  resolution: "tesseract.js@npm:6.0.0"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    blueimp-load-image: "npm:^3.0.0"
 | 
					 | 
				
			||||||
    bmp-js: "npm:^0.1.0"
 | 
					    bmp-js: "npm:^0.1.0"
 | 
				
			||||||
    file-type: "npm:^12.4.1"
 | 
					    idb-keyval: "npm:^6.2.0"
 | 
				
			||||||
    idb-keyval: "npm:^3.2.0"
 | 
					 | 
				
			||||||
    is-electron: "npm:^2.2.0"
 | 
					 | 
				
			||||||
    is-url: "npm:^1.2.4"
 | 
					    is-url: "npm:^1.2.4"
 | 
				
			||||||
    jpeg-autorotate: "npm:^7.1.1"
 | 
					    node-fetch: "npm:^2.6.9"
 | 
				
			||||||
    node-fetch: "npm:^2.6.0"
 | 
					    opencollective-postinstall: "npm:^2.0.3"
 | 
				
			||||||
    opencollective-postinstall: "npm:^2.0.2"
 | 
					 | 
				
			||||||
    regenerator-runtime: "npm:^0.13.3"
 | 
					    regenerator-runtime: "npm:^0.13.3"
 | 
				
			||||||
    resolve-url: "npm:^0.2.1"
 | 
					    tesseract.js-core: "npm:^6.0.0"
 | 
				
			||||||
    tesseract.js-core: "npm:^2.2.0"
 | 
					    wasm-feature-detect: "npm:^1.2.11"
 | 
				
			||||||
    zlibjs: "npm:^0.3.1"
 | 
					    zlibjs: "npm:^0.3.1"
 | 
				
			||||||
  checksum: 10c0/b3aaee9189f3bc7f4217b83e110d0dd4d9afcafc3045b842f72b7ca9beb00bec732bc6b4b00eca14167c16b014c437fcf83dd272a640c9c8b5e1e9b55ea00ff5
 | 
					  checksum: 10c0/f65b816eabc16266bfa74ea61db73afa2d21ce0f57041b87b96abdff8954e042ee16637edea20aaf752227bc075052ca12021f4f68d5d25d52f062ebc4c644e1
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17970,6 +17909,13 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"wasm-feature-detect@npm:^1.2.11":
 | 
				
			||||||
 | 
					  version: 1.8.0
 | 
				
			||||||
 | 
					  resolution: "wasm-feature-detect@npm:1.8.0"
 | 
				
			||||||
 | 
					  checksum: 10c0/2cb43e91bbf7aa7c121bc76b3133de3ab6dc4f482acc1d2dc46c528e8adb7a51c72df5c2aacf1d219f113c04efd1706f18274d5790542aa5dd49e0644e3ee665
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"watchpack-chokidar2@npm:^2.0.1":
 | 
					"watchpack-chokidar2@npm:^2.0.1":
 | 
				
			||||||
  version: 2.0.1
 | 
					  version: 2.0.1
 | 
				
			||||||
  resolution: "watchpack-chokidar2@npm:2.0.1"
 | 
					  resolution: "watchpack-chokidar2@npm:2.0.1"
 | 
				
			||||||
| 
						 | 
					@ -18822,13 +18768,6 @@ __metadata:
 | 
				
			||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"yargs-parser@npm:^20.2.1":
 | 
					 | 
				
			||||||
  version: 20.2.9
 | 
					 | 
				
			||||||
  resolution: "yargs-parser@npm:20.2.9"
 | 
					 | 
				
			||||||
  checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72
 | 
					 | 
				
			||||||
  languageName: node
 | 
					 | 
				
			||||||
  linkType: hard
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"yargs-parser@npm:^21.1.1":
 | 
					"yargs-parser@npm:^21.1.1":
 | 
				
			||||||
  version: 21.1.1
 | 
					  version: 21.1.1
 | 
				
			||||||
  resolution: "yargs-parser@npm:21.1.1"
 | 
					  resolution: "yargs-parser@npm:21.1.1"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue