Lexical: Add media preview

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-07-28 23:23:04 +02:00
parent b3f9edd41e
commit 41ee08cd14
3 changed files with 52 additions and 21 deletions

View file

@ -1,13 +1,13 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { spring } from 'react-motion'; import { spring } from 'react-motion';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import Blurhash from 'soapbox/components/blurhash'; import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon-button'; import { IconButton } from 'soapbox/components/ui';
import Motion from 'soapbox/features/ui/util/optional-motion'; import Motion from 'soapbox/features/ui/util/optional-motion';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import { Attachment } from 'soapbox/types/entities'; import { Attachment } from 'soapbox/types/entities';
@ -57,6 +57,7 @@ export const MIMETYPE_ICONS: Record<string, string> = {
const messages = defineMessages({ const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' }, delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
preview: { id: 'upload_form.preview', defaultMessage: 'Preview' },
}); });
interface IUpload { interface IUpload {
@ -159,20 +160,24 @@ const Upload: React.FC<IUpload> = ({
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }} backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
> >
<div className={clsx('compose-form__upload__actions', { active })}> <div className={clsx('compose-form__upload__actions', { active })}>
{onDelete && (
<IconButton
onClick={handleUndoClick}
src={require('@tabler/icons/x.svg')}
text={<FormattedMessage id='upload_form.undo' defaultMessage='Delete' />}
/>
)}
{/* Only display the "Preview" button for a valid attachment with a URL */}
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && ( {(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && (
<IconButton <IconButton
onClick={handleOpenModal} onClick={handleOpenModal}
src={require('@tabler/icons/zoom-in.svg')} src={require('@tabler/icons/zoom-in.svg')}
text={<FormattedMessage id='upload_form.preview' defaultMessage='Preview' />} theme='dark'
className='hover:scale-105 hover:bg-gray-900'
iconClassName='h-5 w-5'
title={intl.formatMessage(messages.preview)}
/>
)}
{onDelete && (
<IconButton
onClick={handleUndoClick}
src={require('@tabler/icons/x.svg')}
theme='dark'
className='hover:scale-105 hover:bg-gray-900'
iconClassName='h-5 w-5'
title={intl.formatMessage(messages.delete)}
/> />
)} )}
</div> </div>

View file

@ -11,6 +11,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'; import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
import { mergeRegister } from '@lexical/utils'; import { mergeRegister } from '@lexical/utils';
import clsx from 'clsx'; import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import { import {
$getNodeByKey, $getNodeByKey,
$getSelection, $getSelection,
@ -28,7 +29,10 @@ import {
import * as React from 'react'; import * as React from 'react';
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'; import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { IconButton } from 'soapbox/components/ui'; import { openModal } from 'soapbox/actions/modals';
import { HStack, IconButton } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers';
import { $isImageNode } from './image-node'; import { $isImageNode } from './image-node';
@ -40,6 +44,7 @@ import type {
RangeSelection, RangeSelection,
} from 'lexical'; } from 'lexical';
const imageCache = new Set(); const imageCache = new Set();
const useSuspenseImage = (src: string) => { const useSuspenseImage = (src: string) => {
@ -87,6 +92,8 @@ const ImageComponent = ({
nodeKey: NodeKey nodeKey: NodeKey
src: string src: string
}): JSX.Element => { }): JSX.Element => {
const dispatch = useAppDispatch();
const imageRef = useRef<null | HTMLImageElement>(null); const imageRef = useRef<null | HTMLImageElement>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null); const buttonRef = useRef<HTMLButtonElement | null>(null);
const [isSelected, setSelected, clearSelection] = const [isSelected, setSelected, clearSelection] =
@ -109,6 +116,16 @@ const ImageComponent = ({
[nodeKey], [nodeKey],
); );
const previewImage = () => {
const image = normalizeAttachment({
type: 'image',
url: src,
altText,
});
dispatch(openModal('MEDIA', { media: ImmutableList.of(image), index: 0 }));
};
const onDelete = useCallback( const onDelete = useCallback(
(payload: KeyboardEvent) => { (payload: KeyboardEvent) => {
if (isSelected && $isNodeSelection($getSelection())) { if (isSelected && $isNodeSelection($getSelection())) {
@ -248,13 +265,22 @@ const ImageComponent = ({
<Suspense fallback={null}> <Suspense fallback={null}>
<> <>
<div className='relative' draggable={draggable}> <div className='relative' draggable={draggable}>
<IconButton <HStack className='absolute right-2 top-2 z-10' space={2}>
onClick={deleteNode} <IconButton
src={require('@tabler/icons/x.svg')} onClick={previewImage}
theme='dark' src={require('@tabler/icons/zoom-in.svg')}
className='absolute right-2 top-2 z-10 hover:scale-105 hover:bg-gray-900' theme='dark'
iconClassName='h-5 w-5' className='!p-1.5 hover:scale-105 hover:bg-gray-900'
/> iconClassName='h-5 w-5'
/>
<IconButton
onClick={deleteNode}
src={require('@tabler/icons/x.svg')}
theme='dark'
className='!p-1.5 hover:scale-105 hover:bg-gray-900'
iconClassName='h-5 w-5'
/>
</HStack>
<LazyImage <LazyImage
className={ className={
clsx('cursor-default', { clsx('cursor-default', {

View file

@ -50,7 +50,7 @@
overflow: hidden; overflow: hidden;
&__actions { &__actions {
@apply bg-gradient-to-b from-gray-900/80 via-gray-900/50 to-transparent flex items-start justify-between opacity-0 transition-opacity duration-100 ease-linear; @apply p-2 bg-gradient-to-b from-gray-900/80 via-gray-900/50 to-transparent flex items-start gap-2 justify-end opacity-0 transition-opacity duration-100 ease-linear;
&.active { &.active {
@apply opacity-100; @apply opacity-100;