mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
262 lines
8.3 KiB
TypeScript
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;
|