mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat: add challenge modal
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
.container {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(50% - 150px);
|
||||||
|
left: calc(50% - 150px);
|
||||||
|
display: block;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 10pt;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: var(--challenge-modal-background-color);
|
||||||
|
border: var(--challenge-modal-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--challenge-modal-title-font-size);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
padding: 0;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
cursor: move;
|
||||||
|
font-weight: var(--challenge-modal-title-font-weight);
|
||||||
|
background-color: var(--challenge-modal-title-background-color);
|
||||||
|
color: var(--challenge-modal-title-text-color);
|
||||||
|
border: var(--challenge-modal-title-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeIcon {
|
||||||
|
all: unset;
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: none;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
background-image: var(--close-button-background-image);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container input, .container textarea {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
outline: medium none;
|
||||||
|
width: 298px;
|
||||||
|
padding: 2px;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content textarea {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challengeAnswer {
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 0px 2px;
|
||||||
|
font-family: monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challengeMediaWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challengeFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challengeFooter button {
|
||||||
|
margin: 5px 5px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Challenge as ChallengeType, useComment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useChallenges from '../../hooks/use-challenges';
|
||||||
|
import styles from './challenge-modal.module.css';
|
||||||
|
import { getPublicationType } from '../../lib/utils/challenge-utils';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
|
||||||
|
interface ChallengeProps {
|
||||||
|
challenge: ChallengeType;
|
||||||
|
closeModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const challenges = challenge?.[0]?.challenges;
|
||||||
|
const publication = challenge?.[1];
|
||||||
|
|
||||||
|
const publicationType = getPublicationType(publication);
|
||||||
|
const { author, content, link, title } = publication || {};
|
||||||
|
const { displayName } = author || {};
|
||||||
|
|
||||||
|
const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
|
||||||
|
const [answers, setAnswers] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const isTextChallenge = challenges[currentChallengeIndex].type === 'text/plain';
|
||||||
|
const isImageChallenge = challenges[currentChallengeIndex].type === 'image/png';
|
||||||
|
|
||||||
|
const onAnswersChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setAnswers((prevAnswers) => {
|
||||||
|
const updatedAnswers = [...prevAnswers];
|
||||||
|
updatedAnswers[currentChallengeIndex] = e.target.value;
|
||||||
|
return updatedAnswers;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
publication.publishChallengeAnswers(answers);
|
||||||
|
setAnswers([]);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
if (challenges[currentChallengeIndex + 1]) {
|
||||||
|
setCurrentChallengeIndex((prev) => prev + 1);
|
||||||
|
} else {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable handle='.challengeHandle'>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`challengeHandle ${styles.title}`}>
|
||||||
|
Challenge for {publicationType}
|
||||||
|
<button className={styles.closeIcon} onClick={closeModal} title='close' />
|
||||||
|
</div>
|
||||||
|
<div className={styles.publication}>
|
||||||
|
<div className={styles.name}>
|
||||||
|
<input type='text' value={displayName || 'Anonymous'} disabled />
|
||||||
|
</div>
|
||||||
|
{title && (
|
||||||
|
<div className={styles.subject}>
|
||||||
|
<input type='text' value={title} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{content && (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<textarea value={content} disabled cols={48} rows={4} wrap='soft' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{link && (
|
||||||
|
<div className={styles.link}>
|
||||||
|
<input type='text' value={link} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.challengeContainer}>
|
||||||
|
<input
|
||||||
|
className={styles.challengeAnswer}
|
||||||
|
type='text'
|
||||||
|
autoComplete='off'
|
||||||
|
autoCorrect='off'
|
||||||
|
spellCheck='false'
|
||||||
|
placeholder='TYPE THE ANSWER HERE AND PRESS ENTER'
|
||||||
|
onKeyDown={onEnterKey}
|
||||||
|
onChange={onAnswersChange}
|
||||||
|
value={answers[currentChallengeIndex] || ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className={styles.challengeMediaWrapper}>
|
||||||
|
{isTextChallenge && <div className={styles.challengeMedia}>{challenges[currentChallengeIndex]?.challenge}</div>}
|
||||||
|
{isImageChallenge && (
|
||||||
|
<img alt={t('loading')} className={styles.challengeMedia} src={`data:image/png;base64,${challenges[currentChallengeIndex]?.challenge}`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.challengeFooter}>
|
||||||
|
<div className={styles.counter}>{t('challenge_counter', { index: currentChallengeIndex + 1, total: challenges?.length })}</div>
|
||||||
|
<span className={styles.buttons}>
|
||||||
|
{!challenges[currentChallengeIndex + 1] && <button onClick={onSubmit}>{t('submit')}</button>}
|
||||||
|
{challenges.length > 1 && (
|
||||||
|
<button disabled={!challenges[currentChallengeIndex - 1]} onClick={() => setCurrentChallengeIndex((prev) => prev - 1)}>
|
||||||
|
{t('previous')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{challenges[currentChallengeIndex + 1] && <button onClick={() => setCurrentChallengeIndex((prev) => prev + 1)}>{t('next')}</button>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChallengeModal = () => {
|
||||||
|
const { challenges, removeChallenge } = useChallenges();
|
||||||
|
const isOpen = !!challenges.length;
|
||||||
|
const closeModal = () => removeChallenge();
|
||||||
|
|
||||||
|
return isOpen && <Challenge challenge={challenges[0]} closeModal={closeModal} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChallengeModal;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './challenge-modal';
|
||||||
@@ -1,11 +1,55 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useAccount, useComment } from '@plebbit/plebbit-react-hooks';
|
import { PublishCommentOptions, setAccount, useAccount, useAccountComment, useComment, usePublishComment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import styles from './post-form.module.css';
|
||||||
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import { getLinkMediaInfo } from '../../lib/utils/media-utils';
|
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 styles from './post-form.module.css';
|
import challengesStore from '../../hooks/use-challenges';
|
||||||
|
|
||||||
|
type SubmitState = {
|
||||||
|
subplebbitAddress: string | undefined;
|
||||||
|
title: string | undefined;
|
||||||
|
content: string | undefined;
|
||||||
|
link: string | undefined;
|
||||||
|
publishCommentOptions: PublishCommentOptions;
|
||||||
|
setSubmitStore: (data: Partial<SubmitState>) => void;
|
||||||
|
resetSubmitStore: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { addChallenge } = challengesStore.getState();
|
||||||
|
|
||||||
|
const useSubmitStore = create<SubmitState>((set) => ({
|
||||||
|
subplebbitAddress: undefined,
|
||||||
|
title: undefined,
|
||||||
|
content: undefined,
|
||||||
|
link: undefined,
|
||||||
|
publishCommentOptions: {},
|
||||||
|
setSubmitStore: ({ subplebbitAddress, title, content, link }) =>
|
||||||
|
set((state) => {
|
||||||
|
const nextState = { ...state };
|
||||||
|
if (subplebbitAddress !== undefined) nextState.subplebbitAddress = subplebbitAddress;
|
||||||
|
if (title !== undefined) nextState.title = title || undefined;
|
||||||
|
if (content !== undefined) nextState.content = content || undefined;
|
||||||
|
if (link !== undefined) nextState.link = link || undefined;
|
||||||
|
|
||||||
|
nextState.publishCommentOptions = {
|
||||||
|
...nextState,
|
||||||
|
onChallenge: (...args: any) => addChallenge(args),
|
||||||
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.error(error);
|
||||||
|
let errorMessage = error.message;
|
||||||
|
alert(errorMessage);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return nextState;
|
||||||
|
}),
|
||||||
|
resetSubmitStore: () => set({ subplebbitAddress: undefined, title: undefined, content: undefined, link: undefined, publishCommentOptions: {} }),
|
||||||
|
}));
|
||||||
|
|
||||||
const LinkTypePreviewer = ({ link }: { link: string }) => {
|
const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||||
const mediaInfo = getLinkMediaInfo(link);
|
const mediaInfo = getLinkMediaInfo(link);
|
||||||
@@ -14,9 +58,44 @@ const LinkTypePreviewer = ({ link }: { link: string }) => {
|
|||||||
|
|
||||||
const PostFormTable = () => {
|
const PostFormTable = () => {
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const { displayName } = account || {};
|
const { displayName } = account?.author || {};
|
||||||
|
|
||||||
const [link, setLink] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
|
|
||||||
|
const { title, content, link, publishCommentOptions, setSubmitStore, resetSubmitStore } = useSubmitStore();
|
||||||
|
|
||||||
|
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
||||||
|
|
||||||
|
const onPublish = () => {
|
||||||
|
if (!title && !content && !link) {
|
||||||
|
alert(`Cannot post empty comment`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (link && !isValidURL(link)) {
|
||||||
|
alert(`Invalid link`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishComment();
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
|
||||||
|
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
|
||||||
|
useEffect(() => {
|
||||||
|
if (subplebbitAddress) {
|
||||||
|
setSubmitStore({ subplebbitAddress });
|
||||||
|
}
|
||||||
|
}, [subplebbitAddress, setSubmitStore]);
|
||||||
|
|
||||||
|
// redirect to pending page when pending comment is created
|
||||||
|
const navigate = useNavigate();
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
resetSubmitStore();
|
||||||
|
navigate(`/profile/${index}`);
|
||||||
|
}
|
||||||
|
}, [index, resetSubmitStore, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.postFormTable}>
|
<table className={styles.postFormTable}>
|
||||||
@@ -24,30 +103,44 @@ const PostFormTable = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>
|
<td>
|
||||||
<input type='text' placeholder={!displayName ? 'Anonymous' : undefined} defaultValue={displayName || undefined} />
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder={!displayName ? 'Anonymous' : undefined}
|
||||||
|
defaultValue={displayName || undefined}
|
||||||
|
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Subject</td>
|
<td>Subject</td>
|
||||||
<td>
|
<td>
|
||||||
<input type='text' />
|
<input type='text' onChange={(e) => setSubmitStore({ title: e.target.value })} />
|
||||||
<button>Post</button>
|
<button onClick={onPublish}>Post</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Comment</td>
|
<td>Comment</td>
|
||||||
<td>
|
<td>
|
||||||
<textarea cols={48} rows={4} wrap='soft' />
|
<textarea cols={48} rows={4} wrap='soft' onChange={(e) => setSubmitStore({ content: e.target.value })} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Link</td>
|
<td>Link</td>
|
||||||
<td>
|
<td>
|
||||||
<input type='text' onChange={(e) => setLink(e.target.value)} />
|
<input
|
||||||
|
type='text'
|
||||||
|
autoCorrect='off'
|
||||||
|
autoComplete='off'
|
||||||
|
spellCheck='false'
|
||||||
|
onChange={(e) => {
|
||||||
|
setUrl(e.target.value);
|
||||||
|
setSubmitStore({ link: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span className={styles.linkType}>
|
<span className={styles.linkType}>
|
||||||
{link && (
|
{url && (
|
||||||
<>
|
<>
|
||||||
(<LinkTypePreviewer link={link} />)
|
(<LinkTypePreviewer link={url} />)
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -66,9 +159,7 @@ const PostForm = () => {
|
|||||||
const isInDescriptionView = isDescriptionView(location.pathname, params);
|
const isInDescriptionView = isDescriptionView(location.pathname, params);
|
||||||
const isInRulesView = isRulesView(location.pathname, params);
|
const isInRulesView = isRulesView(location.pathname, params);
|
||||||
|
|
||||||
const { subplebbitAddress, commentCid } = params || {};
|
const comment = useComment({ commentCid: useParams().commentCid });
|
||||||
|
|
||||||
const comment = useComment({ commentCid });
|
|
||||||
const { deleted, locked, removed } = comment || {};
|
const { deleted, locked, removed } = comment || {};
|
||||||
const isThreadClosed = deleted || locked || removed || isInDescriptionView || isInRulesView;
|
const isThreadClosed = deleted || locked || removed || isInDescriptionView || isInRulesView;
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,11 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.postMobile .postNumLink a {
|
||||||
|
color: unset;
|
||||||
|
text-decoration: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.postMobile .postMessage {
|
.postMobile .postMessage {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: var(--post-mobile-content-font-size);
|
font-size: var(--post-mobile-content-font-size);
|
||||||
@@ -416,6 +421,11 @@
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingCid {
|
||||||
|
color: red !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.postDesktop {
|
.postDesktop {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Role, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
|||||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||||
import { getFormattedDate } from '../../lib/utils/time-utils';
|
import { getFormattedDate } from '../../lib/utils/time-utils';
|
||||||
import { isPostPageView } 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 useWindowWidth from '../../hooks/use-window-width';
|
import useWindowWidth from '../../hooks/use-window-width';
|
||||||
@@ -24,13 +24,16 @@ interface PostProps {
|
|||||||
|
|
||||||
const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, postCid, replyCount, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, postCid, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
|
||||||
const { address, displayName, shortAddress } = author || {};
|
const { address, displayName, shortAddress } = author || {};
|
||||||
const authorRole = roles?.[address]?.role;
|
const authorRole = roles?.[address]?.role;
|
||||||
|
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
|
|
||||||
const isInPostPage = isPostPageView(useLocation().pathname, useParams());
|
const params = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
const isInPostPage = isPostPageView(location.pathname, params);
|
||||||
|
const isInPendingPostPage = isPendingPostView(location.pathname, params);
|
||||||
|
|
||||||
const displayTitle = title && title.length > 75 ? title?.slice(0, 75) + '...' : title;
|
const displayTitle = title && title.length > 75 ? title?.slice(0, 75) + '...' : title;
|
||||||
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) + '(...)' : content;
|
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) + '(...)' : content;
|
||||||
@@ -120,12 +123,21 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
|||||||
<span className={styles.postNum}>
|
<span className={styles.postNum}>
|
||||||
{!(isDescription || isRules) && (
|
{!(isDescription || isRules) && (
|
||||||
<span className={styles.postNumLink}>
|
<span className={styles.postNumLink}>
|
||||||
<Link to={`/p/${subplebbitAddress}/${cid}`} className={styles.linkToPost} title={t('link_to_post')}>
|
<Link
|
||||||
|
to={`/p/${subplebbitAddress}/${cid}`}
|
||||||
|
className={styles.linkToPost}
|
||||||
|
title={t('link_to_post')}
|
||||||
|
onClick={(e) => isInPendingPostPage && e.preventDefault()}
|
||||||
|
>
|
||||||
c/
|
c/
|
||||||
</Link>
|
</Link>
|
||||||
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
{isInPendingPostPage ? (
|
||||||
{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 && (
|
||||||
@@ -305,14 +317,18 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
|||||||
|
|
||||||
const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
|
const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
|
||||||
const { address, displayName, shortAddress } = author || {};
|
const { address, displayName, shortAddress } = author || {};
|
||||||
const authorRole = roles?.[address]?.role;
|
const authorRole = roles?.[address]?.role;
|
||||||
|
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
const isInPostPage = isPostPageView(location.pathname, params);
|
||||||
|
const isInPendingPostPage = isPendingPostView(location.pathname, params);
|
||||||
|
|
||||||
const linkCount = useCountLinksInReplies(post);
|
const linkCount = useCountLinksInReplies(post);
|
||||||
const isInPostPage = isPostPageView(useLocation().pathname, useParams());
|
|
||||||
const displayTitle = title && title.length > 30 ? title?.slice(0, 30) + '(...)' : title;
|
const displayTitle = title && title.length > 30 ? title?.slice(0, 30) + '(...)' : title;
|
||||||
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) : content;
|
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) : content;
|
||||||
|
|
||||||
@@ -360,10 +376,23 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
|
|||||||
<span className={styles.dateTimePostNum}>
|
<span className={styles.dateTimePostNum}>
|
||||||
{getFormattedDate(timestamp)}{' '}
|
{getFormattedDate(timestamp)}{' '}
|
||||||
{!(isDescription || isRules) && (
|
{!(isDescription || isRules) && (
|
||||||
<>
|
<span className={styles.postNumLink}>
|
||||||
<span className={styles.linkToPost}>c/</span>
|
<Link
|
||||||
<span className={styles.replyToPost}>{shortCid}</span>
|
to={`/p/${subplebbitAddress}/${cid}`}
|
||||||
</>
|
className={styles.linkToPost}
|
||||||
|
title={t('link_to_post')}
|
||||||
|
onClick={(e) => isInPendingPostPage && e.preventDefault()}
|
||||||
|
>
|
||||||
|
c/
|
||||||
|
</Link>
|
||||||
|
{isInPendingPostPage ? (
|
||||||
|
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
|
||||||
|
) : (
|
||||||
|
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
||||||
|
{shortCid}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { Challenge } from '@plebbit/plebbit-react-hooks';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
challenges: Challenge[];
|
||||||
|
addChallenge: (challenge: Challenge) => void;
|
||||||
|
removeChallenge: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useChallengesStore = create<State>((set) => ({
|
||||||
|
challenges: [],
|
||||||
|
addChallenge: (challenge: Challenge) => {
|
||||||
|
set((state) => ({ challenges: [...state.challenges, challenge] }));
|
||||||
|
},
|
||||||
|
removeChallenge: () => {
|
||||||
|
set((state) => {
|
||||||
|
const challenges = [...state.challenges];
|
||||||
|
challenges.shift();
|
||||||
|
return { challenges };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useChallengesStore;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { ChallengeVerification } from '@plebbit/plebbit-react-hooks';
|
||||||
|
|
||||||
|
export const alertChallengeVerificationFailed = (challengeVerification: ChallengeVerification, publication: any) => {
|
||||||
|
if (challengeVerification?.challengeSuccess === false) {
|
||||||
|
console.warn(challengeVerification, publication);
|
||||||
|
alert(`p/${publication?.subplebbitAddress} challenge error: ${[...(challengeVerification?.challengeErrors || []), challengeVerification?.reason].join(' ')}`);
|
||||||
|
} else {
|
||||||
|
console.log(challengeVerification, publication);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPublicationType = (publication: any) => {
|
||||||
|
if (!publication) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof publication.vote === 'number') {
|
||||||
|
return 'vote';
|
||||||
|
}
|
||||||
|
if (publication.parentCid) {
|
||||||
|
return 'reply';
|
||||||
|
}
|
||||||
|
if (publication.commentCid) {
|
||||||
|
return 'edit';
|
||||||
|
}
|
||||||
|
return 'post';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVotePreview = (publication: any) => {
|
||||||
|
if (typeof publication?.vote !== 'number') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let votePreview = '';
|
||||||
|
if (publication.vote === -1) {
|
||||||
|
votePreview += ' -1';
|
||||||
|
} else {
|
||||||
|
votePreview += ` +${publication.vote}`;
|
||||||
|
}
|
||||||
|
return votePreview;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPublicationPreview = (publication: any) => {
|
||||||
|
if (!publication) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let publicationPreview = '';
|
||||||
|
if (publication.title) {
|
||||||
|
publicationPreview += publication.title;
|
||||||
|
}
|
||||||
|
if (publication.content) {
|
||||||
|
if (publicationPreview) {
|
||||||
|
publicationPreview += ': ';
|
||||||
|
}
|
||||||
|
publicationPreview += publication.content;
|
||||||
|
}
|
||||||
|
if (!publicationPreview && publication.link) {
|
||||||
|
publicationPreview += publication.link;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicationPreview.length > 50) {
|
||||||
|
publicationPreview = publicationPreview.substring(0, 50) + '...';
|
||||||
|
}
|
||||||
|
return publicationPreview;
|
||||||
|
};
|
||||||
Vendored
+2
@@ -6,3 +6,5 @@ declare module '*.module.css' {
|
|||||||
declare module 'ext-name';
|
declare module 'ext-name';
|
||||||
|
|
||||||
declare module 'lodash';
|
declare module 'lodash';
|
||||||
|
|
||||||
|
declare module 'react-draggable';
|
||||||
|
|||||||
@@ -30,9 +30,19 @@
|
|||||||
--catalog-post-menu-btn-opacity: 0.5;
|
--catalog-post-menu-btn-opacity: 0.5;
|
||||||
--catalog-post-menu-btn-hover-opacity: 1;
|
--catalog-post-menu-btn-hover-opacity: 1;
|
||||||
|
|
||||||
|
/* challenge modal */
|
||||||
|
--challenge-modal-background-color: #f0e0d6;
|
||||||
|
--challenge-modal-border: 1px solid #d9bfb7;
|
||||||
|
--challenge-modal-title-background-color: #ea8;
|
||||||
|
--challenge-modal-title-text-color: #800;
|
||||||
|
--challenge-modal-title-border: 1px solid #800;
|
||||||
|
--challenge-modal-title-font-size: 10pt;
|
||||||
|
--challenge-modal-title-font-weight: 700;
|
||||||
|
|
||||||
/* desktop buttons */
|
/* desktop buttons */
|
||||||
--button-desktop-text-color: #00e;
|
--button-desktop-text-color: #00e;
|
||||||
--button-desktop-text-color-hover: red;
|
--button-desktop-text-color-hover: red;
|
||||||
|
--close-button-background-image: url("/public/assets/buttons/cross-red.png");
|
||||||
|
|
||||||
/* mobile buttons */
|
/* mobile buttons */
|
||||||
--button-font-size-mobile: 10pt;
|
--button-font-size-mobile: 10pt;
|
||||||
|
|||||||
Reference in New Issue
Block a user