2023-09-28 10:48:32 +03:00
|
|
|
import { Menu } from '@szhsin/react-menu';
|
2023-01-29 10:23:53 +03:00
|
|
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
|
|
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
|
|
|
2023-10-25 14:19:07 +03:00
|
|
|
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
|
|
|
|
2023-01-29 10:23:53 +03:00
|
|
|
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';
|
2023-01-29 10:23:53 +03:00
|
|
|
import Modal from './modal';
|
|
|
|
|
|
|
|
function MediaModal({
|
|
|
|
mediaAttachments,
|
|
|
|
statusID,
|
2023-02-05 19:17:19 +03:00
|
|
|
instance,
|
2023-09-30 18:23:34 +03:00
|
|
|
lang,
|
2023-01-29 10:23:53 +03:00
|
|
|
index = 0,
|
|
|
|
onClose = () => {},
|
|
|
|
}) {
|
|
|
|
const carouselRef = useRef(null);
|
|
|
|
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(index);
|
|
|
|
const carouselFocusItem = useRef(null);
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
carouselFocusItem.current?.scrollIntoView();
|
2023-03-23 10:54:17 +03:00
|
|
|
|
2023-04-14 10:30:04 +03:00
|
|
|
// history.pushState({ mediaModal: true }, '');
|
|
|
|
// const handlePopState = (e) => {
|
|
|
|
// if (e.state?.mediaModal) {
|
|
|
|
// onClose();
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
// window.addEventListener('popstate', handlePopState);
|
|
|
|
// return () => {
|
|
|
|
// window.removeEventListener('popstate', handlePopState);
|
|
|
|
// };
|
2023-01-29 10:23:53 +03:00
|
|
|
}, []);
|
2023-01-30 14:48:33 +03:00
|
|
|
const prevStatusID = useRef(statusID);
|
2023-01-29 10:23:53 +03:00
|
|
|
useEffect(() => {
|
|
|
|
const scrollLeft = index * carouselRef.current.clientWidth;
|
2023-01-30 14:48:33 +03:00
|
|
|
const differentStatusID = prevStatusID.current !== statusID;
|
|
|
|
if (differentStatusID) prevStatusID.current = statusID;
|
2023-01-29 10:23:53 +03:00
|
|
|
carouselRef.current.scrollTo({
|
|
|
|
left: scrollLeft,
|
2023-01-30 14:48:33 +03:00
|
|
|
behavior: differentStatusID ? 'auto' : 'smooth',
|
2023-01-29 10:23:53 +03:00
|
|
|
});
|
2023-08-10 16:58:11 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-30 14:48:33 +03:00
|
|
|
}, [index, statusID]);
|
2023-01-29 10:23:53 +03:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
2023-10-25 21:48:36 +03:00
|
|
|
useHotkeys(
|
|
|
|
'esc',
|
|
|
|
onClose,
|
|
|
|
{
|
|
|
|
ignoreEventWhen: (e) => {
|
|
|
|
const hasModal = !!document.querySelector('#modal-container > *');
|
|
|
|
return hasModal;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[onClose],
|
|
|
|
);
|
2023-01-29 10:23:53 +03:00
|
|
|
|
|
|
|
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);
|
|
|
|
}, []);
|
|
|
|
|
2023-01-29 10:23:53 +03:00
|
|
|
return (
|
2023-04-14 10:30:04 +03:00
|
|
|
<div class="media-modal-container">
|
2023-01-29 10:23:53 +03:00
|
|
|
<div
|
|
|
|
ref={carouselRef}
|
2023-08-10 16:58:11 +03:00
|
|
|
tabIndex="0"
|
2023-01-29 10:23:53 +03:00
|
|
|
data-swipe-threshold="44"
|
|
|
|
class="carousel"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (
|
|
|
|
e.target.classList.contains('carousel-item') ||
|
2023-03-27 19:29:01 +03:00
|
|
|
e.target.classList.contains('media') ||
|
|
|
|
e.target.classList.contains('media-zoom')
|
2023-01-29 10:23:53 +03:00
|
|
|
) {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{mediaAttachments?.map((media, i) => {
|
|
|
|
const { blurhash } = media;
|
2023-10-25 14:19:07 +03:00
|
|
|
let accentColor;
|
|
|
|
if (blurhash) {
|
|
|
|
const averageColor = getBlurHashAverageColor(blurhash);
|
|
|
|
const labAverageColor = rgb2oklab(averageColor);
|
|
|
|
accentColor = oklab2rgb([
|
|
|
|
0.6,
|
|
|
|
labAverageColor[1],
|
|
|
|
labAverageColor[2],
|
|
|
|
]);
|
|
|
|
}
|
2023-01-29 10:23:53 +03:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
class="carousel-item"
|
|
|
|
style={{
|
2023-10-25 14:19:07 +03:00
|
|
|
'--accent-color': `rgb(${accentColor?.join(',')})`,
|
|
|
|
'--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`,
|
2023-01-29 10:23:53 +03:00
|
|
|
}}
|
|
|
|
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"
|
2023-01-29 10:23:53 +03:00
|
|
|
hidden={!showControls}
|
|
|
|
onClick={() => {
|
2023-09-30 18:23:34 +03:00
|
|
|
setShowMediaAlt({
|
|
|
|
alt: media.description,
|
|
|
|
lang,
|
|
|
|
});
|
2023-01-29 10:23:53 +03:00
|
|
|
}}
|
|
|
|
>
|
2023-09-28 18:48:01 +03:00
|
|
|
<span class="alt-badge">ALT</span>
|
2023-09-30 18:23:34 +03:00
|
|
|
<span class="media-alt-desc" lang={lang} dir="auto">
|
|
|
|
{media.description}
|
|
|
|
</span>
|
2023-01-29 10:23:53 +03:00
|
|
|
</button>
|
|
|
|
)}
|
2023-09-30 18:23:34 +03:00
|
|
|
<Media media={media} showOriginal lang={lang} />
|
2023-01-29 10:23:53 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div class="carousel-top-controls" hidden={!showControls}>
|
|
|
|
<span>
|
|
|
|
<button
|
|
|
|
type="button"
|
2023-08-14 15:32:09 +03:00
|
|
|
class="carousel-button"
|
2023-01-29 10:23:53 +03:00
|
|
|
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' : ''}`}
|
2023-01-29 10:23:53 +03:00
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
carouselRef.current.scrollTo({
|
|
|
|
left: carouselRef.current.clientWidth * i,
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
2023-08-01 14:43:52 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
}}
|
|
|
|
>
|
2023-08-10 18:52:29 +03:00
|
|
|
<Icon icon="round" size="s" />
|
2023-01-29 10:23:53 +03:00
|
|
|
</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>{' '}
|
2023-02-18 12:38:42 +03:00
|
|
|
<Link
|
2023-04-14 10:30:04 +03:00
|
|
|
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"
|
2023-06-13 10:32:10 +03:00
|
|
|
// onClick={() => {
|
|
|
|
// // if small screen (not media query min-width 40em + 350px), run onClose
|
|
|
|
// if (
|
|
|
|
// !window.matchMedia('(min-width: calc(40em + 350px))').matches
|
|
|
|
// ) {
|
|
|
|
// onClose();
|
|
|
|
// }
|
|
|
|
// }}
|
2023-02-18 12:38:42 +03:00
|
|
|
>
|
|
|
|
<span class="button-label">See post </span>»
|
2023-03-28 10:59:20 +03:00
|
|
|
</Link>
|
2023-01-29 10:23:53 +03:00
|
|
|
</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"
|
2023-01-29 10:23:53 +03:00
|
|
|
hidden={currentIndex === 0}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2023-08-14 16:56:44 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
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"
|
2023-01-29 10:23:53 +03:00
|
|
|
hidden={currentIndex === mediaAttachments.length - 1}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2023-08-14 16:56:44 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
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);
|
2023-08-01 14:43:52 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2023-04-20 11:10:57 +03:00
|
|
|
<MediaAltModal
|
2023-09-30 18:23:34 +03:00
|
|
|
alt={showMediaAlt.alt || showMediaAlt}
|
|
|
|
lang={showMediaAlt?.lang}
|
2023-04-20 11:10:57 +03:00
|
|
|
onClose={() => setShowMediaAlt(false)}
|
|
|
|
/>
|
2023-01-29 10:23:53 +03:00
|
|
|
</Modal>
|
|
|
|
)}
|
2023-04-14 10:30:04 +03:00
|
|
|
</div>
|
2023-01-29 10:23:53 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default MediaModal;
|