Remove react-motion library (#34293)
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { TransitionMotion, spring } from 'react-motion';
 | 
			
		||||
import { animated, useSpring, config } from '@react-spring/web';
 | 
			
		||||
 | 
			
		||||
import { reduceMotion } from '../initial_state';
 | 
			
		||||
 | 
			
		||||
@@ -11,53 +11,49 @@ interface Props {
 | 
			
		||||
}
 | 
			
		||||
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
 | 
			
		||||
  const [previousValue, setPreviousValue] = useState(value);
 | 
			
		||||
  const [direction, setDirection] = useState<1 | -1>(1);
 | 
			
		||||
  const direction = value > previousValue ? -1 : 1;
 | 
			
		||||
 | 
			
		||||
  if (previousValue !== value) {
 | 
			
		||||
  const [styles, api] = useSpring(
 | 
			
		||||
    () => ({
 | 
			
		||||
      from: { transform: `translateY(${100 * direction}%)` },
 | 
			
		||||
      to: { transform: 'translateY(0%)' },
 | 
			
		||||
      onRest() {
 | 
			
		||||
        setPreviousValue(value);
 | 
			
		||||
    setDirection(value > previousValue ? 1 : -1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
 | 
			
		||||
  const willLeave = useCallback(
 | 
			
		||||
    () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
 | 
			
		||||
    [direction],
 | 
			
		||||
      },
 | 
			
		||||
      config: { ...config.gentle, duration: 200 },
 | 
			
		||||
      immediate: true, // This ensures that the animation is not played when the component is first rendered
 | 
			
		||||
    }),
 | 
			
		||||
    [value, previousValue],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // When the value changes, start the animation
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (value !== previousValue) {
 | 
			
		||||
      void api.start({ reset: true });
 | 
			
		||||
    }
 | 
			
		||||
  }, [api, previousValue, value]);
 | 
			
		||||
 | 
			
		||||
  if (reduceMotion) {
 | 
			
		||||
    return <ShortNumber value={value} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const styles = [
 | 
			
		||||
    {
 | 
			
		||||
      key: `${value}`,
 | 
			
		||||
      data: value,
 | 
			
		||||
      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TransitionMotion
 | 
			
		||||
      styles={styles}
 | 
			
		||||
      willEnter={willEnter}
 | 
			
		||||
      willLeave={willLeave}
 | 
			
		||||
    >
 | 
			
		||||
      {(items) => (
 | 
			
		||||
    <span className='animated-number'>
 | 
			
		||||
          {items.map(({ key, data, style }) => (
 | 
			
		||||
            <span
 | 
			
		||||
              key={key}
 | 
			
		||||
      <animated.span style={styles}>
 | 
			
		||||
        <ShortNumber value={value} />
 | 
			
		||||
      </animated.span>
 | 
			
		||||
      {value !== previousValue && (
 | 
			
		||||
        <animated.span
 | 
			
		||||
          style={{
 | 
			
		||||
                position:
 | 
			
		||||
                  direction * (style.y ?? 0) > 0 ? 'absolute' : 'static',
 | 
			
		||||
                transform: `translateY(${(style.y ?? 0) * 100}%)`,
 | 
			
		||||
            ...styles,
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: `${-100 * direction}%`, // Adds extra space on top of translateY
 | 
			
		||||
          }}
 | 
			
		||||
          role='presentation'
 | 
			
		||||
        >
 | 
			
		||||
              <ShortNumber value={data as number} />
 | 
			
		||||
            </span>
 | 
			
		||||
          ))}
 | 
			
		||||
        </span>
 | 
			
		||||
          <ShortNumber value={previousValue} />
 | 
			
		||||
        </animated.span>
 | 
			
		||||
      )}
 | 
			
		||||
    </TransitionMotion>
 | 
			
		||||
    </span>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,248 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
 | 
			
		||||
import escapeTextContentForBrowser from 'escape-html';
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
 | 
			
		||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
 | 
			
		||||
import { Icon }  from 'mastodon/components/icon';
 | 
			
		||||
import emojify from 'mastodon/features/emoji/emoji';
 | 
			
		||||
import Motion from 'mastodon/features/ui/util/optional_motion';
 | 
			
		||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
 | 
			
		||||
 | 
			
		||||
import { RelativeTimestamp } from './relative_timestamp';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  closed: {
 | 
			
		||||
    id: 'poll.closed',
 | 
			
		||||
    defaultMessage: 'Closed',
 | 
			
		||||
  },
 | 
			
		||||
  voted: {
 | 
			
		||||
    id: 'poll.voted',
 | 
			
		||||
    defaultMessage: 'You voted for this answer',
 | 
			
		||||
  },
 | 
			
		||||
  votes: {
 | 
			
		||||
    id: 'poll.votes',
 | 
			
		||||
    defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Poll extends ImmutablePureComponent {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    identity: identityContextPropShape,
 | 
			
		||||
    poll: ImmutablePropTypes.record.isRequired,
 | 
			
		||||
    status: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    lang: PropTypes.string,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    disabled: PropTypes.bool,
 | 
			
		||||
    refresh: PropTypes.func,
 | 
			
		||||
    onVote: PropTypes.func,
 | 
			
		||||
    onInteractionModal: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    selected: {},
 | 
			
		||||
    expired: null,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static getDerivedStateFromProps (props, state) {
 | 
			
		||||
    const { poll } = props;
 | 
			
		||||
    const expires_at = poll.get('expires_at');
 | 
			
		||||
    const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
 | 
			
		||||
    return (expired === state.expired) ? null : { expired };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    this._setupTimer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate () {
 | 
			
		||||
    this._setupTimer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    clearTimeout(this._timer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _setupTimer () {
 | 
			
		||||
    const { poll } = this.props;
 | 
			
		||||
    clearTimeout(this._timer);
 | 
			
		||||
    if (!this.state.expired) {
 | 
			
		||||
      const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
 | 
			
		||||
      this._timer = setTimeout(() => {
 | 
			
		||||
        this.setState({ expired: true });
 | 
			
		||||
      }, delay);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _toggleOption = value => {
 | 
			
		||||
    if (this.props.poll.get('multiple')) {
 | 
			
		||||
      const tmp = { ...this.state.selected };
 | 
			
		||||
      if (tmp[value]) {
 | 
			
		||||
        delete tmp[value];
 | 
			
		||||
      } else {
 | 
			
		||||
        tmp[value] = true;
 | 
			
		||||
      }
 | 
			
		||||
      this.setState({ selected: tmp });
 | 
			
		||||
    } else {
 | 
			
		||||
      const tmp = {};
 | 
			
		||||
      tmp[value] = true;
 | 
			
		||||
      this.setState({ selected: tmp });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleOptionChange = ({ target: { value } }) => {
 | 
			
		||||
    this._toggleOption(value);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleOptionKeyPress = (e) => {
 | 
			
		||||
    if (e.key === 'Enter' || e.key === ' ') {
 | 
			
		||||
      this._toggleOption(e.target.getAttribute('data-index'));
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleVote = () => {
 | 
			
		||||
    if (this.props.disabled) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.props.identity.signedIn) {
 | 
			
		||||
      this.props.onVote(Object.keys(this.state.selected));
 | 
			
		||||
    } else {
 | 
			
		||||
      this.props.onInteractionModal('vote', this.props.status);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleRefresh = () => {
 | 
			
		||||
    if (this.props.disabled) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.props.refresh();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleReveal = () => {
 | 
			
		||||
    this.setState({ revealed: true });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderOption (option, optionIndex, showResults) {
 | 
			
		||||
    const { poll, lang, disabled, intl } = this.props;
 | 
			
		||||
    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');
 | 
			
		||||
    const percent         = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
 | 
			
		||||
    const leading         = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
 | 
			
		||||
    const active          = !!this.state.selected[`${optionIndex}`];
 | 
			
		||||
    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
 | 
			
		||||
 | 
			
		||||
    const title = option.getIn(['translation', 'title']) || option.get('title');
 | 
			
		||||
    let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
 | 
			
		||||
 | 
			
		||||
    if (!titleHtml) {
 | 
			
		||||
      const emojiMap = emojiMap(poll);
 | 
			
		||||
      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <li key={option.get('title')}>
 | 
			
		||||
        <label className={classNames('poll__option', { selectable: !showResults })}>
 | 
			
		||||
          <input
 | 
			
		||||
            name='vote-options'
 | 
			
		||||
            type={poll.get('multiple') ? 'checkbox' : 'radio'}
 | 
			
		||||
            value={optionIndex}
 | 
			
		||||
            checked={active}
 | 
			
		||||
            onChange={this.handleOptionChange}
 | 
			
		||||
            disabled={disabled}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          {!showResults && (
 | 
			
		||||
            <span
 | 
			
		||||
              className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
 | 
			
		||||
              tabIndex={0}
 | 
			
		||||
              role={poll.get('multiple') ? 'checkbox' : 'radio'}
 | 
			
		||||
              onKeyPress={this.handleOptionKeyPress}
 | 
			
		||||
              aria-checked={active}
 | 
			
		||||
              aria-label={title}
 | 
			
		||||
              lang={lang}
 | 
			
		||||
              data-index={optionIndex}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {showResults && (
 | 
			
		||||
            <span
 | 
			
		||||
              className='poll__number'
 | 
			
		||||
              title={intl.formatMessage(messages.votes, {
 | 
			
		||||
                votes: option.get('votes_count'),
 | 
			
		||||
              })}
 | 
			
		||||
            >
 | 
			
		||||
              {Math.round(percent)}%
 | 
			
		||||
            </span>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <span
 | 
			
		||||
            className='poll__option__text translate'
 | 
			
		||||
            lang={lang}
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: titleHtml }}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          {!!voted && <span className='poll__voted'>
 | 
			
		||||
            <Icon id='check' icon={CheckIcon} className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
 | 
			
		||||
          </span>}
 | 
			
		||||
        </label>
 | 
			
		||||
 | 
			
		||||
        {showResults && (
 | 
			
		||||
          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
 | 
			
		||||
            {({ width }) =>
 | 
			
		||||
              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
 | 
			
		||||
            }
 | 
			
		||||
          </Motion>
 | 
			
		||||
        )}
 | 
			
		||||
      </li>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { poll, intl } = this.props;
 | 
			
		||||
    const { revealed, expired } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (!poll) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
 | 
			
		||||
    const showResults   = poll.get('voted') || revealed || expired;
 | 
			
		||||
    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
 | 
			
		||||
 | 
			
		||||
    let votesCount = null;
 | 
			
		||||
 | 
			
		||||
    if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
 | 
			
		||||
      votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
 | 
			
		||||
    } else {
 | 
			
		||||
      votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='poll'>
 | 
			
		||||
        <ul>
 | 
			
		||||
          {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
        <div className='poll__footer'>
 | 
			
		||||
          {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
 | 
			
		||||
          {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
 | 
			
		||||
          {showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
 | 
			
		||||
          {votesCount}
 | 
			
		||||
          {poll.get('expires_at') && <> · {timeRemaining}</>}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default injectIntl(withIdentity(Poll));
 | 
			
		||||
							
								
								
									
										352
									
								
								app/javascript/mastodon/components/poll.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								app/javascript/mastodon/components/poll.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,352 @@
 | 
			
		||||
import type { KeyboardEventHandler } from 'react';
 | 
			
		||||
import { useCallback, useMemo, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import { animated, useSpring } from '@react-spring/web';
 | 
			
		||||
import escapeTextContentForBrowser from 'escape-html';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import emojify from 'mastodon/features/emoji/emoji';
 | 
			
		||||
import { useIdentity } from 'mastodon/identity_context';
 | 
			
		||||
import { reduceMotion } from 'mastodon/initial_state';
 | 
			
		||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
 | 
			
		||||
import type * as Model from 'mastodon/models/poll';
 | 
			
		||||
import type { Status } from 'mastodon/models/status';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import { RelativeTimestamp } from './relative_timestamp';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  closed: {
 | 
			
		||||
    id: 'poll.closed',
 | 
			
		||||
    defaultMessage: 'Closed',
 | 
			
		||||
  },
 | 
			
		||||
  voted: {
 | 
			
		||||
    id: 'poll.voted',
 | 
			
		||||
    defaultMessage: 'You voted for this answer',
 | 
			
		||||
  },
 | 
			
		||||
  votes: {
 | 
			
		||||
    id: 'poll.votes',
 | 
			
		||||
    defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface PollProps {
 | 
			
		||||
  pollId: string;
 | 
			
		||||
  status: Status;
 | 
			
		||||
  lang?: string;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Poll: React.FC<PollProps> = (props) => {
 | 
			
		||||
  const { pollId, status } = props;
 | 
			
		||||
 | 
			
		||||
  // Third party hooks
 | 
			
		||||
  const poll = useAppSelector((state) => state.polls.get(pollId));
 | 
			
		||||
  const identity = useIdentity();
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
  // State
 | 
			
		||||
  const [revealed, setRevealed] = useState(false);
 | 
			
		||||
  const [selected, setSelected] = useState<Record<string, boolean>>({});
 | 
			
		||||
 | 
			
		||||
  // Derived values
 | 
			
		||||
  const expired = useMemo(() => {
 | 
			
		||||
    if (!poll) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const expiresAt = poll.get('expires_at');
 | 
			
		||||
    return poll.get('expired') || new Date(expiresAt).getTime() < Date.now();
 | 
			
		||||
  }, [poll]);
 | 
			
		||||
  const timeRemaining = useMemo(() => {
 | 
			
		||||
    if (!poll) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    if (expired) {
 | 
			
		||||
      return intl.formatMessage(messages.closed);
 | 
			
		||||
    }
 | 
			
		||||
    return <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
 | 
			
		||||
  }, [expired, intl, poll]);
 | 
			
		||||
  const votesCount = useMemo(() => {
 | 
			
		||||
    if (!poll) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    if (poll.get('voters_count')) {
 | 
			
		||||
      return (
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='poll.total_people'
 | 
			
		||||
          defaultMessage='{count, plural, one {# person} other {# people}}'
 | 
			
		||||
          values={{ count: poll.get('voters_count') }}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='poll.total_votes'
 | 
			
		||||
        defaultMessage='{count, plural, one {# vote} other {# votes}}'
 | 
			
		||||
        values={{ count: poll.get('votes_count') }}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }, [poll]);
 | 
			
		||||
 | 
			
		||||
  const disabled =
 | 
			
		||||
    props.disabled || Object.values(selected).every((item) => !item);
 | 
			
		||||
 | 
			
		||||
  // Event handlers
 | 
			
		||||
  const handleVote = useCallback(() => {
 | 
			
		||||
    if (disabled) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (identity.signedIn) {
 | 
			
		||||
      void dispatch(vote({ pollId, choices: Object.keys(selected) }));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        openModal({
 | 
			
		||||
          modalType: 'INTERACTION',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            type: 'vote',
 | 
			
		||||
            accountId: status.getIn(['account', 'id']),
 | 
			
		||||
            url: status.get('uri'),
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }, [disabled, dispatch, identity, pollId, selected, status]);
 | 
			
		||||
 | 
			
		||||
  const handleReveal = useCallback(() => {
 | 
			
		||||
    setRevealed(true);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleRefresh = useCallback(() => {
 | 
			
		||||
    if (disabled) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    debounce(
 | 
			
		||||
      () => {
 | 
			
		||||
        void dispatch(fetchPoll({ pollId }));
 | 
			
		||||
      },
 | 
			
		||||
      1000,
 | 
			
		||||
      { leading: true },
 | 
			
		||||
    );
 | 
			
		||||
  }, [disabled, dispatch, pollId]);
 | 
			
		||||
 | 
			
		||||
  const handleOptionChange = useCallback(
 | 
			
		||||
    (choiceIndex: number) => {
 | 
			
		||||
      if (!poll) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (poll.get('multiple')) {
 | 
			
		||||
        setSelected((prev) => ({
 | 
			
		||||
          ...prev,
 | 
			
		||||
          [choiceIndex]: !prev[choiceIndex],
 | 
			
		||||
        }));
 | 
			
		||||
      } else {
 | 
			
		||||
        setSelected({ [choiceIndex]: true });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [poll],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!poll) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  const showResults = poll.get('voted') || revealed || expired;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='poll'>
 | 
			
		||||
      <ul>
 | 
			
		||||
        {poll.get('options').map((option, i) => (
 | 
			
		||||
          <PollOption
 | 
			
		||||
            key={option.get('title') || i}
 | 
			
		||||
            index={i}
 | 
			
		||||
            poll={poll}
 | 
			
		||||
            option={option}
 | 
			
		||||
            showResults={showResults}
 | 
			
		||||
            active={!!selected[i]}
 | 
			
		||||
            onChange={handleOptionChange}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ul>
 | 
			
		||||
 | 
			
		||||
      <div className='poll__footer'>
 | 
			
		||||
        {!showResults && (
 | 
			
		||||
          <button
 | 
			
		||||
            className='button button-secondary'
 | 
			
		||||
            disabled={disabled}
 | 
			
		||||
            onClick={handleVote}
 | 
			
		||||
          >
 | 
			
		||||
            <FormattedMessage id='poll.vote' defaultMessage='Vote' />
 | 
			
		||||
          </button>
 | 
			
		||||
        )}
 | 
			
		||||
        {!showResults && (
 | 
			
		||||
          <>
 | 
			
		||||
            <button className='poll__link' onClick={handleReveal}>
 | 
			
		||||
              <FormattedMessage id='poll.reveal' defaultMessage='See results' />
 | 
			
		||||
            </button>{' '}
 | 
			
		||||
            ·{' '}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        {showResults && !disabled && (
 | 
			
		||||
          <>
 | 
			
		||||
            <button className='poll__link' onClick={handleRefresh}>
 | 
			
		||||
              <FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
 | 
			
		||||
            </button>{' '}
 | 
			
		||||
            ·{' '}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        {votesCount}
 | 
			
		||||
        {poll.get('expires_at') && <> · {timeRemaining}</>}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type PollOptionProps = Pick<PollProps, 'disabled' | 'lang'> & {
 | 
			
		||||
  active: boolean;
 | 
			
		||||
  onChange: (index: number) => void;
 | 
			
		||||
  poll: Model.Poll;
 | 
			
		||||
  option: Model.PollOption;
 | 
			
		||||
  index: number;
 | 
			
		||||
  showResults?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const PollOption: React.FC<PollOptionProps> = (props) => {
 | 
			
		||||
  const { active, lang, disabled, poll, option, index, showResults, onChange } =
 | 
			
		||||
    props;
 | 
			
		||||
  const voted = option.get('voted') || poll.get('own_votes')?.includes(index);
 | 
			
		||||
  const title =
 | 
			
		||||
    (option.getIn(['translation', 'title']) as string) || option.get('title');
 | 
			
		||||
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
 | 
			
		||||
  // Derived values
 | 
			
		||||
  const percent = useMemo(() => {
 | 
			
		||||
    const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
 | 
			
		||||
    return pollVotesCount === 0
 | 
			
		||||
      ? 0
 | 
			
		||||
      : (option.get('votes_count') / pollVotesCount) * 100;
 | 
			
		||||
  }, [option, poll]);
 | 
			
		||||
  const isLeading = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      poll
 | 
			
		||||
        .get('options')
 | 
			
		||||
        .filterNot((other) => other.get('title') === option.get('title'))
 | 
			
		||||
        .every(
 | 
			
		||||
          (other) => option.get('votes_count') >= other.get('votes_count'),
 | 
			
		||||
        ),
 | 
			
		||||
    [poll, option],
 | 
			
		||||
  );
 | 
			
		||||
  const titleHtml = useMemo(() => {
 | 
			
		||||
    let titleHtml =
 | 
			
		||||
      (option.getIn(['translation', 'titleHtml']) as string) ||
 | 
			
		||||
      option.get('titleHtml');
 | 
			
		||||
 | 
			
		||||
    if (!titleHtml) {
 | 
			
		||||
      const emojiMap = makeEmojiMap(poll.get('emojis'));
 | 
			
		||||
      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return titleHtml;
 | 
			
		||||
  }, [option, poll, title]);
 | 
			
		||||
 | 
			
		||||
  // Handlers
 | 
			
		||||
  const handleOptionChange = useCallback(() => {
 | 
			
		||||
    onChange(index);
 | 
			
		||||
  }, [index, onChange]);
 | 
			
		||||
  const handleOptionKeyPress: KeyboardEventHandler = useCallback(
 | 
			
		||||
    (event) => {
 | 
			
		||||
      if (event.key === 'Enter' || event.key === ' ') {
 | 
			
		||||
        onChange(index);
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [index, onChange],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const widthSpring = useSpring({
 | 
			
		||||
    from: {
 | 
			
		||||
      width: '0%',
 | 
			
		||||
    },
 | 
			
		||||
    to: {
 | 
			
		||||
      width: `${percent}%`,
 | 
			
		||||
    },
 | 
			
		||||
    immediate: reduceMotion,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <li>
 | 
			
		||||
      <label
 | 
			
		||||
        className={classNames('poll__option', { selectable: !showResults })}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          name='vote-options'
 | 
			
		||||
          type={poll.get('multiple') ? 'checkbox' : 'radio'}
 | 
			
		||||
          value={index}
 | 
			
		||||
          checked={active}
 | 
			
		||||
          onChange={handleOptionChange}
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {!showResults && (
 | 
			
		||||
          <span
 | 
			
		||||
            className={classNames('poll__input', {
 | 
			
		||||
              checkbox: poll.get('multiple'),
 | 
			
		||||
              active,
 | 
			
		||||
            })}
 | 
			
		||||
            tabIndex={0}
 | 
			
		||||
            role={poll.get('multiple') ? 'checkbox' : 'radio'}
 | 
			
		||||
            onKeyDown={handleOptionKeyPress}
 | 
			
		||||
            aria-checked={active}
 | 
			
		||||
            aria-label={title}
 | 
			
		||||
            lang={lang}
 | 
			
		||||
            data-index={index}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {showResults && (
 | 
			
		||||
          <span
 | 
			
		||||
            className='poll__number'
 | 
			
		||||
            title={intl.formatMessage(messages.votes, {
 | 
			
		||||
              votes: option.get('votes_count'),
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
            {Math.round(percent)}%
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <span
 | 
			
		||||
          className='poll__option__text translate'
 | 
			
		||||
          lang={lang}
 | 
			
		||||
          dangerouslySetInnerHTML={{ __html: titleHtml }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {!!voted && (
 | 
			
		||||
          <span className='poll__voted'>
 | 
			
		||||
            <Icon
 | 
			
		||||
              id='check'
 | 
			
		||||
              icon={CheckIcon}
 | 
			
		||||
              className='poll__voted__mark'
 | 
			
		||||
              title={intl.formatMessage(messages.voted)}
 | 
			
		||||
            />
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
      </label>
 | 
			
		||||
 | 
			
		||||
      {showResults && (
 | 
			
		||||
        <animated.span
 | 
			
		||||
          className={classNames('poll__chart', { leading: isLeading })}
 | 
			
		||||
          style={widthSpring}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </li>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -11,7 +11,7 @@ import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 | 
			
		||||
import { Icon }  from 'mastodon/components/icon';
 | 
			
		||||
import PollContainer from 'mastodon/containers/poll_container';
 | 
			
		||||
import { Poll } from 'mastodon/components/poll';
 | 
			
		||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
 | 
			
		||||
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
@@ -245,7 +245,7 @@ class StatusContent extends PureComponent {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const poll = !!status.get('poll') && (
 | 
			
		||||
      <PollContainer pollId={status.get('poll')} status={status} lang={language} />
 | 
			
		||||
      <Poll pollId={status.get('poll')} status={status} lang={language} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (this.props.onClick) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import { fromJS } from 'immutable';
 | 
			
		||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 | 
			
		||||
import MediaGallery from 'mastodon/components/media_gallery';
 | 
			
		||||
import ModalRoot from 'mastodon/components/modal_root';
 | 
			
		||||
import Poll from 'mastodon/components/poll';
 | 
			
		||||
import { Poll } from 'mastodon/components/poll';
 | 
			
		||||
import Audio from 'mastodon/features/audio';
 | 
			
		||||
import Card from 'mastodon/features/status/components/card';
 | 
			
		||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
 | 
			
		||||
import Poll from 'mastodon/components/poll';
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = (dispatch, { pollId }) => ({
 | 
			
		||||
  refresh: debounce(
 | 
			
		||||
    () => {
 | 
			
		||||
      dispatch(fetchPoll({ pollId }));
 | 
			
		||||
    },
 | 
			
		||||
    1000,
 | 
			
		||||
    { leading: true },
 | 
			
		||||
  ),
 | 
			
		||||
 | 
			
		||||
  onVote (choices) {
 | 
			
		||||
    dispatch(vote({ pollId, choices }));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onInteractionModal (type, status) {
 | 
			
		||||
    dispatch(openModal({
 | 
			
		||||
      modalType: 'INTERACTION',
 | 
			
		||||
      modalProps: {
 | 
			
		||||
        type,
 | 
			
		||||
        accountId: status.getIn(['account', 'id']),
 | 
			
		||||
        url: status.get('uri'),
 | 
			
		||||
      },
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { pollId }) => ({
 | 
			
		||||
  poll: state.polls.get(pollId),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(Poll);
 | 
			
		||||
@@ -20,7 +20,6 @@ import PollButtonContainer from '../containers/poll_button_container';
 | 
			
		||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
 | 
			
		||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
 | 
			
		||||
import UploadButtonContainer from '../containers/upload_button_container';
 | 
			
		||||
import WarningContainer from '../containers/warning_container';
 | 
			
		||||
import { countableText } from '../util/counter';
 | 
			
		||||
 | 
			
		||||
import { CharacterCounter } from './character_counter';
 | 
			
		||||
@@ -30,6 +29,7 @@ import { NavigationBar } from './navigation_bar';
 | 
			
		||||
import { PollForm } from "./poll_form";
 | 
			
		||||
import { ReplyIndicator } from './reply_indicator';
 | 
			
		||||
import { UploadForm } from './upload_form';
 | 
			
		||||
import { Warning } from './warning';
 | 
			
		||||
 | 
			
		||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +233,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
			
		||||
      <form className='compose-form' onSubmit={this.handleSubmit}>
 | 
			
		||||
        <ReplyIndicator />
 | 
			
		||||
        {!withoutNavigation && <NavigationBar />}
 | 
			
		||||
        <WarningContainer />
 | 
			
		||||
        <Warning />
 | 
			
		||||
 | 
			
		||||
        <div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
 | 
			
		||||
          <div className='compose-form__scrollable'>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
 | 
			
		||||
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
 | 
			
		||||
import { Icon }  from 'mastodon/components/icon';
 | 
			
		||||
 | 
			
		||||
import Motion from '../../ui/util/optional_motion';
 | 
			
		||||
 | 
			
		||||
export const UploadProgress = ({ active, progress, isProcessing }) => {
 | 
			
		||||
  if (!active) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let message;
 | 
			
		||||
 | 
			
		||||
  if (isProcessing) {
 | 
			
		||||
    message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />;
 | 
			
		||||
  } else {
 | 
			
		||||
    message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='upload-progress'>
 | 
			
		||||
      <Icon id='upload' icon={UploadFileIcon} />
 | 
			
		||||
 | 
			
		||||
      <div className='upload-progress__message'>
 | 
			
		||||
        {message}
 | 
			
		||||
 | 
			
		||||
        <div className='upload-progress__backdrop'>
 | 
			
		||||
          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
 | 
			
		||||
            {({ width }) =>
 | 
			
		||||
              <div className='upload-progress__tracker' style={{ width: `${width}%` }} />
 | 
			
		||||
            }
 | 
			
		||||
          </Motion>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
UploadProgress.propTypes = {
 | 
			
		||||
  active: PropTypes.bool,
 | 
			
		||||
  progress: PropTypes.number,
 | 
			
		||||
  isProcessing: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { animated, useSpring } from '@react-spring/web';
 | 
			
		||||
 | 
			
		||||
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import { reduceMotion } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
interface UploadProgressProps {
 | 
			
		||||
  active: boolean;
 | 
			
		||||
  progress: number;
 | 
			
		||||
  isProcessing: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UploadProgress: React.FC<UploadProgressProps> = ({
 | 
			
		||||
  active,
 | 
			
		||||
  progress,
 | 
			
		||||
  isProcessing,
 | 
			
		||||
}) => {
 | 
			
		||||
  const styles = useSpring({
 | 
			
		||||
    from: { width: '0%' },
 | 
			
		||||
    to: { width: `${progress}%` },
 | 
			
		||||
    reset: true,
 | 
			
		||||
    immediate: reduceMotion,
 | 
			
		||||
  });
 | 
			
		||||
  if (!active) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let message;
 | 
			
		||||
 | 
			
		||||
  if (isProcessing) {
 | 
			
		||||
    message = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='upload_progress.processing'
 | 
			
		||||
        defaultMessage='Processing…'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    message = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='upload_progress.label'
 | 
			
		||||
        defaultMessage='Uploading…'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='upload-progress'>
 | 
			
		||||
      <Icon id='upload' icon={UploadFileIcon} />
 | 
			
		||||
 | 
			
		||||
      <div className='upload-progress__message'>
 | 
			
		||||
        {message}
 | 
			
		||||
 | 
			
		||||
        <div className='upload-progress__backdrop'>
 | 
			
		||||
          <animated.div className='upload-progress__tracker' style={styles} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
 | 
			
		||||
import Motion from '../../ui/util/optional_motion';
 | 
			
		||||
 | 
			
		||||
export default class Warning extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    message: PropTypes.node.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { message } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
 | 
			
		||||
        {({ opacity, scaleX, scaleY }) => (
 | 
			
		||||
          <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
 | 
			
		||||
            {message}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Motion>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,96 @@
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { createSelector } from '@reduxjs/toolkit';
 | 
			
		||||
 | 
			
		||||
import { animated, useSpring } from '@react-spring/web';
 | 
			
		||||
 | 
			
		||||
import { me } from 'mastodon/initial_state';
 | 
			
		||||
import { useAppSelector } from 'mastodon/store';
 | 
			
		||||
import type { RootState } from 'mastodon/store';
 | 
			
		||||
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
 | 
			
		||||
 | 
			
		||||
const selector = createSelector(
 | 
			
		||||
  (state: RootState) => state.compose.get('privacy') as string,
 | 
			
		||||
  (state: RootState) => !!state.compose.getIn(['accounts', me, 'locked']),
 | 
			
		||||
  (state: RootState) => state.compose.get('text') as string,
 | 
			
		||||
  (privacy, locked, text) => ({
 | 
			
		||||
    needsLockWarning: privacy === 'private' && !locked,
 | 
			
		||||
    hashtagWarning: privacy !== 'public' && HASHTAG_PATTERN_REGEX.test(text),
 | 
			
		||||
    directMessageWarning: privacy === 'direct',
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const Warning = () => {
 | 
			
		||||
  const { needsLockWarning, hashtagWarning, directMessageWarning } =
 | 
			
		||||
    useAppSelector(selector);
 | 
			
		||||
  if (needsLockWarning) {
 | 
			
		||||
    return (
 | 
			
		||||
      <WarningMessage>
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='compose_form.lock_disclaimer'
 | 
			
		||||
          defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.'
 | 
			
		||||
          values={{
 | 
			
		||||
            locked: (
 | 
			
		||||
              <a href='/settings/profile'>
 | 
			
		||||
                <FormattedMessage
 | 
			
		||||
                  id='compose_form.lock_disclaimer.lock'
 | 
			
		||||
                  defaultMessage='locked'
 | 
			
		||||
                />
 | 
			
		||||
              </a>
 | 
			
		||||
            ),
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </WarningMessage>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (hashtagWarning) {
 | 
			
		||||
    return (
 | 
			
		||||
      <WarningMessage>
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='compose_form.hashtag_warning'
 | 
			
		||||
          defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag."
 | 
			
		||||
        />
 | 
			
		||||
      </WarningMessage>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (directMessageWarning) {
 | 
			
		||||
    return (
 | 
			
		||||
      <WarningMessage>
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='compose_form.encryption_warning'
 | 
			
		||||
          defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.'
 | 
			
		||||
        />{' '}
 | 
			
		||||
        <a href='/terms' target='_blank'>
 | 
			
		||||
          <FormattedMessage
 | 
			
		||||
            id='compose_form.direct_message_warning_learn_more'
 | 
			
		||||
            defaultMessage='Learn more'
 | 
			
		||||
          />
 | 
			
		||||
        </a>
 | 
			
		||||
      </WarningMessage>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WarningMessage: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
  children,
 | 
			
		||||
}) => {
 | 
			
		||||
  const styles = useSpring({
 | 
			
		||||
    from: {
 | 
			
		||||
      opacity: 0,
 | 
			
		||||
      transform: 'scale(0.85, 0.75)',
 | 
			
		||||
    },
 | 
			
		||||
    to: {
 | 
			
		||||
      opacity: 1,
 | 
			
		||||
      transform: 'scale(1, 1)',
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  return (
 | 
			
		||||
    <animated.div className='compose-form__warning' style={styles}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </animated.div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { me } from 'mastodon/initial_state';
 | 
			
		||||
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
 | 
			
		||||
 | 
			
		||||
import Warning from '../components/warning';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
 | 
			
		||||
  hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
 | 
			
		||||
  directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
 | 
			
		||||
  if (needsLockWarning) {
 | 
			
		||||
    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (hashtagWarning) {
 | 
			
		||||
    return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (directMessageWarning) {
 | 
			
		||||
    const message = (
 | 
			
		||||
      <span>
 | 
			
		||||
        <FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return <Warning message={message} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
WarningWrapper.propTypes = {
 | 
			
		||||
  needsLockWarning: PropTypes.bool,
 | 
			
		||||
  hashtagWarning: PropTypes.bool,
 | 
			
		||||
  directMessageWarning: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(WarningWrapper);
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
import { PureComponent, useCallback, useMemo } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
@@ -9,8 +9,7 @@ import { withRouter } from 'react-router-dom';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
 | 
			
		||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
import { animated, useTransition } from '@react-spring/web';
 | 
			
		||||
import ReactSwipeableViews from 'react-swipeable-views';
 | 
			
		||||
 | 
			
		||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
 | 
			
		||||
@@ -239,18 +238,70 @@ class Reaction extends ImmutablePureComponent {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
 | 
			
		||||
      <animated.button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
 | 
			
		||||
        <span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
 | 
			
		||||
        <span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
 | 
			
		||||
      </button>
 | 
			
		||||
      </animated.button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ReactionsBar extends ImmutablePureComponent {
 | 
			
		||||
const ReactionsBar = ({
 | 
			
		||||
  announcementId,
 | 
			
		||||
  reactions,
 | 
			
		||||
  emojiMap,
 | 
			
		||||
  addReaction,
 | 
			
		||||
  removeReaction,
 | 
			
		||||
}) => {
 | 
			
		||||
  const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
  const handleEmojiPick = useCallback((emoji) => {
 | 
			
		||||
    addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
 | 
			
		||||
  }, [addReaction, announcementId]);
 | 
			
		||||
 | 
			
		||||
  const transitions = useTransition(visibleReactions, {
 | 
			
		||||
    from: {
 | 
			
		||||
      scale: 0,
 | 
			
		||||
    },
 | 
			
		||||
    enter: {
 | 
			
		||||
      scale: 1,
 | 
			
		||||
    },
 | 
			
		||||
    leave: {
 | 
			
		||||
      scale: 0,
 | 
			
		||||
    },
 | 
			
		||||
    immediate: reduceMotion,
 | 
			
		||||
    keys: visibleReactions.map(x => x.get('name')),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('reactions-bar', {
 | 
			
		||||
        'reactions-bar--empty': visibleReactions.length === 0
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      {transitions(({ scale }, reaction) => (
 | 
			
		||||
        <Reaction
 | 
			
		||||
          key={reaction.get('name')}
 | 
			
		||||
          reaction={reaction}
 | 
			
		||||
          style={{ transform: scale.to((s) => `scale(${s})`) }}
 | 
			
		||||
          addReaction={addReaction}
 | 
			
		||||
          removeReaction={removeReaction}
 | 
			
		||||
          announcementId={announcementId}
 | 
			
		||||
          emojiMap={emojiMap}
 | 
			
		||||
        />
 | 
			
		||||
      ))}
 | 
			
		||||
 | 
			
		||||
      {visibleReactions.length < 8 && (
 | 
			
		||||
        <EmojiPickerDropdown
 | 
			
		||||
          onPickEmoji={handleEmojiPick}
 | 
			
		||||
          button={<Icon id='plus' icon={AddIcon} />}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
ReactionsBar.propTypes = {
 | 
			
		||||
  announcementId: PropTypes.string.isRequired,
 | 
			
		||||
  reactions: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
  addReaction: PropTypes.func.isRequired,
 | 
			
		||||
@@ -258,54 +309,6 @@ class ReactionsBar extends ImmutablePureComponent {
 | 
			
		||||
  emojiMap: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  handleEmojiPick = data => {
 | 
			
		||||
    const { addReaction, announcementId } = this.props;
 | 
			
		||||
    addReaction(announcementId, data.native.replace(/:/g, ''));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  willEnter () {
 | 
			
		||||
    return { scale: reduceMotion ? 1 : 0 };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  willLeave () {
 | 
			
		||||
    return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { reactions } = this.props;
 | 
			
		||||
    const visibleReactions = reactions.filter(x => x.get('count') > 0);
 | 
			
		||||
 | 
			
		||||
    const styles = visibleReactions.map(reaction => ({
 | 
			
		||||
      key: reaction.get('name'),
 | 
			
		||||
      data: reaction,
 | 
			
		||||
      style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
 | 
			
		||||
    })).toArray();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
 | 
			
		||||
        {items => (
 | 
			
		||||
          <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
 | 
			
		||||
            {items.map(({ key, data, style }) => (
 | 
			
		||||
              <Reaction
 | 
			
		||||
                key={key}
 | 
			
		||||
                reaction={data}
 | 
			
		||||
                style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
 | 
			
		||||
                announcementId={this.props.announcementId}
 | 
			
		||||
                addReaction={this.props.addReaction}
 | 
			
		||||
                removeReaction={this.props.removeReaction}
 | 
			
		||||
                emojiMap={this.props.emojiMap}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
 | 
			
		||||
            {visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' icon={AddIcon} />} />}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </TransitionMotion>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Announcement extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
 | 
			
		||||
import Motion from '../util/optional_motion';
 | 
			
		||||
 | 
			
		||||
export default class UploadArea extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    active: PropTypes.bool,
 | 
			
		||||
    onClose: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleKeyUp = (e) => {
 | 
			
		||||
    const keyCode = e.keyCode;
 | 
			
		||||
    if (this.props.active) {
 | 
			
		||||
      switch(keyCode) {
 | 
			
		||||
      case 27:
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
        this.props.onClose();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    window.addEventListener('keyup', this.handleKeyUp, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    window.removeEventListener('keyup', this.handleKeyUp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { active } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
 | 
			
		||||
        {({ backgroundOpacity, backgroundScale }) => (
 | 
			
		||||
          <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
 | 
			
		||||
            <div className='upload-area__drop'>
 | 
			
		||||
              <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
 | 
			
		||||
              <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Motion>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,78 @@
 | 
			
		||||
import { useCallback, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { animated, config, useSpring } from '@react-spring/web';
 | 
			
		||||
 | 
			
		||||
import { reduceMotion } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
interface UploadAreaProps {
 | 
			
		||||
  active?: boolean;
 | 
			
		||||
  onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UploadArea: React.FC<UploadAreaProps> = ({ active, onClose }) => {
 | 
			
		||||
  const handleKeyUp = useCallback(
 | 
			
		||||
    (e: KeyboardEvent) => {
 | 
			
		||||
      if (active && e.key === 'Escape') {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
        onClose();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [active, onClose],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.addEventListener('keyup', handleKeyUp, false);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('keyup', handleKeyUp);
 | 
			
		||||
    };
 | 
			
		||||
  }, [handleKeyUp]);
 | 
			
		||||
 | 
			
		||||
  const wrapperAnimStyles = useSpring({
 | 
			
		||||
    from: {
 | 
			
		||||
      opacity: 0,
 | 
			
		||||
    },
 | 
			
		||||
    to: {
 | 
			
		||||
      opacity: 1,
 | 
			
		||||
    },
 | 
			
		||||
    reverse: !active,
 | 
			
		||||
    immediate: reduceMotion,
 | 
			
		||||
  });
 | 
			
		||||
  const backgroundAnimStyles = useSpring({
 | 
			
		||||
    from: {
 | 
			
		||||
      transform: 'scale(0.95)',
 | 
			
		||||
    },
 | 
			
		||||
    to: {
 | 
			
		||||
      transform: 'scale(1)',
 | 
			
		||||
    },
 | 
			
		||||
    reverse: !active,
 | 
			
		||||
    config: config.wobbly,
 | 
			
		||||
    immediate: reduceMotion,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <animated.div
 | 
			
		||||
      className='upload-area'
 | 
			
		||||
      style={{
 | 
			
		||||
        ...wrapperAnimStyles,
 | 
			
		||||
        visibility: active ? 'visible' : 'hidden',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <div className='upload-area__drop'>
 | 
			
		||||
        <animated.div
 | 
			
		||||
          className='upload-area__background'
 | 
			
		||||
          style={backgroundAnimStyles}
 | 
			
		||||
        />
 | 
			
		||||
        <div className='upload-area__content'>
 | 
			
		||||
          <FormattedMessage
 | 
			
		||||
            id='upload_area.title'
 | 
			
		||||
            defaultMessage='Drag & drop to upload'
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </animated.div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -30,7 +30,7 @@ import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding
 | 
			
		||||
 | 
			
		||||
import BundleColumnError from './components/bundle_column_error';
 | 
			
		||||
import Header from './components/header';
 | 
			
		||||
import UploadArea from './components/upload_area';
 | 
			
		||||
import { UploadArea } from './components/upload_area';
 | 
			
		||||
import ColumnsAreaContainer from './containers/columns_area_container';
 | 
			
		||||
import LoadingBarContainer from './containers/loading_bar_container';
 | 
			
		||||
import ModalContainer from './containers/modal_container';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
import Motion from 'react-motion/lib/Motion';
 | 
			
		||||
 | 
			
		||||
import { reduceMotion } from '../../../initial_state';
 | 
			
		||||
 | 
			
		||||
import ReducedMotion from './reduced_motion';
 | 
			
		||||
 | 
			
		||||
export default reduceMotion ? ReducedMotion : Motion;
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
// Like react-motion's Motion, but reduces all animations to cross-fades
 | 
			
		||||
// for the benefit of users with motion sickness.
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import Motion from 'react-motion/lib/Motion';
 | 
			
		||||
 | 
			
		||||
const stylesToKeep = ['opacity', 'backgroundOpacity'];
 | 
			
		||||
 | 
			
		||||
const extractValue = (value) => {
 | 
			
		||||
  // This is either an object with a "val" property or it's a number
 | 
			
		||||
  return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ReducedMotion extends Component {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    defaultStyle: PropTypes.object,
 | 
			
		||||
    style: PropTypes.object,
 | 
			
		||||
    children: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
 | 
			
		||||
    const { style, defaultStyle, children } = this.props;
 | 
			
		||||
 | 
			
		||||
    Object.keys(style).forEach(key => {
 | 
			
		||||
      if (stylesToKeep.includes(key)) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // If it's setting an x or height or scale or some other value, we need
 | 
			
		||||
      // to preserve the end-state value without actually animating it
 | 
			
		||||
      style[key] = defaultStyle[key] = extractValue(style[key]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Motion style={style} defaultStyle={defaultStyle}>
 | 
			
		||||
        {children}
 | 
			
		||||
      </Motion>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ReducedMotion;
 | 
			
		||||
@@ -104,7 +104,6 @@
 | 
			
		||||
    "react-immutable-proptypes": "^2.2.0",
 | 
			
		||||
    "react-immutable-pure-component": "^2.2.2",
 | 
			
		||||
    "react-intl": "^7.0.0",
 | 
			
		||||
    "react-motion": "^0.5.2",
 | 
			
		||||
    "react-overlays": "^5.2.1",
 | 
			
		||||
    "react-redux": "^9.0.4",
 | 
			
		||||
    "react-redux-loading-bar": "^5.0.8",
 | 
			
		||||
@@ -164,7 +163,6 @@
 | 
			
		||||
    "@types/react-dom": "^18.2.4",
 | 
			
		||||
    "@types/react-helmet": "^6.1.6",
 | 
			
		||||
    "@types/react-immutable-proptypes": "^2.1.0",
 | 
			
		||||
    "@types/react-motion": "^0.0.40",
 | 
			
		||||
    "@types/react-router": "^5.1.20",
 | 
			
		||||
    "@types/react-router-dom": "^5.3.3",
 | 
			
		||||
    "@types/react-sparklines": "^1.7.2",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -2771,7 +2771,6 @@ __metadata:
 | 
			
		||||
    "@types/react-dom": "npm:^18.2.4"
 | 
			
		||||
    "@types/react-helmet": "npm:^6.1.6"
 | 
			
		||||
    "@types/react-immutable-proptypes": "npm:^2.1.0"
 | 
			
		||||
    "@types/react-motion": "npm:^0.0.40"
 | 
			
		||||
    "@types/react-router": "npm:^5.1.20"
 | 
			
		||||
    "@types/react-router-dom": "npm:^5.3.3"
 | 
			
		||||
    "@types/react-sparklines": "npm:^1.7.2"
 | 
			
		||||
@@ -2850,7 +2849,6 @@ __metadata:
 | 
			
		||||
    react-immutable-proptypes: "npm:^2.2.0"
 | 
			
		||||
    react-immutable-pure-component: "npm:^2.2.2"
 | 
			
		||||
    react-intl: "npm:^7.0.0"
 | 
			
		||||
    react-motion: "npm:^0.5.2"
 | 
			
		||||
    react-overlays: "npm:^5.2.1"
 | 
			
		||||
    react-redux: "npm:^9.0.4"
 | 
			
		||||
    react-redux-loading-bar: "npm:^5.0.8"
 | 
			
		||||
@@ -4050,15 +4048,6 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@types/react-motion@npm:^0.0.40":
 | 
			
		||||
  version: 0.0.40
 | 
			
		||||
  resolution: "@types/react-motion@npm:0.0.40"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/react": "npm:*"
 | 
			
		||||
  checksum: 10c0/8a560051be917833fdbe051185b53aeafbe8657968ac8e073ac874b9a55c6f16e3793748b13cfb9bd6d9a3d27aba116d6f8f296ec1950f4175dc94d17c5e8470
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"@types/react-router-dom@npm:^5.3.3":
 | 
			
		||||
  version: 5.3.3
 | 
			
		||||
  resolution: "@types/react-router-dom@npm:5.3.3"
 | 
			
		||||
@@ -13189,20 +13178,6 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"performance-now@npm:^0.2.0":
 | 
			
		||||
  version: 0.2.0
 | 
			
		||||
  resolution: "performance-now@npm:0.2.0"
 | 
			
		||||
  checksum: 10c0/d7f3824e443491208f7124b45d3280dbff889f8f048c3aee507109c24644d51a226eb07fd7ac51dd0eef144639590c57410c2d167bd4fdf0c5caa0101a449c3d
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"performance-now@npm:^2.1.0":
 | 
			
		||||
  version: 2.1.0
 | 
			
		||||
  resolution: "performance-now@npm:2.1.0"
 | 
			
		||||
  checksum: 10c0/22c54de06f269e29f640e0e075207af57de5052a3d15e360c09b9a8663f393f6f45902006c1e71aa8a5a1cdfb1a47fe268826f8496d6425c362f00f5bc3e85d9
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"pg-cloudflare@npm:^1.1.1":
 | 
			
		||||
  version: 1.1.1
 | 
			
		||||
  resolution: "pg-cloudflare@npm:1.1.1"
 | 
			
		||||
@@ -14465,7 +14440,7 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
 | 
			
		||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
 | 
			
		||||
  version: 15.8.1
 | 
			
		||||
  resolution: "prop-types@npm:15.8.1"
 | 
			
		||||
  dependencies:
 | 
			
		||||
@@ -14596,15 +14571,6 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"raf@npm:^3.1.0":
 | 
			
		||||
  version: 3.4.1
 | 
			
		||||
  resolution: "raf@npm:3.4.1"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    performance-now: "npm:^2.1.0"
 | 
			
		||||
  checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0":
 | 
			
		||||
  version: 2.1.0
 | 
			
		||||
  resolution: "randombytes@npm:2.1.0"
 | 
			
		||||
@@ -14777,19 +14743,6 @@ __metadata:
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"react-motion@npm:^0.5.2":
 | 
			
		||||
  version: 0.5.2
 | 
			
		||||
  resolution: "react-motion@npm:0.5.2"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    performance-now: "npm:^0.2.0"
 | 
			
		||||
    prop-types: "npm:^15.5.8"
 | 
			
		||||
    raf: "npm:^3.1.0"
 | 
			
		||||
  peerDependencies:
 | 
			
		||||
    react: ^0.14.9 || ^15.3.0 || ^16.0.0
 | 
			
		||||
  checksum: 10c0/4ea6f1cc7079f0161fd786cc755133a822d87d9c0510369b8fb348d9ad602111efa2e3496dbcc390c967229e39e3eb5f6dd5dd6d3d124289443de31d6035a6c8
 | 
			
		||||
  languageName: node
 | 
			
		||||
  linkType: hard
 | 
			
		||||
 | 
			
		||||
"react-overlays@npm:^5.2.1":
 | 
			
		||||
  version: 5.2.1
 | 
			
		||||
  resolution: "react-overlays@npm:5.2.1"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user