[Glitch] Add user content translations with configurable backends
Port 0d6b878808
to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
f3ce9653eb
commit
a3052dad04
7 changed files with 105 additions and 7 deletions
|
@ -34,6 +34,11 @@ export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
||||||
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
||||||
|
|
||||||
|
export const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
|
||||||
|
export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||||
|
export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
|
export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
|
@ -310,4 +315,36 @@ export function toggleStatusCollapse(id, isCollapsed) {
|
||||||
id,
|
id,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const translateStatus = id => (dispatch, getState) => {
|
||||||
|
dispatch(translateStatusRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
|
||||||
|
dispatch(translateStatusSuccess(id, response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(translateStatusFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const translateStatusRequest = id => ({
|
||||||
|
type: STATUS_TRANSLATE_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const translateStatusSuccess = (id, translation) => ({
|
||||||
|
type: STATUS_TRANSLATE_SUCCESS,
|
||||||
|
id,
|
||||||
|
translation,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const translateStatusFail = (id, error) => ({
|
||||||
|
type: STATUS_TRANSLATE_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const undoStatusTranslation = id => ({
|
||||||
|
type: STATUS_TRANSLATE_UNDO,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
|
@ -83,6 +83,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
onToggleHidden: PropTypes.func,
|
onToggleHidden: PropTypes.func,
|
||||||
|
onTranslate: PropTypes.func,
|
||||||
onInteractionModal: PropTypes.func,
|
onInteractionModal: PropTypes.func,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -472,6 +473,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTranslate = () => {
|
||||||
|
this.props.onTranslate(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
renderLoadingMediaGallery () {
|
renderLoadingMediaGallery () {
|
||||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
|
@ -788,6 +793,7 @@ class Status extends ImmutablePureComponent {
|
||||||
mediaIcons={contentMediaIcons}
|
mediaIcons={contentMediaIcons}
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
onExpandedToggle={this.handleExpandedToggle}
|
onExpandedToggle={this.handleExpandedToggle}
|
||||||
|
onTranslate={this.handleTranslate}
|
||||||
parseClick={parseClick}
|
parseClick={parseClick}
|
||||||
disabled={!router}
|
disabled={!router}
|
||||||
tagLinks={settings.get('tag_misleading_links')}
|
tagLinks={settings.get('tag_misleading_links')}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
@ -62,13 +62,15 @@ const isLinkMisleading = (link) => {
|
||||||
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
|
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class StatusContent extends React.PureComponent {
|
export default @injectIntl
|
||||||
|
class StatusContent extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
expanded: PropTypes.bool,
|
expanded: PropTypes.bool,
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
onExpandedToggle: PropTypes.func,
|
onExpandedToggle: PropTypes.func,
|
||||||
|
onTranslate: PropTypes.func,
|
||||||
media: PropTypes.node,
|
media: PropTypes.node,
|
||||||
extraMedia: PropTypes.node,
|
extraMedia: PropTypes.node,
|
||||||
mediaIcons: PropTypes.arrayOf(PropTypes.string),
|
mediaIcons: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
@ -77,6 +79,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
onUpdate: PropTypes.func,
|
onUpdate: PropTypes.func,
|
||||||
tagLinks: PropTypes.bool,
|
tagLinks: PropTypes.bool,
|
||||||
rewriteMentions: PropTypes.string,
|
rewriteMentions: PropTypes.string,
|
||||||
|
intl: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -249,6 +252,10 @@ export default class StatusContent extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTranslate = () => {
|
||||||
|
this.props.onTranslate();
|
||||||
|
}
|
||||||
|
|
||||||
setContentsRef = (c) => {
|
setContentsRef = (c) => {
|
||||||
this.contentsNode = c;
|
this.contentsNode = c;
|
||||||
}
|
}
|
||||||
|
@ -263,18 +270,27 @@ export default class StatusContent extends React.PureComponent {
|
||||||
disabled,
|
disabled,
|
||||||
tagLinks,
|
tagLinks,
|
||||||
rewriteMentions,
|
rewriteMentions,
|
||||||
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||||
|
const renderTranslate = this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && intl.locale !== status.get('language');
|
||||||
|
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
|
||||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||||
const lang = status.get('language');
|
const lang = status.get('translation') ? intl.locale : status.get('language');
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
'status__content--with-action': parseClick && !disabled,
|
'status__content--with-action': parseClick && !disabled,
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const translateButton = (
|
||||||
|
<button className='status__content__read-more-button' onClick={this.handleTranslate}>
|
||||||
|
{status.get('translation') ? <span><FormattedMessage id='status.translated_from' defaultMessage='Translated from {lang}' values={{ lang: languageNames.of(status.get('language')) }} /> · <FormattedMessage id='status.show_original' defaultMessage='Show original' /></span> : <FormattedMessage id='status.translate' defaultMessage='Translate' />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
|
@ -355,6 +371,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
|
|
||||||
{extraMedia}
|
{extraMedia}
|
||||||
|
|
||||||
|
{!hidden && renderTranslate && translateButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (parseClick) {
|
} else if (parseClick) {
|
||||||
|
@ -377,6 +394,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
{media}
|
{media}
|
||||||
{extraMedia}
|
{extraMedia}
|
||||||
|
{!hidden && renderTranslate && translateButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -397,6 +415,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
{media}
|
{media}
|
||||||
{extraMedia}
|
{extraMedia}
|
||||||
|
{!hidden && renderTranslate && translateButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,9 @@ import {
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
editStatus
|
editStatus,
|
||||||
|
translateStatus,
|
||||||
|
undoStatusTranslation,
|
||||||
} from 'flavours/glitch/actions/statuses';
|
} from 'flavours/glitch/actions/statuses';
|
||||||
import {
|
import {
|
||||||
initAddFilter,
|
initAddFilter,
|
||||||
|
@ -187,6 +189,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch(editStatus(status.get('id'), history));
|
dispatch(editStatus(status.get('id'), history));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTranslate (status) {
|
||||||
|
if (status.get('translation')) {
|
||||||
|
dispatch(undoStatusTranslation(status.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account, router) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account, router));
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,6 +34,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
onOpenVideo: PropTypes.func.isRequired,
|
||||||
onToggleHidden: PropTypes.func,
|
onToggleHidden: PropTypes.func,
|
||||||
|
onTranslate: PropTypes.func.isRequired,
|
||||||
expanded: PropTypes.bool,
|
expanded: PropTypes.bool,
|
||||||
measureHeight: PropTypes.bool,
|
measureHeight: PropTypes.bool,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
|
@ -112,6 +113,11 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTranslate = () => {
|
||||||
|
const { onTranslate, status } = this.props;
|
||||||
|
onTranslate(status);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props;
|
const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props;
|
||||||
|
@ -305,6 +311,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
onExpandedToggle={onToggleHidden}
|
onExpandedToggle={onToggleHidden}
|
||||||
|
onTranslate={this.handleTranslate}
|
||||||
parseClick={this.parseClick}
|
parseClick={this.parseClick}
|
||||||
onUpdate={this.handleChildUpdate}
|
onUpdate={this.handleChildUpdate}
|
||||||
tagLinks={settings.get('tag_misleading_links')}
|
tagLinks={settings.get('tag_misleading_links')}
|
||||||
|
|
|
@ -33,7 +33,9 @@ import {
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
editStatus,
|
editStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus
|
revealStatus,
|
||||||
|
translateStatus,
|
||||||
|
undoStatusTranslation,
|
||||||
} from 'flavours/glitch/actions/statuses';
|
} from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
|
@ -437,6 +439,16 @@ class Status extends ImmutablePureComponent {
|
||||||
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
|
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTranslate = status => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
if (status.get('translation')) {
|
||||||
|
dispatch(undoStatusTranslation(status.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.get('id')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleBlockClick = (status) => {
|
handleBlockClick = (status) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
@ -666,6 +678,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
onToggleHidden={this.handleToggleHidden}
|
onToggleHidden={this.handleToggleHidden}
|
||||||
|
onTranslate={this.handleTranslate}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
showMedia={this.state.showMedia}
|
showMedia={this.state.showMedia}
|
||||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
STATUS_REVEAL,
|
STATUS_REVEAL,
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
STATUS_COLLAPSE,
|
STATUS_COLLAPSE,
|
||||||
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
|
STATUS_TRANSLATE_UNDO,
|
||||||
STATUS_FETCH_REQUEST,
|
STATUS_FETCH_REQUEST,
|
||||||
STATUS_FETCH_FAIL,
|
STATUS_FETCH_FAIL,
|
||||||
} from 'flavours/glitch/actions/statuses';
|
} from 'flavours/glitch/actions/statuses';
|
||||||
|
@ -85,6 +87,10 @@ export default function statuses(state = initialState, action) {
|
||||||
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
|
case STATUS_TRANSLATE_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'translation'], fromJS(action.translation));
|
||||||
|
case STATUS_TRANSLATE_UNDO:
|
||||||
|
return state.deleteIn([action.id, 'translation']);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue