[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:
Eugen Rochko 2022-09-23 23:00:12 +02:00 committed by Claire
parent f3ce9653eb
commit a3052dad04
7 changed files with 105 additions and 7 deletions

View File

@ -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,
});

View File

@ -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')}

View File

@ -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>
); );
} }

View File

@ -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));
}, },

View File

@ -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')}

View File

@ -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}

View File

@ -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;
} }