Files
5chan/src/components/views/Thread.jsx
T

1888 lines
95 KiB
React
Raw Normal View History

2023-06-03 16:33:59 +02:00
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2023-05-25 17:48:55 +02:00
import { createPortal } from 'react-dom';
2023-04-21 10:47:39 +02:00
import { Helmet } from 'react-helmet-async';
2023-03-20 17:08:05 +01:00
import { Link, useNavigate, useParams } from 'react-router-dom';
2023-05-22 18:56:34 +02:00
import { confirmAlert } from 'react-confirm-alert';
2023-03-23 15:49:57 +01:00
import { Tooltip } from 'react-tooltip';
2023-05-22 18:56:34 +02:00
import { useAccount, useAccountComments, useComment, usePublishComment, usePublishCommentEdit, useSubplebbit } from '@plebbit/plebbit-react-hooks';
2023-04-25 14:34:48 +02:00
import { flattenCommentsPages } from '@plebbit/plebbit-react-hooks/dist/lib/utils'
2023-03-25 14:54:04 +01:00
import { debounce } from 'lodash';
2023-04-08 13:27:17 +02:00
import useGeneralStore from '../../hooks/stores/useGeneralStore';
2023-05-24 12:15:43 +02:00
import { Container, NavBar, Header, Break, PostForm, PostFormTable, PostMenu } from '../styled/views/Board.styled';
import { ReplyFormLink, TopBar, BottomBar, BoardForm, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled';
2023-05-25 17:48:55 +02:00
import { PostMenuCatalog } from '../styled/views/Catalog.styled';
2023-05-24 12:15:43 +02:00
import EditModal from '../modals/EditModal';
2023-05-31 15:44:15 +02:00
import EditLabel from '../EditLabel';
2023-03-19 18:30:07 +01:00
import ImageBanner from '../ImageBanner';
2023-05-24 12:15:43 +02:00
import ModerationModal from '../modals/ModerationModal';
2023-05-06 15:40:30 +02:00
import OfflineIndicator from '../OfflineIndicator';
2023-06-15 14:17:33 +02:00
import PendingLabel from '../PendingLabel';
2023-03-29 13:37:36 +02:00
import Post from '../Post';
2023-03-19 18:30:07 +01:00
import PostLoader from '../PostLoader';
2023-06-03 16:33:59 +02:00
import PostOnHover from '../PostOnHover';
2023-06-01 10:43:17 +02:00
import StateLabel from '../StateLabel';
2023-05-24 12:15:43 +02:00
import ReplyModal from '../modals/ReplyModal';
import SettingsModal from '../modals/SettingsModal';
2023-04-17 10:37:58 +02:00
import findShortParentCid from '../../utils/findShortParentCid';
2023-03-23 15:49:57 +01:00
import getCommentMediaInfo from '../../utils/getCommentMediaInfo';
import getDate from '../../utils/getDate';
2023-04-20 17:20:26 +02:00
import handleAddressClick from '../../utils/handleAddressClick';
2023-04-29 17:44:27 +02:00
import handleImageClick from '../../utils/handleImageClick';
2023-04-13 17:08:35 +02:00
import handleQuoteClick from '../../utils/handleQuoteClick';
2023-06-01 21:41:48 +02:00
import handleQuoteHover from '../../utils/handleQuoteHover';
2023-03-20 17:08:05 +01:00
import handleStyleChange from '../../utils/handleStyleChange';
2023-06-01 21:41:48 +02:00
import removeHighlight from '../../utils/removeHighlight';
2023-06-16 13:48:40 +02:00
import useAnonMode from '../../hooks/useAnonMode';
2023-03-20 17:08:05 +01:00
import useClickForm from '../../hooks/useClickForm';
2023-04-04 14:42:22 +02:00
import useError from '../../hooks/useError';
2023-05-09 15:58:19 +02:00
import useStateString from '../../hooks/useStateString';
2023-04-24 20:00:11 +00:00
import packageJson from '../../../package.json'
2023-05-18 22:32:32 +02:00
import useSuccess from '../../hooks/useSuccess';
2023-04-24 20:00:11 +00:00
const {version} = packageJson
2023-02-12 21:20:08 +01:00
2023-03-02 21:51:06 +01:00
2023-03-20 17:08:05 +01:00
const Thread = () => {
2023-03-13 14:29:34 +01:00
const {
2023-06-16 13:48:40 +02:00
anonymousMode,
2023-03-20 17:08:05 +01:00
captchaResponse, setCaptchaResponse,
2023-04-10 21:34:37 +02:00
setChallengesArray,
2023-03-20 17:08:05 +01:00
defaultSubplebbits,
editedComment,
2023-05-26 10:10:51 +02:00
setIsAuthorDelete,
setIsAuthorEdit,
2023-04-09 20:55:55 +02:00
setIsCaptchaOpen,
2023-05-19 18:25:01 +02:00
isModerationOpen, setIsModerationOpen,
2023-03-20 17:08:05 +01:00
isSettingsOpen, setIsSettingsOpen,
2023-05-19 18:25:01 +02:00
setModeratingCommentCid,
setResolveCaptchaPromise,
2023-04-24 16:39:20 +02:00
setPendingComment,
2023-04-25 14:34:48 +02:00
setPendingCommentIndex,
2023-05-14 22:25:55 +02:00
setSelectedAddress,
2023-04-12 21:56:25 +02:00
setSelectedParentCid,
setSelectedShortCid,
2023-03-13 14:29:34 +01:00
selectedStyle,
2023-03-20 17:08:05 +01:00
selectedThread, setSelectedThread,
selectedTitle, setSelectedTitle,
showPostForm,
2023-04-09 20:55:55 +02:00
showPostFormLink,
2023-04-08 13:27:17 +02:00
} = useGeneralStore(state => state);
2023-03-20 17:08:05 +01:00
2023-05-18 16:59:51 +02:00
const account = useAccount();
const navigate = useNavigate();
const handleClickForm = useClickForm();
2023-06-14 16:24:25 +02:00
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
2023-06-07 21:24:17 +02:00
2023-04-03 15:08:45 +02:00
const nameRef = useRef();
const commentRef = useRef();
const linkRef = useRef();
2023-05-18 16:59:51 +02:00
const threadMenuRefs = useRef({});
const replyMenuRefs = useRef({});
2023-05-25 17:48:55 +02:00
const postMenuRef = useRef(null);
const postMenuCatalogRef = useRef(null);
2023-06-03 17:22:39 +02:00
const backlinkRefs = useRef({});
const quoteRefs = useRef({});
2023-06-03 16:33:59 +02:00
const postOnHoverRef = useRef(null);
2023-06-16 13:48:40 +02:00
const backlinkRefsMobile = useRef({});
const quoteRefsMobile = useRef({});
2023-04-03 15:08:45 +02:00
2023-05-10 10:00:08 +02:00
const [triggerPublishComment, setTriggerPublishComment] = useState(false);
2023-05-22 18:56:34 +02:00
const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false);
2023-05-21 16:36:03 +02:00
const [deletePost, setDeletePost] = useState(false);
2023-03-17 14:35:03 +01:00
const [isReplyOpen, setIsReplyOpen] = useState(false);
2023-05-22 18:56:34 +02:00
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [originalCommentContent, setOriginalCommentContent] = useState(null);
2023-02-24 20:57:51 +01:00
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true);
2023-05-18 16:59:51 +02:00
const [isImageSearchOpen, setIsImageSearchOpen] = useState(false);
2023-05-18 22:32:32 +02:00
const [isModerator, setIsModerator] = useState(false);
2023-05-22 18:56:34 +02:00
const [commentCid, setCommentCid] = useState(null);
2023-05-25 17:48:55 +02:00
const [menuPosition, setMenuPosition] = useState({top: 0, left: 0});
const [openMenuCid, setOpenMenuCid] = useState(null);
2023-06-02 13:50:58 +02:00
const [outOfViewCid, setOutOfViewCid] = useState(null);
2023-06-03 16:33:59 +02:00
const [outOfViewPosition, setOutOfViewPosition] = useState({top: 0, left: 0});
const [postOnHoverHeight, setPostOnHoverHeight] = useState(0);
2023-06-16 13:48:40 +02:00
const [executeAnonMode, setExecuteAnonMode] = useState(false);
useAnonMode(selectedThread, anonymousMode && executeAnonMode);
2023-05-18 22:32:32 +02:00
2023-03-25 15:34:10 +01:00
const comment = useComment({commentCid: selectedThread});
2023-03-02 18:19:08 +01:00
const { subplebbitAddress, threadCid } = useParams();
2023-05-14 22:25:55 +02:00
const subplebbit = useSubplebbit({subplebbitAddress: comment.subplebbitAddress});
const selectedAddress = subplebbit.address;
2023-05-15 04:44:33 +00:00
const stateString = useStateString(comment);
2023-05-09 15:58:19 +02:00
2023-05-10 10:00:08 +02:00
const commentMediaInfo = getCommentMediaInfo(comment);
const fallbackImgUrl = "assets/filedeleted-res.gif";
2023-05-18 22:32:32 +02:00
useEffect(() => {
if (subplebbit.roles !== undefined) {
2023-05-22 13:16:16 +02:00
const role = subplebbit.roles[account?.author?.address]?.role;
2023-05-19 21:05:51 +02:00
if (role === 'moderator' || role === 'admin' || role === 'owner') {
setIsModerator(true);
} else {
setIsModerator(false);
}
2023-05-18 22:32:32 +02:00
}
2023-05-19 21:05:51 +02:00
}, [account?.author.address, subplebbit.roles]);
2023-05-18 22:32:32 +02:00
2023-05-25 17:48:55 +02:00
const handleOptionClick = () => {
setOpenMenuCid(null);
2023-05-18 22:32:32 +02:00
};
2023-05-25 21:03:17 +02:00
const handleOutsideClick = useCallback((e) => {
2023-05-25 17:48:55 +02:00
if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) {
setOpenMenuCid(null);
}
2023-05-25 21:03:17 +02:00
}, [openMenuCid, postMenuRef, postMenuCatalogRef]);
2023-05-25 17:48:55 +02:00
useEffect(() => {
if (openMenuCid !== null) {
document.addEventListener('click', handleOutsideClick);
} else {
document.removeEventListener('click', handleOutsideClick);
}
return () => {
document.removeEventListener('click', handleOutsideClick);
};
2023-05-25 21:03:17 +02:00
}, [openMenuCid, handleOutsideClick]);
2023-05-25 17:48:55 +02:00
2023-05-15 17:17:36 +02:00
useEffect(() => {
window.scrollTo(0, 0);
}, []);
2023-05-14 22:25:55 +02:00
useEffect(() => {
2023-05-15 09:42:37 +02:00
if ( comment.state === "failed" && selectedAddress === undefined) {
navigate('/404');
2023-05-14 22:25:55 +02:00
}
2023-05-15 09:42:37 +02:00
}, [selectedAddress, navigate, comment.state]);
2023-05-14 22:25:55 +02:00
2023-05-10 10:00:08 +02:00
2023-05-09 15:58:19 +02:00
const errorString = useMemo(() => {
if (comment?.state === 'failed') {
let errorString = 'Failed fetching thread.'
if (comment.error) {
errorString += `: ${comment.error.toString().slice(0, 300)}`
}
return errorString
}
}, [comment?.state, comment?.error])
2023-05-10 10:00:08 +02:00
2023-05-09 15:58:19 +02:00
useEffect(() => {
2023-06-14 16:24:25 +02:00
if (errorString) {
setNewErrorMessage(errorString);
2023-05-09 15:58:19 +02:00
}
2023-06-14 16:24:25 +02:00
}, [errorString, setNewErrorMessage]);
2023-05-09 15:58:19 +02:00
2023-04-24 16:39:20 +02:00
2023-04-25 14:34:48 +02:00
const flattenedReplies = useMemo(() =>
flattenCommentsPages(comment.replies), [comment.replies]
);
const filter = useMemo(() => ({
parentCids: [
selectedThread || 'n/a', ...flattenedReplies.map(reply => reply.cid)
]
2023-04-27 11:42:51 +02:00
}), [flattenedReplies, selectedThread]);
2023-04-25 14:34:48 +02:00
const { accountComments } = useAccountComments({filter});
const accountRepliesNotYetInCommentReplies = useMemo(() => {
const commentReplyCids = new Set(flattenedReplies.map(reply => reply.cid))
return accountComments.filter(accountReply => !commentReplyCids.has(accountReply.cid))
}, [flattenedReplies, accountComments]);
const sortedReplies = useMemo(() => [
...accountRepliesNotYetInCommentReplies, ...flattenedReplies
].sort((a, b) => a.timestamp - b.timestamp
), [accountRepliesNotYetInCommentReplies, flattenedReplies]);
2023-04-15 21:08:16 +02:00
2023-03-08 09:49:42 +01:00
// temporary title from JSON, gets subplebbitAddress and threadCid from URL
2023-03-02 18:19:08 +01:00
useEffect(() => {
setSelectedAddress(subplebbitAddress);
setSelectedThread(threadCid);
const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress);
if (selectedSubplebbit) {
setSelectedTitle(selectedSubplebbit.title);
}
2023-04-27 11:42:51 +02:00
}, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits, setSelectedThread, threadCid]);
2023-02-12 21:20:08 +01:00
2023-03-08 09:49:42 +01:00
// mobile navbar scroll effect
2023-02-24 20:57:51 +01:00
useEffect(() => {
2023-03-25 14:54:04 +01:00
const debouncedHandleScroll = debounce(() => {
2023-05-21 18:12:33 +02:00
const currentScrollPos = window.scrollY;
2023-02-24 20:57:51 +01:00
setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10);
setPrevScrollPos(currentScrollPos);
2023-03-25 14:54:04 +01:00
}, 50);
window.addEventListener('scroll', debouncedHandleScroll);
return () => window.removeEventListener('scroll', debouncedHandleScroll);
2023-02-24 20:57:51 +01:00
}, [prevScrollPos, visible]);
2023-03-02 21:51:06 +01:00
2023-03-20 17:08:05 +01:00
2023-03-12 10:27:59 +01:00
const onChallengeVerification = (challengeVerification) => {
if (challengeVerification.challengeSuccess === true) {
2023-05-18 22:32:32 +02:00
if (challengeVerification.publication?.cid !== undefined) {
2023-06-14 16:24:25 +02:00
return;
2023-05-18 22:32:32 +02:00
} else {
2023-06-14 16:24:25 +02:00
setNewSuccessMessage('Challenge Success');
2023-05-18 22:32:32 +02:00
}
} else if (challengeVerification.challengeSuccess === false) {
2023-06-15 15:50:13 +02:00
setNewErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors});
2023-03-12 10:27:59 +01:00
}
};
2023-03-12 10:27:59 +01:00
const onChallenge = async (challenges, comment) => {
2023-04-10 21:34:37 +02:00
setPendingComment(comment);
2023-03-12 10:27:59 +01:00
let challengeAnswers = [];
2023-04-10 21:34:37 +02:00
2023-03-12 10:27:59 +01:00
try {
challengeAnswers = await getChallengeAnswersFromUser(challenges)
}
catch (error) {
2023-06-14 16:24:25 +02:00
setNewErrorMessage(error);
2023-03-12 10:27:59 +01:00
}
if (challengeAnswers) {
await comment.publishChallengeAnswers(challengeAnswers)
}
2023-04-03 15:08:45 +02:00
};
2023-04-23 15:14:49 +02:00
useEffect(() => {
setPublishCommentOptions((prevPublishCommentOptions) => ({
...prevPublishCommentOptions,
2023-06-14 17:07:00 +02:00
subplebbitAddress: comment.subplebbitAddress,
2023-04-23 15:14:49 +02:00
}));
2023-06-14 17:07:00 +02:00
}, [comment.subplebbitAddress]);
2023-04-23 15:14:49 +02:00
const [publishCommentOptions, setPublishCommentOptions] = useState({
2023-06-14 17:07:00 +02:00
subplebbitAddress: comment.subplebbitAddress,
onChallenge,
2023-04-22 17:37:53 +02:00
onChallengeVerification,
onError: (error) => {
2023-06-14 16:24:25 +02:00
setNewErrorMessage(error);
},
});
2023-04-12 14:02:35 +02:00
const { publishComment, index } = usePublishComment(publishCommentOptions);
useEffect(() => {
if (index !== undefined) {
2023-04-24 15:48:30 +02:00
setPendingCommentIndex(index);
2023-04-12 14:02:35 +02:00
}
2023-04-27 11:42:51 +02:00
}, [index, setPendingCommentIndex]);
2023-04-12 14:02:35 +02:00
2023-04-27 12:36:21 +02:00
const resetFields = useCallback(() => {
2023-04-25 22:07:33 +02:00
if (nameRef.current) {
nameRef.current.value = '';
}
if (commentRef.current) {
commentRef.current.value = '';
}
if (linkRef.current) {
linkRef.current.value = '';
}
2023-04-27 12:36:21 +02:00
}, []);
2023-04-12 14:02:35 +02:00
const handleSubmit = async (event) => {
event.preventDefault();
2023-05-12 21:32:33 +02:00
if (
commentRef.current.value === "" &&
linkRef.current.value === ""
) {
2023-06-14 16:24:25 +02:00
setNewErrorMessage("Please enter a comment or link.");
2023-05-12 21:32:33 +02:00
return;
}
setPublishCommentOptions((prevPublishCommentOptions) => ({
...prevPublishCommentOptions,
2023-04-16 18:07:20 +02:00
author: {
displayName: nameRef.current.value || undefined,
},
content: commentRef.current.value || undefined,
link: linkRef.current.value || undefined,
2023-04-12 14:58:53 +02:00
parentCid: selectedThread,
}));
2023-04-27 12:36:21 +02:00
setTriggerPublishComment(true);
2023-06-16 13:48:40 +02:00
setExecuteAnonMode(false);
};
2023-06-16 13:48:40 +02:00
useEffect(() => {
if (anonymousMode) {
setExecuteAnonMode(true);
}
}, [anonymousMode, selectedThread]);
useEffect(() => {
2023-05-12 21:32:33 +02:00
if (publishCommentOptions && triggerPublishComment) {
(async () => {
2023-06-07 18:26:03 +02:00
await publishComment();
resetFields();
})();
2023-06-07 18:26:03 +02:00
setTriggerPublishComment(false);
}
2023-06-07 18:26:03 +02:00
}, [publishCommentOptions, triggerPublishComment, publishComment, resetFields]);
2023-04-03 15:08:45 +02:00
2023-03-12 10:27:59 +01:00
const getChallengeAnswersFromUser = async (challenges) => {
2023-04-10 21:34:37 +02:00
setChallengesArray(challenges);
2023-03-12 10:27:59 +01:00
return new Promise((resolve, reject) => {
const imageString = challenges?.challenges[0].challenge;
const imageSource = `data:image/png;base64,${imageString}`;
const challengeImg = new Image();
challengeImg.src = imageSource;
challengeImg.onload = () => {
2023-03-12 22:07:15 +01:00
setIsCaptchaOpen(true);
2023-03-12 10:27:59 +01:00
2023-04-22 17:37:53 +02:00
const handleKeyDown = async (event) => {
2023-03-12 10:27:59 +01:00
if (event.key === 'Enter') {
2023-04-22 17:37:53 +02:00
const currentCaptchaResponse = captchaResponse;
resolve(currentCaptchaResponse);
2023-03-12 22:07:15 +01:00
setIsCaptchaOpen(false);
2023-03-12 10:27:59 +01:00
document.removeEventListener('keydown', handleKeyDown);
2023-04-03 15:08:45 +02:00
event.preventDefault();
2023-03-12 10:27:59 +01:00
}
};
2023-03-12 22:07:15 +01:00
setCaptchaResponse('');
2023-03-12 10:27:59 +01:00
document.addEventListener('keydown', handleKeyDown);
setResolveCaptchaPromise(resolve);
2023-03-12 10:27:59 +01:00
};
challengeImg.onerror = () => {
2023-06-14 16:24:25 +02:00
reject(setNewErrorMessage('Could not load challenges'));
2023-03-12 10:27:59 +01:00
};
});
};
2023-05-18 22:32:32 +02:00
2023-05-22 18:56:34 +02:00
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({
commentCid: commentCid,
content: editedComment || undefined,
2023-06-14 17:07:00 +02:00
subplebbitAddress: comment.subplebbitAddress,
2023-05-22 18:56:34 +02:00
onChallenge,
onChallengeVerification,
onError: (error) => {
2023-06-14 16:24:25 +02:00
setNewErrorMessage(error);
2023-05-22 18:56:34 +02:00
},
});
2023-06-14 16:24:25 +02:00
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
2023-05-22 18:56:34 +02:00
2023-06-14 17:07:00 +02:00
const handleAuthorDeleteClick = (comment) => {
handleOptionClick(comment.cid);
2023-05-22 18:56:34 +02:00
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={() => {
2023-05-26 10:10:51 +02:00
setIsAuthorDelete(true);
setIsAuthorEdit(false);
2023-06-14 17:07:00 +02:00
setCommentCid(comment.cid);
setPublishCommentEditOptions(prevOptions => ({
...prevOptions,
deleted: true,
}));
setTriggerPublishCommentEdit(true);
onClose();
}}
>
Yes
</button>
</div>
2023-05-22 18:56:34 +02:00
</div>
</AuthorDeleteAlert>
);
}
});
};
const handleAuthorEditClick = (comment) => {
handleOptionClick(comment.cid);
2023-05-26 10:10:51 +02:00
setIsAuthorEdit(true);
setIsAuthorDelete(false);
2023-05-22 18:56:34 +02:00
setCommentCid(comment.cid);
setOriginalCommentContent(comment.content);
setIsEditModalOpen(true);
}
useEffect(() => {
setPublishCommentEditOptions((prevOptions) => ({
...prevOptions,
commentCid: commentCid,
content: editedComment || undefined,
2023-05-22 18:56:34 +02:00
}));
}, [commentCid, editedComment]);
useEffect(() => {
if (editedComment !== '') {
setTriggerPublishCommentEdit(true);
}
}, [editedComment]);
2023-05-22 18:56:34 +02:00
useEffect(() => {
if (publishCommentEditOptions && triggerPublishCommentEdit) {
(async () => {
2023-06-07 18:26:03 +02:00
await publishCommentEdit();
setTriggerPublishCommentEdit(false);
2023-05-22 18:56:34 +02:00
})();
}
2023-06-07 18:26:03 +02:00
}, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]);
2023-05-22 18:56:34 +02:00
2023-03-08 09:49:42 +01:00
// mobile navbar board select functionality
2023-02-27 18:16:47 +01:00
const handleSelectChange = (event) => {
const selected = event.target.value;
2023-05-03 11:01:41 +02:00
if (selected === 'subscriptions') {
2023-05-07 17:22:55 +02:00
navigate(`/p/subscriptions`);
2023-05-03 11:01:41 +02:00
return;
2023-05-16 16:37:50 +02:00
} else if (selected === 'all') {
navigate(`/p/all`);
return;
2023-05-03 11:01:41 +02:00
}
2023-02-27 18:16:47 +01:00
const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title;
setSelectedTitle(selectedTitle);
setSelectedAddress(selected);
2023-04-08 22:30:44 +02:00
navigate(`/p/${selected}`);
2023-02-12 21:20:08 +01:00
};
2023-03-02 21:51:06 +01:00
2023-06-03 16:33:59 +02:00
useLayoutEffect(() => {
if (postOnHoverRef.current) {
2023-06-10 12:28:21 +02:00
const rect = postOnHoverRef.current.getBoundingClientRect();
setPostOnHoverHeight(rect.height);
2023-06-03 16:33:59 +02:00
}
}, [outOfViewCid]);
2023-02-12 21:20:08 +01:00
return (
2023-04-21 10:47:39 +02:00
<>
<Helmet>
<title>
{(
2023-04-23 20:36:18 +02:00
(comment.content?.slice(0, 40) ?? comment.title?.slice(0, 40) ?? "Thread") + " - "
2023-04-21 10:47:39 +02:00
+ (selectedTitle ? selectedTitle : selectedAddress)
+ " - plebchan"
)}
</title>
</Helmet>
<Container>
<ReplyModal
selectedStyle={selectedStyle}
isOpen={isReplyOpen}
closeModal={() => setIsReplyOpen(false)} />
<SettingsModal
selectedStyle={selectedStyle}
isOpen={isSettingsOpen}
closeModal={() => setIsSettingsOpen(false)} />
2023-05-19 18:25:01 +02:00
<ModerationModal
selectedStyle={selectedStyle}
isOpen={isModerationOpen}
2023-05-30 11:24:10 +02:00
closeModal={() => {setIsModerationOpen(false); setDeletePost(false)}}
2023-05-21 16:36:03 +02:00
deletePost={deletePost} />
2023-05-22 18:56:34 +02:00
<EditModal
selectedStyle={selectedStyle}
isOpen={isEditModalOpen}
closeModal={() => setIsEditModalOpen(false)}
originalCommentContent={originalCommentContent} />
2023-04-21 10:47:39 +02:00
<NavBar selectedStyle={selectedStyle}>
<>
2023-05-01 18:41:10 +02:00
<span className="boardList">
2023-05-04 15:50:35 +02:00
[
2023-05-16 16:37:50 +02:00
<Link to={`/p/all`}>All</Link>
 / 
2023-05-07 17:22:55 +02:00
<Link to={`/p/subscriptions`}>Subscriptions</Link>
2023-05-04 15:50:35 +02:00
]&nbsp;[
2023-05-01 18:41:10 +02:00
{defaultSubplebbits.map((subplebbit, index) => (
2023-04-21 10:47:39 +02:00
<span className="boardList" key={`span-${subplebbit.address}`}>
2023-05-01 18:41:10 +02:00
{index === 0 ? null : "\u00a0"}
<Link to={`/p/${subplebbit.address}`} key={`a-${subplebbit.address}`} onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
2023-04-21 10:47:39 +02:00
}}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</Link>
2023-05-01 18:41:10 +02:00
{index !== defaultSubplebbits.length - 1 ? " /" : null}
2023-04-21 10:47:39 +02:00
</span>
))}
2023-05-04 15:50:35 +02:00
]
</span>
2023-05-01 21:58:19 +02:00
<span className="nav">
2023-05-01 18:41:10 +02:00
[
2023-05-27 12:33:25 +02:00
<span id="button-span" style={{cursor: 'pointer'}} onClick={
2023-04-30 17:37:19 +02:00
() => alert(
2023-05-03 18:20:18 +02:00
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
2023-04-30 17:37:19 +02:00
)
2023-05-27 12:33:25 +02:00
}>Create Board</span>
2023-04-30 17:37:19 +02:00
]
2023-02-12 21:20:08 +01:00
[
2023-04-21 10:47:39 +02:00
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
]
[
2023-05-27 12:55:16 +02:00
<Link to="/">Home</Link>
2023-04-21 10:47:39 +02:00
]
2023-02-12 21:20:08 +01:00
</span>
2023-04-21 10:47:39 +02:00
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
2023-05-27 11:04:23 +02:00
<div className="nav-container">
<div className="board-select">
<strong>Board</strong>
&nbsp;
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
<option value="all">All</option>
<option value="subscriptions">Subscriptions</option>
{defaultSubplebbits.map(subplebbit => (
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
))}
</select> 
2023-05-29 15:48:09 +02:00
<span id="button-span" style={{cursor: 'pointer'}} onClick={
2023-04-30 17:37:19 +02:00
() => alert(
2023-05-03 18:20:18 +02:00
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
2023-04-30 17:37:19 +02:00
)
2023-05-27 11:04:23 +02:00
}>Create Board</span>
</div>
<div className="page-jump">
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp;
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
</div>
2023-04-21 10:47:39 +02:00
</div>
</div>
<div id="separator-mobile">&nbsp;</div>
<div id="separator-mobile">&nbsp;</div>
</>
</NavBar>
<Header selectedStyle={selectedStyle}>
<>
<div className="banner">
<ImageBanner />
</div>
<>
<div className="board-title">{selectedTitle}</div>
2023-05-09 16:36:49 +02:00
<div className="board-address">p/{selectedAddress}
<OfflineIndicator
address={selectedAddress}
className="offline"
tooltipPlace="top" />
</div>
2023-04-21 10:47:39 +02:00
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<PostForm selectedStyle={selectedStyle} name="post" action="" method="post" enctype="multipart/form-data">
<ReplyFormLink id="post-form-link" showReplyFormLink={showPostFormLink} selectedStyle={selectedStyle} >
<div id="return-button-mobile">
<span className="btn-wrap" onClick={()=> {window.scrollTo(0, 0)}}>
2023-04-21 10:47:39 +02:00
<Link to={`/p/${selectedAddress}`}>Return</Link>
</span>
</div>
<div id="catalog-button-mobile">
<span className="btn-wrap">
<Link to={`/p/${selectedAddress}/catalog`} onClick={()=> {window.scrollTo(0, 0)}}>Catalog</Link>
2023-04-21 10:47:39 +02:00
</span>
</div>
<div id="bottom-button-mobile">
<span className="btn-wrap">
2023-04-26 16:22:24 +02:00
<span style={{cursor: 'pointer'}} onClick={() => window.scrollTo(0, document.body.scrollHeight)} onMouseOver={(event) => event.target.style.cursor='pointer'}>Bottom</span>
2023-04-21 10:47:39 +02:00
</span>
</div>
<div id="post-form-link-desktop">
[
2023-04-26 15:17:26 +02:00
<Link to={`/p/${subplebbitAddress}/c/${selectedThread}/post`} onClick={() => {handleClickForm(); setSelectedShortCid(comment.shortCid)}} onMouseOver={(event) => event.target.style.cursor='pointer'}>Post a Reply</Link>
2023-04-21 10:47:39 +02:00
]
</div>
<div id="post-form-link-mobile">
<span className="btn-wrap">
2023-04-26 15:17:26 +02:00
<Link to={`/p/${subplebbitAddress}/c/${selectedThread}/post`} onClick={() => {handleClickForm(); setSelectedShortCid(comment.shortCid)}} onMouseOver={(event) => event.target.style.cursor='pointer'}>Post a Reply</Link>
2023-04-21 10:47:39 +02:00
</span>
</div>
</ReplyFormLink>
<PostFormTable id="post-form" showPostForm={showPostForm} selectedStyle={selectedStyle} className="post-form">
<tbody>
<tr data-type="Name">
<td id="td-name">Name</td>
<td>
2023-05-26 21:09:45 +02:00
{account && account.author && account.author.displayName ? (
<input name="name" type="text" tabIndex={1} value={account.author?.displayName} ref={nameRef} disabled />
) : (
<input name="name" type="text" placeholder="Anonymous" tabIndex={1} ref={nameRef} />
)}
2023-04-21 10:47:39 +02:00
<input id="post-button" type="submit" value="Post" tabIndex={6}
onClick={handleSubmit} />
</td>
</tr>
<tr data-type="Comment">
<td>Comment</td>
<td>
<textarea name="com" cols="48" rows="4" tabIndex={4} wrap="soft" ref={commentRef} />
</td>
</tr>
<tr data-type="File">
<td>Embed File</td>
<td>
<input name="embed" type="text" tabIndex={7} placeholder="Paste link" ref={linkRef} />
<button id="t-help" type="button" onClick={
() => alert("- Embedding media is optional, posts can be text-only. \n- A CAPTCHA challenge will appear after posting. \n- The CAPTCHA is case-sensitive.")}
data-tip="Help"
>?</button>
</td>
</tr>
</tbody>
</PostFormTable>
</PostForm>
<TopBar selectedStyle={selectedStyle}>
<hr />
<span className="style-changer">
Style:
 
