mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
test new virtuoso design for catalogs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// TODO: remove GridContainer when updating other catalogs
|
||||
export const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -14,7 +15,7 @@ export const Threads = styled.div`
|
||||
|
||||
.thread {
|
||||
width: 180px;
|
||||
height: 320px;
|
||||
max-height: 320px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
@@ -211,6 +212,10 @@ export const PostMenuCatalog = styled.div`
|
||||
background-color: #1d1d21 !important;
|
||||
border: 1px solid #111 !important;
|
||||
}
|
||||
|
||||
.catalog-media-wrapper {
|
||||
background-color: rgba(255, 255, 255, 0.01) !important;
|
||||
}
|
||||
|
||||
.post-menu-catalog {
|
||||
border: 1px solid #000;
|
||||
|
||||
+312
-273
@@ -4,11 +4,11 @@ import { Helmet } from 'react-helmet-async';
|
||||
import { Link, useNavigate} from 'react-router-dom';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { debounce } from 'lodash';
|
||||
import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled';
|
||||
import { Threads, PostMenuCatalog, GridContainer } from '../styled/views/Catalog.styled';
|
||||
import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled';
|
||||
import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
|
||||
import CatalogLoader from '../CatalogLoader';
|
||||
import EditModal from '../modals/EditModal';
|
||||
@@ -22,11 +22,14 @@ import countLinks from '../../utils/countLinks';
|
||||
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
|
||||
import handleStyleChange from '../../utils/handleStyleChange';
|
||||
import useError from '../../hooks/useError';
|
||||
import useFeedRows from '../../hooks/useFeedRows';
|
||||
import useFeedStateString from '../../hooks/useFeedStateString';
|
||||
import useSuccess from '../../hooks/useSuccess';
|
||||
import useWindowWidth from '../../hooks/useWindowWidth';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import packageJson from '../../../package.json'
|
||||
const {version} = packageJson
|
||||
let lastVirtuosoStates = {}
|
||||
|
||||
|
||||
const AllCatalog = () => {
|
||||
@@ -52,6 +55,7 @@ const AllCatalog = () => {
|
||||
const threadMenuRefs = useRef({});
|
||||
const postMenuRef = useRef(null);
|
||||
const postMenuCatalogRef = useRef(null);
|
||||
const virtuosoRef = useRef();
|
||||
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
@@ -60,7 +64,6 @@ const AllCatalog = () => {
|
||||
|
||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [isHoveringOnThread, setIsHoveringOnThread] = useState(false);
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [originalCommentContent, setOriginalCommentContent] = useState(null);
|
||||
@@ -68,18 +71,21 @@ const AllCatalog = () => {
|
||||
const [deletePost, setDeletePost] = useState(false);
|
||||
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
|
||||
const [commentCid, setCommentCid] = useState(null);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
const [moderatorPermissions, setModeratorPermissions] = useState({});
|
||||
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
|
||||
|
||||
const addresses = defaultSubplebbits.map(subplebbit => subplebbit.address);
|
||||
const { feed, loadMore } = useFeed({subplebbitAddresses: addresses, sortType: 'active'});
|
||||
const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: addresses, sortType: 'active'});
|
||||
const {subplebbits} = useSubplebbits({subplebbitAddresses: addresses, sortType: 'active'});
|
||||
const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp));
|
||||
|
||||
const stateString = useFeedStateString(subplebbits);
|
||||
|
||||
const columnWidth = 180;
|
||||
const windowWidth = useWindowWidth();
|
||||
const columnCount = Math.floor(windowWidth / columnWidth);
|
||||
const rows = useFeedRows(selectedFeed, columnCount);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let permissions = {};
|
||||
@@ -102,31 +108,6 @@ const AllCatalog = () => {
|
||||
}, [account?.author.address, selectedFeed, subplebbits]);
|
||||
|
||||
|
||||
const handleOptionClick = () => {
|
||||
setOpenMenuCid(null);
|
||||
};
|
||||
|
||||
|
||||
const handleOutsideClick = useCallback((e) => {
|
||||
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
|
||||
setOpenMenuCid(null);
|
||||
}
|
||||
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (openMenuCid !== null) {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
}, [openMenuCid, handleOutsideClick]);
|
||||
|
||||
|
||||
// mobile navbar scroll effect
|
||||
useEffect(() => {
|
||||
const debouncedHandleScroll = debounce(() => {
|
||||
@@ -221,51 +202,6 @@ const AllCatalog = () => {
|
||||
|
||||
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
|
||||
|
||||
|
||||
const handleAuthorDeleteClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
|
||||
confirmAlert({
|
||||
customUI: ({ onClose }) => {
|
||||
return (
|
||||
<AuthorDeleteAlert selectedStyle={selectedStyle}>
|
||||
<div className='author-delete-alert'>
|
||||
<p>Are you sure you want to delete this post?</p>
|
||||
<div className="author-delete-buttons">
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
commentCid: comment.cid,
|
||||
subplebbitAddress: comment.subplebbitAddress,
|
||||
deleted: true,
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthorDeleteAlert>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentEditOptions((prevOptions) => ({
|
||||
@@ -326,6 +262,296 @@ const AllCatalog = () => {
|
||||
};
|
||||
|
||||
|
||||
const CatalogPost = ({post}) => {
|
||||
const thread = post;
|
||||
const commentMediaInfo = getCommentMediaInfo(thread);
|
||||
const linkCount = countLinks(thread);
|
||||
const fallbackImgUrl = "assets/filedeleted-res.gif";
|
||||
const [isHoveringOnThread, setIsHoveringOnThread] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
const isModerator = moderatorPermissions[thread.subplebbitAddress];
|
||||
|
||||
const handleOutsideClick = useCallback((e) => {
|
||||
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
|
||||
setOpenMenuCid(null);
|
||||
}
|
||||
}, [openMenuCid]);
|
||||
|
||||
const handleOptionClick = () => {
|
||||
setOpenMenuCid(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (openMenuCid !== null) {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
}, [openMenuCid, handleOutsideClick]);
|
||||
|
||||
const handleAuthorDeleteClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
|
||||
confirmAlert({
|
||||
customUI: ({ onClose }) => {
|
||||
return (
|
||||
<AuthorDeleteAlert selectedStyle={selectedStyle}>
|
||||
<div className='author-delete-alert'>
|
||||
<p>Are you sure you want to delete this post?</p>
|
||||
<div className="author-delete-buttons">
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
commentCid: comment.cid,
|
||||
subplebbitAddress: comment.subplebbitAddress,
|
||||
deleted: true,
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthorDeleteAlert>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`thread-`} className="thread"
|
||||
onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
|
||||
onMouseLeave={() => {setIsHoveringOnThread('')}}>
|
||||
{commentMediaInfo?.url ? (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<video className="card" key={`fti-`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
<audio className="card" controls
|
||||
key={`fti-`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div key={`ti-`} className="thread-icons" >
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon"
|
||||
tooltipPlace="top" />
|
||||
) : (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon-no-link"
|
||||
tooltipPlace="top" />
|
||||
)}
|
||||
</div>
|
||||
<BoardForm selectedStyle={selectedStyle}
|
||||
style={{ all: "unset"}}>
|
||||
<div key={`meta-`} className="meta" title="(R)eplies / (L)ink Replies" >
|
||||
R: <b key={`b-`}>{thread.replyCount}</b>
|
||||
{linkCount > 0 ? (
|
||||
<>
|
||||
/
|
||||
L: <b key={`i-`}>{linkCount}</b>
|
||||
</>
|
||||
) : null}
|
||||
<PostMenu
|
||||
style={{ display: isHoveringOnThread === thread.cid ? 'inline-block' : 'none',
|
||||
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
|
||||
zIndex: '999'}}
|
||||
key={`pmb-`}
|
||||
title="Post menu"
|
||||
ref={el => {
|
||||
threadMenuRefs.current[thread.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
id='post-menu-button-catalog'
|
||||
rotated={openMenuCid === thread.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
</div>
|
||||
{createPortal(
|
||||
<PostMenuCatalog selectedStyle={selectedStyle}
|
||||
ref={el => {postMenuCatalogRef.current = el}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{position: "absolute",
|
||||
top: menuPosition.top + 7,
|
||||
left: menuPosition.left}}>
|
||||
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
|
||||
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
|
||||
<VerifiedAuthor commentCid={thread.cid}>{({ authorAddress }) => (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}</VerifiedAuthor>
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
</BoardForm>
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
<div key={`t-`} className="teaser">
|
||||
<b key={`b2-`}>{thread.title ? `${thread.title}` : null}</b>
|
||||
{thread.content ? `: ${thread.content}` : null}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const CatalogRow = ({row}) => {
|
||||
const posts = []
|
||||
for (const post of row) {
|
||||
posts.push(<CatalogPost key={post?.cid} post={post} />)
|
||||
}
|
||||
return <div>{posts}</div>
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const setLastVirtuosoState = () => {
|
||||
virtuosoRef.current?.getState((snapshot) => {
|
||||
if (snapshot?.scrollTop === 0 || snapshot?.ranges?.length) {
|
||||
lastVirtuosoStates["allCatalog"] = snapshot;
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener('scroll', setLastVirtuosoState);
|
||||
|
||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||
}, [selectedAddress]);
|
||||
|
||||
|
||||
const lastVirtuosoState = lastVirtuosoStates["allCatalog"];
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -468,205 +694,18 @@ const AllCatalog = () => {
|
||||
<Tooltip id="tooltip" className="tooltip" />
|
||||
<Threads selectedStyle={selectedStyle}>
|
||||
{feed.length > 1 ? (
|
||||
<VirtuosoGrid
|
||||
style={{width: '100%', height: '100%'}}
|
||||
data={feed}
|
||||
<Virtuoso
|
||||
increaseViewportBy={{bottom: 600, top: 600}}
|
||||
itemContent={(index) => {
|
||||
const thread = feed[index];
|
||||
const commentMediaInfo = getCommentMediaInfo(thread);
|
||||
const fallbackImgUrl = "assets/filedeleted-res.gif";
|
||||
const isModerator = moderatorPermissions[thread.subplebbitAddress];
|
||||
const linkCount = countLinks(thread);
|
||||
return (
|
||||
<div key={`thread-${index}`} className="thread"
|
||||
onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
|
||||
onMouseLeave={() => {setIsHoveringOnThread('')}}>
|
||||
{commentMediaInfo?.url ? (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<video className="card" key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
<audio className="card" controls
|
||||
key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div key={`ti-${index}`} className="thread-icons" >
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon"
|
||||
tooltipPlace="top" />
|
||||
) : (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon-no-link"
|
||||
tooltipPlace="top" />
|
||||
)}
|
||||
</div>
|
||||
<BoardForm selectedStyle={selectedStyle}
|
||||
style={{ all: "unset"}}>
|
||||
<div key={`meta-${index}`} className="meta" title="(R)eplies / (L)ink Replies" >
|
||||
R: <b key={`b-${index}`}>{thread.replyCount}</b>
|
||||
{linkCount > 0 ? (
|
||||
<>
|
||||
/
|
||||
L: <b key={`i-${index}`}>{linkCount}</b>
|
||||
</>
|
||||
) : null}
|
||||
<PostMenu
|
||||
style={{ display: isHoveringOnThread === thread.cid ? 'inline-block' : 'none',
|
||||
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
|
||||
zIndex: '999'}}
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => {
|
||||
threadMenuRefs.current[thread.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
id='post-menu-button-catalog'
|
||||
rotated={openMenuCid === thread.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
</div>
|
||||
{createPortal(
|
||||
<PostMenuCatalog selectedStyle={selectedStyle}
|
||||
ref={el => {postMenuCatalogRef.current = el}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{position: "absolute",
|
||||
top: menuPosition.top + 7,
|
||||
left: menuPosition.left}}>
|
||||
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
|
||||
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
|
||||
<VerifiedAuthor commentCid={thread.cid}>{({ authorAddress }) => (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}</VerifiedAuthor>
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
</BoardForm>
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-${index}`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
<div key={`t-${index}`} className="teaser">
|
||||
<b key={`b2-${index}`}>{thread.title ? `${thread.title}` : null}</b>
|
||||
{thread.content ? `: ${thread.content}` : null}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
endReached={tryLoadMore}
|
||||
totalCount={rows?.length || 0}
|
||||
data={rows}
|
||||
style={{maxWidth: '100%'}}
|
||||
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
|
||||
useWindowScroll={true}
|
||||
components={{List: GridContainer}}
|
||||
components={{ Footer: () => hasMore ? <CatalogLoader /> : null}}
|
||||
endReached={tryLoadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
) : (<CatalogLoader />)}
|
||||
</Threads>
|
||||
|
||||
+525
-496
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,11 @@ import { Helmet } from 'react-helmet-async';
|
||||
import { Link, useNavigate} from 'react-router-dom';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { debounce } from 'lodash';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled';
|
||||
import { Threads, PostMenuCatalog, GridContainer } from '../styled/views/Catalog.styled';
|
||||
import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled';
|
||||
import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
|
||||
import CatalogLoader from '../CatalogLoader';
|
||||
import EditModal from '../modals/EditModal';
|
||||
@@ -23,10 +22,14 @@ import countLinks from '../../utils/countLinks';
|
||||
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
|
||||
import handleStyleChange from '../../utils/handleStyleChange';
|
||||
import useError from '../../hooks/useError';
|
||||
import useFeedRows from '../../hooks/useFeedRows';
|
||||
import useFeedStateString from '../../hooks/useFeedStateString';
|
||||
import useGeneralStore from '../../hooks/stores/useGeneralStore';
|
||||
import useSuccess from '../../hooks/useSuccess';
|
||||
import packageJson from '../../../package.json'
|
||||
import useWindowWidth from '../../hooks/useWindowWidth';
|
||||
import packageJson from '../../../package.json';
|
||||
const {version} = packageJson
|
||||
let lastVirtuosoStates = {}
|
||||
|
||||
|
||||
const SubscriptionsCatalog = () => {
|
||||
@@ -51,6 +54,7 @@ const SubscriptionsCatalog = () => {
|
||||
const threadMenuRefs = useRef({});
|
||||
const postMenuRef = useRef(null);
|
||||
const postMenuCatalogRef = useRef(null);
|
||||
const virtuosoRef = useRef();
|
||||
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
@@ -59,7 +63,6 @@ const SubscriptionsCatalog = () => {
|
||||
|
||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [isHoveringOnThread, setIsHoveringOnThread] = useState(false);
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [originalCommentContent, setOriginalCommentContent] = useState(null);
|
||||
@@ -67,17 +70,20 @@ const SubscriptionsCatalog = () => {
|
||||
const [deletePost, setDeletePost] = useState(false);
|
||||
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
|
||||
const [commentCid, setCommentCid] = useState(null);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
const [moderatorPermissions, setModeratorPermissions] = useState({});
|
||||
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
|
||||
|
||||
const { feed, loadMore } = useFeed({subplebbitAddresses: account?.subscriptions, sortType: 'active'});
|
||||
const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: account?.subscriptions, sortType: 'active'});
|
||||
const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp));
|
||||
const {subplebbits} = useSubplebbits({subplebbitAddresses: account?.subscriptions, sortType: 'active'});
|
||||
|
||||
const stateString = useFeedStateString(subplebbits);
|
||||
|
||||
const columnWidth = 180;
|
||||
const windowWidth = useWindowWidth();
|
||||
const columnCount = Math.floor(windowWidth / columnWidth);
|
||||
const rows = useFeedRows(feed, columnCount);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let permissions = {};
|
||||
@@ -100,30 +106,6 @@ const SubscriptionsCatalog = () => {
|
||||
}, [account?.author.address, selectedFeed, subplebbits]);
|
||||
|
||||
|
||||
const handleOptionClick = () => {
|
||||
setOpenMenuCid(null);
|
||||
};
|
||||
|
||||
|
||||
const handleOutsideClick = useCallback((e) => {
|
||||
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
|
||||
setOpenMenuCid(null);
|
||||
}
|
||||
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (openMenuCid !== null) {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
}, [openMenuCid, handleOutsideClick]);
|
||||
|
||||
// mobile navbar scroll effect
|
||||
useEffect(() => {
|
||||
const debouncedHandleScroll = debounce(() => {
|
||||
@@ -138,13 +120,6 @@ const SubscriptionsCatalog = () => {
|
||||
}, [prevScrollPos, visible]);
|
||||
|
||||
|
||||
const tryLoadMore = async () => {
|
||||
try {loadMore()}
|
||||
catch (e)
|
||||
{await new Promise(resolve => setTimeout(resolve, 1000))}
|
||||
};
|
||||
|
||||
|
||||
const onChallengeVerification = (challengeVerification) => {
|
||||
if (challengeVerification.challengeSuccess === true) {
|
||||
setNewSuccessMessage('Challenge Success');
|
||||
@@ -219,51 +194,6 @@ const SubscriptionsCatalog = () => {
|
||||
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
|
||||
|
||||
|
||||
const handleAuthorDeleteClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
|
||||
confirmAlert({
|
||||
customUI: ({ onClose }) => {
|
||||
return (
|
||||
<AuthorDeleteAlert selectedStyle={selectedStyle}>
|
||||
<div className='author-delete-alert'>
|
||||
<p>Are you sure you want to delete this post?</p>
|
||||
<div className="author-delete-buttons">
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
commentCid: comment.cid,
|
||||
subplebbitAddress: comment.subplebbitAddress,
|
||||
deleted: true,
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthorDeleteAlert>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentEditOptions((prevOptions) => ({
|
||||
...prevOptions,
|
||||
@@ -322,6 +252,296 @@ const SubscriptionsCatalog = () => {
|
||||
};
|
||||
|
||||
|
||||
const CatalogPost = ({post}) => {
|
||||
const thread = post;
|
||||
const commentMediaInfo = getCommentMediaInfo(thread);
|
||||
const linkCount = countLinks(thread);
|
||||
const fallbackImgUrl = "assets/filedeleted-res.gif";
|
||||
const [isHoveringOnThread, setIsHoveringOnThread] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
|
||||
const [openMenuCid, setOpenMenuCid] = useState(null);
|
||||
const isModerator = moderatorPermissions[thread.subplebbitAddress];
|
||||
|
||||
const handleOutsideClick = useCallback((e) => {
|
||||
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
|
||||
setOpenMenuCid(null);
|
||||
}
|
||||
}, [openMenuCid]);
|
||||
|
||||
const handleOptionClick = () => {
|
||||
setOpenMenuCid(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (openMenuCid !== null) {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
};
|
||||
}, [openMenuCid, handleOutsideClick]);
|
||||
|
||||
const handleAuthorDeleteClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
|
||||
confirmAlert({
|
||||
customUI: ({ onClose }) => {
|
||||
return (
|
||||
<AuthorDeleteAlert selectedStyle={selectedStyle}>
|
||||
<div className='author-delete-alert'>
|
||||
<p>Are you sure you want to delete this post?</p>
|
||||
<div className="author-delete-buttons">
|
||||
<button onClick={onClose}>No</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAuthorDelete(true);
|
||||
setIsAuthorEdit(false);
|
||||
setPublishCommentEditOptions(prevOptions => ({
|
||||
...prevOptions,
|
||||
commentCid: comment.cid,
|
||||
subplebbitAddress: comment.subplebbitAddress,
|
||||
deleted: true,
|
||||
}));
|
||||
setTriggerPublishCommentEdit(true);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthorDeleteAlert>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleAuthorEditClick = (comment) => {
|
||||
handleOptionClick(comment.cid);
|
||||
setIsAuthorEdit(true);
|
||||
setIsAuthorDelete(false);
|
||||
setCommentCid(comment.cid);
|
||||
setOriginalCommentContent(comment.content);
|
||||
setIsEditModalOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`thread-`} className="thread"
|
||||
onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
|
||||
onMouseLeave={() => {setIsHoveringOnThread('')}}>
|
||||
{commentMediaInfo?.url ? (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<video className="card" key={`fti-`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
<audio className="card" controls
|
||||
key={`fti-`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div key={`ti-`} className="thread-icons" >
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon"
|
||||
tooltipPlace="top" />
|
||||
) : (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon-no-link"
|
||||
tooltipPlace="top" />
|
||||
)}
|
||||
</div>
|
||||
<BoardForm selectedStyle={selectedStyle}
|
||||
style={{ all: "unset"}}>
|
||||
<div key={`meta-`} className="meta" title="(R)eplies / (L)ink Replies" >
|
||||
R: <b key={`b-`}>{thread.replyCount}</b>
|
||||
{linkCount > 0 ? (
|
||||
<>
|
||||
/
|
||||
L: <b key={`i-`}>{linkCount}</b>
|
||||
</>
|
||||
) : null}
|
||||
<PostMenu
|
||||
style={{ display: isHoveringOnThread === thread.cid ? 'inline-block' : 'none',
|
||||
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
|
||||
zIndex: '999'}}
|
||||
key={`pmb-`}
|
||||
title="Post menu"
|
||||
ref={el => {
|
||||
threadMenuRefs.current[thread.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
id='post-menu-button-catalog'
|
||||
rotated={openMenuCid === thread.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
</div>
|
||||
{createPortal(
|
||||
<PostMenuCatalog selectedStyle={selectedStyle}
|
||||
ref={el => {postMenuCatalogRef.current = el}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{position: "absolute",
|
||||
top: menuPosition.top + 7,
|
||||
left: menuPosition.left}}>
|
||||
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
|
||||
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
|
||||
<VerifiedAuthor commentCid={thread.cid}>{({ authorAddress }) => (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}</VerifiedAuthor>
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
</BoardForm>
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
<div key={`t-`} className="teaser">
|
||||
<b key={`b2-`}>{thread.title ? `${thread.title}` : null}</b>
|
||||
{thread.content ? `: ${thread.content}` : null}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const CatalogRow = ({row}) => {
|
||||
const posts = []
|
||||
for (const post of row) {
|
||||
posts.push(<CatalogPost key={post?.cid} post={post} />)
|
||||
}
|
||||
return <div>{posts}</div>
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const setLastVirtuosoState = () => {
|
||||
virtuosoRef.current?.getState((snapshot) => {
|
||||
if (snapshot?.scrollTop === 0 || snapshot?.ranges?.length) {
|
||||
lastVirtuosoStates[`${selectedAddress}-catalog`] = snapshot;
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener('scroll', setLastVirtuosoState);
|
||||
|
||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||
}, [selectedAddress]);
|
||||
|
||||
|
||||
const lastVirtuosoState = lastVirtuosoStates["subscriptionsCatalog"];
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -472,205 +692,18 @@ const SubscriptionsCatalog = () => {
|
||||
<Tooltip id="tooltip" className="tooltip" />
|
||||
<Threads selectedStyle={selectedStyle}>
|
||||
{feed.length > 1 ? (
|
||||
<VirtuosoGrid
|
||||
style={{width: '100%', height: '100%'}}
|
||||
data={feed}
|
||||
<Virtuoso
|
||||
increaseViewportBy={{bottom: 600, top: 600}}
|
||||
itemContent={(index) => {
|
||||
const thread = feed[index];
|
||||
const commentMediaInfo = getCommentMediaInfo(thread);
|
||||
const fallbackImgUrl = "assets/filedeleted-res.gif";
|
||||
const isModerator = moderatorPermissions[thread.subplebbitAddress];
|
||||
const linkCount = countLinks(thread);
|
||||
return (
|
||||
<div key={`thread-${index}`} className="thread"
|
||||
onMouseOver={() => {setIsHoveringOnThread(thread.cid)}}
|
||||
onMouseLeave={() => {setIsHoveringOnThread('')}}>
|
||||
{commentMediaInfo?.url ? (
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link-${index}`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
{commentMediaInfo?.type === "webpage" ? (
|
||||
thread.thumbnailUrl ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;
|
||||
}} />
|
||||
) : null
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "image" ? (
|
||||
<img className="card" key={`img-${index}`}
|
||||
src={commentMediaInfo.url} alt={commentMediaInfo.type}
|
||||
onError={(e) => {
|
||||
e.target.src = fallbackImgUrl
|
||||
e.target.onerror = null;}} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "video" ? (
|
||||
<video className="card" key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
{commentMediaInfo?.type === "audio" ? (
|
||||
<audio className="card" controls
|
||||
key={`fti-${index}`}
|
||||
src={commentMediaInfo.url}
|
||||
alt={commentMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div key={`ti-${index}`} className="thread-icons" >
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon"
|
||||
tooltipPlace="top" />
|
||||
) : (
|
||||
<OfflineIndicator
|
||||
address={thread.subplebbitAddress}
|
||||
className="thread-icon offline-icon-no-link"
|
||||
tooltipPlace="top" />
|
||||
)}
|
||||
</div>
|
||||
<BoardForm selectedStyle={selectedStyle}
|
||||
style={{ all: "unset"}}>
|
||||
<div key={`meta-${index}`} className="meta" title="(R)eplies / (L)ink Replies" >
|
||||
R: <b key={`b-${index}`}>{thread.replyCount}</b>
|
||||
{linkCount > 0 ? (
|
||||
<>
|
||||
/
|
||||
L: <b key={`i-${index}`}>{linkCount}</b>
|
||||
</>
|
||||
) : null}
|
||||
<PostMenu
|
||||
style={{ display: isHoveringOnThread === thread.cid ? 'inline-block' : 'none',
|
||||
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
|
||||
zIndex: '999'}}
|
||||
key={`pmb-${index}`}
|
||||
title="Post menu"
|
||||
ref={el => {
|
||||
threadMenuRefs.current[thread.cid] = el;
|
||||
postMenuRef.current = el;
|
||||
}}
|
||||
className='post-menu-button'
|
||||
id='post-menu-button-catalog'
|
||||
rotated={openMenuCid === thread.cid}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
|
||||
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
|
||||
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</PostMenu>
|
||||
</div>
|
||||
{createPortal(
|
||||
<PostMenuCatalog selectedStyle={selectedStyle}
|
||||
ref={el => {postMenuCatalogRef.current = el}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{position: "absolute",
|
||||
top: menuPosition.top + 7,
|
||||
left: menuPosition.left}}>
|
||||
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
|
||||
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
|
||||
>
|
||||
<ul className="post-menu-catalog">
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>Hide thread</li>
|
||||
<VerifiedAuthor commentCid={thread.cid}>{({ authorAddress }) => (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
<>
|
||||
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
|
||||
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator ? (
|
||||
<>
|
||||
{authorAddress === account?.author.address ||
|
||||
authorAddress === account?.signer.address ? (
|
||||
null
|
||||
) : (
|
||||
<li onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
setDeletePost(true);
|
||||
}}>
|
||||
Delete post
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
setSelectedAddress(thread.subplebbitAddress);
|
||||
setModeratingCommentCid(thread.cid)
|
||||
setIsModerationOpen(true);
|
||||
handleOptionClick(thread.cid);
|
||||
}}>
|
||||
Mod tools
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}</VerifiedAuthor>
|
||||
{(commentMediaInfo && (
|
||||
commentMediaInfo.type === 'image' ||
|
||||
(commentMediaInfo.type === 'webpage' &&
|
||||
commentMediaInfo.thumbnail))) ? (
|
||||
<li
|
||||
onMouseOver={() => {setIsImageSearchOpen(true)}}
|
||||
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
|
||||
Image search »
|
||||
<ul className="dropdown-menu post-menu-catalog"
|
||||
style={{display: isImageSearchOpen ? 'block': 'none'}}>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Google</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>Yandex</a>
|
||||
</li>
|
||||
<li onClick={() => handleOptionClick(thread.cid)}>
|
||||
<a
|
||||
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
|
||||
target="_blank" rel="noreferrer"
|
||||
>SauceNAO</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</PostMenuCatalog>, document.body
|
||||
)}
|
||||
</BoardForm>
|
||||
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-${index}`} to={`/p/${thread.subplebbitAddress}/c/${thread.cid}`}
|
||||
onClick={() => setSelectedThread(thread.cid)}>
|
||||
<div key={`t-${index}`} className="teaser">
|
||||
<b key={`b2-${index}`}>{thread.title ? `${thread.title}` : null}</b>
|
||||
{thread.content ? `: ${thread.content}` : null}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
endReached={tryLoadMore}
|
||||
totalCount={rows?.length || 0}
|
||||
data={rows}
|
||||
style={{maxWidth: '100%'}}
|
||||
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
|
||||
useWindowScroll={true}
|
||||
components={{List: GridContainer}}
|
||||
components={{ Footer: () => hasMore ? <CatalogLoader /> : null}}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
) : (<CatalogLoader />)}
|
||||
</Threads>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMemo, useRef } from "react"
|
||||
|
||||
const useFeedRows = (feedWithDescriptionAndRules, columnCount) => {
|
||||
const rowsRef = useRef([]);
|
||||
return useMemo(() => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
|
||||
if (rowsRef.current?.[rows.length] && rowsRef.current[rows.length].length === columnCount) {
|
||||
rows.push(rowsRef.current[rows.length])
|
||||
}
|
||||
else {
|
||||
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount))
|
||||
}
|
||||
}
|
||||
rowsRef.current = rows
|
||||
return rows;
|
||||
}, [feedWithDescriptionAndRules, columnCount]);
|
||||
}
|
||||
|
||||
export default useFeedRows;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function useWindowWidth() {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowWidth(window.innerWidth)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
return windowWidth
|
||||
}
|
||||
Reference in New Issue
Block a user