Merge pull request #1983 from ClearlyClaire/glitch-soc/features/translation

Port “Translate” feature from upstream
This commit is contained in:
Claire 2022-11-30 18:12:53 +01:00 committed by GitHub
commit fc0e11abdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 152 additions and 17 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,11 +1,11 @@
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';
import { autoPlayGif } from 'flavours/glitch/initial_state'; import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
const textMatchesTarget = (text, origin, host) => { const textMatchesTarget = (text, origin, host) => {
@ -62,13 +62,56 @@ 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 { class TranslateButton extends React.PureComponent {
static propTypes = {
translation: ImmutablePropTypes.map,
onClick: PropTypes.func,
};
render () {
const { translation, onClick } = this.props;
if (translation) {
const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language'));
const languageName = language ? language[2] : translation.get('detected_source_language');
const provider = translation.get('provider');
return (
<div className='translate-button'>
<div className='translate-button__meta'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
</div>
<button className='link-button' onClick={onClick}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button>
</div>
);
}
return (
<button className='status__content__read-more-button' onClick={onClick}>
<FormattedMessage id='status.translate' defaultMessage='Translate' />
</button>
);
}
}
export default @injectIntl
class StatusContent extends React.PureComponent {
static contextTypes = {
identity: PropTypes.object,
};
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 +120,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 +293,10 @@ export default class StatusContent extends React.PureComponent {
} }
} }
handleTranslate = () => {
this.props.onTranslate();
}
setContentsRef = (c) => { setContentsRef = (c) => {
this.contentsNode = c; this.contentsNode = c;
} }
@ -263,18 +311,24 @@ 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 = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('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 = renderTranslate && (
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
);
if (status.get('spoiler_text').length > 0) { if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = ''; let mentionsPlaceholder = '';
@ -350,11 +404,11 @@ export default class StatusContent extends React.PureComponent {
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={lang}
/> />
{!hidden && translateButton}
{media} {media}
</div> </div>
{extraMedia} {extraMedia}
</div> </div>
); );
} else if (parseClick) { } else if (parseClick) {
@ -375,6 +429,7 @@ export default class StatusContent extends React.PureComponent {
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={lang}
/> />
{translateButton}
{media} {media}
{extraMedia} {extraMedia}
</div> </div>
@ -395,6 +450,7 @@ export default class StatusContent extends React.PureComponent {
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={lang}
/> />
{translateButton}
{media} {media}
{extraMedia} {extraMedia}
</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

@ -79,6 +79,7 @@
* @property {boolean} use_blurhash * @property {boolean} use_blurhash
* @property {boolean=} use_pending_items * @property {boolean=} use_pending_items
* @property {string} version * @property {string} version
* @property {boolean} translation_enabled
* @property {object} local_settings * @property {object} local_settings
*/ */
@ -137,6 +138,7 @@ export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash'); export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version'); export const version = getMeta('version');
export const translationEnabled = getMeta('translation_enabled');
export const languages = initialState?.languages; export const languages = initialState?.languages;
// Glitch-soc-specific settings // Glitch-soc-specific settings

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

View file

@ -15,7 +15,7 @@
display: block; display: block;
font-size: 15px; font-size: 15px;
line-height: 20px; line-height: 20px;
color: $ui-highlight-color; color: $highlight-text-color;
border: 0; border: 0;
background: transparent; background: transparent;
padding: 0; padding: 0;

View file

@ -206,15 +206,13 @@
} }
} }
.status__content__edited-label { .translate-button {
display: block; margin-top: 16px;
cursor: default;
font-size: 15px; font-size: 15px;
line-height: 20px; line-height: 20px;
padding: 0; display: flex;
padding-top: 8px; justify-content: space-between;
color: $dark-text-color; color: $dark-text-color;
font-weight: 500;
} }
.status__content__spoiler-link { .status__content__spoiler-link {

View file

@ -268,7 +268,7 @@ a.button.logo-button {
border: 0; border: 0;
background: transparent; background: transparent;
padding: 0; padding: 0;
padding-top: 8px; padding-top: 16px;
text-decoration: none; text-decoration: none;
&:hover, &:hover,