diff --git a/src/stories/chat/_types.tsx b/src/stories/chat/_types.tsx index 8c23a9f6..c96d2810 100644 --- a/src/stories/chat/_types.tsx +++ b/src/stories/chat/_types.tsx @@ -1,6 +1,5 @@ import { PlaceholderOptions } from "@tiptap/extension-placeholder"; import { BubbleMenuProps, EditorOptions } from "@tiptap/react"; -import { isError } from "util"; type validationStatus = "success" | "warning" | "error"; @@ -8,8 +7,6 @@ export type SuggestedUser = { id: number; name: string; email: string }; export interface ChatEditorArgs extends Partial { author: Author; - messageBadFileFormat: string; - placeholderOptions?: Partial; hasFloatingMenu?: boolean; hasButtonsMenu?: boolean; @@ -20,7 +17,7 @@ export interface ChatEditorArgs extends Partial { italic?: string; mention?: string; //react node - attachment?: string | React.ReactNode; + attachment?: React.ReactNode; }; mention?: { noResults?: string; @@ -43,10 +40,13 @@ export interface EditorHeaderArgs { validation?: validationStatus; } -export interface FileItem extends File { - isLoadingMedia: boolean; +export interface CommentMedia { + id: string; + type: string; + name?: string; + isLoadingMedia?: boolean; isError?: boolean; - internal_id: string; + url?: string; } export interface FloatingMenuArgs extends Partial {} diff --git a/src/stories/chat/context/chatContext.tsx b/src/stories/chat/context/chatContext.tsx index 972a9e8d..0e6654f6 100644 --- a/src/stories/chat/context/chatContext.tsx +++ b/src/stories/chat/context/chatContext.tsx @@ -1,14 +1,19 @@ import { Editor } from "@tiptap/react"; -import React, { createContext, useContext, useMemo, useState } from "react"; -import { FileItem, SuggestedUser } from "../_types"; +import React, { + createContext, + useContext, + useMemo, + useState, +} from "react"; +import { CommentMedia, SuggestedUser } from "../_types"; export type ChatContextType = { triggerSave: () => void; editor?: Editor; setEditor: React.Dispatch>; - addThumbnails: (props: { files: FileItem[] }) => void; + addThumbnails: (props: { files: (File & CommentMedia)[] }) => void; removeThumbnail: (index: number) => void; - thumbnails: FileItem[]; + thumbnails: CommentMedia[]; mentionableUsers: (props: { query: string }) => SuggestedUser[]; afterUploadCallback: (failed: string[]) => void; clearInput: () => void; @@ -29,13 +34,13 @@ export const ChatContextProvider = ({ children, }: { onSave?: (editor: Editor, mentions: SuggestedUser[]) => void; - onFileUpload?: (files: FileItem[]) => Promise; + onFileUpload?: (files: (File & CommentMedia)[]) => Promise; onDeleteThumbnail: (id: string) => void; children: React.ReactNode; setMentionableUsers: (props: { query: string }) => SuggestedUser[]; }) => { const [editor, setEditor] = useState(); - const [thumbnails, setThumbnails] = useState([]); + const [thumbnails, setThumbnails] = useState([]); const getMentions = (editor: Editor) => { const result: SuggestedUser[] = []; @@ -64,7 +69,7 @@ export const ChatContextProvider = ({ afterUploadCallback: (failed: string[]) => { setThumbnails( thumbnails.map((file) => { - if (failed.includes(file.name)) { + if (failed.includes(file.id)) { file.isLoadingMedia = false; //file.isError = true; } else { @@ -76,18 +81,16 @@ export const ChatContextProvider = ({ ); }, - addThumbnails: ({ files }: { files: FileItem[] }) => { - files.forEach((file) => (file.isLoadingMedia = true)); + addThumbnails: ({ files }: { files: (File & CommentMedia)[] }) => { setThumbnails((prev) => [...prev, ...files]); if (onFileUpload) { onFileUpload(files).then((data: Data) => { const failed = data.failed?.map((f) => f.name); - setThumbnails((prev) => { return prev.map((file) => { file.isLoadingMedia = false; - if (failed?.length && failed.includes(file.name)) { + if (failed?.length && failed.includes(file.id)) { file.isError = true; } else { file.isError = false; @@ -103,7 +106,6 @@ export const ChatContextProvider = ({ editor.commands.clearContent(); } if (thumbnails.length > 0) setThumbnails([]); - }, onDeleteThumbnail: (id: string) => { diff --git a/src/stories/chat/hooks/useMedia.tsx b/src/stories/chat/hooks/useMedia.tsx new file mode 100644 index 00000000..33fe54b4 --- /dev/null +++ b/src/stories/chat/hooks/useMedia.tsx @@ -0,0 +1,46 @@ +import { useToast, Notification } from "../../notifications"; +import { CommentMedia } from "../_types"; +import { v4 as uuidv4 } from "uuid"; + +export const acceptedMediaTypes = /^(image|video)\//; + +export function useMedia() { + const { addToast } = useToast(); + function getValidMedia(data: FileList): File[] { + const wrongFiles = Array.from(data).filter( + (file) => !acceptedMediaTypes.test(file.type) + ); + if (wrongFiles.length) { + addToast( + ({ close }) => ( + + ), + { placement: "top" } + ); + } + return Array.from(data).filter((file) => + acceptedMediaTypes.test(file.type) + ); + } + + function getMedia(data: FileList) { + return getValidMedia(data).map((file) => { + return Object.assign(file, { + url: URL.createObjectURL(file), + isLoadingMedia: true, + id: uuidv4(), + }); + }); + } + + return { getMedia }; +} \ No newline at end of file diff --git a/src/stories/chat/index.stories.tsx b/src/stories/chat/index.stories.tsx index 0bd63548..3660ab85 100644 --- a/src/stories/chat/index.stories.tsx +++ b/src/stories/chat/index.stories.tsx @@ -7,8 +7,8 @@ import { Button } from "../buttons/button"; import { Col } from "../grid/col"; import { Grid } from "../grid/grid"; import { Row } from "../grid/row"; -import { ChatEditorArgs, FileItem, SuggestedUser } from "./_types"; -import { Comment, MediaType } from "./parts/comment"; +import { ChatEditorArgs, CommentMedia, SuggestedUser } from "./_types"; +import { Comment } from "./parts/comment"; import { theme } from "../theme"; import { Data } from "./context/chatContext"; import { ToastProvider } from "@zendeskgarden/react-notifications"; @@ -27,25 +27,26 @@ interface EditorStoryArgs extends ChatEditorArgs { }; message: string; date: string; - media?: MediaType[]; + media?: CommentMedia[]; }[]; editorText?: string; background?: string; onSave: (editor: TipTapEditor, mentions: SuggestedUser[]) => void; - onFileUpload?: (files: FileItem[]) => Promise; + onFileUpload?: (files: (File & CommentMedia)[]) => Promise; placeholderOptions?: Partial; } const ChatPanel = ({ background, ...args }: EditorStoryArgs) => { - const { editor, triggerSave, clearInput } = useChatContext(); + const { triggerSave, clearInput } = useChatContext(); return ( Titolone - {args.comments?.map((comment) => ( + {args.comments?.map((comment, index) => ( <>altre cose @@ -154,9 +155,9 @@ const Template: StoryFn = ({ children, ...args }) => { console.log("internal_id - ", id); }} - /*setIsMediaUploading={function (value: boolean): void { - throw new Error("Function not implemented."); - }}*/ + /*setIsMediaUploading={function (value: boolean): void { + throw new Error("Function not implemented."); + }}*/ > @@ -170,11 +171,11 @@ const Template: StoryFn = ({ children, ...args }) => { const defaultArgs: EditorStoryArgs = { children: "

I'm a stupid editor!

", - onSave: (editor: TipTapEditor, mentions) => {}, + onSave: (editor: TipTapEditor, mentions) => { }, author: { avatar: "LC", }, - onUpdate: ({ editor }) => {}, + onUpdate: ({ editor }) => { }, comments: [ { message: "Hi, I'm a comment", @@ -212,17 +213,20 @@ const defaultArgs: EditorStoryArgs = { { url: "https://images.unsplash.com/photo-1638799692504-9b3c0093d54d?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", type: "image", - id: 1, + id: "1", + isLoadingMedia: false, + name: "" }, { url: "https://images.unsplash.com/photo-1544085311-11a028465b03?q=80&w=1932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", type: "image", - id: 2, + id: "2", + isLoadingMedia: false, + name: "" }, ], }, ], - messageBadFileFormat: "Format not supported, please upload video or image", }; export const Default = Template.bind({}); diff --git a/src/stories/chat/parts/MediaLightbox.tsx b/src/stories/chat/parts/MediaLightbox.tsx new file mode 100644 index 00000000..c3a64e78 --- /dev/null +++ b/src/stories/chat/parts/MediaLightbox.tsx @@ -0,0 +1,64 @@ +import { Lightbox } from "../../lightbox"; +import { Slider } from "../../slider"; +import { CommentMedia } from "../_types"; +import { Player } from "../../player"; + +interface MediaLightBoxProps { + isOpen: boolean; + header: React.ReactNode; + onClose: () => void; + slideChange: (index: number) => void; + selectedImageIndex: number; + thumbnails: CommentMedia[]; + videoRefs: React.MutableRefObject>; + details?: React.ReactNode; +}; + +const MediaLightBox = ({header, onClose, slideChange, selectedImageIndex, thumbnails, videoRefs, isOpen, details}: MediaLightBoxProps) => { + if (!isOpen) { + return null; + } + return ( + + {header} + + + } + nextArrow={} + onSlideChange={slideChange} + initialSlide={selectedImageIndex} + > + {thumbnails.map((item) => ( + + {item.type.includes("image") && ( + {`media + )} + {item.type.includes("video") && item.url && ( + { + videoRefs.current.push(ref); + }} + url={item.url} + /> + )} + + ))} + + + {details && ( + + {details} + + )} + + + + ); +} + +export default MediaLightBox; \ No newline at end of file diff --git a/src/stories/chat/parts/ThumbnailContainer/DeleteThumbnailX.tsx b/src/stories/chat/parts/ThumbnailContainer/DeleteThumbnailX.tsx index bfe917a9..76ef6bf0 100644 --- a/src/stories/chat/parts/ThumbnailContainer/DeleteThumbnailX.tsx +++ b/src/stories/chat/parts/ThumbnailContainer/DeleteThumbnailX.tsx @@ -10,6 +10,7 @@ const StyledDeleteThumbnailX = styled.div` width: 32px; height: 32px; opacity: 0; + transition: opacity 0.2s; z-index: 2; `; diff --git a/src/stories/chat/parts/ThumbnailContainer/ImageThumbnail.tsx b/src/stories/chat/parts/ThumbnailContainer/ImageThumbnail.tsx deleted file mode 100644 index 856c88df..00000000 --- a/src/stories/chat/parts/ThumbnailContainer/ImageThumbnail.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import styled from "styled-components"; -import DeleteThumbnailX from "./DeleteThumbnailX"; -import { Spinner } from "@zendeskgarden/react-loaders"; -import { SpecialCard } from "../../../special-cards"; - -const ImageCard = styled(SpecialCard)` - padding: 0; - :hover .deleteThumbnail { - opacity: 1; - } -`; - -const Preview = styled.div<{ - url: string; -}>` - display: flex; - justify-content: center; - align-items: center; - height: 150px; - width: 100%; - background-image: url(${(props) => props.url}); - background-color: ${({ theme }) => theme.palette.grey[100]}; - background-size: contain; - background-position: center; - background-repeat: no-repeat; -`; - -interface Props { - src: string; - index?: number; - removeThumbnail?: (index: number) => void; - clickThumbnail: () => void; - showX?: boolean; - isLoadingMedia: boolean; - isError?: boolean; -} - -const ImageThumbnail = ({ - src, - index = 0, - removeThumbnail, - clickThumbnail, - showX = true, - isLoadingMedia = true, - isError = false, -}: Props) => { - const handleCancel = (e: any) => { - e.stopPropagation(); - if (removeThumbnail) removeThumbnail(index); - }; - - return ( - - {isLoadingMedia && ( - - - - )} - {isError && ( - // todo: add error icon - error uploading media - )} - {!isLoadingMedia && ( - - {showX && ( - handleCancel(e)} /> - )} - - )} - - ); -}; - -export default ImageThumbnail; diff --git a/src/stories/chat/parts/ThumbnailContainer/Thumbnail.tsx b/src/stories/chat/parts/ThumbnailContainer/Thumbnail.tsx new file mode 100644 index 00000000..089c49e2 --- /dev/null +++ b/src/stories/chat/parts/ThumbnailContainer/Thumbnail.tsx @@ -0,0 +1,136 @@ +import styled from "styled-components"; +import DeleteThumbnailX from "./DeleteThumbnailX"; +import { Spinner } from "@zendeskgarden/react-loaders"; +import { SpecialCard } from "../../../special-cards"; +import { ReactComponent as VideoPlayIcon } from "../../../../assets/icons/video-play-icon.svg"; + +const ImageCard = styled(SpecialCard)` + padding: 0; + position: relative; + overflow: hidden; + min-width: 90px; + + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.palette.grey[800]}; + opacity: 0; + transition: opacity 0.2s; + z-index: 1; + } + + &:hover { + .deleteThumbnail { + opacity: 1; + } + &:before { + opacity: 0.3; + } + } + + &.video { + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 32px; + height: 32px; + z-index: 2; + } + } +`; + +const Preview = styled.div<{ + url?: string; +}>` + display: flex; + justify-content: center; + align-items: center; + height: 100px; + width: 100%; + + ${(p) => + p.url && + ` + background-image: url(${p.url}); + background-color: ${p.theme.palette.grey[100]}; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + `} + + > video { + width: 100%; + height: 100%; + } +`; + +interface Props { + src?: string; + type: string; + clickThumbnail?: () => void; + isLoadingMedia?: boolean; + removeThumbnail?: () => void; + showX?: boolean; + isError?: boolean; +} + +const Thumbnail = ({ + src, + type, + removeThumbnail, + clickThumbnail, + showX, + isLoadingMedia = true, + isError = false, +}: Props) => { + const handleCancel = (e: any) => { + e.stopPropagation(); + if (removeThumbnail) removeThumbnail(); + }; + + return ( + + {isError && ( + // todo: add error icon + error uploading media + )} + {isLoadingMedia ? ( + + + + ) : ( + + {showX && ( + handleCancel(e)} /> + )} + {type.includes("video") && ( + <> + + + + )} + + )} + + ); +}; + +export default Thumbnail; diff --git a/src/stories/chat/parts/ThumbnailContainer/VideoThumbnail.tsx b/src/stories/chat/parts/ThumbnailContainer/VideoThumbnail.tsx deleted file mode 100644 index c2a78a5a..00000000 --- a/src/stories/chat/parts/ThumbnailContainer/VideoThumbnail.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import styled from "styled-components"; -import DeleteThumbnailX from "./DeleteThumbnailX"; -import { Spinner } from "@zendeskgarden/react-loaders"; -import { SpecialCard } from "../../../special-cards"; -import { ReactComponent as VideoPlayIcon } from "../../../../assets/icons/video-play-icon.svg"; - -const VideoCard = styled(SpecialCard)` - padding: 0; - position: relative; - overflow: hidden; - - &:hover .deleteThumbnail { - opacity: 1; - z-index: 9999; - } - - &:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: ${({ theme }) => theme.palette.grey[800]}; - opacity: 0.3; - z-index: 1; - } - - > svg { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 32px; - height: 32px; - z-index: 2; - } -`; - -const Preview = styled.div` - padding: ${({ theme }) => theme.space.md}; - display: flex; - justify-content: center; - align-items: center; - height: 150px; - width: 100%; - - > video { - width: 100%; - height: 100%; - } -`; - -interface Props { - src: string; - index?: number; - removeThumbnail?: (index: number) => void; - clickThumbnail: () => void; - showX?: boolean; - isLoadingMedia: boolean; - isError?: boolean; -} - -const VideoThumbnail = ({ - src, - index = 0, - removeThumbnail, - clickThumbnail, - showX = true, - isLoadingMedia = true, - isError = false, -}: Props) => { - const handleCancel = (e: any) => { - e.stopPropagation(); - if (removeThumbnail) removeThumbnail(index); - }; - - return ( - - {isLoadingMedia && ( - - - - )} - {isError && ( - // todo: add error icon - error uploading media - )} - {!isLoadingMedia && ( - <> - {showX && ( - handleCancel(e)}/> - )} - - - - - - - )} - - - ); -}; - -export default VideoThumbnail; diff --git a/src/stories/chat/parts/ThumbnailContainer/index.tsx b/src/stories/chat/parts/ThumbnailContainer/index.tsx index 2dec11ab..8e282d5f 100644 --- a/src/stories/chat/parts/ThumbnailContainer/index.tsx +++ b/src/stories/chat/parts/ThumbnailContainer/index.tsx @@ -1,10 +1,13 @@ -import ImageThumbnail from "./ImageThumbnail"; -import VideoThumbnail from "./VideoThumbnail"; +import Thumbnail from "./Thumbnail"; import { useChatContext } from "../../context/chatContext"; -import { Grid } from "../../../grid/grid"; -import { Row } from "../../../grid/row"; -import { Col } from "../../../grid/col"; import { useMemo } from "react"; +import { styled } from "styled-components"; + +const FlexContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.space.xs}; + flex-wrap: wrap; +`; export interface FileElement { fileName: string; @@ -16,7 +19,7 @@ export interface FileElement { } interface Props { - openLightbox: (file: File, index: number) => void; + openLightbox: (index: number) => void; } const ThumbnailContainer = ({ openLightbox }: Props) => { @@ -26,8 +29,8 @@ const ThumbnailContainer = ({ openLightbox }: Props) => { return thumbnails.map((file) => ({ fileName: file.name, fileType: file.type, - previewUrl: URL.createObjectURL(file), - internal_id: file.internal_id, + previewUrl: file.url, + id: file.id, isLoadingMedia: file.isLoadingMedia, })); }, [thumbnails]); @@ -37,52 +40,24 @@ const ThumbnailContainer = ({ openLightbox }: Props) => { } return ( - - - {mediaFiles.map((file, index) => { - // Check if item is an image or a video - if (file.fileType.includes("image")) - return ( - - { - removeThumbnail(index); - onDeleteThumbnail(file.internal_id); - }} - clickThumbnail={() => { - openLightbox(thumbnails[index], index); - }} - /> - - ); - if (file.fileType.includes("video")) - return ( - - { - removeThumbnail(index); - onDeleteThumbnail(file.internal_id); - }} - clickThumbnail={() => { - openLightbox(thumbnails[index], index); - }} - /> - - ); - return null; - })} - - + + {mediaFiles.map((file, index) => ( + { + removeThumbnail(index); + onDeleteThumbnail(file.id); + }} + clickThumbnail={() => { + openLightbox(index); + }} + /> + ))} + ); }; diff --git a/src/stories/chat/parts/bar.tsx b/src/stories/chat/parts/bar.tsx index 0c146683..8462c92b 100644 --- a/src/stories/chat/parts/bar.tsx +++ b/src/stories/chat/parts/bar.tsx @@ -1,7 +1,6 @@ import styled from "styled-components"; -import { v4 as uuidv4 } from "uuid"; import { Tooltip } from "../../tooltip"; -import { ChatEditorArgs, FileItem } from "../_types"; +import { ChatEditorArgs } from "../_types"; import { Editor } from "@tiptap/react"; import { isMac } from "../../theme/utils"; import { ReactComponent as BoldIcon } from "../../../assets/icons/bold-stroke.svg"; @@ -10,6 +9,8 @@ import { ReactComponent as MentionIcon } from "../../../assets/icons/at-stroke.s import { ReactComponent as AttachmentIcon } from "../../../assets/icons/clipboard.svg"; import { IconButton } from "../../buttons/icon-button"; import { useChatContext } from "../context/chatContext"; +import { useMedia } from "../hooks/useMedia"; +import { theme } from "../../theme"; const MenuContainer = styled.div` padding: ${({ theme }) => theme.space.xs} 0; @@ -34,65 +35,35 @@ const CommentBar = ({ editor?: Editor; }) => { const { addThumbnails } = useChatContext(); - + const { getMedia } = useMedia(); if (!editor) return null; - type MenuItem = { - type: "bold" | "italic" | "mention" | "attachment"; + const handleBoldClick = () => { + editor.chain().focus().toggleBold().run(); }; - const getIcon = (type: MenuItem["type"]) => { - switch (type) { - case "bold": - return ; - case "italic": - return ; - case "mention": - return ; - case "attachment": - return ; - default: - return null; - } + const handleItalicClick = () => { + editor.chain().focus().toggleItalic().run(); }; - const handleClick = (type: MenuItem["type"]) => { - switch (type) { - case "bold": - return editor.chain().focus().toggleBold().run(); - case "italic": - return editor.chain().focus().toggleItalic().run(); - case "mention": - const { from } = editor.state.selection; - const char = from > 1 ? " @" : "@"; - return editor.chain().focus().insertContent(char).run(); - case "attachment": - //open a file browser to select one or more images - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = "image/*,video/*"; - fileInput.multiple = true; - fileInput.click(); - - fileInput.onchange = () => { - const files = fileInput.files; - if (files) { - const mediaFiles: FileItem[] = Array.from(files).map((file) => { - return Object.assign(file, { - isLoadingMedia: false, - internal_id: uuidv4(), - }); - }); + const handleMentionClick = () => { + const { from } = editor.state.selection; + const char = from > 1 ? " @" : "@"; + editor.chain().focus().insertContent(char).run(); + }; - if (mediaFiles.length === 0) return; + const handleAttachmentClick = () => { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "image/*,video/*"; + fileInput.multiple = true; + fileInput.click(); - addThumbnails({ files: mediaFiles }); - } - }; - return; - default: - return; - } + fileInput.onchange = () => { + if (fileInput.files) { + addThumbnails({ files: getMedia(fileInput.files) }); + } + }; }; return ( @@ -112,9 +83,9 @@ const CommentBar = ({ isBasic={!editor.isActive("bold")} isPrimary={editor.isActive("bold")} isPill={false} - onClick={() => handleClick("bold")} + onClick={handleBoldClick} > - {getIcon("bold")} + handleClick("italic")} + onClick={handleItalicClick} > - {getIcon("italic")} + @@ -149,14 +120,22 @@ const CommentBar = ({ isBasic={!editor.isActive("mention")} isPrimary={editor.isActive("mention")} isPill={false} - onClick={() => handleClick("mention")} + onClick={handleMentionClick} > - {getIcon("mention")} + + Upload images and video.{" "} + + {" "} +
Max size: 5GB{" "} +
+ + ) } placement="top" type="light" @@ -168,9 +147,9 @@ const CommentBar = ({ isBasic={!editor.isActive("attachment")} isPrimary={editor.isActive("attachment")} isPill={false} - onClick={() => handleClick("attachment")} + onClick={handleAttachmentClick} > - {getIcon("attachment")} +
diff --git a/src/stories/chat/parts/comment.tsx b/src/stories/chat/parts/comment.tsx index 2fda030d..60b57606 100644 --- a/src/stories/chat/parts/comment.tsx +++ b/src/stories/chat/parts/comment.tsx @@ -2,21 +2,18 @@ import { PropsWithChildren, useCallback, useRef, useState } from "react"; import { Title } from "../../title"; import { Card } from "../../cards"; import { styled } from "styled-components"; -import { Author } from "../_types"; +import { Author, CommentMedia } from "../_types"; import { Avatar } from "../../avatar"; import { useChatContext } from "../context/chatContext"; import { Content, useEditor, EditorContent } from "@tiptap/react"; import { editorExtensions } from "./extensions"; import { EditorContainer } from "./containers"; -import { Lightbox } from "../../lightbox"; -import { Slider } from "../../slider"; import { MD } from "@zendeskgarden/react-typography"; -import { Player } from "../../player"; import { Grid } from "../../grid/grid"; import { Row } from "../../grid/row"; import { Col } from "../../grid/col"; -import ImageThumbnail from "./ThumbnailContainer/ImageThumbnail"; -import VideoThumbnail from "./ThumbnailContainer/VideoThumbnail"; +import Thumbnail from "./ThumbnailContainer/Thumbnail"; +import MediaLightBox from "./MediaLightbox"; const CommentCard = styled(Card)` padding: ${({ theme }) => `${theme.space.base * 3}px ${theme.space.sm}`}; @@ -64,12 +61,6 @@ const Grey800Span = styled.span` color: ${({ theme }) => theme.palette.grey[800]}; `; -export type MediaType = { - url: string; - id: number; - type: "image" | "video"; -}; - export const Comment = ({ author, message, @@ -81,7 +72,7 @@ export const Comment = ({ author: Author; message: string; date: string; - media?: MediaType[]; + media?: CommentMedia[]; header: { title: string; message?: string; @@ -89,17 +80,12 @@ export const Comment = ({ }>) => { const { mentionableUsers } = useChatContext(); const [isOpen, setIsOpen] = useState(false); - const [selectedImage, setSelectedImage] = useState( - {} as MediaType - ); + const [selectedImageIndex, setSelectedImageIndex] = useState(0); const ext = editorExtensions({ mentionableUsers }); - const handleClickThumbnail = (file: MediaType, index: number) => { - if (!file) throw Error("Error with the image"); - - setSelectedImage(file); + const handleClickThumbnail = (index: number) => { setSelectedImageIndex(index); setIsOpen(true); }; @@ -112,7 +98,6 @@ export const Comment = ({ const slideChange = useCallback( (index: number) => { - setSelectedImage(media[index]); setSelectedImageIndex(index); videoRefs.current.forEach((ref) => { if (ref) { @@ -145,8 +130,7 @@ export const Comment = ({
- {author.name ?? "User"}{" "} - {date} + {author.name ?? "User"} {date} @@ -157,99 +141,52 @@ export const Comment = ({ - {media.map((file, index) => { - // Check if item is an image or a video - if (file.type.includes("image")) - return ( - - { - handleClickThumbnail(file, index); - }} - /> - - ); - if (file.type.includes("video")) - return ( - - { - handleClickThumbnail(file, index); - }} - /> - - ); - return null; - })} + {media.map((file, index) => ( + + { + handleClickThumbnail(index); + }} + /> + + ))} - {isOpen && selectedImage && ( - - - - {header && header.title} - {header && header.message && ( - | {header.message} - )} - - - - - } - nextArrow={} - onSlideChange={slideChange} - initialSlide={selectedImageIndex} - > - {media.map((item, index) => ( - - {item.type === "image" && ( - {`{{${item.url}}}`} - )} - {item.type === "video" && ( - { - videoRefs.current.push(ref); - }} - url={item.url} - /> - )} - - ))} - - - - - <> -
- -
-
-
- - - -
- )} - + + {header && header.title} + {header && header.message && ( + | {header.message} + )} + + } + onClose={closeLightbox} + slideChange={slideChange} + selectedImageIndex={selectedImageIndex} + thumbnails={media} + videoRefs={videoRefs} + details={ + + <> +
+ +
+ } + />
{children}
); diff --git a/src/stories/chat/parts/commentBox.tsx b/src/stories/chat/parts/commentBox.tsx index c7078360..853886f2 100644 --- a/src/stories/chat/parts/commentBox.tsx +++ b/src/stories/chat/parts/commentBox.tsx @@ -1,12 +1,11 @@ import styled from "styled-components"; -import { v4 as uuidv4 } from "uuid"; import { useEditor, EditorContent, Editor as TipTapEditor, Content, } from "@tiptap/react"; -import { ChatEditorArgs, FileItem } from "../_types"; +import { ChatEditorArgs } from "../_types"; import { KeyboardEvent as ReactKeyboardEvent, PropsWithChildren, @@ -14,19 +13,14 @@ import { useRef, useCallback, } from "react"; - -import { Notification } from "../../notifications"; -import { useToast } from "../../notifications"; - import { FloatingMenu } from "../../editor/floatingMenu"; import { useChatContext } from "../context/chatContext"; import { CommentBar } from "./bar"; import { editorExtensions } from "./extensions"; import { EditorContainer } from "./containers"; import ThumbnailContainer from "./ThumbnailContainer"; -import { Lightbox } from "../../lightbox"; -import { Slider } from "../../slider"; -import { Player } from "../../player"; +import MediaLightBox from "./MediaLightbox"; +import { useMedia } from "../hooks/useMedia"; const ChatBoxContainer = styled.div` display: flex; @@ -61,14 +55,16 @@ export const CommentBox = ({ thumbnails, addThumbnails, } = useChatContext(); - - const { addToast } = useToast(); const [isOpen, setIsOpen] = useState(false); - const [selectedImage, setSelectedImage] = useState({} as File); const [selectedImageIndex, setSelectedImageIndex] = useState(0); - + const {getMedia} = useMedia(); const ext = editorExtensions({ placeholderOptions, mentionableUsers }); + function handleEvent(data: DataTransfer | null) { + if (!data || !data.files) return; + addThumbnails({ files: getMedia(data.files) }); + } + const closeLightbox = () => { setIsOpen(false); }; @@ -86,10 +82,7 @@ export const CommentBox = ({ }, [videoRefs] ); - const handleOpenLightbox = (file: File, index: number) => { - if (!file) throw Error("Error with the image"); - - setSelectedImage(file); + const handleOpenLightbox = (index: number) => { setSelectedImageIndex(index); setIsOpen(true); }; @@ -106,91 +99,13 @@ export const CommentBox = ({ }, handleDrop: function (view, event, slice, moved) { - if (!event.dataTransfer || !event.dataTransfer.files) return false; - event.preventDefault(); - - const files: FileItem[] = Array.from(event.dataTransfer.files).map( - (file) => { - return Object.assign(file, { - isLoadingMedia: false, - internal_id: uuidv4(), - }); - } - ); - const wrongFiles = files.filter( - (file) => !/^(image|video)\//.test(file.type) - ); - - if (wrongFiles.length > 0) { - for (const file of wrongFiles) { - addToast( - ({ close }) => ( - - ), - { placement: "top" } - ); - } - } - - const mediaFiles: FileItem[] = files.filter((file) => - /^(image|video)\//.test(file.type) - ); - - if (mediaFiles.length === 0) return false; - - addThumbnails({ files: mediaFiles }); - - return false; + handleEvent(event.dataTransfer); }, handlePaste: (view, event, slice) => { - - if (!event.clipboardData || !event.clipboardData.items) return false; - event.preventDefault(); - - const files: FileItem[] = Array.from(event.clipboardData.files).map( - (file) => { - return Object.assign(file, { - isLoadingMedia: false, - internal_id: uuidv4(), - }); - } - ); - const wrongFiles = files.filter( - (file) => !/^(image|video)\//.test(file.type) - ); - - if (wrongFiles.length > 0) { - for (const file of wrongFiles) { - addToast( - ({ close }) => ( - - ), - { placement: "top" } - ); - } - } - - const mediaFiles: FileItem[] = files.filter((file) => - /^(image|video)\//.test(file.type) - ); - if (mediaFiles.length === 0) return false; - - addThumbnails({ files: mediaFiles }); - - return false; + handleEvent(event.clipboardData); }, }, ...props, @@ -208,48 +123,17 @@ export const CommentBox = ({ ed.on("create", ({ editor }) => setEditor(editor as TipTapEditor)); ed.on("update", ({ editor }) => setEditor(editor as TipTapEditor)); - const mediaFiles = thumbnails.map((file) => { - return Object.assign(file, { isLoadingMedia: file.isLoadingMedia }); - }); - return ( <> - {isOpen && selectedImage && ( - - {selectedImage.name} - - - } - nextArrow={} - onSlideChange={slideChange} - initialSlide={selectedImageIndex} - > - {mediaFiles.map((item, index) => ( - - {item.type.includes("image") && ( - {`media - )} - {item.type.includes("video") && ( - { - videoRefs.current.push(ref); - }} - url={URL.createObjectURL(item)} - /> - )} - - ))} - - - - - - )} - + {hasFloatingMenu && (