Change media reordering design in the compose form in web UI (#32093)
This commit is contained in:
		@@ -1,81 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
 | 
			
		||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
 | 
			
		||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
 | 
			
		||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
 | 
			
		||||
import { undoUploadCompose, initMediaEditModal } from 'mastodon/actions/compose';
 | 
			
		||||
import { Blurhash } from 'mastodon/components/blurhash';
 | 
			
		||||
import { Icon }  from 'mastodon/components/icon';
 | 
			
		||||
import Motion from 'mastodon/features/ui/util/optional_motion';
 | 
			
		||||
 | 
			
		||||
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
 | 
			
		||||
  const sensitive = useSelector(state => state.getIn(['compose', 'spoiler']));
 | 
			
		||||
 | 
			
		||||
  const handleUndoClick = useCallback(() => {
 | 
			
		||||
    dispatch(undoUploadCompose(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  const handleFocalPointClick = useCallback(() => {
 | 
			
		||||
    dispatch(initMediaEditModal(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  const handleDragStart = useCallback(() => {
 | 
			
		||||
    onDragStart(id);
 | 
			
		||||
  }, [onDragStart, id]);
 | 
			
		||||
 | 
			
		||||
  const handleDragEnter = useCallback(() => {
 | 
			
		||||
    onDragEnter(id);
 | 
			
		||||
  }, [onDragEnter, id]);
 | 
			
		||||
 | 
			
		||||
  if (!media) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const focusX = media.getIn(['meta', 'focus', 'x']);
 | 
			
		||||
  const focusY = media.getIn(['meta', 'focus', 'y']);
 | 
			
		||||
  const x = ((focusX /  2) + .5) * 100;
 | 
			
		||||
  const y = ((focusY / -2) + .5) * 100;
 | 
			
		||||
  const missingDescription = (media.get('description') || '').length === 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
 | 
			
		||||
      <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
 | 
			
		||||
        {({ scale }) => (
 | 
			
		||||
          <div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
 | 
			
		||||
            {sensitive && <Blurhash
 | 
			
		||||
              hash={media.get('blurhash')}
 | 
			
		||||
              className='compose-form__upload__preview'
 | 
			
		||||
            />}
 | 
			
		||||
 | 
			
		||||
            <div className='compose-form__upload__actions'>
 | 
			
		||||
              <button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
 | 
			
		||||
              <button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className='compose-form__upload__warning'>
 | 
			
		||||
              <button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Motion>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Upload.propTypes = {
 | 
			
		||||
  id: PropTypes.string,
 | 
			
		||||
  onDragEnter: PropTypes.func,
 | 
			
		||||
  onDragStart: PropTypes.func,
 | 
			
		||||
  onDragEnd: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										130
									
								
								app/javascript/mastodon/features/compose/components/upload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								app/javascript/mastodon/features/compose/components/upload.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import { useSortable } from '@dnd-kit/sortable';
 | 
			
		||||
import { CSS } from '@dnd-kit/utilities';
 | 
			
		||||
 | 
			
		||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
 | 
			
		||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
 | 
			
		||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
 | 
			
		||||
import {
 | 
			
		||||
  undoUploadCompose,
 | 
			
		||||
  initMediaEditModal,
 | 
			
		||||
} from 'mastodon/actions/compose';
 | 
			
		||||
import { Blurhash } from 'mastodon/components/blurhash';
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
export const Upload: React.FC<{
 | 
			
		||||
  id: string;
 | 
			
		||||
  dragging?: boolean;
 | 
			
		||||
  overlay?: boolean;
 | 
			
		||||
  tall?: boolean;
 | 
			
		||||
  wide?: boolean;
 | 
			
		||||
}> = ({ id, dragging, overlay, tall, wide }) => {
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
  const media = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
 | 
			
		||||
        .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
        .find((item: MediaAttachment) => item.get('id') === id) as  // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
        | MediaAttachment
 | 
			
		||||
        | undefined,
 | 
			
		||||
  );
 | 
			
		||||
  const sensitive = useAppSelector(
 | 
			
		||||
    (state) => state.compose.get('spoiler') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleUndoClick = useCallback(() => {
 | 
			
		||||
    dispatch(undoUploadCompose(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  const handleFocalPointClick = useCallback(() => {
 | 
			
		||||
    dispatch(initMediaEditModal(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  const { attributes, listeners, setNodeRef, transform, transition } =
 | 
			
		||||
    useSortable({ id });
 | 
			
		||||
 | 
			
		||||
  if (!media) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const focusX = media.getIn(['meta', 'focus', 'x']) as number;
 | 
			
		||||
  const focusY = media.getIn(['meta', 'focus', 'y']) as number;
 | 
			
		||||
  const x = (focusX / 2 + 0.5) * 100;
 | 
			
		||||
  const y = (focusY / -2 + 0.5) * 100;
 | 
			
		||||
  const missingDescription =
 | 
			
		||||
    ((media.get('description') as string | undefined) ?? '').length === 0;
 | 
			
		||||
 | 
			
		||||
  const style = {
 | 
			
		||||
    transform: CSS.Transform.toString(transform),
 | 
			
		||||
    transition,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('compose-form__upload media-gallery__item', {
 | 
			
		||||
        dragging,
 | 
			
		||||
        overlay,
 | 
			
		||||
        'media-gallery__item--tall': tall,
 | 
			
		||||
        'media-gallery__item--wide': wide,
 | 
			
		||||
      })}
 | 
			
		||||
      ref={setNodeRef}
 | 
			
		||||
      style={style}
 | 
			
		||||
      {...attributes}
 | 
			
		||||
      {...listeners}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className='compose-form__upload__thumbnail'
 | 
			
		||||
        style={{
 | 
			
		||||
          backgroundImage: !sensitive
 | 
			
		||||
            ? `url(${media.get('preview_url') as string})`
 | 
			
		||||
            : undefined,
 | 
			
		||||
          backgroundPosition: `${x}% ${y}%`,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {sensitive && (
 | 
			
		||||
          <Blurhash
 | 
			
		||||
            hash={media.get('blurhash') as string}
 | 
			
		||||
            className='compose-form__upload__preview'
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <div className='compose-form__upload__actions'>
 | 
			
		||||
          <button
 | 
			
		||||
            type='button'
 | 
			
		||||
            className='icon-button compose-form__upload__delete'
 | 
			
		||||
            onClick={handleUndoClick}
 | 
			
		||||
          >
 | 
			
		||||
            <Icon id='close' icon={CloseIcon} />
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            type='button'
 | 
			
		||||
            className='icon-button'
 | 
			
		||||
            onClick={handleFocalPointClick}
 | 
			
		||||
          >
 | 
			
		||||
            <Icon id='edit' icon={EditIcon} />{' '}
 | 
			
		||||
            <FormattedMessage id='upload_form.edit' defaultMessage='Edit' />
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='compose-form__upload__warning'>
 | 
			
		||||
          <button
 | 
			
		||||
            type='button'
 | 
			
		||||
            className={classNames('icon-button', {
 | 
			
		||||
              active: missingDescription,
 | 
			
		||||
            })}
 | 
			
		||||
            onClick={handleFocalPointClick}
 | 
			
		||||
          >
 | 
			
		||||
            {missingDescription && <Icon id='warning' icon={WarningIcon} />} ALT
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
import { useRef, useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { useSelector, useDispatch } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { changeMediaOrder } from 'mastodon/actions/compose';
 | 
			
		||||
 | 
			
		||||
import { Upload } from './upload';
 | 
			
		||||
import { UploadProgress } from './upload_progress';
 | 
			
		||||
 | 
			
		||||
export const UploadForm = () => {
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
 | 
			
		||||
  const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
 | 
			
		||||
  const progress = useSelector(state => state.getIn(['compose', 'progress']));
 | 
			
		||||
  const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
 | 
			
		||||
 | 
			
		||||
  const dragItem = useRef();
 | 
			
		||||
  const dragOverItem = useRef();
 | 
			
		||||
 | 
			
		||||
  const handleDragStart = useCallback(id => {
 | 
			
		||||
    dragItem.current = id;
 | 
			
		||||
  }, [dragItem]);
 | 
			
		||||
 | 
			
		||||
  const handleDragEnter = useCallback(id => {
 | 
			
		||||
    dragOverItem.current = id;
 | 
			
		||||
  }, [dragOverItem]);
 | 
			
		||||
 | 
			
		||||
  const handleDragEnd = useCallback(() => {
 | 
			
		||||
    dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
 | 
			
		||||
    dragItem.current = null;
 | 
			
		||||
    dragOverItem.current = null;
 | 
			
		||||
  }, [dispatch, dragItem, dragOverItem]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
 | 
			
		||||
 | 
			
		||||
      {mediaIds.size > 0 && (
 | 
			
		||||
        <div className='compose-form__uploads'>
 | 
			
		||||
          {mediaIds.map(id => (
 | 
			
		||||
            <Upload
 | 
			
		||||
              key={id}
 | 
			
		||||
              id={id}
 | 
			
		||||
              onDragStart={handleDragStart}
 | 
			
		||||
              onDragEnter={handleDragEnter}
 | 
			
		||||
              onDragEnd={handleDragEnd}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,185 @@
 | 
			
		||||
import { useState, useCallback, useMemo } from 'react';
 | 
			
		||||
 | 
			
		||||
import { useIntl, defineMessages } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import type { List } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  DragStartEvent,
 | 
			
		||||
  DragEndEvent,
 | 
			
		||||
  UniqueIdentifier,
 | 
			
		||||
  Announcements,
 | 
			
		||||
  ScreenReaderInstructions,
 | 
			
		||||
} from '@dnd-kit/core';
 | 
			
		||||
import {
 | 
			
		||||
  DndContext,
 | 
			
		||||
  closestCenter,
 | 
			
		||||
  KeyboardSensor,
 | 
			
		||||
  PointerSensor,
 | 
			
		||||
  useSensor,
 | 
			
		||||
  useSensors,
 | 
			
		||||
  DragOverlay,
 | 
			
		||||
} from '@dnd-kit/core';
 | 
			
		||||
import {
 | 
			
		||||
  SortableContext,
 | 
			
		||||
  sortableKeyboardCoordinates,
 | 
			
		||||
  rectSortingStrategy,
 | 
			
		||||
} from '@dnd-kit/sortable';
 | 
			
		||||
 | 
			
		||||
import { changeMediaOrder } from 'mastodon/actions/compose';
 | 
			
		||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
			
		||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import { Upload } from './upload';
 | 
			
		||||
import { UploadProgress } from './upload_progress';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  screenReaderInstructions: {
 | 
			
		||||
    id: 'upload_form.drag_and_drop.instructions',
 | 
			
		||||
    defaultMessage:
 | 
			
		||||
      '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.',
 | 
			
		||||
  },
 | 
			
		||||
  onDragStart: {
 | 
			
		||||
    id: 'upload_form.drag_and_drop.on_drag_start',
 | 
			
		||||
    defaultMessage: 'Picked up media attachment {item}.',
 | 
			
		||||
  },
 | 
			
		||||
  onDragOver: {
 | 
			
		||||
    id: 'upload_form.drag_and_drop.on_drag_over',
 | 
			
		||||
    defaultMessage: 'Media attachment {item} was moved.',
 | 
			
		||||
  },
 | 
			
		||||
  onDragEnd: {
 | 
			
		||||
    id: 'upload_form.drag_and_drop.on_drag_end',
 | 
			
		||||
    defaultMessage: 'Media attachment {item} was dropped.',
 | 
			
		||||
  },
 | 
			
		||||
  onDragCancel: {
 | 
			
		||||
    id: 'upload_form.drag_and_drop.on_drag_cancel',
 | 
			
		||||
    defaultMessage:
 | 
			
		||||
      'Dragging was cancelled. Media attachment {item} was dropped.',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const UploadForm: React.FC = () => {
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const mediaIds = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
 | 
			
		||||
        .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
        .map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
  );
 | 
			
		||||
  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
 | 
			
		||||
  );
 | 
			
		||||
  const progress = useAppSelector(
 | 
			
		||||
    (state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
			
		||||
  );
 | 
			
		||||
  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
 | 
			
		||||
  );
 | 
			
		||||
  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
 | 
			
		||||
  const sensors = useSensors(
 | 
			
		||||
    useSensor(PointerSensor, {
 | 
			
		||||
      activationConstraint: {
 | 
			
		||||
        distance: 5,
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    useSensor(KeyboardSensor, {
 | 
			
		||||
      coordinateGetter: sortableKeyboardCoordinates,
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleDragStart = useCallback(
 | 
			
		||||
    (e: DragStartEvent) => {
 | 
			
		||||
      const { active } = e;
 | 
			
		||||
 | 
			
		||||
      setActiveId(active.id);
 | 
			
		||||
    },
 | 
			
		||||
    [setActiveId],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleDragEnd = useCallback(
 | 
			
		||||
    (e: DragEndEvent) => {
 | 
			
		||||
      const { active, over } = e;
 | 
			
		||||
 | 
			
		||||
      if (over && active.id !== over.id) {
 | 
			
		||||
        dispatch(changeMediaOrder(active.id, over.id));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setActiveId(null);
 | 
			
		||||
    },
 | 
			
		||||
    [dispatch, setActiveId],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const accessibility: {
 | 
			
		||||
    screenReaderInstructions: ScreenReaderInstructions;
 | 
			
		||||
    announcements: Announcements;
 | 
			
		||||
  } = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
      screenReaderInstructions: {
 | 
			
		||||
        draggable: intl.formatMessage(messages.screenReaderInstructions),
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      announcements: {
 | 
			
		||||
        onDragStart({ active }) {
 | 
			
		||||
          return intl.formatMessage(messages.onDragStart, { item: active.id });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onDragOver({ active }) {
 | 
			
		||||
          return intl.formatMessage(messages.onDragOver, { item: active.id });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onDragEnd({ active }) {
 | 
			
		||||
          return intl.formatMessage(messages.onDragEnd, { item: active.id });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onDragCancel({ active }) {
 | 
			
		||||
          return intl.formatMessage(messages.onDragCancel, { item: active.id });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    [intl],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <UploadProgress
 | 
			
		||||
        active={active}
 | 
			
		||||
        progress={progress}
 | 
			
		||||
        isProcessing={isProcessing}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {mediaIds.size > 0 && (
 | 
			
		||||
        <div
 | 
			
		||||
          className={`compose-form__uploads media-gallery media-gallery--layout-${mediaIds.size}`}
 | 
			
		||||
        >
 | 
			
		||||
          <DndContext
 | 
			
		||||
            sensors={sensors}
 | 
			
		||||
            collisionDetection={closestCenter}
 | 
			
		||||
            onDragStart={handleDragStart}
 | 
			
		||||
            onDragEnd={handleDragEnd}
 | 
			
		||||
            accessibility={accessibility}
 | 
			
		||||
          >
 | 
			
		||||
            <SortableContext
 | 
			
		||||
              items={mediaIds.toArray()}
 | 
			
		||||
              strategy={rectSortingStrategy}
 | 
			
		||||
            >
 | 
			
		||||
              {mediaIds.map((id, idx) => (
 | 
			
		||||
                <Upload
 | 
			
		||||
                  key={id}
 | 
			
		||||
                  id={id}
 | 
			
		||||
                  dragging={id === activeId}
 | 
			
		||||
                  tall={mediaIds.size < 3 || (mediaIds.size === 3 && idx === 0)}
 | 
			
		||||
                  wide={mediaIds.size === 1}
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
            </SortableContext>
 | 
			
		||||
 | 
			
		||||
            <DragOverlay>
 | 
			
		||||
              {activeId ? <Upload id={activeId as string} overlay /> : null}
 | 
			
		||||
            </DragOverlay>
 | 
			
		||||
          </DndContext>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -852,6 +852,11 @@
 | 
			
		||||
  "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.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_over": "Media attachment {item} was moved.",
 | 
			
		||||
  "upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
 | 
			
		||||
  "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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								app/javascript/mastodon/models/media_attachment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/javascript/mastodon/models/media_attachment.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
// Temporary until we type it correctly
 | 
			
		||||
export type MediaAttachment = Immutable.Map<string, unknown>;
 | 
			
		||||
@@ -653,19 +653,39 @@ body > [data-popper-placement] {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__uploads {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 8px;
 | 
			
		||||
    padding: 0 12px;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    align-self: stretch;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
    align-content: flex-start;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    aspect-ratio: 3/2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .media-gallery {
 | 
			
		||||
    gap: 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__upload {
 | 
			
		||||
    flex: 1 1 0;
 | 
			
		||||
    min-width: calc(50% - 8px);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    cursor: grab;
 | 
			
		||||
 | 
			
		||||
    &.dragging {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.overlay {
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      border-radius: 8px;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__drag-handle {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      inset-inline-start: 0;
 | 
			
		||||
      transform: translateY(-50%);
 | 
			
		||||
      color: $white;
 | 
			
		||||
      background: transparent;
 | 
			
		||||
      border: 0;
 | 
			
		||||
      padding: 8px 3px;
 | 
			
		||||
      cursor: grab;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__actions {
 | 
			
		||||
      display: flex;
 | 
			
		||||
@@ -686,8 +706,7 @@ body > [data-popper-placement] {
 | 
			
		||||
 | 
			
		||||
    &__thumbnail {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 144px;
 | 
			
		||||
      border-radius: 6px;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      background-position: center;
 | 
			
		||||
      background-size: cover;
 | 
			
		||||
      background-repeat: no-repeat;
 | 
			
		||||
@@ -7098,30 +7117,30 @@ a.status-card {
 | 
			
		||||
  gap: 2px;
 | 
			
		||||
 | 
			
		||||
  &--layout-2 {
 | 
			
		||||
    .media-gallery__item:nth-child(1) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(1) {
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(2) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(2) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--layout-3 {
 | 
			
		||||
    .media-gallery__item:nth-child(1) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(1) {
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(2) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(2) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(3) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(3) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
@@ -7129,26 +7148,26 @@ a.status-card {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--layout-4 {
 | 
			
		||||
    .media-gallery__item:nth-child(1) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(1) {
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(2) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(2) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(3) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(3) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
      border-end-end-radius: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .media-gallery__item:nth-child(4) {
 | 
			
		||||
    & > .media-gallery__item:nth-child(4) {
 | 
			
		||||
      border-start-start-radius: 0;
 | 
			
		||||
      border-end-start-radius: 0;
 | 
			
		||||
      border-start-end-radius: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,9 @@
 | 
			
		||||
    "@babel/preset-react": "^7.22.3",
 | 
			
		||||
    "@babel/preset-typescript": "^7.21.5",
 | 
			
		||||
    "@babel/runtime": "^7.22.3",
 | 
			
		||||
    "@dnd-kit/core": "^6.1.0",
 | 
			
		||||
    "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
    "@dnd-kit/utilities": "^3.2.2",
 | 
			
		||||
    "@formatjs/intl-pluralrules": "^5.2.2",
 | 
			
		||||
    "@gamestdio/websocket": "^0.3.2",
 | 
			
		||||
    "@github/webauthn-json": "^2.1.1",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -2010,6 +2010,55 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@dnd-kit/accessibility@npm:^3.1.0":
 | 
			
		||||
  version: 3.1.0
 | 
			
		||||
  resolution: "@dnd-kit/accessibility@npm:3.1.0"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib: "npm:^2.0.0"
 | 
			
		||||
  peerDependencies:
 | 
			
		||||
    react: ">=16.8.0"
 | 
			
		||||
  checksum: 10c0/4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@dnd-kit/core@npm:^6.1.0":
 | 
			
		||||
  version: 6.1.0
 | 
			
		||||
  resolution: "@dnd-kit/core@npm:6.1.0"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@dnd-kit/accessibility": "npm:^3.1.0"
 | 
			
		||||
    "@dnd-kit/utilities": "npm:^3.2.2"
 | 
			
		||||
    tslib: "npm:^2.0.0"
 | 
			
		||||
  peerDependencies:
 | 
			
		||||
    react: ">=16.8.0"
 | 
			
		||||
    react-dom: ">=16.8.0"
 | 
			
		||||
  checksum: 10c0/c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@dnd-kit/sortable@npm:^8.0.0":
 | 
			
		||||
  version: 8.0.0
 | 
			
		||||
  resolution: "@dnd-kit/sortable@npm:8.0.0"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@dnd-kit/utilities": "npm:^3.2.2"
 | 
			
		||||
    tslib: "npm:^2.0.0"
 | 
			
		||||
  peerDependencies:
 | 
			
		||||
    "@dnd-kit/core": ^6.1.0
 | 
			
		||||
    react: ">=16.8.0"
 | 
			
		||||
  checksum: 10c0/a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@dnd-kit/utilities@npm:^3.2.2":
 | 
			
		||||
  version: 3.2.2
 | 
			
		||||
  resolution: "@dnd-kit/utilities@npm:3.2.2"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib: "npm:^2.0.0"
 | 
			
		||||
  peerDependencies:
 | 
			
		||||
    react: ">=16.8.0"
 | 
			
		||||
  checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@dual-bundle/import-meta-resolve@npm:^4.1.0":
 | 
			
		||||
  version: 4.1.0
 | 
			
		||||
  resolution: "@dual-bundle/import-meta-resolve@npm:4.1.0"
 | 
			
		||||
@@ -2753,6 +2802,9 @@ __metadata:
 | 
			
		||||
    "@babel/preset-react": "npm:^7.22.3"
 | 
			
		||||
    "@babel/preset-typescript": "npm:^7.21.5"
 | 
			
		||||
    "@babel/runtime": "npm:^7.22.3"
 | 
			
		||||
    "@dnd-kit/core": "npm:^6.1.0"
 | 
			
		||||
    "@dnd-kit/sortable": "npm:^8.0.0"
 | 
			
		||||
    "@dnd-kit/utilities": "npm:^3.2.2"
 | 
			
		||||
    "@formatjs/cli": "npm:^6.1.1"
 | 
			
		||||
    "@formatjs/intl-pluralrules": "npm:^5.2.2"
 | 
			
		||||
    "@gamestdio/websocket": "npm:^0.3.2"
 | 
			
		||||
@@ -17205,6 +17257,13 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"tslib@npm:^2.0.0":
 | 
			
		||||
  version: 2.7.0
 | 
			
		||||
  resolution: "tslib@npm:2.7.0"
 | 
			
		||||
  checksum: 10c0/469e1d5bf1af585742128827000711efa61010b699cb040ab1800bcd3ccdd37f63ec30642c9e07c4439c1db6e46345582614275daca3e0f4abae29b0083f04a6
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"tslib@npm:^2.4.0, tslib@npm:^2.6.2":
 | 
			
		||||
  version: 2.6.3
 | 
			
		||||
  resolution: "tslib@npm:2.6.3"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user