diff --git a/src/components/styled/views/Catalog.styled.jsx b/src/components/styled/views/Catalog.styled.jsx index ade45065..d8d49b82 100644 --- a/src/components/styled/views/Catalog.styled.jsx +++ b/src/components/styled/views/Catalog.styled.jsx @@ -8,8 +8,7 @@ export const Threads = styled.div` .thread { width: 180px; - position:relative; - position:relative; + position: relative; max-height: 320px; vertical-align: top; display: inline-block; @@ -31,6 +30,7 @@ export const Threads = styled.div` padding-left:5px;padding-right:5px; border-radius:3px; } + .thread_popup_content{ color: #dedede; display: inline-block; @@ -38,6 +38,7 @@ export const Threads = styled.div` white-space:nowrap; font-size:13px; } + .thread_popup_lastReply{ color: #bbbfbd; display: block; @@ -47,34 +48,35 @@ export const Threads = styled.div` text-overflow: ellipsis; font-size:90%; } + .thread_popup_title{ color:#a89baa } + .thread_popup_author{ color:#519860;font-weight:bold } + .thread_popup_authorAdmin{ color:#682071; } + video, audio, img { max-width: 150px; max-height: 150px; - } +} - .file-thumb { +.file-thumb { background-color: rgba(0, 0, 0, 0.05); - display: inline-block; - text-align: center; - line-height: 150px; + display: inline-flex; + justify-content: center; + align-items: center; margin-bottom: 5px; } .card { - display: inline-block; - margin: auto; - box-shadow: 0 0 5px rgba(0, 0, 0, .25); - border: 0; - vertical-align: middle; + box-shadow: 0 0 5px rgba(0, 0, 0, .25); + border: 0; } .thread-icons { @@ -133,9 +135,9 @@ export const Threads = styled.div` .teaser { display: block; padding: 0 15px; - height:200px; - overflow:hidden; + overflow: hidden; } + ${({ selectedStyle }) => { switch (selectedStyle) { case 'Yotsuba': diff --git a/src/components/views/AllCatalog.jsx b/src/components/views/AllCatalog.jsx index 88bd8f1b..da8a0bf6 100755 --- a/src/components/views/AllCatalog.jsx +++ b/src/components/views/AllCatalog.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import { Link, useNavigate} from 'react-router-dom'; @@ -21,6 +21,7 @@ import OfflineIndicator from '../OfflineIndicator'; import SettingsModal from '../modals/SettingsModal'; import countLinks from '../../utils/countLinks'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; +import getFormattedTime from '../../utils/getFormattedTime'; import handleShareClick from '../../utils/handleShareClick'; import handleStyleChange from '../../utils/handleStyleChange'; import useError from '../../hooks/useError'; @@ -75,6 +76,51 @@ const CatalogPost = ({post}) => { const threadMenuRefs = useRef({}); const postMenuRef = useRef(null); const postMenuCatalogRef = useRef(null); + const popupRef = useRef(null); + const threadRefs = useRef([]); + const [isEnoughSpaceOnRight, setIsEnoughSpaceOnRight] = useState(null); + const [isEnoughSpaceOnLeft,setIsEnoughSpaceOnLeft] = useState(null); + const [isCalculationDone,setIsCalculationDone] = useState(false); + const [hoverTimeoutId, setHoverTimeoutId] = useState(null); + + + useLayoutEffect(() => { + const executeLayoutEffectLogic = () => { + let ref; + ref = threadRefs.current[isHoveringOnThread]; + + if (ref && popupRef.current) { + const threadRect = ref.getBoundingClientRect(); + const popupRect = popupRef.current.getBoundingClientRect(); + const viewportWidth = document.documentElement.clientWidth; + const spaceOnRight = viewportWidth - (threadRect.left + threadRect.width); + const spaceOnLeft = threadRect.left; + const popupWidth = popupRect.width; + setIsEnoughSpaceOnLeft(spaceOnLeft >= popupWidth + 10); + setIsEnoughSpaceOnRight(spaceOnRight >= popupWidth + 10); + setIsCalculationDone(true); + } + }; + + if (isHoveringOnThread) { + const timeoutId = setTimeout(executeLayoutEffectLogic, 500); + return () => clearTimeout(timeoutId); + } + + }, [isHoveringOnThread]); + + + const handleMouseOnLeaveThread = () => { + if (hoverTimeoutId) { + clearTimeout(hoverTimeoutId); + setHoverTimeoutId(null); + setIsHoveringOnThread("") + } else if (isHoveringOnThread !== "") { + setIsHoveringOnThread(""); + setIsEnoughSpaceOnRight(null); + setIsCalculationDone(false); + } + }; useEffect(() => { @@ -272,16 +318,51 @@ const CatalogPost = ({post}) => { let scale = Math.min(1, 150 / Math.max(thread.linkWidth, thread.linkHeight)); displayWidth = `${thread.linkWidth * scale}px`; displayHeight = `${thread.linkHeight * scale}px`; - } else { + } else if (thread.link) { displayWidth = '150px'; displayHeight = '150px'; + } else { + displayWidth = '0px'; + displayHeight = '0px'; } return (
{setIsHoveringOnThread(thread.cid)}} - onMouseLeave={() => {setIsHoveringOnThread('')}}> + onMouseLeave={() => {handleMouseOnLeaveThread()}}> + {isHoveringOnThread === thread.cid ? +
handleMouseOnLeaveThread()} + style={{ + opacity: isCalculationDone && isEnoughSpaceOnRight !== null ? 1 : 0, + visibility: isCalculationDone && isEnoughSpaceOnRight !== null ? 'visible' : 'hidden', + left: isCalculationDone && isEnoughSpaceOnRight !== null && isEnoughSpaceOnRight ? '100%' : 'auto', + right: isCalculationDone && isEnoughSpaceOnRight !== null && !isEnoughSpaceOnRight ? '100%' : 'auto',}} + className="thread_popup"> +

