diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 1faca6495..51c5da77d 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -65,6 +65,7 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-pacer": "^0.16.4", "@tanstack/react-query": "^5.62.11", "@transfem-org/sfm-js": "^0.24.6", "@twemoji/svg": "^15.0.0", diff --git a/packages/pl-fe/src/actions/account-notes.test.ts b/packages/pl-fe/src/actions/account-notes.test.ts deleted file mode 100644 index e766e6527..000000000 --- a/packages/pl-fe/src/actions/account-notes.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { __stub } from 'pl-fe/api'; -import { mockStore, rootState } from 'pl-fe/jest/test-helpers'; - -import { submitAccountNote } from './account-notes'; - -describe('submitAccountNote()', () => { - let store: ReturnType; - - beforeEach(() => { - store = mockStore(rootState); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost('/api/v1/accounts/1/note').reply(200, {}); - }); - }); - - it('post the note to the API', async() => { - const expectedActions = [ - { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, - { type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} }, - ]; - await store.dispatch(submitAccountNote('1', 'hello')); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost('/api/v1/accounts/1/note').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, - { - type: 'ACCOUNT_NOTE_SUBMIT_FAIL', - error: new Error('Network Error'), - }, - ]; - await store.dispatch(submitAccountNote('1', 'hello')); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); diff --git a/packages/pl-fe/src/actions/account-notes.ts b/packages/pl-fe/src/actions/account-notes.ts deleted file mode 100644 index dc72037c1..000000000 --- a/packages/pl-fe/src/actions/account-notes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { importEntities } from 'pl-fe/actions/importer'; - -import { getClient } from '../api'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const submitAccountNote = (accountId: string, value: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).accounts.updateAccountNote(accountId, value) - .then(response => dispatch(importEntities({ relationships: [response] }))); - -export { submitAccountNote }; diff --git a/packages/pl-fe/src/actions/chats.ts b/packages/pl-fe/src/actions/chats.ts index cfccec407..364f65be7 100644 --- a/packages/pl-fe/src/actions/chats.ts +++ b/packages/pl-fe/src/actions/chats.ts @@ -3,7 +3,7 @@ import { useSettingsStore } from 'pl-fe/stores/settings'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const toggleMainWindow = () => +const toggleChatPane = () => (dispatch: AppDispatch, getState: () => RootState) => { const main = useSettingsStore.getState().settings.chats.mainWindow; const state = main === 'minimized' ? 'open' : 'minimized'; @@ -11,5 +11,5 @@ const toggleMainWindow = () => }; export { - toggleMainWindow, + toggleChatPane, }; diff --git a/packages/pl-fe/src/contexts/chat-context.tsx b/packages/pl-fe/src/contexts/chat-context.tsx index a5a35ef9e..dbd879c0c 100644 --- a/packages/pl-fe/src/contexts/chat-context.tsx +++ b/packages/pl-fe/src/contexts/chat-context.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { toggleMainWindow } from 'pl-fe/actions/chats'; +import { toggleChatPane } from 'pl-fe/actions/chats'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useChat } from 'pl-fe/queries/chats'; import { useSettings } from 'pl-fe/stores/settings'; @@ -45,13 +45,13 @@ const ChatProvider: React.FC = ({ children }) => { setScreen(screen); }; - const toggleChatPane = () => dispatch(toggleMainWindow()); + const handleChatPaneToggle = () => dispatch(toggleChatPane()); const value = useMemo(() => ({ chat, isOpen, isUsingMainChatPage, - toggleChatPane, + toggleChatPane: handleChatPaneToggle, screen, changeScreen, currentChatId, diff --git a/packages/pl-fe/src/features/ui/components/panels/account-note-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/account-note-panel.tsx index d0072fa3f..0aa716912 100644 --- a/packages/pl-fe/src/features/ui/components/panels/account-note-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/account-note-panel.tsx @@ -1,23 +1,15 @@ -import debounce from 'lodash/debounce'; -import React, { useEffect, useRef, useState } from 'react'; +import { debounce } from '@tanstack/react-pacer/debouncer'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { submitAccountNote } from 'pl-fe/actions/account-notes'; import HStack from 'pl-fe/components/ui/hstack'; import Text from 'pl-fe/components/ui/text'; import Textarea from 'pl-fe/components/ui/textarea'; import Widget from 'pl-fe/components/ui/widget'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useUpdateAccountNoteMutation } from 'pl-fe/queries/accounts/use-relationship'; import type { Account as AccountEntity } from 'pl-fe/normalizers/account'; -import type { AppDispatch } from 'pl-fe/store'; - -const onSave = debounce( - (dispatch: AppDispatch, accountId: string, value: string, callback: () => void) => - dispatch(submitAccountNote(accountId, value)).then(() => callback()), - 900, -); const messages = defineMessages({ placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' }, @@ -30,9 +22,12 @@ interface IAccountNotePanel { const AccountNotePanel: React.FC = ({ account }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); const me = useAppSelector((state) => state.me); + const { mutate: updateAccountNote } = useUpdateAccountNoteMutation(account.id); + + const debouncedUpdateAccountNote = useCallback(debounce(updateAccountNote, { wait: 900 }), []); + const textarea = useRef(null); const [value, setValue] = useState(account.relationship?.note); @@ -41,9 +36,11 @@ const AccountNotePanel: React.FC = ({ account }) => { const handleChange: React.ChangeEventHandler = e => { setValue(e.target.value); - onSave(dispatch, account.id, e.target.value, () => { - setSaved(true); - setTimeout(() => setSaved(false), 2000); + debouncedUpdateAccountNote(e.target.value, { + onSuccess: () => { + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }, }); }; diff --git a/packages/pl-fe/src/queries/accounts/use-relationship.ts b/packages/pl-fe/src/queries/accounts/use-relationship.ts index 347188957..37efa8521 100644 --- a/packages/pl-fe/src/queries/accounts/use-relationship.ts +++ b/packages/pl-fe/src/queries/accounts/use-relationship.ts @@ -236,6 +236,23 @@ const useRemoveAccountFromFollowersMutation = (accountId: string) => { }); }; +const useUpdateAccountNoteMutation = (accountId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['accountNote', accountId], + mutationFn: (note: string) => client.accounts.updateAccountNote(accountId, note), + onMutate: (note) => updateRelationship(accountId, { + note, + }, queryClient), + onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient), + onSuccess: (data) => { + queryClient.setQueryData(['accountRelationships', accountId], data); + }, + }); +}; + export { useRelationshipQuery, useFollowAccountMutation, @@ -247,4 +264,5 @@ export { usePinAccountMutation, useUnpinAccountMutation, useRemoveAccountFromFollowersMutation, + useUpdateAccountNoteMutation, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2e4a5875..a0113610a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,6 +196,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17) + '@tanstack/react-pacer': + specifier: ^0.16.4 + version: 0.16.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': specifier: ^5.62.11 version: 5.84.1(react@18.3.1) @@ -2322,14 +2325,38 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tanstack/devtools-event-client@0.3.3': + resolution: {integrity: sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg==} + engines: {node: '>=18'} + + '@tanstack/pacer@0.15.4': + resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==} + engines: {node: '>=18'} + '@tanstack/query-core@5.83.1': resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} + '@tanstack/react-pacer@0.16.4': + resolution: {integrity: sha512-nuQLE8bx0rYMiJau4jOTPZFp3XC/GnIHDKfKVVWeKUHNF4grRdVHPgTlJ8EV/nt/HJxSUnIcy+IIKX+Bj0bLSw==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-query@5.84.1': resolution: {integrity: sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==} peerDependencies: react: ^18 || ^19 + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -8625,13 +8652,36 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 + '@tanstack/devtools-event-client@0.3.3': {} + + '@tanstack/pacer@0.15.4': + dependencies: + '@tanstack/devtools-event-client': 0.3.3 + '@tanstack/store': 0.7.7 + '@tanstack/query-core@5.83.1': {} + '@tanstack/react-pacer@0.16.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/pacer': 0.15.4 + '@tanstack/react-store': 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-query@5.84.1(react@18.3.1)': dependencies: '@tanstack/query-core': 5.83.1 react: 18.3.1 + '@tanstack/react-store@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/store': 0.7.7 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) + + '@tanstack/store@0.7.7': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -10299,7 +10349,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) transitivePeerDependencies: - supports-color