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 { List as ImmutableList } from 'immutable';
import React, { useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { spring } from 'react-motion';
import { openModal } from 'soapbox/actions/modals';
import Blurhash from 'soapbox/components/blurhash';
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 { useAppDispatch } from 'soapbox/hooks';
import { Attachment } from 'soapbox/types/entities';
@ -57,6 +57,7 @@ export const MIMETYPE_ICONS: Record<string, string> = {
const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
preview: { id: 'upload_form.preview', defaultMessage: 'Preview' },
});
interface IUpload {
@ -159,20 +160,24 @@ const Upload: React.FC<IUpload> = ({
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
>
<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)) && (
<IconButton
onClick={handleOpenModal}
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>

View file

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

View file

@ -50,7 +50,7 @@
overflow: hidden;
&__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 {
@apply opacity-100;