Change media reordering design in the compose form in web UI (#32093)
This commit is contained in:
		
							parent
							
								
									cdd7526531
								
							
						
					
					
						commit
						11a12e56b3
					
				| @ -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_error.poll": "File upload not allowed with polls.", | ||||||
|   "upload_form.audio_description": "Describe for people who are deaf or hard of hearing", |   "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.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.edit": "Edit", | ||||||
|   "upload_form.thumbnail": "Change thumbnail", |   "upload_form.thumbnail": "Change thumbnail", | ||||||
|   "upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision", |   "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 { |   &__uploads { | ||||||
|     display: flex; |  | ||||||
|     gap: 8px; |  | ||||||
|     padding: 0 12px; |     padding: 0 12px; | ||||||
|     flex-wrap: wrap; |     aspect-ratio: 3/2; | ||||||
|     align-self: stretch; |   } | ||||||
|     align-items: flex-start; | 
 | ||||||
|     align-content: flex-start; |   .media-gallery { | ||||||
|     justify-content: center; |     gap: 8px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__upload { |   &__upload { | ||||||
|     flex: 1 1 0; |     position: relative; | ||||||
|     min-width: calc(50% - 8px); |     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 { |     &__actions { | ||||||
|       display: flex; |       display: flex; | ||||||
| @ -686,8 +706,7 @@ body > [data-popper-placement] { | |||||||
| 
 | 
 | ||||||
|     &__thumbnail { |     &__thumbnail { | ||||||
|       width: 100%; |       width: 100%; | ||||||
|       height: 144px; |       height: 100%; | ||||||
|       border-radius: 6px; |  | ||||||
|       background-position: center; |       background-position: center; | ||||||
|       background-size: cover; |       background-size: cover; | ||||||
|       background-repeat: no-repeat; |       background-repeat: no-repeat; | ||||||
| @ -7098,30 +7117,30 @@ a.status-card { | |||||||
|   gap: 2px; |   gap: 2px; | ||||||
| 
 | 
 | ||||||
|   &--layout-2 { |   &--layout-2 { | ||||||
|     .media-gallery__item:nth-child(1) { |     & > .media-gallery__item:nth-child(1) { | ||||||
|       border-end-end-radius: 0; |       border-end-end-radius: 0; | ||||||
|       border-start-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-start-start-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &--layout-3 { |   &--layout-3 { | ||||||
|     .media-gallery__item:nth-child(1) { |     & > .media-gallery__item:nth-child(1) { | ||||||
|       border-end-end-radius: 0; |       border-end-end-radius: 0; | ||||||
|       border-start-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-start-start-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|       border-end-end-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-start-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|       border-start-end-radius: 0; |       border-start-end-radius: 0; | ||||||
| @ -7129,26 +7148,26 @@ a.status-card { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &--layout-4 { |   &--layout-4 { | ||||||
|     .media-gallery__item:nth-child(1) { |     & > .media-gallery__item:nth-child(1) { | ||||||
|       border-end-end-radius: 0; |       border-end-end-radius: 0; | ||||||
|       border-start-end-radius: 0; |       border-start-end-radius: 0; | ||||||
|       border-end-start-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-start-start-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|       border-end-end-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-start-radius: 0; | ||||||
|       border-start-end-radius: 0; |       border-start-end-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|       border-end-end-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-start-start-radius: 0; | ||||||
|       border-end-start-radius: 0; |       border-end-start-radius: 0; | ||||||
|       border-start-end-radius: 0; |       border-start-end-radius: 0; | ||||||
|  | |||||||
| @ -42,6 +42,9 @@ | |||||||
|     "@babel/preset-react": "^7.22.3", |     "@babel/preset-react": "^7.22.3", | ||||||
|     "@babel/preset-typescript": "^7.21.5", |     "@babel/preset-typescript": "^7.21.5", | ||||||
|     "@babel/runtime": "^7.22.3", |     "@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", |     "@formatjs/intl-pluralrules": "^5.2.2", | ||||||
|     "@gamestdio/websocket": "^0.3.2", |     "@gamestdio/websocket": "^0.3.2", | ||||||
|     "@github/webauthn-json": "^2.1.1", |     "@github/webauthn-json": "^2.1.1", | ||||||
|  | |||||||
							
								
								
									
										59
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -2010,6 +2010,55 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "@dual-bundle/import-meta-resolve@npm:^4.1.0": | ||||||
|   version: 4.1.0 |   version: 4.1.0 | ||||||
|   resolution: "@dual-bundle/import-meta-resolve@npm: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-react": "npm:^7.22.3" | ||||||
|     "@babel/preset-typescript": "npm:^7.21.5" |     "@babel/preset-typescript": "npm:^7.21.5" | ||||||
|     "@babel/runtime": "npm:^7.22.3" |     "@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/cli": "npm:^6.1.1" | ||||||
|     "@formatjs/intl-pluralrules": "npm:^5.2.2" |     "@formatjs/intl-pluralrules": "npm:^5.2.2" | ||||||
|     "@gamestdio/websocket": "npm:^0.3.2" |     "@gamestdio/websocket": "npm:^0.3.2" | ||||||
| @ -17205,6 +17257,13 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "tslib@npm:^2.4.0, tslib@npm:^2.6.2": | ||||||
|   version: 2.6.3 |   version: 2.6.3 | ||||||
|   resolution: "tslib@npm:2.6.3" |   resolution: "tslib@npm:2.6.3" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user