diff --git a/.rubocop.yml b/.rubocop.yml index 1cbdadd49..ae3697174 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -27,6 +27,7 @@ Metrics/AbcSize: Max: 100 Metrics/BlockLength: + Max: 35 Exclude: - 'lib/tasks/**/*' @@ -35,10 +36,10 @@ Metrics/BlockNesting: Metrics/ClassLength: CountComments: false - Max: 200 + Max: 300 Metrics/CyclomaticComplexity: - Max: 15 + Max: 25 Metrics/LineLength: AllowURI: true @@ -53,11 +54,11 @@ Metrics/ModuleLength: Max: 200 Metrics/ParameterLists: - Max: 4 + Max: 5 CountKeywordArgs: true Metrics/PerceivedComplexity: - Max: 10 + Max: 20 Rails: Enabled: true diff --git a/Aptfile b/Aptfile index f89f74bd4..5e922ca18 100644 --- a/Aptfile +++ b/Aptfile @@ -1,3 +1,4 @@ +libpq-dev protobuf-compiler libprotobuf-dev ffmpeg diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index ba2736d7a..8a60c4192 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -9,16 +9,19 @@ export default class ColumnBackButton extends React.PureComponent { }; handleClick = () => { - if (window.history && window.history.length === 1) this.context.router.history.push('/'); - else this.context.router.history.goBack(); + if (window.history && window.history.length === 1) { + this.context.router.history.push('/'); + } else { + this.context.router.history.goBack(); + } } render () { return ( -
+
+ ); } diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index f62a75183..28631f463 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -134,7 +134,7 @@ export default class DropdownMenu extends React.PureComponent { return ( - + diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index febdabbc0..3e5f8ac8c 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -13,6 +13,7 @@ export default class IconButton extends React.PureComponent { size: PropTypes.number, active: PropTypes.bool, pressed: PropTypes.bool, + expanded: PropTypes.bool, style: PropTypes.object, activeStyle: PropTypes.object, disabled: PropTypes.bool, @@ -77,6 +78,7 @@ export default class IconButton extends React.PureComponent { ); } else { const size = media.take(4).size; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 5471c52f7..ceb512d96 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -172,7 +172,7 @@ export default class Status extends ImmutablePureComponent { // Exclude intersectionObserverWrapper from `other` variable // because intersection is managed in here. - const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props; + const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props; const { isExpanded, isIntersecting, isHidden } = this.state; if (status === null) { @@ -234,7 +234,7 @@ export default class Status extends ImmutablePureComponent { } return ( -
+
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 5c83d626e..0d8c9add4 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -151,8 +151,8 @@ export default class StatusActionBar extends ImmutablePureComponent { return (
- - + + {shareButton}
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 1b803a22e..fdf7aa531 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -146,7 +146,7 @@ export default class StatusContent extends React.PureComponent { } return ( -
+
+
); } else if (this.props.onClick) { return (
{ switch (e.key) { case 'PageDown': - return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].nextElementSibling; + return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling; case 'PageUp': - return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].previousElementSibling; + return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling; case 'End': return this.node.querySelector('[role="feed"] > article:last-of-type'); case 'Home': diff --git a/app/javascript/mastodon/features/compose/components/character_counter.js b/app/javascript/mastodon/features/compose/components/character_counter.js index 6c488b661..0ecfc9141 100644 --- a/app/javascript/mastodon/features/compose/components/character_counter.js +++ b/app/javascript/mastodon/features/compose/components/character_counter.js @@ -13,12 +13,12 @@ export default class CharacterCounter extends React.PureComponent { if (diff < 0) { return {diff}; } + return {diff}; } render () { const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); } diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 170fb0f28..f3320a42b 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -18,6 +18,7 @@ import WarningContainer from '../containers/warning_container'; import { isMobile } from '../../../is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; +import { countableText } from '../util/counter'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -145,9 +146,9 @@ export default class ComposeForm extends ImmutablePureComponent { render () { const { intl, onPaste, showSearch } = this.props; const disabled = this.props.is_submitting; - const text = [this.props.spoiler_text, this.props.text].join(''); + const text = [this.props.spoiler_text, countableText(this.props.text)].join(''); - let publishText = ''; + let publishText = ''; if (this.props.privacy === 'private' || this.props.privacy === 'direct') { publishText = {intl.formatMessage(messages.publish)}; @@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
-
+
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index b0f3b30fc..9d05b7a34 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -124,7 +124,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { return ( - + 🙂 -
+
{open && this.options.map(item =>
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js index 63c0e8ae4..8624849f3 100644 --- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -15,6 +15,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ visible: state.getIn(['compose', 'media_attachments']).size > 0, active: state.getIn(['compose', 'sensitive']), + disabled: state.getIn(['compose', 'spoiler']), }); const mapDispatchToProps = dispatch => ({ @@ -30,12 +31,13 @@ class SensitiveButton extends React.PureComponent { static propTypes = { visible: PropTypes.bool, active: PropTypes.bool, + disabled: PropTypes.bool, onClick: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; render () { - const { visible, active, onClick, intl } = this.props; + const { visible, active, disabled, onClick, intl } = this.props; return ( @@ -53,6 +55,7 @@ class SensitiveButton extends React.PureComponent { onClick={onClick} size={18} active={active} + disabled={disabled} style={{ lineHeight: null, height: null }} inverted /> diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js new file mode 100644 index 000000000..f0fea1a0e --- /dev/null +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -0,0 +1,7 @@ +const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; + +export function countableText(inputText) { + return inputText + .replace(/https?:\/\/\S+/g, urlPlaceholder) + .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2'); +}; diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index dcc9becd3..828419d5a 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -10,6 +10,8 @@ import ImageLoader from './image_loader'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, + previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, + next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); @injectIntl @@ -66,16 +68,10 @@ export default class MediaModal extends ImmutablePureComponent { const index = this.getIndex(); - let leftNav, rightNav, content; + const leftNav = media.size > 1 && ; + const rightNav = media.size > 1 && ; - leftNav = rightNav = content = ''; - - if (media.size > 1) { - leftNav =
; - rightNav =
; - } - - content = media.map((image) => { + const content = media.map((image) => { const width = image.getIn(['meta', 'original', 'width']) || null; const height = image.getIn(['meta', 'original', 'height']) || null; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 3ca19e4d5..5b598bddf 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -53,7 +53,7 @@ export default class ModalRoot extends React.PureComponent { } componentDidUpdate (prevProps) { - if (!this.type && !!prevProps.type) { + if (!this.props.type && !!prevProps.type) { this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); this.activeElement.focus(); this.activeElement = null; diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 4d488f82d..af9e6bf45 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import NavLink from 'react-router-dom/NavLink'; import { FormattedMessage, injectIntl } from 'react-intl'; +import { debounce } from 'lodash'; +import { isUserTouching } from '../../../is_mobile'; export const links = [ , @@ -25,16 +27,56 @@ export function getLink (index) { @injectIntl export default class TabsBar extends React.Component { + static contextTypes = { + router: PropTypes.object.isRequired, + } + static propTypes = { intl: PropTypes.object.isRequired, } + setRef = ref => { + this.node = ref; + } + + handleClick = (e) => { + // Only apply optimization for touch devices, which we assume are slower + // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices + if (isUserTouching()) { + e.preventDefault(); + e.persist(); + + requestAnimationFrame(() => { + const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); + const currentTab = tabs.find(tab => tab.classList.contains('active')); + const nextTab = tabs.find(tab => tab.contains(e.target)); + const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; + + + if (currentTab !== nextTab) { + if (currentTab) { + currentTab.classList.remove('active'); + } + + const listener = debounce(() => { + nextTab.removeEventListener('transitionend', listener); + this.context.router.history.push(to); + }, 50); + + nextTab.addEventListener('transitionend', listener); + nextTab.classList.add('active'); + } + }); + } + + } + render () { const { intl: { formatMessage } } = this.props; return ( -