2023-03-07 17:38:06 +03:00
|
|
|
import { Menu, MenuItem } 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';
|
|
|
|
|
|
|
|
import Icon from './icon';
|
|
|
|
import Link from './link';
|
|
|
|
import Media from './media';
|
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';
|
2023-03-07 17:38:06 +03:00
|
|
|
import TranslationBlock from './translation-block';
|
2023-01-29 10:23:53 +03:00
|
|
|
|
|
|
|
function MediaModal({
|
|
|
|
mediaAttachments,
|
|
|
|
statusID,
|
2023-02-05 19:17:19 +03:00
|
|
|
instance,
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useHotkeys('esc', onClose, [onClose]);
|
|
|
|
|
|
|
|
const [showMediaAlt, setShowMediaAlt] = useState(false);
|
|
|
|
|
|
|
|
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;
|
|
|
|
const rgbAverageColor = blurhash
|
|
|
|
? getBlurHashAverageColor(blurhash)
|
|
|
|
: null;
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
class="carousel-item"
|
|
|
|
style={{
|
|
|
|
'--average-color': `rgb(${rgbAverageColor?.join(',')})`,
|
|
|
|
'--average-color-alpha': `rgba(${rgbAverageColor?.join(
|
|
|
|
',',
|
|
|
|
)}, .5)`,
|
|
|
|
}}
|
|
|
|
tabindex="0"
|
|
|
|
key={media.id}
|
|
|
|
ref={i === currentIndex ? carouselFocusItem : null}
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target !== e.currentTarget) {
|
|
|
|
setShowControls(!showControls);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{!!media.description && (
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="plain2 media-alt"
|
|
|
|
hidden={!showControls}
|
|
|
|
onClick={() => {
|
|
|
|
setShowMediaAlt(media.description);
|
|
|
|
}}
|
|
|
|
>
|
2023-02-11 17:36:19 +03:00
|
|
|
<Icon icon="info" />
|
2023-01-29 10:23:53 +03:00
|
|
|
<span class="media-alt-desc">{media.description}</span>
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
<Media media={media} showOriginal />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div class="carousel-top-controls" hidden={!showControls}>
|
|
|
|
<span>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="carousel-button plain3"
|
|
|
|
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-02-28 16:02:55 +03:00
|
|
|
class={`plain3 carousel-dot ${
|
2023-01-29 10:23:53 +03:00
|
|
|
i === currentIndex ? 'active' : ''
|
|
|
|
}`}
|
|
|
|
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
|
|
|
}}
|
|
|
|
>
|
|
|
|
•
|
|
|
|
</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={
|
|
|
|
<button type="button" class="carousel-button plain3">
|
|
|
|
<Icon icon="more" alt="More" />
|
|
|
|
</button>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<MenuLink
|
|
|
|
href={
|
|
|
|
mediaAttachments[currentIndex]?.remoteUrl ||
|
|
|
|
mediaAttachments[currentIndex]?.url
|
|
|
|
}
|
|
|
|
class="carousel-button plain3"
|
|
|
|
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-02-18 12:38:42 +03:00
|
|
|
class="button carousel-button media-post-link plain3"
|
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"
|
|
|
|
class="carousel-button plain3"
|
|
|
|
hidden={currentIndex === 0}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
carouselRef.current.scrollTo({
|
|
|
|
left: carouselRef.current.clientWidth * (currentIndex - 1),
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
2023-08-01 14:43:52 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="arrow-left" />
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="carousel-button plain3"
|
|
|
|
hidden={currentIndex === mediaAttachments.length - 1}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
carouselRef.current.scrollTo({
|
|
|
|
left: carouselRef.current.clientWidth * (currentIndex + 1),
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
2023-08-01 14:43:52 +03:00
|
|
|
carouselRef.current.focus();
|
2023-01-29 10:23:53 +03:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
<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
|
|
|
|
alt={showMediaAlt}
|
|
|
|
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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-20 11:10:57 +03:00
|
|
|
function MediaAltModal({ alt, onClose }) {
|
2023-03-07 17:38:06 +03:00
|
|
|
const [forceTranslate, setForceTranslate] = useState(false);
|
|
|
|
return (
|
|
|
|
<div class="sheet">
|
2023-04-20 11:10:57 +03:00
|
|
|
{!!onClose && (
|
|
|
|
<button type="button" class="sheet-close outer" onClick={onClose}>
|
|
|
|
<Icon icon="x" />
|
|
|
|
</button>
|
|
|
|
)}
|
2023-03-07 17:38:06 +03:00
|
|
|
<header class="header-grid">
|
|
|
|
<h2>Media description</h2>
|
|
|
|
<div class="header-side">
|
|
|
|
<Menu
|
|
|
|
align="end"
|
|
|
|
menuButton={
|
|
|
|
<button type="button" class="plain4">
|
|
|
|
<Icon icon="more" alt="More" size="xl" />
|
|
|
|
</button>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<MenuItem
|
|
|
|
disabled={forceTranslate}
|
|
|
|
onClick={() => {
|
|
|
|
setForceTranslate(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="translate" />
|
|
|
|
<span>Translate</span>
|
|
|
|
</MenuItem>
|
|
|
|
</Menu>
|
|
|
|
</div>
|
|
|
|
</header>
|
|
|
|
<main>
|
|
|
|
<p
|
|
|
|
style={{
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{alt}
|
|
|
|
</p>
|
|
|
|
{forceTranslate && (
|
|
|
|
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
|
|
|
|
)}
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-29 10:23:53 +03:00
|
|
|
export default MediaModal;
|