diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 5930b7a160..a3e2a24f28 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -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,
+});
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 800832dc8e..4041b48194 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -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
;
}
@@ -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')}
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index c618cedcab..c59f42220c 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -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 (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+}
+
+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 && (
+
+ );
+
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}
{extraMedia}
-
);
} else if (parseClick) {
@@ -375,6 +429,7 @@ export default class StatusContent extends React.PureComponent {
onMouseLeave={this.handleMouseLeave}
lang={lang}
/>
+ {translateButton}
{media}
{extraMedia}
@@ -395,6 +450,7 @@ export default class StatusContent extends React.PureComponent {
onMouseLeave={this.handleMouseLeave}
lang={lang}
/>
+ {translateButton}
{media}
{extraMedia}
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index c12b2e6143..947573fc7d 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -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));
},
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 46770930f5..7d2c2aace3 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -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')}
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index aaa9c7928f..e190652b06 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -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}
diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js
index 5be177cedf..bbf25c8a85 100644
--- a/app/javascript/flavours/glitch/initial_state.js
+++ b/app/javascript/flavours/glitch/initial_state.js
@@ -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
diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index b47155c5f6..f0c4c804b7 100644
--- a/app/javascript/flavours/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -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;
}
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 80b0598a50..84aca2ebc0 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -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;
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 054110e410..7cc6f5386b 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -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 {
diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss
index 947a5d3aed..88fa3ffa07 100644
--- a/app/javascript/flavours/glitch/styles/statuses.scss
+++ b/app/javascript/flavours/glitch/styles/statuses.scss
@@ -268,7 +268,7 @@ a.button.logo-button {
border: 0;
background: transparent;
padding: 0;
- padding-top: 8px;
+ padding-top: 16px;
text-decoration: none;
&:hover,