Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: copy and paste photos #1397

Merged
merged 20 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 13 additions & 27 deletions apps/web/src/components/Composer/Actions/Attachment.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import { Spinner } from '@components/UI/Spinner';
import { Tooltip } from '@components/UI/Tooltip';
import useOnClickOutside from '@components/utils/hooks/useOnClickOutside';
import type { LensterAttachment } from '@generated/types';
import useUploadAttachments from '@components/utils/hooks/useUploadAttachments';
import { Menu, Transition } from '@headlessui/react';
import { MusicNoteIcon, PhotographIcon, VideoCameraIcon } from '@heroicons/react/outline';
import { Leafwatch } from '@lib/leafwatch';
import uploadToIPFS from '@lib/uploadToIPFS';
import clsx from 'clsx';
import {
ALLOWED_AUDIO_TYPES,
ALLOWED_IMAGE_TYPES,
ALLOWED_MEDIA_TYPES,
ALLOWED_VIDEO_TYPES
} from 'data/constants';
import type { ChangeEvent, Dispatch, FC } from 'react';
import type { ChangeEvent, FC } from 'react';
import { Fragment, useId, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { usePublicationStore } from 'src/store/publication';
import { PUBLICATION } from 'src/tracking';

interface Props {
attachments: LensterAttachment[];
setAttachments: Dispatch<LensterAttachment[]>;
}

const Attachment: FC<Props> = ({ attachments, setAttachments }) => {
const [loading, setLoading] = useState(false);
const Attachment: FC = () => {
const attachments = usePublicationStore((state) => state.attachments);
const isUploading = usePublicationStore((state) => state.isUploading);
const { handleUploadAttachments } = useUploadAttachments();
const [showMenu, setShowMenu] = useState(false);
const id = useId();
const dropdownRef = useRef(null);
Expand Down Expand Up @@ -77,34 +74,23 @@ const Attachment: FC<Props> = ({ attachments, setAttachments }) => {
const handleAttachment = async (evt: ChangeEvent<HTMLInputElement>) => {
evt.preventDefault();
setShowMenu(false);
setLoading(true);

try {
const { files } = evt.target;
// Count check
if (files && (hasVideos(files) || (isImageType(files) && files.length + attachments.length > 4))) {
return toast.error('Please choose either 1 video or total 4 photos.');
return toast.error('Please choose either 1 video or up to 4 photos.');
bigint marked this conversation as resolved.
Show resolved Hide resolved
}

// Type check
if (isTypeAllowed(files)) {
const attachment = await uploadToIPFS(files);

if (attachment) {
if (isImageType(attachment) && isImageType(attachments)) {
const combineAttachment = attachments.concat(attachment);
setAttachments(combineAttachment);
evt.target.value = '';
} else {
setAttachments(attachment);
evt.target.value = '';
}
}
await handleUploadAttachments(files);
evt.target.value = '';
} else {
return toast.error('File format not allowed.');
}
} finally {
setLoading(false);
} catch {
toast.error('Something went wrong while uploading!');
}
};

Expand All @@ -115,7 +101,7 @@ const Attachment: FC<Props> = ({ attachments, setAttachments }) => {
className="rounded-full hover:bg-gray-300 hover:bg-opacity-20"
aria-label="More"
>
{loading ? (
{isUploading ? (
<Spinner size="sm" />
) : (
<Tooltip placement="top" content="Media">
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/Composer/Actions/Giphy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { motion } from 'framer-motion';
import dynamic from 'next/dynamic';
import type { FC } from 'react';
import { useState } from 'react';
import { usePublicationStore } from 'src/store/publication';
import { PUBLICATION } from 'src/tracking';

const GifSelector = dynamic(() => import('./GifSelector'), {
Expand All @@ -19,6 +20,7 @@ interface Props {
}

const Giphy: FC<Props> = ({ setGifAttachment }) => {
const attachments = usePublicationStore((state) => state.attachments);
const [showModal, setShowModal] = useState(false);

return (
Expand All @@ -31,6 +33,7 @@ const Giphy: FC<Props> = ({ setGifAttachment }) => {
setShowModal(!showModal);
Leafwatch.track(PUBLICATION.NEW.OPEN_GIF);
}}
disabled={attachments.length >= 4}
aria-label="Choose GIFs"
>
<div className="w-full fill-brand-500 dark:fill-brand-400">
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/components/Composer/Editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import LexicalAutoLinkPlugin from '@components/Shared/Lexical/Plugins/AutoLinkPlugin';
import EmojisPlugin from '@components/Shared/Lexical/Plugins/EmojisPlugin';
import ImagesPlugin from '@components/Shared/Lexical/Plugins/ImagesPlugin';
import ToolbarPlugin from '@components/Shared/Lexical/Plugins/ToolbarPlugin';
import useUploadAttachments from '@components/utils/hooks/useUploadAttachments';
import { $convertToMarkdownString, TEXT_FORMAT_TRANSFORMERS } from '@lexical/markdown';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin';
Expand All @@ -10,6 +12,7 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ERROR_MESSAGE } from 'data/constants';
import type { FC } from 'react';
import { toast } from 'react-hot-toast';
import { usePublicationStore } from 'src/store/publication';

import MentionsPlugin from '../../Shared/Lexical/Plugins/AtMentionsPlugin';
Expand All @@ -18,6 +21,17 @@ const TRANSFORMERS = [...TEXT_FORMAT_TRANSFORMERS];

const Editor: FC = () => {
const setPublicationContent = usePublicationStore((state) => state.setPublicationContent);
const attachments = usePublicationStore((state) => state.attachments);
const { handleUploadAttachments } = useUploadAttachments();

const handlePaste = async (pastedFiles: FileList) => {
if (attachments.length === 4 || attachments.length + pastedFiles.length > 4) {
return toast.error('You can only upload 4 files.');
}
if (pastedFiles) {
await handleUploadAttachments(pastedFiles);
}
};

return (
<div className="relative">
Expand All @@ -44,6 +58,7 @@ const Editor: FC = () => {
<HistoryPlugin />
<HashtagPlugin />
<MentionsPlugin />
<ImagesPlugin onPaste={handlePaste} />
bigint marked this conversation as resolved.
Show resolved Hide resolved
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
</div>
);
Expand Down
28 changes: 19 additions & 9 deletions apps/web/src/components/Composer/NewPublication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ const NewPublication: FC<Props> = ({ publication }) => {
const setPublicationContent = usePublicationStore((state) => state.setPublicationContent);
const audioPublication = usePublicationStore((state) => state.audioPublication);
const setShowNewPostModal = usePublicationStore((state) => state.setShowNewPostModal);
const attachments = usePublicationStore((state) => state.attachments);
const setAttachments = usePublicationStore((state) => state.setAttachments);
const addAttachments = usePublicationStore((state) => state.addAttachments);
const isUploading = usePublicationStore((state) => state.isUploading);

// Transaction persist store
const txnQueue = useTransactionPersistStore((state) => state.txnQueue);
Expand All @@ -112,9 +116,8 @@ const NewPublication: FC<Props> = ({ publication }) => {
const restricted = useAccessSettingsStore((state) => state.restricted);

// States
const [publicationContentError, setPublicationContentError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [attachments, setAttachments] = useState<LensterAttachment[]>([]);
const [publicationContentError, setPublicationContentError] = useState('');
const [editor] = useLexicalComposerContext();
const provider = useProvider();
const { data: signer } = useSigner();
Expand Down Expand Up @@ -402,14 +405,20 @@ const NewPublication: FC<Props> = ({ publication }) => {
});
}

const attachmentsInput: LensterAttachment[] = attachments.map((attachment) => ({
type: attachment.type,
altTag: attachment.altTag,
item: attachment.item!
}));

const metadata: PublicationMetadataV2Input = {
version: '2.0.0',
metadata_id: uuid(),
description: trimify(publicationContent),
content: trimify(publicationContent),
external_url: `https://lenster.xyz/u/${currentProfile?.handle}`,
image: attachments.length > 0 ? getAttachmentImage() : textNftImageUrl,
imageMimeType: attachments.length > 0 ? getAttachmentImageMimeType() : 'image/svg+xml',
image: attachmentsInput.length > 0 ? getAttachmentImage() : textNftImageUrl,
imageMimeType: attachmentsInput.length > 0 ? getAttachmentImageMimeType() : 'image/svg+xml',
name: isAudioPublication
? audioPublication.title
: `${isComment ? 'Comment' : 'Post'} by @${currentProfile?.handle}`,
Expand All @@ -418,7 +427,7 @@ const NewPublication: FC<Props> = ({ publication }) => {
mainContentFocus: getMainContentFocus(),
contentWarning: null,
attributes,
media: attachments,
media: attachmentsInput,
locale: getUserLocale(),
appId: APP_NAME
};
Expand Down Expand Up @@ -473,11 +482,12 @@ const NewPublication: FC<Props> = ({ publication }) => {

const setGifAttachment = (gif: IGif) => {
const attachment = {
id: uuid(),
item: gif.images.original.url,
type: 'image/gif',
altTag: gif.title
};
setAttachments([...attachments, attachment]);
addAttachments([attachment]);
};

return (
Expand All @@ -489,15 +499,15 @@ const NewPublication: FC<Props> = ({ publication }) => {
)}
<div className="block items-center sm:flex px-5">
<div className="flex items-center space-x-4">
<Attachment attachments={attachments} setAttachments={setAttachments} />
<Attachment />
<Giphy setGifAttachment={(gif: IGif) => setGifAttachment(gif)} />
<CollectSettings />
<ReferenceSettings />
<AccessSettings />
</div>
<div className="ml-auto pt-2 sm:pt-0">
<Button
disabled={isSubmitting}
disabled={isSubmitting || isUploading}
icon={
isSubmitting ? (
<Spinner size="xs" />
Expand All @@ -514,7 +524,7 @@ const NewPublication: FC<Props> = ({ publication }) => {
</div>
</div>
<div className="px-5">
<Attachments attachments={attachments} setAttachments={setAttachments} isNew />
<Attachments attachments={attachments} isNew />
</div>
</Card>
);
Expand Down
22 changes: 12 additions & 10 deletions apps/web/src/components/Shared/Attachments.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@components/UI/Button';
import { LightBox } from '@components/UI/LightBox';
import type { LensterAttachment, LensterPublication } from '@generated/types';
import type { LensterPublication, NewLensterAttachment } from '@generated/types';
import { ExternalLinkIcon, XIcon } from '@heroicons/react/outline';
import getIPFSLink from '@lib/getIPFSLink';
import imageProxy from '@lib/imageProxy';
Expand All @@ -10,6 +10,7 @@ import { ALLOWED_AUDIO_TYPES, ALLOWED_VIDEO_TYPES, ATTACHMENT } from 'data/const
import type { MediaSet } from 'lens';
import type { FC } from 'react';
import { useState } from 'react';
import { usePublicationStore } from 'src/store/publication';
import { PUBLICATION } from 'src/tracking';

import Audio from './Audio';
Expand All @@ -36,21 +37,20 @@ const getClass = (attachments: number, isNew = false) => {

interface Props {
attachments: any;
setAttachments?: any;
isNew?: boolean;
hideDelete?: boolean;
publication?: LensterPublication;
txn?: any;
}

const Attachments: FC<Props> = ({
attachments,
setAttachments,
attachments = [],
isNew = false,
hideDelete = false,
publication,
txn
}) => {
const setAttachments = usePublicationStore((state) => state.setAttachments);
const [expandedImage, setExpandedImage] = useState<string | null>(null);

const removeAttachment = (attachment: any) => {
Expand All @@ -64,16 +64,18 @@ const Attachments: FC<Props> = ({

const slicedAttachments = isNew
? attachments?.slice(0, 4)
: attachments?.some((e: any) => ALLOWED_VIDEO_TYPES.includes(e.original.mimeType))
: attachments?.some((e: any) => ALLOWED_VIDEO_TYPES.includes(e?.original?.mimeType))
? attachments?.slice(0, 1)
: attachments?.slice(0, 4);

return slicedAttachments?.length !== 0 ? (
<>
<div className={clsx(getClass(slicedAttachments?.length)?.row, 'grid gap-2 pt-3')}>
{slicedAttachments?.map((attachment: LensterAttachment & MediaSet, index: number) => {
const type = isNew ? attachment.type : attachment.original.mimeType;
const url = isNew ? getIPFSLink(attachment.item) : getIPFSLink(attachment.original.url);
{slicedAttachments?.map((attachment: NewLensterAttachment & MediaSet, index: number) => {
const type = isNew ? attachment.type : attachment.original?.mimeType;
const url = isNew
? attachment.previewItem || getIPFSLink(attachment.item!)
: getIPFSLink(attachment.original?.url);

return (
<div
Expand Down Expand Up @@ -119,8 +121,8 @@ const Attachments: FC<Props> = ({
setExpandedImage(url);
Leafwatch.track(PUBLICATION.ATTACHEMENT.IMAGE.OPEN);
}}
src={imageProxy(url, ATTACHMENT)}
alt={imageProxy(url, ATTACHMENT)}
src={isNew ? url : imageProxy(url, ATTACHMENT)}
alt={isNew ? url : imageProxy(url, ATTACHMENT)}
/>
)}
{isNew && !hideDelete && (
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/components/Shared/Lexical/Plugins/ImagesPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { COMMAND_PRIORITY_NORMAL, PASTE_COMMAND } from 'lexical';
import { useEffect } from 'react';

type ImagesPluginProps = {
onPaste: (files: FileList) => void;
};

const ImagesPlugin = (props: ImagesPluginProps): JSX.Element | null => {
const { onPaste } = props;
const [editor] = useLexicalComposerContext();

useEffect(() => {
return mergeRegister(
editor.registerCommand<InputEvent>(
PASTE_COMMAND,
(event: InputEvent) => {
/*
* This registers a paste event listener on the editor.
* The InputEvent/ClipboardEvent will be triggered both when the user pastes something into the editor.
*/
if (event) {
const { dataTransfer } = event;
if (dataTransfer && dataTransfer.files.length) {
const { files } = dataTransfer;
onPaste && onPaste(files);
}
return true;
}
return false;
},
COMMAND_PRIORITY_NORMAL
)
);
}, [editor, onPaste]);

return null;
};

export default ImagesPlugin;
Loading