+ + {thread.title ? `${thread.title} ` : "Posted "} + + by + + {thread.author.displayName ?` ${thread.author.displayName} ` : " Anonymous "} + + {thread.timestamp ? getFormattedTime(thread.timestamp) : null}

+
+ {thread.replyCount > 0 ? +

+ Last reply by Anonymous + {getFormattedTime(thread.lastReplyTimestamp)}

+ : null}
+
: null } {commentMediaInfo?.url ? ( setSelectedThread(thread.cid)}> @@ -491,7 +572,8 @@ const CatalogPost = ({post}) => { setSelectedThread(thread.cid)}> -
+
(threadRefs.current[thread.cid] = el)}> {thread.title ? `${thread.title}` : null} {thread.content ? `: ${thread.content}` : null}
diff --git a/src/components/views/Catalog.jsx b/src/components/views/Catalog.jsx index f608af8b..52d718a4 100755 --- a/src/components/views/Catalog.jsx +++ b/src/components/views/Catalog.jsx @@ -1,4 +1,4 @@ -import React, { useCallback,useLayoutEffect, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import { Link, useNavigate, useParams } from 'react-router-dom'; @@ -23,6 +23,7 @@ import OfflineIndicator from '../OfflineIndicator'; import VerifiedAuthor from '../VerifiedAuthor'; import countLinks from '../../utils/countLinks'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; +import getFormattedTime from '../../utils/getFormattedTime'; import handleShareClick from '../../utils/handleShareClick'; import handleStyleChange from '../../utils/handleStyleChange'; import useAnonModeRef from '../../hooks/useAnonModeRef'; @@ -88,64 +89,54 @@ const CatalogPost = ({post}) => { const [isEnoughSpaceOnRight, setIsEnoughSpaceOnRight] = useState(null); const [isEnoughSpaceOnLeft,setIsEnoughSpaceOnLeft] = useState(null); const [isCalculationDone,setIsCalculationDone] = useState(false); + const [hoverTimeoutId, setHoverTimeoutId] = useState(null); + const [isHoveringForMenu, setIsHoveringForMenu] = useState(false); + useLayoutEffect(() => { - setIsEnoughSpaceOnRight(null); - setIsEnoughSpaceOnLeft(null); - const calculateSpaceOnRight = () => { - if (thread.cid === isHoveringOnThread){ - if(threadRefs.current[thread.cid] && popupRef.current) { - const threadRect = threadRefs.current[thread.cid].getBoundingClientRect(); - const popupRect = popupRef.current.getBoundingClientRect(); - // Get the width of the viewport area inside the browser window - const viewportWidth = document.documentElement.clientWidth; + const executeLayoutEffectLogic = () => { + let ref; + if (isHoveringOnThread === 'rules' || isHoveringOnThread === 'description') { + ref = popupRef.current; + } else { + ref = threadRefs.current[isHoveringOnThread]; + } - // Calculate the space on the right side (relative to the viewport) - const spaceOnRight = viewportWidth - (threadRect.left + threadRect.width); - const spaceOnLeft = threadRect.left; - // Define the popup width - const popupWidth = popupRect.width; - // Check if there is enough space for the popup on the right side - const EnoughSpaceOnRight = spaceOnRight >= popupWidth + 10; - const EnoughSpaceOnLeft = spaceOnLeft >= popupWidth + 10; - setIsEnoughSpaceOnLeft(EnoughSpaceOnLeft); - setIsEnoughSpaceOnRight(EnoughSpaceOnRight); - return setIsCalculationDone(true); - }} + if (ref && popupRef.current) { + const threadRect = ref.getBoundingClientRect(); + const popupRect = popupRef.current.getBoundingClientRect(); + const viewportWidth = document.documentElement.clientWidth; + const spaceOnRight = viewportWidth - (threadRect.left + threadRect.width); + const spaceOnLeft = threadRect.left; + const popupWidth = popupRect.width; + setIsEnoughSpaceOnLeft(spaceOnLeft >= popupWidth + 10); + setIsEnoughSpaceOnRight(spaceOnRight >= popupWidth + 10); + setIsCalculationDone(true); + } }; - if(!isCalculationDone && isHoveringOnThread){ - calculateSpaceOnRight(); + + if (isHoveringOnThread) { + const timeoutId = setTimeout(executeLayoutEffectLogic, 500); + return () => clearTimeout(timeoutId); } + }, [isHoveringOnThread]); - - const timeElapsedCalc = (date) => { - const currentDateTime = new Date() - const targetToDate = new Date(date * 1000 ); // unix timestamp * 1000 - const monthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - const targetDateTime = new Date(`${monthNames[targetToDate.getMonth()]} ${targetToDate.getDate()} ${targetToDate.getFullYear()} ${targetToDate.getHours()}:${targetToDate.getMinutes()}:${targetToDate.getSeconds()}`) - const elapsedDateTime = currentDateTime - targetDateTime; - const elapsedDay = Math.floor(elapsedDateTime / (1000 * 60 * 60 * 24)); - const elapsedHour = Math.floor((elapsedDateTime / (1000 * 60 * 60)) % 24); - const elapsedMinute = Math.floor((elapsedDateTime / (1000 * 60)) % 60); - - if(elapsedDay > 0){ - return `${elapsedDay} ${elapsedDay > 0 ? "days" : "day"}`; - }else if(elapsedHour > 0 ){ - return `${elapsedHour} ${elapsedHour > 0 ? "hours" : "hour"}`; - }else if(elapsedMinute > 0){ - return `${elapsedMinute} ${elapsedMinute > 0 ? "minutes" : "minute"}` - } - } - + + const handleMouseOnLeaveThread = () => { - setIsHoveringOnThread(""); - setIsEnoughSpaceOnRight(null); - setIsCalculationDone(false); - } - + setIsHoveringForMenu(false); + if (hoverTimeoutId) { + clearTimeout(hoverTimeoutId); + setHoverTimeoutId(null); + setIsHoveringOnThread("") + } else if (isHoveringOnThread !== "") { + setIsHoveringOnThread(""); + setIsEnoughSpaceOnRight(null); + setIsCalculationDone(false); + } + }; + const handleOutsideClick = useCallback((e) => { if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { setOpenMenuCid(null); @@ -323,32 +314,39 @@ const CatalogPost = ({post}) => { let scale = Math.min(1, 150 / Math.max(thread.linkWidth, thread.linkHeight)); displayWidth = `${thread.linkWidth * scale}px`; displayHeight = `${thread.linkHeight * scale}px`; - } else { + } else if (thread.link) { displayWidth = '150px'; displayHeight = '150px'; + } else { + displayWidth = '0px'; + displayHeight = '0px'; } if (post.type === 'rules') { return (
setIsHoveringOnThread("rules")} onMouseLeave={()=>{handleMouseOnLeaveThread()}}> {isHoveringOnThread === 'rules' ?
setIsHoveringOnThread("")} className={"thread_popup"}>

Rules by ## Board Admins - {timeElapsedCalc(subplebbit.createdAt)}

-
-

Last reply by - Anonymous - 560 days ago

-
+ {getFormattedTime(subplebbit.createdAt)}

: null } - + { + setIsHoveringForMenu('rules'); + if (!hoverTimeoutId) { + const timeoutId = setTimeout(() => { + setIsHoveringOnThread("rules"); + }, 500); + setHoverTimeoutId(timeoutId); + } + }}>
R: 0 / L: 0
{ imageRendering: "pixelated",}} />
{ , document.body )} - -
+ { + setIsHoveringForMenu('rules'); + if (!hoverTimeoutId) { + const timeoutId = setTimeout(() => { + setIsHoveringOnThread("rules"); + }, 500); + setHoverTimeoutId(timeoutId); + } + }}> +
Rules {": " + subplebbit.rules?.map((rule, index) => `${index + 1}. ${rule}`).join(' ')}
@@ -419,26 +426,29 @@ const CatalogPost = ({post}) => { return ( <>
setIsHoveringOnThread("description")} onMouseLeave={()=>{handleMouseOnLeaveThread()}}> - {/* */} {isHoveringOnThread === 'description' ?
setIsHoveringOnThread("")} className={"thread_popup"}>

Welcome to {subplebbit.title || subplebbit.address} by ## Board Admins - {timeElapsedCalc(subplebbit.createdAt)} ago

-
-

Last reply by - Anonymous - 560 days ago

-
+ {getFormattedTime(subplebbit.createdAt)}

: null } {subplebbit.suggested?.avatarUrl ? ( { + setIsHoveringForMenu('description'); + if (!hoverTimeoutId) { + const timeoutId = setTimeout(() => { + setIsHoveringOnThread("description"); + }, 500); + setHoverTimeoutId(timeoutId); + } + }} src={subplebbit.suggested.avatarUrl} alt="board avatar" /> ) : null} @@ -452,8 +462,17 @@ const CatalogPost = ({post}) => { imageRendering: "pixelated",}} />
) : null} - -
+ { + setIsHoveringForMenu('description'); + if (!hoverTimeoutId) { + const timeoutId = setTimeout(() => { + setIsHoveringOnThread("description"); + }, 500); + setHoverTimeoutId(timeoutId); + } + }}> +
R: 0 / L: 0 {subplebbit.suggested?.avatarUrl ? null : (
{
)} { )} setSelectedThread("description")}> -
- Welcome to {subplebbit.title || subplebbit.address} + onClick={() => setSelectedThread("description")} onMouseOver={() => { + setIsHoveringForMenu('description'); + if (!hoverTimeoutId) { + const timeoutId = setTimeout(() => { + setIsHoveringOnThread("description"); + }, 500); + setHoverTimeoutId(timeoutId); + } + }}> +
+ Welcome to {subplebbit.title || subplebbit.address}! {": " + subplebbit.description}
@@ -553,38 +580,40 @@ const CatalogPost = ({post}) => { ) } else { return ( -
- {isHoveringOnThread === thread.cid ? -
-

- - {thread.title ? `${thread.title} ` : "Posted "} - - by - - {thread.author.displayName ?` ${thread.author.displayName} ` : " Anonymous "} - - {thread.timestamp ? timeElapsedCalc(thread.timestamp) : null} ago

-
- {thread.replyCount > 0 ? -

- Last reply by Anonymous - {timeElapsedCalc(thread.lastReplyTimestamp)} ago

- : null}
-
: null } - +
{handleMouseOnLeaveThread()}} onMouseOver={() => setIsHoveringOnThread(thread.cid)}> + {isHoveringOnThread === thread.cid ? +
handleMouseOnLeaveThread()} + style={{ + opacity: isCalculationDone && isEnoughSpaceOnRight !== null ? 1 : 0, + visibility: isCalculationDone && isEnoughSpaceOnRight !== null ? 'visible' : 'hidden', + left: isCalculationDone && isEnoughSpaceOnRight !== null && isEnoughSpaceOnRight ? '100%' : 'auto', + right: isCalculationDone && isEnoughSpaceOnRight !== null && !isEnoughSpaceOnRight ? '100%' : 'auto',}} + className="thread_popup"> +

+ + {thread.title ? `${thread.title} ` : "Posted "} + + by + + {thread.author.displayName ?` ${thread.author.displayName} ` : " Anonymous "} + + {thread.timestamp ? getFormattedTime(thread.timestamp) : null}

+
+ {thread.replyCount > 0 ? +

+ Last reply by Anonymous + {getFormattedTime(thread.lastReplyTimestamp)}

+ : null}
+
: null } {commentMediaInfo?.url ? ( setSelectedThread(thread.cid)}> @@ -593,8 +622,6 @@ const CatalogPost = ({post}) => { {commentMediaInfo.type} setIsHoveringOnThread(thread.cid)} - onMouseLeave={()=>{handleMouseOnLeaveThread()}} onError={(e) => { e.target.src = fallbackImgUrl e.target.onerror = null; @@ -606,8 +633,6 @@ const CatalogPost = ({post}) => { {commentMediaInfo.type} setIsHoveringOnThread(thread.cid)} - onMouseLeave={()=>{handleMouseOnLeaveThread()}} onError={(e) => { e.target.src = fallbackImgUrl e.target.onerror = null;}} /> @@ -616,8 +641,6 @@ const CatalogPost = ({post}) => { {commentMediaInfo?.type === "video" ? (