diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 491d1aaeb..348daa636 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -9,7 +9,7 @@ import { setComposeToStatus } from './compose'; import { importEntities } from './importer'; import { deleteFromTimelines } from './timelines'; -import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, Translation } from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus } 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'; @@ -48,11 +48,6 @@ const STATUS_HIDE_MEDIA = 'STATUS_HIDE_MEDIA' as const; const STATUS_EXPAND_SPOILER = 'STATUS_EXPAND_SPOILER' as const; const STATUS_COLLAPSE_SPOILER = 'STATUS_COLLAPSE_SPOILER' as const; -const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST' as const; -const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS' as const; -const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL' as const; -const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO' as const; - const STATUS_UNFILTER = 'STATUS_UNFILTER' as const; const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE' as const; @@ -269,75 +264,54 @@ const expandStatusSpoiler = (statusIds: string[] | string) => { }; }; -let TRANSLATIONS_QUEUE: Set = new Set(); -let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null; +// let TRANSLATIONS_QUEUE: Set = new Set(); +// let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null; -const translateStatus = (statusId: string, targetLanguage: string, lazy?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - const client = getClient(getState); - const features = client.features; +// const translateStatus = (statusId: string, targetLanguage: string, lazy?: boolean) => +// (dispatch: AppDispatch, getState: () => RootState) => { +// const client = getClient(getState); +// const features = client.features; - dispatch({ type: STATUS_TRANSLATE_REQUEST, statusId }); +// const handleTranslateMany = () => { +// const copy = [...TRANSLATIONS_QUEUE]; +// TRANSLATIONS_QUEUE = new Set(); +// if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); - const handleTranslateMany = () => { - const copy = [...TRANSLATIONS_QUEUE]; - TRANSLATIONS_QUEUE = new Set(); - if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); +// return client.statuses.translateStatuses(copy, targetLanguage).then((response) => { +// response.forEach((translation) => { +// dispatch({ +// type: STATUS_TRANSLATE_SUCCESS, +// statusId: translation.id, +// translation: translation, +// }); - return client.statuses.translateStatuses(copy, targetLanguage).then((response) => { - response.forEach((translation) => { - dispatch({ - type: STATUS_TRANSLATE_SUCCESS, - statusId: translation.id, - translation: translation, - }); +// copy +// .filter((statusId) => !response.some(({ id }) => id === statusId)) +// .forEach((statusId) => dispatch({ +// type: STATUS_TRANSLATE_FAIL, +// statusId, +// })); +// }); +// }).catch(error => { +// dispatch({ +// type: STATUS_TRANSLATE_FAIL, +// statusId, +// error, +// }); +// }); +// }; - copy - .filter((statusId) => !response.some(({ id }) => id === statusId)) - .forEach((statusId) => dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - })); - }); - }).catch(error => { - dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - error, - }); - }); - }; +// if (features.lazyTranslations && lazy) { +// TRANSLATIONS_QUEUE.add(statusId); - if (features.lazyTranslations && lazy) { - TRANSLATIONS_QUEUE.add(statusId); +// if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); +// TRANSLATIONS_TIMEOUT = setTimeout(() => handleTranslateMany(), 3000); +// } else if (features.lazyTranslations && TRANSLATIONS_QUEUE.size) { +// TRANSLATIONS_QUEUE.add(statusId); - if (TRANSLATIONS_TIMEOUT) clearTimeout(TRANSLATIONS_TIMEOUT); - TRANSLATIONS_TIMEOUT = setTimeout(() => handleTranslateMany(), 3000); - } else if (features.lazyTranslations && TRANSLATIONS_QUEUE.size) { - TRANSLATIONS_QUEUE.add(statusId); - - handleTranslateMany(); - } else { - return client.statuses.translateStatus(statusId, targetLanguage).then(response => { - dispatch({ - type: STATUS_TRANSLATE_SUCCESS, - statusId, - translation: response, - }); - }).catch(error => { - dispatch({ - type: STATUS_TRANSLATE_FAIL, - statusId, - error, - }); - }); - } - }; - -const undoStatusTranslation = (statusId: string) => ({ - type: STATUS_TRANSLATE_UNDO, - statusId, -}); +// handleTranslateMany(); +// } +// }; const unfilterStatus = (statusId: string) => ({ type: STATUS_UNFILTER, @@ -376,10 +350,6 @@ type StatusesAction = | ReturnType | ReturnType | ReturnType - | { type: typeof STATUS_TRANSLATE_REQUEST; statusId: string } - | { type: typeof STATUS_TRANSLATE_SUCCESS; statusId: string | null; translation: Translation } - | { type: typeof STATUS_TRANSLATE_FAIL; statusId: string; error?: unknown } - | ReturnType | ReturnType | ReturnType; @@ -409,10 +379,6 @@ export { STATUS_HIDE_MEDIA, STATUS_EXPAND_SPOILER, STATUS_COLLAPSE_SPOILER, - STATUS_TRANSLATE_REQUEST, - STATUS_TRANSLATE_SUCCESS, - STATUS_TRANSLATE_FAIL, - STATUS_TRANSLATE_UNDO, STATUS_UNFILTER, STATUS_LANGUAGE_CHANGE, createStatus, @@ -430,8 +396,6 @@ export { toggleStatusMediaHidden, expandStatusSpoiler, collapseStatusSpoiler, - translateStatus, - undoStatusTranslation, unfilterStatus, changeStatusLanguage, type StatusesAction, diff --git a/packages/pl-fe/src/api/hooks/statuses/use-status-translation.ts b/packages/pl-fe/src/api/hooks/statuses/use-status-translation.ts new file mode 100644 index 000000000..8f1ace4d4 --- /dev/null +++ b/packages/pl-fe/src/api/hooks/statuses/use-status-translation.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useClient } from 'pl-fe/hooks/use-client'; + +import type { Translation } from 'pl-api'; + +const useStatusTranslation = (statusId: string, targetLanguage?: string) => { + const client = useClient(); + + return useQuery({ + queryKey: ['statuses', 'translations', statusId, targetLanguage], + queryFn: () => client.statuses.translateStatus(statusId, targetLanguage) + .then(translation => translation).catch(() => false), + enabled: !!targetLanguage, + }); +}; + +export { useStatusTranslation }; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 7e5eb9dea..dbeb44665 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -11,7 +11,7 @@ import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { changeSetting } from 'pl-fe/actions/settings'; -import { deleteStatus, editStatus, toggleMuteStatus, translateStatus, undoStatusTranslation } from 'pl-fe/actions/statuses'; +import { deleteStatus, editStatus, toggleMuteStatus } from 'pl-fe/actions/statuses'; import { deleteFromTimelines } from 'pl-fe/actions/timelines'; import { useBlockGroupMember } from 'pl-fe/api/hooks/groups/use-block-group-member'; import { useDeleteGroupStatus } from 'pl-fe/api/hooks/groups/use-delete-group-status'; @@ -33,6 +33,7 @@ import { useSettings } from 'pl-fe/hooks/use-settings'; import { useChats } from 'pl-fe/queries/chats'; import { RootState } from 'pl-fe/store'; import { useModalsStore } from 'pl-fe/stores/modals'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import toast from 'pl-fe/toast'; import copy from 'pl-fe/utils/copy'; @@ -574,7 +575,6 @@ interface IMenuButton extends IActionButton { const MenuButton: React.FC = ({ status, statusActionButtonTheme, - withLabels, me, expandable, fromBookmarks, @@ -585,6 +585,8 @@ const MenuButton: React.FC = ({ const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const { boostModal } = useSettings(); + const { statuses: statusesMeta, fetchTranslation, hideTranslation } = useStatusMetaStore(); + const targetLanguage = statusesMeta[status.id]?.targetLanguage; const { openModal } = useModalsStore(); const { group } = useGroup((status.group as Group)?.id as string); const deleteGroupStatus = useDeleteGroupStatus(group as Group, status.id); @@ -774,10 +776,10 @@ const MenuButton: React.FC = ({ }; const handleTranslate = () => { - if (status.translation) { - dispatch(undoStatusTranslation(status.id)); + if (targetLanguage) { + hideTranslation(status.id); } else { - dispatch(translateStatus(status.id, intl.locale)); + fetchTranslation(status.id, intl.locale); } }; @@ -941,7 +943,7 @@ const MenuButton: React.FC = ({ } if (autoTranslating) { - if (status.translation) { + if (targetLanguage) { menu.push({ text: intl.formatMessage(messages.hideTranslation), action: handleTranslate, diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index b6a187ed0..b2288966d 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -3,6 +3,7 @@ import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 're import { FormattedMessage } from 'react-intl'; import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses'; +import { useStatusTranslation } from 'pl-fe/api/hooks/statuses/use-status-translation'; import Icon from 'pl-fe/components/icon'; import Button from 'pl-fe/components/ui/button'; import Stack from 'pl-fe/components/ui/stack'; @@ -11,6 +12,7 @@ import Emojify from 'pl-fe/features/emoji/emojify'; import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content'; import { getTextDirection } from '../utils/rtl'; @@ -91,6 +93,9 @@ const StatusContent: React.FC = React.memo(({ const node = useRef(null); const spoilerNode = useRef(null); + const { statuses: statusesMeta } = useStatusMetaStore(); + const { data: translation } = useStatusTranslation(status.id, statusesMeta[status.id]?.targetLanguage); + const maybeSetCollapsed = (): void => { if (!node.current) return; @@ -125,12 +130,12 @@ const StatusContent: React.FC = React.memo(({ }); const content = useMemo( - (): string => translatable && status.translation - ? status.translation.content! + (): string => translation + ? translation.content : (status.content_map && status.currentLanguage) ? (status.content_map[status.currentLanguage] || status.content) : status.content, - [status.content, status.translation, status.currentLanguage], + [status.content, translation, status.currentLanguage], ); const { content: parsedContent, hashtags } = useMemo(() => parseContent({ diff --git a/packages/pl-fe/src/components/translate-button.tsx b/packages/pl-fe/src/components/translate-button.tsx index e9f85166f..3d8e41198 100644 --- a/packages/pl-fe/src/components/translate-button.tsx +++ b/packages/pl-fe/src/components/translate-button.tsx @@ -1,26 +1,25 @@ import React, { useEffect } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { translateStatus, undoStatusTranslation } from 'pl-fe/actions/statuses'; import { useTranslationLanguages } from 'pl-fe/api/hooks/instance/use-translation-languages'; +import { useStatusTranslation } from 'pl-fe/api/hooks/statuses/use-status-translation'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; 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 { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import type { Status } from 'pl-fe/normalizers/status'; interface ITranslateButton { - status: Pick; + status: Pick; } const TranslateButton: React.FC = ({ status }) => { - const dispatch = useAppDispatch(); const intl = useIntl(); const features = useFeatures(); const instance = useInstance(); @@ -30,6 +29,10 @@ const TranslateButton: React.FC = ({ status }) => { const me = useAppSelector((state) => state.me); const { translationLanguages } = useTranslationLanguages(); + const { statuses: statusesMeta, fetchTranslation, hideTranslation } = useStatusMetaStore(); + + const targetLanguage = statusesMeta[status.id]?.targetLanguage; + const translationQuery = useStatusTranslation(status.id, targetLanguage); const { allow_remote: allowRemote, @@ -43,45 +46,45 @@ const TranslateButton: React.FC = ({ status }) => { const handleTranslate: React.MouseEventHandler = (e) => { e.stopPropagation(); - if (status.translation) { - dispatch(undoStatusTranslation(status.id)); + if (targetLanguage) { + hideTranslation(status.id); } else { - dispatch(translateStatus(status.id, intl.locale)); + fetchTranslation(status.id, intl.locale); } }; useEffect(() => { - if (status.translation === null && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && status.translation !== false && status.language !== null && !knownLanguages.includes(status.language)) { - dispatch(translateStatus(status.id, intl.locale, true)); + if (translationQuery.data === undefined && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && translationQuery.data !== false && status.language !== null && !knownLanguages.includes(status.language)) { + fetchTranslation(status.id, intl.locale); } }, []); - if (!features.translations || !renderTranslate || !supportsLanguages || status.translation === false) return null; + if (!features.translations || !renderTranslate || !supportsLanguages || translationQuery.data === false) return null; const button = ( ); - if (status.translation) { + if (translationQuery.data) { const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' }); const languageName = languageNames.of(status.language!); - const provider = status.translation.provider; + const provider = translationQuery.data.provider; return ( diff --git a/packages/pl-fe/src/normalizers/status.ts b/packages/pl-fe/src/normalizers/status.ts index 9c2c5ebc7..6f3a47ce1 100644 --- a/packages/pl-fe/src/normalizers/status.ts +++ b/packages/pl-fe/src/normalizers/status.ts @@ -3,7 +3,7 @@ * Converts API statuses into our internal format. * @see {@link https://docs.joinmastodon.org/entities/status/} */ -import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api'; +import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema } from 'pl-api'; import * as v from 'valibot'; import { unescapeHTML } from 'pl-fe/utils/html'; @@ -20,7 +20,6 @@ type CalculatedValues = { search_index: string; expanded?: boolean | null; hidden?: boolean | null; - translation?: Translation | null | false; currentLanguage?: string; }; @@ -57,11 +56,11 @@ const buildSearchContent = (status: Pick { if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) { const { - search_index, hidden, expanded, translation, currentLanguage, + search_index, hidden, expanded, currentLanguage, } = oldStatus; return { - search_index, hidden, expanded, translation, currentLanguage, + search_index, hidden, expanded, currentLanguage, }; } else { const searchContent = buildSearchContent(status); @@ -129,7 +128,6 @@ const normalizeStatus = (status: BaseStatus & { reblog_id: status.reblog?.id || null, poll_id: status.poll?.id || null, group_id: status.group?.id || null, - translating: false, expectsCard: false, showFiltered: null as null | boolean, ...status, @@ -144,7 +142,6 @@ const normalizeStatus = (status: BaseStatus & { group, media_attachments, ...calculated, - translation: (status.translation || calculated.translation || null) as Translation | null | false, }; }; diff --git a/packages/pl-fe/src/reducers/statuses.ts b/packages/pl-fe/src/reducers/statuses.ts index 95c4e5f78..bc5e5f49c 100644 --- a/packages/pl-fe/src/reducers/statuses.ts +++ b/packages/pl-fe/src/reducers/statuses.ts @@ -39,10 +39,6 @@ import { STATUS_HIDE_MEDIA, STATUS_MUTE_SUCCESS, STATUS_REVEAL_MEDIA, - STATUS_TRANSLATE_FAIL, - STATUS_TRANSLATE_REQUEST, - STATUS_TRANSLATE_SUCCESS, - STATUS_TRANSLATE_UNDO, STATUS_UNFILTER, STATUS_UNMUTE_SUCCESS, STATUS_LANGUAGE_CHANGE, @@ -52,7 +48,7 @@ import { } from '../actions/statuses'; import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; -import type { Status as BaseStatus, CreateStatusParams, Translation } from 'pl-api'; +import type { Status as BaseStatus, CreateStatusParams } from 'pl-api'; type State = Record; @@ -162,18 +158,6 @@ const simulateDislike = ( state[statusId] = updatedStatus; }; -/** Import translation from translation service into the store. */ -const importTranslation = (state: State, statusId: string, translation: Translation) => { - if (!state[statusId]) return; - state[statusId].translation = translation; - state[statusId].translating = false; -}; - -/** Delete translation from the store. */ -const deleteTranslation = (state: State, statusId: string) => { - state[statusId].translation = null; -}; - const initialState: State = {}; const statuses = (state = initialState, action: EmojiReactsAction | EventsAction | ImporterAction | InteractionsAction | StatusesAction | TimelineAction): State => { @@ -300,7 +284,6 @@ const statuses = (state = initialState, action: EmojiReactsAction | EventsAction const status = draft[id]; if (status) { status.expanded = false; - status.translation = false; } }); }); @@ -308,25 +291,6 @@ const statuses = (state = initialState, action: EmojiReactsAction | EventsAction return create(state, (draft) => decrementReplyCount(draft, action.params)); case STATUS_DELETE_FAIL: return create(state, (draft) => incrementReplyCount(draft, action.params)); - case STATUS_TRANSLATE_REQUEST: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.translating = true; - } - }); - case STATUS_TRANSLATE_SUCCESS: - return action.statusId !== null ? create(state, (draft) => importTranslation(draft, action.statusId!, action.translation)) : state; - case STATUS_TRANSLATE_FAIL: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.translating = false; - status.translation = false; - } - }); - case STATUS_TRANSLATE_UNDO: - return create(state, (draft) => deleteTranslation(draft, action.statusId)); case STATUS_UNFILTER: return create(state, (draft) => { const status = draft[action.statusId]; diff --git a/packages/pl-fe/src/stores/status-meta.ts b/packages/pl-fe/src/stores/status-meta.ts new file mode 100644 index 000000000..8625842c4 --- /dev/null +++ b/packages/pl-fe/src/stores/status-meta.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +type State = { + statuses: Record; + revealStatus: (statusId: string) => void; + hideStatus: (statusId: string) => void; + fetchTranslation: (statusId: string, targetLanguage: string) => void; + hideTranslation: (statusId: string) => void; +}; + +const useStatusMetaStore = create()(mutative((set) => ({ + statuses: {}, + revealStatus: (statusId) => set((state: State) => { + if (!state.statuses[statusId]) state.statuses[statusId] = {}; + + state.statuses[statusId].visible = true; + }), + hideStatus: (statusId) => set((state: State) => { + if (!state.statuses[statusId]) state.statuses[statusId] = {}; + + state.statuses[statusId].visible = false; + }), + fetchTranslation: (statusId, targetLanguage) => set((state: State) => { + if (!state.statuses[statusId]) state.statuses[statusId] = {}; + + state.statuses[statusId].targetLanguage = targetLanguage; + }), + hideTranslation: (statusId) => set((state: State) => { + if (!state.statuses[statusId]) state.statuses[statusId] = {}; + + state.statuses[statusId].targetLanguage = undefined; + }), +}))); + +export { useStatusMetaStore };