phanpy/src/components/media-modal.jsx

314 lines
9.2 KiB
React
Raw Normal View History

2023-09-28 10:48:32 +03:00
import { Menu } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
import Icon from './icon';
import Link from './link';
import Media from './media';
2023-09-28 10:48:32 +03:00
import MediaAltModal from './media-alt-modal';
2023-03-28 10:59:20 +03:00
import MenuLink from './menu-link';
import Modal from './modal';
function MediaModal({
mediaAttachments,
statusID,
instance,
lang,
index = 0,
onClose = () => {},
}) {
const carouselRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(index);
const carouselFocusItem = useRef(null);
useLayoutEffect(() => {
carouselFocusItem.current?.scrollIntoView();
// history.pushState({ mediaModal: true }, '');
// const handlePopState = (e) => {
// if (e.state?.mediaModal) {
// onClose();
// }
// };
// window.addEventListener('popstate', handlePopState);
// return () => {
// window.removeEventListener('popstate', handlePopState);
// };
}, []);
const prevStatusID = useRef(statusID);
useEffect(() => {
const scrollLeft = index * carouselRef.current.clientWidth;
const differentStatusID = prevStatusID.current !== statusID;
if (differentStatusID) prevStatusID.current = statusID;
carouselRef.current.scrollTo({
left: scrollLeft,
behavior: differentStatusID ? 'auto' : 'smooth',
});
carouselRef.current.focus();
}, [index, statusID]);
const [showControls, setShowControls] = useState(true);
useEffect(() => {
let handleSwipe = () => {
onClose();
};
if (carouselRef.current) {
carouselRef.current.addEventListener('swiped-down', handleSwipe);
}
return () => {
if (carouselRef.current) {
carouselRef.current.removeEventListener('swiped-down', handleSwipe);
}
};
}, []);
useHotkeys(
'esc',
onClose,
{
ignoreEventWhen: (e) => {
const hasModal = !!document.querySelector('#modal-container > *');
return hasModal;
},
},
[onClose],
);
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
carouselRef.current.addEventListener('scroll', handleScroll, {
passive: true,
});
}
return () => {
if (carouselRef.current) {
carouselRef.current.removeEventListener('scroll', handleScroll);
}
};
}, []);
2023-04-17 09:41:40 +03:00
useEffect(() => {
let timer = setTimeout(() => {
carouselRef.current?.focus?.();
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<div class="media-modal-container">
<div
ref={carouselRef}
tabIndex="0"
data-swipe-threshold="44"
class="carousel"
onClick={(e) => {
if (
e.target.classList.contains('carousel-item') ||
e.target.classList.contains('media') ||
e.target.classList.contains('media-zoom')
) {
onClose();
}
}}
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
let accentColor;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);
const labAverageColor = rgb2oklab(averageColor);
accentColor = oklab2rgb([
0.6,
labAverageColor[1],
labAverageColor[2],
]);
}
return (
<div
class="carousel-item"
style={{
'--accent-color': `rgb(${accentColor?.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`,
}}
tabindex="0"
key={media.id}
ref={i === currentIndex ? carouselFocusItem : null}
onClick={(e) => {
if (e.target !== e.currentTarget) {
setShowControls(!showControls);
}
}}
>
{!!media.description && (
<button
type="button"
2023-08-14 15:32:09 +03:00
class="media-alt"
hidden={!showControls}
onClick={() => {
setShowMediaAlt({
alt: media.description,
lang,
});
}}
>
<span class="alt-badge">ALT</span>
<span class="media-alt-desc" lang={lang} dir="auto">
{media.description}
</span>
</button>
)}
<Media media={media} showOriginal lang={lang} />
</div>
);
})}
</div>
<div class="carousel-top-controls" hidden={!showControls}>
<span>
<button
type="button"
2023-08-14 15:32:09 +03:00
class="carousel-button"
onClick={() => onClose()}
>
<Icon icon="x" />
</button>
</span>
{mediaAttachments?.length > 1 ? (
<span class="carousel-dots">
{mediaAttachments?.map((media, i) => (
<button
key={media.id}
type="button"
disabled={i === currentIndex}
2023-08-14 15:32:09 +03:00
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * i,
behavior: 'smooth',
});
carouselRef.current.focus();
}}
>
2023-08-10 18:52:29 +03:00
<Icon icon="round" size="s" />
</button>
))}
</span>
) : (
<span />
)}
<span>
2023-03-28 10:59:20 +03:00
<Menu
overflow="auto"
align="end"
position="anchor"
boundingBoxPadding="8 8 8 8"
2023-06-13 12:46:37 +03:00
gap={4}
2023-03-28 10:59:20 +03:00
menuClassName="glass-menu"
menuButton={
2023-08-14 15:32:09 +03:00
<button type="button" class="carousel-button">
2023-03-28 10:59:20 +03:00
<Icon icon="more" alt="More" />
</button>
}
>
<MenuLink
href={
mediaAttachments[currentIndex]?.remoteUrl ||
mediaAttachments[currentIndex]?.url
}
2023-08-14 15:32:09 +03:00
class="carousel-button"
2023-03-28 10:59:20 +03:00
target="_blank"
title="Open original media in new window"
>
<Icon icon="popout" />
<span>Open original media</span>
</MenuLink>
</Menu>{' '}
<Link
to={`${instance ? `/${instance}` : ''}/s/${statusID}${
window.matchMedia('(min-width: calc(40em + 350px))').matches
? `?media=${currentIndex + 1}`
: ''
}`}
2023-08-14 15:32:09 +03:00
class="button carousel-button media-post-link"
// onClick={() => {
// // if small screen (not media query min-width 40em + 350px), run onClose
// if (
// !window.matchMedia('(min-width: calc(40em + 350px))').matches
// ) {
// onClose();
// }
// }}
>
<span class="button-label">See post </span>&raquo;
2023-03-28 10:59:20 +03:00
</Link>
</span>
</div>
{mediaAttachments?.length > 1 && (
<div class="carousel-controls" hidden={!showControls}>
<button
type="button"
2023-08-14 15:32:09 +03:00
class="carousel-button"
hidden={currentIndex === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-left" />
</button>
<button
type="button"
2023-08-14 15:32:09 +03:00
class="carousel-button"
hidden={currentIndex === mediaAttachments.length - 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex + 1),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-right" />
</button>
</div>
)}
{!!showMediaAlt && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowMediaAlt(false);
carouselRef.current.focus();
}
}}
>
2023-04-20 11:10:57 +03:00
<MediaAltModal
alt={showMediaAlt.alt || showMediaAlt}
lang={showMediaAlt?.lang}
2023-04-20 11:10:57 +03:00
onClose={() => setShowMediaAlt(false)}
/>
</Modal>
)}
</div>
);
}
export default MediaModal;