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();
|
let cancelFetchComposeSuggestions = new AbortController();
|
||||||
|
|
||||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
|
const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
|
||||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
|
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
|
||||||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
|
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
|
||||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const;
|
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const;
|
||||||
const COMPOSE_REPLY = 'COMPOSE_REPLY' as const;
|
const COMPOSE_REPLY = 'COMPOSE_REPLY' as const;
|
||||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const;
|
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const;
|
||||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const;
|
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const;
|
||||||
const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const;
|
const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const;
|
||||||
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const;
|
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const;
|
||||||
const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const;
|
const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const;
|
||||||
const COMPOSE_MENTION = 'COMPOSE_MENTION' as const;
|
const COMPOSE_MENTION = 'COMPOSE_MENTION' as const;
|
||||||
const COMPOSE_RESET = 'COMPOSE_RESET' as const;
|
const COMPOSE_RESET = 'COMPOSE_RESET' as const;
|
||||||
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const;
|
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const;
|
||||||
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const;
|
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const;
|
||||||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const;
|
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const;
|
||||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const;
|
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const;
|
||||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const;
|
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const;
|
||||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const;
|
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const;
|
||||||
|
|
||||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const;
|
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const;
|
||||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' 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_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const;
|
||||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_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_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const;
|
||||||
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const;
|
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const;
|
||||||
const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const;
|
const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const;
|
||||||
const COMPOSE_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const;
|
const COMPOSE_MODIFIED_LANGUAGE_CHANGE = 'COMPOSE_MODIFIED_LANGUAGE_CHANGE' as const;
|
||||||
const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' 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_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
|
||||||
|
|
||||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' 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_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const;
|
||||||
const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' 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_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const;
|
||||||
|
|
||||||
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const;
|
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const;
|
||||||
const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' 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_ADD = 'COMPOSE_POLL_OPTION_ADD' as const;
|
||||||
const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' 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_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const;
|
||||||
const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' 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_ADD = 'COMPOSE_SCHEDULE_ADD' as const;
|
||||||
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const;
|
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const;
|
||||||
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const;
|
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const;
|
||||||
|
|
||||||
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' 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_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 COMPOSE_ADD_SUGGESTED_LANGUAGE = 'COMPOSE_ADD_SUGGESTED_LANGUAGE' as const;
|
||||||
|
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
@ -301,7 +302,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c
|
||||||
};
|
};
|
||||||
|
|
||||||
const needsDescriptions = (state: RootState, composeId: string) => {
|
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 missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
|
||||||
|
|
||||||
const hasMissing = media.filter(item => !item.description).size > 0;
|
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 compose = state.compose.get(composeId)!;
|
||||||
|
|
||||||
const status = compose.text;
|
const status = compose.text;
|
||||||
const media = compose.media_attachments;
|
const media = compose.media_attachments;
|
||||||
const statusId = compose.id;
|
const statusId = compose.id;
|
||||||
let to = compose.to;
|
let to = compose.to;
|
||||||
|
|
||||||
if (!validateSchedule(state, composeId)) {
|
if (!validateSchedule(state, composeId)) {
|
||||||
toast.error(messages.scheduleError);
|
toast.error(messages.scheduleError);
|
||||||
|
@ -387,6 +388,22 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||||
to,
|
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') {
|
if (compose.privacy === 'group') {
|
||||||
params.group_id = compose.group_id;
|
params.group_id = compose.group_id;
|
||||||
}
|
}
|
||||||
|
@ -425,7 +442,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments;
|
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);
|
const progress = new Array(files.length).fill(0);
|
||||||
let total = Array.from(files).reduce((a, v) => a + v.size, 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));
|
dispatch(uploadComposeRequest(composeId));
|
||||||
|
|
||||||
Array.from(files).forEach(async(f, i) => {
|
Array.from(files).forEach(async (f, i) => {
|
||||||
if (mediaCount + i > attachmentLimit - 1) return;
|
if (mediaCount + i > attachmentLimit - 1) return;
|
||||||
|
|
||||||
dispatch(uploadFile(
|
dispatch(uploadFile(
|
||||||
|
@ -659,15 +676,15 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||||
let completion = '', startPosition = position;
|
let completion = '', startPosition = position;
|
||||||
|
|
||||||
if (typeof suggestion === 'object' && suggestion.id) {
|
if (typeof suggestion === 'object' && suggestion.id) {
|
||||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
|
|
||||||
dispatch(chooseEmoji(suggestion));
|
dispatch(chooseEmoji(suggestion));
|
||||||
} else if (typeof suggestion === 'string' && suggestion[0] === '#') {
|
} else if (typeof suggestion === 'string' && suggestion[0] === '#') {
|
||||||
completion = suggestion;
|
completion = suggestion;
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
} else if (typeof suggestion === 'string') {
|
} else if (typeof suggestion === 'string') {
|
||||||
completion = selectAccount(getState(), suggestion)!.acct;
|
completion = selectAccount(getState(), suggestion)!.acct;
|
||||||
startPosition = position;
|
startPosition = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,13 +760,19 @@ const changeComposeLanguage = (composeId: string, value: Language | null) => ({
|
||||||
value,
|
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,
|
type: COMPOSE_LANGUAGE_ADD,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteComposeLanguage = (composeId: string, value: Language | null) => ({
|
const deleteComposeLanguage = (composeId: string, value: Language) => ({
|
||||||
type: COMPOSE_LANGUAGE_DELETE,
|
type: COMPOSE_LANGUAGE_DELETE,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
value,
|
value,
|
||||||
|
@ -945,6 +968,7 @@ type ComposeAction =
|
||||||
| ReturnType<typeof changeComposeSpoilerText>
|
| ReturnType<typeof changeComposeSpoilerText>
|
||||||
| ReturnType<typeof changeComposeVisibility>
|
| ReturnType<typeof changeComposeVisibility>
|
||||||
| ReturnType<typeof changeComposeLanguage>
|
| ReturnType<typeof changeComposeLanguage>
|
||||||
|
| ReturnType<typeof changeComposeModifiedLanguage>
|
||||||
| ReturnType<typeof addComposeLanguage>
|
| ReturnType<typeof addComposeLanguage>
|
||||||
| ReturnType<typeof deleteComposeLanguage>
|
| ReturnType<typeof deleteComposeLanguage>
|
||||||
| ReturnType<typeof insertEmojiCompose>
|
| ReturnType<typeof insertEmojiCompose>
|
||||||
|
@ -994,6 +1018,7 @@ export {
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
COMPOSE_VISIBILITY_CHANGE,
|
||||||
COMPOSE_LANGUAGE_CHANGE,
|
COMPOSE_LANGUAGE_CHANGE,
|
||||||
|
COMPOSE_MODIFIED_LANGUAGE_CHANGE,
|
||||||
COMPOSE_LANGUAGE_ADD,
|
COMPOSE_LANGUAGE_ADD,
|
||||||
COMPOSE_LANGUAGE_DELETE,
|
COMPOSE_LANGUAGE_DELETE,
|
||||||
COMPOSE_LISTABILITY_CHANGE,
|
COMPOSE_LISTABILITY_CHANGE,
|
||||||
|
@ -1057,6 +1082,7 @@ export {
|
||||||
changeComposeSpoilerText,
|
changeComposeSpoilerText,
|
||||||
changeComposeVisibility,
|
changeComposeVisibility,
|
||||||
changeComposeLanguage,
|
changeComposeLanguage,
|
||||||
|
changeComposeModifiedLanguage,
|
||||||
addComposeLanguage,
|
addComposeLanguage,
|
||||||
deleteComposeLanguage,
|
deleteComposeLanguage,
|
||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
|
|
|
@ -13,42 +13,44 @@ import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
||||||
const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
|
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_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
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_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
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_REQUEST = 'STATUS_DELETE_REQUEST';
|
||||||
const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
|
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_REQUEST = 'CONTEXT_FETCH_REQUEST';
|
||||||
const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
|
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_REQUEST = 'STATUS_MUTE_REQUEST';
|
||||||
const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
|
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_REQUEST = 'STATUS_UNMUTE_REQUEST';
|
||||||
const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
|
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_REVEAL = 'STATUS_REVEAL';
|
||||||
const STATUS_HIDE = 'STATUS_HIDE';
|
const STATUS_HIDE = 'STATUS_HIDE';
|
||||||
|
|
||||||
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
|
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
|
||||||
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||||
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
const STATUS_UNFILTER = 'STATUS_UNFILTER';
|
const STATUS_UNFILTER = 'STATUS_UNFILTER';
|
||||||
|
|
||||||
|
const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE';
|
||||||
|
|
||||||
const statusExists = (getState: () => RootState, statusId: string) =>
|
const statusExists = (getState: () => RootState, statusId: string) =>
|
||||||
(getState().statuses.get(statusId) || null) !== null;
|
(getState().statuses.get(statusId) || null) !== null;
|
||||||
|
|
||||||
|
@ -185,7 +187,7 @@ const fetchContext = (id: string) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchStatusWithContext = (id: string) =>
|
const fetchStatusWithContext = (id: string) =>
|
||||||
async(dispatch: AppDispatch, getState: () => RootState) => {
|
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dispatch(fetchContext(id)),
|
dispatch(fetchContext(id)),
|
||||||
dispatch(fetchStatus(id)),
|
dispatch(fetchStatus(id)),
|
||||||
|
@ -286,6 +288,12 @@ const unfilterStatus = (id: string) => ({
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changeStatusLanguage = (id: string, language: string) => ({
|
||||||
|
type: STATUS_LANGUAGE_CHANGE,
|
||||||
|
id,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STATUS_CREATE_REQUEST,
|
STATUS_CREATE_REQUEST,
|
||||||
STATUS_CREATE_SUCCESS,
|
STATUS_CREATE_SUCCESS,
|
||||||
|
@ -315,6 +323,7 @@ export {
|
||||||
STATUS_TRANSLATE_FAIL,
|
STATUS_TRANSLATE_FAIL,
|
||||||
STATUS_TRANSLATE_UNDO,
|
STATUS_TRANSLATE_UNDO,
|
||||||
STATUS_UNFILTER,
|
STATUS_UNFILTER,
|
||||||
|
STATUS_LANGUAGE_CHANGE,
|
||||||
createStatus,
|
createStatus,
|
||||||
editStatus,
|
editStatus,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -331,4 +340,5 @@ export {
|
||||||
translateStatus,
|
translateStatus,
|
||||||
undoStatusTranslation,
|
undoStatusTranslation,
|
||||||
unfilterStatus,
|
unfilterStatus,
|
||||||
|
changeStatusLanguage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -192,8 +192,6 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
const statuses = deduplicateStatuses(response.json);
|
const statuses = deduplicateStatuses(response.json);
|
||||||
dispatch(importFetchedStatuses(statuses.filter(status => status.accounts)));
|
dispatch(importFetchedStatuses(statuses.filter(status => status.accounts)));
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
dispatch(expandTimelineSuccess(
|
dispatch(expandTimelineSuccess(
|
||||||
timelineId,
|
timelineId,
|
||||||
statuses,
|
statuses,
|
||||||
|
|
|
@ -23,7 +23,6 @@ interface IInstanceFavicon {
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
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 }) => {
|
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||||
|
@ -91,11 +90,11 @@ interface IAccount {
|
||||||
withLinkToProfile?: boolean;
|
withLinkToProfile?: boolean;
|
||||||
withRelationship?: boolean;
|
withRelationship?: boolean;
|
||||||
showEdit?: boolean;
|
showEdit?: boolean;
|
||||||
showMultiLanguage?: boolean;
|
|
||||||
approvalStatus?: StatusApprovalStatus;
|
approvalStatus?: StatusApprovalStatus;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
emojiUrl?: string;
|
emojiUrl?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
items?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Account = ({
|
const Account = ({
|
||||||
|
@ -118,11 +117,11 @@ const Account = ({
|
||||||
withLinkToProfile = true,
|
withLinkToProfile = true,
|
||||||
withRelationship = true,
|
withRelationship = true,
|
||||||
showEdit = false,
|
showEdit = false,
|
||||||
showMultiLanguage = false,
|
|
||||||
approvalStatus,
|
approvalStatus,
|
||||||
emoji,
|
emoji,
|
||||||
emojiUrl,
|
emojiUrl,
|
||||||
note,
|
note,
|
||||||
|
items,
|
||||||
}: IAccount) => {
|
}: IAccount) => {
|
||||||
const overflowRef = useRef<HTMLDivElement>(null);
|
const overflowRef = useRef<HTMLDivElement>(null);
|
||||||
const actionRef = useRef<HTMLDivElement>(null);
|
const actionRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -271,16 +270,6 @@ const Account = ({
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : 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 ? (
|
{actionType === 'muting' && account.mute_expires_at ? (
|
||||||
<>
|
<>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<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>
|
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{items}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{note ? (
|
{note ? (
|
||||||
|
|
|
@ -242,10 +242,6 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
if (refs.floating.current) {
|
|
||||||
(refs.floating.current?.querySelector('li a[role=\'button\']') as HTMLAnchorElement)?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', handleDocumentClick, false);
|
document.addEventListener('click', handleDocumentClick, false);
|
||||||
document.addEventListener('keydown', handleKeyDown, false);
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||||
|
|
|
@ -100,10 +100,11 @@ interface IPollOption {
|
||||||
showResults?: boolean;
|
showResults?: boolean;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onToggle: (value: number) => void;
|
onToggle: (value: number) => void;
|
||||||
|
language?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PollOption: React.FC<IPollOption> = (props): JSX.Element | 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();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
<Text
|
<Text
|
||||||
theme='inherit'
|
theme='inherit'
|
||||||
weight='medium'
|
weight='medium'
|
||||||
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
|
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
|
||||||
className='relative'
|
className='relative'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,11 +10,13 @@ import { Stack, Text } from '../ui';
|
||||||
import PollFooter from './poll-footer';
|
import PollFooter from './poll-footer';
|
||||||
import PollOption from './poll-option';
|
import PollOption from './poll-option';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
type Selected = Record<number, boolean>;
|
type Selected = Record<number, boolean>;
|
||||||
|
|
||||||
interface IPoll {
|
interface IPoll {
|
||||||
id: string;
|
id: string;
|
||||||
status?: string;
|
status?: Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -33,7 +35,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||||
const openUnauthorizedModal = () =>
|
const openUnauthorizedModal = () =>
|
||||||
dispatch(openModal('UNAUTHORIZED', {
|
dispatch(openModal('UNAUTHORIZED', {
|
||||||
action: 'POLL_VOTE',
|
action: 'POLL_VOTE',
|
||||||
ap_id: status,
|
ap_id: status?.url,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleVote = (selectedId: number) => dispatch(vote(id, [String(selectedId)]));
|
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}
|
showResults={showResults}
|
||||||
active={!!selected[i]}
|
active={!!selected[i]}
|
||||||
onToggle={toggleOption}
|
onToggle={toggleOption}
|
||||||
|
language={status?.currentLanguage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -77,8 +77,12 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedHtml = useMemo(
|
const parsedHtml = useMemo(
|
||||||
(): string => translatable && status.translation ? status.translation.get('content')! : status.contentHtml,
|
(): string => translatable && status.translation
|
||||||
[status.contentHtml, 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) {
|
if (status.content.length === 0) {
|
||||||
|
@ -160,7 +164,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||||
|
|
||||||
const hasPoll = status.poll && typeof status.poll === 'string';
|
const hasPoll = status.poll && typeof status.poll === 'string';
|
||||||
if (hasPoll) {
|
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>;
|
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') {
|
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}</>;
|
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 EventPreview from './event-preview';
|
||||||
import StatusActionBar from './status-action-bar';
|
import StatusActionBar from './status-action-bar';
|
||||||
import StatusContent from './status-content';
|
import StatusContent from './status-content';
|
||||||
|
import StatusLanguagePicker from './status-language-picker';
|
||||||
import StatusMedia from './status-media';
|
import StatusMedia from './status-media';
|
||||||
import StatusReplyMentions from './status-reply-mentions';
|
import StatusReplyMentions from './status-reply-mentions';
|
||||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||||
|
@ -428,11 +429,11 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
action={accountAction}
|
action={accountAction}
|
||||||
hideActions={!accountAction}
|
hideActions={!accountAction}
|
||||||
showEdit={!!actualStatus.edited_at}
|
showEdit={!!actualStatus.edited_at}
|
||||||
showMultiLanguage={!!actualStatus.content_map && actualStatus.content_map?.count() > 1}
|
|
||||||
showProfileHoverCard={hoverable}
|
showProfileHoverCard={hoverable}
|
||||||
withLinkToProfile={hoverable}
|
withLinkToProfile={hoverable}
|
||||||
approvalStatus={actualStatus.approval_status}
|
approvalStatus={actualStatus.approval_status}
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
|
items={<StatusLanguagePicker status={status} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='status__content-wrapper'>
|
<div className='status__content-wrapper'>
|
||||||
|
|
|
@ -47,6 +47,10 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
||||||
|
|
||||||
if (visible && !showHideButton) return null;
|
if (visible && !showHideButton) return null;
|
||||||
|
|
||||||
|
const spoilerText = status.currentLanguage
|
||||||
|
? status.spoilerMapHtml!.get(status.currentLanguage, status.spoilerHtml)
|
||||||
|
: status.spoilerHtml;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('absolute z-40', {
|
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='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'>
|
<div className='space-y-1'>
|
||||||
<Text theme='white' weight='semibold'>
|
<Text theme='white' weight='semibold'>
|
||||||
{intl.formatMessage(messages.sensitiveTitle)}
|
{intl.formatMessage(messages.sensitiveTitle)}
|
||||||
|
@ -79,7 +83,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
||||||
{status.spoiler_text && (
|
{status.spoiler_text && (
|
||||||
<div className='py-4 italic'>
|
<div className='py-4 italic'>
|
||||||
<Text className='line-clamp-6' theme='white' size='md' weight='medium'>
|
<Text className='line-clamp-6' theme='white' size='md' weight='medium'>
|
||||||
“<span dangerouslySetInnerHTML={{ __html: status.spoilerHtml }} />”
|
“<span dangerouslySetInnerHTML={{ __html: spoilerText }} />”
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -27,7 +27,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||||
target_languages: targetLanguages,
|
target_languages: targetLanguages,
|
||||||
} = instance.pleroma.metadata.translation;
|
} = 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));
|
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,
|
schedule: scheduledAt,
|
||||||
group_id: groupId,
|
group_id: groupId,
|
||||||
text,
|
text,
|
||||||
|
modified_language: modifiedLanguage,
|
||||||
} = compose;
|
} = compose;
|
||||||
|
|
||||||
const prevSpoiler = usePrevious(spoiler);
|
const prevSpoiler = usePrevious(spoiler);
|
||||||
|
@ -282,6 +283,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
<div>
|
<div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<ComposeEditor
|
<ComposeEditor
|
||||||
|
key={modifiedLanguage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className='mt-2'
|
className='mt-2'
|
||||||
composeId={id}
|
composeId={id}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { createSelector } from 'reselect';
|
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 { Button, Icon, Input, Portal } from 'soapbox/components/ui';
|
||||||
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
|
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
|
||||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
|
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<
|
const handleClick: React.EventHandler<
|
||||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
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 handleOptionKeyDown: React.KeyboardEventHandler = e => {
|
||||||
const value = e.currentTarget.getAttribute('data-index');
|
const value = e.currentTarget.getAttribute('data-index');
|
||||||
const index = results.findIndex(([key]) => key === value);
|
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 handleOptionClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||||
const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language;
|
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();
|
e.preventDefault();
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
handleChange(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddLanguageClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
const handleAddLanguageClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||||
|
@ -288,7 +298,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||||
|
|
||||||
let buttonLabel = intl.formatMessage(messages.languagePrompt);
|
let buttonLabel = intl.formatMessage(messages.languagePrompt);
|
||||||
if (language) {
|
if (language) {
|
||||||
const list: string[] = [languagesObject[language]];
|
const list: string[] = [languagesObject[modifiedLanguage || language]];
|
||||||
if (textMap.size) list.push(intl.formatMessage(messages.multipleLanguages, {
|
if (textMap.size) list.push(intl.formatMessage(messages.multipleLanguages, {
|
||||||
count: textMap.size,
|
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}>
|
<div className='h-96 w-full overflow-scroll' ref={node} tabIndex={-1}>
|
||||||
{results.map(([code, name]) => {
|
{results.map(([code, name]) => {
|
||||||
const active = code === language;
|
const active = code === language;
|
||||||
|
const modified = code === modifiedLanguage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -357,15 +368,20 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||||
onKeyDown={handleOptionKeyDown}
|
onKeyDown={handleOptionKeyDown}
|
||||||
onClick={handleOptionClick}
|
onClick={handleOptionClick}
|
||||||
className={clsx(
|
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',
|
'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 hover:bg-gray-200 dark:hover:bg-gray-700': active },
|
{
|
||||||
|
'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}
|
aria-selected={active}
|
||||||
ref={active ? focusedItem : null}
|
ref={active ? focusedItem : null}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
|
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}
|
{name}
|
||||||
|
|
|
@ -48,7 +48,7 @@ const Option: React.FC<IOption> = ({
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const suggestions = useCompose(composeId).suggestions;
|
const { suggestions } = useCompose(composeId);
|
||||||
|
|
||||||
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
|
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 intl = useIntl();
|
||||||
const { configuration } = useInstance();
|
const { configuration } = useInstance();
|
||||||
|
|
||||||
const compose = useCompose(composeId);
|
const { poll, language, modified_language: modifiedLanguage } = useCompose(composeId);
|
||||||
|
|
||||||
const options = compose.poll?.options;
|
const options = !modifiedLanguage || modifiedLanguage === language ? poll?.options : poll?.options_map.map((option, key) => option.get(modifiedLanguage, poll.options.get(key)!));
|
||||||
const expiresIn = compose.poll?.expires_in;
|
const expiresIn = poll?.expires_in;
|
||||||
const isMultiple = compose.poll?.multiple;
|
const isMultiple = poll?.multiple;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
max_options: maxOptions,
|
max_options: maxOptions,
|
||||||
|
|
|
@ -26,7 +26,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
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) => {
|
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
dispatch(changeComposeSpoilerText(composeId, e.target.value));
|
dispatch(changeComposeSpoilerText(composeId, e.target.value));
|
||||||
|
@ -36,12 +36,14 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
||||||
dispatch(changeComposeSpoilerness(composeId));
|
dispatch(changeComposeSpoilerness(composeId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
space={4}
|
space={4}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
'relative transition-height': true,
|
'relative transition-height': true,
|
||||||
'hidden': !compose.spoiler,
|
'hidden': !spoiler,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -53,10 +55,10 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
|
||||||
|
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={compose.spoiler_text}
|
value={value}
|
||||||
onChange={handleChangeSpoilerText}
|
onChange={handleChangeSpoilerText}
|
||||||
disabled={!compose.spoiler}
|
disabled={!spoiler}
|
||||||
suggestions={compose.suggestions}
|
suggestions={suggestions}
|
||||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
onSuggestionSelected={onSuggestionSelected}
|
onSuggestionSelected={onSuggestionSelected}
|
||||||
|
|
|
@ -90,7 +90,8 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
||||||
placeholder,
|
placeholder,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const dispatch = useAppDispatch();
|
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 nodes = useNodes(isWysiwyg);
|
||||||
|
|
||||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
|
@ -106,20 +107,28 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
||||||
|
|
||||||
if (!compose) return;
|
if (!compose) return;
|
||||||
|
|
||||||
if (compose.editorState) {
|
const editorState = !compose.modified_language || compose.modified_language === compose.language
|
||||||
return compose.editorState;
|
? compose.editorState
|
||||||
|
: compose.editorStateMap.get(compose.modified_language, '');
|
||||||
|
|
||||||
|
if (editorState) {
|
||||||
|
return editorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
const text = !compose.modified_language || compose.modified_language === compose.language
|
||||||
|
? compose.text
|
||||||
|
: compose.textMap.get(compose.modified_language, '');
|
||||||
|
|
||||||
if (isWysiwyg) {
|
if (isWysiwyg) {
|
||||||
$createRemarkImport({
|
$createRemarkImport({
|
||||||
handlers: {
|
handlers: {
|
||||||
image: importImage,
|
image: importImage,
|
||||||
},
|
},
|
||||||
})(compose.text);
|
})(text);
|
||||||
} else {
|
} else {
|
||||||
const paragraph = $createParagraphNode();
|
const paragraph = $createParagraphNode();
|
||||||
const textNode = $createTextNode(compose.text);
|
const textNode = $createTextNode(text);
|
||||||
|
|
||||||
paragraph.append(textNode);
|
paragraph.append(textNode);
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ const DeleteAccount = () => {
|
||||||
</Form>
|
</Form>
|
||||||
</Stack>
|
</Stack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card >
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Account from 'soapbox/components/account';
|
import Account from 'soapbox/components/account';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
|
import StatusLanguagePicker from 'soapbox/components/status-language-picker';
|
||||||
import StatusMedia from 'soapbox/components/status-media';
|
import StatusMedia from 'soapbox/components/status-media';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
||||||
|
@ -181,7 +182,10 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<StatusLanguagePicker status={status} showLabel />
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -79,6 +79,7 @@ const StatusRecord = ImmutableRecord({
|
||||||
replies_count: 0,
|
replies_count: 0,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
|
spoiler_text_map: null as ImmutableMap<string, string> | null,
|
||||||
tags: ImmutableList<ImmutableMap<string, any>>(),
|
tags: ImmutableList<ImmutableMap<string, any>>(),
|
||||||
tombstone: null as Tombstone | null,
|
tombstone: null as Tombstone | null,
|
||||||
uri: '',
|
uri: '',
|
||||||
|
@ -88,12 +89,15 @@ const StatusRecord = ImmutableRecord({
|
||||||
|
|
||||||
// Internal fields
|
// Internal fields
|
||||||
contentHtml: '',
|
contentHtml: '',
|
||||||
|
spoilerHtml: '',
|
||||||
|
contentMapHtml: null as ImmutableMap<string, string> | null,
|
||||||
|
spoilerMapHtml: null as ImmutableMap<string, string> | null,
|
||||||
expectsCard: false,
|
expectsCard: false,
|
||||||
hidden: null as boolean | null,
|
hidden: null as boolean | null,
|
||||||
search_index: '',
|
search_index: '',
|
||||||
showFiltered: true,
|
showFiltered: true,
|
||||||
spoilerHtml: '',
|
|
||||||
translation: null as ImmutableMap<string, string> | null,
|
translation: null as ImmutableMap<string, string> | null,
|
||||||
|
currentLanguage: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeAttachments = (status: ImmutableMap<string, any>) =>
|
const normalizeAttachments = (status: ImmutableMap<string, any>) =>
|
||||||
|
|
|
@ -34,6 +34,10 @@ import {
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
COMPOSE_VISIBILITY_CHANGE,
|
||||||
COMPOSE_LANGUAGE_CHANGE,
|
COMPOSE_LANGUAGE_CHANGE,
|
||||||
|
COMPOSE_MODIFIED_LANGUAGE_CHANGE,
|
||||||
|
COMPOSE_LANGUAGE_ADD,
|
||||||
|
COMPOSE_LANGUAGE_DELETE,
|
||||||
|
COMPOSE_ADD_SUGGESTED_LANGUAGE,
|
||||||
COMPOSE_EMOJI_INSERT,
|
COMPOSE_EMOJI_INSERT,
|
||||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||||
|
@ -56,9 +60,6 @@ import {
|
||||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||||
COMPOSE_ADD_SUGGESTED_QUOTE,
|
COMPOSE_ADD_SUGGESTED_QUOTE,
|
||||||
ComposeAction,
|
ComposeAction,
|
||||||
COMPOSE_ADD_SUGGESTED_LANGUAGE,
|
|
||||||
COMPOSE_LANGUAGE_ADD,
|
|
||||||
COMPOSE_LANGUAGE_DELETE,
|
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
|
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
|
||||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me';
|
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({
|
const PollRecord = ImmutableRecord({
|
||||||
options: ImmutableList(['', '']),
|
options: ImmutableList(['', '']),
|
||||||
|
options_map: ImmutableList<ImmutableMap<Language, string>>([ImmutableMap(), ImmutableMap()]),
|
||||||
expires_in: 24 * 3600,
|
expires_in: 24 * 3600,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
});
|
||||||
|
@ -90,6 +92,7 @@ const ReducerCompose = ImmutableRecord({
|
||||||
content_type: 'text/plain',
|
content_type: 'text/plain',
|
||||||
draft_id: null as string | null,
|
draft_id: null as string | null,
|
||||||
editorState: null as string | null,
|
editorState: null as string | null,
|
||||||
|
editorStateMap: ImmutableMap<Language, string | null>(),
|
||||||
focusDate: null as Date | null,
|
focusDate: null as Date | null,
|
||||||
group_id: null as string | null,
|
group_id: null as string | null,
|
||||||
idempotencyKey: '',
|
idempotencyKey: '',
|
||||||
|
@ -109,6 +112,7 @@ const ReducerCompose = ImmutableRecord({
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
|
spoilerTextMap: ImmutableMap<Language, string>(),
|
||||||
suggestions: ImmutableList<string>(),
|
suggestions: ImmutableList<string>(),
|
||||||
suggestion_token: null as string | null,
|
suggestion_token: null as string | null,
|
||||||
tagHistory: ImmutableList<string>(),
|
tagHistory: ImmutableList<string>(),
|
||||||
|
@ -118,6 +122,7 @@ const ReducerCompose = ImmutableRecord({
|
||||||
parent_reblogged_by: null as string | null,
|
parent_reblogged_by: null as string | null,
|
||||||
dismissed_quotes: ImmutableOrderedSet<string>(),
|
dismissed_quotes: ImmutableOrderedSet<string>(),
|
||||||
language: null as Language | null,
|
language: null as Language | null,
|
||||||
|
modified_language: null as Language | null,
|
||||||
suggested_language: null as string | null,
|
suggested_language: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -302,9 +307,11 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
}));
|
}));
|
||||||
case COMPOSE_SPOILER_TEXT_CHANGE:
|
case COMPOSE_SPOILER_TEXT_CHANGE:
|
||||||
return updateCompose(state, action.id, compose => compose
|
return updateCompose(state, action.id, compose => {
|
||||||
.set('spoiler_text', action.text)
|
return compose
|
||||||
.set('idempotencyKey', uuid()));
|
.setIn(compose.modified_language === compose.language ? ['spoiler_text'] : ['spoilerTextMap', compose.modified_language], action.text)
|
||||||
|
.set('idempotencyKey', uuid());
|
||||||
|
});
|
||||||
case COMPOSE_VISIBILITY_CHANGE:
|
case COMPOSE_VISIBILITY_CHANGE:
|
||||||
return updateCompose(state, action.id, compose => compose
|
return updateCompose(state, action.id, compose => compose
|
||||||
.set('privacy', action.value)
|
.set('privacy', action.value)
|
||||||
|
@ -312,6 +319,12 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
||||||
case COMPOSE_LANGUAGE_CHANGE:
|
case COMPOSE_LANGUAGE_CHANGE:
|
||||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||||
map.set('language', action.value);
|
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());
|
map.set('idempotencyKey', uuid());
|
||||||
}));
|
}));
|
||||||
case COMPOSE_CHANGE:
|
case COMPOSE_CHANGE:
|
||||||
|
@ -513,11 +526,21 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
||||||
case COMPOSE_SCHEDULE_REMOVE:
|
case COMPOSE_SCHEDULE_REMOVE:
|
||||||
return updateCompose(state, action.id, compose => compose.set('schedule', null));
|
return updateCompose(state, action.id, compose => compose.set('schedule', null));
|
||||||
case COMPOSE_POLL_OPTION_ADD:
|
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:
|
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:
|
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:
|
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||||
return updateCompose(state, action.id, compose => compose.update('poll', poll => {
|
return updateCompose(state, action.id, compose => compose.update('poll', poll => {
|
||||||
if (!poll) return null;
|
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));
|
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
|
||||||
case COMPOSE_EDITOR_STATE_SET:
|
case COMPOSE_EDITOR_STATE_SET:
|
||||||
return updateCompose(state, action.id, compose => compose
|
return updateCompose(state, action.id, compose => compose
|
||||||
.set('editorState', action.editorState as string)
|
.setIn(!compose.modified_language || compose.modified_language === compose.language ? ['editorState'] : ['editorStateMap', compose.modified_language], action.editorState as string)
|
||||||
.set('text', action.text as string));
|
.setIn(!compose.modified_language || compose.modified_language === compose.language ? ['text'] : ['textMap', compose.modified_language], action.text as string));
|
||||||
case EVENT_COMPOSE_CANCEL:
|
case EVENT_COMPOSE_CANCEL:
|
||||||
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
|
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
|
||||||
case EVENT_FORM_SET:
|
case EVENT_FORM_SET:
|
||||||
|
@ -560,9 +583,21 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
||||||
case COMPOSE_ADD_SUGGESTED_LANGUAGE:
|
case COMPOSE_ADD_SUGGESTED_LANGUAGE:
|
||||||
return updateCompose(state, action.id, compose => compose.set('suggested_language', action.language));
|
return updateCompose(state, action.id, compose => compose.set('suggested_language', action.language));
|
||||||
case COMPOSE_LANGUAGE_ADD:
|
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:
|
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:
|
case COMPOSE_QUOTE_CANCEL:
|
||||||
return updateCompose(state, action.id, compose => compose
|
return updateCompose(state, action.id, compose => compose
|
||||||
.update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes)
|
.update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes)
|
||||||
|
|
|
@ -43,6 +43,7 @@ import {
|
||||||
STATUS_TRANSLATE_SUCCESS,
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
STATUS_TRANSLATE_UNDO,
|
STATUS_TRANSLATE_UNDO,
|
||||||
STATUS_UNFILTER,
|
STATUS_UNFILTER,
|
||||||
|
STATUS_LANGUAGE_CHANGE,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
|
||||||
|
@ -95,6 +96,9 @@ const buildSearchContent = (status: StatusRecord): string => {
|
||||||
return unescapeHTML(fields.join('\n\n')) || '';
|
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
|
// Only calculate these values when status first encountered
|
||||||
// Otherwise keep the ones already in the reducer
|
// Otherwise keep the ones already in the reducer
|
||||||
const calculateStatus = (
|
const calculateStatus = (
|
||||||
|
@ -102,21 +106,23 @@ const calculateStatus = (
|
||||||
oldStatus?: StatusRecord,
|
oldStatus?: StatusRecord,
|
||||||
): StatusRecord => {
|
): StatusRecord => {
|
||||||
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
|
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({
|
return status.merge({
|
||||||
search_index: oldStatus.search_index,
|
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, translation, currentLanguage,
|
||||||
contentHtml: oldStatus.contentHtml,
|
|
||||||
spoilerHtml: oldStatus.spoilerHtml,
|
|
||||||
hidden: oldStatus.hidden,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const spoilerText = status.spoiler_text;
|
|
||||||
const searchContent = buildSearchContent(status);
|
const searchContent = buildSearchContent(status);
|
||||||
const emojiMap = makeEmojiMap(status.emojis);
|
const emojiMap = makeEmojiMap(status.emojis);
|
||||||
|
|
||||||
return status.merge({
|
return status.merge({
|
||||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||||
contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(status.content, emojiMap)), { USE_PROFILES: { html: true } }),
|
contentHtml: calculateContent(status.content, emojiMap),
|
||||||
spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap), { USE_PROFILES: { html: true } }),
|
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);
|
return deleteTranslation(state, action.id);
|
||||||
case STATUS_UNFILTER:
|
case STATUS_UNFILTER:
|
||||||
return state.setIn([action.id, 'showFiltered'], false);
|
return state.setIn([action.id, 'showFiltered'], false);
|
||||||
|
case STATUS_LANGUAGE_CHANGE:
|
||||||
|
return state.setIn([action.id, 'currentLanguage'], action.language);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
case EVENT_JOIN_REQUEST:
|
case EVENT_JOIN_REQUEST:
|
||||||
|
|
|
@ -7,8 +7,11 @@ import emojify from 'soapbox/features/emoji';
|
||||||
import { customEmojiSchema } from './custom-emoji';
|
import { customEmojiSchema } from './custom-emoji';
|
||||||
import { filteredArray, makeCustomEmojiMap } from './utils';
|
import { filteredArray, makeCustomEmojiMap } from './utils';
|
||||||
|
|
||||||
|
const sanitizeTitle = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { ALLOWED_TAGS: [] });
|
||||||
|
|
||||||
const pollOptionSchema = z.object({
|
const pollOptionSchema = z.object({
|
||||||
title: z.string().catch(''),
|
title: z.string().catch(''),
|
||||||
|
title_map: z.record(z.string(), z.string()).nullable().catch(null),
|
||||||
votes_count: z.number().catch(0),
|
votes_count: z.number().catch(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,7 +34,10 @@ const pollSchema = z.object({
|
||||||
|
|
||||||
const emojifiedOptions = poll.options.map((option) => ({
|
const emojifiedOptions = poll.options.map((option) => ({
|
||||||
...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.
|
// If the user has votes, they have certainly voted.
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
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 { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { type MRFSimple } from 'soapbox/schemas/pleroma';
|
import { type MRFSimple } from 'soapbox/schemas/pleroma';
|
||||||
import { getDomain } from 'soapbox/utils/accounts';
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
|
@ -136,9 +136,10 @@ const makeGetStatus = () => createSelector(
|
||||||
getFilters,
|
getFilters,
|
||||||
(state: RootState) => state.me,
|
(state: RootState) => state.me,
|
||||||
(state: RootState) => getFeatures(state.instance),
|
(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;
|
if (!statusBase) return null;
|
||||||
const { account } = statusBase;
|
const { account } = statusBase;
|
||||||
const accountUsername = account.acct;
|
const accountUsername = account.acct;
|
||||||
|
@ -151,6 +152,14 @@ const makeGetStatus = () => createSelector(
|
||||||
return statusBase.withMutations((map: Status) => {
|
return statusBase.withMutations((map: Status) => {
|
||||||
map.set('reblog', statusReblog || null);
|
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) {
|
if ((features.filters) && account.id !== me) {
|
||||||
const filtered = checkFiltered(statusReblog?.search_index || statusBase.search_index, filters);
|
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.
|
* Ability to include multiple language variants for a post.
|
||||||
* @see POST /api/v1/statuses
|
* @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.
|
* Ability to hide notifications from people you don't follow.
|
||||||
|
|
Loading…
Reference in a new issue