Merge pull request #189 from plebbit/development

Development
This commit is contained in:
plebeius.eth
2024-01-19 22:25:38 +01:00
committed by GitHub
13 changed files with 237 additions and 81 deletions

View File

@@ -27,12 +27,12 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "13.2.2",
"react-markdown": "^9.0.1",
"react-markdown": "9.0.1",
"react-router-dom": "6.16.0",
"react-scripts": "5.0.1",
"react-virtuoso": "4.6.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.0",
"rehype-sanitize": "6.0.0",
"remark-gfm": "4.0.0",
"tcp-port-used": "1.0.2",
"typescript": "5.2.2",
"zustand": "4.4.3"

View File

@@ -80,6 +80,9 @@ function App() {
</Route>
<Route element={pagesLayout}>
<Route path='/submit' element={<Submit />} />
<Route path='/about' element={<About />} />
<Route path='/p/all/about' element={<About />} />
<Route path='/p/:subplebbitAddress/c/:commentCid' element={<Post />} />
<Route path='/p/:subplebbitAddress/c/:commentCid/about' element={<About />} />

View File

@@ -7,10 +7,12 @@ import {
getAboutLink,
isAboutView,
isAllView,
isAllAboutView,
isAuthorView,
isAuthorCommentsView,
isAuthorSubmittedView,
isProfileDownvotedView,
isHomeAboutView,
isHomeView,
isInboxView,
isPendingView,
@@ -235,6 +237,7 @@ const HeaderTabs = () => {
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAuthorView = isAuthorView(location.pathname);
const isInHomeAboutView = isHomeAboutView(location.pathname);
const isInHomeView = isHomeView(location.pathname, params);
const isInInboxView = isInboxView(location.pathname);
const isInPendingView = isPendingView(location.pathname, params);
@@ -247,7 +250,7 @@ const HeaderTabs = () => {
if (isInPostView) {
return <CommentsButton />;
} else if (isInHomeView || (isInSubplebbitView && !isInSubplebbitSubmitView && !isInSubplebbitSettingsView) || isInAllView) {
} else if (isInHomeView || isInHomeAboutView || (isInSubplebbitView && !isInSubplebbitSubmitView && !isInSubplebbitSettingsView) || isInAllView) {
return <SortItems />;
} else if ((isInProfileView || isInAuthorView) && !isInPendingView) {
return <AuthorHeaderTabs />;
@@ -321,6 +324,7 @@ const Header = () => {
const isMobile = window.innerWidth < 768;
const isInAboutView = isAboutView(location.pathname);
const isInAllAboutView = isAllAboutView(location.pathname);
const isInAllView = isAllView(location.pathname);
const isInAuthorView = isAuthorView(location.pathname);
const isInHomeView = isHomeView(location.pathname, params);
@@ -343,7 +347,7 @@ const Header = () => {
isInHomeView ||
(isInSubplebbitView && !isInSubplebbitSubmitView && !isInSubplebbitSettingsView && !isInPostView && !isInAboutView) ||
(isInProfileView && !isInAboutView) ||
isInAllView ||
(isInAllView && !isInAllAboutView) ||
(isInAuthorView && !isInAboutView);
const logoSrc = isInSubplebbitView ? suggested?.avatarUrl : isInProfileView ? imageUrl : 'assets/logo/seedit.png';
const logoIsAvatar = (isInSubplebbitView && suggested?.avatarUrl) || (isInProfileView && imageUrl);
@@ -385,7 +389,7 @@ const Header = () => {
<div className={`${styles.tabs} ${hasFewTabs ? styles.fewTabs : ''}`}>
<ul className={styles.tabMenu}>
<HeaderTabs />
{(isInSubplebbitView || isInSubplebbitSubmitView || isInPostView) && <AboutButton />}
{(isInHomeView || isInAllView || isInAboutView || isInSubplebbitView || isInSubplebbitSubmitView || isInPostView) && <AboutButton />}
</ul>
</div>
)}

View File

@@ -258,11 +258,14 @@ a {
}
.footer {
font-family: arial, verdana, helvetica, sans-serif;
font: normal x-small verdana, arial, helvetica, sans-serif;
}
.footer ul {
text-align: left;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
list-style: none;
}
@@ -286,11 +289,32 @@ a {
}
.version {
margin: 14px 0 24px 0;
margin: 20px 0 24px 0;
text-align: center;
font-size: 10px;
}
.unstable a {
color: var(--yellow);
}
.desktopAd {
padding: 0px 0 15px 0;
margin-top: -5px;
}
.desktopAdSubtitle {
font: normal x-small verdana, arial, helvetica, sans-serif;
font-size: 11px;
color: var(--gray-strong);
}
.desktopAdLogo img {
height: 40px;
margin: 5px 10px 0 0;
float: left;
}
.footerSecondTitle {
margin-top: 10px;
}

View File

@@ -5,7 +5,7 @@ import { useAccount, useBlock, Role, useSubplebbitStats, useAccountComment } fro
import styles from './sidebar.module.css';
import { getFormattedDate, getFormattedTimeDuration, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { findSubplebbitCreator } from '../../lib/utils/user-utils';
import { isAboutView, isAllView, isHomeView, isPendingView, isPostView, isSubplebbitSettingsView, isSubplebbitsView } from '../../lib/utils/view-utils';
import { isAboutView, isAllView, isHomeAboutView, isHomeView, isPendingView, isPostView, isSubplebbitSettingsView, isSubplebbitsView } from '../../lib/utils/view-utils';
import Markdown from '../markdown';
import SearchBar from '../search-bar';
import SubscribeButton from '../subscribe-button';
@@ -109,6 +109,7 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
const params = useParams();
const isInAboutView = isAboutView(location.pathname);
const isInAllView = isAllView(location.pathname);
const isInHomeAboutView = isHomeAboutView(location.pathname);
const isInHomeView = isHomeView(location.pathname, params);
const isInPendingView = isPendingView(location.pathname, params);
const isInPostView = isPostView(location.pathname, params);
@@ -118,7 +119,8 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
const subplebbitCreator = findSubplebbitCreator(roles);
const creatorAddress = subplebbitCreator === 'anonymous' ? 'anonymous' : `${getShortAddress(subplebbitCreator)}`;
const submitRoute = isInHomeView || isInAllView ? '/submit' : isInPendingView ? `/p/${pendingPost?.subplebbitAddress}/submit` : `/p/${address}/submit`;
const submitRoute =
isInHomeView || isInHomeAboutView || isInAllView ? '/submit' : isInPendingView ? `/p/${pendingPost?.subplebbitAddress}/submit` : `/p/${address}/submit`;
const { blocked, unblock, block } = useBlock({ address });
@@ -152,13 +154,7 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
<div className={styles.nub} />
</div>
</Link>
<Link to='/communities/create' onClick={(e) => e.preventDefault()}>
<div className={styles.largeButton} onClick={alertCreateCommunity}>
{t('create_community')}
<div className={styles.nub} />
</div>
</Link>
{!isInHomeView && !isInAllView && !isInPendingView && !isInSubplebbitsView && (
{!isInHomeView && !isInHomeAboutView && !isInAllView && !isInPendingView && !isInSubplebbitsView && (
<div className={styles.titleBox}>
<Link className={styles.title} to={`/p/${address}`}>
{address}
@@ -200,9 +196,26 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
)}
{isModerator && <ModerationTools address={address} />}
{roles && <ModeratorsList roles={roles} />}
<Link to='/communities/create' onClick={(e) => e.preventDefault()}>
<div className={styles.largeButton} onClick={alertCreateCommunity}>
{t('create_community')}
<div className={styles.nub} />
</div>
</Link>
<div className={styles.desktopAd}>
<a className={styles.desktopAdLogo} href='https://github.com/plebbit/seedit/releases/latest' target='_blank' rel='noopener noreferrer'>
<img src='icon.png' alt='seedit mascot' />
</a>
<span className={styles.desktopAdSubtitle}>
<br />
...each community needs to be seeded.
<br />
...the desktop app seeds automatically!
</span>
</div>
<div className={styles.footer}>
<div className={styles.footerTitle}>{t('about')}</div>
<ul>
<li className={styles.footerTitle}>about</li>
<li>
<a href='https://plebbit.com' target='_blank' rel='noopener noreferrer'>
plebbit
@@ -224,9 +237,32 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
</a>
</li>
</ul>
<div className={`${styles.footerTitle} ${styles.footerSecondTitle}`}>apps & tools</div>
<ul>
<li>
<a href={`https://github.com/plebbit/seedit/releases/download/v${version}/seedit-${version}.AppImage`} target='_blank' rel='noopener noreferrer'>
download for linux
</a>
</li>
<li>
<a href={`https://github.com/plebbit/seedit/releases/download/v${version}/seedit.Portable.${version}.exe`} target='_blank' rel='noopener noreferrer'>
download for windows
</a>
</li>
<li>
<a href={`https://github.com/plebbit/seedit/releases/download/v${version}/seedit-${version}.dmg`} target='_blank' rel='noopener noreferrer'>
download for macOS
</a>
</li>
<li>
<a href={`https://github.com/plebbit/seedit/releases/latest`} target='_blank' rel='noopener noreferrer'>
download for android
</a>
</li>
</ul>
<div className={`${styles.version} ${commitRef ? styles.unstable : ''}`}>
<a href='https://github.com/plebbit/seedit/releases/latest' target='_blank' rel='noopener noreferrer'>
seedit {commitRef ? 'development build ' + commitRef : window.electron && window.electron.isElectron ? 'desktop' : 'web'} v{version} - GPL 2.0
<a href={`https://github.com/plebbit/seedit/releases/tag/v${version}`} target='_blank' rel='noopener noreferrer'>
seedit {commitRef ? 'dev build (unstable) ' + commitRef : window.electron && window.electron.isElectron ? 'desktop' : 'web'} v{version} - GPL 2.0
</a>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useAccount } from '@plebbit/plebbit-react-hooks';
import { getShortAddress } from '@plebbit/plebbit-js';
import styles from './topbar.module.css';
import { SubplebbitWithDisplay, useDefaultAndSubscriptionsSubplebbits } from '../../lib/utils/addresses-utils';
import { useDefaultSubplebbitAddresses } from '../../lib/utils/addresses-utils';
import useTimeFilter from '../../hooks/use-time-filter';
import { isAllView, isHomeView, isSubplebbitView } from '../../lib/utils/view-utils';
@@ -12,17 +12,18 @@ const sortTypes = ['hot', 'new', 'active', 'controversialAll', 'topAll'];
const TopBar = () => {
const account = useAccount();
const subplebbitAddresses = useDefaultAndSubscriptionsSubplebbits();
const subplebbitAddresses = useDefaultSubplebbitAddresses();
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
const { timeFilterNames } = useTimeFilter();
const selectedTimeFilterName = params.timeFilterName || timeFilterNames[5];
const subscriptions = account?.subscriptions;
const isAll = isAllView(location.pathname);
const isHome = isHomeView(location.pathname, params);
const isSubplebbit = isSubplebbitView(location.pathname, params);
const homeButtonClass = isHome ? styles.selected : styles.choice;
const isinAllView = isAllView(location.pathname);
const isInHomeView = isHomeView(location.pathname, params);
const isInSubplebbitView = isSubplebbitView(location.pathname, params);
const homeButtonClass = isInHomeView ? styles.selected : styles.choice;
const selectedTimeFilterName = params.timeFilterName || (isInHomeView ? timeFilterNames[2] : timeFilterNames[5]);
const [isSubsDropdownOpen, setIsSubsDropdownOpen] = useState(false);
const toggleSubsDropdown = () => setIsSubsDropdownOpen(!isSubsDropdownOpen);
@@ -46,9 +47,9 @@ const TopBar = () => {
const [selectedSortType, setSelectedSortType] = useState(params.sortType || '/hot');
const getTimeFilterLink = (choice: string) => {
return isSubplebbit
return isInSubplebbitView
? `/p/${params.subplebbitAddress}/${selectedSortType}/${choice}`
: isAll
: isinAllView
? `p/all/${selectedSortType}/${choice}`
: `/${selectedSortType}/${choice}`;
};
@@ -125,7 +126,7 @@ const TopBar = () => {
<span className={styles.selectedTitle}>{getSelectedSortLabel()}</span>
<div className={`${styles.dropChoices} ${styles.sortsDropChoices} ${sortsDropdownClass}`} ref={sortsDropdownChoicesRef}>
{sortTypes.map((choice, index) => (
<Link to={isSubplebbit ? `/p/${params.subplebbitAddress}/${choice}` : choice} key={index} className={styles.dropdownChoice}>
<Link to={isInSubplebbitView ? `/p/${params.subplebbitAddress}/${choice}` : choice} key={index} className={styles.dropdownChoice}>
{sortLabels[index]}
</Link>
))}
@@ -150,16 +151,16 @@ const TopBar = () => {
</li>
<li>
<span className={styles.separator}>-</span>
<Link to='/p/all' className={isAll ? styles.selected : styles.choice}>
<Link to='/p/all' className={isinAllView ? styles.selected : styles.choice}>
{t('all')}
</Link>
</li>
<span className={styles.separator}> | </span>
{subplebbitAddresses?.map((subplebbit: SubplebbitWithDisplay, index) => (
{subplebbitAddresses?.map((address, index) => (
<li key={index}>
{index !== 0 && <span className={styles.separator}>-</span>}
<Link to={`/p/${subplebbit.address}`} className={params.subplebbitAddress === subplebbit.address ? styles.selected : styles.choice}>
{subplebbit.displayAddress}
<Link to={`/p/${address}`} className={params.subplebbitAddress === address ? styles.selected : styles.choice}>
{address}
</Link>
</li>
))}

View File

@@ -21,6 +21,8 @@ export const getAboutLink = (pathname: string, params: ParamsType): string => {
return '/profile/about';
} else if (pathname.startsWith('/u/')) {
return `/u/${params.authorAddress}/c/${params.commentCid}/about`;
} else if (pathname.startsWith('/p/all')) {
return '/p/all/about';
} else {
return '/about';
}
@@ -34,6 +36,10 @@ export const isAllView = (pathname: string): boolean => {
return pathname.startsWith('/p/all');
};
export const isAllAboutView = (pathname: string): boolean => {
return pathname === '/p/all/about';
};
export const isAuthorView = (pathname: string): boolean => {
return pathname.startsWith('/u/');
};
@@ -51,7 +57,11 @@ export const isProfileDownvotedView = (pathname: string): boolean => {
};
export const isHomeView = (pathname: string, params: ParamsType): boolean => {
return pathname === '/' || sortTypes.includes(pathname) || (timeFilterNames.includes(params.timeFilterName as TimeFilterKey) && !pathname.startsWith('/p/all'));
return pathname === '/' || sortTypes.includes(pathname) || (timeFilterNames.includes(params.timeFilterName as TimeFilterKey) && !pathname.startsWith('/p/'));
};
export const isHomeAboutView = (pathname: string): boolean => {
return pathname === '/about';
};
export const isInboxView = (pathname: string): boolean => {

View File

@@ -9,6 +9,7 @@
--code-background: #fcfcfb;
--code-border: #e6e6de;
--red: red;
--gray-strong: dimgray;
--gray-footer: #777;
--gray-contrast: #888;
--gray-overlay: #F7F7F7;
@@ -68,6 +69,7 @@
--code-background: rgb(19, 19, 13);
--code-border: rgb(52, 58, 60);
--red: rgb(200, 0, 0);
--gray-strong: rgb(168, 160, 147);
--gray-footer: rgb(158, 149, 136);
--gray-contrast: #c7c7c7;
--gray-overlay: #1f1f1f;

View File

@@ -8,7 +8,7 @@
@media (max-width: 768px) {
.content {
padding: 7px 0px 0px 0px;
padding: 7px 5px 0px 0px;
}
.sidebar {

View File

@@ -27,7 +27,7 @@ const Home = () => {
const params = useParams<{ sortType?: string; timeFilterName?: string }>();
const sortType = params?.sortType || 'hot';
const timeFilterName = (params.timeFilterName as TimeFilterKey) || 'all';
const timeFilterName = (params.timeFilterName as TimeFilterKey) || '1w';
const { timeFilter } = useTimeFilter(sortType, timeFilterName);
const { feed, hasMore, loadMore } = useFeed({

View File

@@ -104,6 +104,21 @@
margin-top: 5px;
}
.challengeOption {
font-size: 15px;
margin-top: 10px !important;
}
.challengeDescription {
margin: 5px 0 15px 0;
font-size: 15px;
}
.challengeOptionDescription {
margin: 5px 0;
font-size: 12px;
}
.moderator {
font-size: 15px;
color: var(--text);

View File

@@ -146,67 +146,128 @@ const Moderators = ({ roles }: { roles: RolesCollection | undefined }) => {
);
};
const challenges = ['text-math', 'captcha-canvas-v3', 'fail', 'blacklist', 'question', 'evm-contract-call'];
const Challenge = ({ challenge, selected, setSelected }: { challenge: string; selected: string; setSelected: (challenge: string) => void }) => {
const { t } = useTranslation();
const [selectedChallenge, setSelectedChallenge] = useState('captcha');
const [selectedChallenge, setSelectedChallenge] = useState('none');
return (
<div className={styles.box}>
<div className={styles.boxTitle}>{t('challenge')}</div>
<div className={styles.boxSubtitle}>choose a challenge to prevent spam</div>
<div className={`${styles.boxInput} ${styles.captchaSelect}`}>
<select defaultValue='captcha' onChange={(e) => setSelectedChallenge(e.target.value)}>
<option value='captcha'>captcha</option>
<option value='karma'>karma</option>
<option value='token'>token</option>
<option value='password'>password</option>
<option value='custom'>custom</option>
<select defaultValue='none' onChange={(e) => setSelectedChallenge(e.target.value)}>
{challenges.map((challenge) => (
<option key={challenge} value={challenge}>
{challenge}
</option>
))}
<option value='none'>none</option>
</select>
{selectedChallenge === 'captcha' && (
{selectedChallenge === 'text-math' && (
<>
<br />
<label>
<input type='checkbox' /> case sensitive
</label>
<div className={styles.challengeDescription}>Ask a plain text math question, insecure, use ONLY for testing.</div>
<div className={styles.challengeOption}>
Difficulty
<div className={styles.challengeOptionDescription}>The math difficulty of the challenge between 1-3.</div>
<input type='number' defaultValue={1} placeholder='1' />
</div>
</>
)}
{selectedChallenge === 'karma' && (
{selectedChallenge === 'captcha-canvas-v3' && (
<>
<br />
<label>
Minimum karma: <input type='number' />
</label>
</>
)}
{selectedChallenge === 'token' && (
<>
<br />
<label>
Contract address (ERC20): <br />
<input type='text' />
</label>
<br />
<label>
Minumum balance: <br />
<div className={styles.challengeDescription}>make custom image captcha</div>
<div className={styles.challengeOption}>
Characters
<div className={styles.challengeOptionDescription}>Amount of characters of the captcha.</div>
<input type='number' />
</label>
</div>
<div className={styles.challengeOption}>
Width
<div className={styles.challengeOptionDescription}>Height of the captcha.</div>
<input type='number' />
</div>
<div className={styles.challengeOption}>
Height
<div className={styles.challengeOptionDescription}>Width of the captcha.</div>
<input type='number' />
</div>
<div className={styles.challengeOption}>
Color
<div className={styles.challengeOptionDescription}>Color of the captcha.</div>
<input type='color' />
</div>
</>
)}
{selectedChallenge === 'password' && (
{selectedChallenge === 'fail' && (
<>
<br />
<label>
Password (case sensitive): <input type='password' />
</label>
<div className={styles.challengeDescription}>A challenge that automatically fails with a custom error message.</div>
<div className={styles.challengeOption}>
Error
<div className={styles.challengeOptionDescription}>The error to display to the author.</div>
<input type='text' defaultValue="You're not allowed to publish." placeholder="You're not allowed to publish." />
</div>
</>
)}
{selectedChallenge === 'custom' && (
{selectedChallenge === 'blacklist' && (
<>
<br />
<label>
Paste code: <textarea />
</label>
<div className={styles.challengeDescription}>Blacklist author addresses.</div>
<div className={styles.challengeOption}>
Blacklist
<div className={styles.challengeOptionDescription}>Comma separated list of author addresses to be blacklisted.</div>
<input type='text' placeholder='address1.eth,address2.eth,address3.eth' />
</div>
<div className={styles.challengeOption}>
Error
<div className={styles.challengeOptionDescription}>The error to display to the author.</div>
<input type='text' defaultValue="You're blacklisted." placeholder="You're blacklisted." />
</div>
</>
)}
{selectedChallenge === 'question' && (
<>
<div className={styles.challengeDescription}>Ask a question, like 'What is the password?'</div>
<div className={styles.challengeOption}>
Question
<div className={styles.challengeOptionDescription}>The question to answer.</div>
<input type='text' />
</div>
<div className={styles.challengeOption}>
Answer
<div className={styles.challengeOptionDescription}>The answer to the question.</div>
<input type='text' />
</div>
</>
)}
{selectedChallenge === 'evm-contract-call' && (
<>
<div className={styles.challengeDescription}>The response from an EVM contract call passes a condition, e.g. a token balance challenge.</div>
<div className={styles.challengeOption}>
chainTicker
<div className={styles.challengeOptionDescription}>The chain ticker</div>
<input type='text' placeholder='eth' defaultValue='eth' />
</div>
<div className={styles.challengeOption}>
Address
<div className={styles.challengeOptionDescription}>The contract address.</div>
<input type='text' placeholder='0x...' />
</div>
<div className={styles.challengeOption}>
ABI
<div className={styles.challengeOptionDescription}>The ABI of the contract method.</div>
<textarea placeholder='{"constant":true,"inputs":[{"internalType":"address","name":"account...' autoCorrect='off' autoComplete='off' spellCheck='false' />
</div>
<div className={styles.challengeOption}>
Condition
<div className={styles.challengeOptionDescription}>The condition the contract call response must pass.</div>
<textarea placeholder='>1000' autoCorrect='off' autoComplete='off' spellCheck='false' />
</div>
<div className={styles.challengeOption}>
Error
<div className={styles.challengeOptionDescription}>The error to display to the author.</div>
<input type='text' defaultValue="Contract call response doesn't pass condition." placeholder="Contract call response doesn't pass condition." />
</div>
</>
)}
{selectedChallenge === 'none' && <span className={styles.noChallengeWarning}>Warning: vulnerable to spam attacks.</span>}

View File

@@ -13248,7 +13248,7 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-markdown@^9.0.1:
react-markdown@9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1"
integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==
@@ -13567,7 +13567,7 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
rehype-sanitize@^6.0.0:
rehype-sanitize@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz#16e95f4a67a69cbf0f79e113c8e0df48203db73c"
integrity sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==
@@ -13580,7 +13580,7 @@ relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
remark-gfm@^4.0.0:
remark-gfm@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de"
integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==