diff --git a/src/actions/compose.ts b/src/actions/compose.ts index 8c54fc8711..601a813f1f 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -31,24 +31,24 @@ import type { History } from 'soapbox/types/history'; let cancelFetchComposeSuggestions = new AbortController(); -const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; -const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; -const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const; -const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const; -const COMPOSE_REPLY = 'COMPOSE_REPLY' as const; -const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const; -const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const; -const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const; -const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const; -const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const; -const COMPOSE_MENTION = 'COMPOSE_MENTION' as const; -const COMPOSE_RESET = 'COMPOSE_RESET' as const; -const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const; -const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const; -const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const; +const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; +const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; +const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const; +const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const; +const COMPOSE_REPLY = 'COMPOSE_REPLY' as const; +const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const; +const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const; +const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const; +const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const; +const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const; +const COMPOSE_MENTION = 'COMPOSE_MENTION' as const; +const COMPOSE_RESET = 'COMPOSE_RESET' as const; +const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const; +const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const; +const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const; const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const; -const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const; -const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const; +const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const; +const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const; const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const; const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const; @@ -60,27 +60,28 @@ const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE' as const; const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const; const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const; const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const; -const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const; -const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const; -const COMPOSE_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const; -const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' as const; +const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const; +const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const; +const COMPOSE_MODIFIED_LANGUAGE_CHANGE = 'COMPOSE_MODIFIED_LANGUAGE_CHANGE' as const; +const COMPOSE_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const; +const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' as const; const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const; const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const; -const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const; -const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const; -const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const; +const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const; +const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const; +const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const; -const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const; -const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const; -const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const; -const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const; -const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const; +const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const; +const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const; +const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const; +const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const; +const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const; const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const; -const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const; -const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const; +const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const; +const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const; const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const; const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const; @@ -92,7 +93,7 @@ const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const; const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const; -const COMPOSE_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const; +const COMPOSE_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const; const COMPOSE_ADD_SUGGESTED_LANGUAGE = 'COMPOSE_ADD_SUGGESTED_LANGUAGE' as const; const getAccount = makeGetAccount(); @@ -301,7 +302,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c }; const needsDescriptions = (state: RootState, composeId: string) => { - const media = state.compose.get(composeId)!.media_attachments; + const media = state.compose.get(composeId)!.media_attachments; const missingDescriptionModal = getSettings(state).get('missingDescriptionModal'); const hasMissing = media.filter(item => !item.description).size > 0; @@ -332,10 +333,10 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => const compose = state.compose.get(composeId)!; - const status = compose.text; - const media = compose.media_attachments; + const status = compose.text; + const media = compose.media_attachments; const statusId = compose.id; - let to = compose.to; + let to = compose.to; if (!validateSchedule(state, composeId)) { toast.error(messages.scheduleError); @@ -387,6 +388,22 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => to, }; + if (compose.language && compose.textMap.size) { + params.status_map = compose.textMap.toJS(); + params.status_map[compose.language] = status; + + if (params.spoiler_text) { + params.spoiler_text_map = compose.spoilerTextMap.toJS(); + params.spoiler_text_map[compose.language] = compose.spoiler_text; + } + + if (params.poll) { + const poll = params.poll.toJS(); + poll.options.forEach((option: any, index: number) => poll.options_map[index][compose.language!] = option); + params.poll = poll; + } + } + if (compose.privacy === 'group') { params.group_id = compose.group_id; } @@ -425,7 +442,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => if (!isLoggedIn(getState)) return; const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; - const media = getState().compose.get(composeId)?.media_attachments; + const media = getState().compose.get(composeId)?.media_attachments; const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); @@ -438,7 +455,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => dispatch(uploadComposeRequest(composeId)); - Array.from(files).forEach(async(f, i) => { + Array.from(files).forEach(async (f, i) => { if (mediaCount + i > attachmentLimit - 1) return; dispatch(uploadFile( @@ -659,15 +676,15 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str let completion = '', startPosition = position; if (typeof suggestion === 'object' && suggestion.id) { - completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; + completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; startPosition = position - 1; dispatch(chooseEmoji(suggestion)); } else if (typeof suggestion === 'string' && suggestion[0] === '#') { - completion = suggestion; + completion = suggestion; startPosition = position - 1; } else if (typeof suggestion === 'string') { - completion = selectAccount(getState(), suggestion)!.acct; + completion = selectAccount(getState(), suggestion)!.acct; startPosition = position; } @@ -743,13 +760,19 @@ const changeComposeLanguage = (composeId: string, value: Language | null) => ({ value, }); -const addComposeLanguage = (composeId: string, value: Language | null) => ({ +const changeComposeModifiedLanguage = (composeId: string, value: Language | null) => ({ + type: COMPOSE_MODIFIED_LANGUAGE_CHANGE, + id: composeId, + value, +}); + +const addComposeLanguage = (composeId: string, value: Language) => ({ type: COMPOSE_LANGUAGE_ADD, id: composeId, value, }); -const deleteComposeLanguage = (composeId: string, value: Language | null) => ({ +const deleteComposeLanguage = (composeId: string, value: Language) => ({ type: COMPOSE_LANGUAGE_DELETE, id: composeId, value, @@ -945,6 +968,7 @@ type ComposeAction = | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType @@ -994,6 +1018,7 @@ export { COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, COMPOSE_LANGUAGE_CHANGE, + COMPOSE_MODIFIED_LANGUAGE_CHANGE, COMPOSE_LANGUAGE_ADD, COMPOSE_LANGUAGE_DELETE, COMPOSE_LISTABILITY_CHANGE, @@ -1057,6 +1082,7 @@ export { changeComposeSpoilerText, changeComposeVisibility, changeComposeLanguage, + changeComposeModifiedLanguage, addComposeLanguage, deleteComposeLanguage, insertEmojiCompose, diff --git a/src/actions/statuses.ts b/src/actions/statuses.ts index cc4543dcfa..6fb9a31250 100644 --- a/src/actions/statuses.ts +++ b/src/actions/statuses.ts @@ -13,42 +13,44 @@ import type { APIEntity, Status } from 'soapbox/types/entities'; const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST'; const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS'; -const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL'; +const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL'; const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST'; const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS'; -const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL'; +const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL'; const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; -const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; +const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; -const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; +const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; -const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; +const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; -const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; +const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; -const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; +const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; const STATUS_REVEAL = 'STATUS_REVEAL'; -const STATUS_HIDE = 'STATUS_HIDE'; +const STATUS_HIDE = 'STATUS_HIDE'; const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST'; const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; -const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; -const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; +const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; +const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; const STATUS_UNFILTER = 'STATUS_UNFILTER'; +const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE'; + const statusExists = (getState: () => RootState, statusId: string) => (getState().statuses.get(statusId) || null) !== null; @@ -185,7 +187,7 @@ const fetchContext = (id: string) => }; const fetchStatusWithContext = (id: string) => - async(dispatch: AppDispatch, getState: () => RootState) => { + async (dispatch: AppDispatch, getState: () => RootState) => { await Promise.all([ dispatch(fetchContext(id)), dispatch(fetchStatus(id)), @@ -286,6 +288,12 @@ const unfilterStatus = (id: string) => ({ id, }); +const changeStatusLanguage = (id: string, language: string) => ({ + type: STATUS_LANGUAGE_CHANGE, + id, + language, +}); + export { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -315,6 +323,7 @@ export { STATUS_TRANSLATE_FAIL, STATUS_TRANSLATE_UNDO, STATUS_UNFILTER, + STATUS_LANGUAGE_CHANGE, createStatus, editStatus, fetchStatus, @@ -331,4 +340,5 @@ export { translateStatus, undoStatusTranslation, unfilterStatus, + changeStatusLanguage, }; diff --git a/src/actions/timelines.ts b/src/actions/timelines.ts index 91eef46ce7..de7bff5662 100644 --- a/src/actions/timelines.ts +++ b/src/actions/timelines.ts @@ -192,8 +192,6 @@ const expandTimeline = (timelineId: string, path: string, params: Record status.accounts))); - console.log(response); - dispatch(expandTimelineSuccess( timelineId, statuses, diff --git a/src/components/account.tsx b/src/components/account.tsx index 41404fd86b..79000cd246 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -23,7 +23,6 @@ interface IInstanceFavicon { const messages = defineMessages({ bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, - languageVersions: { id: 'status.language_versions', defaultMessage: 'The post has multiple language versions.' }, }); const InstanceFavicon: React.FC = ({ account, disabled }) => { @@ -91,11 +90,11 @@ interface IAccount { withLinkToProfile?: boolean; withRelationship?: boolean; showEdit?: boolean; - showMultiLanguage?: boolean; approvalStatus?: StatusApprovalStatus; emoji?: string; emojiUrl?: string; note?: string; + items?: React.ReactNode; } const Account = ({ @@ -118,11 +117,11 @@ const Account = ({ withLinkToProfile = true, withRelationship = true, showEdit = false, - showMultiLanguage = false, approvalStatus, emoji, emojiUrl, note, + items, }: IAccount) => { const overflowRef = useRef(null); const actionRef = useRef(null); @@ -271,16 +270,6 @@ const Account = ({ ) : null} - {showMultiLanguage ? ( - <> - · - - - - ) : null} - {actionType === 'muting' && account.mute_expires_at ? ( <> · @@ -288,6 +277,8 @@ const Account = ({ ) : null} + + {items} {note ? ( diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index b751bdcdec..3268cb106b 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -242,10 +242,6 @@ const DropdownMenu = (props: IDropdownMenu) => { useEffect(() => { if (isOpen) { - if (refs.floating.current) { - (refs.floating.current?.querySelector('li a[role=\'button\']') as HTMLAnchorElement)?.focus(); - } - document.addEventListener('click', handleDocumentClick, false); document.addEventListener('keydown', handleKeyDown, false); document.addEventListener('touchend', handleDocumentClick, listenerOptions); diff --git a/src/components/polls/poll-option.tsx b/src/components/polls/poll-option.tsx index fbb8985ef1..b16cbede15 100644 --- a/src/components/polls/poll-option.tsx +++ b/src/components/polls/poll-option.tsx @@ -100,10 +100,11 @@ interface IPollOption { showResults?: boolean; active: boolean; onToggle: (value: number) => void; + language?: string | null; } const PollOption: React.FC = (props): JSX.Element | null => { - const { index, poll, option, showResults } = props; + const { index, poll, option, showResults, language } = props; const intl = useIntl(); @@ -133,7 +134,7 @@ const PollOption: React.FC = (props): JSX.Element | null => { diff --git a/src/components/polls/poll.tsx b/src/components/polls/poll.tsx index 3f59686136..1a7741f133 100644 --- a/src/components/polls/poll.tsx +++ b/src/components/polls/poll.tsx @@ -10,11 +10,13 @@ import { Stack, Text } from '../ui'; import PollFooter from './poll-footer'; import PollOption from './poll-option'; +import type { Status } from 'soapbox/types/entities'; + type Selected = Record; interface IPoll { id: string; - status?: string; + status?: Status; } const messages = defineMessages({ @@ -33,7 +35,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { const openUnauthorizedModal = () => dispatch(openModal('UNAUTHORIZED', { action: 'POLL_VOTE', - ap_id: status, + ap_id: status?.url, })); const handleVote = (selectedId: number) => dispatch(vote(id, [String(selectedId)])); @@ -83,6 +85,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { showResults={showResults} active={!!selected[i]} onToggle={toggleOption} + language={status?.currentLanguage} /> ))} diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 107f7b1f59..35700438c4 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -77,8 +77,12 @@ const StatusContent: React.FC = React.memo(({ }); const parsedHtml = useMemo( - (): string => translatable && status.translation ? status.translation.get('content')! : status.contentHtml, - [status.contentHtml, status.translation], + (): string => translatable && status.translation + ? status.translation.get('content')! + : (status.contentMapHtml && status.currentLanguage) + ? status.contentMapHtml.get(status.currentLanguage, status.contentHtml) + : status.contentHtml, + [status.contentHtml, status.translation, status.currentLanguage], ); if (status.content.length === 0) { @@ -160,7 +164,7 @@ const StatusContent: React.FC = React.memo(({ const hasPoll = status.poll && typeof status.poll === 'string'; if (hasPoll) { - output.push(); + output.push(); } return
{output}
; @@ -182,7 +186,7 @@ const StatusContent: React.FC = React.memo(({ ]; if (status.poll && typeof status.poll === 'string') { - output.push(); + output.push(); } return <>{output}; diff --git a/src/components/status-language-picker.tsx b/src/components/status-language-picker.tsx new file mode 100644 index 0000000000..16cd1bd859 --- /dev/null +++ b/src/components/status-language-picker.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { changeStatusLanguage } from 'soapbox/actions/statuses'; +import { type Language, languages } from 'soapbox/features/preferences'; +import { useAppDispatch } from 'soapbox/hooks'; + +import DropdownMenu from './dropdown-menu'; +import { HStack, Icon, Text } from './ui'; + +import type { Status } from 'soapbox/types/entities'; + +const messages = defineMessages({ + languageVersions: { id: 'status.language_versions', defaultMessage: 'The post has multiple language versions.' }, +}); + +interface IStatusLanguagePicker { + status: Status; + showLabel?: boolean; +} + +const StatusLanguagePicker: React.FC = ({ status, showLabel }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + if (!status.contentMapHtml || status.contentMapHtml.isEmpty()) return null; + + const icon = ; + + return ( + <> + · + + ({ + text: languages[language as Language] || language, + action: () => dispatch(changeStatusLanguage(status.id, language)), + active: language === status.currentLanguage, + }))} + > + + + + ); +}; + +export { + StatusLanguagePicker as default, +}; diff --git a/src/components/status.tsx b/src/components/status.tsx index 4a2920290a..21a0ba9890 100644 --- a/src/components/status.tsx +++ b/src/components/status.tsx @@ -18,6 +18,7 @@ import { textForScreenReader, getActualStatus } from 'soapbox/utils/status'; import EventPreview from './event-preview'; import StatusActionBar from './status-action-bar'; import StatusContent from './status-content'; +import StatusLanguagePicker from './status-language-picker'; import StatusMedia from './status-media'; import StatusReplyMentions from './status-reply-mentions'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; @@ -428,11 +429,11 @@ const Status: React.FC = (props) => { action={accountAction} hideActions={!accountAction} showEdit={!!actualStatus.edited_at} - showMultiLanguage={!!actualStatus.content_map && actualStatus.content_map?.count() > 1} showProfileHoverCard={hoverable} withLinkToProfile={hoverable} approvalStatus={actualStatus.approval_status} avatarSize={avatarSize} + items={} />
diff --git a/src/components/statuses/sensitive-content-overlay.tsx b/src/components/statuses/sensitive-content-overlay.tsx index e856a7b521..3b4b4d6d71 100644 --- a/src/components/statuses/sensitive-content-overlay.tsx +++ b/src/components/statuses/sensitive-content-overlay.tsx @@ -47,6 +47,10 @@ const SensitiveContentOverlay = React.forwardRef ) : (
-
+
{intl.formatMessage(messages.sensitiveTitle)} @@ -79,7 +83,7 @@ const SensitiveContentOverlay = React.forwardRef - “” + “
)} diff --git a/src/components/translate-button.tsx b/src/components/translate-button.tsx index 68c7d64c94..7991ba1e3c 100644 --- a/src/components/translate-button.tsx +++ b/src/components/translate-button.tsx @@ -27,7 +27,7 @@ const TranslateButton: React.FC = ({ status }) => { target_languages: targetLanguages, } = instance.pleroma.metadata.translation; - const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; + const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language && !status.contentMapHtml?.has(intl.locale); const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale)); diff --git a/src/features/compose/components/compose-form.tsx b/src/features/compose/components/compose-form.tsx index 8d18578877..3dcdc8c52c 100644 --- a/src/features/compose/components/compose-form.tsx +++ b/src/features/compose/components/compose-form.tsx @@ -87,6 +87,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab schedule: scheduledAt, group_id: groupId, text, + modified_language: modifiedLanguage, } = compose; const prevSpoiler = usePrevious(spoiler); @@ -282,6 +283,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab
= ({ composeId }) => { ], }); - const { language, suggested_language: suggestedLanguage, textMap } = useCompose(composeId); + const { + language, + modified_language: modifiedLanguage, + suggested_language: suggestedLanguage, + textMap, + } = useCompose(composeId); const handleClick: React.EventHandler< React.MouseEvent | React.KeyboardEvent @@ -87,8 +92,6 @@ const LanguageDropdown: React.FC = ({ composeId }) => { } }; - const handleChange = (language: Language | null) => dispatch(changeComposeLanguage(composeId, language)); - const handleOptionKeyDown: React.KeyboardEventHandler = e => { const value = e.currentTarget.getAttribute('data-index'); const index = results.findIndex(([key]) => key === value); @@ -125,10 +128,17 @@ const LanguageDropdown: React.FC = ({ composeId }) => { const handleOptionClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language; + if (textMap.size) { + if (!(textMap.has(value) || language === value)) return; + + dispatch(changeComposeModifiedLanguage(composeId, value)); + } else { + dispatch(changeComposeLanguage(composeId, value)); + } + e.preventDefault(); setIsOpen(false); - handleChange(value); }; const handleAddLanguageClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { @@ -288,7 +298,7 @@ const LanguageDropdown: React.FC = ({ composeId }) => { let buttonLabel = intl.formatMessage(messages.languagePrompt); if (language) { - const list: string[] = [languagesObject[language]]; + const list: string[] = [languagesObject[modifiedLanguage || language]]; if (textMap.size) list.push(intl.formatMessage(messages.multipleLanguages, { count: textMap.size, })); @@ -347,6 +357,7 @@ const LanguageDropdown: React.FC = ({ composeId }) => {
{results.map(([code, name]) => { const active = code === language; + const modified = code === modifiedLanguage; return (
= ({ composeId }) => { onKeyDown={handleOptionKeyDown} onClick={handleOptionClick} className={clsx( - 'flex cursor-pointer gap-2 p-2.5 text-sm text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800', - { 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active }, + 'flex gap-2 p-2.5 text-sm text-gray-700 dark:text-gray-400', + { + 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700': modified, + 'cursor-pointer hover:bg-gray-100 black:hover:bg-gray-900 dark:hover:bg-gray-800': !textMap.size || textMap.has(code), + 'cursor-pointer': active, + 'cursor-default': !active && !(!textMap.size || textMap.has(code)), + }, )} aria-selected={active} ref={active ? focusedItem : null} >
{name} diff --git a/src/features/compose/components/polls/poll-form.tsx b/src/features/compose/components/polls/poll-form.tsx index 44dab7dff0..9b6e505118 100644 --- a/src/features/compose/components/polls/poll-form.tsx +++ b/src/features/compose/components/polls/poll-form.tsx @@ -48,7 +48,7 @@ const Option: React.FC = ({ const dispatch = useAppDispatch(); const intl = useIntl(); - const suggestions = useCompose(composeId).suggestions; + const { suggestions } = useCompose(composeId); const handleOptionTitleChange = (event: React.ChangeEvent) => onChange(index, event.target.value); @@ -112,11 +112,11 @@ const PollForm: React.FC = ({ composeId }) => { const intl = useIntl(); const { configuration } = useInstance(); - const compose = useCompose(composeId); + const { poll, language, modified_language: modifiedLanguage } = useCompose(composeId); - const options = compose.poll?.options; - const expiresIn = compose.poll?.expires_in; - const isMultiple = compose.poll?.multiple; + const options = !modifiedLanguage || modifiedLanguage === language ? poll?.options : poll?.options_map.map((option, key) => option.get(modifiedLanguage, poll.options.get(key)!)); + const expiresIn = poll?.expires_in; + const isMultiple = poll?.multiple; const { max_options: maxOptions, diff --git a/src/features/compose/components/spoiler-input.tsx b/src/features/compose/components/spoiler-input.tsx index 22476efb34..e8a5fd59a7 100644 --- a/src/features/compose/components/spoiler-input.tsx +++ b/src/features/compose/components/spoiler-input.tsx @@ -26,7 +26,7 @@ const SpoilerInput = React.forwardRef(({ }, ref) => { const intl = useIntl(); const dispatch = useAppDispatch(); - const compose = useCompose(composeId); + const { language, modified_language, spoiler, spoiler_text: spoilerText, spoilerTextMap, suggestions } = useCompose(composeId); const handleChangeSpoilerText: React.ChangeEventHandler = (e) => { dispatch(changeComposeSpoilerText(composeId, e.target.value)); @@ -36,12 +36,14 @@ const SpoilerInput = React.forwardRef(({ dispatch(changeComposeSpoilerness(composeId)); }; + const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, ''); + return ( @@ -53,10 +55,10 @@ const SpoilerInput = React.forwardRef(({ (({ placeholder, }, ref) => { const dispatch = useAppDispatch(); - const isWysiwyg = useCompose(composeId).content_type === 'wysiwyg'; + const { content_type: contentType } = useCompose(composeId); + const isWysiwyg = contentType === 'wysiwyg'; const nodes = useNodes(isWysiwyg); const [suggestionsHidden, setSuggestionsHidden] = useState(true); @@ -106,20 +107,28 @@ const ComposeEditor = React.forwardRef(({ if (!compose) return; - if (compose.editorState) { - return compose.editorState; + const editorState = !compose.modified_language || compose.modified_language === compose.language + ? compose.editorState + : compose.editorStateMap.get(compose.modified_language, ''); + + if (editorState) { + return editorState; } return () => { + const text = !compose.modified_language || compose.modified_language === compose.language + ? compose.text + : compose.textMap.get(compose.modified_language, ''); + if (isWysiwyg) { $createRemarkImport({ handlers: { image: importImage, }, - })(compose.text); + })(text); } else { const paragraph = $createParagraphNode(); - const textNode = $createTextNode(compose.text); + const textNode = $createTextNode(text); paragraph.append(textNode); diff --git a/src/features/delete-account/index.tsx b/src/features/delete-account/index.tsx index 16240fce3b..eafd24abf4 100644 --- a/src/features/delete-account/index.tsx +++ b/src/features/delete-account/index.tsx @@ -75,7 +75,7 @@ const DeleteAccount = () => { - + ); }; diff --git a/src/features/status/components/detailed-status.tsx b/src/features/status/components/detailed-status.tsx index 298ecede9c..1da5d28f31 100644 --- a/src/features/status/components/detailed-status.tsx +++ b/src/features/status/components/detailed-status.tsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import Account from 'soapbox/components/account'; import StatusContent from 'soapbox/components/status-content'; +import StatusLanguagePicker from 'soapbox/components/status-language-picker'; import StatusMedia from 'soapbox/components/status-media'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay'; @@ -181,7 +182,10 @@ const DetailedStatus: React.FC = ({ )} + + +
diff --git a/src/normalizers/status.ts b/src/normalizers/status.ts index 78f5ea3d38..2d2cebc3c5 100644 --- a/src/normalizers/status.ts +++ b/src/normalizers/status.ts @@ -79,6 +79,7 @@ const StatusRecord = ImmutableRecord({ replies_count: 0, sensitive: false, spoiler_text: '', + spoiler_text_map: null as ImmutableMap | null, tags: ImmutableList>(), tombstone: null as Tombstone | null, uri: '', @@ -88,12 +89,15 @@ const StatusRecord = ImmutableRecord({ // Internal fields contentHtml: '', + spoilerHtml: '', + contentMapHtml: null as ImmutableMap | null, + spoilerMapHtml: null as ImmutableMap | null, expectsCard: false, hidden: null as boolean | null, search_index: '', showFiltered: true, - spoilerHtml: '', translation: null as ImmutableMap | null, + currentLanguage: null as string | null, }); const normalizeAttachments = (status: ImmutableMap) => diff --git a/src/reducers/compose.ts b/src/reducers/compose.ts index a47aef0fb3..3bf0a2921a 100644 --- a/src/reducers/compose.ts +++ b/src/reducers/compose.ts @@ -34,6 +34,10 @@ import { COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, COMPOSE_LANGUAGE_CHANGE, + COMPOSE_MODIFIED_LANGUAGE_CHANGE, + COMPOSE_LANGUAGE_ADD, + COMPOSE_LANGUAGE_DELETE, + COMPOSE_ADD_SUGGESTED_LANGUAGE, COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_SUCCESS, @@ -56,9 +60,6 @@ import { COMPOSE_CHANGE_MEDIA_ORDER, COMPOSE_ADD_SUGGESTED_QUOTE, ComposeAction, - COMPOSE_ADD_SUGGESTED_LANGUAGE, - COMPOSE_LANGUAGE_ADD, - COMPOSE_LANGUAGE_DELETE, } from '../actions/compose'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; @@ -81,6 +82,7 @@ const getResetFileKey = () => Math.floor((Math.random() * 0x10000)); const PollRecord = ImmutableRecord({ options: ImmutableList(['', '']), + options_map: ImmutableList>([ImmutableMap(), ImmutableMap()]), expires_in: 24 * 3600, multiple: false, }); @@ -90,6 +92,7 @@ const ReducerCompose = ImmutableRecord({ content_type: 'text/plain', draft_id: null as string | null, editorState: null as string | null, + editorStateMap: ImmutableMap(), focusDate: null as Date | null, group_id: null as string | null, idempotencyKey: '', @@ -109,6 +112,7 @@ const ReducerCompose = ImmutableRecord({ sensitive: false, spoiler: false, spoiler_text: '', + spoilerTextMap: ImmutableMap(), suggestions: ImmutableList(), suggestion_token: null as string | null, tagHistory: ImmutableList(), @@ -118,6 +122,7 @@ const ReducerCompose = ImmutableRecord({ parent_reblogged_by: null as string | null, dismissed_quotes: ImmutableOrderedSet(), language: null as Language | null, + modified_language: null as Language | null, suggested_language: null as string | null, }); @@ -302,9 +307,11 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me map.set('idempotencyKey', uuid()); })); case COMPOSE_SPOILER_TEXT_CHANGE: - return updateCompose(state, action.id, compose => compose - .set('spoiler_text', action.text) - .set('idempotencyKey', uuid())); + return updateCompose(state, action.id, compose => { + return compose + .setIn(compose.modified_language === compose.language ? ['spoiler_text'] : ['spoilerTextMap', compose.modified_language], action.text) + .set('idempotencyKey', uuid()); + }); case COMPOSE_VISIBILITY_CHANGE: return updateCompose(state, action.id, compose => compose .set('privacy', action.value) @@ -312,6 +319,12 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me case COMPOSE_LANGUAGE_CHANGE: return updateCompose(state, action.id, compose => compose.withMutations(map => { map.set('language', action.value); + map.set('modified_language', action.value); + map.set('idempotencyKey', uuid()); + })); + case COMPOSE_MODIFIED_LANGUAGE_CHANGE: + return updateCompose(state, action.id, compose => compose.withMutations(map => { + map.set('modified_language', action.value); map.set('idempotencyKey', uuid()); })); case COMPOSE_CHANGE: @@ -513,11 +526,21 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me case COMPOSE_SCHEDULE_REMOVE: return updateCompose(state, action.id, compose => compose.set('schedule', null)); case COMPOSE_POLL_OPTION_ADD: - return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList).push(action.title))); + return updateCompose(state, action.id, compose => + compose + .updateIn(['poll', 'options'], options => (options as ImmutableList).push(action.title)) + .updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList>).push(ImmutableMap(compose.textMap.map(_ => action.title)))), + ); case COMPOSE_POLL_OPTION_CHANGE: - return updateCompose(state, action.id, compose => compose.setIn(['poll', 'options', action.index], action.title)); + return updateCompose(state, action.id, compose => + compose.setIn(!compose.modified_language || compose.modified_language === compose.language ? ['poll', 'options', action.index] : ['poll', 'options_map', action.index, compose.modified_language], action.title), + ); case COMPOSE_POLL_OPTION_REMOVE: - return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index))); + return updateCompose(state, action.id, compose => + compose + .updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index)) + .updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList>).delete(action.index)), + ); case COMPOSE_POLL_SETTINGS_CHANGE: return updateCompose(state, action.id, compose => compose.update('poll', poll => { if (!poll) return null; @@ -541,8 +564,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); case COMPOSE_EDITOR_STATE_SET: return updateCompose(state, action.id, compose => compose - .set('editorState', action.editorState as string) - .set('text', action.text as string)); + .setIn(!compose.modified_language || compose.modified_language === compose.language ? ['editorState'] : ['editorStateMap', compose.modified_language], action.editorState as string) + .setIn(!compose.modified_language || compose.modified_language === compose.language ? ['text'] : ['textMap', compose.modified_language], action.text as string)); case EVENT_COMPOSE_CANCEL: return updateCompose(state, 'event-compose-modal', compose => compose.set('text', '')); case EVENT_FORM_SET: @@ -560,9 +583,21 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me case COMPOSE_ADD_SUGGESTED_LANGUAGE: return updateCompose(state, action.id, compose => compose.set('suggested_language', action.language)); case COMPOSE_LANGUAGE_ADD: - return updateCompose(state, action.id, compose => compose.setIn(['textMap', action.value], '')); + return updateCompose(state, action.id, compose => + compose + .setIn(['editorStateMap', action.value], compose.editorState) + .setIn(['textMap', action.value], compose.text) + .setIn(['spoilerTextMap', action.value], compose.spoiler_text) + .update('poll', poll => { + if (!poll) return poll; + return poll.update('options_map', optionsMap => optionsMap.map((option, key) => option.set(action.value, poll.options.get(key)!))); + }), + ); case COMPOSE_LANGUAGE_DELETE: - return updateCompose(state, action.id, compose => compose.removeIn(['textMap', action.value])); + return updateCompose(state, action.id, compose => compose + .removeIn(['editorStateMap', action.value]) + .removeIn(['textMap', action.value]) + .removeIn(['spoilerTextMap', action.value])); case COMPOSE_QUOTE_CANCEL: return updateCompose(state, action.id, compose => compose .update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes) diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 48d0bb384e..dfeb869f7d 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -43,6 +43,7 @@ import { STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO, STATUS_UNFILTER, + STATUS_LANGUAGE_CHANGE, } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -95,6 +96,9 @@ const buildSearchContent = (status: StatusRecord): string => { return unescapeHTML(fields.join('\n\n')) || ''; }; +const calculateContent = (text: string, emojiMap: any) => DOMPurify.sanitize(stripCompatibilityFeatures(emojify(text, emojiMap)), { USE_PROFILES: { html: true } }); +const calculateSpoiler = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } }); + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer const calculateStatus = ( @@ -102,21 +106,23 @@ const calculateStatus = ( oldStatus?: StatusRecord, ): StatusRecord => { if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) { + const { + search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, translation, currentLanguage, + } = oldStatus; + return status.merge({ - search_index: oldStatus.search_index, - contentHtml: oldStatus.contentHtml, - spoilerHtml: oldStatus.spoilerHtml, - hidden: oldStatus.hidden, + search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, translation, currentLanguage, }); } else { - const spoilerText = status.spoiler_text; const searchContent = buildSearchContent(status); - const emojiMap = makeEmojiMap(status.emojis); + const emojiMap = makeEmojiMap(status.emojis); return status.merge({ search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '', - contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(status.content, emojiMap)), { USE_PROFILES: { html: true } }), - spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap), { USE_PROFILES: { html: true } }), + contentHtml: calculateContent(status.content, emojiMap), + spoilerHtml: calculateSpoiler(status.spoiler_text, emojiMap), + contentMapHtml: status.content_map?.map(value => calculateContent(value, emojiMap)), + spoilerMapHtml: status.spoiler_text_map?.map(value => calculateSpoiler(value, emojiMap)), }); } }; @@ -318,6 +324,8 @@ const statuses = (state = initialState, action: AnyAction): State => { return deleteTranslation(state, action.id); case STATUS_UNFILTER: return state.setIn([action.id, 'showFiltered'], false); + case STATUS_LANGUAGE_CHANGE: + return state.setIn([action.id, 'currentLanguage'], action.language); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); case EVENT_JOIN_REQUEST: diff --git a/src/schemas/poll.ts b/src/schemas/poll.ts index 1a60af410a..b655a2850e 100644 --- a/src/schemas/poll.ts +++ b/src/schemas/poll.ts @@ -7,8 +7,11 @@ import emojify from 'soapbox/features/emoji'; import { customEmojiSchema } from './custom-emoji'; import { filteredArray, makeCustomEmojiMap } from './utils'; +const sanitizeTitle = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { ALLOWED_TAGS: [] }); + const pollOptionSchema = z.object({ title: z.string().catch(''), + title_map: z.record(z.string(), z.string()).nullable().catch(null), votes_count: z.number().catch(0), }); @@ -31,7 +34,10 @@ const pollSchema = z.object({ const emojifiedOptions = poll.options.map((option) => ({ ...option, - title_emojified: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(option.title), emojiMap), { ALLOWED_TAGS: [] }), + title_emojified: sanitizeTitle(option.title, emojiMap), + title_map_emojified: option.title_map + ? Object.fromEntries(Object.entries(option.title_map).map(([key, title]) => [key, sanitizeTitle(title, emojiMap)])) + : null, })); // If the user has votes, they have certainly voted. diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 8a603516ac..a64df87731 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -7,7 +7,7 @@ import { } from 'immutable'; import { createSelector } from 'reselect'; -import { getSettings } from 'soapbox/actions/settings'; +import { getLocale, getSettings } from 'soapbox/actions/settings'; import { Entities } from 'soapbox/entity-store/entities'; import { type MRFSimple } from 'soapbox/schemas/pleroma'; import { getDomain } from 'soapbox/utils/accounts'; @@ -136,9 +136,10 @@ const makeGetStatus = () => createSelector( getFilters, (state: RootState) => state.me, (state: RootState) => getFeatures(state.instance), + (state: RootState) => getLocale(state, 'en'), ], - (statusBase, statusReblog, username, filters, me, features) => { + (statusBase, statusReblog, username, filters, me, features, locale) => { if (!statusBase) return null; const { account } = statusBase; const accountUsername = account.acct; @@ -151,6 +152,14 @@ const makeGetStatus = () => createSelector( return statusBase.withMutations((map: Status) => { map.set('reblog', statusReblog || null); + if (map.currentLanguage === null && map.content_map?.size) { + let currentLanguage: string | null = null; + if (map.content_map.has(locale)) currentLanguage = locale; + else if (map.language && map.content_map.has(map.language)) currentLanguage = map.language; + else currentLanguage = map.content_map.keySeq().first(); + map.set('currentLanguage', currentLanguage); + } + if ((features.filters) && account.id !== me) { const filtered = checkFiltered(statusReblog?.search_index || statusBase.search_index, filters); diff --git a/src/utils/features.ts b/src/utils/features.ts index d1ca5d69cb..98578767db 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -626,7 +626,7 @@ const getInstanceFeatures = (instance: Instance) => { * Ability to include multiple language variants for a post. * @see POST /api/v1/statuses */ - multiLanguage: false, // features.includes('pleroma:multi_language'), + multiLanguage: features.includes('pleroma:multi_language'), /** * Ability to hide notifications from people you don't follow.