Multilanguage posting
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
a20e57e062
commit
ed9dc9eee3
25 changed files with 323 additions and 136 deletions
|
@ -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<typeof changeComposeSpoilerText>
|
||||
| ReturnType<typeof changeComposeVisibility>
|
||||
| ReturnType<typeof changeComposeLanguage>
|
||||
| ReturnType<typeof changeComposeModifiedLanguage>
|
||||
| ReturnType<typeof addComposeLanguage>
|
||||
| ReturnType<typeof deleteComposeLanguage>
|
||||
| ReturnType<typeof insertEmojiCompose>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -192,8 +192,6 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
const statuses = deduplicateStatuses(response.json);
|
||||
dispatch(importFetchedStatuses(statuses.filter(status => status.accounts)));
|
||||
|
||||
console.log(response);
|
||||
|
||||
dispatch(expandTimelineSuccess(
|
||||
timelineId,
|
||||
statuses,
|
||||
|
|
|
@ -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<IInstanceFavicon> = ({ 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<HTMLDivElement>(null);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -271,16 +270,6 @@ const Account = ({
|
|||
</>
|
||||
) : null}
|
||||
|
||||
{showMultiLanguage ? (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<button title={intl.formatMessage(messages.languageVersions)}>
|
||||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actionType === 'muting' && account.mute_expires_at ? (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
@ -288,6 +277,8 @@ const Account = ({
|
|||
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{items}
|
||||
</HStack>
|
||||
|
||||
{note ? (
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -100,10 +100,11 @@ interface IPollOption {
|
|||
showResults?: boolean;
|
||||
active: boolean;
|
||||
onToggle: (value: number) => void;
|
||||
language?: string | null;
|
||||
}
|
||||
|
||||
const PollOption: React.FC<IPollOption> = (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<IPollOption> = (props): JSX.Element | null => {
|
|||
<Text
|
||||
theme='inherit'
|
||||
weight='medium'
|
||||
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
|
||||
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
|
||||
className='relative'
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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<number, boolean>;
|
||||
|
||||
interface IPoll {
|
||||
id: string;
|
||||
status?: string;
|
||||
status?: Status;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -33,7 +35,7 @@ const Poll: React.FC<IPoll> = ({ 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<IPoll> = ({ id, status }): JSX.Element | null => {
|
|||
showResults={showResults}
|
||||
active={!!selected[i]}
|
||||
onToggle={toggleOption}
|
||||
language={status?.currentLanguage}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
@ -77,8 +77,12 @@ const StatusContent: React.FC<IStatusContent> = 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<IStatusContent> = React.memo(({
|
|||
|
||||
const hasPoll = status.poll && typeof status.poll === 'string';
|
||||
if (hasPoll) {
|
||||
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||
output.push(<Poll id={status.poll} key='poll' status={status} />);
|
||||
}
|
||||
|
||||
return <div className={clsx({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
|
@ -182,7 +186,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
];
|
||||
|
||||
if (status.poll && typeof status.poll === 'string') {
|
||||
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||
output.push(<Poll id={status.poll} key='poll' status={status} />);
|
||||
}
|
||||
|
||||
return <>{output}</>;
|
||||
|
|
58
src/components/status-language-picker.tsx
Normal file
58
src/components/status-language-picker.tsx
Normal file
|
@ -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<IStatusLanguagePicker> = ({ status, showLabel }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!status.contentMapHtml || status.contentMapHtml.isEmpty()) return null;
|
||||
|
||||
const icon = <Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<DropdownMenu
|
||||
items={status.contentMapHtml.keySeq().toJS().map((language) => ({
|
||||
text: languages[language as Language] || language,
|
||||
action: () => dispatch(changeStatusLanguage(status.id, language)),
|
||||
active: language === status.currentLanguage,
|
||||
}))}
|
||||
>
|
||||
<button title={intl.formatMessage(messages.languageVersions)} className='hover:underline'>
|
||||
{showLabel ? (
|
||||
<HStack space={1} alignItems='center'>
|
||||
{icon}
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
{languages[status.currentLanguage as Language] || status.currentLanguage}
|
||||
</Text>
|
||||
</HStack>
|
||||
) : icon}
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
StatusLanguagePicker as default,
|
||||
};
|
|
@ -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<IStatus> = (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={<StatusLanguagePicker status={status} />}
|
||||
/>
|
||||
|
||||
<div className='status__content-wrapper'>
|
||||
|
|
|
@ -47,6 +47,10 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
|
||||
if (visible && !showHideButton) return null;
|
||||
|
||||
const spoilerText = status.currentLanguage
|
||||
? status.spoilerMapHtml!.get(status.currentLanguage, status.spoilerHtml)
|
||||
: status.spoilerHtml;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('absolute z-40', {
|
||||
|
@ -66,7 +70,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
/>
|
||||
) : (
|
||||
<div className='flex max-h-screen items-center justify-center'>
|
||||
<div className='mx-auto w-3/4 space-y-4 text-center' ref={ref}>
|
||||
<div className='mx-auto space-y-4 text-center' ref={ref}>
|
||||
<div className='space-y-1'>
|
||||
<Text theme='white' weight='semibold'>
|
||||
{intl.formatMessage(messages.sensitiveTitle)}
|
||||
|
@ -79,7 +83,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
{status.spoiler_text && (
|
||||
<div className='py-4 italic'>
|
||||
<Text className='line-clamp-6' theme='white' size='md' weight='medium'>
|
||||
“<span dangerouslySetInnerHTML={{ __html: status.spoilerHtml }} />”
|
||||
“<span dangerouslySetInnerHTML={{ __html: spoilerText }} />”
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -27,7 +27,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ 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));
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ const ComposeForm = <ID extends string>({ 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 extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
<div>
|
||||
<Suspense>
|
||||
<ComposeEditor
|
||||
key={modifiedLanguage}
|
||||
ref={editorRef}
|
||||
className='mt-2'
|
||||
composeId={id}
|
||||
|
|
|
@ -7,7 +7,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { addComposeLanguage, changeComposeLanguage, deleteComposeLanguage } from 'soapbox/actions/compose';
|
||||
import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'soapbox/actions/compose';
|
||||
import { Button, Icon, Input, Portal } from 'soapbox/components/ui';
|
||||
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
|
||||
|
@ -66,7 +66,12 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ 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<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
|
@ -87,8 +92,6 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ 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<ILanguageDropdown> = ({ composeId }) => {
|
|||
const handleOptionClick: React.EventHandler<any> = (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<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||
|
@ -288,7 +298,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ 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<ILanguageDropdown> = ({ composeId }) => {
|
|||
<div className='h-96 w-full overflow-scroll' ref={node} tabIndex={-1}>
|
||||
{results.map(([code, name]) => {
|
||||
const active = code === language;
|
||||
const modified = code === modifiedLanguage;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -357,15 +368,20 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ 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}
|
||||
>
|
||||
<div
|
||||
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
|
||||
'text-black dark:text-white': active,
|
||||
'text-black dark:text-white': modified,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
|
|
|
@ -48,7 +48,7 @@ const Option: React.FC<IOption> = ({
|
|||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const suggestions = useCompose(composeId).suggestions;
|
||||
const { suggestions } = useCompose(composeId);
|
||||
|
||||
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
|
||||
|
||||
|
@ -112,11 +112,11 @@ const PollForm: React.FC<IPollForm> = ({ 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,
|
||||
|
|
|
@ -26,7 +26,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
|||
}, 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<HTMLInputElement> = (e) => {
|
||||
dispatch(changeComposeSpoilerText(composeId, e.target.value));
|
||||
|
@ -36,12 +36,14 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
|||
dispatch(changeComposeSpoilerness(composeId));
|
||||
};
|
||||
|
||||
const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, '');
|
||||
|
||||
return (
|
||||
<Stack
|
||||
space={4}
|
||||
className={clsx({
|
||||
'relative transition-height': true,
|
||||
'hidden': !compose.spoiler,
|
||||
'hidden': !spoiler,
|
||||
})}
|
||||
>
|
||||
<Divider />
|
||||
|
@ -53,10 +55,10 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
|||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={compose.spoiler_text}
|
||||
value={value}
|
||||
onChange={handleChangeSpoilerText}
|
||||
disabled={!compose.spoiler}
|
||||
suggestions={compose.suggestions}
|
||||
disabled={!spoiler}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
|
|
|
@ -90,7 +90,8 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
|||
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<LexicalEditor, IComposeEditor>(({
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ const DeleteAccount = () => {
|
|||
</Form>
|
||||
</Stack>
|
||||
</CardBody>
|
||||
</Card >
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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<IDetailedStatus> = ({
|
|||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<StatusLanguagePicker status={status} showLabel />
|
||||
</HStack>
|
||||
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -79,6 +79,7 @@ const StatusRecord = ImmutableRecord({
|
|||
replies_count: 0,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
spoiler_text_map: null as ImmutableMap<string, string> | null,
|
||||
tags: ImmutableList<ImmutableMap<string, any>>(),
|
||||
tombstone: null as Tombstone | null,
|
||||
uri: '',
|
||||
|
@ -88,12 +89,15 @@ const StatusRecord = ImmutableRecord({
|
|||
|
||||
// Internal fields
|
||||
contentHtml: '',
|
||||
spoilerHtml: '',
|
||||
contentMapHtml: null as ImmutableMap<string, string> | null,
|
||||
spoilerMapHtml: null as ImmutableMap<string, string> | null,
|
||||
expectsCard: false,
|
||||
hidden: null as boolean | null,
|
||||
search_index: '',
|
||||
showFiltered: true,
|
||||
spoilerHtml: '',
|
||||
translation: null as ImmutableMap<string, string> | null,
|
||||
currentLanguage: null as string | null,
|
||||
});
|
||||
|
||||
const normalizeAttachments = (status: ImmutableMap<string, any>) =>
|
||||
|
|
|
@ -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<Language, string>>([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<Language, string | null>(),
|
||||
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<Language, string>(),
|
||||
suggestions: ImmutableList<string>(),
|
||||
suggestion_token: null as string | null,
|
||||
tagHistory: ImmutableList<string>(),
|
||||
|
@ -118,6 +122,7 @@ const ReducerCompose = ImmutableRecord({
|
|||
parent_reblogged_by: null as string | null,
|
||||
dismissed_quotes: ImmutableOrderedSet<string>(),
|
||||
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<string>).push(action.title)));
|
||||
return updateCompose(state, action.id, compose =>
|
||||
compose
|
||||
.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).push(action.title))
|
||||
.updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList<ImmutableMap<Language, string>>).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<string>).delete(action.index)));
|
||||
return updateCompose(state, action.id, compose =>
|
||||
compose
|
||||
.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).delete(action.index))
|
||||
.updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList<ImmutableMap<Language, string>>).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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue