| @@ -92,22 +92,6 @@ module StatusesHelper | |||
| end | |||
| end | |||
| def rtl_status?(status) | |||
| status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) | |||
| end | |||
| def rtl?(text) | |||
| text = simplified_text(text) | |||
| rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) | |||
| if rtl_words.present? | |||
| total_size = text.size.to_f | |||
| rtl_size(rtl_words) / total_size > 0.3 | |||
| else | |||
| false | |||
| end | |||
| end | |||
| def fa_visibility_icon(status) | |||
| case status.visibility | |||
| when 'public' | |||
| @@ -143,10 +127,6 @@ module StatusesHelper | |||
| end | |||
| end | |||
| def rtl_size(words) | |||
| words.reduce(0) { |acc, elem| acc + elem.size }.to_f | |||
| end | |||
| def embedded_view? | |||
| params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION | |||
| end | |||
| @@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji'; | |||
| import AutosuggestHashtag from './autosuggest_hashtag'; | |||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
| import PropTypes from 'prop-types'; | |||
| import { isRtl } from '../rtl'; | |||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
| import classNames from 'classnames'; | |||
| import { List as ImmutableList } from 'immutable'; | |||
| @@ -189,11 +188,6 @@ export default class AutosuggestInput extends ImmutablePureComponent { | |||
| render () { | |||
| const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; | |||
| const { suggestionsHidden } = this.state; | |||
| const style = { direction: 'ltr' }; | |||
| if (isRtl(value)) { | |||
| style.direction = 'rtl'; | |||
| } | |||
| return ( | |||
| <div className='autosuggest-input'> | |||
| @@ -212,7 +206,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { | |||
| onKeyUp={onKeyUp} | |||
| onFocus={this.onFocus} | |||
| onBlur={this.onBlur} | |||
| style={style} | |||
| dir='auto' | |||
| aria-autocomplete='list' | |||
| id={id} | |||
| className={className} | |||
| @@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji'; | |||
| import AutosuggestHashtag from './autosuggest_hashtag'; | |||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
| import PropTypes from 'prop-types'; | |||
| import { isRtl } from '../rtl'; | |||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
| import Textarea from 'react-textarea-autosize'; | |||
| import classNames from 'classnames'; | |||
| @@ -195,11 +194,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||
| render () { | |||
| const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; | |||
| const { suggestionsHidden } = this.state; | |||
| const style = { direction: 'ltr' }; | |||
| if (isRtl(value)) { | |||
| style.direction = 'rtl'; | |||
| } | |||
| return [ | |||
| <div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'> | |||
| @@ -220,7 +214,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||
| onFocus={this.onFocus} | |||
| onBlur={this.onBlur} | |||
| onPaste={this.onPaste} | |||
| style={style} | |||
| dir='auto' | |||
| aria-autocomplete='list' | |||
| /> | |||
| </label> | |||
| @@ -1,7 +1,6 @@ | |||
| import React from 'react'; | |||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
| import PropTypes from 'prop-types'; | |||
| import { isRtl } from '../rtl'; | |||
| import { FormattedMessage } from 'react-intl'; | |||
| import Permalink from './permalink'; | |||
| import classnames from 'classnames'; | |||
| @@ -186,17 +185,12 @@ export default class StatusContent extends React.PureComponent { | |||
| const content = { __html: status.get('contentHtml') }; | |||
| const spoilerContent = { __html: status.get('spoilerHtml') }; | |||
| const directionStyle = { direction: 'ltr' }; | |||
| const classNames = classnames('status__content', { | |||
| 'status__content--with-action': this.props.onClick && this.context.router, | |||
| 'status__content--with-spoiler': status.get('spoiler_text').length > 0, | |||
| 'status__content--collapsed': renderReadMore, | |||
| }); | |||
| if (isRtl(status.get('search_index'))) { | |||
| directionStyle.direction = 'rtl'; | |||
| } | |||
| const showThreadButton = ( | |||
| <button className='status__content__read-more-button' onClick={this.props.onClick}> | |||
| <FormattedMessage id='status.show_thread' defaultMessage='Show thread' /> | |||
| @@ -225,7 +219,7 @@ export default class StatusContent extends React.PureComponent { | |||
| } | |||
| return ( | |||
| <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> | |||
| <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> | |||
| <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> | |||
| <span dangerouslySetInnerHTML={spoilerContent} /> | |||
| {' '} | |||
| @@ -234,7 +228,7 @@ export default class StatusContent extends React.PureComponent { | |||
| {mentionsPlaceholder} | |||
| <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> | |||
| <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} dangerouslySetInnerHTML={content} /> | |||
| {!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />} | |||
| @@ -243,8 +237,8 @@ export default class StatusContent extends React.PureComponent { | |||
| ); | |||
| } else if (this.props.onClick) { | |||
| const output = [ | |||
| <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> | |||
| <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> | |||
| <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> | |||
| <div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} /> | |||
| {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} | |||
| @@ -259,8 +253,8 @@ export default class StatusContent extends React.PureComponent { | |||
| return output; | |||
| } else { | |||
| return ( | |||
| <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}> | |||
| <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> | |||
| <div className={classNames} ref={this.setRef} tabIndex='0'> | |||
| <div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} /> | |||
| {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} | |||
| @@ -6,7 +6,6 @@ import IconButton from '../../../components/icon_button'; | |||
| import DisplayName from '../../../components/display_name'; | |||
| import { defineMessages, injectIntl } from 'react-intl'; | |||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
| import { isRtl } from '../../../rtl'; | |||
| import AttachmentList from 'mastodon/components/attachment_list'; | |||
| const messages = defineMessages({ | |||
| @@ -45,9 +44,6 @@ class ReplyIndicator extends ImmutablePureComponent { | |||
| } | |||
| const content = { __html: status.get('contentHtml') }; | |||
| const style = { | |||
| direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr', | |||
| }; | |||
| return ( | |||
| <div className='reply-indicator'> | |||
| @@ -60,7 +56,7 @@ class ReplyIndicator extends ImmutablePureComponent { | |||
| </a> | |||
| </div> | |||
| <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} /> | |||
| <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> | |||
| {status.get('media_attachments').size > 0 && ( | |||
| <AttachmentList | |||
| @@ -1,32 +0,0 @@ | |||
| // U+0590 to U+05FF - Hebrew | |||
| // U+0600 to U+06FF - Arabic | |||
| // U+0700 to U+074F - Syriac | |||
| // U+0750 to U+077F - Arabic Supplement | |||
| // U+0780 to U+07BF - Thaana | |||
| // U+07C0 to U+07FF - N'Ko | |||
| // U+0800 to U+083F - Samaritan | |||
| // U+08A0 to U+08FF - Arabic Extended-A | |||
| // U+FB1D to U+FB4F - Hebrew presentation forms | |||
| // U+FB50 to U+FDFF - Arabic presentation forms A | |||
| // U+FE70 to U+FEFF - Arabic presentation forms B | |||
| const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; | |||
| export function isRtl(text) { | |||
| if (text.length === 0) { | |||
| return false; | |||
| } | |||
| text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, ''); | |||
| text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, ''); | |||
| text = text.replace(/\s+/g, ''); | |||
| text = text.replace(/(\w\S+\.\w{2,}\S*)/g, ''); | |||
| const matches = text.match(rtlChars); | |||
| if (!matches) { | |||
| return false; | |||
| } | |||
| return matches.length / text.length > 0.3; | |||
| }; | |||
| @@ -58,6 +58,16 @@ td { | |||
| vertical-align: top; | |||
| } | |||
| .auto-dir { | |||
| p { | |||
| unicode-bidi: plaintext; | |||
| } | |||
| a { | |||
| unicode-bidi: isolate; | |||
| } | |||
| } | |||
| .email-table, | |||
| .content-section, | |||
| .column, | |||
| @@ -96,7 +106,7 @@ body { | |||
| .col-3, | |||
| .col-4, | |||
| .col-5, | |||
| .col-6, { | |||
| .col-6 { | |||
| font-size: 0; | |||
| display: inline-block; | |||
| width: 100%; | |||
| @@ -831,6 +831,7 @@ | |||
| p { | |||
| margin-bottom: 20px; | |||
| white-space: pre-wrap; | |||
| unicode-bidi: plaintext; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| @@ -840,6 +841,7 @@ | |||
| a { | |||
| color: $secondary-text-color; | |||
| text-decoration: none; | |||
| unicode-bidi: isolate; | |||
| &:hover { | |||
| text-decoration: underline; | |||
| @@ -26,11 +26,11 @@ | |||
| = "@#{status.account.acct}" | |||
| - if status.spoiler_text? | |||
| %div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } | |||
| %div.auto-dir | |||
| %p | |||
| = Formatter.instance.format_spoiler(status) | |||
| %div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } | |||
| %div.auto-dir | |||
| = Formatter.instance.format(status) | |||
| - if status.media_attachments.size > 0 | |||
| @@ -20,7 +20,7 @@ | |||
| %p< | |||
| %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)} | |||
| %button.status__content__spoiler-link= t('statuses.show_more') | |||
| .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } | |||
| .e-content | |||
| = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) | |||
| - if status.preloadable_poll | |||
| = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do | |||
| @@ -29,7 +29,7 @@ | |||
| %p< | |||
| %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)} | |||
| %button.status__content__spoiler-link= t('statuses.show_more') | |||
| .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } | |||
| .e-content | |||
| = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) | |||
| - if status.preloadable_poll | |||
| = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do | |||
| @@ -149,22 +149,4 @@ RSpec.describe StatusesHelper, type: :helper do | |||
| expect(css_class).to eq 'h-cite' | |||
| end | |||
| end | |||
| describe '#rtl?' do | |||
| it 'is false if text is empty' do | |||
| expect(helper).not_to be_rtl '' | |||
| end | |||
| it 'is false if there are no right to left characters' do | |||
| expect(helper).not_to be_rtl 'hello world' | |||
| end | |||
| it 'is false if right to left characters are fewer than 1/3 of total text' do | |||
| expect(helper).not_to be_rtl 'hello ݟ world' | |||
| end | |||
| it 'is true if right to left characters are greater than 1/3 of total text' do | |||
| expect(helper).to be_rtl 'aaݟaaݟ' | |||
| end | |||
| end | |||
| end | |||