From 0f9ce151ec4661c66dc8fe891601a952e5894b1a Mon Sep 17 00:00:00 2001 From: Mboulgour Date: Tue, 29 Sep 2026 15:55:06 +0200 Subject: [PATCH] fix(storage): stop crashing when browser storage is full or blocked Text editor drafts and session-stored input state called setItem/getItem unguarded (via @rooks storage hooks), so a QuotaExceededError or SecurityError crashed the input. Replace both rooks hooks with guarded access: a useSessionStorageState drop-in hook and direct try/catch in the text editor autosave/reset plugins. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 - pnpm-lock.yaml | 67 --------------- .../auth/signup_form/SignupForm.jsx | 2 +- .../auth/social_auth_form/SocialAuthForm.jsx | 2 +- src/components/auth/sso_form/SSOForm.jsx | 2 +- .../hooks/use_session_storage_state/index.js | 1 + .../useSessionStorageState.js | 28 ++++++ .../useSessionStorageState.test.jsx | 36 ++++++++ .../input/argument_input/ArgumentInput.jsx | 2 +- .../input/text_editor/TextEditor.test.jsx | 86 +++++++++++++++++++ .../text_editor/plugins/AutoSavePlugin.jsx | 30 +++---- .../input/text_editor/plugins/ResetPlugin.jsx | 9 +- .../intl/intl_provider/IntlProvider.jsx | 2 +- .../suggestion_input/SuggestionInput.jsx | 2 +- src/components/vote/vote_box/VoteBox.jsx | 2 +- src/index.js | 1 + 16 files changed, 175 insertions(+), 99 deletions(-) create mode 100644 src/components/hooks/use_session_storage_state/index.js create mode 100644 src/components/hooks/use_session_storage_state/useSessionStorageState.js create mode 100644 src/components/hooks/use_session_storage_state/useSessionStorageState.test.jsx diff --git a/package.json b/package.json index 27c3c48a..bda9ba5b 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,7 @@ "@lexical/rich-text": "^0.45.0", "@lexical/selection": "^0.45.0", "@lexical/utils": "^0.45.0", - "@rooks/use-localstorage-state": "^4.11.2", "@rooks/use-previous": "^4.11.2", - "@rooks/use-sessionstorage-state": "^4.11.2", "axios": "^1.15.2", "chart.js": "^4.5.1", "chartjs-plugin-datalabels": "^2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59ecb219..24706537 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,15 +32,9 @@ importers: '@lexical/utils': specifier: ^0.45.0 version: 0.45.0 - '@rooks/use-localstorage-state': - specifier: ^4.11.2 - version: 4.11.2(react@18.3.1) '@rooks/use-previous': specifier: ^4.11.2 version: 4.11.2 - '@rooks/use-sessionstorage-state': - specifier: ^4.11.2 - version: 4.11.2(react@18.3.1) axios: specifier: ^1.15.2 version: 1.18.0 @@ -200,28 +194,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.36.3': resolution: {integrity: sha512-2XRmNYuovZu0Pa4J3or4PKMkQZnXXfpVcCrPwWB/2ytX7XUo+TWLgYE8rPVnJOyw5zujkveFb0XUrro9mQgLzw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.36.3': resolution: {integrity: sha512-mTwPRbBi1feGqR2b5TWC5gkEDeRi8wfk4euF5sKNihfMGHj6pdfINHQ3QvLVO4C7z0r/wgWLAvditFA0b997dg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.36.3': resolution: {integrity: sha512-tMGPrT+zuZzJK6n1cD1kOii7HYZE9gUXjwtVNE/uZqXEaWP6lmkfoTMbLjnxEe74VQbmaoDGh1/cjrDBnqC6Uw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.36.3': resolution: {integrity: sha512-7pFyr9+dyV+4cBJJ1I57gg6PDXP3GBQeVAsEEitzEruxx4Hb4cyNro54gGtlsS+6ty+N0t004tPQxYO2VrsPIg==} @@ -358,28 +348,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.5.0': resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.5.0': resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.5.0': resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.5.0': resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==} @@ -985,56 +971,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.127.0': resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.127.0': resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.127.0': resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.127.0': resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.127.0': resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.127.0': resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} @@ -1107,49 +1085,41 @@ packages: resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.20.0': resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.20.0': resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.20.0': resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.20.0': resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==} @@ -1200,42 +1170,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -1312,79 +1276,66 @@ packages: resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.0': resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.0': resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.0': resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.0': resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.0': resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.0': resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.0': resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.0': resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.0': resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.0': resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.0': resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.0': resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.62.0': resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} @@ -1416,19 +1367,9 @@ packages: cpu: [x64] os: [win32] - '@rooks/use-localstorage-state@4.11.2': - resolution: {integrity: sha512-HJeV+396dQBgSElMSr14t0qCvh2gPo0g/NQ2ZGSGPEvrKDls88FHq/PiXV8PGqirjvogSIW1PEQjkUsfb5cS7A==} - peerDependencies: - react: '>=16.8.0' - '@rooks/use-previous@4.11.2': resolution: {integrity: sha512-hZFDm5NPleSANTbDaQfrwWH6XSYElEToee7KC1fu38BqI09XzWCjjPPUak+4P6otTArgudFyuX8SiA4C28Tacw==} - '@rooks/use-sessionstorage-state@4.11.2': - resolution: {integrity: sha512-HXbqiNRV4Su6nvIj3L3b9r3P8SdUjFfByj6CP481SWpj9zs9BdGFauhC1AH+Ms6a3rqVmEJZA+NyuzN0LKfD8g==} - peerDependencies: - react: '>=16.8.0' - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3699,16 +3640,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.0': optional: true - '@rooks/use-localstorage-state@4.11.2(react@18.3.1)': - dependencies: - react: 18.3.1 - '@rooks/use-previous@4.11.2': {} - '@rooks/use-sessionstorage-state@4.11.2(react@18.3.1)': - dependencies: - react: 18.3.1 - '@standard-schema/spec@1.1.0': {} '@storybook/addon-a11y@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': diff --git a/src/components/auth/signup_form/SignupForm.jsx b/src/components/auth/signup_form/SignupForm.jsx index e5adb911..c94dedc9 100644 --- a/src/components/auth/signup_form/SignupForm.jsx +++ b/src/components/auth/signup_form/SignupForm.jsx @@ -2,7 +2,7 @@ import { Button } from "@logora/debate/action/button"; import { EMAIL_CONSENT_STORAGE_KEY } from "@logora/debate/auth/use_auth"; import { TextInput } from "@logora/debate/input/text_input"; import { Toggle } from "@logora/debate/input/toggle"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import cx from "classnames"; import React, { useState } from "react"; import { useIntl } from "react-intl"; diff --git a/src/components/auth/social_auth_form/SocialAuthForm.jsx b/src/components/auth/social_auth_form/SocialAuthForm.jsx index 77431d8b..055dfb21 100644 --- a/src/components/auth/social_auth_form/SocialAuthForm.jsx +++ b/src/components/auth/social_auth_form/SocialAuthForm.jsx @@ -5,7 +5,7 @@ import { SignupForm } from "@logora/debate/auth/signup_form"; import { EMAIL_CONSENT_STORAGE_KEY } from "@logora/debate/auth/use_auth"; import { Icon } from "@logora/debate/icons/icon"; import { Toggle } from "@logora/debate/input/toggle"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import styles from "./SocialAuthForm.module.scss"; diff --git a/src/components/auth/sso_form/SSOForm.jsx b/src/components/auth/sso_form/SSOForm.jsx index 96c3d6b4..4282e2a5 100644 --- a/src/components/auth/sso_form/SSOForm.jsx +++ b/src/components/auth/sso_form/SSOForm.jsx @@ -5,7 +5,7 @@ import { } from "@logora/debate/auth/use_auth"; import { Icon } from "@logora/debate/icons/icon"; import { Toggle } from "@logora/debate/input/toggle"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import cx from "classnames"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/components/hooks/use_session_storage_state/index.js b/src/components/hooks/use_session_storage_state/index.js new file mode 100644 index 00000000..d1f9fffd --- /dev/null +++ b/src/components/hooks/use_session_storage_state/index.js @@ -0,0 +1 @@ +export { useSessionStorageState } from "./useSessionStorageState"; diff --git a/src/components/hooks/use_session_storage_state/useSessionStorageState.js b/src/components/hooks/use_session_storage_state/useSessionStorageState.js new file mode 100644 index 00000000..675ee2fe --- /dev/null +++ b/src/components/hooks/use_session_storage_state/useSessionStorageState.js @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; + +const read = (key, initialState) => { + try { + const value = JSON.parse(sessionStorage.getItem(key)); + return value === null ? initialState : value; + } catch (e) { + return initialState; + } +}; + +export const useSessionStorageState = (key, initialState) => { + const [value, setValue] = useState(() => read(key, initialState)); + + useEffect(() => { + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch (e) {} + }, [value]); + + const remove = () => { + try { + sessionStorage.removeItem(key); + } catch (e) {} + }; + + return [value, setValue, remove]; +}; diff --git a/src/components/hooks/use_session_storage_state/useSessionStorageState.test.jsx b/src/components/hooks/use_session_storage_state/useSessionStorageState.test.jsx new file mode 100644 index 00000000..5dd6bc20 --- /dev/null +++ b/src/components/hooks/use_session_storage_state/useSessionStorageState.test.jsx @@ -0,0 +1,36 @@ +import { act, renderHook } from "@testing-library/react"; +import { useSessionStorageState } from "./useSessionStorageState"; + +describe("useSessionStorageState", () => { + afterEach(() => { + vi.restoreAllMocks(); + sessionStorage.clear(); + }); + + it("should read, save and remove the value", () => { + sessionStorage.setItem("key", JSON.stringify({ a: 1 })); + const { result } = renderHook(() => useSessionStorageState("key", {})); + expect(result.current[0]).toEqual({ a: 1 }); + + act(() => result.current[1]({ a: 2 })); + expect(JSON.parse(sessionStorage.getItem("key"))).toEqual({ a: 2 }); + + act(() => result.current[2]()); + expect(sessionStorage.getItem("key")).toBeNull(); + }); + + it("should not throw when storage is full or blocked", () => { + const error = () => { + throw new DOMException("quota", "QuotaExceededError"); + }; + vi.spyOn(Storage.prototype, "getItem").mockImplementation(error); + vi.spyOn(Storage.prototype, "setItem").mockImplementation(error); + vi.spyOn(Storage.prototype, "removeItem").mockImplementation(error); + + const { result } = renderHook(() => useSessionStorageState("key", {})); + expect(result.current[0]).toEqual({}); + act(() => result.current[1]({ a: 2 })); + expect(result.current[0]).toEqual({ a: 2 }); + act(() => result.current[2]()); + }); +}); diff --git a/src/components/input/argument_input/ArgumentInput.jsx b/src/components/input/argument_input/ArgumentInput.jsx index 838354a6..4ed4e3fe 100644 --- a/src/components/input/argument_input/ArgumentInput.jsx +++ b/src/components/input/argument_input/ArgumentInput.jsx @@ -13,7 +13,7 @@ import { TogglePosition } from "@logora/debate/input/toggle_position"; import { useList } from "@logora/debate/list/list_provider"; import { AuthorBox } from "@logora/debate/user/author_box"; import { Avatar } from "@logora/debate/user/avatar"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import React, { useState, useEffect, useRef, lazy, Suspense } from "react"; import { FormattedMessage } from "react-intl"; import { useIntl } from "react-intl"; diff --git a/src/components/input/text_editor/TextEditor.test.jsx b/src/components/input/text_editor/TextEditor.test.jsx index a55d594c..b2d37313 100644 --- a/src/components/input/text_editor/TextEditor.test.jsx +++ b/src/components/input/text_editor/TextEditor.test.jsx @@ -792,5 +792,91 @@ describe("AutoSavePlugin", () => { await waitFor(() => { expect(getItemMock).toHaveBeenCalled(); }); + + const saved = JSON.parse(window.localStorage.getItem("TextEditor:content_34")); + expect(saved.editorState).toContain("Integer pretium varius odio ac eleifend."); + + container.unmount(); + render( + + + + + null} + onSubmit={() => null} + onActivation={() => null} + uid={"34"} + placeholder={"Add something"} + /> + + + + , + ); + await waitFor(() => { + expect(screen.getByText("Integer pretium varius odio ac eleifend.")).toBeTruthy(); + }); + }); + + it("should not crash when local storage quota is exceeded", async () => { + const setItemMock = vi + .spyOn(window.localStorage, "setItem") + .mockImplementation(() => { + throw new DOMException("quota", "QuotaExceededError"); + }); + + const SetContent = () => { + const { setInputRichContent } = useInput(); + return ( +
+ setInputRichContent({ + root: { + children: [ + { + children: [{ detail: 0, format: 0, mode: "normal", style: "", text: "Hello", type: "text", version: 1 }], + direction: "auto", format: "", indent: 0, type: "paragraph", version: 1, + }, + ], + direction: "auto", format: "", indent: 0, type: "root", version: 1, + }, + }) + } + > + Set content +
+ ); + }; + + render( + + + + + + null} + onSubmit={() => null} + onActivation={() => null} + uid={"quota"} + placeholder={"Add something"} + /> + + + + , + ); + + await userEvent.click(screen.getByText("Set content")); + await waitFor(() => expect(screen.getByText("Hello")).toBeTruthy()); + + await act(async () => { + await new Promise((r) => setTimeout(r, 1500)); + }); + + expect(setItemMock).toHaveBeenCalled(); + expect(screen.getByText("Hello")).toBeTruthy(); + setItemMock.mockRestore(); }); }); diff --git a/src/components/input/text_editor/plugins/AutoSavePlugin.jsx b/src/components/input/text_editor/plugins/AutoSavePlugin.jsx index 7f7a9c50..b1f686c9 100644 --- a/src/components/input/text_editor/plugins/AutoSavePlugin.jsx +++ b/src/components/input/text_editor/plugins/AutoSavePlugin.jsx @@ -1,34 +1,30 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; -import useLocalstorageState from "@rooks/use-localstorage-state"; import React, { useEffect } from "react"; import { useDebouncedCallback } from "use-debounce"; export const AutoSavePlugin = ({ storageUid, onSetContent }) => { const [editor] = useLexicalComposerContext(); - const [content, setContent, removeContent] = useLocalstorageState( - `TextEditor:content_${storageUid}`, - {}, - ); + const storageKey = `TextEditor:content_${storageUid}`; useEffect(() => { - if (content) { - if (Object.keys(content).length !== 0) { - const editorState = editor.parseEditorState(content.editorState); - editor.setEditorState(editorState); + try { + const content = JSON.parse(localStorage.getItem(storageKey)); + if (content?.editorState) { + editor.setEditorState(editor.parseEditorState(content.editorState)); onSetContent?.(); } - } + } catch (e) {} }, []); const onChange = useDebouncedCallback( - (editorState, editor) => { - editorState.read(() => { - const sessionUserContent = { - editorState: JSON.stringify(editor.getEditorState()), - }; - setContent(sessionUserContent); - }); + (editorState) => { + try { + localStorage.setItem( + storageKey, + JSON.stringify({ editorState: JSON.stringify(editorState) }), + ); + } catch (e) {} }, 1000, false, diff --git a/src/components/input/text_editor/plugins/ResetPlugin.jsx b/src/components/input/text_editor/plugins/ResetPlugin.jsx index 479d6954..487759c7 100644 --- a/src/components/input/text_editor/plugins/ResetPlugin.jsx +++ b/src/components/input/text_editor/plugins/ResetPlugin.jsx @@ -1,16 +1,11 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { useInput } from "@logora/debate/input/input_provider"; -import useLocalstorageState from "@rooks/use-localstorage-state"; import { $addUpdateTag, $createParagraphNode, $getRoot } from "lexical"; import { useEffect, useRef } from "react"; export const ResetPlugin = ({ storageUid, resetSignal, isReply = false }) => { const [editor] = useLexicalComposerContext(); const { reset, setReset } = useInput(); - const [content, setContent, removeContent] = useLocalstorageState( - `TextEditor:content_${storageUid}`, - {}, - ); const previousResetSignal = useRef(resetSignal); useEffect(() => { @@ -30,7 +25,9 @@ export const ResetPlugin = ({ storageUid, resetSignal, isReply = false }) => { selection.format = 0; selection.style = ""; - removeContent(); + try { + localStorage.removeItem(`TextEditor:content_${storageUid}`); + } catch (e) {} if (reset) { setReset(false); } diff --git a/src/components/intl/intl_provider/IntlProvider.jsx b/src/components/intl/intl_provider/IntlProvider.jsx index a97ef804..b4b61d51 100644 --- a/src/components/intl/intl_provider/IntlProvider.jsx +++ b/src/components/intl/intl_provider/IntlProvider.jsx @@ -1,4 +1,4 @@ -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import { flatten } from "flat"; import React, { useEffect, useState } from "react"; import { IntlProvider as ReactIntlProvider } from "react-intl"; diff --git a/src/components/suggestion/suggestion_input/SuggestionInput.jsx b/src/components/suggestion/suggestion_input/SuggestionInput.jsx index b9529a79..b729a52d 100644 --- a/src/components/suggestion/suggestion_input/SuggestionInput.jsx +++ b/src/components/suggestion/suggestion_input/SuggestionInput.jsx @@ -7,7 +7,7 @@ import { useAuthRequired } from "@logora/debate/hooks/use_auth_required"; import { useInput } from "@logora/debate/input/input_provider"; import { TextInput } from "@logora/debate/input/text_input"; import { useList } from "@logora/debate/list/list_provider"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import cx from "classnames"; import React, { useState, useRef, useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/components/vote/vote_box/VoteBox.jsx b/src/components/vote/vote_box/VoteBox.jsx index 4d867033..e901a305 100644 --- a/src/components/vote/vote_box/VoteBox.jsx +++ b/src/components/vote/vote_box/VoteBox.jsx @@ -8,7 +8,7 @@ import { useAuthRequired } from "@logora/debate/hooks/use_auth_required"; import { Icon } from "@logora/debate/icons/icon"; import { ProgressBar } from "@logora/debate/progress/progress_bar"; import { useTranslatedContent } from "@logora/debate/translation/translated_content"; -import useSessionStorageState from "@rooks/use-sessionstorage-state"; +import { useSessionStorageState } from "@logora/debate/hooks/use_session_storage_state"; import cx from "classnames"; import React, { useState, useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; diff --git a/src/index.js b/src/index.js index 07e049cb..d4ec7a0a 100644 --- a/src/index.js +++ b/src/index.js @@ -51,6 +51,7 @@ export * from "./components/hooks/use_font_loader"; export * from "./components/hooks/use_matomo"; export * from "./components/hooks/use_relative_time"; export * from "./components/hooks/use_responsive"; +export * from "./components/hooks/use_session_storage_state"; export * from "./components/icons/icon"; export * from "./components/icons/icon_provider"; export * as regularIcons from "./components/icons/regular_icons";