From 50f373eb83eb5266895396a3266433751502a281 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 13:43:17 +0800 Subject: [PATCH 01/12] fix(deps): resolve dependabot alert --- package.json | 1 + yarn.lock | 31 +++++++++++-------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 6be0eac4..50cf63b0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 28728cb6..07ca88a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 4898b5aa4fedda1f2b1018708907172a69c236d5 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 15:30:25 +0800 Subject: [PATCH 02/12] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 712a4d8b..d66dbf00 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,5 @@ yarn-debug.log* yarn-error.log* .vscode -.cursor \ No newline at end of file +.cursor +.claude \ No newline at end of file From 78274054606648b5e50fceba794e175c06b7b60b Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 15:46:06 +0800 Subject: [PATCH 03/12] Update AGENTS.md --- AGENTS.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 15eaad1a..57fd227e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: From e9558a618a17f9a15e77ae81f384eed031ad5b98 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 16:06:09 +0800 Subject: [PATCH 04/12] chore(deps): upgrade plebbit-react-hooks --- package.json | 2 +- yarn.lock | 72 ++++++++++++++++------------------------------------ 2 files changed, 23 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 50cf63b0..fca8c2f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 07ca88a6..9ca9d3bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== @@ -8793,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== @@ -12296,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" @@ -15587,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" @@ -16211,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" From 9f14a63268a81b2a79fd2a9a9a665f4247f8d654 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 16:07:01 +0800 Subject: [PATCH 05/12] feat: populate quotedCids when publishing replies with quote references Parse reply content for >>{postNumber} references, resolve post numbers to CIDs via usePostNumberStore, and include resolved CIDs in publishCommentOptions when publishing. Extracted quote handling into reply-quote-utils.ts for testability and clarity. --- src/hooks/use-publish-reply.ts | 11 +++++++++-- src/lib/utils/reply-quote-utils.ts | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/lib/utils/reply-quote-utils.ts diff --git a/src/hooks/use-publish-reply.ts b/src/hooks/use-publish-reply.ts index 1715ed83..229421a1 100644 --- a/src/hooks/use-publish-reply.ts +++ b/src/hooks/use-publish-reply.ts @@ -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, diff --git a/src/lib/utils/reply-quote-utils.ts b/src/lib/utils/reply-quote-utils.ts new file mode 100644 index 00000000..b00fa991 --- /dev/null +++ b/src/lib/utils/reply-quote-utils.ts @@ -0,0 +1,23 @@ +import { QUOTE_NUMBER_REGEX } from './url-utils'; + +export const getQuotedCidsFromContent = (content: string | undefined, numberToCid: Record) => { + if (!content) return undefined; + const cids = new Set(); + 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 | undefined, quotedCids: string[] | undefined) => { + if (!publishCommentOptions || !quotedCids?.length) return publishCommentOptions; + const currentQuotedCids = publishCommentOptions.quotedCids ?? []; + const mergedQuotedCids = [...new Set([...currentQuotedCids, ...quotedCids])]; + + return { + ...publishCommentOptions, + quotedCids: mergedQuotedCids, + }; +}; From af259ad032c401625401a40cd1ec89f4e6968332 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 16:38:40 +0800 Subject: [PATCH 06/12] fix(reply modal): make parent reply quote editable --- src/components/reply-modal/reply-modal.tsx | 38 +++++++++------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index 2a237ad5..c32ec98c 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -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) => { - 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) => { - 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); From 6c4c12c136ea7edf1397330eb0dc5515a7d8490b Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 18:15:06 +0800 Subject: [PATCH 07/12] fix(reply): replies should always target the OP, not other replies --- src/stores/use-reply-modal-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/use-reply-modal-store.ts b/src/stores/use-reply-modal-store.ts index 0f5b7ac4..94afed88 100644 --- a/src/stores/use-reply-modal-store.ts +++ b/src/stores/use-reply-modal-store.ts @@ -78,7 +78,7 @@ const useReplyModalStore = create((set, get) => ({ const scrollY = isMobile ? window.scrollY : 0; set({ - activeCid: parentCid, + activeCid: postCid, parentNumber: parentNumber ?? null, threadNumber: threadNumber ?? null, threadCid: postCid, From 716c782d1a00514e60d56b065d7faa0389296244 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 18:43:39 +0800 Subject: [PATCH 08/12] fix: scroll OP quote to thread card on same-route click --- src/components/post-desktop/post-desktop.tsx | 18 ++++++ src/components/post-mobile/post-mobile.tsx | 61 +++++++++++++------ .../reply-quote-preview.tsx | 33 +++++++--- src/views/post/post.tsx | 2 + 4 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index 17024134..b24e76ca 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -76,6 +76,7 @@ const PostInfo = ({ onApprove, onReject, quotedByMap, + cidToReply, }: PostProps) => { const { t } = useTranslation(); const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {}; @@ -438,6 +439,14 @@ const PostInfo = ({ reply?.cid && !(reply?.deleted || reply?.removed) && , )} + {cid && + !parentCid && + isInPostPageView && + cidToReply && + [...new Set((post?.quotedCids || []) as string[])].map((quotedCid) => { + const reply = cidToReply.get(quotedCid); + return reply?.cid && !(reply.deleted || reply.removed) && ; + })} ); @@ -681,6 +690,14 @@ const PostDesktop = ({ return map; }, [filteredReplies, numberToCid]); + const cidToReply = useMemo(() => { + const map = new Map(); + for (const reply of filteredReplies) { + if (reply.cid) map.set(reply.cid, reply); + } + return map; + }, [filteredReplies]); + // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); const virtuosoStateKey = `replies-desktop-${cid}`; @@ -769,6 +786,7 @@ const PostDesktop = ({ isPublishing={isPublishing} onApprove={onApprove} onReject={onReject} + cidToReply={cidToReply} /> {!isHidden && !content && !(deleted || removed) &&
} {!isHidden && } diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index efc5c928..19b6f757 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -380,32 +380,46 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => { ); }; -const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => { +const ReplyBacklinks = ({ post, quotedByMap, cidToReply, isInPostPageView }: 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) && ( -
- {replies.map( + !parentCid && + isInPostPageView && + cidToReply && + post?.quotedCids?.length && + [...new Set(post.quotedCids as string[])] + .map((quotedCid) => { + const reply = cidToReply.get(quotedCid); + return reply?.cid && !(reply.deleted || reply.removed) && ; + }) + .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) && , + )} + {quotedByMap + ?.get(cid) + ?.map( (reply: Comment, index: number) => - reply?.parentCid === cid && + reply?.parentCid !== cid && reply?.cid && - !(reply?.deleted || reply?.removed) && , + !(reply?.deleted || reply?.removed) && , )} - {quotedByMap - ?.get(cid) - ?.map( - (reply: Comment, index: number) => - reply?.parentCid !== cid && - reply?.cid && - !(reply?.deleted || reply?.removed) && , - )} -
- ) + ); + + return opBacklinks?.length > 0 || replyBacklinks ? ( +
+ {opBacklinks} + {replyBacklinks} +
+ ) : null; }; const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => { @@ -434,7 +448,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: Post > {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && } - +
@@ -520,6 +534,14 @@ const PostMobile = ({ return map; }, [filteredReplies, numberToCid]); + const cidToReply = useMemo(() => { + const map = new Map(); + for (const reply of filteredReplies) { + if (reply.cid) map.set(reply.cid, reply); + } + return map; + }, [filteredReplies]); + // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); const virtuosoStateKey = `replies-mobile-${cid}`; @@ -588,6 +610,7 @@ const PostMobile = ({ {shouldShowSnow() && } + {!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
diff --git a/src/components/reply-quote-preview/reply-quote-preview.tsx b/src/components/reply-quote-preview/reply-quote-preview.tsx index d35ee248..6432e844 100644 --- a/src/components/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/reply-quote-preview/reply-quote-preview.tsx @@ -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(`[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(null); const [outOfViewCid, setOutOfViewCid] = useState(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 ( - handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}> + handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress, !!isOP)}> {' '} # diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx index cebcea9b..7d3ffcab 100644 --- a/src/views/post/post.tsx +++ b/src/views/post/post.tsx @@ -32,6 +32,8 @@ export interface PostProps { onApprove?: () => void; onReject?: () => void; quotedByMap?: Map; + cidToReply?: Map; + isInPostPageView?: boolean; } export const Post = ({ From d862550132d10131f3cb157a6997547df1832e45 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 18:52:10 +0800 Subject: [PATCH 09/12] fix(post): render OP backlinks from replies quoting OP --- src/components/post-desktop/post-desktop.tsx | 22 ++++++----------- src/components/post-mobile/post-mobile.tsx | 25 +++++++------------- src/views/post/post.tsx | 1 - 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index b24e76ca..0976c446 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -76,7 +76,6 @@ const PostInfo = ({ onApprove, onReject, quotedByMap, - cidToReply, }: PostProps) => { const { t } = useTranslation(); const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {}; @@ -442,11 +441,12 @@ const PostInfo = ({ {cid && !parentCid && isInPostPageView && - cidToReply && - [...new Set((post?.quotedCids || []) as string[])].map((quotedCid) => { - const reply = cidToReply.get(quotedCid); - return reply?.cid && !(reply.deleted || reply.removed) && ; - })} + quotedByMap + ?.get(cid) + ?.map( + (reply: Comment) => + reply?.cid && !(reply?.deleted || reply?.removed) && , + )}
); @@ -690,14 +690,6 @@ const PostDesktop = ({ return map; }, [filteredReplies, numberToCid]); - const cidToReply = useMemo(() => { - const map = new Map(); - for (const reply of filteredReplies) { - if (reply.cid) map.set(reply.cid, reply); - } - return map; - }, [filteredReplies]); - // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); const virtuosoStateKey = `replies-desktop-${cid}`; @@ -786,7 +778,7 @@ const PostDesktop = ({ isPublishing={isPublishing} onApprove={onApprove} onReject={onReject} - cidToReply={cidToReply} + quotedByMap={quotedByMap} /> {!isHidden && !content && !(deleted || removed) &&
} {!isHidden && } diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 19b6f757..358699c0 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -380,7 +380,7 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => { ); }; -const ReplyBacklinks = ({ post, quotedByMap, cidToReply, isInPostPageView }: PostProps) => { +const ReplyBacklinks = ({ post, quotedByMap, isInPostPageView }: PostProps) => { const { cid, parentCid } = post || {}; const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } }); @@ -388,13 +388,12 @@ const ReplyBacklinks = ({ post, quotedByMap, cidToReply, isInPostPageView }: Pos cid && !parentCid && isInPostPageView && - cidToReply && - post?.quotedCids?.length && - [...new Set(post.quotedCids as string[])] - .map((quotedCid) => { - const reply = cidToReply.get(quotedCid); - return reply?.cid && !(reply.deleted || reply.removed) && ; - }) + quotedByMap + ?.get(cid) + ?.map( + (reply: Comment) => + reply?.cid && !(reply?.deleted || reply?.removed) && , + ) .filter(Boolean); const replyBacklinks = cid && parentCid && ((replies?.length || 0) > 0 || quotedByMap?.get(cid)?.length) && ( @@ -534,14 +533,6 @@ const PostMobile = ({ return map; }, [filteredReplies, numberToCid]); - const cidToReply = useMemo(() => { - const map = new Map(); - for (const reply of filteredReplies) { - if (reply.cid) map.set(reply.cid, reply); - } - return map; - }, [filteredReplies]); - // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); const virtuosoStateKey = `replies-mobile-${cid}`; @@ -610,7 +601,7 @@ const PostMobile = ({ {shouldShowSnow() && } - +
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx index 7d3ffcab..8ea0cd0d 100644 --- a/src/views/post/post.tsx +++ b/src/views/post/post.tsx @@ -32,7 +32,6 @@ export interface PostProps { onApprove?: () => void; onReject?: () => void; quotedByMap?: Map; - cidToReply?: Map; isInPostPageView?: boolean; } From 18cea185e74836a8eaded22b7e593287cac69299 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 11 Feb 2026 19:01:09 +0800 Subject: [PATCH 10/12] refactor: replace default lodash imports with named imports Replaced `import _ from 'lodash'` with specific named imports (`capitalize`, `lowerCase`, `debounce`, `startCase`) across 18 files. This reduces bundle size and improves tree-shaking efficiency. --- .../board-buttons/board-buttons.tsx | 10 ++++----- src/components/board-header/board-header.tsx | 4 ++-- src/components/catalog-row/catalog-row.tsx | 6 ++--- .../catalog-search/catalog-search.tsx | 4 ++-- .../challenge-modal/challenge-modal.tsx | 4 ++-- .../comment-content/comment-content.tsx | 10 ++++----- src/components/edit-menu/edit-menu.tsx | 18 +++++++-------- src/components/post-desktop/post-desktop.tsx | 16 +++++++------- .../post-menu-desktop/post-menu-desktop.tsx | 4 ++-- src/components/post-form/post-form.tsx | 8 +++---- src/components/post-mobile/post-mobile.tsx | 14 ++++++------ src/components/reply-modal/reply-modal.tsx | 10 ++++----- .../avatar-settings/avatar-settings.tsx | 8 +++---- .../crypto-wallets-setting.tsx | 10 ++++----- .../interface-settings/interface-settings.tsx | 22 +++++++++---------- src/components/topbar/topbar.tsx | 8 +++---- src/views/home/home.tsx | 4 ++-- src/views/mod-queue/mod-queue.tsx | 6 ++--- 18 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 6ee39ad0..87fcaf6b 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -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 ( - {pinned && `${_.capitalize(t('sticky'))} / `} - {closed && `${_.capitalize(t('closed'))} / `} - / + {pinned && `${capitalize(t('sticky'))} / `} + {closed && `${capitalize(t('closed'))} / `} + / ); }; diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index 920acf00..96447a58 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -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 || ''}`; diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index 19aca254..2b1f386a 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -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') )}{' '} - {author?.displayName || _.capitalize(t('anonymous'))} + {author?.displayName || capitalize(t('anonymous'))} {isCatalogPostAuthorMod && {` ## Board ${catalogPostAuthorRole}`}} {(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${Plebbit.getShortAddress({ address: subplebbitAddress })}`} @@ -295,7 +295,7 @@ const CatalogPost = memo(
{t('last_reply_by')}{' '} - {lastReply?.author?.displayName || _.capitalize(t('anonymous'))} + {lastReply?.author?.displayName || capitalize(t('anonymous'))} {isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`} {getFormattedTimeAgo(lastReply?.timestamp)} diff --git a/src/components/catalog-search/catalog-search.tsx b/src/components/catalog-search/catalog-search.tsx index 3be3ad64..86685b30 100644 --- a/src/components/catalog-search/catalog-search.tsx +++ b/src/components/catalog-search/catalog-search.tsx @@ -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); diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index 1eb1b3b2..eb4120cf 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -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 : ( <>
- +
{title && (
diff --git a/src/components/comment-content/comment-content.tsx b/src/components/comment-content/comment-content.tsx index dacedfd5..29e08097 100644 --- a/src/components/comment-content/comment-content.tsx +++ b/src/components/comment-content/comment-content.tsx @@ -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 }) => { ({t('this_post_was_removed')})

- {`${_.capitalize(t('reason'))}: "${reason}"`} + {`${capitalize(t('reason'))}: "${reason}"`} ) : ( - {_.capitalize(t('this_post_was_removed'))}. + {capitalize(t('this_post_was_removed'))}. ) ) : deleted ? ( reason ? ( <> {t('user_deleted_this_post')}{' '} - {`${_.capitalize(t('reason'))}: "${reason}"`} + {`${capitalize(t('reason'))}: "${reason}"`} ) : ( {t('user_deleted_this_post')} @@ -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}"` : ''}`} /> )} diff --git a/src/components/edit-menu/edit-menu.tsx b/src/components/edit-menu/edit-menu.tsx index ed969bea..07c9bd64 100644 --- a/src/components/edit-menu/edit-menu.tsx +++ b/src/components/edit-menu/edit-menu.tsx @@ -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 }) => {
{isContentEditorOpen && ( @@ -307,13 +307,13 @@ const EditMenu = ({ post }: { post: Comment }) => { {' '}
@@ -322,7 +322,7 @@ const EditMenu = ({ post }: { post: Comment }) => { [ ]
@@ -331,7 +331,7 @@ const EditMenu = ({ post }: { post: Comment }) => { [ ] @@ -339,7 +339,7 @@ const EditMenu = ({ post }: { post: Comment }) => { [ ] @@ -378,7 +378,7 @@ const EditMenu = ({ post }: { post: Comment }) => { )}
- {_.capitalize(t('reason'))}? ({t('optional')}) + {capitalize(t('reason'))}? ({t('optional')}) {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 && ( @@ -328,7 +328,7 @@ const PostInfo = ({ <> No. - {state === 'failed' || stateString === 'Failed' ? _.capitalize(t('failed')) : state === 'pending' ? _.capitalize(t('pending')) : ''} + {state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''} )} @@ -346,7 +346,7 @@ const PostInfo = ({ [ !cid && e.preventDefault()}> - {_.capitalize(t('reply'))} + {capitalize(t('reply'))} ] @@ -507,9 +507,9 @@ const PostMedia = ({ )} {t('link')}:{' '} - {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} {' '} - ({type && _.lowerCase(getDisplayMediaInfoType(type, t))} + ({type && lowerCase(getDisplayMediaInfoType(type, t))} {mediaDimensions && `, ${mediaDimensions}`}) {!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && ( diff --git a/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx b/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx index 0f3c0013..e61d0ef7 100644 --- a/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx +++ b/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx @@ -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 && (
diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index 2cea55fc..4fc8b088 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -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(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: { 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'))}? ] diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 358699c0..3040333d 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -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 {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 && ( @@ -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')) ) : ( No. - {state === 'failed' || stateString === 'Failed' ? _.capitalize(t('failed')) : state === 'pending' ? _.capitalize(t('pending')) : ''} + {state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''} )} diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index c32ec98c..d7d8ea8f 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -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(null); const checkContentLength = useRef( - _.debounce((content: string, t: Function) => { + debounce((content: string, t: Function) => { const length = content.trim().length; if (length > 2000) { setError(null); @@ -327,7 +327,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa { setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } }); setPublishReplyOptions({ displayName: e.target.value }); @@ -338,7 +338,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa { setUrl(e.target.value); setPublishReplyOptions({ link: e.target.value }); @@ -386,7 +386,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa [ ] diff --git a/src/components/settings-modal/avatar-settings/avatar-settings.tsx b/src/components/settings-modal/avatar-settings/avatar-settings.tsx index 58ab356d..aee274b7 100644 --- a/src/components/settings-modal/avatar-settings/avatar-settings.tsx +++ b/src/components/settings-modal/avatar-settings/avatar-settings.tsx @@ -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 = () => {
- {_.capitalize(t('chain_ticker'))}: + {capitalize(t('chain_ticker'))}: {
- {_.capitalize(t('timestamp'))}: + {capitalize(t('timestamp'))}: { />
- {_.capitalize(t('paste_signature'))}: + {capitalize(t('paste_signature'))}: { walletsArray.length > 0 ? (
- {_.capitalize(t('chain_ticker'))}: + {capitalize(t('chain_ticker'))}: setWalletsArrayProperty(selectedWallet, 'chainTicker', e.target.value)} @@ -111,7 +111,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => { />
- {_.capitalize(t('wallet_address'))}: + {capitalize(t('wallet_address'))}: setWalletsArrayProperty(selectedWallet, 'address', e.target.value)} @@ -133,7 +133,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
- {_.capitalize(t('timestamp'))}: + {capitalize(t('timestamp'))}: setWalletsArrayProperty(selectedWallet, 'timestamp', Number(e.target.value))} @@ -142,7 +142,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => { />
- {_.capitalize(t('paste_signature'))}: + {capitalize(t('paste_signature'))}: setWalletsArrayProperty(selectedWallet, 'signature', e.target.value)} diff --git a/src/components/settings-modal/interface-settings/interface-settings.tsx b/src/components/settings-modal/interface-settings/interface-settings.tsx index 9be9bf36..8146db14 100644 --- a/src/components/settings-modal/interface-settings/interface-settings.tsx +++ b/src/components/settings-modal/interface-settings/interface-settings.tsx @@ -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 (
- {_.capitalize(t('version'))}: + {capitalize(t('version'))}:
- {_.capitalize(t('update'))}: + {capitalize(t('update'))}:
- {_.capitalize(t('style'))}: