Merge pull request #1983 from ClearlyClaire/glitch-soc/features/translation
Port “Translate” feature from upstreammain
commit
fc0e11abdb
|
@ -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_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) {
|
||||
return {
|
||||
type: STATUS_FETCH_REQUEST,
|
||||
|
@ -310,4 +315,36 @@ export function toggleStatusCollapse(id, isCollapsed) {
|
|||
id,
|
||||
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,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
@ -472,6 +473,10 @@ class Status extends ImmutablePureComponent {
|
|||
this.node = c;
|
||||
}
|
||||
|
||||
handleTranslate = () => {
|
||||
this.props.onTranslate(this.props.status);
|
||||
}
|
||||
|
||||
renderLoadingMediaGallery () {
|
||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
@ -788,6 +793,7 @@ class Status extends ImmutablePureComponent {
|
|||
mediaIcons={contentMediaIcons}
|
||||
expanded={isExpanded}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
onTranslate={this.handleTranslate}
|
||||
parseClick={parseClick}
|
||||
disabled={!router}
|
||||
tagLinks={settings.get('tag_misleading_links')}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import Permalink from './permalink';
|
||||
import classnames from 'classnames';
|
||||
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';
|
||||
|
||||
const textMatchesTarget = (text, origin, host) => {
|
||||
|
@ -62,13 +62,56 @@ const isLinkMisleading = (link) => {
|
|||
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 = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
expanded: PropTypes.bool,
|
||||
collapsed: PropTypes.bool,
|
||||
onExpandedToggle: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
media: PropTypes.node,
|
||||
extraMedia: PropTypes.node,
|
||||
mediaIcons: PropTypes.arrayOf(PropTypes.string),
|
||||
|
@ -77,6 +120,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
onUpdate: PropTypes.func,
|
||||
tagLinks: PropTypes.bool,
|
||||
rewriteMentions: PropTypes.string,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -249,6 +293,10 @@ export default class StatusContent extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleTranslate = () => {
|
||||
this.props.onTranslate();
|
||||
}
|
||||
|
||||
setContentsRef = (c) => {
|
||||
this.contentsNode = c;
|
||||
}
|
||||
|
@ -263,18 +311,24 @@ export default class StatusContent extends React.PureComponent {
|
|||
disabled,
|
||||
tagLinks,
|
||||
rewriteMentions,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
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 lang = status.get('language');
|
||||
const lang = status.get('translation') ? intl.locale : status.get('language');
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': parseClick && !disabled,
|
||||
'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) {
|
||||
let mentionsPlaceholder = '';
|
||||
|
||||
|
@ -350,11 +404,11 @@ export default class StatusContent extends React.PureComponent {
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
/>
|
||||
{!hidden && translateButton}
|
||||
{media}
|
||||
</div>
|
||||
|
||||
{extraMedia}
|
||||
|
||||
</div>
|
||||
);
|
||||
} else if (parseClick) {
|
||||
|
@ -375,6 +429,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
/>
|
||||
{translateButton}
|
||||
{media}
|
||||
{extraMedia}
|
||||
</div>
|
||||
|
@ -395,6 +450,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
/>
|
||||
{translateButton}
|
||||
{media}
|
||||
{extraMedia}
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,9 @@ import {
|
|||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
editStatus
|
||||
editStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from 'flavours/glitch/actions/statuses';
|
||||
import {
|
||||
initAddFilter,
|
||||
|
@ -187,6 +189,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
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) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
onOpenMedia: PropTypes.func.isRequired,
|
||||
onOpenVideo: PropTypes.func.isRequired,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onTranslate: PropTypes.func.isRequired,
|
||||
expanded: PropTypes.bool,
|
||||
measureHeight: PropTypes.bool,
|
||||
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');
|
||||
}
|
||||
|
||||
handleTranslate = () => {
|
||||
const { onTranslate, status } = this.props;
|
||||
onTranslate(status);
|
||||
}
|
||||
|
||||
render () {
|
||||
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;
|
||||
|
@ -305,6 +311,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
expanded={expanded}
|
||||
collapsed={false}
|
||||
onExpandedToggle={onToggleHidden}
|
||||
onTranslate={this.handleTranslate}
|
||||
parseClick={this.parseClick}
|
||||
onUpdate={this.handleChildUpdate}
|
||||
tagLinks={settings.get('tag_misleading_links')}
|
||||
|
|
|
@ -33,7 +33,9 @@ import {
|
|||
deleteStatus,
|
||||
editStatus,
|
||||
hideStatus,
|
||||
revealStatus
|
||||
revealStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
|
@ -437,6 +439,16 @@ class Status extends ImmutablePureComponent {
|
|||
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) => {
|
||||
const { dispatch } = this.props;
|
||||
const account = status.get('account');
|
||||
|
@ -666,6 +678,7 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenMedia={this.handleOpenMedia}
|
||||
expanded={isExpanded}
|
||||
onToggleHidden={this.handleToggleHidden}
|
||||
onTranslate={this.handleTranslate}
|
||||
domain={domain}
|
||||
showMedia={this.state.showMedia}
|
||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
* @property {boolean} use_blurhash
|
||||
* @property {boolean=} use_pending_items
|
||||
* @property {string} version
|
||||
* @property {boolean} translation_enabled
|
||||
* @property {object} local_settings
|
||||
*/
|
||||
|
||||
|
@ -137,6 +138,7 @@ export const unfollowModal = getMeta('unfollow_modal');
|
|||
export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const version = getMeta('version');
|
||||
export const translationEnabled = getMeta('translation_enabled');
|
||||
export const languages = initialState?.languages;
|
||||
|
||||
// Glitch-soc-specific settings
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
STATUS_REVEAL,
|
||||
STATUS_HIDE,
|
||||
STATUS_COLLAPSE,
|
||||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
STATUS_FETCH_REQUEST,
|
||||
STATUS_FETCH_FAIL,
|
||||
} from 'flavours/glitch/actions/statuses';
|
||||
|
@ -85,6 +87,10 @@ export default function statuses(state = initialState, action) {
|
|||
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
||||
case TIMELINE_DELETE:
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
color: $ui-highlight-color;
|
||||
color: $highlight-text-color;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
|
|
@ -206,15 +206,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status__content__edited-label {
|
||||
display: block;
|
||||
cursor: default;
|
||||
.translate-button {
|
||||
margin-top: 16px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
padding: 0;
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: $dark-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status__content__spoiler-link {
|
||||
|
|
|
@ -268,7 +268,7 @@ a.button.logo-button {
|
|||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
padding-top: 8px;
|
||||
padding-top: 16px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
|
|
Loading…
Reference in New Issue