mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(post form): add reply publishing in post page
This commit is contained in:
+7
-1
@@ -18,18 +18,24 @@ import PostForm from './components/post-form';
|
|||||||
|
|
||||||
const BoardLayout = () => {
|
const BoardLayout = () => {
|
||||||
const { subplebbitAddress } = useParams();
|
const { subplebbitAddress } = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// force rerender of post form when navigating between pages
|
||||||
|
const key = `${subplebbitAddress}-${location.pathname}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BoardNav />
|
<BoardNav />
|
||||||
<BoardBanner />
|
<BoardBanner />
|
||||||
<MobileBoardButtons />
|
<MobileBoardButtons />
|
||||||
<PostForm key={subplebbitAddress} />
|
<PostForm key={key} />
|
||||||
<SubplebbitStats />
|
<SubplebbitStats />
|
||||||
<DesktopBoardButtons />
|
<DesktopBoardButtons />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isInHomeView = isHomeView(location.pathname);
|
const isInHomeView = isHomeView(location.pathname);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { PublishCommentOptions, setAccount, useAccount, useAccountComment, useComment, usePublishComment } from '@plebbit/plebbit-react-hooks';
|
import { PublishCommentOptions, setAccount, useAccount, useAccountComment, useComment, usePublishComment } from '@plebbit/plebbit-react-hooks';
|
||||||
@@ -9,6 +9,7 @@ import { getLinkMediaInfo } from '../../lib/utils/media-utils';
|
|||||||
import { isValidURL } from '../../lib/utils/url-utils';
|
import { isValidURL } from '../../lib/utils/url-utils';
|
||||||
import { isDescriptionView, isPostPageView, isRulesView } from '../../lib/utils/view-utils';
|
import { isDescriptionView, isPostPageView, isRulesView } from '../../lib/utils/view-utils';
|
||||||
import challengesStore from '../../hooks/use-challenges';
|
import challengesStore from '../../hooks/use-challenges';
|
||||||
|
import useReply from '../../hooks/use-reply';
|
||||||
|
|
||||||
type SubmitState = {
|
type SubmitState = {
|
||||||
subplebbitAddress: string | undefined;
|
subplebbitAddress: string | undefined;
|
||||||
@@ -66,7 +67,7 @@ const PostFormTable = () => {
|
|||||||
|
|
||||||
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
||||||
|
|
||||||
const onPublish = () => {
|
const onPublishPost = () => {
|
||||||
if (!title && !content && !link) {
|
if (!title && !content && !link) {
|
||||||
alert(`Cannot post empty comment`);
|
alert(`Cannot post empty comment`);
|
||||||
return;
|
return;
|
||||||
@@ -97,6 +98,38 @@ const PostFormTable = () => {
|
|||||||
}
|
}
|
||||||
}, [index, resetSubmitStore, navigate]);
|
}, [index, resetSubmitStore, navigate]);
|
||||||
|
|
||||||
|
// in post page, publish a reply to the post
|
||||||
|
const location = useLocation();
|
||||||
|
const isInPostPage = isPostPageView(location.pathname, params);
|
||||||
|
const cid = params?.commentCid as string;
|
||||||
|
const { setContent, resetContent, replyIndex, publishReply } = useReply({ cid, subplebbitAddress });
|
||||||
|
|
||||||
|
const textRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const urlRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const onPublishReply = () => {
|
||||||
|
const currentContent = textRef.current?.value || '';
|
||||||
|
const currentUrl = urlRef.current?.value || '';
|
||||||
|
|
||||||
|
if (!currentContent.trim() && !currentUrl) {
|
||||||
|
alert(`Cannot post empty comment`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUrl && !isValidURL(currentUrl)) {
|
||||||
|
alert('The provided link is not a valid URL.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishReply();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof replyIndex === 'number') {
|
||||||
|
resetContent();
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
}, [replyIndex, resetContent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.postFormTable}>
|
<table className={styles.postFormTable}>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -109,19 +142,35 @@ const PostFormTable = () => {
|
|||||||
defaultValue={displayName || undefined}
|
defaultValue={displayName || undefined}
|
||||||
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
|
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
|
||||||
/>
|
/>
|
||||||
|
<button onClick={onPublishReply}>Post</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
{!isInPostPage && (
|
||||||
<td>Subject</td>
|
<tr>
|
||||||
<td>
|
<td>Subject</td>
|
||||||
<input type='text' onChange={(e) => setSubmitStore({ title: e.target.value })} />
|
<td>
|
||||||
<button onClick={onPublish}>Post</button>
|
<input
|
||||||
</td>
|
type='text'
|
||||||
</tr>
|
onChange={(e) => {
|
||||||
|
setSubmitStore({ title: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button onClick={onPublishPost}>Post</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Comment</td>
|
<td>Comment</td>
|
||||||
<td>
|
<td>
|
||||||
<textarea cols={48} rows={4} wrap='soft' onChange={(e) => setSubmitStore({ content: e.target.value })} />
|
<textarea
|
||||||
|
cols={48}
|
||||||
|
rows={4}
|
||||||
|
wrap='soft'
|
||||||
|
ref={textRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
isInPostPage ? setContent.content(e.target.value) : setSubmitStore({ content: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -132,9 +181,10 @@ const PostFormTable = () => {
|
|||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
spellCheck='false'
|
spellCheck='false'
|
||||||
|
ref={urlRef}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setUrl(e.target.value);
|
setUrl(e.target.value);
|
||||||
setSubmitStore({ link: e.target.value });
|
isInPostPage ? setContent.link(e.target.value) : setSubmitStore({ link: e.target.value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className={styles.linkType}>
|
<span className={styles.linkType}>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import { getFormattedDate } from '../../lib/utils/time-utils';
|
|||||||
import { isPostPageView, isPendingPostView } from '../../lib/utils/view-utils';
|
import { isPostPageView, isPendingPostView } from '../../lib/utils/view-utils';
|
||||||
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
||||||
import useReplies from '../../hooks/use-replies';
|
import useReplies from '../../hooks/use-replies';
|
||||||
|
import useStateString from '../../hooks/use-state-string';
|
||||||
import useWindowWidth from '../../hooks/use-window-width';
|
import useWindowWidth from '../../hooks/use-window-width';
|
||||||
import styles from './post.module.css';
|
import styles from './post.module.css';
|
||||||
|
import LoadingEllipsis from '../loading-ellipsis';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
import CommentMedia from '../comment-media';
|
import CommentMedia from '../comment-media';
|
||||||
import { canEmbed } from '../embed';
|
import { canEmbed } from '../embed';
|
||||||
@@ -201,7 +203,7 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
|||||||
|
|
||||||
const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, content, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, subplebbitAddress, timestamp } = reply || {};
|
const { author, cid, content, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, state, subplebbitAddress, timestamp } = reply || {};
|
||||||
const { address, displayName, shortAddress } = author || {};
|
const { address, displayName, shortAddress } = author || {};
|
||||||
const authorRole = roles?.[address]?.role;
|
const authorRole = roles?.[address]?.role;
|
||||||
|
|
||||||
@@ -215,6 +217,11 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
|||||||
|
|
||||||
const [menuBtnRotated, setMenuBtnRotated] = useState(false);
|
const [menuBtnRotated, setMenuBtnRotated] = useState(false);
|
||||||
|
|
||||||
|
const stateString = useStateString(reply);
|
||||||
|
const loadingString = stateString && (
|
||||||
|
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.replyDesktop}>
|
<div className={styles.replyDesktop}>
|
||||||
<div className={styles.sideArrows}>{'>>'}</div>
|
<div className={styles.sideArrows}>{'>>'}</div>
|
||||||
@@ -233,12 +240,16 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
|||||||
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
|
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
|
||||||
<span className={styles.postNum}>
|
<span className={styles.postNum}>
|
||||||
<span className={styles.postNumLink}>
|
<span className={styles.postNumLink}>
|
||||||
<Link to={`/p/${subplebbitAddress}/c/${reply.cid}`} className={styles.linkToPost} title='Link to post'>
|
<Link to={`/p/${subplebbitAddress}/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}>
|
||||||
c/
|
c/
|
||||||
</Link>
|
</Link>
|
||||||
<span className={styles.replyToPost} title='Reply to post'>
|
{!cid ? (
|
||||||
{shortCid}
|
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
|
||||||
</span>
|
) : (
|
||||||
|
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
||||||
|
{shortCid}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{pinned && (
|
{pinned && (
|
||||||
<span className={styles.stickyIconWrapper}>
|
<span className={styles.stickyIconWrapper}>
|
||||||
@@ -308,6 +319,12 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Markdown content={content} />
|
<Markdown content={content} />
|
||||||
|
{!cid && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{loadingString}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ChallengeVerification, Comment, PublishCommentOptions, usePublishComment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import useChallengesStore from './use-challenges';
|
||||||
|
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||||
|
|
||||||
|
type SetReplyStoreData = {
|
||||||
|
subplebbitAddress: string;
|
||||||
|
parentCid: string;
|
||||||
|
content: string | undefined;
|
||||||
|
link: string | undefined;
|
||||||
|
spoiler: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReplyState = {
|
||||||
|
content: { [parentCid: string]: string | undefined };
|
||||||
|
link: { [parentCid: string]: string | undefined };
|
||||||
|
spoiler: { [parentCid: string]: boolean | undefined };
|
||||||
|
publishCommentOptions: PublishCommentOptions;
|
||||||
|
setReplyStore: (data: SetReplyStoreData) => void;
|
||||||
|
resetReplyStore: (parentCid: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
|
const useReplyStore = create<ReplyState>((set) => ({
|
||||||
|
content: {},
|
||||||
|
link: {},
|
||||||
|
spoiler: {},
|
||||||
|
publishCommentOptions: {},
|
||||||
|
setReplyStore: (data: SetReplyStoreData) =>
|
||||||
|
set((state) => {
|
||||||
|
const { subplebbitAddress, parentCid, content, link, spoiler } = data;
|
||||||
|
const publishCommentOptions = {
|
||||||
|
subplebbitAddress,
|
||||||
|
parentCid,
|
||||||
|
content,
|
||||||
|
link,
|
||||||
|
spoiler,
|
||||||
|
onChallenge: (...args: any) => addChallenge(args),
|
||||||
|
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
|
||||||
|
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.error(error);
|
||||||
|
alert(error.message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
content: { ...state.content, [parentCid]: content },
|
||||||
|
link: { ...state.link, [parentCid]: link },
|
||||||
|
spoiler: { ...state.spoiler, [parentCid]: spoiler },
|
||||||
|
publishCommentOptions: { ...state.publishCommentOptions, [parentCid]: publishCommentOptions },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetReplyStore: (parentCid) =>
|
||||||
|
set((state) => ({
|
||||||
|
content: { ...state.content, [parentCid]: undefined },
|
||||||
|
link: { ...state.link, [parentCid]: undefined },
|
||||||
|
spoiler: { ...state.spoiler, [parentCid]: undefined },
|
||||||
|
publishCommentOptions: { ...state.publishCommentOptions, [parentCid]: undefined },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const useReply = ({ cid, subplebbitAddress }: { cid: string; subplebbitAddress: string }) => {
|
||||||
|
const parentCid = cid;
|
||||||
|
const { content, link, spoiler, publishCommentOptions } = useReplyStore((state) => ({
|
||||||
|
content: state.content[parentCid],
|
||||||
|
link: state.link[parentCid],
|
||||||
|
spoiler: state.spoiler[parentCid],
|
||||||
|
publishCommentOptions: state.publishCommentOptions[parentCid],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setReplyStore = useReplyStore((state) => state.setReplyStore);
|
||||||
|
const resetReplyStore = useReplyStore((state) => state.resetReplyStore);
|
||||||
|
|
||||||
|
const setContent = useMemo(
|
||||||
|
() => ({
|
||||||
|
content: (newContent: string) =>
|
||||||
|
setReplyStore({
|
||||||
|
subplebbitAddress,
|
||||||
|
parentCid,
|
||||||
|
content: newContent === '' ? undefined : newContent,
|
||||||
|
link: link || undefined,
|
||||||
|
spoiler: spoiler || false,
|
||||||
|
}),
|
||||||
|
link: (newLink: string) =>
|
||||||
|
setReplyStore({
|
||||||
|
subplebbitAddress,
|
||||||
|
parentCid,
|
||||||
|
content: content,
|
||||||
|
link: newLink || undefined,
|
||||||
|
spoiler: spoiler || false,
|
||||||
|
}),
|
||||||
|
spoiler: (newSpoiler: boolean) =>
|
||||||
|
setReplyStore({
|
||||||
|
subplebbitAddress,
|
||||||
|
parentCid,
|
||||||
|
content: content,
|
||||||
|
link: link || undefined,
|
||||||
|
spoiler: newSpoiler,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[subplebbitAddress, parentCid, setReplyStore, content, link, spoiler],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetContent = useMemo(() => () => resetReplyStore(parentCid), [parentCid, resetReplyStore]);
|
||||||
|
|
||||||
|
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
||||||
|
|
||||||
|
return { setContent, resetContent, replyIndex: index, publishReply: publishComment };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useReply;
|
||||||
Reference in New Issue
Block a user