diff --git a/packages/pl-fe/src/actions/admin.ts b/packages/pl-fe/src/actions/admin.ts index 4fc495e84..b3a007c51 100644 --- a/packages/pl-fe/src/actions/admin.ts +++ b/packages/pl-fe/src/actions/admin.ts @@ -8,8 +8,9 @@ import { setComposeToStatus } from './compose'; import { STATUS_FETCH_SOURCE_FAIL, type StatusesAction } from './statuses'; import { deleteFromTimelines } from './timelines'; -import type { PleromaConfig } from 'pl-api'; +import type { PleromaConfig, Poll } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; +import { queryClient } from 'pl-fe/queries/client'; const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS' as const; @@ -125,7 +126,7 @@ const redactStatus = (statusId: string) => (dispatch: AppDispatch, getState: () const state = getState(); const status = state.statuses[statusId]!; - const poll = status.poll_id ? state.polls[status.poll_id] : undefined; + const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; return getClient(state).statuses.getStatusSource(statusId).then(response => { dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false, undefined, undefined, true)); diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index e6778c511..c61ef66eb 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -1,6 +1,7 @@ import { importEntities as importEntityStoreEntities } from 'pl-fe/entity-store/actions'; import { Entities } from 'pl-fe/entity-store/entities'; import { normalizeGroup } from 'pl-fe/normalizers/group'; +import { queryClient } from 'pl-fe/queries/client'; import type { Account as BaseAccount, Group as BaseGroup, Poll as BasePoll, Relationship as BaseRelationship, Status as BaseStatus } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -113,7 +114,11 @@ const importEntities = (entities: { if (!isEmpty(accounts)) dispatch(importEntityStoreEntities(Object.values(accounts), Entities.ACCOUNTS)); if (!isEmpty(groups)) dispatch(importEntityStoreEntities(Object.values(groups).map(normalizeGroup), Entities.GROUPS)); - if (!isEmpty(polls)) dispatch(({ type: POLLS_IMPORT, polls: Object.values(polls) })); + if (!isEmpty(polls)) { + for (const poll of Object.values(polls)) { + queryClient.setQueryData(['statuses', 'polls', poll.id], poll); + } + } if (!isEmpty(relationships)) dispatch(importEntityStoreEntities(Object.values(relationships), Entities.RELATIONSHIPS)); if (!isEmpty(statuses)) dispatch({ type: STATUSES_IMPORT, statuses: Object.values(statuses) }); }; diff --git a/packages/pl-fe/src/actions/polls.ts b/packages/pl-fe/src/actions/polls.ts deleted file mode 100644 index 43458d058..000000000 --- a/packages/pl-fe/src/actions/polls.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getClient } from '../api'; - -import { importEntities } from './importer'; - -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const vote = (pollId: string, choices: number[]) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState()).polls.vote(pollId, choices).then((data) => { - dispatch(importEntities({ polls: [data] })); - }); - -const fetchPoll = (pollId: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState()).polls.getPoll(pollId).then((data) => { - dispatch(importEntities({ polls: [data] })); - }); - -export { - vote, - fetchPoll, -}; diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 4894a5dc7..ad50e0b2a 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -11,7 +11,7 @@ import { setComposeToStatus } from './compose'; import { importEntities } from './importer'; import { deleteFromTimelines } from './timelines'; -import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, StatusSource } from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, StatusSource, Poll } from 'pl-api'; import type { Status } from 'pl-fe/normalizers/status'; import type { AppDispatch, RootState } from 'pl-fe/store'; import type { IntlShape } from 'react-intl'; @@ -91,7 +91,7 @@ const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => const state = getState(); const status = state.statuses[statusId]!; - const poll = status.poll_id ? state.polls[status.poll_id] : undefined; + const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); @@ -123,7 +123,7 @@ const deleteStatus = (statusId: string, groupId?: string, withRedraft = false) = const state = getState(); const status = state.statuses[statusId]!; - const poll = status.poll_id ? state.polls[status.poll_id] : undefined; + const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; dispatch({ type: STATUS_DELETE_REQUEST, params: status }); diff --git a/packages/pl-fe/src/components/polls/poll-footer.tsx b/packages/pl-fe/src/components/polls/poll-footer.tsx index 26b532c78..797b4351c 100644 --- a/packages/pl-fe/src/components/polls/poll-footer.tsx +++ b/packages/pl-fe/src/components/polls/poll-footer.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchPoll, vote } from 'pl-fe/actions/polls'; import Button from 'pl-fe/components/ui/button'; import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import Tooltip from 'pl-fe/components/ui/tooltip'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { usePollQuery, usePollVoteMutation } from 'pl-fe/queries/statuses/use-poll'; import { useStatusMetaActions } from 'pl-fe/stores/status-meta'; import RelativeTimestamp from '../relative-timestamp'; @@ -28,15 +27,17 @@ interface IPollFooter { } const PollFooter: React.FC = ({ poll, showResults, selected, statusId }): JSX.Element => { - const dispatch = useAppDispatch(); const intl = useIntl(); + const { refetch } = usePollQuery(poll.id); + const { mutate: vote } = usePollVoteMutation(poll.id); + const { toggleShowPollResults } = useStatusMetaActions(); - const handleVote = () => dispatch(vote(poll.id, Object.keys(selected) as any as number[])); + const handleVote = () => vote(Object.keys(selected) as any as number[]); const handleRefresh: React.EventHandler = (e) => { - dispatch(fetchPoll(poll.id)); + refetch(); e.stopPropagation(); e.preventDefault(); }; diff --git a/packages/pl-fe/src/components/polls/poll.tsx b/packages/pl-fe/src/components/polls/poll.tsx index a9b71ca0d..2cd6fda57 100644 --- a/packages/pl-fe/src/components/polls/poll.tsx +++ b/packages/pl-fe/src/components/polls/poll.tsx @@ -1,11 +1,10 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { vote } from 'pl-fe/actions/polls'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { usePollQuery, usePollVoteMutation } from 'pl-fe/queries/statuses/use-poll'; import { useModalsActions } from 'pl-fe/stores/modals'; import { useStatusMeta } from 'pl-fe/stores/status-meta'; @@ -25,10 +24,12 @@ interface IPoll { const Poll: React.FC = ({ id, status, language, truncate }): JSX.Element | null => { const { openModal } = useModalsActions(); - const dispatch = useAppDispatch(); const isLoggedIn = useAppSelector((state) => state.me); - const poll = useAppSelector((state) => state.polls[id]); + + const { data: poll } = usePollQuery(id); + // TODO: handle pending mutation state + const { mutate: vote } = usePollVoteMutation(id); const { showPollResults } = useStatusMeta(status.id); @@ -40,8 +41,6 @@ const Poll: React.FC = ({ id, status, language, truncate }): JSX.Element ap_id: status?.url, }); - const handleVote = (selectedId: number) => dispatch(vote(id, [selectedId])); - const toggleOption = (value: number) => { if (isLoggedIn) { if (poll?.multiple) { @@ -56,7 +55,7 @@ const Poll: React.FC = ({ id, status, language, truncate }): JSX.Element const tmp: Selected = {}; tmp[value] = true; setSelected(tmp); - handleVote(value); + vote([value]); } } else { openUnauthorizedModal(); diff --git a/packages/pl-fe/src/queries/statuses/use-poll.ts b/packages/pl-fe/src/queries/statuses/use-poll.ts new file mode 100644 index 000000000..5c40d9542 --- /dev/null +++ b/packages/pl-fe/src/queries/statuses/use-poll.ts @@ -0,0 +1,29 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { useClient } from 'pl-fe/hooks/use-client'; + +import type { Poll } from 'pl-api'; + +const usePollQuery = (pollId: string) => { + const client = useClient(); + + return useQuery({ + queryKey: ['statuses', 'polls', pollId], + queryFn: () => client.polls.getPoll(pollId), + }); +}; + +const usePollVoteMutation = (pollId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['statuses', 'polls', pollId, 'vote'], + mutationFn: (choices: number[]) => client.polls.vote(pollId, choices), + onSuccess: (poll) => { + queryClient.setQueryData(['statuses', 'polls', pollId], poll); + }, + }); +}; + +export { usePollQuery, usePollVoteMutation }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index b9d55c69d..713ded7b5 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -17,7 +17,6 @@ import meta from './meta'; import notifications from './notifications'; import pending_statuses from './pending-statuses'; import plfe from './pl-fe'; -import polls from './polls'; import push_notifications from './push-notifications'; import statuses from './statuses'; import timelines from './timelines'; @@ -37,7 +36,6 @@ const reducers = { notifications, pending_statuses, plfe, - polls, push_notifications, statuses, timelines, diff --git a/packages/pl-fe/src/reducers/polls.test.ts b/packages/pl-fe/src/reducers/polls.test.ts deleted file mode 100644 index a911241f3..000000000 --- a/packages/pl-fe/src/reducers/polls.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { POLLS_IMPORT } from 'pl-fe/actions/importer'; - -import reducer from './polls'; - -describe('polls reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); - - describe('POLLS_IMPORT', () => { - it('normalizes the poll', () => { - const polls = [{ id: '3', options: [{ title: 'Apples' }, { title: 'Oranges' }] }]; - const action = { type: POLLS_IMPORT, polls }; - - const result = reducer(undefined, action); - - const expected = { - '3': { - options: [ - { title: 'Apples', votes_count: 0 }, - { title: 'Oranges', votes_count: 0 }, - ], - emojis: [], - expired: false, - multiple: false, - voters_count: 0, - votes_count: 0, - own_votes: null, - voted: false, - }, - }; - - expect(result.toJS()).toMatchObject(expected); - }); - }); -}); diff --git a/packages/pl-fe/src/reducers/polls.ts b/packages/pl-fe/src/reducers/polls.ts deleted file mode 100644 index f5557958d..000000000 --- a/packages/pl-fe/src/reducers/polls.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { create } from 'mutative'; - -import { POLLS_IMPORT, type ImporterAction } from 'pl-fe/actions/importer'; - -import type { Poll } from 'pl-api'; - -type State = Record; - -const initialState: State = {}; - -const polls = (state: State = initialState, action: ImporterAction): State => { - switch (action.type) { - case POLLS_IMPORT: - return create(state, (draft) => action.polls.forEach(poll => draft[poll.id] = poll)); - default: - return state; - } -}; - -export { polls as default }; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index c98a05c81..14b4cf389 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -131,14 +131,13 @@ const makeGetStatus = () => createSelector( if (group) return state.entities[Entities.GROUPS]?.store[group] as Group; return undefined; }, - (state: RootState, { id }: APIStatus) => state.polls[id] || null, (_state: RootState, { username }: APIStatus) => username, getFilters, (state: RootState) => state.me, (state: RootState) => state.auth.client.features, ], - (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features) => { + (statusBase, statusReblog, statusQuote, statusGroup, username, filters, me, features) => { // const locale = getLocale('en'); if (!statusBase) return null; @@ -159,7 +158,6 @@ const makeGetStatus = () => createSelector( reblog: statusReblog || null, quote: statusQuote || null, group: statusGroup || null, - poll, filtered, }; },