mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(markdown): when the user is publishing a comment, automatically format it to follow markdown rules
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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, ' \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={{
|
||||||
|
|||||||
@@ -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,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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 /n" if followed by a newline
|
||||||
|
md = md.replace(/\n(?=\n)/g, '\n \n');
|
||||||
|
// Replace single newline with double newline if between two characters
|
||||||
|
md = md.replace(/\n(?=\S)/g, '\n\n');
|
||||||
|
}
|
||||||
|
return md;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user