mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+2
-1
@@ -165,4 +165,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.vscode
|
||||
.cursor
|
||||
.cursor
|
||||
.claude
|
||||
@@ -107,6 +107,26 @@ Discover and install skills from the open agent skills ecosystem.
|
||||
npx skills add https://github.com/vercel-labs/skills --skill find-skills
|
||||
```
|
||||
|
||||
### playwright-cli (browser automation)
|
||||
|
||||
Use **playwright-cli** for browser automation—navigation, form filling, screenshots, testing, and data extraction. The CLI + skill approach is token-efficient: it avoids loading large MCP tool schemas and verbose accessibility trees into context.
|
||||
|
||||
**Do NOT use browser MCPs** (cursor-ide-browser, playwright-mcp, chrome MCP, etc.) for this project. MCP servers inject heavy tool definitions and page snapshots into the context window, degrading quality and response speed.
|
||||
|
||||
**Install** (globally, once per machine). Agents working on this project without playwright-cli should run:
|
||||
|
||||
```bash
|
||||
npm install -g @playwright/cli@latest
|
||||
```
|
||||
|
||||
Then from the project root, install the skill so the agent can use it:
|
||||
|
||||
```bash
|
||||
playwright-cli install --skills
|
||||
```
|
||||
|
||||
The skill installs to `.cursor/skills/playwright-cli/` (or `.claude/skills/` for Claude Code). Agents invoke commands via Bash (e.g. `playwright-cli open`, `playwright-cli snapshot`, `playwright-cli click e1`).
|
||||
|
||||
## AI Agent Hooks (Recommended)
|
||||
|
||||
If you're using an AI coding assistant (Cursor, Claude Code, Codex, etc.), set up hooks to automatically enforce code quality. Most modern AI agents support lifecycle hooks.
|
||||
@@ -192,6 +212,10 @@ For Dependabot security alerts, GitHub Actions logs, issue/PR searches, or cross
|
||||
|
||||
If not available, suggest the user install it.
|
||||
|
||||
### Browser MCPs — Do NOT Use
|
||||
|
||||
**Avoid browser-related MCP servers** for this project (cursor-ide-browser, playwright-mcp, chrome MCP, etc.). Use **playwright-cli** (skill) instead—see [playwright-cli (browser automation)](#playwright-cli-browser-automation) above. Browser MCPs bloat the context window with tool schemas and page snapshots; the CLI + skill workflow is more token-efficient for coding agents.
|
||||
|
||||
### Context Window Warning
|
||||
|
||||
Each MCP server injects its tool definitions into the context window, consuming tokens even when the tools aren't being used. Too many servers will:
|
||||
|
||||
+2
-1
@@ -11,7 +11,7 @@
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#df3cb6901f63054870dc127cd7565c98617c2a4f",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#e197af2381ad651e5b74a93bcb705de78f94742c",
|
||||
"@react-spring/web": "9.7.5",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.25",
|
||||
@@ -138,6 +138,7 @@
|
||||
"wait-on": "9.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"axios": "1.13.5",
|
||||
"js-yaml": "4.1.1",
|
||||
"baseline-browser-mapping": "^2.9.11",
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
|
||||
@@ -102,8 +102,7 @@ const htmlSection = section('Static HTML build', [htmlZip && `- 5chan-html (zip)
|
||||
const downloads = [macSection, winSection, linuxSection, androidSection, htmlSection].filter(Boolean).join('\n\n');
|
||||
|
||||
// One-liner summary of what changed in this release. Update before each release.
|
||||
const oneLinerDescription =
|
||||
'This version adds multiple-replying support, 4chan-like user anonymity with or without user IDs depending on the board, and several bug fixes.';
|
||||
const oneLinerDescription = 'This version adds backlinks for quoted posts, improves reply functionality with proper quote references, and includes several bug fixes.';
|
||||
|
||||
const releaseBody = `${oneLinerDescription}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import CatalogSearch from '../catalog-search';
|
||||
import Tooltip from '../tooltip';
|
||||
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
|
||||
import styles from './board-buttons.module.css';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
interface BoardButtonsProps {
|
||||
address?: string | undefined;
|
||||
@@ -510,13 +510,13 @@ const PostPageStats = () => {
|
||||
const linkCount = useCountLinksInReplies(comment);
|
||||
|
||||
const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?';
|
||||
const replyCountTooltip = replyCount !== undefined ? _.capitalize(t('replies')) : t('loading');
|
||||
const replyCountTooltip = replyCount !== undefined ? capitalize(t('replies')) : t('loading');
|
||||
|
||||
return (
|
||||
<span>
|
||||
{pinned && `${_.capitalize(t('sticky'))} / `}
|
||||
{closed && `${_.capitalize(t('closed'))} / `}
|
||||
<Tooltip children={displayReplyCount} content={replyCountTooltip} /> / <Tooltip children={linkCount?.toString()} content={_.capitalize(t('links'))} />
|
||||
{pinned && `${capitalize(t('sticky'))} / `}
|
||||
{closed && `${capitalize(t('closed'))} / `}
|
||||
<Tooltip children={displayReplyCount} content={replyCountTooltip} /> / <Tooltip children={linkCount?.toString()} content={capitalize(t('links'))} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import Tooltip from '../tooltip';
|
||||
import _ from 'lodash';
|
||||
import { startCase } from 'lodash';
|
||||
import { BANNERS } from '../../generated/asset-manifest';
|
||||
|
||||
const ImageBanner = () => {
|
||||
@@ -78,7 +78,7 @@ const BoardHeader = () => {
|
||||
: isInSubscriptionsView
|
||||
? '/subs/ - Subscriptions'
|
||||
: isInModView
|
||||
? _.startCase(t('boards_you_moderate'))
|
||||
? startCase(t('boards_you_moderate'))
|
||||
: defaultSubplebbit?.title || stableSubplebbit?.title;
|
||||
const subtitle = isInAllView ? '' : isInSubscriptionsView ? subscriptionsSubtitle : isInModView ? '/mod/' : `${address || subplebbitAddress || ''}`;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import useWindowWidth from '../../hooks/use-window-width';
|
||||
import { ContentPreview } from '../../views/home/popular-threads-box';
|
||||
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
|
||||
import styles from './catalog-row.module.css';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||
|
||||
interface CatalogPostMediaProps {
|
||||
@@ -286,7 +286,7 @@ const CatalogPost = memo(
|
||||
t('posted_by')
|
||||
)}{' '}
|
||||
<span className={`${styles.postAuthor} ${isCatalogPostAuthorMod && styles.capcode}`}>
|
||||
{author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{author?.displayName || capitalize(t('anonymous'))}
|
||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
||||
</span>
|
||||
{(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${Plebbit.getShortAddress({ address: subplebbitAddress })}`}
|
||||
@@ -295,7 +295,7 @@ const CatalogPost = memo(
|
||||
<div className={styles.postLast}>
|
||||
{t('last_reply_by')}{' '}
|
||||
<span className={`${styles.postAuthor} ${isLastReplyAuthorMod && styles.capcode}`}>
|
||||
{lastReply?.author?.displayName || _.capitalize(t('anonymous'))}
|
||||
{lastReply?.author?.displayName || capitalize(t('anonymous'))}
|
||||
{isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`}
|
||||
</span>
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import styles from './catalog-search.module.css';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import _ from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const CatalogSearch = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -44,7 +44,7 @@ const CatalogSearch = () => {
|
||||
// Create a debounced version of setSearchFilter and URL update
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedSetSearchFilter = useCallback(
|
||||
_.debounce((text: string) => {
|
||||
debounce((text: string) => {
|
||||
if (text.trim()) {
|
||||
setSearchFilter(text);
|
||||
updateURL(text);
|
||||
|
||||
@@ -6,7 +6,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
import useTheme from '../../hooks/use-theme';
|
||||
import styles from './challenge-modal.module.css';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
@@ -269,7 +269,7 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
{isIframeChallenge && !showIframeConfirmation ? null : (
|
||||
<>
|
||||
<div className={styles.name}>
|
||||
<input type='text' value={displayName || _.capitalize(t('anonymous'))} disabled />
|
||||
<input type='text' value={displayName || capitalize(t('anonymous'))} disabled />
|
||||
</div>
|
||||
{title && (
|
||||
<div className={styles.subject}>
|
||||
|
||||
@@ -14,7 +14,7 @@ import ReplyQuotePreview from '../../components/reply-quote-preview';
|
||||
import Markdown from '../../components/markdown';
|
||||
import Tooltip from '../../components/tooltip';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
||||
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
||||
@@ -88,16 +88,16 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
<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('reason'))}: "${reason}"`}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.grayEditMessage}>{_.capitalize(t('this_post_was_removed'))}.</span>
|
||||
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_removed'))}.</span>
|
||||
)
|
||||
) : deleted ? (
|
||||
reason ? (
|
||||
<>
|
||||
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '}
|
||||
<span className={styles.grayEditMessage}>{`${_.capitalize(t('reason'))}: "${reason}"`}</span>
|
||||
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>
|
||||
@@ -159,7 +159,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
address: subplebbitAddress && Plebbit.getShortAddress({ address: subplebbitAddress }),
|
||||
timestamp: getFormattedDate(post?.author?.subplebbit?.banExpiresAt),
|
||||
interpolation: { escapeValue: false },
|
||||
})}${reason ? `. ${_.capitalize(t('reason'))}: "${reason}"` : ''}`}
|
||||
})}${reason ? `. ${capitalize(t('reason'))}: "${reason}"` : ''}`}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import styles from './edit-menu.module.css';
|
||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useAuthorPrivileges from '../../hooks/use-author-privileges';
|
||||
|
||||
@@ -277,14 +277,14 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
<label>
|
||||
[
|
||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.deleted ?? false} type='checkbox' id='deleted' />
|
||||
{_.capitalize(t('delete'))}?]
|
||||
{capitalize(t('delete'))}?]
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.menuItem}>
|
||||
<label>
|
||||
[
|
||||
<input type='checkbox' onChange={() => setIsContentEditorOpen(!isContentEditorOpen)} checked={isContentEditorOpen} />
|
||||
{_.capitalize(t('edit'))}?]
|
||||
{capitalize(t('edit'))}?]
|
||||
</label>
|
||||
</div>
|
||||
{isContentEditorOpen && (
|
||||
@@ -307,13 +307,13 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
<label>
|
||||
[
|
||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.removed ?? false} type='checkbox' id='removed' />
|
||||
{_.capitalize(t('remove'))}?]
|
||||
{capitalize(t('remove'))}?]
|
||||
</label>{' '}
|
||||
<span className={styles.purgeItem}>
|
||||
<label>
|
||||
[
|
||||
<input onChange={onPurgeCheckbox} checked={publishCommentEditOptions.commentModeration?.purged ?? false} type='checkbox' id='purged' />
|
||||
{_.capitalize(t('purge'))}?]
|
||||
{capitalize(t('purge'))}?]
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
@@ -322,7 +322,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
[
|
||||
<label>
|
||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.locked ?? false} type='checkbox' id='locked' />
|
||||
{_.capitalize(t('close_thread'))}?
|
||||
{capitalize(t('close_thread'))}?
|
||||
</label>
|
||||
]
|
||||
</div>
|
||||
@@ -331,7 +331,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
[
|
||||
<label>
|
||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.spoiler ?? false} type='checkbox' id='spoiler' />
|
||||
{_.capitalize(t('spoiler'))}?
|
||||
{capitalize(t('spoiler'))}?
|
||||
</label>
|
||||
]
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
[
|
||||
<label>
|
||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.pinned ?? false} type='checkbox' id='pinned' />
|
||||
{_.capitalize(t('sticky'))}?
|
||||
{capitalize(t('sticky'))}?
|
||||
</label>
|
||||
]
|
||||
</div>
|
||||
@@ -378,7 +378,7 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
</>
|
||||
)}
|
||||
<div className={`${styles.menuItem} ${styles.menuReason}`}>
|
||||
{_.capitalize(t('reason'))}? ({t('optional')})
|
||||
{capitalize(t('reason'))}? ({t('optional')})
|
||||
<input
|
||||
type='text'
|
||||
value={publishCommentEditOptions.reason || ''}
|
||||
|
||||
@@ -33,7 +33,7 @@ import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
import { PostProps } from '../../views/post/post';
|
||||
import { create } from 'zustand';
|
||||
import _ from 'lodash';
|
||||
import { capitalize, lowerCase } from 'lodash';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||
@@ -241,9 +241,9 @@ const PostInfo = ({
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={`${styles.name} ${authorRole && !(deleted || removed) && (authorRole === 'mod' ? styles.capcodeMod : styles.capcodeAdmin)}`}>
|
||||
{deleted ? (
|
||||
_.capitalize(t('deleted'))
|
||||
capitalize(t('deleted'))
|
||||
) : removed ? (
|
||||
_.capitalize(t('removed'))
|
||||
capitalize(t('removed'))
|
||||
) : displayName ? (
|
||||
displayName.length <= 20 ? (
|
||||
displayName
|
||||
@@ -254,7 +254,7 @@ const PostInfo = ({
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
_.capitalize(t('anonymous'))
|
||||
capitalize(t('anonymous'))
|
||||
)}
|
||||
{!(deleted || removed) && authorRole && (
|
||||
<span className='capitalize'>
|
||||
@@ -328,7 +328,7 @@ const PostInfo = ({
|
||||
<>
|
||||
<span>No.</span>
|
||||
<span className={styles.pendingCid}>
|
||||
{state === 'failed' || stateString === 'Failed' ? _.capitalize(t('failed')) : state === 'pending' ? _.capitalize(t('pending')) : ''}
|
||||
{state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -346,7 +346,7 @@ const PostInfo = ({
|
||||
<span className={styles.replyButton}>
|
||||
[
|
||||
<Link to={boardPath ? `/${boardPath}/thread/${postCid}` : `/thread/${postCid}`} onClick={(e) => !cid && e.preventDefault()}>
|
||||
{_.capitalize(t('reply'))}
|
||||
{capitalize(t('reply'))}
|
||||
</Link>
|
||||
]
|
||||
</span>
|
||||
@@ -438,6 +438,14 @@ const PostInfo = ({
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{cid &&
|
||||
!parentCid &&
|
||||
quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment) =>
|
||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -498,9 +506,9 @@ const PostMedia = ({
|
||||
)}
|
||||
{t('link')}:{' '}
|
||||
<a href={url} target='_blank' rel='noopener noreferrer'>
|
||||
{spoiler ? _.capitalize(t('spoiler')) : url && url.length > 30 ? url.slice(0, 30) + '...' : url}
|
||||
{spoiler ? capitalize(t('spoiler')) : url && url.length > 30 ? url.slice(0, 30) + '...' : url}
|
||||
</a>{' '}
|
||||
({type && _.lowerCase(getDisplayMediaInfoType(type, t))}
|
||||
({type && lowerCase(getDisplayMediaInfoType(type, t))}
|
||||
{mediaDimensions && `, ${mediaDimensions}`})
|
||||
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
|
||||
<span>
|
||||
@@ -769,6 +777,7 @@ const PostDesktop = ({
|
||||
isPublishing={isPublishing}
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
quotedByMap={quotedByMap}
|
||||
/>
|
||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||
{!isHidden && <CommentContent comment={post} />}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import { useDirectories } from '../../../hooks/use-directories';
|
||||
import { isAllView, isCatalogView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
||||
import useHide from '../../../hooks/use-hide';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
import { PostMenuProps } from '../../../lib/utils/post-menu-props';
|
||||
|
||||
type CopyLinkButtonProps =
|
||||
@@ -99,7 +99,7 @@ const ImageSearchButton = ({ url, onClose }: { url: string; onClose: () => void
|
||||
ref={refs.setReference}
|
||||
onClick={onClose}
|
||||
>
|
||||
{_.capitalize(t('image_search'))} »
|
||||
{capitalize(t('image_search'))} »
|
||||
{isImageSearchMenuOpen && (
|
||||
<div ref={refs.setFloating} style={floatingStyles} className={styles.dropdownMenu}>
|
||||
<a href={`https://lens.google.com/uploadbyurl?url=${url}`} target='_blank' rel='noreferrer'>
|
||||
|
||||
@@ -18,7 +18,7 @@ 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';
|
||||
import { capitalize, debounce } from 'lodash';
|
||||
|
||||
const isAndroid = Capacitor.getPlatform() === 'android';
|
||||
|
||||
@@ -79,7 +79,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
||||
|
||||
const checkContentLength = useRef(
|
||||
_.debounce((content: string, t: Function) => {
|
||||
debounce((content: string, t: Function) => {
|
||||
const length = content.trim().length;
|
||||
if (length > 2000) {
|
||||
setLengthError(`${t('error')}: ${t('comment_field_too_long', { length })}`);
|
||||
@@ -247,7 +247,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
<td>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={!displayName ? _.capitalize(t('anonymous')) : undefined}
|
||||
placeholder={!displayName ? capitalize(t('anonymous')) : undefined}
|
||||
defaultValue={displayName || undefined}
|
||||
onChange={(e) => {
|
||||
const newDisplayName = e.target.value.trim() || undefined;
|
||||
@@ -326,7 +326,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
type='checkbox'
|
||||
onChange={(e) => (isInPostView ? setPublishReplyOptions({ spoiler: e.target.checked }) : setPublishPostOptions({ spoiler: e.target.checked }))}
|
||||
/>
|
||||
{_.capitalize(t('spoiler'))}?
|
||||
{capitalize(t('spoiler'))}?
|
||||
</label>
|
||||
]
|
||||
</td>
|
||||
|
||||
@@ -30,7 +30,7 @@ import PostMenuMobile from './post-menu-mobile';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
import { PostProps } from '../../views/post/post';
|
||||
import _ from 'lodash';
|
||||
import { capitalize, lowerCase } from 'lodash';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
@@ -204,9 +204,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={`${styles.name} ${authorRole && !(deleted || removed) && (authorRole === 'mod' ? styles.capcodeMod : styles.capcodeAdmin)}`}>
|
||||
{removed ? (
|
||||
_.capitalize(t('removed'))
|
||||
capitalize(t('removed'))
|
||||
) : deleted ? (
|
||||
_.capitalize(t('deleted'))
|
||||
capitalize(t('deleted'))
|
||||
) : displayName ? (
|
||||
displayName.length <= 20 ? (
|
||||
displayName
|
||||
@@ -217,7 +217,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
_.capitalize(t('anonymous'))
|
||||
capitalize(t('anonymous'))
|
||||
)}{' '}
|
||||
{!(deleted || removed) && authorRole && (
|
||||
<span className='capitalize'>
|
||||
@@ -242,9 +242,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
<>
|
||||
(ID: {''}
|
||||
{removed ? (
|
||||
_.lowerCase(t('removed'))
|
||||
lowerCase(t('removed'))
|
||||
) : deleted ? (
|
||||
_.lowerCase(t('deleted'))
|
||||
lowerCase(t('deleted'))
|
||||
) : (
|
||||
<Tooltip
|
||||
children={
|
||||
@@ -320,7 +320,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
<>
|
||||
<span>No.</span>
|
||||
<span className={styles.pendingCid}>
|
||||
{state === 'failed' || stateString === 'Failed' ? _.capitalize(t('failed')) : state === 'pending' ? _.capitalize(t('pending')) : ''}
|
||||
{state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -384,28 +384,40 @@ const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
||||
const { cid, parentCid } = post || {};
|
||||
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
|
||||
|
||||
return (
|
||||
const opBacklinks =
|
||||
cid &&
|
||||
parentCid &&
|
||||
(replies.length > 0 || quotedByMap?.get(cid)?.length) && (
|
||||
<div className={styles.mobileReplyBacklinks}>
|
||||
{replies.map(
|
||||
!parentCid &&
|
||||
quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment) =>
|
||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const replyBacklinks = cid && parentCid && ((replies?.length || 0) > 0 || quotedByMap?.get(cid)?.length) && (
|
||||
<>
|
||||
{replies?.map(
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.parentCid !== cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid !== cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</>
|
||||
);
|
||||
|
||||
return opBacklinks?.length > 0 || replyBacklinks ? (
|
||||
<div className={styles.mobileReplyBacklinks}>
|
||||
{opBacklinks}
|
||||
{replyBacklinks}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
||||
@@ -434,7 +446,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: Post
|
||||
>
|
||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
|
||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
|
||||
<ReplyBacklinks post={reply} quotedByMap={quotedByMap} />
|
||||
<ReplyBacklinks post={post} quotedByMap={quotedByMap} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -588,6 +600,7 @@ const PostMobile = ({
|
||||
{shouldShowSnow() && <img src='assets/xmashat.gif' className={styles.xmasHat} alt='' />}
|
||||
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
|
||||
<CommentContent comment={post} />
|
||||
<ReplyBacklinks post={post} quotedByMap={quotedByMap} />
|
||||
</div>
|
||||
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
|
||||
<div className={styles.postLink}>
|
||||
|
||||
@@ -13,7 +13,7 @@ import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import styles from './reply-modal.module.css';
|
||||
import { LinkTypePreviewer } from '../post-form';
|
||||
import _ from 'lodash';
|
||||
import { capitalize, debounce } from 'lodash';
|
||||
import FileUploader from '../../plugins/file-uploader';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
@@ -56,7 +56,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
||||
|
||||
const checkContentLength = useRef(
|
||||
_.debounce((content: string, t: Function) => {
|
||||
debounce((content: string, t: Function) => {
|
||||
const length = content.trim().length;
|
||||
if (length > 2000) {
|
||||
setError(null);
|
||||
@@ -68,7 +68,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
).current;
|
||||
|
||||
const onPublishReply = () => {
|
||||
const currentContent = textRef.current?.value.slice(contentPrefix.length).trim() || '';
|
||||
const currentContent = textRef.current?.value.trim() || '';
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
|
||||
if (!currentContent && !currentUrl) {
|
||||
@@ -186,16 +186,19 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contentPrefix = `>>${parentNumber ?? '?'}\n`;
|
||||
const defaultParentQuote = `>>${parentNumber ?? '?'}\n`;
|
||||
|
||||
// enable spellcheck after the prefix is set
|
||||
// Enable spellcheck after initial content is injected into the textarea.
|
||||
useEffect(() => {
|
||||
if (showReplyModal && textRef.current) {
|
||||
textRef.current.spellcheck = false;
|
||||
textRef.current.value = contentPrefix + (selectedText || '');
|
||||
textRef.current.value = `${defaultParentQuote}${selectedText || ''}`;
|
||||
const len = textRef.current.value.length;
|
||||
lastSelectionStartRef.current = len;
|
||||
lastSelectionEndRef.current = len;
|
||||
const formattedContent = formatMarkdown(textRef.current.value);
|
||||
setPublishReplyOptions({ content: formattedContent });
|
||||
checkContentLength(formattedContent, t);
|
||||
|
||||
setTimeout(() => {
|
||||
if (textRef.current) {
|
||||
@@ -203,24 +206,17 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [showReplyModal, contentPrefix, selectedText]);
|
||||
}, [showReplyModal, defaultParentQuote, selectedText]);
|
||||
|
||||
const handleContentInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { value } = e.target;
|
||||
if (!value.startsWith(contentPrefix)) {
|
||||
e.target.value = contentPrefix + value.slice(contentPrefix.length);
|
||||
}
|
||||
lastSelectionStartRef.current = e.target.selectionStart ?? e.target.value.length;
|
||||
lastSelectionEndRef.current = e.target.selectionEnd ?? lastSelectionStartRef.current;
|
||||
};
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const contentWithoutPrefix = e.target.value.slice(contentPrefix.length);
|
||||
const formattedContent = formatMarkdown(contentWithoutPrefix);
|
||||
if (textRef.current && textRef.current.value !== formattedContent) {
|
||||
setPublishReplyOptions({ content: formattedContent });
|
||||
checkContentLength(formattedContent, t);
|
||||
}
|
||||
const formattedContent = formatMarkdown(e.target.value);
|
||||
setPublishReplyOptions({ content: formattedContent });
|
||||
checkContentLength(formattedContent, t);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -244,12 +240,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const isFocused = document.activeElement === textarea;
|
||||
const rawStart = isFocused ? (textarea.selectionStart ?? textarea.value.length) : lastSelectionStartRef.current;
|
||||
const selectionEnd = isFocused ? (textarea.selectionEnd ?? rawStart) : lastSelectionEndRef.current;
|
||||
const minStart = contentPrefix.length;
|
||||
const start = Math.max(rawStart, minStart);
|
||||
const end = Math.max(selectionEnd, minStart);
|
||||
const start = Math.max(rawStart, 0);
|
||||
const end = Math.max(selectionEnd, 0);
|
||||
const before = textarea.value.slice(0, start);
|
||||
const after = textarea.value.slice(end);
|
||||
const needsLeadingNewline = before.length > minStart && !before.endsWith('\n');
|
||||
const needsLeadingNewline = before.length > 0 && !before.endsWith('\n');
|
||||
let insertion = `${needsLeadingNewline ? '\n' : ''}${quote}\n`;
|
||||
if (selectedQuote) {
|
||||
insertion += `${selectedQuote}\n`;
|
||||
@@ -263,11 +258,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
lastSelectionStartRef.current = nextCursor;
|
||||
lastSelectionEndRef.current = nextCursor;
|
||||
|
||||
const contentWithoutPrefix = nextValue.slice(contentPrefix.length);
|
||||
const formattedContent = formatMarkdown(contentWithoutPrefix);
|
||||
const formattedContent = formatMarkdown(nextValue);
|
||||
setPublishReplyOptions({ content: formattedContent });
|
||||
checkContentLength(formattedContent, t);
|
||||
}, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, contentPrefix, setPublishReplyOptions, checkContentLength, t]);
|
||||
}, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, setPublishReplyOptions, checkContentLength, t]);
|
||||
|
||||
// on android, auto upload file to image hosting sites with open api
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
@@ -333,7 +327,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
<input
|
||||
type='text'
|
||||
defaultValue={displayName}
|
||||
placeholder={displayName ? undefined : _.capitalize(t('name'))}
|
||||
placeholder={displayName ? undefined : capitalize(t('name'))}
|
||||
onChange={(e) => {
|
||||
setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } });
|
||||
setPublishReplyOptions({ displayName: e.target.value });
|
||||
@@ -344,7 +338,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
<input
|
||||
type='text'
|
||||
ref={urlRef}
|
||||
placeholder={_.capitalize(t('link'))}
|
||||
placeholder={capitalize(t('link'))}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setPublishReplyOptions({ link: e.target.value });
|
||||
@@ -392,7 +386,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
[
|
||||
<label>
|
||||
<input type='checkbox' onChange={(e) => setPublishReplyOptions({ spoiler: e.target.checked })} />
|
||||
{_.capitalize(t('spoiler'))}?
|
||||
{capitalize(t('spoiler'))}?
|
||||
</label>
|
||||
]
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Comment, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
@@ -63,6 +63,13 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToThreadCardTop = (threadCid: string) => {
|
||||
const threadCard = document.querySelector<HTMLElement>(`[data-cid="${threadCid}"][data-post-cid="${threadCid}"]`);
|
||||
if (!threadCard) return false;
|
||||
threadCard.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
return true;
|
||||
};
|
||||
|
||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
@@ -107,12 +114,18 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
}, [update]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined, isOpQuote = false) => {
|
||||
e.preventDefault();
|
||||
if (cid && subplebbitAddress) {
|
||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||
navigate(`/${boardPath}/thread/${cid}`);
|
||||
const threadRoute = `/${boardPath}/thread/${cid}`;
|
||||
if (isOpQuote && location.pathname === threadRoute) {
|
||||
scrollToThreadCardTop(cid);
|
||||
return;
|
||||
}
|
||||
navigate(threadRoute);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,7 +188,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
className={styles.quoteLink}
|
||||
onMouseOver={() => handleMouseOver(quotelinkReply?.cid)}
|
||||
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
|
||||
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}
|
||||
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}
|
||||
>
|
||||
{`>>${quotelinkReply?.number ?? '?'}`}
|
||||
{isOP && ' (OP)'}
|
||||
@@ -217,12 +230,18 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
}, [update]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined, isOpQuote = false) => {
|
||||
e.preventDefault();
|
||||
if (cid && subplebbitAddress) {
|
||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||
navigate(`/${boardPath}/thread/${cid}`);
|
||||
const threadRoute = `/${boardPath}/thread/${cid}`;
|
||||
if (isOpQuote && location.pathname === threadRoute) {
|
||||
scrollToThreadCardTop(cid);
|
||||
return;
|
||||
}
|
||||
navigate(threadRoute);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,7 +319,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
: `/thread/${quotelinkReply.cid}`
|
||||
: '#';
|
||||
return (
|
||||
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}>
|
||||
<Link className={styles.quoteLink} to={quotelinkRoute} onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}>
|
||||
{' '}
|
||||
#
|
||||
</Link>
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from './avatar-settings.module.css';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import LoadingEllipsis from '../../loading-ellipsis';
|
||||
import ErrorDisplay from '../../error-display/error-display';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
const AvatarPreview = ({ avatar }: any) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -132,7 +132,7 @@ const AvatarSettings = () => {
|
||||
<AvatarPreview avatar={avatar} />
|
||||
<div className={styles.avatarSettingsForm}>
|
||||
<div className={`${styles.settingField} ${styles.step1}`}>
|
||||
<span className={styles.settingTitle}>{_.capitalize(t('chain_ticker'))}: </span>
|
||||
<span className={styles.settingTitle}>{capitalize(t('chain_ticker'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='eth/sol/matic'
|
||||
@@ -197,7 +197,7 @@ const AvatarSettings = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.settingField}>
|
||||
<span className={`${styles.settingTitle} ${styles.timestampfield}`}>{_.capitalize(t('timestamp'))}: </span>
|
||||
<span className={`${styles.settingTitle} ${styles.timestampfield}`}>{capitalize(t('timestamp'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='1234567890'
|
||||
@@ -209,7 +209,7 @@ const AvatarSettings = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.settingField} ${styles.step5}`}>
|
||||
<span className={styles.settingTitle}>{_.capitalize(t('paste_signature'))}: </span>
|
||||
<span className={styles.settingTitle}>{capitalize(t('paste_signature'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='0x...'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Account, setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './crypto-wallets-setting.module.css';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
interface Wallet {
|
||||
chainTicker: string;
|
||||
@@ -102,7 +102,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
|
||||
walletsArray.length > 0 ? (
|
||||
<div key={selectedWallet} className={styles.walletBox}>
|
||||
<div className={`${styles.walletField} ${styles.step1}`}>
|
||||
<span className={styles.walletFieldTitle}>{_.capitalize(t('chain_ticker'))}: </span>
|
||||
<span className={styles.walletFieldTitle}>{capitalize(t('chain_ticker'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'chainTicker', e.target.value)}
|
||||
@@ -111,7 +111,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.walletField} ${styles.step2}`}>
|
||||
<span className={styles.walletFieldTitle}>{_.capitalize(t('wallet_address'))}: </span>
|
||||
<span className={styles.walletFieldTitle}>{capitalize(t('wallet_address'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'address', e.target.value)}
|
||||
@@ -133,7 +133,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletField}>
|
||||
<span className={`${styles.walletFieldTitle} ${styles.timestampfield}`}>{_.capitalize(t('timestamp'))}: </span>
|
||||
<span className={`${styles.walletFieldTitle} ${styles.timestampfield}`}>{capitalize(t('timestamp'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'timestamp', Number(e.target.value))}
|
||||
@@ -142,7 +142,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.walletField} ${styles.step4}`}>
|
||||
<span className={styles.walletFieldTitle}>{_.capitalize(t('paste_signature'))}: </span>
|
||||
<span className={styles.walletFieldTitle}>{capitalize(t('paste_signature'))}: </span>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'signature', e.target.value)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import useAvatarVisibilityStore from '../../../stores/use-avatar-visibility-stor
|
||||
import useTheme from '../../../hooks/use-theme';
|
||||
import packageJson from '../../../../package.json';
|
||||
import styles from './interface-settings.module.css';
|
||||
import _ from 'lodash';
|
||||
import { capitalize } from 'lodash';
|
||||
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
|
||||
@@ -138,16 +138,16 @@ const InterfaceSettings = () => {
|
||||
return (
|
||||
<div className={styles.interfaceSettings}>
|
||||
<div className={styles.version}>
|
||||
{_.capitalize(t('version'))}: <Version />
|
||||
{capitalize(t('version'))}: <Version />
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
{_.capitalize(t('update'))}: <CheckForUpdates />
|
||||
{capitalize(t('update'))}: <CheckForUpdates />
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
{_.capitalize(t('style'))}: <Style />
|
||||
{capitalize(t('style'))}: <Style />
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
{_.capitalize(t('interface_language'))}: <InterfaceLanguage />
|
||||
{capitalize(t('interface_language'))}: <InterfaceLanguage />
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
@@ -159,22 +159,22 @@ const InterfaceSettings = () => {
|
||||
setShowTextOnlyThreads(!e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{_.capitalize(t('hide_threads_without_images'))}
|
||||
{capitalize(t('hide_threads_without_images'))}
|
||||
</label>
|
||||
<div className={styles.settingTip}>{_.capitalize(t('threads_without_images_tip'))}</div>
|
||||
<div className={styles.settingTip}>{capitalize(t('threads_without_images_tip'))}</div>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={fitExpandedImagesToScreen} onChange={(e) => setFitExpandedImagesToScreen(e.target.checked)} />
|
||||
{_.capitalize(t('fit_expanded_images_to_screen'))}
|
||||
{capitalize(t('fit_expanded_images_to_screen'))}
|
||||
</label>
|
||||
<div className={styles.settingTip}>{_.capitalize(t('fit_expanded_images_to_screen_tip'))}</div>
|
||||
<div className={styles.settingTip}>{capitalize(t('fit_expanded_images_to_screen_tip'))}</div>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={hideAvatars} onChange={handleHideAvatarsChange} /> {_.capitalize(t('hide_avatars'))}
|
||||
<input type='checkbox' checked={hideAvatars} onChange={handleHideAvatarsChange} /> {capitalize(t('hide_avatars'))}
|
||||
</label>
|
||||
<div className={styles.settingTip}>{_.capitalize(t('hide_avatars_tip'))}</div>
|
||||
<div className={styles.settingTip}>{capitalize(t('hide_avatars_tip'))}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,14 +15,14 @@ import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
||||
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||
import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes';
|
||||
import styles from './topbar.module.css';
|
||||
import _, { debounce } from 'lodash';
|
||||
import { capitalize, debounce, lowerCase } from 'lodash';
|
||||
|
||||
const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const searchBarRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const placeholder = _.lowerCase(t('enter_board_address'));
|
||||
const placeholder = lowerCase(t('enter_board_address'));
|
||||
|
||||
useEffect(() => {
|
||||
searchInputRef.current?.focus();
|
||||
@@ -229,7 +229,7 @@ const TopBarDesktop = () => {
|
||||
)}
|
||||
[
|
||||
<span className={styles.temporaryButton} onClick={() => openTopbarEditModal()} style={{ cursor: 'pointer' }}>
|
||||
{_.capitalize(t('edit'))}
|
||||
{capitalize(t('edit'))}
|
||||
</span>
|
||||
] [
|
||||
<span className={styles.temporaryButton} onClick={() => openCreateBoardModal()} style={{ cursor: 'pointer' }}>
|
||||
@@ -338,7 +338,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
</div>
|
||||
<div className={styles.pageJump}>
|
||||
<Link to={useLocation().pathname.replace(/\/$/, '') + '/settings'}>{t('settings')}</Link>
|
||||
<span onClick={() => setShowSearchBar(!showSearchBar)}>{_.capitalize(t('search'))}</span>
|
||||
<span onClick={() => setShowSearchBar(!showSearchBar)}>{capitalize(t('search'))}</span>
|
||||
<Link to='/'>{t('home')}</Link>
|
||||
{showSearchBar && <SearchBar setShowSearchBar={setShowSearchBar} />}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Comment, useAccount, usePublishComment } from '@plebbit/plebbit-react-hooks';
|
||||
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
||||
import usePostNumberStore from '../stores/use-post-number-store';
|
||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
||||
|
||||
const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; subplebbitAddress: string; postCid?: string }) => {
|
||||
const parentCid = cid;
|
||||
@@ -54,7 +56,12 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
|
||||
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
|
||||
|
||||
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
||||
const numberToCid = usePostNumberStore((state) => state.numberToCid);
|
||||
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, numberToCid), [content, numberToCid]);
|
||||
|
||||
const publishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, quotedCids), [publishCommentOptions, quotedCids]);
|
||||
|
||||
const { index, publishComment } = usePublishComment(publishOptions);
|
||||
|
||||
return {
|
||||
setPublishReplyOptions,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { QUOTE_NUMBER_REGEX } from './url-utils';
|
||||
|
||||
export const getQuotedCidsFromContent = (content: string | undefined, numberToCid: Record<number, string>) => {
|
||||
if (!content) return undefined;
|
||||
const cids = new Set<string>();
|
||||
for (const match of content.matchAll(new RegExp(QUOTE_NUMBER_REGEX.source, 'g'))) {
|
||||
const num = parseInt(match[1], 10);
|
||||
const cid = numberToCid[num];
|
||||
if (cid) cids.add(cid);
|
||||
}
|
||||
return cids.size > 0 ? [...cids] : undefined;
|
||||
};
|
||||
|
||||
export const mergeQuotedCids = (publishCommentOptions: Record<string, any> | undefined, quotedCids: string[] | undefined) => {
|
||||
if (!publishCommentOptions || !quotedCids?.length) return publishCommentOptions;
|
||||
const currentQuotedCids = publishCommentOptions.quotedCids ?? [];
|
||||
const mergedQuotedCids = [...new Set([...currentQuotedCids, ...quotedCids])];
|
||||
|
||||
return {
|
||||
...publishCommentOptions,
|
||||
quotedCids: mergedQuotedCids,
|
||||
};
|
||||
};
|
||||
@@ -78,7 +78,7 @@ const useReplyModalStore = create<ReplyModalState>((set, get) => ({
|
||||
const scrollY = isMobile ? window.scrollY : 0;
|
||||
|
||||
set({
|
||||
activeCid: parentCid,
|
||||
activeCid: postCid,
|
||||
parentNumber: parentNumber ?? null,
|
||||
threadNumber: threadNumber ?? null,
|
||||
threadCid: postCid,
|
||||
|
||||
@@ -12,7 +12,7 @@ import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||
import DisclaimerModal from '../../components/disclaimer-modal';
|
||||
import DirectoryModal from '../../components/directory-modal';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import _ from 'lodash';
|
||||
import { lowerCase } from 'lodash';
|
||||
|
||||
// https://github.com/plebbit/lists/blob/master/5chan-directories.json
|
||||
|
||||
@@ -40,7 +40,7 @@ const SearchBar = () => {
|
||||
spellCheck='false'
|
||||
autoCapitalize='off'
|
||||
type='text'
|
||||
placeholder={_.lowerCase(t('enter_board_address'))}
|
||||
placeholder={lowerCase(t('enter_board_address'))}
|
||||
ref={searchInputRef}
|
||||
/>
|
||||
<button className={styles.searchButton}>{t('go')}</button>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-util
|
||||
import Tooltip from '../../components/tooltip';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import { Post } from '../post/post';
|
||||
import _ from 'lodash';
|
||||
import { capitalize, lowerCase } from 'lodash';
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
@@ -273,7 +273,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
|
||||
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.type}>{isReply ? _.capitalize(t('reply')) : _.capitalize(t('post'))}</div>
|
||||
<div className={styles.type}>{isReply ? capitalize(t('reply')) : capitalize(t('post'))}</div>
|
||||
<div className={styles.image}>{hasThumbnail ? t('yes') : t('no')}</div>
|
||||
<div className={styles.actions}>{renderActions()}</div>
|
||||
</div>
|
||||
@@ -390,7 +390,7 @@ const ModQueueCard = ({ comment }: ModQueueCardProps) => {
|
||||
) : (
|
||||
<span title={excerpt}>{excerpt}</span>
|
||||
)}{' '}
|
||||
/ {t('type')}: {isReply ? t('reply') : t('post')} / {_.capitalize(t('image'))}: {hasThumbnail ? _.lowerCase(t('yes')) : _.lowerCase(t('no'))}
|
||||
/ {t('type')}: {isReply ? t('reply') : t('post')} / {capitalize(t('image'))}: {hasThumbnail ? lowerCase(t('yes')) : lowerCase(t('no'))}
|
||||
</div>
|
||||
{renderActions()}
|
||||
</div>
|
||||
|
||||
@@ -1012,18 +1012,18 @@
|
||||
bs58 "5.0.0"
|
||||
buffer "^6.0.3"
|
||||
|
||||
"@bonfida/spl-name-service@3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@bonfida/spl-name-service/-/spl-name-service-3.0.10.tgz#883f2e077eef7d91147f44ae383990a82479d5f8"
|
||||
integrity sha512-CrqlEDz43K9fPIVaUXn29OiMWZUOs3+sxU7zHrlPjjdXn9GrJSVao5gjmflfK6tcwLxSSGC4F4GOg5erSfyR/A==
|
||||
"@bonfida/spl-name-service@3.0.16":
|
||||
version "3.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@bonfida/spl-name-service/-/spl-name-service-3.0.16.tgz#5ebcadbb1908af9348ff15c20a4e4fdc8a1408da"
|
||||
integrity sha512-Y47inKaoGYQ2gxykZsLepc0fNPMLWd5ycGQ6k2FiHvIzlY4trHijc7zr9TyDhB/eI9c3XTy5ItM01Izy5pIZ/A==
|
||||
dependencies:
|
||||
"@bonfida/sns-records" "0.0.1"
|
||||
"@noble/curves" "^1.4.0"
|
||||
"@scure/base" "^1.1.6"
|
||||
"@noble/curves" "^1.9.1"
|
||||
"@scure/base" "^1.2.5"
|
||||
"@solana/spl-token" "0.4.6"
|
||||
borsh "2.0.0"
|
||||
buffer "^6.0.3"
|
||||
graphemesplit "^2.4.4"
|
||||
graphemesplit "^2.6.0"
|
||||
ipaddr.js "^2.2.0"
|
||||
punycode "^2.3.1"
|
||||
|
||||
@@ -3974,7 +3974,7 @@
|
||||
dependencies:
|
||||
"@noble/hashes" "1.8.0"
|
||||
|
||||
"@noble/curves@^1.4.0", "@noble/curves@^1.4.2", "@noble/curves@~1.9.0":
|
||||
"@noble/curves@^1.4.0", "@noble/curves@^1.4.2", "@noble/curves@^1.9.1", "@noble/curves@~1.9.0":
|
||||
version "1.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951"
|
||||
integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==
|
||||
@@ -4282,11 +4282,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#8e7d818688d0b4aab2ce6271b271167a4eb75225":
|
||||
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851":
|
||||
version "0.0.8"
|
||||
resolved "https://github.com/plebbit/plebbit-js.git#8e7d818688d0b4aab2ce6271b271167a4eb75225"
|
||||
resolved "https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851"
|
||||
dependencies:
|
||||
"@bonfida/spl-name-service" "3.0.10"
|
||||
"@bonfida/spl-name-service" "3.0.16"
|
||||
"@enhances/with-resolvers" "0.0.5"
|
||||
"@helia/block-brokers" "5.0.13"
|
||||
"@helia/delegated-routing-v1-http-api-client" "5.1.4"
|
||||
@@ -4344,8 +4344,8 @@
|
||||
ts-custom-error "3.3.1"
|
||||
typestub-ipfs-only-hash "4.0.0"
|
||||
uuid "11.1.0"
|
||||
viem "2.43.1"
|
||||
zod "4.1.12"
|
||||
viem "2.45.0"
|
||||
zod "4.3.6"
|
||||
|
||||
"@plebbit/plebbit-logger@github:plebbit/plebbit-logger#355a96d7659ed820047980049dfa627d30d83a69":
|
||||
version "0.0.1"
|
||||
@@ -4360,11 +4360,11 @@
|
||||
dependencies:
|
||||
debug "4.3.4"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#df3cb6901f63054870dc127cd7565c98617c2a4f":
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#e197af2381ad651e5b74a93bcb705de78f94742c":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#df3cb6901f63054870dc127cd7565c98617c2a4f"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#e197af2381ad651e5b74a93bcb705de78f94742c"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#8e7d818688d0b4aab2ce6271b271167a4eb75225"
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
@@ -4660,7 +4660,7 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@scure/base@^1.1.6", "@scure/base@~1.2.5":
|
||||
"@scure/base@^1.2.5", "@scure/base@~1.2.5":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6"
|
||||
integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==
|
||||
@@ -5747,22 +5747,13 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.13.2:
|
||||
version "1.13.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687"
|
||||
integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
|
||||
axios@1.13.5, axios@^1.13.2, axios@^1.7.2:
|
||||
version "1.13.5"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43"
|
||||
integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.4"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axios@^1.7.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
|
||||
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.4"
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
babel-plugin-add-react-displayname@^0.0.5:
|
||||
@@ -8341,7 +8332,7 @@ fmix@^0.1.0:
|
||||
dependencies:
|
||||
imul "^1.0.0"
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.11, follow-redirects@^1.15.6:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.11:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
|
||||
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||
@@ -8361,10 +8352,10 @@ foreground-child@^3.1.0:
|
||||
cross-spawn "^7.0.6"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
form-data@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
|
||||
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
@@ -8802,7 +8793,7 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
graphemesplit@^2.4.4:
|
||||
graphemesplit@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/graphemesplit/-/graphemesplit-2.6.0.tgz#548ec986e83dcdfec31adf5c98b282985077ce46"
|
||||
integrity sha512-rG9w2wAfkpg0DILa1pjnjNfucng3usON360shisqIMUBw/87pojcBSrHmeE4UwryAuBih7g8m1oilf5/u8EWdQ==
|
||||
@@ -12305,20 +12296,6 @@ own-keys@^1.0.1:
|
||||
object-keys "^1.1.1"
|
||||
safe-push-apply "^1.0.0"
|
||||
|
||||
ox@0.10.5:
|
||||
version "0.10.5"
|
||||
resolved "https://registry.yarnpkg.com/ox/-/ox-0.10.5.tgz#dbef388065011b0109a25ed60359ca04cdd56434"
|
||||
integrity sha512-mXJRiZswmX46abrzNkJpTN9sPJ/Rhevsp5Dfg0z80D55aoLNmEV4oN+/+feSNW593c2CnHavMqSVBanpJ0lUkQ==
|
||||
dependencies:
|
||||
"@adraffy/ens-normalize" "^1.11.0"
|
||||
"@noble/ciphers" "^1.3.0"
|
||||
"@noble/curves" "1.9.1"
|
||||
"@noble/hashes" "^1.8.0"
|
||||
"@scure/bip32" "^1.7.0"
|
||||
"@scure/bip39" "^1.6.0"
|
||||
abitype "^1.2.3"
|
||||
eventemitter3 "5.0.1"
|
||||
|
||||
ox@0.11.3:
|
||||
version "0.11.3"
|
||||
resolved "https://registry.yarnpkg.com/ox/-/ox-0.11.3.tgz#f50be1cae88822f5111b1e3f4a324c4c07eed422"
|
||||
@@ -15596,20 +15573,6 @@ vfile@^6.0.0:
|
||||
"@types/unist" "^3.0.0"
|
||||
vfile-message "^4.0.0"
|
||||
|
||||
viem@2.43.1:
|
||||
version "2.43.1"
|
||||
resolved "https://registry.yarnpkg.com/viem/-/viem-2.43.1.tgz#ec6eb4841ed8eb4a11d54cb0b39afa8204fcd287"
|
||||
integrity sha512-S33pBNlRvOlVv4+L94Z8ydCMDB1j0cuHFUvaC28i6OTxw3uY1P4M3h1YDFK8YC1H9/lIbeBTTvCRhi0FqU/2iw==
|
||||
dependencies:
|
||||
"@noble/curves" "1.9.1"
|
||||
"@noble/hashes" "1.8.0"
|
||||
"@scure/bip32" "1.7.0"
|
||||
"@scure/bip39" "1.6.0"
|
||||
abitype "1.2.3"
|
||||
isows "1.0.7"
|
||||
ox "0.10.5"
|
||||
ws "8.18.3"
|
||||
|
||||
viem@2.45.0:
|
||||
version "2.45.0"
|
||||
resolved "https://registry.yarnpkg.com/viem/-/viem-2.45.0.tgz#5f7f28b639193b783b7fbb6bef7575e9cc8d6f86"
|
||||
@@ -16220,10 +16183,10 @@ yocto-queue@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.1.tgz#36d7c4739f775b3cbc28e6136e21aa057adec418"
|
||||
integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==
|
||||
|
||||
zod@4.1.12:
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
|
||||
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==
|
||||
zod@4.3.6:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
|
||||
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
|
||||
|
||||
zustand@4.0.0:
|
||||
version "4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user