diff --git a/src/app.tsx b/src/app.tsx index 599aa91f..81a67d78 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -33,8 +33,10 @@ const BoardLayout = () => { return ; } - // force rerender of post form when navigating between pages - const key = `${subplebbitAddress}-${location.pathname}`; + // force rerender of post form when navigating between pages, except when opening settings modal in current view + const key = location.pathname.endsWith('/settings') + ? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}` + : `${subplebbitAddress}-${location.pathname}`; return (
diff --git a/src/components/catalog-row/catalog-row.module.css b/src/components/catalog-row/catalog-row.module.css index 5f0e7333..ba5cce64 100644 --- a/src/components/catalog-row/catalog-row.module.css +++ b/src/components/catalog-row/catalog-row.module.css @@ -124,6 +124,7 @@ color: var(--catalog-post-preview-color); font-family: arial, helvetica, sans-serif; font-size: 10pt; + overflow: hidden; } .postPreview .postSubject { diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 1a149f92..556c9860 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; -import { Comment, useAccount, useAuthorAvatar, useComment, useEditedComment } from '@plebbit/plebbit-react-hooks'; +import { Comment, useAccount, useAccountComments, useAuthorAvatar, useComment, useEditedComment } from '@plebbit/plebbit-react-hooks'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import styles from '../../views/post/post.module.css'; import { getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail } from '../../lib/utils/media-utils'; @@ -101,7 +101,7 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }: {!(isDescription || isRules) && ( <> - {isReply && author?.avatar && ( + {author?.avatar && ( @@ -138,7 +138,7 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }: !cid && e.preventDefault()}> c/ - openReplyModal && openReplyModal(cid)}> + openReplyModal && openReplyModal(cid, postCid)}> {shortCid} @@ -240,6 +240,7 @@ const PostMedia = ({ post }: PostProps) => { const PostMessage = ({ post }: PostProps) => { const { cid, content, deleted, edit, original, parentCid, postCid, reason, removed, state, subplebbitAddress } = post || {}; + const { isDescription, isRules } = post || {}; // custom properties, not from api // TODO: commentAuthor is not available outside of editedComment, update when available // const banned = !!post?.commentAuthor?.banExpiresAt; const { t } = useTranslation(); @@ -320,7 +321,11 @@ const PostMessage = ({ post }: PostProps) => { {!isReply && content.length > 1000 && !isInPostView && (
- }} /> + }} + />
)} {!cid && state === 'pending' && stateString !== 'Failed' && ( @@ -378,6 +383,9 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies const visiblelinksCount = useCountLinksInReplies(post, 5); const totalLinksCount = useCountLinksInReplies(post); const replyCount = replies?.length; + const { accountComments } = useAccountComments(); + const isPostInAccountComments = accountComments?.find((comment) => comment.cid === cid); + const repliesCount = pinned ? replyCount : replyCount - 5; const linksCount = pinned ? totalLinksCount : totalLinksCount - visiblelinksCount; const { showOmittedReplies, setShowOmittedReplies } = useShowOmittedReplies(); @@ -434,7 +442,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies )}
)} - {post?.replyCount === undefined && !isInPendingPostView && ( + {post?.replyCount === undefined && !isPostInAccountComments && !isInPendingPostView && ( diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css index 822084b7..82373d42 100644 --- a/src/components/post-form/post-form.module.css +++ b/src/components/post-form/post-form.module.css @@ -89,6 +89,7 @@ .linkType { font-weight: normal; + text-transform: lowercase; } .linkField input[type="text"] { diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index 2da9cd37..a7206323 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { @@ -18,13 +18,17 @@ import { getLinkMediaInfo } from '../../lib/utils/media-utils'; import { isValidURL } from '../../lib/utils/url-utils'; import { isAllView, isDescriptionView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; -import useReply from '../../hooks/use-reply'; +import usePublishReply from '../../hooks/use-publish-reply'; import useChallengesStore from '../../stores/use-challenges-store'; import styles from './post-form.module.css'; import _ from 'lodash'; import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; +import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; +import useAnonMode from '../../hooks/use-anon-mode'; type SubmitState = { + author: any | undefined; + signer: any | undefined; subplebbitAddress: string | undefined; title: string | undefined; content: string | undefined; @@ -38,15 +42,19 @@ type SubmitState = { const { addChallenge } = useChallengesStore.getState(); const useSubmitStore = create((set) => ({ + author: undefined, + signer: undefined, subplebbitAddress: undefined, title: undefined, content: undefined, link: undefined, spoiler: undefined, publishCommentOptions: {}, - setSubmitStore: ({ subplebbitAddress, title, content, link, spoiler }) => + setSubmitStore: ({ author, signer, subplebbitAddress, title, content, link, spoiler }) => set((state) => { const nextState = { ...state }; + if (author !== undefined) nextState.author = author; + if (signer !== undefined) nextState.signer = signer; if (subplebbitAddress !== undefined) nextState.subplebbitAddress = subplebbitAddress; if (title !== undefined) nextState.title = title || undefined; if (content !== undefined) nextState.content = content || undefined; @@ -54,7 +62,13 @@ const useSubmitStore = create((set) => ({ if (spoiler !== undefined) nextState.spoiler = spoiler || undefined; nextState.publishCommentOptions = { - ...nextState, + author: nextState.author, + signer: nextState.signer, + subplebbitAddress: nextState.subplebbitAddress, + title: nextState.title, + content: nextState.content, + link: nextState.link, + spoiler: nextState.spoiler, onChallenge: (...args: any) => addChallenge(args), onChallengeVerification: alertChallengeVerificationFailed, onError: (error: Error) => { @@ -65,19 +79,39 @@ const useSubmitStore = create((set) => ({ }; return nextState; }), - resetSubmitStore: () => set({ subplebbitAddress: undefined, title: undefined, content: undefined, link: undefined, spoiler: undefined, publishCommentOptions: {} }), + resetSubmitStore: () => + set({ + author: undefined, + signer: undefined, + subplebbitAddress: undefined, + title: undefined, + content: undefined, + link: undefined, + spoiler: undefined, + publishCommentOptions: {}, + }), })); export const LinkTypePreviewer = ({ link }: { link: string }) => { const { t } = useTranslation(); const mediaInfo = getLinkMediaInfo(link); - return isValidURL(link) ? mediaInfo?.type : t('invalid_url'); + let type = mediaInfo?.type; + const gifFrameUrl = useFetchGifFirstFrame(mediaInfo?.url); + + if (type === 'gif' && gifFrameUrl !== null) { + type = t('animated_gif'); + } else if (type === 'gif' && gifFrameUrl === null) { + type = t('static_gif'); + } + + return isValidURL(link) ? type : t('invalid_url'); }; -const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { +const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: string }) => { const { t } = useTranslation(); const account = useAccount(); - const { displayName } = account?.author || {}; + const author = account?.author || {}; + const { displayName } = author || {}; const [url, setUrl] = useState(''); const { title, content, link, publishCommentOptions, setSubmitStore, resetSubmitStore } = useSubmitStore(); const { index, publishComment } = usePublishComment(publishCommentOptions); @@ -92,6 +126,10 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { const subscriptions = account?.subscriptions || []; const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses(); + const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid); + const comment = useComment({ commentCid: postCid }); + const address = comment?.author?.address; + const resetFields = () => { if (textRef.current) { textRef.current.value = ''; @@ -104,13 +142,29 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { } }; - const onPublishPost = () => { + const hasCalledAnonAddressRef = useRef(false); + + const getAnonAddressForPost = useCallback(async () => { + if (anonMode && !hasCalledAnonAddressRef.current) { + hasCalledAnonAddressRef.current = true; + const newSigner = (await getNewSigner()) || {}; + setSubmitStore({ + signer: newSigner, + author: { + displayName, + address: newSigner.address, + }, + }); + } + }, [anonMode, getNewSigner, setSubmitStore, displayName]); + + const onPublishPost = async () => { if (!title && !content && !link) { alert(`Cannot post empty comment`); return; } if (link && !isValidURL(link)) { - alert(`Invalid link`); + alert('The provided link is not a valid URL.'); return; } @@ -139,7 +193,32 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { // in post page, publish a reply to the post const isInPostView = isPostPageView(location.pathname, params); const cid = params?.commentCid as string; - const { setContent, resetContent, replyIndex, publishReply } = useReply({ cid, subplebbitAddress }); + const { setPublishReplyOptions, resetPublishReplyOptions, replyIndex, publishReply } = usePublishReply({ cid, subplebbitAddress }); + + const getAnonAddressForReply = useCallback(async () => { + if (anonMode && !hasCalledAnonAddressRef.current) { + hasCalledAnonAddressRef.current = true; + const existingSigner = getExistingSigner(address); + if (existingSigner) { + setPublishReplyOptions({ + signer: existingSigner, + author: { + displayName, + address: existingSigner.address, + }, + }); + } else { + const newSigner = await getNewSigner(); + setPublishReplyOptions({ + signer: newSigner, + author: { + displayName, + address: newSigner.address, + }, + }); + } + } + }, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, displayName, anonMode]); const onPublishReply = () => { const currentContent = textRef.current?.value || ''; @@ -154,16 +233,27 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { alert('The provided link is not a valid URL.'); return; } + publishReply(); }; useEffect(() => { if (typeof replyIndex === 'number') { - resetContent(); + resetPublishReplyOptions(); resetFields(); closeForm(); } - }, [replyIndex, resetContent, closeForm]); + }, [replyIndex, resetPublishReplyOptions, closeForm]); + + useEffect(() => { + if (anonMode) { + if (isInPostView) { + getAnonAddressForReply(); + } else { + getAnonAddressForPost(); + } + } + }, [anonMode, getAnonAddressForPost, getAnonAddressForReply, isInPostView]); return ( @@ -205,7 +295,7 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { ref={textRef} onChange={(e) => { const content = e.target.value.replace(/\n/g, '\n\n'); - isInPostView ? setContent.content(content) : setSubmitStore({ content }); + isInPostView ? setPublishReplyOptions({ content }) : setSubmitStore({ content }); }} /> @@ -221,7 +311,7 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => { ref={urlRef} onChange={(e) => { setUrl(e.target.value); - isInPostView ? setContent.link(e.target.value) : setSubmitStore({ link: e.target.value }); + isInPostView ? setPublishReplyOptions({ link: e.target.value }) : setSubmitStore({ link: e.target.value }); }} /> {url && } @@ -232,7 +322,10 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => {
[ ] @@ -283,7 +376,7 @@ const PostForm = () => { comment = editedComment; } - const { deleted, locked, removed } = comment || {}; + const { deleted, locked, removed, postCid } = comment || {}; const isThreadClosed = deleted || locked || removed || isInDescriptionView || isInRulesView; const [showForm, setShowForm] = useState(false); @@ -310,7 +403,7 @@ const PostForm = () => { ] ) : ( - setShowForm(false)} /> + setShowForm(false)} postCid={postCid} /> )}
@@ -326,7 +419,7 @@ const PostForm = () => { - {showForm && setShowForm(false)} />} + {showForm && setShowForm(false)} postCid={postCid} />}
)} diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 0eb84770..025b65e5 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -74,7 +74,7 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P {!(isDescription || isRules) && ( <> - {isReply && author?.avatar && ( + {author?.avatar && ( @@ -127,7 +127,7 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P !cid && e.preventDefault()}> c/ - openReplyModal && openReplyModal(cid)}> + openReplyModal && openReplyModal(cid, postCid)}> {shortCid} @@ -164,6 +164,7 @@ const ReplyBacklinks = ({ post }: PostProps) => { const PostMessageMobile = ({ post }: PostProps) => { const { t } = useTranslation(); const { cid, content, deleted, edit, original, parentCid, postCid, reason, removed, state, subplebbitAddress } = post || {}; + const { isDescription, isRules } = post || {}; // custom properties, not from api // TODO: commentAuthor is not available outside of editedComment, update when available // const banned = !!post?.commentAuthor?.banExpiresAt; const [showOriginal, setShowOriginal] = useState(false); @@ -202,7 +203,12 @@ const PostMessageMobile = ({ post }: PostProps) => { {showOriginal && }
- {t('comment_edited_at_timestamp', { timestamp: getFormattedDate(edit?.timestamp), interpolation: { escapeValue: false } })}{' '} + } /> }} + />{' '} {reason && <>{t('reason_reason', { reason: reason, interpolation: { escapeValue: false } })} } {showOriginal ? ( { {!isReply && content.length > 1000 && !isInPostView && (
- }} /> + }} + />
)} {!cid && state === 'pending' && stateString !== 'Failed' && ( diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index c8842b93..f320a9f8 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -1,29 +1,31 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Draggable from 'react-draggable'; -import { setAccount, useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { setAccount, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isValidURL } from '../../lib/utils/url-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useSelectedTextStore from '../../stores/use-selected-text-store'; -import useReply from '../../hooks/use-reply'; +import usePublishReply from '../../hooks/use-publish-reply'; import useIsMobile from '../../hooks/use-is-mobile'; import styles from './reply-modal.module.css'; import { LinkTypePreviewer } from '../post-form'; import _ from 'lodash'; +import useAnonMode from '../../hooks/use-anon-mode'; interface ReplyModalProps { closeModal: () => void; parentCid: string; + postCid: string; scrollY: number; } -const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { +const ReplyModal = ({ closeModal, parentCid, postCid, scrollY }: ReplyModalProps) => { const { t } = useTranslation(); const { subplebbitAddress } = useParams() as { subplebbitAddress: string }; - const { setContent, publishReply } = useReply({ cid: parentCid, subplebbitAddress }); + const { setPublishReplyOptions, publishReply } = usePublishReply({ cid: parentCid, subplebbitAddress }); const account = useAccount(); const { displayName } = account?.author || {}; const [url, setUrl] = useState(''); @@ -31,7 +33,38 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { const urlRef = useRef(null); const { selectedText } = useSelectedTextStore(); - const onPublishReply = () => { + const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid); + const comment = useComment({ commentCid: postCid }); + const address = comment?.author?.address; + + const hasCalledAnonAddressRef = useRef(false); + + const getAnonAddressForReply = useCallback(async () => { + if (anonMode && !hasCalledAnonAddressRef.current) { + hasCalledAnonAddressRef.current = true; + let signer = getExistingSigner(address); + if (!signer) { + signer = await getNewSigner(); + if (signer) { + setPublishReplyOptions({ + signer, + author: { + displayName, + address: signer.address, + }, + }); + } + } + } + }, [anonMode, address, getExistingSigner, getNewSigner, displayName, setPublishReplyOptions]); + + useEffect(() => { + if (anonMode) { + getAnonAddressForReply(); + } + }, [anonMode, getAnonAddressForReply]); + + const onPublishReply = async () => { const currentContent = textRef.current?.value || ''; const currentUrl = urlRef.current?.value || ''; @@ -44,6 +77,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { alert('The provided link is not a valid URL.'); return; } + publishReply(); closeModal(); }; @@ -109,7 +143,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { // remove the prefix from the content to publish, and also add newlines for markdown const contentWithoutPrefix = e.target.value.slice(contentPrefix.length).replace(/\n/g, '\n\n'); if (textRef.current && textRef.current.value !== contentWithoutPrefix) { - setContent.content(contentWithoutPrefix); + setPublishReplyOptions({ content: contentWithoutPrefix }); } }; @@ -142,7 +176,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { placeholder={_.capitalize(t('link'))} onChange={(e) => { setUrl(e.target.value); - setContent.link(e.target.value); + setPublishReplyOptions({ link: e.target.value }); }} />
@@ -169,7 +203,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => { [ ] diff --git a/src/components/settings-modal/account-settings/account-settings.module.css b/src/components/settings-modal/account-settings/account-settings.module.css index 2371bb88..1fd66e37 100644 --- a/src/components/settings-modal/account-settings/account-settings.module.css +++ b/src/components/settings-modal/account-settings/account-settings.module.css @@ -36,11 +36,32 @@ } .deleteAccount { - color: red; display: block; - margin-top: 10px; +} + +.deleteAccount:not(:disabled) { + color: red; } .setting .createAccount { margin-left: 5px; +} + +.settingTip { + display: block; + font-size: 0.85em; + margin: 2px 0 10px 0; + padding-left: 19px; +} + +.anonMode { + padding: 10px 0; +} + +.anonMode label { + text-transform: capitalize; +} + +.anonMode input[type="checkbox"] { + margin-right: 3px; } \ No newline at end of file diff --git a/src/components/settings-modal/account-settings/account-settings.tsx b/src/components/settings-modal/account-settings/account-settings.tsx index df7e74b6..c8a48dc8 100644 --- a/src/components/settings-modal/account-settings/account-settings.tsx +++ b/src/components/settings-modal/account-settings/account-settings.tsx @@ -3,6 +3,21 @@ import { useTranslation } from 'react-i18next'; import { createAccount, deleteAccount, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks'; import stringify from 'json-stringify-pretty-compact'; import styles from './account-settings.module.css'; +import useAnonModeStore from '../../../stores/use-anon-mode-store'; + +const AnonMode = () => { + const anonMode = useAnonModeStore((state) => state.anonMode); + const setAnonMode = useAnonModeStore((state) => state.setAnonMode); + + return ( +
+ + Automatically use a different user ID in each thread +
+ ); +}; const AccountSettings = () => { const { t } = useTranslation(); @@ -167,7 +182,7 @@ const AccountSettings = () => {