Merge pull request #495 from plebbit/development

Development
This commit is contained in:
Tom (plebeius.eth)
2024-08-27 21:38:09 +02:00
committed by GitHub
14 changed files with 169 additions and 135 deletions
+18 -9
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils';
import useIsMobile from './hooks/use-is-mobile';
@@ -18,7 +18,6 @@ import SubplebbitStats from './components/subplebbit-stats';
import TopBar from './components/topbar';
import { timeFilterNames } from './hooks/use-time-filter';
import useTheme from './hooks/use-theme';
import useInitialTheme from './hooks/use-initial-theme';
const BoardLayout = () => {
const { accountCommentIndex, subplebbitAddress, timeFilterName } = useParams();
@@ -62,7 +61,23 @@ const BoardLayout = () => {
};
const GlobalLayout = () => {
useTheme();
const [theme, setTheme] = useState('');
const [currentTheme] = useTheme();
useEffect(() => {
if (currentTheme !== theme) {
setTheme(currentTheme);
}
}, [currentTheme, theme]);
useEffect(() => {
if (theme) {
document.body.classList.add(theme);
return () => {
document.body.classList.remove(theme);
};
}
}, [theme]);
return (
<>
@@ -73,12 +88,6 @@ const GlobalLayout = () => {
};
const App = () => {
const initialTheme = useInitialTheme();
useEffect(() => {
document.body.classList.add(initialTheme);
}, [initialTheme]);
return (
<div className={styles.app}>
<Routes>
+7 -6
View File
@@ -4,6 +4,7 @@ import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDis
import { Comment, PublishCommentEditOptions, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import styles from './edit-menu.module.css';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { formatMarkdown } from '../../lib/utils/post-utils';
import useChallengesStore from '../../stores/use-challenges-store';
import _ from 'lodash';
import useIsMobile from '../../hooks/use-is-mobile';
@@ -113,6 +114,11 @@ const EditMenu = ({ isAccountMod, isAccountCommentAuthor, isCommentAuthorMod, po
setIsEditMenuOpen(false);
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const formattedContent = formatMarkdown(e.target.value);
setPublishCommentEditOptions((state) => ({ ...state, content: formattedContent }));
};
return (
<>
<span className={`${styles.checkbox} ${isReply && styles.replyCheckbox}`} ref={refs.setReference} {...(cid && getReferenceProps())}>
@@ -140,12 +146,7 @@ const EditMenu = ({ isAccountMod, isAccountCommentAuthor, isCommentAuthorMod, po
</div>
{isContentEditorOpen && (
<div>
<textarea
className={styles.editTextarea}
value={publishCommentEditOptions.content ?? ''}
onChange={(e) => setPublishCommentEditOptions((state) => ({ ...state, content: e.target.value }))}
autoFocus={true}
/>
<textarea className={styles.editTextarea} defaultValue={publishCommentEditOptions.content ?? ''} onChange={handleContentChange} />
</div>
)}
</>
+1 -6
View File
@@ -168,12 +168,7 @@ const Markdown = ({ content, title }: MarkdownProps) => {
video: ({ src }) => <span>{src}</span>,
iframe: ({ src }) => <span>{src}</span>,
source: ({ src }) => <span>{src}</span>,
hr: () =>
!isInCatalogView && (
<div className={styles.hrWrapper}>
<hr />
</div>
),
hr: () => null,
a: ({ href, children }) => {
if (href && !isInCatalogView) {
const linkMediaInfo = getLinkMediaInfo(href);
+20 -12
View File
@@ -262,7 +262,7 @@ const PostMedia = ({ post }: PostProps) => {
const PostMessage = ({ post }: PostProps) => {
const { cid, content, deleted, edit, original, parentCid, postCid, reason, removed, state } = post || {};
// TODO: commentAuthor is not available outside of editedComment, update when available
// TODO: commentAuthor is not yet available outside of editedComment, wait for API to be updated
// const banned = !!post?.commentAuthor?.banExpiresAt;
const { t } = useTranslation();
const params = useParams();
@@ -287,17 +287,25 @@ const PostMessage = ({ post }: PostProps) => {
<blockquote className={styles.postMessage}>
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
{removed ? (
<Tooltip
children={<span className={styles.removedContent}>({t('this_post_was_removed')})</span>}
content={`${_.capitalize(t('reason'))}: "${reason}"`}
showTooltip={!!reason}
/>
reason ? (
<>
<span className={styles.redEditMessage}>({t('this_post_was_removed')})</span>
<br />
<br />
<span className={styles.grayEditMessage}>{`${_.capitalize(t('reason'))}: "${reason}"`}.</span>
</>
) : (
<span className={styles.grayEditMessage}>{_.capitalize(t('this_post_was_removed'))}.</span>
)
) : deleted ? (
<Tooltip
children={<span className={styles.deletedContent}>{t('user_deleted_this_post')}</span>}
content={reason && `${t('reason')}: ${reason}`}
showTooltip={!!reason}
/>
reason ? (
<>
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '}
<span className={styles.grayEditMessage}>{`${_.capitalize(t('reason'))}: "${reason}"`}.</span>
</>
) : (
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>
)
) : (
<>
{!showOriginal && <Markdown content={displayContent} />}
@@ -329,7 +337,7 @@ const PostMessage = ({ post }: PostProps) => {
)}
</>
)}
{/* TODO: commentAuthor is not available outside of editedComment, update when available */}
{/* TODO: commentAuthor is not yet available outside of editedComment, wait for API to be updated */}
{/* {banned && (
<span className={styles.removedContent}>
<br />
+15 -26
View File
@@ -15,6 +15,7 @@ import {
import { create } from 'zustand';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { 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';
@@ -164,7 +165,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
signer: newSigner,
author: {
address: newSigner.address,
displayName: account?.author?.displayName,
displayName: displayName || undefined,
},
});
}
@@ -173,11 +174,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
signer: undefined,
author: {
address: account?.author?.address,
displayName: account?.author?.displayName,
displayName: displayName || undefined,
},
});
}
}, [anonMode, getNewSigner, account, setSubmitStore]);
}, [anonMode, getNewSigner, account, setSubmitStore, displayName]);
const onPublishPost = async () => {
if (!title && !content && !link) {
@@ -189,16 +190,6 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
return;
}
if (!anonMode) {
setSubmitStore({
signer: undefined,
author: {
address: account?.author?.address,
displayName: account?.author?.displayName,
},
});
}
publishComment();
};
@@ -235,6 +226,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
signer: existingSigner,
author: {
address: existingSigner.address,
displayName: displayName || undefined,
},
});
} else {
@@ -243,11 +235,17 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
signer: newSigner,
author: {
address: newSigner.address,
displayName: displayName || undefined,
},
});
}
}
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, anonMode]);
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, anonMode, displayName]);
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const formattedContent = formatMarkdown(e.target.value);
isInPostView ? setPublishReplyOptions({ content: formattedContent }) : setSubmitStore({ content: formattedContent });
};
const onPublishReply = () => {
const currentContent = textRef.current?.value || '';
@@ -297,9 +295,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
onChange={(e) => {
setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } });
if (isInPostView) {
setPublishReplyOptions({ displayName: e.target.value });
setPublishReplyOptions({ displayName: e.target.value || undefined });
} else {
setSubmitStore({ displayName: e.target.value });
setSubmitStore({ displayName: e.target.value || undefined });
}
}}
/>
@@ -324,16 +322,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
<tr>
<td>{t('comment')}</td>
<td>
<textarea
cols={48}
rows={4}
wrap='soft'
ref={textRef}
onChange={(e) => {
const content = e.target.value.replace(/\n/g, '\n\n');
isInPostView ? setPublishReplyOptions({ content }) : setSubmitStore({ content });
}}
/>
<textarea cols={48} rows={4} wrap='soft' ref={textRef} onChange={handleContentChange} />
</td>
</tr>
<tr>
+18 -10
View File
@@ -210,17 +210,25 @@ const PostMessageMobile = ({ post }: PostProps) => {
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
{removed ? (
<Tooltip
children={<span className={styles.removedContent}>({t('this_post_was_removed')})</span>}
content={`${_.capitalize(t('reason'))}: "${reason}"`}
showTooltip={!!reason}
/>
reason ? (
<>
<span className={styles.redEditMessage}>({t('this_post_was_removed')})</span>
<br />
<br />
<span className={styles.grayEditMessage}>{`${_.capitalize(t('reason'))}: "${reason}"`}.</span>
</>
) : (
<span className={styles.grayEditMessage}>{_.capitalize(t('this_post_was_removed'))}.</span>
)
) : deleted ? (
<Tooltip
children={<span className={styles.deletedContent}>{t('user_deleted_this_post')}</span>}
content={reason && `${t('reason')}: ${reason}`}
showTooltip={!!reason}
/>
reason ? (
<>
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '}
<span className={styles.grayEditMessage}>{`${_.capitalize(t('reason'))}: "${reason}"`}.</span>
</>
) : (
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>
)
) : (
<>
{!showOriginal && <Markdown content={displayContent} />}
+5 -3
View File
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import Draggable from 'react-draggable';
import { setAccount, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { formatMarkdown } from '../../lib/utils/post-utils';
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
@@ -154,9 +155,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const contentWithoutPrefix = e.target.value.slice(contentPrefix.length).replace(/\n/g, '\n\n');
if (textRef.current && textRef.current.value !== contentWithoutPrefix) {
setPublishReplyOptions({ content: contentWithoutPrefix });
const contentWithoutPrefix = e.target.value.slice(contentPrefix.length);
const formattedContent = formatMarkdown(contentWithoutPrefix);
if (textRef.current && textRef.current.value !== formattedContent) {
setPublishReplyOptions({ content: formattedContent });
}
};
+22 -13
View File
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import useThemeStore from '../stores/use-theme-store';
import useDefaultSubplebbits from './use-default-subplebbits';
@@ -17,20 +18,28 @@ const useInitialTheme = () => {
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInPendingPostView = isPendingPostView(location.pathname, params);
if (isInPendingPostView) {
return currentTheme || 'yotsuba';
} else if (isInAllView || isInSubscriptionsView) {
return getTheme('sfw') || 'yotsuba-b';
} else if (isInHomeView || isInNotFoundView) {
return 'yotsuba';
} else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) {
return getTheme('nsfw') || 'yotsuba';
const initialTheme = useMemo(() => {
let theme = 'yotsuba';
if (isInPendingPostView) {
theme = currentTheme || 'yotsuba';
} else if (isInAllView || isInSubscriptionsView) {
theme = getTheme('sfw', false) || 'yotsuba-b'; // Add 'false' parameter
} else if (isInHomeView || isInNotFoundView) {
theme = 'yotsuba';
} else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) {
theme = getTheme('nsfw', false) || 'yotsuba'; // Add 'false' parameter
} else {
theme = getTheme('sfw', false) || 'yotsuba-b'; // Add 'false' parameter
}
}
return getTheme('sfw') || 'yotsuba-b';
}
return 'yotsuba';
return theme;
}, [isInPendingPostView, isInAllView, isInSubscriptionsView, isInHomeView, isInNotFoundView, subplebbitAddress, getTheme, currentTheme, subplebbits]);
return initialTheme;
};
export default useInitialTheme;
-6
View File
@@ -62,8 +62,6 @@ const useReplyStore = create<ReplyState>((set) => ({
},
};
console.log('Final publishCommentOptions:', publishCommentOptions);
return {
author: { ...state.author, [parentCid]: updatedAuthor },
displayName: { ...state.displayName, [parentCid]: displayName },
@@ -117,8 +115,6 @@ const useReply = ({ cid, subplebbitAddress }: { cid: string; subplebbitAddress:
signer: anonMode ? signer || options.signer : undefined,
};
console.log('Final options to set in state:', newOptions);
setReplyStore(newOptions as SetReplyStoreData);
},
[subplebbitAddress, parentCid, author, displayName, signer, content, link, spoiler, setReplyStore, anonMode],
@@ -126,8 +122,6 @@ const useReply = ({ cid, subplebbitAddress }: { cid: string; subplebbitAddress:
const resetPublishReplyOptions = useCallback(() => resetReplyStore(parentCid), [parentCid, resetReplyStore]);
console.log('Final options before publishing', publishCommentOptions);
const { index, publishComment } = usePublishComment(publishCommentOptions);
return { setPublishReplyOptions, resetPublishReplyOptions, replyIndex: index, publishReply: publishComment, setReplyStore };
+39 -38
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { isAllView, isSubscriptionsView } from '../lib/utils/view-utils';
import useThemeStore from '../stores/use-theme-store';
@@ -24,63 +24,64 @@ const useTheme = (): [string, (theme: string) => void] => {
const subplebbits = useDefaultSubplebbits();
const initialTheme = useInitialTheme();
const [theme, setLocalTheme] = useState<string>(() => initialTheme);
const [themesLoaded, setThemesLoaded] = useState(false);
useEffect(() => {
const loadAndApplyThemes = async () => {
await loadThemes();
setThemesLoaded(true);
};
loadAndApplyThemes();
}, [loadThemes]);
useEffect(() => {
if (!themesLoaded) return;
const [currentTheme, setCurrentTheme] = useState(initialTheme);
const getCurrentTheme = useCallback(() => {
const subplebbitAddress = params?.subplebbitAddress;
const isInAllView = isAllView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
let storedTheme = null;
if (isInAllView || isInSubscriptionsView) {
storedTheme = getTheme('sfw');
storedTheme = getTheme('sfw', false);
} else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) {
storedTheme = getTheme('nsfw');
storedTheme = getTheme('nsfw', false);
} else {
storedTheme = getTheme('sfw');
storedTheme = getTheme('sfw', false);
}
}
const themeToSet = storedTheme || initialTheme;
setLocalTheme(themeToSet);
updateThemeClass(themeToSet);
}, [initialTheme, location.pathname, params, getTheme, themesLoaded, subplebbits]);
return storedTheme || initialTheme;
}, [location.pathname, params, getTheme, subplebbits, initialTheme]);
const setSubplebbitTheme = async (newTheme: string) => {
const subplebbitAddress = params?.subplebbitAddress;
const isInAllView = isAllView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
useEffect(() => {
const newTheme = getCurrentTheme();
if (newTheme !== currentTheme) {
setCurrentTheme(newTheme);
updateThemeClass(newTheme);
}
}, [getCurrentTheme, currentTheme]);
if (isInAllView || isInSubscriptionsView) {
await setThemeStore('sfw', newTheme);
} else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) {
await setThemeStore('nsfw', newTheme);
} else {
useEffect(() => {
loadThemes();
}, [loadThemes]);
const setSubplebbitTheme = useCallback(
async (newTheme: string) => {
const subplebbitAddress = params?.subplebbitAddress;
const isInAllView = isAllView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
if (isInAllView || isInSubscriptionsView) {
await setThemeStore('sfw', newTheme);
} else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) {
await setThemeStore('nsfw', newTheme);
} else {
await setThemeStore('sfw', newTheme);
}
}
}
setLocalTheme(newTheme);
updateThemeClass(newTheme);
};
setCurrentTheme(newTheme);
updateThemeClass(newTheme);
},
[location.pathname, params, setThemeStore, subplebbits],
);
return [theme, setSubplebbitTheme];
return [currentTheme, setSubplebbitTheme];
};
export default useTheme;
+4
View File
@@ -30,6 +30,10 @@ hr {
text-transform: capitalize;
}
.uppercase {
text-transform: uppercase;
}
.red {
color: red !important;
}
+11
View File
@@ -20,3 +20,14 @@ export function getTextColorForBackground(rgb: string): string {
const brightness = r * 0.299 + g * 0.587 + b * 0.114;
return brightness > 125 ? 'black' : 'white';
}
export const formatMarkdown = (content: string): string => {
let md = content;
if (md) {
// Replace single newline with "/n&nbsp;/n" if followed by a newline
md = md.replace(/\n(?=\n)/g, '\n&nbsp;\n');
// Replace single newline with double newline if between two characters
md = md.replace(/\n(?=\S)/g, '\n\n');
}
return md;
};
+5 -3
View File
@@ -8,7 +8,7 @@ interface ThemeState {
};
currentTheme: string | null;
setTheme: (category: keyof ThemeState['themes'], theme: string) => void;
getTheme: (category: keyof ThemeState['themes']) => string | null;
getTheme: (category: keyof ThemeState['themes'], updateCurrentTheme?: boolean) => string | null;
loadThemes: () => Promise<void>;
}
@@ -29,10 +29,12 @@ const useThemeStore = create<ThemeState>((set: StoreApi<ThemeState>['setState'],
await themeStore.setItem(category, theme);
set({ themes: updatedThemes, currentTheme: theme });
},
getTheme: (category) => {
getTheme: (category, updateCurrentTheme = true) => {
const currentThemes = get().themes;
const theme = currentThemes[category] || null;
set({ currentTheme: theme });
if (updateCurrentTheme) {
set({ currentTheme: theme });
}
return theme;
},
loadThemes: async () => {
+4 -3
View File
@@ -441,13 +441,14 @@
cursor: pointer;
}
.removedContent, .deletedContent {
.redEditMessage {
text-transform: uppercase;
font-weight: 700;
color: red;
}
.removedContent {
text-transform: uppercase;
.grayEditMessage {
color: var(--post-mobile-abbr-text-color);
}
.backlink, .backlinkHash {