Files
5chan/src/components/reply-quote-preview/reply-quote-preview.tsx
T

262 lines
8.3 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
import { Comment, useAccount } from '@plebbit/plebbit-react-hooks';
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
import useIsMobile from '../../hooks/use-is-mobile';
import styles from '../../views/post/post.module.css';
import { Post } from '../../views/post';
interface ReplyQuotePreviewProps {
isBacklinkReply?: boolean;
backlinkReply?: Comment;
isQuotelinkReply?: boolean;
quotelinkReply?: Comment;
}
const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
const targetElements = document.querySelectorAll(`[data-cid="${cid}"]`);
if (targetElements.length === 0) return;
const isInViewport = (element: HTMLElement) => {
const bounding = element.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
let anyInView = false;
targetElements.forEach((element) => {
const htmlElement = element as HTMLElement;
if (isInViewport(htmlElement)) {
htmlElement.classList.add('highlight');
anyInView = true;
} else {
htmlElement.classList.remove('highlight');
}
});
if (!anyInView) {
onElementOutOfView();
}
};
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
const placementRef = useRef<Placement>('right');
const availableWidthRef = useRef<number>(0);
const { refs, floatingStyles, update } = useFloating({
placement: placementRef.current,
middleware: [
shift({ padding: 10 }),
offset({ mainAxis: placementRef.current === 'right' ? 8 : 4 }),
size({
apply({ availableWidth, elements }) {
availableWidthRef.current = availableWidth;
if (availableWidth >= 250) {
elements.floating.style.maxWidth = `${availableWidth - 12}px`;
} else if (placementRef.current === 'right') {
placementRef.current = 'left';
}
},
}),
],
whileElementsMounted: autoUpdate,
});
useEffect(() => {
const handleResize = () => {
const availableWidth = availableWidthRef.current;
if (availableWidth >= 250) {
placementRef.current = 'right';
} else {
placementRef.current = 'left';
}
update();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [update]);
const handleMouseOver = (cid: string | undefined) => {
if (!cid) return;
handleQuoteHover(cid, () => setOutOfViewCid(cid));
setHoveredCid(cid);
};
const handleMouseLeave = (cid: string | null) => {
if (cid) {
const targetElements = document.querySelectorAll(`[data-cid="${cid}"]`);
targetElements.forEach((element) => {
element.classList.remove('highlight');
});
}
setHoveredCid(null);
setOutOfViewCid(null);
};
const replyBacklink = (
<>
<Link
className={styles.backlink}
to={`/p/${backlinkReply?.subplebbitAddress}/c/${backlinkReply?.cid}`}
ref={refs.setReference}
onMouseOver={() => handleMouseOver(backlinkReply?.cid)}
onMouseLeave={() => handleMouseLeave(backlinkReply?.cid)}
>
c/{backlinkReply?.shortCid}
</Link>
{hoveredCid === backlinkReply?.cid &&
outOfViewCid === backlinkReply?.cid &&
createPortal(
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
<Post post={backlinkReply} showReplies={false} />
</div>,
document.body,
)}
</>
);
const account = useAccount();
const replyQuotelink = (
<>
<Link
to={`/p/${quotelinkReply?.subplebbitAddress}/c/${quotelinkReply?.cid}`}
ref={refs.setReference}
className={styles.quoteLink}
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
>
{`c/${quotelinkReply?.shortCid}`}
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
</Link>
<br />
{hoveredCid === quotelinkReply?.cid &&
outOfViewCid === quotelinkReply?.cid &&
createPortal(
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
<Post post={quotelinkReply} showReplies={false} />
</div>,
document.body,
)}
</>
);
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
};
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
const { refs, floatingStyles, update } = useFloating({
placement: 'bottom',
middleware: [shift({ padding: isBacklinkReply ? 5 : 10 })],
whileElementsMounted: autoUpdate,
});
useEffect(() => {
window.addEventListener('resize', () => update());
return () => {
window.removeEventListener('resize', () => update());
};
}, [update]);
const handleMouseOver = (cid: string | undefined) => {
if (!cid) return;
handleQuoteHover(cid, () => setOutOfViewCid(cid));
setHoveredCid(cid);
};
const handleMouseLeave = (cid: string | null) => {
if (cid) {
const targetElements = document.querySelectorAll(`[data-cid="${cid}"]`);
targetElements.forEach((element) => {
element.classList.remove('highlight');
});
}
setHoveredCid(null);
setOutOfViewCid(null);
};
const replyBacklink = (
<>
<span
className={styles.backlink}
ref={refs.setReference}
onMouseOver={() => handleMouseOver(backlinkReply?.cid)}
onMouseLeave={() => handleMouseLeave(backlinkReply?.cid)}
>
c/{backlinkReply?.shortCid}{' '}
</span>
<Link to={`/p/${backlinkReply?.subplebbitAddress}/c/${backlinkReply?.cid}`} className={styles.backlinkHash}>
#
</Link>
{hoveredCid === backlinkReply?.cid &&
outOfViewCid === backlinkReply?.cid &&
createPortal(
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
<Post post={backlinkReply} showReplies={false} />
</div>,
document.body,
)}
</>
);
const account = useAccount();
const replyQuotelink = (
<>
<span
ref={refs.setReference}
className={styles.quoteLink}
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
>
c/{quotelinkReply?.shortCid}
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
</span>
<Link className={styles.quoteLink} to={`/p/${quotelinkReply?.subplebbitAddress}/c/${quotelinkReply?.cid}`}>
{' '}
#
</Link>
<br />
{hoveredCid === quotelinkReply?.cid &&
outOfViewCid === quotelinkReply?.cid &&
createPortal(
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
<Post post={quotelinkReply} showReplies={false} />
</div>,
document.body,
)}
</>
);
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
};
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const isMobile = useIsMobile();
return isMobile ? (
<MobileQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
) : (
<DesktopQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
);
};
export default ReplyQuotePreview;