mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+7
-6
@@ -9,7 +9,7 @@
|
||||
"@babel/plugin-transform-react-jsx": "7.21.0",
|
||||
"@capacitor/android": "3.6.0",
|
||||
"@capacitor/core": "3.6.0",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#d41d7a4f150cecd831aeda57c3ec9edf1c22f9f5",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#5c5521672fac68ab00c83b96614445f7ccfd5b04",
|
||||
"@testing-library/dom": "9.0.1",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "14.0.0",
|
||||
@@ -71,9 +71,11 @@
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
"chrome >= 67",
|
||||
"edge >= 79",
|
||||
"firefox >= 68",
|
||||
"opera >= 54",
|
||||
"safari >= 14"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
@@ -132,7 +134,6 @@
|
||||
"wait-on": "7.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"styled-components": "^5",
|
||||
"@adraffy/ens-normalize": "1.8.3"
|
||||
"styled-components": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
address: account?.author.address,
|
||||
},
|
||||
content: commentRef.current.value || undefined,
|
||||
link: linkRef.current.value || undefined,
|
||||
@@ -215,7 +216,7 @@ const ReplyModal = ({ isOpen, closeModal }) => {
|
||||
signer,
|
||||
author: {
|
||||
...prevPublishCommentOptions.author,
|
||||
address: signer?.address
|
||||
address: signer?.address,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,30 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
const author = {...account?.author, address: ensName};
|
||||
const { resolvedAddress, state } = useResolvedAuthorAddress({ author, cache: false });
|
||||
|
||||
const isElectron = window.electron && window.electron.isElectron;
|
||||
|
||||
const defaultGatewayUrls = isElectron ? undefined : [
|
||||
'https://ipfs.io',
|
||||
'https://ipfsgateway.xyz',
|
||||
'https://cloudflare-ipfs.com',
|
||||
'https://plebpubsub.live'
|
||||
];
|
||||
|
||||
const defaultPubsubHttpClientsOptions = isElectron ? undefined : [
|
||||
'https://pubsubprovider.xyz/api/v0',
|
||||
'https://plebpubsub.live/api/v0'
|
||||
];
|
||||
|
||||
const defaultEthereumRpcUrls = [
|
||||
'ethers.js',
|
||||
'https://ethrpc.xyz',
|
||||
'viem',
|
||||
];
|
||||
|
||||
const defaultPolygonRpcUrls = [
|
||||
'https://polygon-rpc.com',
|
||||
];
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (checkedENS && resolvedAddress && state === 'succeeded') {
|
||||
@@ -85,29 +109,6 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
setNewErrorMessage(`Failed resolving address, ${ensName}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const defaultGatewayUrls = [
|
||||
'https://ipfs.io',
|
||||
'https://ipfsgateway.xyz',
|
||||
'https://cloudflare-ipfs.com',
|
||||
'https://plebpubsub.live'
|
||||
];
|
||||
|
||||
const defaultPubsubHttpClientsOptions = [
|
||||
'https://pubsubprovider.xyz/api/v0',
|
||||
'https://plebpubsub.live/api/v0'
|
||||
];
|
||||
|
||||
const defaultEthereumRpcUrls = [
|
||||
'ethers.js',
|
||||
'https://ethrpc.xyz',
|
||||
'viem',
|
||||
];
|
||||
|
||||
const defaultPolygonRpcUrls = [
|
||||
'https://polygon-rpc.com',
|
||||
];
|
||||
|
||||
|
||||
const isValidURL = (url) => {
|
||||
@@ -147,22 +148,22 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
|
||||
|
||||
const handleSavePlebbitOptions = async () => {
|
||||
let gatewayUrls = gatewayRef.current.value.split('\n').filter(url => url.trim());
|
||||
let gatewayUrls = gatewayRef.current.value.split('\n').filter((url) => url.trim());
|
||||
let ipfsClientsOptions = ipfsRef.current.value.split('\n').filter(url => url.trim());
|
||||
let pubsubClientsOptions = pubsubRef.current.value.split('\n').filter(url => url.trim());
|
||||
|
||||
if (!gatewayUrls.length) {
|
||||
gatewayUrls = defaultGatewayUrls;
|
||||
let pubsubClientsOptions = pubsubRef.current.value.split('\n').filter((url) => url.trim());
|
||||
|
||||
if (!isElectron) {
|
||||
if (!gatewayUrls.length) gatewayUrls = defaultGatewayUrls;
|
||||
if (!pubsubClientsOptions.length) pubsubClientsOptions = defaultPubsubHttpClientsOptions;
|
||||
} else {
|
||||
if (!gatewayUrls.length) gatewayUrls = undefined;
|
||||
if (!pubsubClientsOptions.length) pubsubClientsOptions = undefined;
|
||||
}
|
||||
|
||||
if (!ipfsClientsOptions.length) {
|
||||
ipfsClientsOptions = undefined;
|
||||
}
|
||||
|
||||
if (!pubsubClientsOptions.length) {
|
||||
pubsubClientsOptions = defaultPubsubHttpClientsOptions;
|
||||
}
|
||||
|
||||
const invalidUrls = [
|
||||
...gatewayUrls || defaultGatewayUrls,
|
||||
...(ipfsClientsOptions || []),
|
||||
@@ -445,7 +446,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={anonymousMode}
|
||||
onChange={() => {setAnonymousMode(!anonymousMode)}}
|
||||
onChange={() => {setAnonymousMode(!anonymousMode); window.location.reload();}}
|
||||
/>
|
||||
Anon Mode
|
||||
</label>
|
||||
@@ -571,7 +572,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="IPFS Gateway URLs"
|
||||
defaultValue={account?.plebbitOptions?.ipfsGatewayUrls.join("\n")}
|
||||
defaultValue={isElectron ? '' : account?.plebbitOptions?.ipfsGatewayUrls.join('\n')}
|
||||
ref={gatewayRef}
|
||||
/>
|
||||
</div>
|
||||
@@ -593,7 +594,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
|
||||
<li className="settings-tip">Optional URLs or IpfsHttpClientOptions used for pubsub publishing when ipfsHttpClientOptions isn't available, like in the browser.</li>
|
||||
<div className="settings-input">
|
||||
<textarea placeholder="PubSub HTTP Clients Options"
|
||||
defaultValue={account?.plebbitOptions?.pubsubHttpClientsOptions.join("\n")}
|
||||
defaultValue={isElectron ? '' : account?.plebbitOptions?.pubsubHttpClientsOptions.join('\n')}
|
||||
ref={pubsubRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Threads = styled.div`
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
@@ -14,7 +7,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 +204,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;
|
||||
|
||||
@@ -1364,6 +1364,17 @@ const All = () => {
|
||||
replyMediaInfo?.url.slice(0, 30) + "(...)" :
|
||||
replyMediaInfo?.url
|
||||
}</a> ({replyMediaInfo?.type})
|
||||
{replyMediaInfo?.type === "video" || "iframe" ? (
|
||||
isReplyThumbnailClicked[index] ? (
|
||||
<span>
|
||||
-[
|
||||
<span className='reply-link'
|
||||
style={{textDecoration: 'underline', cursor: 'pointer'}}
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}>Close</span>
|
||||
]
|
||||
</span>)
|
||||
: null)
|
||||
: null}
|
||||
</div>
|
||||
{replyMediaInfo?.type === "webpage" ? (
|
||||
<div key={`enlarge-reply-${index}`} className="img-container">
|
||||
@@ -1390,12 +1401,29 @@ const All = () => {
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === "video" ? (
|
||||
<div key={`enlarge-reply-${index}`} className={`img-container ${isReplyThumbnailClicked[index] ? 'expanded-container' : ''}`}>
|
||||
<span key={`fta-${index}`} className="file-thumb-reply" style={isReplyThumbnailClicked[index] ? {} : {width: replyDisplayWidth, height: replyDisplayHeight}}>
|
||||
<video controls
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url} alt={replyMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
{isReplyThumbnailClicked[index] ? (
|
||||
<video controls
|
||||
className='enlarged'
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{cursor: "pointer"}}
|
||||
onError={(e) => e.target.src = fallbackImgUrl}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url}
|
||||
alt="thumbnail"
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}
|
||||
style={{cursor: "pointer"}}
|
||||
onError={(e) => e.target.src = fallbackImgUrl}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === "audio" ? (
|
||||
<span key={`fta-${index}`} className="file-thumb-reply" style={isReplyThumbnailClicked[index] ? {} : {width: replyDisplayWidth, height: replyDisplayHeight}}>
|
||||
|
||||
+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>
|
||||
|
||||
@@ -445,6 +445,7 @@ const Board = () => {
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
address: account?.author.address,
|
||||
},
|
||||
title: subjectRef.current.value || undefined,
|
||||
content: commentRef.current.value || undefined,
|
||||
@@ -482,7 +483,7 @@ const Board = () => {
|
||||
signer,
|
||||
author: {
|
||||
...prevPublishCommentOptions.author,
|
||||
address: signer?.address
|
||||
address: signer?.address,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2018,15 +2019,16 @@ const Board = () => {
|
||||
replyMediaInfo?.url
|
||||
}</a> ({replyMediaInfo?.type})
|
||||
{replyMediaInfo?.type === "video" || "iframe" ? (
|
||||
isReplyThumbnailClicked[index] ? (
|
||||
<span>
|
||||
-[
|
||||
<span className='reply-link'
|
||||
style={{textDecoration: 'underline', cursor: 'pointer'}}
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}>Close</span>
|
||||
]
|
||||
</span>) : null)
|
||||
: null}
|
||||
isReplyThumbnailClicked[index] ? (
|
||||
<span>
|
||||
-[
|
||||
<span className='reply-link'
|
||||
style={{textDecoration: 'underline', cursor: 'pointer'}}
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}>Close</span>
|
||||
]
|
||||
</span>)
|
||||
: null)
|
||||
: null}
|
||||
</div>
|
||||
{replyMediaInfo?.type === "webpage" ? (
|
||||
<div key={`enlarge-reply-${index}`} className="img-container">
|
||||
|
||||
+527
-497
File diff suppressed because it is too large
Load Diff
@@ -1375,6 +1375,17 @@ const Subscriptions = () => {
|
||||
replyMediaInfo?.url.slice(0, 30) + "(...)" :
|
||||
replyMediaInfo?.url
|
||||
}</a> ({replyMediaInfo?.type})
|
||||
{replyMediaInfo?.type === "video" || "iframe" ? (
|
||||
isReplyThumbnailClicked[index] ? (
|
||||
<span>
|
||||
-[
|
||||
<span className='reply-link'
|
||||
style={{textDecoration: 'underline', cursor: 'pointer'}}
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}>Close</span>
|
||||
]
|
||||
</span>)
|
||||
: null)
|
||||
: null}
|
||||
</div>
|
||||
{replyMediaInfo?.type === "webpage" ? (
|
||||
<div key={`enlarge-reply-${index}`} className="img-container">
|
||||
@@ -1401,12 +1412,29 @@ const Subscriptions = () => {
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === "video" ? (
|
||||
<div key={`enlarge-reply-${index}`} className={`img-container ${isReplyThumbnailClicked[index] ? 'expanded-container' : ''}`}>
|
||||
<span key={`fta-${index}`} className="file-thumb-reply" style={isReplyThumbnailClicked[index] ? {} : {width: replyDisplayWidth, height: replyDisplayHeight}}>
|
||||
<video controls
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url} alt={replyMediaInfo.type}
|
||||
onError={(e) => e.target.src = fallbackImgUrl} />
|
||||
{isReplyThumbnailClicked[index] ? (
|
||||
<video controls
|
||||
className='enlarged'
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url}
|
||||
alt={replyMediaInfo.type}
|
||||
style={{cursor: "pointer"}}
|
||||
onError={(e) => e.target.src = fallbackImgUrl}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
key={`fti-${index}`}
|
||||
src={replyMediaInfo.url}
|
||||
alt="thumbnail"
|
||||
onClick={() => {handleThumbnailClick(index, 'reply')}}
|
||||
style={{cursor: "pointer"}}
|
||||
onError={(e) => e.target.src = fallbackImgUrl}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{replyMediaInfo?.type === "audio" ? (
|
||||
<span key={`fta-${index}`} className="file-thumb-reply" style={isReplyThumbnailClicked[index] ? {} : {width: replyDisplayWidth, height: replyDisplayHeight}}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -386,6 +386,7 @@ const Thread = () => {
|
||||
...prevPublishCommentOptions,
|
||||
author: {
|
||||
displayName: nameRef.current.value || undefined,
|
||||
address: account?.author.address,
|
||||
},
|
||||
content: commentRef.current.value || undefined,
|
||||
link: linkRef.current.value || undefined,
|
||||
@@ -423,7 +424,7 @@ const Thread = () => {
|
||||
signer,
|
||||
author: {
|
||||
...prevPublishCommentOptions.author,
|
||||
address: signer?.address
|
||||
address: signer?.address,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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