Merge pull request #1983 from ClearlyClaire/glitch-soc/features/translation
Port “Translate” feature from upstream
This commit is contained in:
		
						commit
						fc0e11abdb
					
				
					 11 changed files with 152 additions and 17 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_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 a new issue