@@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,3 +1,4 @@ - - + + \ No newline at end of file @@ -0,0 +1,3 @@ + + +export * from './use-images-library' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface ImageWithState { + image: string + state: boolean +} \ No newline at end of file @@ -0,0 +1,71 @@ +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import { ImageWithState } from './types' +import { Message } from '@/src/shared/lib/types/model' + +export const useImagesLibrary = ( + images: string[], + current: string | null, + reverse: boolean, + setModal: Dispatch> +) => { + const [imagesWithState, setImagesWithState] = useState(getImagesWithState(images)) + const [initialCount, setInitialCount] = useState(0) + + const esc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + setModal(false) + } + } + + function getImagesWithState(images: string[]) { + const imagesWithState = images.map((image) => ({ + image, + state: false, + })) + + return imagesWithState + } + + function updateImageState(image: string, state: boolean) { + setImagesWithState((prev) => prev.map((item) => (item.image === image ? { ...item, state } : item))) + } + + const currentImageIndex = useMemo( + () => (!current ? undefined : imagesWithState.findIndex((image) => image.image === current)), + [imagesWithState, current] + ) + + useEffect(() => { + if (initialCount === images.length) { + return + } + + setInitialCount(images.length) + + const filteredMessages = images.filter((item) => + !imagesWithState.find((i) => i.image === item) ? true : false + ) + + if (filteredMessages.length === images.length) { + return setImagesWithState(getImagesWithState(filteredMessages)) + } + + if (reverse) setImagesWithState([...imagesWithState, ...getImagesWithState(filteredMessages)]) + else setImagesWithState([...getImagesWithState(filteredMessages), ...imagesWithState]) + }, [images]) + + useEffect(() => { + window.addEventListener('keydown', esc) + return () => window.removeEventListener('keydown', esc) + }, []) + + return { + imagesWithState, + currentImageIndex, + initialCount, + setInitialCount, + setImagesWithState, + updateImageState, + } +} @@ -0,0 +1,54 @@ +import { debounce } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { Swiper as SwiperCore } from 'swiper' +import { ImageWithState } from './types' + +export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { + const [swiper, setSwiper] = useState(null) + + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault() + slideNext() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + slidePrev() + } + } + + const slidePrev = function () { + const result = swiper?.slidePrev() + + if (!result && onSlideFalse && !reverse) { + onSlideFalse() + } + } + + const slideNext = function () { + const result = swiper?.slideNext() + + if (!result && onSlideFalse && reverse) { + onSlideFalse() + } + } + + useEffect(() => { + document.addEventListener('keydown', keydown, true) + return () => { + document.removeEventListener('keydown', keydown, true) + } + }, [swiper]) + + // useEffect(() => { + // swiper?.slideTo(currentImageIndex || 0) + // }, [currentImageIndex]) + + return { + swiper, + setSwiper, + slidePrev, + slideNext, + } +} @@ -0,0 +1,193 @@ +import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import Image from 'next/image' + +import styles from './modal-styles.module.scss' +import { ArrowDropDown } from '@mui/icons-material' +import { c, Loader, TooltipCustom } from '@/src/shared' +import { useImagesLibrary } from '../model' + +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/css' +import { useLibrarySwiper } from '../model/use-swiper' +import { ImageIcons, useImageIcons } from '@/src/widgets/messages' +import { Message } from '@/src/shared/lib/types/model' +import { Typography } from '@mui/material' + +interface IProps { + modal: boolean + setModal: Dispatch> + current: string | null + setCurrent?: (value: string | null) => void + onSlideFalse?: (...args: any) => any + reverse?: boolean + images: Message[] +} + +export default function FullScreenModal({ modal, setModal, current, images, onSlideFalse, reverse = false }: IProps) { + const filteredMessages = useMemo(() => images.map((message) => message.file as string), [images]) + + const { imagesWithState, updateImageState, currentImageIndex, initialCount } = useImagesLibrary( + filteredMessages, + current, + reverse, + setModal + ) + + const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) + + const { downloadFile } = useImageIcons() + + useEffect(() => { + if (!(initialCount < images.length && swiper)) return + + if (reverse) { + setTimeout(() => swiper.slideTo(initialCount, 1000), 1000) + return + } + + setTimeout(() => swiper.slideTo(images.length - initialCount - 1, 1000), 1000) + }, [images]) + + return ( +
setModal(false)} + > +
+
+
+ { + e.stopPropagation() + if (!swiper) return + const { file, content } = images[swiper.activeIndex] + downloadFile(file as string, content) + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
+ { + e.stopPropagation() + if (!swiper) return + const { file } = images[swiper.activeIndex] + window.open(file as string, '_blank') + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
setModal(false)}> + + + +
+
+
+ + {modal && ( + setSwiper(swiper)} + > + {imagesWithState.map(({ image, state }, index) => ( + + {!state && } +
+ {!image.includes('.svg') ? ( + e.stopPropagation()} + onLoadingComplete={() => { + updateImageState(image, true) + }} + loading='lazy' + src={image} + width={'1500'} + height={'1500'} + alt='К сожалению, изображение не загрузилось' + /> + ) : ( + e.stopPropagation()} + onLoad={() => updateImageState(image, true)} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + )} +
+
+ ))} +
+ )} + +
+
+ ) +} @@ -0,0 +1 @@ +export { default as ImageModal } from './full-screen-modal' @@ -0,0 +1,105 @@ +.close_block { +} +.download { +} +.actions { + position: absolute; + z-index: 105; + cursor: pointer; + right: 25px; + top: 25px; + display: flex; + align-items: center; + gap: 15px; +} + +.arrow { + position: absolute; + z-index: 1205; + cursor: pointer; + top: 50%; + + width: 50px; + height: 50px; + + background: transparent; + border: none; + outline: none; + + svg { + fill: white; + width: 50px; + height: 50px; + } + + &_left { + transform: translateY(-50%) rotate(90deg); + left: -40px; + @media screen and (max-width: 768px) { + left: -3px; + } + } + + &_right { + transform: translateY(-50%) rotate(-90deg); + right: -50px; + + @media screen and (max-width: 768px) { + right: -10px; + } + } +} + +.image_block { + position: absolute; + padding: 0 20px; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 105; + margin: auto; + width: fit-content; + height: fit-content; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.image_style { + width: auto; + max-width: 70vw; + height: 100%; + position: relative; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + img { + width: 100%; + height: 100%; + object-fit: contain; + user-select: none; + } +} + +.slide { + width: 80vw !important; + display: flex !important; + align-items: center; + justify-content: center; + height: 100% !important; + max-height: unset; +} + +.swiper { + position: relative; + width: 100%; + max-width: 80vw; + max-height: 1000px; + height: 80vh; +} @@ -1,61 +0,0 @@ -import React, { Dispatch, SetStateAction } from 'react' -import Image from 'next/image' - -import styles from './modal-styles.module.scss' - -interface IProps { - modal: boolean - setModal: Dispatch> - image: string -} -export default function FullScreenModal({ modal, setModal, image }: IProps) { - const isSvg = image.includes('.svg') - - return ( -
-
-
setModal(false)}> - - - -
-
- {!isSvg ? ( - К сожалению, изображение не загрузилось - ) : ( - К сожалению, изображение не загрузилось - )} -
-
- ) -} @@ -0,0 +1 @@ +export * from './ui' @@ -1,30 +0,0 @@ -.close_block{ - position: absolute; - z-index: 105; - cursor: pointer; - right: 25px; - top: 25px; -} - -.image_block{ - position: absolute; - padding: 0 20px; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 105; - margin: auto; - width: fit-content; - height: fit-content; -} - -.image_style{ - position: relative; - width: 100%; - height: 100%; - max-width: 960px; - max-height: 700px; - border-radius: 15px; - object-fit: contain; -} \ No newline at end of file @@ -5,7 +5,7 @@ import axios from 'axios' import { UniqInput } from '@/src/main/components/uniq_input' import { useAppSelector } from '@/src/main/store/store' import { ChatProps } from '@/src/shared/lib/types/model' -import { ChatMessagesList } from '@/src/widgets/messages/chat-messages-list' +import { ChatMessagesList } from '@/src/widgets/messages/ui/chat-messages-list' import 'intro.js/introjs.css' @@ -64,7 +64,13 @@ function Chat({ spacing={0} sx={{ borderRadius: desktop ? '15px' : '13px', - backgroundColor: desktop ? (theme === 'light' ? 'white' : '#151518') : theme === 'light' ? 'white' : '#151518', + backgroundColor: desktop + ? theme === 'light' + ? 'white' + : '#151518' + : theme === 'light' + ? 'white' + : '#151518', padding: desktop ? 4 : 1.5, paddingTop: 0, paddingBottom: 2, @@ -24,7 +24,5 @@ height: 25px; border-radius: 7px; border: var(--new-ui-ctrl-f-button-border); - background: var(--new-ui-ctrl-f-button-bg); - - + background: var(--new-ui-ctrl-f-button-bg); } @@ -25,7 +25,7 @@ import { getTypeDevice } from '@/src/shared/lib/helpers' import { getDeviceOs } from '@/src/shared/lib/helpers/get-type-device' import { useShowData } from '@/src/shared/lib/hooks' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { ImageMessagesList } from '@/src/widgets/messages/image-messages-list' +import { ImageMessagesList } from '@/src/widgets/messages/ui/image-messages-list' export async function getServerSideProps(context: any): Promise<{ props: any }> { const deviceType = getTypeDevice(context) @@ -55,7 +55,11 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { const includeParams = useAppSelector((state) => state.params.params) const dispatch = useDispatch() - const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages(showError, modelType, deviceType) + const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages( + showError, + modelType, + deviceType + ) const [chatScrollHeight, setChatScrollHeight] = React.useState(0) const [scrollBottom, setScrollBottom] = React.useState(0) @@ -73,14 +77,19 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { dispatch( setParametres( res.parameters.reduce( - (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), + (a, v) => + v.versions.includes(res.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, {} ) ) ) } else { setVersion('') - dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + dispatch( + setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) } }) .catch((err) => {}) @@ -109,14 +118,19 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { dispatch( setParametres( botParams.parameters.reduce( - (a, v) => (v.versions.includes(botParams.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), + (a, v) => + v.versions.includes(botParams.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, {} ) ) ) } else { setVersion(botParams.slug) - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + dispatch( + setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) } } } @@ -128,13 +142,16 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { dispatch( setParametres( botParams.parameters.reduce( - (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), + (a, v) => + v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }, {} ) ) ) } else { - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + dispatch( + setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) } } } @@ -189,7 +206,11 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { }, [isComplete]) const handleMobileScroll = () => { - setScrollBottom(refScrollMobile.current?.scrollHeight - refScrollMobile.current?.scrollTop - refScrollMobile.current?.clientHeight) + setScrollBottom( + refScrollMobile.current?.scrollHeight - + refScrollMobile.current?.scrollTop - + refScrollMobile.current?.clientHeight + ) if (refScrollMobile.current && messages?.length !== 0) { const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current @@ -247,7 +268,11 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { justifyContent='space-between' alignItems='start' flexDirection={desktop ? 'row' : 'column-reverse'} - sx={{ marginBottom: desktop ? 0 : 3, width: desktop ? '97%' : '100%', marginTop: desktop ? 3 : '15px' }} + sx={{ + marginBottom: desktop ? 0 : 3, + width: desktop ? '97%' : '100%', + marginTop: desktop ? 3 : '15px', + }} > = ({ deviceType, deviceOs }) => { )} - + getMessagesPagination(deviceType)} + isComplete={isComplete} + device={deviceType} + images={messages} + /> ) : ( @@ -319,7 +349,12 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { className={'smallScroll'} onScroll={handleMobileScroll} > - + getMessagesPagination(deviceType)} + isComplete={isComplete} + device={deviceType} + images={messages} + /> {botParams && ( @@ -342,10 +377,21 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { )} {desktop && ( - + {botParams?.versions && botParams.versions.length !== 0 ? ( <> - + ВЕРСИИ = ({ deviceType, deviceOs }) => { setParams(!params) }} > - + ПАРАМЕТРЫ = ({ deviceType, deviceOs }) => { {botParams && botParams.parameters?.length > 0 ? ( - + ) : ( - Параметры отсутствуют + + Параметры отсутствуют + )} )} @@ -438,10 +497,16 @@ const Images: React.FC = ({ deviceType, deviceOs }) => { ПАРАМЕТРЫ - + ) : ( - Параметры отсутствуют + + Параметры отсутствуют + )} @@ -31,6 +31,7 @@ export default function Document() {
+ @@ -236,7 +236,7 @@ export function useModelImages(showError: (message: string) => void, type: st const { getData, sendData } = ModelsWithImagesEndpoints - const [messages, setMessages] = useState(null) + const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) @@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; + +type ClientOnlyProps = { children: JSX.Element }; +const ClientOnly = (props: ClientOnlyProps) => { + const { children } = props; + + return children; +}; + +export default dynamic(() => Promise.resolve(ClientOnly), { + ssr: false, +}); @@ -0,0 +1 @@ +export { default as ClientOnly } from './client-only' @@ -0,0 +1 @@ +export * from './ui' @@ -1,2 +1,3 @@ export { getAccessToken } from './get-token' export { getTypeDevice } from './get-type-device' +export * from './string' @@ -0,0 +1,3 @@ +export const c = (...classes: string[]) => { + return classes.filter(Boolean).join(' ') +} @@ -3,10 +3,11 @@ import { Tooltip } from '@mui/material' import { useAppSelector } from '@/src/main/store/store' -import styles from './styles.module.scss' interface Props { title?: string children: React.ReactNode + className?: string + maxWidth?: string placement?: | 'bottom-end' | 'bottom-start' @@ -21,11 +22,17 @@ interface Props { | 'top-start' | 'top' } -export const TooltipCustom: React.FC = ({ children, title, placement }) => { +export const TooltipCustom: React.FC = ({ children, title, placement, className, maxWidth }) => { const theme = useAppSelector((state) => state.theme.theme) return ( e.stopPropagation()} + PopperProps={{ + onClick(e) { + e.stopPropagation() + }, + }} componentsProps={{ tooltip: { sx: { @@ -33,6 +40,7 @@ export const TooltipCustom: React.FC = ({ children, title, placement }) = color: '#7F7DF3', fontSize: 15, borderRadius: '10px', + maxWidth: maxWidth || 'auto', padding: '10px 13px', '& .MuiTooltip-arrow': { color: theme === 'light' ? '#E8E8FA' : '#4B4B4B', @@ -41,6 +49,7 @@ export const TooltipCustom: React.FC = ({ children, title, placement }) = }, }, }} + className={className} title={title} arrow placement={placement} @@ -8,8 +8,10 @@ export { ButtonUI } from './ui/button/button' export { ButtonGray } from './ui/button/button-gray' export { CheckBoxAgreeWithRules } from './ui/check-box-agree-with-rules' export { DalleFilterMenu } from './ui/dalle-filter-menu' +export * from './client-only' export * from './ui/drawer' export { Error } from './ui/error' +export * from './lib/helpers' export * from './ui/graphics-main-page' export { Input, InputStyleDark, InputStyleLight } from './ui/input' export { InputImagesModels } from './ui/input-images-models/input-images-models' @@ -1,9 +1,9 @@ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' import Stack from '@mui/material/Stack' import Image from 'next/image' -import FullScreenModal from '@/src/features/image-modal/full-screen-modal' +import { ImageModal } from '@/src/features/image-modal' import { useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' import { Message } from '@/src/shared/lib/types/model' @@ -18,18 +18,19 @@ interface IMessagesList { device: 'mobile' | 'desktop' modelType: string isNewMessage: boolean + setCurrentSrc: (value: string) => void message: Message + setModal: (value: boolean) => void deleteMessage?: (message_uid: string) => void modelTitle: string | undefined setResendValue: (value: string) => void onLoadImage?: (event: React.ChangeEvent | null, file?: File) => void } -export const UserMessage = React.memo(function UserMessage(props: IMessagesList) { +export const UserMessage = React.memo(function UserMessage({ setModal, setCurrentSrc, ...props }: IMessagesList) { const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const desktop = props.device === 'desktop' - const [modal, setModal] = useState(false) const [loaded, setLoaded] = useState(false) const copy = (text: string) => { @@ -64,11 +65,15 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) return ( <> - {props.message.file && } {!props.message.from_model ? ( - // - + { + if (!props.message.file) return + setCurrentSrc(props.message.file.toString()) setModal(true) }} onLoadingComplete={() => setLoaded(true)} @@ -129,7 +136,8 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) {!loaded && ( - + @@ -198,12 +212,18 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={handleClose} sx={{ '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', color: '#8280FF', borderRadius: '15px', }, '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', borderRadius: '15px', }, }} @@ -262,7 +282,8 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '5px', }} onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) + if (props.deleteMessage) + props.deleteMessage(props.message.uid) }} > 30 ? 'pre-wrap' : 'pre', + whiteSpace: + props.message.content.length > 30 + ? 'pre-wrap' + : 'pre', }} > {/**/} @@ -360,7 +384,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) stroke='#A4AAB5' strokeWidth='2' /> - + @@ -373,12 +403,14 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={handleClose} sx={{ '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', color: '#8280FF', borderRadius: '15px', }, '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', borderRadius: '15px', }, }} @@ -437,7 +469,8 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '5px', }} onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) + if (props.deleteMessage) + props.deleteMessage(props.message.uid) }} > 30 ? 'pre-wrap' : 'pre', + whiteSpace: + props.message.content.length > 30 ? 'pre-wrap' : 'pre', }} > {/**/} @@ -493,7 +527,7 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) )} - + ) : ( ('') + const { error, showError, isError } = useShowData() + + const toggleMenu = (uid: string) => { + if (iconsMenu === uid) { + setIconsMenu('') + } else { + setIconsMenu(uid) + } + } + + const downloadFile = (url: string | null, content: string | undefined) => { + if (url) { + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + // const image_name = '123' + const image_name = content?.replaceAll(' ', '_').substring(0, 25) + link.setAttribute('download', `${image_name}`) + document.body.appendChild(link) + link.click() + }) + .catch((error) => { + showError('Что-то пошло не так', true) + }) + } else { + showError('Изображение не найдено', true) + } + } + + return { iconsMenu, toggleMenu, downloadFile } +} @@ -0,0 +1,26 @@ +import { Message } from '@/src/shared/lib/types/model' +import { useMemo, useState } from 'react' + +export const useMessages = (messages: Message[]) => { + const [chosenImage, setChosenImage] = useState(null) + + const [loaded, setLoaded] = useState(false) + + // типизация ну супер кривая))) + const computedLibraryImages = useMemo(() => { + return messages + .map((el) => { + if (el.file && (el.file as any).includes('.zip')) return null + return el + }) + .filter((el) => el !== null) + }, [messages]) + + return { + chosenImage, + setChosenImage, + loaded, + setLoaded, + computedLibraryImages, + } +} @@ -1,11 +1,14 @@ -import React, { memo, useEffect } from 'react' +import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import { Box, CircularProgress } from '@mui/material' import { useSession } from 'next-auth/react' import { Message } from '@/src/shared/lib/types/model' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { IsNextDay } from '@/src/widgets/messages/is-next-day' +import { IsNextDay } from '@/src/widgets/messages/ui/is-next-day' import { PreviewView } from '@/src/widgets/messages/message-components/preview-view' +import { ImageModal } from '@/src/features/image-modal' +import { ClientOnly } from '@/src/shared' +import { createPortal } from 'react-dom' interface IMessagesList { device: 'mobile' | 'desktop' @@ -22,7 +25,18 @@ interface IMessagesList { } export const ChatMessagesList: React.FC = memo( - ({ messageResponse, onLoadImage, setResendValue, modelTitle, device, modelType, mode, getMessagesPagination, deleteMessage, loading }) => { + ({ + messageResponse, + onLoadImage, + setResendValue, + modelTitle, + device, + modelType, + mode, + getMessagesPagination, + deleteMessage, + loading, + }) => { const paginationScroll = React.useRef() const [isPaginating, setIsPaginating] = React.useState(false) const [chatScrollHeight, setChatScrollHeight] = React.useState(0) @@ -30,13 +44,25 @@ export const ChatMessagesList: React.FC = memo( const desktop = device === 'desktop' const { status } = useSession() - const [isNewMessage, setIsNewMessage] = React.useState(false) + const [modal, setModal] = useState(false) + const [isNewMessage, setIsNewMessage] = useState(false) + + const [currentSrc, setCurrentSrc] = useState(null) + + const onlyImageMessage = useMemo(() => { + if (!messageResponse) return [] + console.log(messageResponse) + return messageResponse + .map((item) => item) + .filter((item) => item.file !== null && !(item.file as string).includes('.zip')) as Message[] + }, [messageResponse]) React.useEffect(() => { const block = paginationScroll.current if (messageResponse != undefined && !isPaginating) { setChatScrollHeight(paginationScroll.current.scrollHeight) + const time = setTimeout(() => { if (block) { //@ts-ignore @@ -62,8 +88,11 @@ export const ChatMessagesList: React.FC = memo( }, [messageResponse]) const handleScroll = () => { - console.log(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) - setScrollBottom(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) + setScrollBottom( + paginationScroll.current?.scrollHeight - + paginationScroll.current?.scrollTop - + paginationScroll.current?.clientHeight + ) if (paginationScroll.current && messageResponse?.length !== 0) { const { scrollTop, scrollHeight, clientHeight } = paginationScroll.current @@ -77,89 +106,108 @@ export const ChatMessagesList: React.FC = memo( } return ( - - {scrollBottom > 500 && ( - { - //@ts-ignore - const block = paginationScroll.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - - - )} - - {loading && ( - - + + {createPortal( + { + getMessagesPagination && (await getMessagesPagination()) + }} + images={onlyImageMessage} + />, + document.getElementById('modal-container')! + )} + + + + {scrollBottom > 500 && ( + - - )} - - {messageResponse?.length === 0 && status === 'authenticated' ? ( - <>{desktop && modelType !== 'deepl' && } - ) : ( - messageResponse?.map((message, idx) => { - return ( - { + //@ts-ignore + const block = paginationScroll.current + + block.scrollTo({ + top: block.scrollHeight, + behavior: 'smooth', // добавляем плавную прокрутку + }) + }} + > + + + )} + + {loading && ( + + - ) - }) - )} - + + )} + + {messageResponse?.length === 0 && status === 'authenticated' ? ( + <>{desktop && modelType !== 'deepl' && } + ) : ( + messageResponse?.map((message, idx) => { + return ( + + ) + }) + )} + + ) } ) @@ -0,0 +1,105 @@ +import { useAppSelector } from '@/src/main/store/store' +import { TooltipCustom, useShowData } from '@/src/shared' +import { Grow, Box } from '@mui/material' +import { useImageIcons } from '../model' + +export interface ImageIconsProps { + uid: string + url: string | null + content?: string +} + +export const ImageIcons = ({ uid, url, content }: ImageIconsProps) => { + const theme = useAppSelector((state) => state.theme.theme) + + const { toggleMenu, downloadFile, iconsMenu } = useImageIcons() + + return ( + <> + { + e.stopPropagation() + toggleMenu(uid) + }} + width='35' + height='35' + viewBox='0 0 35 35' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + + + + { + window.open(`${url ? url : ''}`, '_blank') + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + { + downloadFile(url, content) + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + + + ) +} @@ -4,143 +4,48 @@ import Box from '@mui/material/Box' import Image from 'next/image' import Link from 'next/link' -import FullScreenModal from '@/src/features/image-modal/full-screen-modal' +import { ImageModal } from '@/src/features/image-modal' import { useAppSelector } from '@/src/main/store/store' -import { Success, TooltipCustom, useShowData } from '@/src/shared' +import { ClientOnly, Success, TooltipCustom, useShowData } from '@/src/shared' import { useAutoScroll } from '@/src/shared/lib/hooks' import { Message } from '@/src/shared/lib/types/model' import styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { ImageIcons } from './image-icons' interface MessagesList { device: 'mobile' | 'desktop' - images: Message[] | null + images: Message[] + getMessagesPagination?: () => Promise isComplete: boolean } -export const ImageMessagesList: React.FC = memo(({ device, images }) => { +export const ImageMessagesList: React.FC = memo(({ device, images, getMessagesPagination }) => { const { error, showError, isError } = useShowData() const [modal, setModal] = useState(false) - const [chosenImage, setChosenImage] = useState('') const theme = useAppSelector((state) => state.theme.theme) - const [iconsMenu, setIconsMenu] = React.useState('') - const [loaded, setLoaded] = useState(false) + - const downloadFile = (url: string | null, content: string | undefined) => { - if (url) { - fetch(url) - .then((response) => response.blob()) - .then((blob) => { - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - // const image_name = '123' - const image_name = content?.replaceAll(' ', '_').substring(0, 25) - link.setAttribute('download', `${image_name}`) - document.body.appendChild(link) - link.click() - }) - .catch((error) => { - showError('Что-то пошло не так', true) - }) - } else { - showError('Изображение не найдено', true) - } - } - - const toggleMenu = (uid: string) => { - if (iconsMenu === uid) { - setIconsMenu('') - } else { - setIconsMenu(uid) - } - } - - const ImageIcons = ({ uid, url, content }: { uid: string; url: string | null; content: string | undefined }) => { - return ( - <> - toggleMenu(uid)} - width='35' - height='35' - viewBox='0 0 35 35' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - - { - window.open(`${url ? url : ''}`, '_blank') - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - { - downloadFile(url, content) - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - ) - } + const { chosenImage, setChosenImage, loaded, setLoaded, computedLibraryImages } = useMessages(images) return ( <> - + + {createPortal( + getMessagesPagination && getMessagesPagination()} + setModal={setModal} + reverse={device === 'desktop'} + current={chosenImage} + />, + document.getElementById('modal-container')! + )} + + = memo(({ device, images }} width={500} height={500} - src={(message.file as unknown as string) || ''} - alt={'К сожалению, изображение не загрузилось'} + src={ + (message.file as unknown as string) || + '' + } + alt={ + 'К сожалению, изображение не загрузилось' + } /> = memo(({ device, images }} width={10} height={10} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='' /> @@ -267,7 +180,10 @@ export const ImageMessagesList: React.FC = memo(({ device, images userSelect: 'none', objectFit: 'contain', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> = memo(({ device, images right: 0, filter: 'blur(10px) brightness(0.7)', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> @@ -295,14 +214,19 @@ export const ImageMessagesList: React.FC = memo(({ device, images {!loaded && ( @@ -331,7 +255,9 @@ export const ImageMessagesList: React.FC = memo(({ device, images {!loaded ? '' : message.content.length > 0 - ? message?.content.replaceAll('"', '').slice(0, 30) + ? message?.content + .replaceAll('"', '') + .slice(0, 30) : 'описание отсутствует'} {message?.content.length > 30 && loaded && '...'} @@ -0,0 +1 @@ +export * from './image-icons' \ No newline at end of file @@ -1,4 +1,4 @@ -import React, { memo } from 'react' +import React, { memo, useMemo } from 'react' import { Box, Typography } from '@mui/material' import { Message } from '@/src/shared/lib/types/model' @@ -12,8 +12,9 @@ interface IProps { messageResponse: Message[] | null modelTitle: string | undefined deleteMessage?: (message_uid: string) => void + setCurrentSrc: (value: string) => void isNewMessage: boolean - // clickPreview: (text: string) => void + setModal: (value: boolean) => void modelType: string device: 'mobile' | 'desktop' setResendValue: (value: string) => void @@ -21,7 +22,20 @@ interface IProps { } export const IsNextDay = memo( - ({ message, index, onLoadImage, messageResponse, modelTitle, deleteMessage, isNewMessage, modelType, device, setResendValue }: IProps) => { + ({ + message, + index, + onLoadImage, + messageResponse, + modelTitle, + deleteMessage, + setCurrentSrc, + setModal, + isNewMessage, + modelType, + device, + setResendValue, + }: IProps) => { if (messageResponse && message.created_at) { const date = getDateFromString(message.created_at) const prevDate = getDateFromString(messageResponse[index]?.created_at) @@ -51,7 +65,9 @@ export const IsNextDay = memo( setResendValue={setResendValue} modelTitle={modelTitle} deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} key={message.uid} + setModal={setModal} message={message} isNewMessage={isNewMessage} modelType={modelType} @@ -81,9 +97,11 @@ export const IsNextDay = memo( = memo(({ device, token, favori justifyContent='space-between' alignItems='center' sx={{ - backgroundColor: desktop ? (theme === 'light' ? '#F8F8F8' : '#4B4B4B') : theme === 'light' ? '#F8F8F8' : 'transparent', + backgroundColor: desktop + ? theme === 'light' + ? '#F8F8F8' + : '#4B4B4B' + : theme === 'light' + ? '#F8F8F8' + : 'transparent', borderRadius: desktop ? '15px' : '15px 15px', marginTop: desktop ? 1 : 0, marginBottom: desktop ? 2 : 1, padding: desktop ? '8px 5px' : '4px 0', - boxShadow: desktop ? 'none' : 'rgba(17, 17, 26, 0.05) 0px 4px 16px, rgba(17, 17, 26, 0.05) 0px 8px 32px;', + boxShadow: desktop + ? 'none' + : 'rgba(17, 17, 26, 0.05) 0px 4px 16px, rgba(17, 17, 26, 0.05) 0px 8px 32px;', }} > - {desktop && } + {desktop && ( + + )} = memo(({ device, token, favori > {title} - {!desktop && } + {!desktop && } {desktop ? ( ) : ( - + )} @@ -1,12 +1,10 @@ { - "useTabs": true, - "tabWidth": 5, - "singleQuote": true, - "trailingComma": "es5", - "jsxBracketSameLine": false, - "semi": false, - "printWidth": 150, - "jsxSingleQuote": true + "useTabs": true, + "tabWidth": 5, + "singleQuote": true, + "trailingComma": "es5", + "jsxBracketSameLine": false, + "semi": false, + "printWidth": 120, + "jsxSingleQuote": true } - - @@ -62,6 +62,7 @@ "remark-gfm": "^3.0.1", "sass": "^1.63.4", "styled-components": "^5.3.9", + "swiper": "^11.2.1", "typescript": "5.1.3" }, "devDependencies": { @@ -10063,6 +10064,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swiper": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.1.tgz", + "integrity": "sha512-62G69+iQRIfUqTmJkWpZDcX891Ra8O9050ckt1/JI2H+0483g+gq0m7gINecDqMtDh2zt5dK+uzBRxGhGOOvQA==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -79,6 +79,7 @@ "remark-gfm": "^3.0.1", "sass": "^1.63.4", "styled-components": "^5.3.9", + "swiper": "^11.2.1", "typescript": "5.1.3" }, "devDependencies": {