Merge pull request #641 from plebbit/development

Development
This commit is contained in:
Tom (plebeius.eth)
2025-01-30 23:26:06 +01:00
committed by GitHub
17 changed files with 444 additions and 165 deletions
+60 -4
View File
@@ -125,31 +125,84 @@ const blockquoteToGreentext = () => (tree: any) => {
});
};
const spoilerToDiv = () => (tree: any) => {
tree.children.forEach((node: any) => {
if (node.type === 'paragraph') {
const text = node.children.map((child: any) => child.value).join('');
if (text.includes('§§SPOILER_START§§')) {
node.type = 'span';
node.data = {
hName: 'span',
};
node.children.forEach((child: any) => {
if (child.type === 'text') {
const parts = child.value.split(/(§§SPOILER_START§§.*?§§SPOILER_END§§)/);
if (parts.length > 1) {
const newChildren = parts.map((part: string) => {
const spoilerMatch = part.match(/§§SPOILER_START§§(.*?)§§SPOILER_END§§/);
if (spoilerMatch) {
return {
type: 'span',
data: {
hName: 'span',
hProperties: {
className: 'spoilertext',
},
},
children: [
{
type: 'text',
value: spoilerMatch[1],
},
],
};
}
return {
type: 'text',
value: part,
};
});
node.children = newChildren;
}
}
});
}
}
});
};
interface MarkdownProps {
content: string;
title?: string;
}
const Markdown = ({ content, title }: MarkdownProps) => {
const preprocessedContent = useMemo(() => {
if (!content) return '';
return content.replace(/<spoiler>([^<]*)<\/spoiler>/g, '§§SPOILER_START§§$1§§SPOILER_END§§').replace(/<img[^>]*src=['"]([^'"]+)['"][^>]*>/gi, '$1');
}, [content]);
const remarkPlugins: any[] = [[supersub]];
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
if (preprocessedContent && preprocessedContent.length <= MAX_LENGTH_FOR_GFM) {
remarkPlugins.push([remarkGfm, { singleTilde: false }]);
}
const customSchema = useMemo(
() => ({
...defaultSchema,
tagNames: [...(defaultSchema.tagNames || []), 'div'],
tagNames: [...(defaultSchema.tagNames || []), 'div', 'span'],
attributes: {
...defaultSchema.attributes,
div: ['className'],
span: ['className'],
},
}),
[],
);
remarkPlugins.push([blockquoteToGreentext]);
remarkPlugins.push([spoilerToDiv]);
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
@@ -162,7 +215,7 @@ const Markdown = ({ content, title }: MarkdownProps) => {
</span>
)}
<ReactMarkdown
children={content}
children={preprocessedContent}
remarkPlugins={remarkPlugins}
rehypePlugins={[[rehypeSanitize, customSchema]]}
components={{
@@ -173,7 +226,10 @@ const Markdown = ({ content, title }: MarkdownProps) => {
h4: ({ children }) => <p className={styles.header}>{children}</p>,
h5: ({ children }) => <p className={styles.header}>{children}</p>,
h6: ({ children }) => <p className={styles.header}>{children}</p>,
img: ({ src }) => <span>{src}</span>,
img: ({ src, alt }) => {
const displayText = src || alt || 'image';
return <span>{displayText}</span>;
},
video: ({ src }) => <span>{src}</span>,
iframe: ({ src }) => <span>{src}</span>,
source: ({ src }) => <span>{src}</span>,
+91 -66
View File
@@ -1,21 +1,21 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Comment, setAccount, useAccount, useAccountComment, useComment, useEditedComment, usePublishComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { Comment, setAccount, useAccount, useAccountComment, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
import { formatMarkdown } from '../../lib/utils/post-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 usePublishReply from '../../hooks/use-publish-reply';
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';
import usePublishPostStore from '../../stores/use-publish-post-store';
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import usePublishPost from '../../hooks/use-publish-post';
import usePublishReply from '../../hooks/use-publish-reply';
import FileUploader from '../../plugins/file-uploader';
import styles from './post-form.module.css';
import { Capacitor } from '@capacitor/core';
import _ from 'lodash';
const isAndroid = Capacitor.getPlatform() === 'android';
@@ -36,12 +36,14 @@ export const LinkTypePreviewer = ({ link }: { link: string }) => {
const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: string }) => {
const { t } = useTranslation();
const params = useParams();
const account = useAccount();
const [url, setUrl] = useState('');
const author = account?.author || {};
const { displayName } = author || {};
const [url, setUrl] = useState('');
const { publishCommentOptions, setPublishPostStore, resetPublishPostStore } = usePublishPostStore();
const { index, publishComment } = usePublishComment(publishCommentOptions);
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
const { setPublishPostOptions, postIndex, publishPost, publishPostOptions, resetPublishPostOptions } = usePublishPost({ subplebbitAddress });
const textRef = useRef<HTMLTextAreaElement>(null);
const urlRef = useRef<HTMLInputElement>(null);
@@ -69,23 +71,18 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
};
const hasCalledAnonAddressRef = useRef(false);
const getAnonAddressForPost = useCallback(async () => {
if (anonMode) {
if (!hasCalledAnonAddressRef.current) {
hasCalledAnonAddressRef.current = true;
const newSigner = (await getNewSigner()) || {};
setPublishPostStore({
signer: newSigner,
author: {
address: newSigner.address,
displayName: displayName || undefined,
},
});
}
const newSigner = (await getNewSigner()) || {};
setPublishPostOptions({
signer: newSigner,
author: {
address: newSigner.address,
displayName: displayName || undefined,
},
});
} else {
setPublishPostStore({
setPublishPostOptions({
signer: undefined,
author: {
address: account?.author?.address,
@@ -93,7 +90,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
},
});
}
}, [anonMode, getNewSigner, account, setPublishPostStore, displayName]);
}, [anonMode, getNewSigner, account, setPublishPostOptions, displayName]);
const onPublishPost = () => {
const currentTitle = subjectRef.current?.value.trim() || '';
@@ -109,7 +106,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
return;
}
if ((isInAllView || isInSubscriptionsView) && !publishCommentOptions.subplebbitAddress) {
if ((isInAllView || isInSubscriptionsView) && !publishPostOptions.subplebbitAddress) {
alert(t('no_board_selected_warning'));
return;
}
@@ -126,27 +123,40 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
}
publishComment();
publishPost();
};
const params = useParams();
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
useEffect(() => {
if (subplebbitAddress) {
setPublishPostStore({ subplebbitAddress });
}
}, [subplebbitAddress, setPublishPostStore]);
// redirect to pending page when pending comment is created
const navigate = useNavigate();
useEffect(() => {
if (typeof index === 'number') {
resetPublishPostStore();
if (typeof postIndex === 'number') {
resetPublishPostOptions();
resetFields();
navigate(`/profile/${index}`);
navigate(`/profile/${postIndex}`);
}
}, [index, resetPublishPostStore, navigate]);
}, [postIndex, resetPublishPostOptions, navigate]);
useEffect(() => {
if (anonMode) {
setPublishPostOptions({
signer: undefined,
author: {
address: undefined,
displayName: displayName || undefined,
},
});
getAnonAddressForPost();
} else {
setPublishPostOptions({
signer: undefined,
author: {
...account?.author,
displayName: displayName || account?.author?.displayName,
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [anonMode]);
// in post page, publish a reply to the post
const isInPostView = isPostPageView(location.pathname, params);
@@ -154,19 +164,18 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const { setPublishReplyOptions, resetPublishReplyOptions, replyIndex, publishReply } = usePublishReply({ cid, subplebbitAddress });
const getAnonAddressForReply = useCallback(async () => {
if (anonMode && !hasCalledAnonAddressRef.current) {
hasCalledAnonAddressRef.current = true;
const existingSigner = await getExistingSigner(address);
if (existingSigner) {
setPublishReplyOptions({
signer: existingSigner,
author: {
address: existingSigner.address,
displayName: displayName || undefined,
},
});
} else {
const newSigner = await getNewSigner();
const existingSigner = await getExistingSigner(address);
if (existingSigner) {
setPublishReplyOptions({
signer: existingSigner,
author: {
address: existingSigner.address,
displayName: displayName || undefined,
},
});
} else {
const newSigner = await getNewSigner();
if (newSigner) {
setPublishReplyOptions({
signer: newSigner,
author: {
@@ -176,11 +185,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
});
}
}
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, anonMode, displayName]);
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, displayName]);
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const formattedContent = formatMarkdown(e.target.value);
isInPostView ? setPublishReplyOptions({ content: formattedContent }) : setPublishPostStore({ content: formattedContent });
isInPostView ? setPublishReplyOptions({ content: formattedContent }) : setPublishPostOptions({ content: formattedContent });
};
const onPublishReply = () => {
@@ -216,7 +225,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
getAnonAddressForPost();
}
}
}, [anonMode, getAnonAddressForPost, getAnonAddressForReply, isInPostView]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [anonMode, isInPostView]);
// on android, auto upload file to image hosting sites with open api
const [isUploading, setIsUploading] = useState(false);
@@ -231,7 +241,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
if (urlRef.current) {
urlRef.current.value = result.url;
}
isInPostView ? setPublishReplyOptions({ link: result.url || undefined }) : setPublishPostStore({ link: result.url || undefined });
isInPostView ? setPublishReplyOptions({ link: result.url || undefined }) : setPublishPostOptions({ link: result.url || undefined });
if (result.fileName) {
setUploadedFileName(result.fileName);
}
@@ -248,6 +258,18 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
};
const hasInitializedDisplayName = useRef(false);
useEffect(() => {
if (displayName && !hasInitializedDisplayName.current) {
hasInitializedDisplayName.current = true;
if (isInPostView) {
setPublishReplyOptions({ displayName });
} else {
setPublishPostOptions({ displayName });
}
}
}, [displayName, isInPostView, setPublishReplyOptions, setPublishPostOptions]);
return (
<table className={styles.postFormTable}>
<tbody>
@@ -259,11 +281,12 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
placeholder={!displayName ? _.capitalize(t('anonymous')) : undefined}
defaultValue={displayName || undefined}
onChange={(e) => {
setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } });
const newDisplayName = e.target.value.trim() || undefined;
setAccount({ ...account, author: { ...account?.author, displayName: newDisplayName } });
if (isInPostView) {
setPublishReplyOptions({ displayName: e.target.value || undefined });
setPublishReplyOptions({ displayName: newDisplayName });
} else {
setPublishPostStore({ displayName: e.target.value || undefined });
setPublishPostOptions({ displayName: newDisplayName });
}
}}
/>
@@ -282,7 +305,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
type='text'
ref={subjectRef}
onChange={(e) => {
setPublishPostStore({ title: e.target.value || undefined });
setPublishPostOptions({ title: e.target.value });
}}
/>
<button onClick={onPublishPost}>{t('post')}</button>
@@ -307,7 +330,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
disabled={isUploading}
onChange={(e) => {
setUrl(e.target.value);
isInPostView ? setPublishReplyOptions({ link: e.target.value || undefined }) : setPublishPostStore({ link: e.target.value || undefined });
isInPostView ? setPublishReplyOptions({ link: e.target.value }) : setPublishPostOptions({ link: e.target.value });
}}
/>
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} />}</span>
@@ -331,7 +354,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
<label>
<input
type='checkbox'
onChange={(e) => (isInPostView ? setPublishReplyOptions({ spoiler: e.target.checked }) : setPublishPostStore({ spoiler: e.target.checked }))}
onChange={(e) => (isInPostView ? setPublishReplyOptions({ spoiler: e.target.checked }) : setPublishPostOptions({ spoiler: e.target.checked }))}
/>
{_.capitalize(t('spoiler'))}?
</label>
@@ -342,7 +365,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
<tr>
<td>{t('board')}</td>
<td>
<select onChange={(e) => setPublishPostStore({ subplebbitAddress: e.target.value })} value={subplebbitAddress}>
<select onChange={(e) => setPublishPostOptions({ subplebbitAddress: e.target.value })} value={subplebbitAddress}>
<option value=''>{t('choose_one')}</option>
{isInAllView &&
defaultSubplebbitAddresses.map((address: string) => (
@@ -388,7 +411,9 @@ const PostForm = () => {
const [showForm, setShowForm] = useState(false);
const subplebbit = useSubplebbit({ subplebbitAddress: params?.subplebbitAddress });
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
const subplebbit = useSubplebbit({ subplebbitAddress });
const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit);
return (
+54 -24
View File
@@ -47,34 +47,29 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
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;
const existingSigner = await getExistingSigner(address);
if (existingSigner) {
const existingSigner = await getExistingSigner(address);
if (existingSigner) {
setPublishReplyOptions({
signer: existingSigner,
author: {
address: existingSigner.address,
displayName: displayName || undefined,
},
});
} else {
const newSigner = await getNewSigner();
if (newSigner) {
setPublishReplyOptions({
signer: existingSigner,
signer: newSigner,
author: {
address: existingSigner.address,
address: newSigner.address,
displayName: displayName || undefined,
},
});
} else {
const newSigner = await getNewSigner();
if (newSigner) {
setPublishReplyOptions({
signer: newSigner,
author: {
address: newSigner.address,
displayName: displayName || undefined,
},
});
}
}
}
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, anonMode, displayName]);
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, displayName]);
const [error, setError] = useState<string | null>(null);
@@ -98,9 +93,25 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
useEffect(() => {
if (anonMode) {
setPublishReplyOptions({
signer: undefined,
author: {
address: undefined,
displayName: displayName || undefined,
},
});
getAnonAddressForReply();
} else {
setPublishReplyOptions({
signer: undefined,
author: {
...account?.author,
displayName: displayName || account?.author?.displayName,
},
});
}
}, [anonMode, getAnonAddressForReply]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [anonMode]);
useEffect(() => {
if (typeof replyIndex === 'number') {
@@ -151,8 +162,19 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
textRef.current.focus();
}
}, 0);
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}
}, [showReplyModal]);
}, [showReplyModal, closeModal]);
useEffect(() => {
if (textRef.current) {
@@ -222,6 +244,14 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
}
};
const hasInitializedDisplayName = useRef(false);
useEffect(() => {
if (displayName && !hasInitializedDisplayName.current) {
hasInitializedDisplayName.current = true;
setPublishReplyOptions({ displayName });
}
}, [displayName, setPublishReplyOptions]);
const modalContent = (
<div className={styles.container} ref={nodeRef}>
<div className={`replyModalHandle ${styles.title}`}>
@@ -243,7 +273,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
placeholder={displayName ? undefined : _.capitalize(t('name'))}
onChange={(e) => {
setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } });
setPublishReplyOptions({ displayName: e.target.value || undefined });
setPublishReplyOptions({ displayName: e.target.value });
}}
/>
</div>
@@ -254,7 +284,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
placeholder={_.capitalize(t('link'))}
onChange={(e) => {
setUrl(e.target.value);
setPublishReplyOptions({ link: e.target.value || undefined });
setPublishReplyOptions({ link: e.target.value });
}}
/>
</div>
@@ -9,6 +9,7 @@ import useInterfaceSettingsStore from '../../../stores/use-interface-settings-st
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
import useSpecialThemeStore from '../../../stores/use-special-theme-store';
import { isChristmas } from '../../../lib/utils/time-utils';
const commitRef = process.env.REACT_APP_COMMIT_REF;
const isElectron = window.isElectron === true;
@@ -71,10 +72,7 @@ const CheckForUpdates = () => {
const Style = () => {
const [theme, setTheme] = useTheme();
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
const today = new Date();
const month = today.getMonth();
const day = today.getDate();
const isChristmas = (month === 11 && day >= 24) || (month === 0 && day <= 5);
const isChristmasTime = isChristmas();
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newTheme = e.target.value;
@@ -96,7 +94,7 @@ const Style = () => {
<option value='burichan'>Burichan</option>
<option value='tomorrow'>Tomorrow</option>
<option value='photon'>Photon</option>
{isChristmas && <option value='special'>Special</option>}
{isChristmasTime && <option value='special'>Special</option>}
</select>
);
};
@@ -21,6 +21,19 @@ const SettingsModal = () => {
navigate(newPath);
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [closeModal]);
const [showInterfaceSettings, setShowInterfaceSettings] = useState(false);
const [showAccountSettings, setShowAccountSettings] = useState(false);
const [showAvatarSettings, setShowAvatarSettings] = useState(false);
+75
View File
@@ -0,0 +1,75 @@
import { useCallback } from 'react';
import { Comment, useAccount, usePublishComment } from '@plebbit/plebbit-react-hooks';
import useAnonMode from './use-anon-mode';
import usePublishPostStore from '../stores/use-publish-post-store';
const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
const account = useAccount();
const { anonMode } = useAnonMode();
const { author, signer, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
author: state.author,
signer: state.signer,
title: state.title || undefined,
content: state.content || undefined,
link: state.link || undefined,
spoiler: state.spoiler || false,
publishCommentOptions: state.publishCommentOptions,
}));
const setPublishPostStore = usePublishPostStore((state) => state.setPublishPostStore);
const resetPublishPostStore = usePublishPostStore((state) => state.resetPublishPostStore);
const createBaseOptions = useCallback(() => {
const baseOptions: Comment = {
subplebbitAddress,
title,
content,
link,
spoiler,
};
if (anonMode) {
baseOptions.author = {
address: signer?.address,
displayName: author?.displayName,
};
baseOptions.signer = signer;
} else {
baseOptions.author = {
...account?.author,
displayName: author?.displayName || account?.author?.displayName,
};
}
return baseOptions;
}, [anonMode, author, content, link, signer, spoiler, subplebbitAddress, title, account]);
const setPublishPostOptions = useCallback(
(options: Partial<Comment>) => {
const baseOptions = createBaseOptions();
const sanitizedOptions = Object.entries(options).reduce((acc, [key, value]) => {
acc[key] = value === '' ? undefined : value;
return acc;
}, {} as Partial<Comment>);
const newOptions = { ...baseOptions, ...sanitizedOptions };
setPublishPostStore(newOptions);
},
[createBaseOptions, setPublishPostStore],
);
const resetPublishPostOptions = useCallback(() => resetPublishPostStore(), [resetPublishPostStore]);
const { index, publishComment } = usePublishComment(publishCommentOptions);
return {
setPublishPostOptions,
resetPublishPostOptions,
postIndex: index,
publishPost: publishComment,
publishPostOptions: publishCommentOptions,
};
};
export default usePublishPost;
+14 -12
View File
@@ -17,8 +17,8 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
publishCommentOptions: state.publishCommentOptions[parentCid],
}));
const setReplyStore = usePublishReplyStore((state) => state.setReplyStore);
const resetReplyStore = usePublishReplyStore((state) => state.resetReplyStore);
const setPublishReplyStore = usePublishReplyStore((state) => state.setPublishReplyStore);
const resetPublishReplyStore = usePublishReplyStore((state) => state.resetPublishReplyStore);
const createBaseOptions = useCallback(() => {
const baseOptions: Comment = {
@@ -30,15 +30,17 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
spoiler,
};
const authorOptions = {
displayName: author?.displayName,
address: anonMode ? signer?.address : account?.author?.address,
};
baseOptions.author = authorOptions;
if (anonMode) {
baseOptions.author = {
address: signer?.address,
displayName: author?.displayName,
};
baseOptions.signer = signer;
} else {
baseOptions.author = {
...account?.author,
displayName: author?.displayName || account?.author?.displayName,
};
}
return baseOptions;
@@ -53,12 +55,12 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
}, {} as Partial<Comment>);
const newOptions = { ...baseOptions, ...sanitizedOptions };
setReplyStore(newOptions);
setPublishReplyStore(newOptions);
},
[createBaseOptions, setReplyStore],
[createBaseOptions, setPublishReplyStore],
);
const resetPublishReplyOptions = useCallback(() => resetReplyStore(parentCid), [parentCid, resetReplyStore]);
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
const { index, publishComment } = usePublishComment(publishCommentOptions);
+5 -5
View File
@@ -7,6 +7,7 @@ import useInitialTheme from './use-initial-theme';
import { nsfwTags } from '../views/home/home';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import useSpecialThemeStore from '../stores/use-special-theme-store';
import { isChristmas } from '../lib/utils/time-utils';
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
@@ -39,16 +40,15 @@ const useTheme = (): [string, (theme: string) => void] => {
// Check for Christmas and initialize special theme if needed
useEffect(() => {
const today = new Date();
const month = today.getMonth();
const day = today.getDate();
const isChristmas = (month === 11 && day >= 24) || (month === 0 && day <= 5);
const isChristmasTime = isChristmas();
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
if (isChristmas && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView) {
if (isChristmasTime && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView) {
setIsEnabled(true);
setCurrentTheme('tomorrow');
updateThemeClass('tomorrow');
} else if (!isChristmasTime && isEnabled) {
setIsEnabled(false);
}
}, [isEnabled, setIsEnabled, params, pendingPostSubplebbitAddress, location.pathname, isInAllView, isInSubscriptionsView]);
+10
View File
@@ -26,6 +26,16 @@ hr {
color: var(--post-greentext-color);
}
.spoilertext {
color: #000 !important;
background: #000 !important;
text-decoration: none;
}
.spoilertext:hover {
color: #fff !important;
}
.capitalize {
text-transform: capitalize;
}
+1 -1
View File
@@ -102,5 +102,5 @@ export const shouldShowSnow = (): boolean => {
const today = new Date();
const month = today.getMonth();
const day = today.getDate();
return (month === 11 && day >= 24) || (month === 0 && day <= 5);
return month === 11 && (day === 24 || day === 25);
};
+7
View File
@@ -68,3 +68,10 @@ export const getFormattedTimeAgo = (unixTimestamp: number): string => {
}
return t('time_x_years_ago', { count: Math.floor(timeDifference / 31104000) });
};
export const isChristmas = (): boolean => {
const today = new Date();
const month = today.getMonth();
const day = today.getDate();
return month === 11 && (day === 24 || day === 25);
};
+34 -27
View File
@@ -1,4 +1,4 @@
import { PublishCommentOptions } from '@plebbit/plebbit-react-hooks';
import { ChallengeVerification, Comment, PublishCommentOptions } from '@plebbit/plebbit-react-hooks';
import { create } from 'zustand';
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
import useChallengesStore from './use-challenges-store';
@@ -29,45 +29,52 @@ const usePublishPostStore = create<SubmitState>((set) => ({
link: undefined,
spoiler: undefined,
publishCommentOptions: {},
setPublishPostStore: ({ author, displayName, signer, subplebbitAddress, title, content, link, spoiler }) =>
set((state) => {
const nextState = { ...state };
if (author !== undefined) nextState.author = author;
if (displayName !== undefined) nextState.displayName = displayName;
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;
if (link !== undefined) nextState.link = link || undefined;
if (spoiler !== undefined) nextState.spoiler = spoiler || undefined;
setPublishPostStore: (comment: Comment) =>
set(() => {
const { subplebbitAddress, author, content, link, signer, spoiler, title } = comment;
const displayName = 'displayName' in comment ? comment.displayName || undefined : author?.displayName;
const baseAuthor = author ? { ...author } : {};
delete baseAuthor.displayName;
const updatedAuthor = displayName ? { ...baseAuthor, displayName } : baseAuthor;
const publishCommentOptions: PublishCommentOptions = {
subplebbitAddress: nextState.subplebbitAddress,
title: nextState.title,
content: nextState.content,
link: nextState.link,
spoiler: nextState.spoiler,
subplebbitAddress,
title,
content,
link,
spoiler,
onChallenge: (...args: any) => addChallenge(args),
onChallengeVerification: alertChallengeVerificationFailed,
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
alertChallengeVerificationFailed(challengeVerification, comment);
},
onError: (error: Error) => {
console.error(error);
alert(error.message);
},
};
if (nextState.signer) {
publishCommentOptions.signer = nextState.signer;
if (Object.keys(updatedAuthor).length > 0) {
publishCommentOptions.author = updatedAuthor;
}
if (nextState.author || nextState.displayName) {
publishCommentOptions.author = {
...nextState.author,
displayName: nextState.displayName,
};
if (signer) {
publishCommentOptions.signer = signer;
}
nextState.publishCommentOptions = publishCommentOptions;
return nextState;
return {
author: updatedAuthor,
displayName,
signer,
subplebbitAddress,
title,
content,
link,
spoiler,
publishCommentOptions,
};
}),
resetPublishPostStore: () =>
set({
+4 -4
View File
@@ -11,8 +11,8 @@ type ReplyState = {
signer: { [parentCid: string]: any | undefined };
spoiler: { [parentCid: string]: boolean | undefined };
publishCommentOptions: { [parentCid: string]: PublishCommentOptions | undefined };
setReplyStore: (comment: Comment) => void;
resetReplyStore: (parentCid: string) => void;
setPublishReplyStore: (comment: Comment) => void;
resetPublishReplyStore: (parentCid: string) => void;
};
const { addChallenge } = useChallengesStore.getState();
@@ -26,7 +26,7 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
spoiler: {},
publishCommentOptions: {},
setReplyStore: (comment: Comment) =>
setPublishReplyStore: (comment: Comment) =>
set((state) => {
const { subplebbitAddress, parentCid, author, content, link, signer, spoiler } = comment;
@@ -73,7 +73,7 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
};
}),
resetReplyStore: (parentCid) =>
resetPublishReplyStore: (parentCid) =>
set((state) => ({
author: { ...state.author, [parentCid]: undefined },
displayName: { ...state.displayName, [parentCid]: undefined },
+14 -1
View File
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { isChristmas } from '../lib/utils/time-utils';
interface SpecialThemeStore {
isEnabled: boolean | null;
@@ -10,10 +11,22 @@ const useSpecialThemeStore = create(
persist<SpecialThemeStore>(
(set) => ({
isEnabled: null,
setIsEnabled: (value: boolean) => set({ isEnabled: value }),
setIsEnabled: (value: boolean) => {
if (value && !isChristmas()) {
return;
}
set({ isEnabled: value });
},
}),
{
name: 'Special-theme-storage',
onRehydrateStorage: () => {
return (state) => {
if (state && !isChristmas()) {
state.isEnabled = null;
}
};
},
},
),
);
+56 -14
View File
@@ -64,6 +64,12 @@ const FAQ = () => {
<li>
<HashLink to='#replyimage'>Can I reply with an image?</HashLink>
</li>
<li>
<HashLink to='#quote'>How do I quote somebody?</HashLink>
</li>
<li>
<HashLink to='#spoiler'>Can I mark a submission as a spoiler?</HashLink>
</li>
</ul>
</ul>
</li>
@@ -87,19 +93,22 @@ const FAQ = () => {
<dt id='howaccess'>How do I access the boards?</dt>
<dd>
Anyone can access any board at a any time by simply knowing its address. Paste it in the search box, located in the homepage or at the top of the board
pages. Hit enter, and you will connect peer-to-peer to the board owner. Each board is completely independent and moderates itself, the board admin has
full ownership of their board.
pages. Hit enter, and you will connect peer-to-peer to the board. Each board is completely independent and moderates itself, the board owner can do
whatever they want with it.
</dd>
<dt id='whatbasics'>What should I know before I post?</dt>
<dd>
Whatever community you decide to post to, please remember to read the rules and guidelines of that board. Each board is independent and has its own
rules and guidelines. If you are unsure about the rules, you should try to ask the board owner or the community.
Please remember to read the rules and guidelines of whatever board you decide to post to. Each board is completely independent and has its own rules and
guidelines, as there are no global admins nor global rules.
</dd>
<dt id='postanon'>How do I post anonymously?</dt>
<dd>
To post as "Anonymous", simply do not fill in the [Name] field when submitting content. Plebchan uses the Plebbit protocol to function, which does not
leak IP addresses of people who post. This means that when you post on Plebchan, no board admin can know your IP address, nor can the app itself.
However, the Plebbit protocol is not fully anonymous, it uses IPFS, which means your IP address is part of a public P2P swarm, similarly to BitTorrent.
To post as "Anonymous", simply do not fill in the [Name] field when submitting content.
<br />
<br />
Plebchan uses the Plebbit protocol to function, which does not leak IP addresses of people who post. When you post on Plebchan, no board admin can know
your IP address, nor can the app itself, which is just static HTML. However, the Plebbit protocol is not fully anonymous, it uses IPFS, which means your
IP address is part of a public P2P swarm, similarly to how BitTorrent works.
</dd>
<dt id='register'>Can I register a username?</dt>
<dd>
@@ -113,9 +122,9 @@ const FAQ = () => {
</dd>
<dt id='howimage'>How do I post an image?</dt>
<dd>
You need a link to the image, ideally using an image hosting service, like Imgur. Paste the link to the image in the [Link] field when submitting
content. Plebchan will attempt to load the media from the link and show its type next to the [Link] field. If the link type is "webpage", the link is
not an image, and you should try another link.
You need a link to the image, ideally using an image hosting service, like Imgur or catbox.moe. Paste the link to the image in the [Link] field when
submitting content. Plebchan will attempt to load the media from the link and show its type next to the [Link] field. If the link type is "webpage", the
link is not an image, and you should try another link.
<br />
<br />
You can also post videos, audios and gifs by pasting their direct links. Plebchan also supports the following websites to embed media without a direct
@@ -123,14 +132,24 @@ const FAQ = () => {
</dd>
<dt id='uploadimage'>Can I upload an image?</dt>
<dd>
No, because Plebchan is a client for the Plebbit protocol, which is text-only (including links, from which media is embedded by clients). However,
Plebbit uses IPFS, so in theory Plebchan could upload media to IPFS, and then post the direct IPFS link for the media. This is not enabled on Plebchan
because loading media from IPFS is extremely slow, at the moment (because most people have slow internet).
Yes, but only in the Android app, which you can download on <a href='https://github.com/plebbit/plebchan/releases/latest'>GitHub</a>. The app is able to
automatically upload media to image hosting services, like Imgur or catbox.moe, sharing your IP address with the image hosting service. This is not
possible in the browser, which can't make backend requests.
<br />
<br />
If you use the browser version, you should upload images to an image hosting service of your choice, and then past the image link in the [Link] field
when submitting content. You can paste any link, not just image links. You can also get an image link from social media, by right clicking the image and
selecting "Copy image URL", or by using tools such as <a href='https://cobalt.tools/'>cobalt</a>.<br />
<br />
The reason why uploading media directly to boards is not possible, is because Plebchan is a client for the Plebbit protocol, which is text-only
(including links, from which media is embedded by clients). However, Plebbit uses IPFS, so in theory it could let users upload media to the subplebbit
(board) owner's IPFS node, and then post the direct IPFS link for the media. This is not enabled because loading media from IPFS is extremely slow at
the moment (because most people have slow internet).
</dd>
<dt id='postimage'>Must I post an image?</dt>
<dd>
It depends on the board. Each board has its own rules, and a board owner might decide to only allow posts with images in their community. Plebchan
automatically filters out text-only threads in the catalog view, and you can disable this in the [Filters] menu.
automatically filters out text-only threads in the catalog view, to resemble an imageboard, and you can disable this in the [Filters] menu.
</dd>
<dt id='replyimage'>Can I reply with an image?</dt>
<dd>
@@ -138,6 +157,29 @@ const FAQ = () => {
ending in .png or .jpeg) in the "Link" field. Plebchan will attempt to load the image, and if it worked it will show the Link type as "image", next to
the field, before posting. If the Link type is "webpage", the link is not an image, and you should try another link.
</dd>
<dt id='quote'>How do I quote somebody?</dt>
<dd>
To quote a portion of text, simply place a pointer ("{'>'}") in front of the text you wish to highlight (ex. "
<span className='greentext'>{'>'}This is a quote</span>").
<br />
<br />
Unlike 4chan and other imageboards, Plebchan does <i>not</i> allow to quote more than one post at a time. You can only reply to one post at a time. This
is because Plebchan is a client for the Plebbit protocol, which is designed to be an alternative to Reddit-like social media, in which you can only
reply to one post at a time.
<br />
<br />
Further, post numbers are not possible on Plebchan, because Plebbit is fully decentralized (serverless) using IPFS, meaning there is no central database
to store post numbers, and it uses CIDs to load posts directly. Retrieving the CID from an hypothetical post number from the single board's database
would be far too expensive for the node to calculate.
<br />
<br />
</dd>
<dt id='spoiler'>Can I mark a submission as a spoiler?</dt>
<dd>
All boards allow you to mask plot-spoiling content. To mark your image as a spoiler, check the [x Spoiler?] box before submission. Spoilerizing text
makes it unreadable to others until they mouse over it. To spoilerize a comment, place {`<spoiler>`} tags around the text you wish to hide (ex. "
{`<spoiler>`}SPIKE DIES!{`</spoiler>`}").
</dd>
</dl>
</div>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
.content {
margin-bottom: 15px;
overflow: visible;
overflow-y: visible;
overflow-x: hidden;
}
.error {
+1 -1
View File
@@ -14584,7 +14584,7 @@ react-scripts@5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react-virtuoso@^4.12.3:
react-virtuoso@4.12.3:
version "4.12.3"
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.3.tgz#beecf0582b31058c5a6ed3ec58fc43fd780e5844"
integrity sha512-6X1p/sU7hecmjDZMAwN+r3go9EVjofKhwkUbVlL8lXhBZecPv9XVCkZ/kBPYOr0Mv0Vl5+Ziwgexg9Kh7+NNXQ==