kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Support translation feature on Mastodon
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-translatio-h7npbq/deployments/1213
rodzic
df7f2d4dca
commit
3448022965
|
@ -1,5 +1,6 @@
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
import { gte } from 'semver';
|
||||||
|
|
||||||
import KVStore from 'soapbox/storage/kv_store';
|
import KVStore from 'soapbox/storage/kv_store';
|
||||||
import { RootState } from 'soapbox/store';
|
import { RootState } from 'soapbox/store';
|
||||||
|
@ -37,6 +38,12 @@ const needsNodeinfo = (instance: Record<string, any>): boolean => {
|
||||||
return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']);
|
return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Mastodon exposes features availabiliy under /api/v2/instance since 4.0.0 */
|
||||||
|
const supportsInstanceV2 = (instance: Record<string, any>): boolean => {
|
||||||
|
const v = parseVersion(get(instance, 'version'));
|
||||||
|
return v.software === 'Mastodon' && gte(v.compatVersion, '4.0.0');
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchInstance = createAsyncThunk<void, void, { state: RootState }>(
|
export const fetchInstance = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
'instance/fetch',
|
'instance/fetch',
|
||||||
async(_arg, { dispatch, getState, rejectWithValue }) => {
|
async(_arg, { dispatch, getState, rejectWithValue }) => {
|
||||||
|
@ -45,6 +52,9 @@ export const fetchInstance = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
if (needsNodeinfo(instance)) {
|
if (needsNodeinfo(instance)) {
|
||||||
dispatch(fetchNodeinfo());
|
dispatch(fetchNodeinfo());
|
||||||
}
|
}
|
||||||
|
if (supportsInstanceV2(instance)) {
|
||||||
|
dispatch(fetchInstanceV2());
|
||||||
|
}
|
||||||
return instance;
|
return instance;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return rejectWithValue(e);
|
return rejectWithValue(e);
|
||||||
|
@ -64,9 +74,15 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
export const fetchInstanceV2 = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
'nodeinfo/fetch',
|
'nodeinfo/fetch',
|
||||||
async(_arg, { getState }) => {
|
async(_arg, { getState }) => {
|
||||||
return await api(getState).get('/nodeinfo/2.1.json');
|
const { data: instance } = await api(getState).get('/api/v2/instance');
|
||||||
|
return instance;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
||||||
|
'nodeinfo/fetch',
|
||||||
|
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
|
||||||
|
);
|
||||||
|
|
|
@ -43,6 +43,11 @@ 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_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||||
|
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
|
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||||
return (getState().statuses.get(statusId) || null) !== null;
|
return (getState().statuses.get(statusId) || null) !== null;
|
||||||
};
|
};
|
||||||
|
@ -305,6 +310,29 @@ const toggleStatusHidden = (status: Status) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const translateStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
|
||||||
|
dispatch({
|
||||||
|
type: STATUS_TRANSLATE_SUCCESS,
|
||||||
|
id,
|
||||||
|
translation: response.data,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({
|
||||||
|
type: STATUS_TRANSLATE_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const undoStatusTranslation = (id: string) => ({
|
||||||
|
type: STATUS_TRANSLATE_UNDO,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STATUS_CREATE_REQUEST,
|
STATUS_CREATE_REQUEST,
|
||||||
STATUS_CREATE_SUCCESS,
|
STATUS_CREATE_SUCCESS,
|
||||||
|
@ -329,6 +357,10 @@ export {
|
||||||
STATUS_UNMUTE_FAIL,
|
STATUS_UNMUTE_FAIL,
|
||||||
STATUS_REVEAL,
|
STATUS_REVEAL,
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
|
STATUS_TRANSLATE_REQUEST,
|
||||||
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
|
STATUS_TRANSLATE_FAIL,
|
||||||
|
STATUS_TRANSLATE_UNDO,
|
||||||
createStatus,
|
createStatus,
|
||||||
editStatus,
|
editStatus,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -345,4 +377,6 @@ export {
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
toggleStatusHidden,
|
toggleStatusHidden,
|
||||||
|
translateStatus,
|
||||||
|
undoStatusTranslation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -385,8 +386,11 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
expanded={!status.hidden}
|
expanded={!status.hidden}
|
||||||
onExpandedToggle={handleExpandedToggle}
|
onExpandedToggle={handleExpandedToggle}
|
||||||
collapsable
|
collapsable
|
||||||
|
translatable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TranslateButton status={actualStatus} />
|
||||||
|
|
||||||
<StatusMedia
|
<StatusMedia
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
|
|
|
@ -71,10 +71,11 @@ interface IStatusContent {
|
||||||
onExpandedToggle?: () => void,
|
onExpandedToggle?: () => void,
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
collapsable?: boolean,
|
collapsable?: boolean,
|
||||||
|
translatable?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders the text content of a status */
|
/** Renders the text content of a status */
|
||||||
const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false }) => {
|
const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false, translatable }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const [hidden, setHidden] = useState(true);
|
const [hidden, setHidden] = useState(true);
|
||||||
|
@ -199,14 +200,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedHtml = useMemo((): string => {
|
const parsedHtml = useMemo((): string => {
|
||||||
const { contentHtml: html } = status;
|
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||||
|
|
||||||
if (greentext) {
|
if (greentext) {
|
||||||
return addGreentext(html);
|
return addGreentext(html);
|
||||||
} else {
|
} else {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
}, [status.contentHtml]);
|
}, [status.contentHtml, status.translation]);
|
||||||
|
|
||||||
if (status.content.length === 0) {
|
if (status.content.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
||||||
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { Stack } from './ui';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface ITranslateButton {
|
||||||
|
status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const me = useAppSelector((state) => state.me);
|
||||||
|
|
||||||
|
const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||||
|
|
||||||
|
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
dispatch(undoStatusTranslation(status.id));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!features.translations || !renderTranslate) return null;
|
||||||
|
|
||||||
|
if (status.translation) {
|
||||||
|
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||||
|
const languageName = languageNames.of(status.language!);
|
||||||
|
const provider = status.translation.get('provider');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
|
||||||
|
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||||
|
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||||
|
</button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
|
||||||
|
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TranslateButton;
|
|
@ -19,14 +19,17 @@ const justifyContentOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignItemsOptions = {
|
const alignItemsOptions = {
|
||||||
|
top: 'items-start',
|
||||||
|
bottom: 'items-end',
|
||||||
center: 'items-center',
|
center: 'items-center',
|
||||||
|
start: 'items-start',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
/** Size of the gap between elements. */
|
/** Size of the gap between elements. */
|
||||||
space?: keyof typeof spaces
|
space?: keyof typeof spaces
|
||||||
/** Horizontal alignment of children. */
|
/** Horizontal alignment of children. */
|
||||||
alignItems?: 'center'
|
alignItems?: keyof typeof alignItemsOptions
|
||||||
/** Vertical alignment of children. */
|
/** Vertical alignment of children. */
|
||||||
justifyContent?: keyof typeof justifyContentOptions
|
justifyContent?: keyof typeof justifyContentOptions
|
||||||
/** Extra class names on the <div> element. */
|
/** Extra class names on the <div> element. */
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StatusMedia from 'soapbox/components/status-media';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import StatusContent from 'soapbox/components/status_content';
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
||||||
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||||
|
@ -109,8 +110,11 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
expanded={!actualStatus.hidden}
|
expanded={!actualStatus.hidden}
|
||||||
onExpandedToggle={handleExpandedToggle}
|
onExpandedToggle={handleExpandedToggle}
|
||||||
|
translatable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TranslateButton status={actualStatus} />
|
||||||
|
|
||||||
<StatusMedia
|
<StatusMedia
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
showMedia={showMedia}
|
showMedia={showMedia}
|
||||||
|
|
|
@ -1211,8 +1211,11 @@
|
||||||
"status.show_less_all": "Zwiń wszystkie",
|
"status.show_less_all": "Zwiń wszystkie",
|
||||||
"status.show_more": "Rozwiń",
|
"status.show_more": "Rozwiń",
|
||||||
"status.show_more_all": "Rozwiń wszystkie",
|
"status.show_more_all": "Rozwiń wszystkie",
|
||||||
|
"status.show_original": "Pokaż oryginalny wpis",
|
||||||
"status.title": "Wpis",
|
"status.title": "Wpis",
|
||||||
"status.title_direct": "Wiadomość bezpośrednia",
|
"status.title_direct": "Wiadomość bezpośrednia",
|
||||||
|
"status.translated_from_with": "Przetłumaczono z {lang} z użyciem {provider}",
|
||||||
|
"status.translate": "Przetłumacz wpis",
|
||||||
"status.unbookmark": "Usuń z zakładek",
|
"status.unbookmark": "Usuń z zakładek",
|
||||||
"status.unbookmarked": "Usunięto z zakładek.",
|
"status.unbookmarked": "Usunięto z zakładek.",
|
||||||
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
|
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
|
||||||
|
|
|
@ -63,6 +63,7 @@ export const StatusRecord = ImmutableRecord({
|
||||||
hidden: false,
|
hidden: false,
|
||||||
search_index: '',
|
search_index: '',
|
||||||
spoilerHtml: '',
|
spoilerHtml: '',
|
||||||
|
translation: null as ImmutableMap<string, string> | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
* @module soapbox/precheck
|
* @module soapbox/precheck
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Whether pre-rendered data exists in Mastodon's format. */
|
/** Whether pre-rendered data exists in Pleroma's format. */
|
||||||
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
||||||
|
|
||||||
/** Whether pre-rendered data exists in Pleroma's format. */
|
/** Whether pre-rendered data exists in Mastodon's format. */
|
||||||
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
||||||
|
|
||||||
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
|
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
rememberInstance,
|
rememberInstance,
|
||||||
fetchInstance,
|
fetchInstance,
|
||||||
fetchNodeinfo,
|
fetchNodeinfo,
|
||||||
|
fetchInstanceV2,
|
||||||
} from '../actions/instance';
|
} from '../actions/instance';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
@ -32,10 +33,20 @@ const nodeinfoToInstance = (nodeinfo: ImmutableMap<string, any>) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const instanceV2ToInstance = (instanceV2: ImmutableMap<string, any>) =>
|
||||||
|
normalizeInstance(ImmutableMap({
|
||||||
|
configuration: instanceV2.get('configuration'),
|
||||||
|
}));
|
||||||
|
|
||||||
const importInstance = (_state: typeof initialState, instance: ImmutableMap<string, any>) => {
|
const importInstance = (_state: typeof initialState, instance: ImmutableMap<string, any>) => {
|
||||||
return normalizeInstance(instance);
|
return normalizeInstance(instance);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importInstanceV2 = (state: typeof initialState, instanceV2: ImmutableMap<string, any>) => {
|
||||||
|
console.log(instanceV2.toJS());
|
||||||
|
return state.mergeDeep(instanceV2ToInstance(instanceV2));
|
||||||
|
};
|
||||||
|
|
||||||
const importNodeinfo = (state: typeof initialState, nodeinfo: ImmutableMap<string, any>) => {
|
const importNodeinfo = (state: typeof initialState, nodeinfo: ImmutableMap<string, any>) => {
|
||||||
return nodeinfoToInstance(nodeinfo).mergeDeep(state);
|
return nodeinfoToInstance(nodeinfo).mergeDeep(state);
|
||||||
};
|
};
|
||||||
|
@ -120,6 +131,8 @@ export default function instance(state = initialState, action: AnyAction) {
|
||||||
case fetchInstance.fulfilled.type:
|
case fetchInstance.fulfilled.type:
|
||||||
persistInstance(action.payload);
|
persistInstance(action.payload);
|
||||||
return importInstance(state, ImmutableMap(fromJS(action.payload)));
|
return importInstance(state, ImmutableMap(fromJS(action.payload)));
|
||||||
|
case fetchInstanceV2.fulfilled.type:
|
||||||
|
return importInstanceV2(state, ImmutableMap(fromJS(action.payload)));
|
||||||
case fetchInstance.rejected.type:
|
case fetchInstance.rejected.type:
|
||||||
return handleInstanceFetchFail(state, action.error);
|
return handleInstanceFetchFail(state, action.error);
|
||||||
case fetchNodeinfo.fulfilled.type:
|
case fetchNodeinfo.fulfilled.type:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji/emoji';
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
|
@ -30,6 +30,8 @@ import {
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
STATUS_DELETE_REQUEST,
|
STATUS_DELETE_REQUEST,
|
||||||
STATUS_DELETE_FAIL,
|
STATUS_DELETE_FAIL,
|
||||||
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
|
STATUS_TRANSLATE_UNDO,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
|
||||||
|
@ -255,6 +257,10 @@ export default function statuses(state = initialState, action: AnyAction): State
|
||||||
return decrementReplyCount(state, action.params);
|
return decrementReplyCount(state, action.params);
|
||||||
case STATUS_DELETE_FAIL:
|
case STATUS_DELETE_FAIL:
|
||||||
return incrementReplyCount(state, action.params);
|
return incrementReplyCount(state, action.params);
|
||||||
|
case STATUS_TRANSLATE_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'translation'], fromJS(action.translation));
|
||||||
|
case STATUS_TRANSLATE_UNDO:
|
||||||
|
return state.deleteIn([action.id, 'translation']);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -353,6 +353,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
importData: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
importData: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports V2 instance endpoint.
|
||||||
|
* @see GET /api/v2/instance
|
||||||
|
*/
|
||||||
|
instanceV2: v.software === MASTODON && gte(v.compatVersion, '4.0.0'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can create, view, and manage lists.
|
* Can create, view, and manage lists.
|
||||||
* @see {@link https://docs.joinmastodon.org/methods/timelines/lists/}
|
* @see {@link https://docs.joinmastodon.org/methods/timelines/lists/}
|
||||||
|
@ -608,6 +614,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
features.includes('v2_suggestions'),
|
features.includes('v2_suggestions'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can translate statuses.
|
||||||
|
* @see POST /api/v1/statuses/:id/translate
|
||||||
|
*/
|
||||||
|
translations: v.software === MASTODON && instance.configuration.getIn(['translation', 'enabled'], false),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trending statuses.
|
* Trending statuses.
|
||||||
* @see GET /api/v1/trends/statuses
|
* @see GET /api/v1/trends/statuses
|
||||||
|
|
Ładowanie…
Reference in New Issue