feat(markdown): when the user is publishing a comment, automatically format it to follow markdown rules

This commit is contained in:
Tom (plebeius.eth)
2024-08-27 18:46:42 +02:00
parent e9c7e868f2
commit c521ccbc2e
5 changed files with 31 additions and 33 deletions
+7 -5
View File
@@ -4,6 +4,7 @@ import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDis
import { Comment, PublishCommentEditOptions, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks'; import { Comment, PublishCommentEditOptions, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import styles from './edit-menu.module.css'; import styles from './edit-menu.module.css';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { formatMarkdown } from '../../lib/utils/post-utils';
import useChallengesStore from '../../stores/use-challenges-store'; import useChallengesStore from '../../stores/use-challenges-store';
import _ from 'lodash'; import _ from 'lodash';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
@@ -113,6 +114,11 @@ const EditMenu = ({ isAccountMod, isAccountCommentAuthor, isCommentAuthorMod, po
setIsEditMenuOpen(false); setIsEditMenuOpen(false);
}; };
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const formattedContent = formatMarkdown(e.target.value);
setPublishCommentEditOptions((state) => ({ ...state, content: formattedContent }));
};
return ( return (
<> <>
<span className={`${styles.checkbox} ${isReply && styles.replyCheckbox}`} ref={refs.setReference} {...(cid && getReferenceProps())}> <span className={`${styles.checkbox} ${isReply && styles.replyCheckbox}`} ref={refs.setReference} {...(cid && getReferenceProps())}>
@@ -140,11 +146,7 @@ const EditMenu = ({ isAccountMod, isAccountCommentAuthor, isCommentAuthorMod, po
</div> </div>
{isContentEditorOpen && ( {isContentEditorOpen && (
<div> <div>
<textarea <textarea className={styles.editTextarea} value={publishCommentEditOptions.content ?? ''} onChange={handleContentChange} />
className={styles.editTextarea}
value={publishCommentEditOptions.content ?? ''}
onChange={(e) => setPublishCommentEditOptions((state) => ({ ...state, content: e.target.value }))}
/>
</div> </div>
)} )}
</> </>
+2 -16
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import supersub from 'remark-supersub'; import supersub from 'remark-supersub';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
@@ -129,7 +128,7 @@ interface MarkdownProps {
} }
const Markdown = ({ content, title }: MarkdownProps) => { const Markdown = ({ content, title }: MarkdownProps) => {
const remarkPlugins: any[] = [[remarkBreaks, supersub]]; const remarkPlugins: any[] = [[supersub]];
if (content && content.length <= MAX_LENGTH_FOR_GFM) { if (content && content.length <= MAX_LENGTH_FOR_GFM) {
remarkPlugins.push([remarkGfm, { singleTilde: false }]); remarkPlugins.push([remarkGfm, { singleTilde: false }]);
@@ -151,19 +150,6 @@ const Markdown = ({ content, title }: MarkdownProps) => {
const isInCatalogView = isCatalogView(useLocation().pathname, useParams()); const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
const processedContent = useMemo(() => {
let md = content;
if (md) {
// Preserve newlines in code blocks
md = md.replace(/```[\s\S]*?```/g, (m) => m.replace(/\n/g, '\n '));
// Add an extra newline after blockquote lines
md = md.replace(/^(>.*)\n(?!>)/gm, '$1\n\n');
// Retain empty lines
md = md.replace(/(?<=\n\n)(?![*-])\n/g, '&nbsp;\n ');
}
return md;
}, [content]);
return ( return (
<span className={styles.markdown}> <span className={styles.markdown}>
{isInCatalogView && title && ( {isInCatalogView && title && (
@@ -173,7 +159,7 @@ const Markdown = ({ content, title }: MarkdownProps) => {
</span> </span>
)} )}
<ReactMarkdown <ReactMarkdown
children={processedContent} children={content}
remarkPlugins={remarkPlugins} remarkPlugins={remarkPlugins}
rehypePlugins={[[rehypeSanitize, customSchema]]} rehypePlugins={[[rehypeSanitize, customSchema]]}
components={{ components={{
+7 -10
View File
@@ -15,6 +15,7 @@ import {
import { create } from 'zustand'; import { create } from 'zustand';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { getLinkMediaInfo } from '../../lib/utils/media-utils'; import { getLinkMediaInfo } from '../../lib/utils/media-utils';
import { formatMarkdown } from '../../lib/utils/post-utils';
import { isValidURL } from '../../lib/utils/url-utils'; import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isDescriptionView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isDescriptionView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
@@ -241,6 +242,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
} }
}, [address, getExistingSigner, getNewSigner, setPublishReplyOptions, anonMode, displayName]); }, [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 onPublishReply = () => {
const currentContent = textRef.current?.value || ''; const currentContent = textRef.current?.value || '';
const currentUrl = urlRef.current?.value || ''; const currentUrl = urlRef.current?.value || '';
@@ -316,16 +322,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
<tr> <tr>
<td>{t('comment')}</td> <td>{t('comment')}</td>
<td> <td>
<textarea <textarea cols={48} rows={4} wrap='soft' ref={textRef} onChange={handleContentChange} />
cols={48}
rows={4}
wrap='soft'
ref={textRef}
onChange={(e) => {
const content = e.target.value;
isInPostView ? setPublishReplyOptions({ content }) : setSubmitStore({ content });
}}
/>
</td> </td>
</tr> </tr>
<tr> <tr>
+4 -2
View File
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { setAccount, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import { setAccount, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { formatMarkdown } from '../../lib/utils/post-utils';
import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils'; import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
@@ -155,8 +156,9 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const contentWithoutPrefix = e.target.value.slice(contentPrefix.length); const contentWithoutPrefix = e.target.value.slice(contentPrefix.length);
if (textRef.current && textRef.current.value !== contentWithoutPrefix) { const formattedContent = formatMarkdown(contentWithoutPrefix);
setPublishReplyOptions({ content: contentWithoutPrefix }); if (textRef.current && textRef.current.value !== formattedContent) {
setPublishReplyOptions({ content: formattedContent });
} }
}; };
+11
View File
@@ -20,3 +20,14 @@ export function getTextColorForBackground(rgb: string): string {
const brightness = r * 0.299 + g * 0.587 + b * 0.114; const brightness = r * 0.299 + g * 0.587 + b * 0.114;
return brightness > 125 ? 'black' : 'white'; 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;
};