<select id="style-selector" onChange={handleStyleChange} value={selectedStyle}>
<option value="Yotsuba">Yotsuba</option>
<option value="Yotsuba-B">Yotsuba B</option>
<option value="Futaba">Futaba</option>
<option value="Burichan">Burichan</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Photon">Photon</option>
</select>
</span>
<span className="return-button" id="return-button-desktop">
2023-02-23 16:45:29 +01:00
[
2023-04-21 10:47:39 +02:00
<Link to={`/p/${selectedAddress}`}>Return</Link>
2023-02-23 16:45:29 +01:00
]
2023-02-12 21:20:08 +01:00
</span>
2023-04-21 10:47:39 +02:00
<span className="return-button catalog-button" id="catalog-button-desktop">
2023-02-12 21:20:08 +01:00
[
2023-04-21 10:47:39 +02:00
<Link to={`/p/${selectedAddress}/catalog`}>Catalog</Link>
2023-02-12 21:20:08 +01:00
]
2023-04-21 10:47:39 +02:00
</span>
<span className="return-button catalog-button" id="bottom-button-desktop">
[
2023-04-26 16:22:24 +02:00
<span id="button" style={{cursor: 'pointer'}} onClick={() => window.scrollTo(0, document.body.scrollHeight)}
2023-04-21 10:47:39 +02:00
onMouseOver={(event) => event.target.style.cursor='pointer'}
2023-04-26 16:22:24 +02:00
onTouchStart={() => window.scrollTo(0, document.body.scrollHeight)}>Bottom</span>
2023-04-21 10:47:39 +02:00
]
</span>
{comment ? (
comment.replyCount !== undefined ? (
comment.replyCount > 0 ? (
comment.replyCount === 1 ? (
<span className="reply-stat">{comment.replyCount} reply</span>
) : (
<span className="reply-stat">{comment.replyCount} replies</span>
)
2023-03-18 22:05:46 +01:00
) : (
2023-04-21 10:47:39 +02:00
<span className="reply-stat">No replies yet</span>
2023-03-18 22:05:46 +01:00
)
) : (
<span className={stateString ? "reply-stat ellipsis" : ""}>{stateString}</span>
2023-03-18 22:05:46 +01:00
)
2023-02-21 14:49:00 +01:00
) : (
<span className={stateString ? "reply-stat ellipsis" : ""}>{stateString}</span>
2023-04-21 10:47:39 +02:00
)}
<hr />
</TopBar>
<Tooltip id="tooltip" className="tooltip" />
<BoardForm selectedStyle={selectedStyle}>
{comment !== undefined ? (
comment.state === "fetching-ipfs" ? (
<PostLoader />
) : (
2023-04-21 10:47:39 +02:00
<>
<div className="thread">
<div className="op-container">
<div className="post op op-desktop">
<div className="post-info">
{commentMediaInfo?.url ? (
<div key={`f-${comment.cid}`} className="file" style={{marginBottom: "5px"}}>
<div key={`ft-${comment.cid}`} className="file-text">
Link:&nbsp;
2023-04-25 21:30:10 +02:00
<a key={`fa-${comment.cid}`} href={commentMediaInfo.url}
target="_blank" rel="noreferrer">{
2023-04-21 10:47:39 +02:00
commentMediaInfo?.url.length > 30 ?
commentMediaInfo?.url.slice(0, 30) + "(...)" :
commentMediaInfo?.url
}</a>&nbsp;({commentMediaInfo?.type})
2023-03-08 09:49:42 +01:00
</div>
2023-04-21 10:47:39 +02:00
{commentMediaInfo?.type === "webpage" ? (
2023-04-30 13:32:52 +02:00
<div key="enlarge" className="img-container">
<span key={`fta-${comment.cid}`} className="file-thumb">
{comment.thumbnailUrl ? (
<img key={`fti-${comment.cid}`}
2023-04-30 16:23:32 +02:00
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
2023-04-30 13:32:52 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
</span>
</div>
) : null}
{commentMediaInfo?.type === "image" ? (
<div key="enlarge" className="img-container">
<span key={`fta-${comment.cid}`} className="file-thumb">
<img key={`fti-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
2023-04-29 17:44:27 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-30 13:32:52 +02:00
</span>
</div>
2023-04-21 10:47:39 +02:00
) : null}
{commentMediaInfo?.type === "video" ? (
<span key={`fta-${comment.cid}`} className="file-thumb">
2023-04-29 17:44:27 +02:00
<video controls key={`fti-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-21 10:47:39 +02:00
</span>
) : null}
{commentMediaInfo?.type === "audio" ? (
<span key={`fta-${comment.cid}`} className="file-thumb">
2023-04-29 17:44:27 +02:00
<audio controls key={`fti-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-21 10:47:39 +02:00
</span>
) : null}
</div>
) : null}
<span className="name-block">
{comment.title ? (
comment.title.length > 75 ?
2023-02-28 16:34:33 +01:00
<>
2023-04-21 10:47:39 +02:00
<span key={`q-${comment.cid}`} className="title"
2023-03-25 21:38:52 +01:00
data-tooltip-id="tooltip"
2023-02-28 16:34:33 +01:00
data-tooltip-content={comment.title}
data-tooltip-place="top">
2023-04-21 10:47:39 +02:00
{comment.title.slice(0, 75) + " (...)"}
2023-02-28 16:34:33 +01:00
</span>
</>
2023-04-21 10:47:39 +02:00
: <span key={`q-${comment.cid}`} className="title">
{comment.title}
</span>)
: null}
2023-02-27 12:35:23 +01:00
&nbsp;
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName
? comment?.author?.displayName.length > 20
2023-04-21 10:47:39 +02:00
? <>
<span key={`n-${comment.cid}`} className="name"
data-tooltip-id="tooltip"
2023-05-22 13:16:16 +02:00
data-tooltip-content={comment?.author?.displayName}
2023-04-21 10:47:39 +02:00
data-tooltip-place="top">
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName.slice(0, 20) + " (...)"}
2023-04-21 10:47:39 +02:00
</span>
</>
: <span key={`n-${comment.cid}`} className="name">
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName}</span>
2023-04-21 10:47:39 +02:00
: <span key={`n-${comment.cid}`} className="name">
Anonymous</span>}
&nbsp;
&nbsp;
<span className="poster-address address-desktop"
id="reply-button" style={{cursor: "pointer"}}
2023-05-22 13:16:16 +02:00
onClick={() => handleAddressClick(comment?.author?.shortAddress)}>
2023-04-21 10:47:39 +02:00
(u/
<span key={`pa-${comment.cid}`} className="poster-address">
2023-05-22 13:16:16 +02:00
{comment?.author?.shortAddress}
2023-04-21 10:47:39 +02:00
</span>)
</span>
&nbsp;
<span className="date-time" data-utc="data">{getDate(comment?.timestamp)}</span>
&nbsp;
<span className="post-number post-number-desktop">
2023-05-03 11:01:41 +02:00
<span style={{cursor: 'pointer'}}>c/</span>
2023-04-21 10:47:39 +02:00
<button id="reply-button" style={{ all: 'unset', cursor: 'pointer' }} onClick={() => {
setIsReplyOpen(true); setSelectedParentCid(comment.cid); setSelectedShortCid(comment.shortCid);
}} title="Reply to this post">{comment.shortCid}</button>
2023-05-19 08:20:18 +02:00
</span>&nbsp;
{comment.pinned ? (
<>
&nbsp;
<img src="assets/sticky.gif" alt="Sticky" title="Sticky"
style={{marginBottom: "-3px"}} />
</>
) : null}
{comment.locked ? (
<>
&nbsp;
<img src="assets/closed.gif" alt="Closed" title="Closed"
style={{marginBottom: "-3px"}} />
</>
) : null}
2023-05-18 16:59:51 +02:00
<PostMenu
key={`pmb-${index}`}
title="Post menu"
2023-05-25 17:48:55 +02:00
ref={el => {
threadMenuRefs.current[comment.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button'
2023-05-25 17:48:55 +02:00
rotated={openMenuCid === comment.cid}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect();
2023-05-25 17:48:55 +02:00
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
setOpenMenuCid(prevCid => (prevCid === comment.cid ? null : comment.cid));
}}
>
</PostMenu>
2023-05-25 17:48:55 +02:00
{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-${comment.cid}`}
style={{ display: openMenuCid === comment.cid ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<li onClick={() => handleOptionClick(comment.cid)}>Hide thread</li>
2023-05-26 10:10:51 +02:00
{comment.author?.shortAddress === account?.author.shortAddress ? (
2023-05-25 17:48:55 +02:00
<>
<li onClick={() => handleAuthorEditClick(comment)}>Edit post</li>
2023-06-14 17:07:00 +02:00
<li onClick={() => handleAuthorDeleteClick(comment)}>Delete post</li>
2023-05-25 17:48:55 +02:00
</>
) : null}
{isModerator ? (
<>
2023-05-26 10:10:51 +02:00
{comment.author?.shortAddress === account?.author.shortAddress ? (
2023-05-25 17:48:55 +02:00
null
) : (
<li onClick={() => {
setModeratingCommentCid(comment.cid)
setIsModerationOpen(true);
handleOptionClick(comment.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(comment.cid)
setIsModerationOpen(true);
handleOptionClick(comment.cid);
}}>
2023-05-25 17:48:55 +02:00
Mod tools
</li>
2023-05-25 17:48:55 +02:00
</>
) : null}
{(commentMediaInfo && (
commentMediaInfo.type === 'image' ||
(commentMediaInfo.type === 'webpage' &&
commentMediaInfo.thumbnail))) ? (
<li
onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search »
2023-05-26 20:54:23 +02:00
<ul className="dropdown-menu post-menu-catalog"
2023-05-25 17:48:55 +02:00
style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick(comment.cid)}>
<a
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Google</a>
</li>
<li onClick={() => handleOptionClick(comment.cid)}>
<a
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Yandex</a>
</li>
<li onClick={() => handleOptionClick(comment.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
)}
2023-06-03 16:33:59 +02:00
<div key={`bi-${index}`} id="backlink-id" className="backlink">
{comment.replies?.pages?.topAll.comments
2023-04-21 10:47:39 +02:00
.sort((a, b) => a.timestamp - b.timestamp)
2023-04-22 15:12:26 +02:00
.map((reply, index) => (
2023-06-03 16:33:59 +02:00
<div key={`div-${index}`} style={{display: 'inline-block'}}
ref={el => {
2023-06-03 17:22:39 +02:00
backlinkRefs.current[reply.cid] = el;
2023-06-03 16:33:59 +02:00
}}>
<Link key={`ql-${index}`}
to={() => {}} className="quote-link"
onClick={(event) => handleQuoteClick(reply, null, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, null, () => {
setOutOfViewCid(reply.cid);
2023-06-03 17:22:39 +02:00
const rect = backlinkRefs.current[reply.cid].getBoundingClientRect();
2023-06-11 13:59:56 +02:00
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-10 12:28:21 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 13:07:16 +02:00
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5
2023-06-10 12:28:21 +02:00
});
} else {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 12:28:21 +02:00
left: rect.left + rect.width + 5,
2023-06-11 13:59:56 +02:00
maxWidth: window.innerWidth - rect.left - rect.width - 5
2023-06-10 12:28:21 +02:00
});
}
2023-06-03 16:33:59 +02:00
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}>
c/{reply.shortCid}</Link>
2023-04-21 10:47:39 +02:00
&nbsp;
</div>
))
}
</div>
2023-02-27 12:35:23 +01:00
</span>
2023-04-21 10:47:39 +02:00
<blockquote key={`blockquote-${comment.cid}`}>
<Post content={comment.content} comment={comment} key={`post-${comment.cid}`} />
2023-05-31 15:44:15 +02:00
<EditLabel key={`edit-label-thread-${index}`}
commentCid={comment.cid}
className="ttl"/>
2023-06-01 10:43:17 +02:00
<StateLabel key={`state-label-thread-${index}`}
commentIndex={comment.index}
2023-06-01 10:43:17 +02:00
className="ttl ellipsis"/>
2023-04-21 10:47:39 +02:00
</blockquote>
2023-02-27 12:35:23 +01:00
</div>
</div>
</div>
2023-05-20 17:58:11 +02:00
{comment.replyCount === undefined ? <PostLoader /> : null}
2023-04-25 14:34:48 +02:00
{sortedReplies.map((reply, index) => {
2023-04-21 10:47:39 +02:00
const replyMediaInfo = getCommentMediaInfo(reply);
2023-04-29 12:47:30 +02:00
const fallbackImgUrl = "assets/filedeleted-res.gif";
2023-04-21 10:47:39 +02:00
const shortParentCid = findShortParentCid(reply.parentCid, comment);
return (
2023-04-22 15:12:26 +02:00
<div key={`pc-${index}`} className="reply-container">
<div key={`sa-${index}`} className="side-arrows">{'>>'}</div>
<div key={`pr-${index}`} className="post-reply post-reply-desktop">
<div key={`pi-${index}`} className="post-info">
2023-04-21 10:47:39 +02:00
&nbsp;
2023-04-22 15:12:26 +02:00
<span key={`nb-${index}`} className="nameblock">
2023-04-21 10:47:39 +02:00
{reply.author?.displayName
? reply.author?.displayName.length > 20
? <>
2023-04-22 15:12:26 +02:00
<span key={`mob-n-${index}`} className="name"
2023-04-21 10:47:39 +02:00
data-tooltip-id="tooltip"
data-tooltip-content={reply.author?.displayName}
data-tooltip-place="top">
{reply.author?.displayName.slice(0, 20) + " (...)"}
</span>
</>
2023-04-22 15:12:26 +02:00
: <span key={`mob-n-${index}`} className="name">
2023-04-21 10:47:39 +02:00
{reply.author?.displayName ?? reply.displayName}</span>
2023-04-22 15:12:26 +02:00
: <span key={`mob-n-${index}`} className="name">
2023-04-21 10:47:39 +02:00
Anonymous</span>}
&nbsp;
2023-04-22 15:12:26 +02:00
<span key={`pa-${index}`} className="poster-address address-desktop"
2023-04-21 10:47:39 +02:00
id="reply-button" style={{cursor: "pointer"}}
onClick={() => handleAddressClick(reply.author?.shortAddress)}
>
(u/
2023-04-22 15:43:38 +02:00
{reply.author?.shortAddress ?
(
<span key={`mob-ha-${index}`}>
{reply.author?.shortAddress}
</span>
) : (
<span key={`mob-ha-${index}`}
>
2023-05-29 18:11:19 +02:00
{account?.author?.shortAddress}
2023-04-22 15:43:38 +02:00
</span>
)
}
)
2023-02-28 16:34:33 +01:00
</span>
2023-04-21 10:47:39 +02:00
</span>
&nbsp;
2023-04-22 15:12:26 +02:00
<span key={`dt-${index}`} className="date-time" data-utc="data">{getDate(reply?.timestamp)}</span>
2023-04-21 10:47:39 +02:00
&nbsp;
2023-04-26 16:22:24 +02:00
<span style={{cursor: 'pointer'}} key={`pn-${index}`} className="post-number post-number-desktop">
2023-05-03 11:01:41 +02:00
<span key={`pl1-${index}`}>c/</span>
2023-04-22 15:43:38 +02:00
{reply.shortCid ? (
<button id="reply-button" style={{ all: 'unset', cursor: 'pointer' }} key={`pl2-${index}`} onClick={() => {
setIsReplyOpen(true); setSelectedParentCid(reply.cid); setSelectedShortCid(reply.shortCid);
}} title="Reply to this post">{reply.shortCid}</button>
) : (
2023-06-15 14:17:33 +02:00
<PendingLabel key="pending" commentIndex={reply.index} />
2023-04-22 15:43:38 +02:00
)}
2023-04-21 10:47:39 +02:00
</span>&nbsp;
2023-05-18 16:59:51 +02:00
<PostMenu
key={`pmb-${index}`}
title="Post menu"
2023-05-25 17:48:55 +02:00
ref={el => {
replyMenuRefs.current[reply.cid] = el;
postMenuRef.current = el;
}}
2023-05-18 16:59:51 +02:00
className='post-menu-button'
2023-05-25 17:48:55 +02:00
rotated={openMenuCid === reply.cid}
onClick={(event) => {
event.stopPropagation();
2023-05-18 16:59:51 +02:00
const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect();
2023-05-25 17:48:55 +02:00
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid));
}}
2023-05-18 16:59:51 +02:00
>
</PostMenu>
2023-05-25 17:48:55 +02:00
{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-reply post-menu-reply-${reply.cid}`}
style={{ display: openMenuCid === reply.cid ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<li onClick={() => handleOptionClick(reply.cid)}>Hide post</li>
{reply.author.shortAddress === account?.author.shortAddress ? (
<>
<li onClick={() => handleAuthorEditClick(reply)}>Edit post</li>
2023-06-14 17:07:00 +02:00
<li onClick={() => handleAuthorDeleteClick(reply)}>Delete post</li>
2023-05-25 17:48:55 +02:00
</>
) : null}
{isModerator ? (
<>
{reply.author.shortAddress === account?.author.shortAddress ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(reply.cid)
setIsModerationOpen(true);
handleOptionClick(reply.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
2023-05-21 16:36:03 +02:00
setModeratingCommentCid(reply.cid)
setIsModerationOpen(true);
handleOptionClick(reply.cid);
}}>
2023-05-25 17:48:55 +02:00
Mod tools
2023-05-21 16:36:03 +02:00
</li>
2023-05-25 17:48:55 +02:00
</>
) : null}
{(replyMediaInfo && (
replyMediaInfo.type === 'image' ||
(replyMediaInfo.type === 'webpage' &&
replyMediaInfo.thumbnail))) ? (
<li
onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search »
2023-05-26 20:54:23 +02:00
<ul className="dropdown-menu post-menu-catalog"
2023-05-25 17:48:55 +02:00
style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick(reply.cid)}>
<a
href={`https://lens.google.com/uploadbyurl?url=${replyMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Google</a>
</li>
<li onClick={() => handleOptionClick(reply.cid)}>
<a
href={`https://yandex.com/images/search?url=${replyMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Yandex</a>
</li>
<li onClick={() => handleOptionClick(reply.cid)}>
<a
href={`https://saucenao.com/search.php?url=${replyMediaInfo.url}`}
target="_blank" rel="noreferrer"
>SauceNAO</a>
</li>
</ul>
</li>
) : null
}
</ul>
</div>
</PostMenuCatalog>, document.body
)}
2023-06-03 16:33:59 +02:00
<div key={`bi-${index}`} id="backlink-id" className="backlink">
2023-04-21 10:47:39 +02:00
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
2023-04-22 15:12:26 +02:00
.map((reply, index) => (
2023-06-03 16:33:59 +02:00
<div key={`div-${index}`} style={{display: 'inline-block'}}
ref={el => {
2023-06-03 17:22:39 +02:00
backlinkRefs.current[reply.cid] = el;
2023-06-03 16:33:59 +02:00
}}>
<Link key={`ql-${index}`}
to={() => {}} className="quote-link"
onClick={(event) => handleQuoteClick(reply, null, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, null, () => {
2023-06-11 13:59:56 +02:00
setOutOfViewCid(reply.cid);
const rect = backlinkRefs.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-10 12:28:21 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 13:07:16 +02:00
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5
2023-06-10 12:28:21 +02:00
});
} else {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 12:28:21 +02:00
left: rect.left + rect.width + 5,
2023-06-11 13:59:56 +02:00
maxWidth: window.innerWidth - rect.left - rect.width - 5
2023-06-10 12:28:21 +02:00
});
}
2023-06-03 16:33:59 +02:00
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}>
c/{reply.shortCid}</Link>
2023-04-21 10:47:39 +02:00
&nbsp;
</div>
))
}
</div>
</div>
{replyMediaInfo?.url ? (
2023-04-22 15:12:26 +02:00
<div key={`f-${index}`} className="file"
2023-04-21 10:47:39 +02:00
style={{marginBottom: "5px"}}>
2023-04-22 15:12:26 +02:00
<div key={`ft-${index}`} className="reply-file-text">
2023-04-21 10:47:39 +02:00
Link:&nbsp;
2023-04-25 21:30:10 +02:00
<a key={`fa-${index}`} href={replyMediaInfo.url}
target="_blank" rel="noreferrer">{
2023-04-21 10:47:39 +02:00
replyMediaInfo?.url.length > 30 ?
replyMediaInfo?.url.slice(0, 30) + "(...)" :
replyMediaInfo?.url
}</a>&nbsp;({replyMediaInfo?.type})
</div>
{replyMediaInfo?.type === "webpage" ? (
2023-04-30 13:32:52 +02:00
<div key="enlarge-reply" className="img-container">
<span key={`fta-${index}`} className="file-thumb-reply">
{reply.thumbnailUrl ? (
<img key={`fti-${index}`}
2023-04-30 16:23:32 +02:00
src={replyMediaInfo.thumbnail} alt="thumbnail"
2023-04-30 13:32:52 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
</span>
</div>
) : null}
{replyMediaInfo?.type === "image" ? (
<div key="enlarge-reply" className="img-container">
<span key={`fta-${index}`} className="file-thumb-reply">
2023-04-27 18:06:16 +02:00
<img key={`fti-${index}`}
2023-04-30 13:32:52 +02:00
src={replyMediaInfo.url} alt={replyMediaInfo.type}
2023-04-29 17:44:27 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
2023-04-27 18:06:16 +02:00
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-30 13:32:52 +02:00
</span>
</div>
2023-04-21 10:47:39 +02:00
) : null}
{replyMediaInfo?.type === "video" ? (
2023-04-22 15:12:26 +02:00
<span key={`fta-${index}`} className="file-thumb-reply">
2023-04-29 17:44:27 +02:00
<video controls key={`fti-${index}`}
2023-04-21 10:47:39 +02:00
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
) : null}
{replyMediaInfo?.type === "audio" ? (
2023-04-22 15:12:26 +02:00
<span key={`fta-${index}`} className="file-thumb-reply">
2023-04-29 17:44:27 +02:00
<audio controls key={`fti-${index}`}
2023-04-21 10:47:39 +02:00
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
) : null}
</div>
) : null}
2023-04-22 15:12:26 +02:00
<blockquote key={`pm-${index}`} className="post-message">
2023-06-03 17:22:39 +02:00
<Link to={() => {}} key={`r-pm-${index}`} className="quotelink"
ref={el => {
2023-06-14 10:08:46 +02:00
quoteRefs.current[reply.cid] = el;
2023-04-21 10:47:39 +02:00
}}
2023-06-03 17:22:39 +02:00
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
2023-06-11 12:17:22 +02:00
setOutOfViewCid(reply.parentCid);
2023-06-14 10:08:46 +02:00
const rect = quoteRefs.current[reply.cid].getBoundingClientRect();
2023-06-11 13:59:56 +02:00
const distanceToRight = window.innerWidth - rect.right;
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-11 12:17:22 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-11 12:17:22 +02:00
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5
});
} else {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-11 12:17:22 +02:00
left: rect.left + rect.width + 5,
2023-06-11 13:59:56 +02:00
maxWidth: window.innerWidth - rect.left - rect.width - 5
2023-06-11 12:17:22 +02:00
});
2023-06-03 17:22:39 +02:00
}
});
}}
2023-06-02 13:50:58 +02:00
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
2023-06-03 17:22:39 +02:00
}}>
{`c/${shortParentCid}`}{shortParentCid === comment.shortCid ? " (OP)" : null}
</Link>
2023-04-22 15:12:26 +02:00
<Post content={reply.content} comment={reply} key={`post-${index}`} />
2023-05-31 15:44:15 +02:00
<EditLabel key={`edit-label-reply-${index}`}
commentCid={reply.cid}
className="ttl"/>
2023-06-01 10:43:17 +02:00
<StateLabel key={`state-label-reply-${index}`}
commentIndex={reply.index}
2023-06-01 10:43:17 +02:00
className="ttl ellipsis"/>
2023-04-21 10:47:39 +02:00
</blockquote>
</div>
</div>
)
})
}
</div>
<div className="thread-mobile" key="thread-mobile">
<hr />
<div className="op-container" key="op-container">
<div key={`mob-po-${comment.cid}`} className="post op op-mobile">
<div key={`mob-pi-${comment.cid}`} className="post-info-mobile">
2023-04-26 16:22:24 +02:00
<button style={{ all: 'unset', cursor: 'pointer' }} key={`mob-pb-${comment.cid}`} className="post-menu-button-mobile">...</button>
2023-04-21 10:47:39 +02:00
<span className="name-block-mobile">
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName
? comment?.author?.displayName.length > 15
2023-04-21 10:47:39 +02:00
? <>
<span key={`mob-n-${comment.cid}`} className="name-mobile"
data-tooltip-id="tooltip"
2023-05-22 13:16:16 +02:00
data-tooltip-content={comment?.author?.displayName}
2023-04-21 10:47:39 +02:00
data-tooltip-place="top">
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName.slice(0, 15) + " (...)"}
2023-04-21 10:47:39 +02:00
</span>
</>
: <span key={`mob-n-${comment.cid}`} className="name-mobile">
2023-05-22 13:16:16 +02:00
{comment?.author?.displayName}</span>
2023-04-21 10:47:39 +02:00
: <span key={`mob-n-${comment.cid}`} className="name-mobile">
2023-02-28 16:34:33 +01:00
Anonymous</span>}
2023-02-27 12:35:23 +01:00
&nbsp;
2023-04-21 10:47:39 +02:00
<span key={`mob-pa-${comment.cid}`} className="poster-address-mobile address-mobile"
2023-04-20 17:20:26 +02:00
id="reply-button" style={{cursor: "pointer"}}
2023-05-22 13:16:16 +02:00
onClick={() => handleAddressClick(comment?.author?.shortAddress)}
2023-04-20 17:20:26 +02:00
>
2023-03-05 14:15:51 +01:00
(u/
2023-04-21 10:47:39 +02:00
<span key={`mob-ha-${comment.cid}`} className="highlight-address-mobile">
2023-05-22 13:16:16 +02:00
{comment?.author?.shortAddress}
2023-03-29 14:36:41 +02:00
</span>
2023-04-21 10:47:39 +02:00
)&nbsp;
2023-02-27 12:35:23 +01:00
</span>
2023-04-21 10:47:39 +02:00
<br key={`mob-br1-${comment.cid}`} />
{comment.title ? (
comment.title.length > 30 ?
<>
<span key={`mob-t-${comment.cid}`} className="subject-mobile"
data-tooltip-id="tooltip"
data-tooltip-content={comment.title}
data-tooltip-place="top">
{comment.title.slice(0, 30) + " (...)"}
</span>
</>
: <span key={`mob-t-${comment.cid}`} className="subject-mobile">
{comment.title}
</span>) : null}
2023-02-27 12:35:23 +01:00
</span>
2023-04-21 10:47:39 +02:00
<span key={`mob-dt-${comment.cid}`} className="date-time-mobile post-number-mobile">
{getDate(comment?.timestamp)}
&nbsp;
2023-05-03 11:01:41 +02:00
<span key={`mob-no-${comment.cid}`}>c/</span>
<span id="reply-button" style={{cursor: 'pointer'}} key={`mob-no2-${comment.cid}`} title="Reply to this post"
onClick={() => {
2023-04-21 10:47:39 +02:00
setIsReplyOpen(true); setSelectedParentCid(comment.cid); setSelectedShortCid(comment.shortCid);
2023-05-03 11:01:41 +02:00
}}>{comment.shortCid}</span>
2023-02-27 12:35:23 +01:00
</span>
</div>
2023-04-21 10:47:39 +02:00
{commentMediaInfo?.url ? (
commentMediaInfo.type === "webpage" ? (
<div key={`mob-f-${comment.cid}`} className="file-mobile">
2023-04-30 13:32:52 +02:00
<div key="enlarge-reply-mob" className="img-container">
<span key={`mob-ft${comment.cid}`} className="file-thumb-mobile">
{comment.thumbnailUrl ? (
<img key={`mob-img-${comment.cid}`}
2023-04-30 16:23:32 +02:00
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
2023-04-30 13:32:52 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
<div key={`mob-fi-${comment.cid}`} className="file-info-mobile">{commentMediaInfo.type}</div>
</span>
</div>
2023-04-16 12:54:51 +02:00
</div>
2023-04-21 10:47:39 +02:00
) : commentMediaInfo.type === "image" ? (
<div key={`mob-f-${comment.cid}`} className="file-mobile">
2023-04-30 13:32:52 +02:00
<div key="enlarge-reply-mob" className="img-container">
<span key={`mob-ft${comment.cid}`} className="file-thumb-mobile">
<img key={`mob-img-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
<div key={`mob-fi-${comment.cid}`} className="file-info-mobile">{commentMediaInfo.type}</div>
</span>
</div>
2023-04-21 10:47:39 +02:00
</div>
) : commentMediaInfo.type === "video" ? (
<div key={`mob-f-${comment.cid}`} className="file-mobile">
<span key={`mob-ft${comment.cid}`} className="file-thumb-mobile">
2023-04-29 17:54:25 +02:00
<video controls key={`mob-img-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-21 10:47:39 +02:00
<div key={`mob-fi-${comment.cid}`} className="file-info-mobile">{commentMediaInfo.type}</div>
</span>
</div>
) : commentMediaInfo.type === "audio" ? (
<div key={`mob-f-${comment.cid}`} className="file-mobile">
<span key={`mob-ft${comment.cid}`} className="file-thumb-mobile">
2023-04-29 17:54:25 +02:00
<audio controls key={`mob-img-${comment.cid}`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-21 10:47:39 +02:00
<div key={`mob-fi-${comment.cid}`} className="file-info-mobile">{commentMediaInfo.type}</div>
</span>
</div>
) : null
2023-04-16 12:54:51 +02:00
) : null}
2023-04-21 10:47:39 +02:00
<blockquote key={`mob-bq-${comment.cid}`} className="post-message-mobile">
{comment.content ? (
<>
2023-05-31 15:44:15 +02:00
<Post content={comment.content} comment={comment} key={`post-mobile-${comment.cid}`} />
<EditLabel key={`edit-label-thread-mob-${index}`}
commentCid={comment.cid}
className="ttl"/>
2023-06-01 10:43:17 +02:00
<StateLabel key={`state-label-thread-mob-${index}`}
commentIndex={comment.index}
2023-06-01 10:43:17 +02:00
className="ttl ellipsis"/>
2023-04-21 10:47:39 +02:00
</>
) : null}
</blockquote>
2023-02-27 12:35:23 +01:00
</div>
</div>
2023-04-21 10:47:39 +02:00
{comment.replyCount === undefined ? <PostLoader /> : null}
2023-04-25 14:34:48 +02:00
{sortedReplies.map((reply, index) => {
2023-04-23 13:37:09 +02:00
const replyMediaInfo = getCommentMediaInfo(reply);
2023-04-21 10:47:39 +02:00
const shortParentCid = findShortParentCid(reply.parentCid, comment);
return (
2023-04-25 22:13:23 +02:00
<div key={`mob-rc-${index}`} className="reply-container">
2023-04-22 15:12:26 +02:00
<div key={`mob-pr-${index}`} className="post-reply post-reply-mobile">
<div key={`mob-pi-${index}`} className="post-info-mobile">
2023-04-21 10:47:39 +02:00
<button className="post-menu-button-mobile" title="Post menu" style={{ all: 'unset', cursor: 'pointer' }}>...</button>
2023-04-22 15:12:26 +02:00
<span key={`mob-nb-${index}`} className="name-block-mobile">
2023-04-21 10:47:39 +02:00
{reply.author?.displayName
? reply.author?.displayName.length > 12
? <>
2023-04-22 15:12:26 +02:00
<span key={`mob-n-${index}`} className="name-mobile"
2023-04-21 10:47:39 +02:00
data-tooltip-id="tooltip"
data-tooltip-content={reply.author?.displayName}
data-tooltip-place="top">
{reply.author?.displayName.slice(0, 12) + " (...)"}
</span>
</>
2023-04-22 15:12:26 +02:00
: <span key={`mob-n-${index}`} className="name-mobile">
2023-04-21 10:47:39 +02:00
{reply.author?.displayName}</span>
2023-04-22 15:12:26 +02:00
: <span key={`mob-n-${index}`} className="name-mobile">
2023-04-21 10:47:39 +02:00
Anonymous</span>}
&nbsp;
2023-04-22 15:12:26 +02:00
<span key={`mob-pa-${index}`} className="poster-address-mobile address-mobile"
2023-04-21 10:47:39 +02:00
id="reply-button" style={{cursor: "pointer"}}
onClick={() => handleAddressClick(reply.author?.shortAddress)}
>
(u/
2023-04-22 15:43:38 +02:00
{reply.author?.shortAddress ?
(
<span key={`mob-ha-${index}`} className="highlight-address-mobile">
{reply.author?.shortAddress}
</span>
) : (
2023-05-31 15:14:12 +02:00
<span key={`mob-ha-${index}`}>
{account?.author?.shortAddress}
2023-04-22 15:43:38 +02:00
</span>
)
}
2023-04-21 10:47:39 +02:00
)
</span>
2023-04-22 15:12:26 +02:00
<br key={`mob-br-${index}`} />
2023-04-21 10:47:39 +02:00
</span>
2023-04-22 15:12:26 +02:00
<span key={`mob-dt-${index}`} className="date-time-mobile post-number-mobile">
2023-04-21 10:47:39 +02:00
{getDate(reply?.timestamp)}&nbsp;
2023-05-03 11:01:41 +02:00
<span key={`mob-pl1-${index}`}>c/</span>
2023-04-22 15:43:38 +02:00
{reply.shortCid ? (
<button id="reply-button" style={{ all: 'unset', cursor: 'pointer' }} key={`mob-pl2-${index}`} onClick={() => {
setIsReplyOpen(true); setSelectedParentCid(reply.cid); setSelectedShortCid(reply.shortCid);
}} title="Reply to this post">{reply.shortCid}</button>
) : (
2023-06-15 14:17:33 +02:00
<PendingLabel key="pending-mob" commentIndex={reply.index} />
2023-04-22 15:43:38 +02:00
)}
2023-04-21 10:47:39 +02:00
</span>
</div>
2023-04-23 13:37:09 +02:00
{reply.link ? (
<div key={`mob-f-${reply.cid}`} className="file-mobile">
{replyMediaInfo?.url ? (
replyMediaInfo.type === "webpage" ? (
2023-04-30 13:32:52 +02:00
<div key="enlarge-reply-mob" className="img-container">
2023-04-23 13:37:09 +02:00
<span key={`mob-ft${reply.cid}`} className="file-thumb-mobile">
2023-04-27 17:59:14 +02:00
{reply.thumbnailUrl ? (
2023-04-29 17:54:25 +02:00
<img key={`mob-img-${reply.cid}`}
2023-04-30 16:23:32 +02:00
src={replyMediaInfo.thumbnail} alt={replyMediaInfo.type}
2023-04-29 17:54:25 +02:00
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-27 17:59:14 +02:00
) : null}
2023-04-23 13:37:09 +02:00
<div key={`mob-fi-${reply.cid}`} className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
2023-04-30 13:32:52 +02:00
</div>
2023-04-23 13:37:09 +02:00
) : replyMediaInfo.type === "image" ? (
2023-04-30 13:32:52 +02:00
<div key="enlarge-reply-mob" className="img-container">
2023-04-23 13:37:09 +02:00
<span key={`mob-ft${reply.cid}`} className="file-thumb-mobile">
2023-04-29 17:54:25 +02:00
<img key={`mob-img-${reply.cid}`}
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onClick={handleImageClick}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-23 13:37:09 +02:00
<div key={`mob-fi-${reply.cid}`} className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
2023-04-30 13:32:52 +02:00
</div>
2023-04-23 13:37:09 +02:00
) : replyMediaInfo.type === "video" ? (
<span key={`mob-ft${reply.cid}`} className="file-thumb-mobile">
2023-04-29 17:54:25 +02:00
<video key={`fti-${reply.cid}`}
src={replyMediaInfo.url} alt={replyMediaInfo.type}
style={{ pointerEvents: "none" }}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-23 13:37:09 +02:00
<div key={`mob-fi-${reply.cid}`} className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
) : replyMediaInfo.type === "audio" ? (
<span key={`mob-ft${reply.cid}`} className="file-thumb-mobile">
2023-04-29 17:54:25 +02:00
<audio key={`mob-img-${reply.cid}`}
src={replyMediaInfo.url} alt={replyMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
2023-04-23 13:37:09 +02:00
<div key={`mob-fi-${reply.cid}`} className="file-info-mobile">{replyMediaInfo.type}</div>
</span>
) : null
) : null}
</div>
) : null}
2023-04-22 15:12:26 +02:00
<blockquote key={`mob-pm-${index}`} className="post-message-mobile">
2023-04-26 16:22:24 +02:00
<span style={{cursor: 'pointer'}} key={`mob-ql-${index}`} className="quotelink-mobile"
2023-06-10 18:52:45 +02:00
ref={el => {
2023-06-14 10:08:46 +02:00
quoteRefsMobile.current[reply.cid] = el;
2023-06-10 18:52:45 +02:00
}}
onClick={(event) => handleQuoteClick(reply, shortParentCid, event)}
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, shortParentCid, () => {
2023-06-11 12:17:22 +02:00
setOutOfViewCid(reply.parentCid);
2023-06-14 10:08:46 +02:00
const rect = quoteRefsMobile.current[reply.cid].getBoundingClientRect();
2023-06-11 12:17:22 +02:00
const distanceToRight = window.innerWidth - rect.right;
2023-06-11 13:59:56 +02:00
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-11 12:17:22 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-11 12:17:22 +02:00
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5
});
} else {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-11 12:17:22 +02:00
left: rect.left + rect.width + 5,
2023-06-11 13:59:56 +02:00
maxWidth: window.innerWidth - rect.left - rect.width - 5
2023-06-11 12:17:22 +02:00
});
2023-06-10 18:52:45 +02:00
}
});
}}
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
}}>
2023-04-21 10:47:39 +02:00
{`c/${shortParentCid}`}{shortParentCid === comment.shortCid ? " (OP)" : null}
2023-04-26 16:22:24 +02:00
</span>
2023-04-22 15:12:26 +02:00
<Post content={reply.content} comment={reply} key={`post-mobile-${index}`} />
2023-05-31 15:44:15 +02:00
<EditLabel key={`edit-label-reply-mob-${index}`}
commentCid={reply.cid}
className="ttl"/>
2023-06-01 10:43:17 +02:00
<StateLabel key={`state-label-reply-mob-${index}`}
commentIndex={reply.index}
2023-06-01 10:43:17 +02:00
className="ttl ellipsis"/>
2023-04-21 10:47:39 +02:00
</blockquote>
{reply.replyCount > 0 ? (
2023-04-22 15:12:26 +02:00
<div key={`back-mob-${index}`} className='backlink backlink-mobile'>
2023-04-21 10:47:39 +02:00
{reply.replies?.pages?.topAll.comments
.sort((a, b) => a.timestamp - b.timestamp)
2023-04-22 15:12:26 +02:00
.map((reply, index) => (
2023-06-10 18:52:45 +02:00
<div key={`div-back${index}`} style={{display: 'inline-block'}}
ref={el => {
backlinkRefsMobile.current[reply.cid] = el;
}}>
2023-04-26 16:22:24 +02:00
<span style={{cursor: 'pointer'}} key={`ql-${index}`} className="quote-link"
2023-06-01 21:41:48 +02:00
onClick={(event) => handleQuoteClick(reply, reply.shortCid, event)}
2023-06-10 18:52:45 +02:00
onMouseOver={(event) => {
event.stopPropagation();
handleQuoteHover(reply, reply.shortCid, () => {
setOutOfViewCid(reply.cid)
const rect = backlinkRefsMobile.current[reply.cid].getBoundingClientRect();
const distanceToRight = window.innerWidth - rect.right;
2023-06-11 13:59:56 +02:00
const distanceToTop = rect.top;
const distanceToBottom = window.innerHeight - rect.bottom;
let top;
2023-06-10 18:52:45 +02:00
2023-06-11 13:59:56 +02:00
if (distanceToTop < postOnHoverHeight / 2) {
top = window.scrollY - 5;
} else if (distanceToBottom < postOnHoverHeight / 2) {
top = window.scrollY - postOnHoverHeight + window.innerHeight - 10;
} else {
top = rect.top + window.scrollY - postOnHoverHeight / 2;
}
2023-06-10 18:52:45 +02:00
if (distanceToRight < 200) {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 18:52:45 +02:00
right: window.innerWidth - rect.left - 10,
maxWidth: rect.left - 5
});
} else {
setOutOfViewPosition({
2023-06-11 13:59:56 +02:00
top,
2023-06-10 18:52:45 +02:00
left: rect.left + rect.width + 5,
2023-06-11 13:59:56 +02:00
maxWidth: window.innerWidth - rect.left - rect.width - 5
2023-06-10 18:52:45 +02:00
});
}
});
}}
2023-06-02 13:50:58 +02:00
onMouseLeave={() => {
removeHighlight();
setOutOfViewCid(null);
2023-06-10 18:52:45 +02:00
}} >
2023-04-26 16:22:24 +02:00
c/{reply.shortCid}</span>
2023-04-21 10:47:39 +02:00
&nbsp;
</div>
))}
</div>
) : null}
</div>
</div>
2023-03-18 22:14:31 +01:00
)
2023-04-21 10:47:39 +02:00
})
}
</div>
<BottomBar selectedStyle={selectedStyle}>
<div id="bottombar-desktop">
<hr />
<span className="bottom-bar-return">
[
2023-04-08 22:30:44 +02:00
<Link to={`/p/${selectedAddress}`}>Return</Link>
2023-04-21 10:47:39 +02:00
]
2023-02-24 18:13:02 +01:00
</span>
2023-04-21 10:47:39 +02:00
<span className="bottom-bar-catalog">
[
2023-04-08 22:30:44 +02:00
<Link to={`/p/${selectedAddress}/catalog`}>Catalog</Link>
2023-04-21 10:47:39 +02:00
]
2023-02-24 18:13:02 +01:00
</span>
2023-04-21 10:47:39 +02:00
<span className="bottom-bar-top">
[
2023-04-26 16:22:24 +02:00
<span id="button" onClick={() => window.scrollTo(0, 0)}
2023-04-01 13:59:54 +02:00
onMouseOver={(event) => event.target.style.cursor='pointer'}
2023-04-26 16:22:24 +02:00
onTouchStart={() => window.scrollTo(0, 0)}>Top</span>
2023-04-21 10:47:39 +02:00
]
2023-02-27 12:35:23 +01:00
</span>
2023-04-21 10:47:39 +02:00
<span className="quickreply-button">
[
2023-04-26 16:22:24 +02:00
<span id="button" onClick={() => {setIsReplyOpen(true); setSelectedParentCid(comment.cid); setSelectedShortCid(comment.shortCid);}} onMouseOver={(event) => event.target.style.cursor='pointer'}>Post a Reply</span>
2023-04-21 10:47:39 +02:00
]
</span>
{comment ? (
comment.replyCount !== undefined ? (
comment.replyCount > 0 ? (
comment.replyCount === 1 ? (
<span className="reply-stat">{comment.replyCount} reply</span>
) : (
<span className="reply-stat">{comment.replyCount} replies</span>
)
) : (
<span className="reply-stat">No replies yet</span>
)
) : (
<span className={stateString ? "reply-stat ellipsis" : ""}>{stateString}</span>
)
2023-04-21 10:47:39 +02:00
) : (
<span className={stateString ? "reply-stat ellipsis" : ""}>{stateString}</span>
2023-04-21 10:47:39 +02:00
)}
<hr />
</div>
</BottomBar>
2023-04-21 18:16:05 +02:00
{comment.replyCount > 2 ? (
2023-04-21 17:13:13 +02:00
<div id="bottombar-mobile">
2023-04-24 13:09:59 +02:00
<hr style={{marginTop: "30px"}} />
<TopBar selectedStyle={selectedStyle} style={{marginTop: "-5px"}}>
2023-04-21 17:13:13 +02:00
<span className="style-changer">
Style:
 
<select id="style-selector" onChange={handleStyleChange} value={selectedStyle}>
<option value="Yotsuba">Yotsuba</option>
<option value="Yotsuba-B">Yotsuba B</option>
<option value="Futaba">Futaba</option>
<option value="Burichan">Burichan</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Photon">Photon</option>
</select>
</span>
{comment ? (
comment.replyCount !== undefined ? (
comment.replyCount > 0 ? (
comment.replyCount === 1 ? (
<span className="reply-stat">{comment.replyCount} reply</span>
) : (
<span className="reply-stat">{comment.replyCount} replies</span>
)
) : (
<span className="reply-stat">No replies yet</span>
)
2023-04-21 10:47:39 +02:00
) : (
2023-04-21 17:13:13 +02:00
<span className="reply-stat">Loading...</span>
2023-04-21 10:47:39 +02:00
)
) : (
2023-04-21 17:13:13 +02:00
null
)}
<hr />
</TopBar>
<ReplyFormLink id="post-form-link" selectedStyle={selectedStyle} >
<div id="post-form-link-mobile" className="post-button-mobile">
<span className="btn-wrap">
2023-04-26 16:22:24 +02:00
<span style={{cursor: 'pointer'}} onClick={() => {setIsReplyOpen(true); setSelectedParentCid(comment.cid); setSelectedShortCid(comment.shortCid);}} onMouseOver={(event) => event.target.style.cursor='pointer'}>Post a Reply</span>
2023-04-21 17:13:13 +02:00
</span>
</div>
<div id="btns-container">
<div id="return-button-mobile">
<span className="btn-wrap">
<Link to={`/p/${selectedAddress}`}>Return</Link>
</span>
</div>
<div id="catalog-button-mobile" style={{paddingRight: "2px"}}>
<span className="btn-wrap">
<Link to={`/p/${selectedAddress}/catalog`}>Catalog</Link>
</span>
</div>
<span className="bottom-bar-top">
<span className="btn-wrap">
2023-04-26 16:22:24 +02:00
<span onClick={() => window.scrollTo(0, 0)}
2023-04-21 17:13:13 +02:00
onMouseOver={(event) => event.target.style.cursor='pointer'}
onTouchStart={() => window.scrollTo(0, 0)}
2023-04-26 16:22:24 +02:00
style={{cursor: 'pointer', marginRight: "10px", marginLeft: "10px"}}
>Top</span>
2023-04-21 17:13:13 +02:00
</span>
</span>
</div>
</ReplyFormLink>
2023-04-21 10:47:39 +02:00
</div>
2023-04-21 17:13:13 +02:00
) : (null)}
2023-04-21 10:47:39 +02:00
</>
)) : null}
2023-06-10 12:28:21 +02:00
{outOfViewCid && outOfViewPosition && createPortal(
2023-06-03 16:33:59 +02:00
<div
2023-06-10 12:28:21 +02:00
ref={postOnHoverRef}
2023-06-03 16:33:59 +02:00
style={{
2023-06-10 12:28:21 +02:00
display: "block",
2023-06-03 16:33:59 +02:00
position: "absolute",
2023-06-11 13:59:56 +02:00
top: outOfViewPosition.top,
2023-06-10 13:07:16 +02:00
right: outOfViewPosition.right,
2023-06-11 13:59:56 +02:00
left: outOfViewPosition.left,
2023-06-10 12:28:21 +02:00
maxWidth: outOfViewPosition.maxWidth,
2023-06-03 16:33:59 +02:00
}}
2023-06-10 12:28:21 +02:00
>
<PostOnHover
cid={outOfViewCid}
feed={comment}
/>
</div>
, document.body)}
2023-04-21 10:47:39 +02:00
</BoardForm>
2023-04-24 13:09:59 +02:00
<Footer selectedStyle={selectedStyle}>
<Break id="break" selectedStyle={selectedStyle} style={{
marginTop: "-36px",
width: "100%",
}} />
<Break selectedStyle={selectedStyle} style={{
width: "100%",
}} />
<span className="style-changer" style={{
float: "right",
marginTop: "2px",
}}>
Style:
 
<select id="style-selector" onChange={handleStyleChange} value={selectedStyle}>
<option value="Yotsuba">Yotsuba</option>
<option value="Yotsuba-B">Yotsuba B</option>
<option value="Futaba">Futaba</option>
<option value="Burichan">Burichan</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Photon">Photon</option>
</select>
</span>
<NavBar selectedStyle={selectedStyle} style={{
marginTop: "42px",
}}>
<>
2023-05-01 21:58:19 +02:00
<span className="boardList">
[
2023-05-16 16:37:50 +02:00
<Link to={`/p/all`}>All</Link>
 / 
<Link to={`/p/subscriptions`}>Subscriptions</Link>
2023-05-03 11:01:41 +02:00
]&nbsp;
2023-05-01 21:58:19 +02:00
</span>
{defaultSubplebbits.map((subplebbit, index) => (
<span className="boardList" key={`span-${subplebbit.address}`}>
{index === 0 ? null : "\u00a0"}
<Link to={`/p/${subplebbit.address}`} key={`a-${subplebbit.address}`} onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</Link>
{index !== defaultSubplebbits.length - 1 ? " /" : null}
</span>
))}
<span className="nav">
[
2023-05-29 15:48:09 +02:00
<span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert(
'To create a board, first you have to run a full node.\nYou can run a full node by simply browsing with the plebbit desktop app. After you download it, open it, wait for it loading, then click on "Home" in the top left, then "Create Community".\n\nAfter you create the community, you can go back to plebchan at any time to see it as a board by pasting its address (begins with p/12D3KooW...) in the search bar, which is in the Home.\n\nNote:\n\n- Your community will be online for as long as you leave the app open, because it functions like a server for the community.\n- The longer you leave the app open, the more data you are seeding to the protocol, which helps performance for everybody.\n - All the data in the plebbit protocol is just text, which is extremely lightweight. All media is generated by links, which is text, embedded by the clients.\n\nDownload the plebbit app here: https://github.com/plebbit/plebbit-react/releases\n\nYou can also use a CLI: https://github.com/plebbit/plebbit-cli\n\nRunning boards in the plebchan app is a planned feature.\n\n'
)
}>Create Board</span>
2023-05-01 21:58:19 +02:00
]
2023-04-24 13:09:59 +02:00
[
<Link to={`/p/${selectedAddress}/c/${selectedThread}/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
]
[
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
)}>Home</Link>
]
</span>
</>
</NavBar>
<div id="version">
2023-04-24 20:00:11 +00:00
plebchan v{version}. GPL-2.0
2023-04-24 13:09:59 +02:00
</div>
<div className="footer-links"
2023-04-24 17:53:09 +00:00
style={{
textAlign: "center",
fontSize: "x-small",
fontFamily: "arial",
marginTop: "5px",
marginBottom: "15px",
}}>
<a style={{textDecoration: 'underline'}} href="https://plebbit.com" target="_blank" rel="noopener noreferrer">About</a>
2023-04-24 13:09:59 +02:00
&nbsp;&nbsp;
2023-04-24 20:06:35 +00:00
<a style={{textDecoration: 'underline'}} href="https://github.com/plebbit/plebchan/releases/latest" target="_blank" rel="noopener noreferrer">App</a>
2023-04-24 13:09:59 +02:00
&nbsp;&nbsp;
2023-04-24 20:06:35 +00:00
<a style={{textDecoration: 'underline'}} href="https://twitter.com/plebchan_eth" target="_blank" rel="noopener noreferrer">Twitter</a>
2023-04-24 13:09:59 +02:00
&nbsp;&nbsp;
2023-04-24 17:53:09 +00:00
<a style={{textDecoration: 'underline'}} href="https://t.me/plebbit" target="_blank" rel="noopener noreferrer">Telegram</a>
2023-04-24 13:09:59 +02:00
</div>
</Footer>
2023-04-21 10:47:39 +02:00
</Container>
</>
2023-02-12 21:20:08 +01:00
);
}
export default Thread;