Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		@@ -1,7 +1,6 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
import compareId from '../compare_id';
 | 
			
		||||
import { showAlertForError } from './alerts';
 | 
			
		||||
 | 
			
		||||
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
 | 
			
		||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
 | 
			
		||||
@@ -29,15 +28,19 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify(params),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (navigator && navigator.sendBeacon) {
 | 
			
		||||
    // Failing that, we can use sendBeacon, but we have to encode the data as
 | 
			
		||||
    // FormData for DoorKeeper to recognize the token.
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
 | 
			
		||||
    formData.append('bearer_token', accessToken);
 | 
			
		||||
 | 
			
		||||
    for (const [id, value] of Object.entries(params)) {
 | 
			
		||||
      formData.append(`${id}[last_read_id]`, value.last_read_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (navigator.sendBeacon('/api/v1/markers', formData)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -85,11 +88,9 @@ const debouncedSubmitMarkers = debounce((dispatch, getState) => {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  api().post('/api/v1/markers', params).then(() => {
 | 
			
		||||
  api(getState).post('/api/v1/markers', params).then(() => {
 | 
			
		||||
    dispatch(submitMarkersSuccess(params));
 | 
			
		||||
  }).catch(error => {
 | 
			
		||||
    dispatch(showAlertForError(error));
 | 
			
		||||
  });
 | 
			
		||||
  }).catch(() => {});
 | 
			
		||||
}, 300000, { leading: true, trailing: true });
 | 
			
		||||
 | 
			
		||||
export function submitMarkersSuccess({ home, notifications }) {
 | 
			
		||||
@@ -102,9 +103,11 @@ export function submitMarkersSuccess({ home, notifications }) {
 | 
			
		||||
 | 
			
		||||
export function submitMarkers(params = {}) {
 | 
			
		||||
  const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
 | 
			
		||||
 | 
			
		||||
  if (params.immediate === true) {
 | 
			
		||||
    debouncedSubmitMarkers.flush();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										112
									
								
								app/javascript/mastodon/blurhash.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/javascript/mastodon/blurhash.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
const DIGIT_CHARACTERS = [
 | 
			
		||||
  '0',
 | 
			
		||||
  '1',
 | 
			
		||||
  '2',
 | 
			
		||||
  '3',
 | 
			
		||||
  '4',
 | 
			
		||||
  '5',
 | 
			
		||||
  '6',
 | 
			
		||||
  '7',
 | 
			
		||||
  '8',
 | 
			
		||||
  '9',
 | 
			
		||||
  'A',
 | 
			
		||||
  'B',
 | 
			
		||||
  'C',
 | 
			
		||||
  'D',
 | 
			
		||||
  'E',
 | 
			
		||||
  'F',
 | 
			
		||||
  'G',
 | 
			
		||||
  'H',
 | 
			
		||||
  'I',
 | 
			
		||||
  'J',
 | 
			
		||||
  'K',
 | 
			
		||||
  'L',
 | 
			
		||||
  'M',
 | 
			
		||||
  'N',
 | 
			
		||||
  'O',
 | 
			
		||||
  'P',
 | 
			
		||||
  'Q',
 | 
			
		||||
  'R',
 | 
			
		||||
  'S',
 | 
			
		||||
  'T',
 | 
			
		||||
  'U',
 | 
			
		||||
  'V',
 | 
			
		||||
  'W',
 | 
			
		||||
  'X',
 | 
			
		||||
  'Y',
 | 
			
		||||
  'Z',
 | 
			
		||||
  'a',
 | 
			
		||||
  'b',
 | 
			
		||||
  'c',
 | 
			
		||||
  'd',
 | 
			
		||||
  'e',
 | 
			
		||||
  'f',
 | 
			
		||||
  'g',
 | 
			
		||||
  'h',
 | 
			
		||||
  'i',
 | 
			
		||||
  'j',
 | 
			
		||||
  'k',
 | 
			
		||||
  'l',
 | 
			
		||||
  'm',
 | 
			
		||||
  'n',
 | 
			
		||||
  'o',
 | 
			
		||||
  'p',
 | 
			
		||||
  'q',
 | 
			
		||||
  'r',
 | 
			
		||||
  's',
 | 
			
		||||
  't',
 | 
			
		||||
  'u',
 | 
			
		||||
  'v',
 | 
			
		||||
  'w',
 | 
			
		||||
  'x',
 | 
			
		||||
  'y',
 | 
			
		||||
  'z',
 | 
			
		||||
  '#',
 | 
			
		||||
  '$',
 | 
			
		||||
  '%',
 | 
			
		||||
  '*',
 | 
			
		||||
  '+',
 | 
			
		||||
  ',',
 | 
			
		||||
  '-',
 | 
			
		||||
  '.',
 | 
			
		||||
  ':',
 | 
			
		||||
  ';',
 | 
			
		||||
  '=',
 | 
			
		||||
  '?',
 | 
			
		||||
  '@',
 | 
			
		||||
  '[',
 | 
			
		||||
  ']',
 | 
			
		||||
  '^',
 | 
			
		||||
  '_',
 | 
			
		||||
  '{',
 | 
			
		||||
  '|',
 | 
			
		||||
  '}',
 | 
			
		||||
  '~',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const decode83 = (str) => {
 | 
			
		||||
  let value = 0;
 | 
			
		||||
  let c, digit;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < str.length; i++) {
 | 
			
		||||
    c = str[i];
 | 
			
		||||
    digit = DIGIT_CHARACTERS.indexOf(c);
 | 
			
		||||
    value = value * 83 + digit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const intToRGB = int => ({
 | 
			
		||||
  r: Math.max(0, (int >> 16)),
 | 
			
		||||
  g: Math.max(0, (int >> 8) & 255),
 | 
			
		||||
  b: Math.max(0, (int & 255)),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const getAverageFromBlurhash = blurhash => {
 | 
			
		||||
  if (!blurhash) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intToRGB(decode83(blurhash.slice(2, 6)));
 | 
			
		||||
};
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import 'wicg-inert';
 | 
			
		||||
import { normal } from 'color-blend';
 | 
			
		||||
import { multiply } from 'color-blend';
 | 
			
		||||
 | 
			
		||||
export default class ModalRoot extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +98,7 @@ export default class ModalRoot extends React.PureComponent {
 | 
			
		||||
    let backgroundColor = null;
 | 
			
		||||
 | 
			
		||||
    if (this.props.backgroundColor) {
 | 
			
		||||
      backgroundColor = normal({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.3 });
 | 
			
		||||
      backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    cachedMediaWidth: PropTypes.number,
 | 
			
		||||
    scrollKey: PropTypes.string,
 | 
			
		||||
    deployPictureInPicture: PropTypes.func,
 | 
			
		||||
    pictureInPicture: PropTypes.shape({
 | 
			
		||||
    pictureInPicture: ImmutablePropTypes.contains({
 | 
			
		||||
      inUse: PropTypes.bool,
 | 
			
		||||
      available: PropTypes.bool,
 | 
			
		||||
    }),
 | 
			
		||||
@@ -192,8 +192,9 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    return <div className='audio-player' style={{ height: '110px' }} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpenVideo = (media, options) => {
 | 
			
		||||
    this.props.onOpenVideo(this._properStatus().get('id'), media, options);
 | 
			
		||||
  handleOpenVideo = (options) => {
 | 
			
		||||
    const status = this._properStatus();
 | 
			
		||||
    this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpenMedia = (media, index) => {
 | 
			
		||||
@@ -202,15 +203,15 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  handleHotkeyOpenMedia = e => {
 | 
			
		||||
    const { onOpenMedia, onOpenVideo } = this.props;
 | 
			
		||||
    const statusId = this._properStatus().get('id');
 | 
			
		||||
    const status = this._properStatus();
 | 
			
		||||
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (status.get('media_attachments').size > 0) {
 | 
			
		||||
      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
			
		||||
        onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
 | 
			
		||||
        onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 });
 | 
			
		||||
      } else {
 | 
			
		||||
        onOpenMedia(statusId, status.get('media_attachments'), 0);
 | 
			
		||||
        onOpenMedia(status.get('id'), status.get('media_attachments'), 0);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -353,7 +354,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
      status  = status.get('reblog');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (pictureInPicture.inUse) {
 | 
			
		||||
    if (pictureInPicture.get('inUse')) {
 | 
			
		||||
      media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
 | 
			
		||||
    } else if (status.get('media_attachments').size > 0) {
 | 
			
		||||
      if (this.props.muted) {
 | 
			
		||||
@@ -380,7 +381,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                width={this.props.cachedMediaWidth}
 | 
			
		||||
                height={110}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
 | 
			
		||||
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
@@ -403,7 +404,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                sensitive={status.get('sensitive')}
 | 
			
		||||
                onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
 | 
			
		||||
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
			
		||||
                visible={this.state.showMedia}
 | 
			
		||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
              />
 | 
			
		||||
@@ -431,7 +432,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
 | 
			
		||||
      media = (
 | 
			
		||||
        <Card
 | 
			
		||||
          onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
          onOpenMedia={this.handleOpenMedia}
 | 
			
		||||
          card={status.get('card')}
 | 
			
		||||
          compact
 | 
			
		||||
          cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import Status from '../components/status';
 | 
			
		||||
import { makeGetStatus } from '../selectors';
 | 
			
		||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
 | 
			
		||||
import {
 | 
			
		||||
  replyCompose,
 | 
			
		||||
  mentionCompose,
 | 
			
		||||
@@ -54,14 +54,11 @@ const messages = defineMessages({
 | 
			
		||||
 | 
			
		||||
const makeMapStateToProps = () => {
 | 
			
		||||
  const getStatus = makeGetStatus();
 | 
			
		||||
  const getPictureInPicture = makeGetPictureInPicture();
 | 
			
		||||
 | 
			
		||||
  const mapStateToProps = (state, props) => ({
 | 
			
		||||
    status: getStatus(state, props),
 | 
			
		||||
 | 
			
		||||
    pictureInPicture: {
 | 
			
		||||
      inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id,
 | 
			
		||||
      available: state.getIn(['meta', 'layout']) !== 'mobile',
 | 
			
		||||
    },
 | 
			
		||||
    pictureInPicture: getPictureInPicture(state, props),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mapStateToProps;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,9 @@ import RadioButton from 'mastodon/components/radio_button';
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
 | 
			
		||||
  deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
 | 
			
		||||
  all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
 | 
			
		||||
  no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
 | 
			
		||||
  list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
 | 
			
		||||
  followed:   { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
 | 
			
		||||
  none:    { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
 | 
			
		||||
  list:  { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, props) => ({
 | 
			
		||||
@@ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent {
 | 
			
		||||
                <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
 | 
			
		||||
              </span>
 | 
			
		||||
              <div className='column-settings__row'>
 | 
			
		||||
                { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
 | 
			
		||||
                { ['none', 'list', 'followed'].map(policy => (
 | 
			
		||||
                  <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
 | 
			
		||||
                ))}
 | 
			
		||||
              </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    compact: PropTypes.bool,
 | 
			
		||||
    showMedia: PropTypes.bool,
 | 
			
		||||
    usingPiP: PropTypes.bool,
 | 
			
		||||
    pictureInPicture: ImmutablePropTypes.contains({
 | 
			
		||||
      inUse: PropTypes.bool,
 | 
			
		||||
      available: PropTypes.bool,
 | 
			
		||||
    }),
 | 
			
		||||
    onToggleMediaVisibility: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -58,8 +61,8 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpenVideo = (media, options) => {
 | 
			
		||||
    this.props.onOpenVideo(media, options);
 | 
			
		||||
  handleOpenVideo = (options) => {
 | 
			
		||||
    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleExpandedToggle = () => {
 | 
			
		||||
@@ -102,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
  render () {
 | 
			
		||||
    const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
 | 
			
		||||
    const outerStyle = { boxSizing: 'border-box' };
 | 
			
		||||
    const { intl, compact, usingPiP } = this.props;
 | 
			
		||||
    const { intl, compact, pictureInPicture } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!status) {
 | 
			
		||||
      return null;
 | 
			
		||||
@@ -118,7 +121,7 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
      outerStyle.height = `${this.state.height}px`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (usingPiP) {
 | 
			
		||||
    if (pictureInPicture.get('inUse')) {
 | 
			
		||||
      media = <PictureInPicturePlaceholder />;
 | 
			
		||||
    } else if (status.get('media_attachments').size > 0) {
 | 
			
		||||
      if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import DetailedStatus from '../components/detailed_status';
 | 
			
		||||
import { makeGetStatus } from '../../../selectors';
 | 
			
		||||
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
 | 
			
		||||
import {
 | 
			
		||||
  replyCompose,
 | 
			
		||||
  mentionCompose,
 | 
			
		||||
@@ -40,10 +40,12 @@ const messages = defineMessages({
 | 
			
		||||
 | 
			
		||||
const makeMapStateToProps = () => {
 | 
			
		||||
  const getStatus = makeGetStatus();
 | 
			
		||||
  const getPictureInPicture = makeGetPictureInPicture();
 | 
			
		||||
 | 
			
		||||
  const mapStateToProps = (state, props) => ({
 | 
			
		||||
    status: getStatus(state, props),
 | 
			
		||||
    domain: state.getIn(['meta', 'domain']),
 | 
			
		||||
    pictureInPicture: getPictureInPicture(state, props),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mapStateToProps;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ import {
 | 
			
		||||
import { initMuteModal } from '../../actions/mutes';
 | 
			
		||||
import { initBlockModal } from '../../actions/blocks';
 | 
			
		||||
import { initReport } from '../../actions/reports';
 | 
			
		||||
import { makeGetStatus } from '../../selectors';
 | 
			
		||||
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
 | 
			
		||||
import { ScrollContainer } from 'react-router-scroll-4';
 | 
			
		||||
import ColumnBackButton from '../../components/column_back_button';
 | 
			
		||||
import ColumnHeader from '../../components/column_header';
 | 
			
		||||
@@ -72,6 +72,7 @@ const messages = defineMessages({
 | 
			
		||||
 | 
			
		||||
const makeMapStateToProps = () => {
 | 
			
		||||
  const getStatus = makeGetStatus();
 | 
			
		||||
  const getPictureInPicture = makeGetPictureInPicture();
 | 
			
		||||
 | 
			
		||||
  const getAncestorsIds = createSelector([
 | 
			
		||||
    (_, { id }) => id,
 | 
			
		||||
@@ -129,11 +130,12 @@ const makeMapStateToProps = () => {
 | 
			
		||||
 | 
			
		||||
  const mapStateToProps = (state, props) => {
 | 
			
		||||
    const status = getStatus(state, { id: props.params.statusId });
 | 
			
		||||
    let ancestorsIds = Immutable.List();
 | 
			
		||||
 | 
			
		||||
    let ancestorsIds   = Immutable.List();
 | 
			
		||||
    let descendantsIds = Immutable.List();
 | 
			
		||||
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
 | 
			
		||||
      ancestorsIds   = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
 | 
			
		||||
      descendantsIds = getDescendantsIds(state, { id: status.get('id') });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +145,7 @@ const makeMapStateToProps = () => {
 | 
			
		||||
      descendantsIds,
 | 
			
		||||
      askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
 | 
			
		||||
      domain: state.getIn(['meta', 'domain']),
 | 
			
		||||
      usingPiP: state.get('picture_in_picture').statusId === props.params.statusId,
 | 
			
		||||
      pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -168,7 +170,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    askReplyConfirmation: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    usingPiP: PropTypes.bool,
 | 
			
		||||
    pictureInPicture: ImmutablePropTypes.contains({
 | 
			
		||||
      inUse: PropTypes.bool,
 | 
			
		||||
      available: PropTypes.bool,
 | 
			
		||||
    }),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
@@ -492,7 +497,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    let ancestors, descendants;
 | 
			
		||||
    const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props;
 | 
			
		||||
    const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
 | 
			
		||||
    const { fullscreen } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (status === null) {
 | 
			
		||||
@@ -550,7 +555,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                  domain={domain}
 | 
			
		||||
                  showMedia={this.state.showMedia}
 | 
			
		||||
                  onToggleMediaVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
                  usingPiP={usingPiP}
 | 
			
		||||
                  pictureInPicture={pictureInPicture}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ActionBar
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,11 @@ import PropTypes from 'prop-types';
 | 
			
		||||
import Audio from 'mastodon/features/audio';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { previewState } from './video_modal';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import Footer from 'mastodon/features/picture_in_picture/components/footer';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { status }) => ({
 | 
			
		||||
  account: state.getIn(['accounts', status.get('account')]),
 | 
			
		||||
const mapStateToProps = (state, { statusId }) => ({
 | 
			
		||||
  accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
@@ -18,12 +16,13 @@ class AudioModal extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    media: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    status: ImmutablePropTypes.map,
 | 
			
		||||
    statusId: PropTypes.string.isRequired,
 | 
			
		||||
    accountStaticAvatar: PropTypes.string.isRequired,
 | 
			
		||||
    options: PropTypes.shape({
 | 
			
		||||
      autoPlay: PropTypes.bool,
 | 
			
		||||
    }),
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    onClose: PropTypes.func.isRequired,
 | 
			
		||||
    onChangeBackgroundColor: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
@@ -52,15 +51,8 @@ class AudioModal extends ImmutablePureComponent {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleStatusClick = e => {
 | 
			
		||||
    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, status, account } = this.props;
 | 
			
		||||
    const { media, accountStaticAvatar, statusId, onClose } = this.props;
 | 
			
		||||
    const options = this.props.options || {};
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -71,7 +63,7 @@ class AudioModal extends ImmutablePureComponent {
 | 
			
		||||
            alt={media.get('description')}
 | 
			
		||||
            duration={media.getIn(['meta', 'original', 'duration'], 0)}
 | 
			
		||||
            height={150}
 | 
			
		||||
            poster={media.get('preview_url') || account.get('avatar_static')}
 | 
			
		||||
            poster={media.get('preview_url') || accountStaticAvatar}
 | 
			
		||||
            backgroundColor={media.getIn(['meta', 'colors', 'background'])}
 | 
			
		||||
            foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
 | 
			
		||||
            accentColor={media.getIn(['meta', 'colors', 'accent'])}
 | 
			
		||||
@@ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent {
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {status && (
 | 
			
		||||
          <div className={classNames('media-modal__meta')}>
 | 
			
		||||
            <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        <div className='media-modal__overlay'>
 | 
			
		||||
          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps() {
 | 
			
		||||
    this.setState({ shouldAnimate: false });
 | 
			
		||||
    if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
 | 
			
		||||
      this.setState({ shouldAnimate: false });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
@@ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent {
 | 
			
		||||
    if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
 | 
			
		||||
      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
 | 
			
		||||
    }
 | 
			
		||||
    this.lastIndex = getIndex(this.context.router.history.location.pathname);
 | 
			
		||||
    this.setState({ shouldAnimate: true });
 | 
			
		||||
 | 
			
		||||
    const newIndex = getIndex(this.context.router.history.location.pathname);
 | 
			
		||||
 | 
			
		||||
    if (this.lastIndex !== newIndex) {
 | 
			
		||||
      this.lastIndex = newIndex;
 | 
			
		||||
      this.setState({ shouldAnimate: true });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import Icon from 'mastodon/components/icon';
 | 
			
		||||
import GIFV from 'mastodon/components/gifv';
 | 
			
		||||
import { disableSwiping } from 'mastodon/initial_state';
 | 
			
		||||
import Footer from 'mastodon/features/picture_in_picture/components/footer';
 | 
			
		||||
import { getAverageFromBlurhash } from 'mastodon/blurhash';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
			
		||||
@@ -21,111 +22,6 @@ const messages = defineMessages({
 | 
			
		||||
 | 
			
		||||
export const previewState = 'previewMediaModal';
 | 
			
		||||
 | 
			
		||||
const digitCharacters = [
 | 
			
		||||
  '0',
 | 
			
		||||
  '1',
 | 
			
		||||
  '2',
 | 
			
		||||
  '3',
 | 
			
		||||
  '4',
 | 
			
		||||
  '5',
 | 
			
		||||
  '6',
 | 
			
		||||
  '7',
 | 
			
		||||
  '8',
 | 
			
		||||
  '9',
 | 
			
		||||
  'A',
 | 
			
		||||
  'B',
 | 
			
		||||
  'C',
 | 
			
		||||
  'D',
 | 
			
		||||
  'E',
 | 
			
		||||
  'F',
 | 
			
		||||
  'G',
 | 
			
		||||
  'H',
 | 
			
		||||
  'I',
 | 
			
		||||
  'J',
 | 
			
		||||
  'K',
 | 
			
		||||
  'L',
 | 
			
		||||
  'M',
 | 
			
		||||
  'N',
 | 
			
		||||
  'O',
 | 
			
		||||
  'P',
 | 
			
		||||
  'Q',
 | 
			
		||||
  'R',
 | 
			
		||||
  'S',
 | 
			
		||||
  'T',
 | 
			
		||||
  'U',
 | 
			
		||||
  'V',
 | 
			
		||||
  'W',
 | 
			
		||||
  'X',
 | 
			
		||||
  'Y',
 | 
			
		||||
  'Z',
 | 
			
		||||
  'a',
 | 
			
		||||
  'b',
 | 
			
		||||
  'c',
 | 
			
		||||
  'd',
 | 
			
		||||
  'e',
 | 
			
		||||
  'f',
 | 
			
		||||
  'g',
 | 
			
		||||
  'h',
 | 
			
		||||
  'i',
 | 
			
		||||
  'j',
 | 
			
		||||
  'k',
 | 
			
		||||
  'l',
 | 
			
		||||
  'm',
 | 
			
		||||
  'n',
 | 
			
		||||
  'o',
 | 
			
		||||
  'p',
 | 
			
		||||
  'q',
 | 
			
		||||
  'r',
 | 
			
		||||
  's',
 | 
			
		||||
  't',
 | 
			
		||||
  'u',
 | 
			
		||||
  'v',
 | 
			
		||||
  'w',
 | 
			
		||||
  'x',
 | 
			
		||||
  'y',
 | 
			
		||||
  'z',
 | 
			
		||||
  '#',
 | 
			
		||||
  '$',
 | 
			
		||||
  '%',
 | 
			
		||||
  '*',
 | 
			
		||||
  '+',
 | 
			
		||||
  ',',
 | 
			
		||||
  '-',
 | 
			
		||||
  '.',
 | 
			
		||||
  ':',
 | 
			
		||||
  ';',
 | 
			
		||||
  '=',
 | 
			
		||||
  '?',
 | 
			
		||||
  '@',
 | 
			
		||||
  '[',
 | 
			
		||||
  ']',
 | 
			
		||||
  '^',
 | 
			
		||||
  '_',
 | 
			
		||||
  '{',
 | 
			
		||||
  '|',
 | 
			
		||||
  '}',
 | 
			
		||||
  '~',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const decode83 = (str) => {
 | 
			
		||||
  let value = 0;
 | 
			
		||||
  let c, digit;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < str.length; i++) {
 | 
			
		||||
    c = str[i];
 | 
			
		||||
    digit = digitCharacters.indexOf(c);
 | 
			
		||||
    value = value * 83 + digit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const decodeRGB = int => ({
 | 
			
		||||
  r: Math.max(0, (int >> 16)),
 | 
			
		||||
  g: Math.max(0, (int >> 8) & 255),
 | 
			
		||||
  b: Math.max(0, (int & 255)),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
class MediaModal extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
@@ -224,7 +120,7 @@ class MediaModal extends ImmutablePureComponent {
 | 
			
		||||
    const blurhash = media.getIn([index, 'blurhash']);
 | 
			
		||||
 | 
			
		||||
    if (blurhash) {
 | 
			
		||||
      const backgroundColor = decodeRGB(decode83(blurhash.slice(2, 6)));
 | 
			
		||||
      const backgroundColor = getAverageFromBlurhash(blurhash);
 | 
			
		||||
      onChangeBackgroundColor(backgroundColor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import Video from 'mastodon/features/video';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import Footer from 'mastodon/features/picture_in_picture/components/footer';
 | 
			
		||||
import { getAverageFromBlurhash } from 'mastodon/blurhash';
 | 
			
		||||
 | 
			
		||||
export const previewState = 'previewVideoModal';
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +19,7 @@ export default class VideoModal extends ImmutablePureComponent {
 | 
			
		||||
      defaultVolume: PropTypes.number,
 | 
			
		||||
    }),
 | 
			
		||||
    onClose: PropTypes.func.isRequired,
 | 
			
		||||
    onChangeBackgroundColor: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
@@ -24,29 +27,35 @@ export default class VideoModal extends ImmutablePureComponent {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    if (this.context.router) {
 | 
			
		||||
      const history = this.context.router.history;
 | 
			
		||||
    const { router } = this.context;
 | 
			
		||||
    const { media, onChangeBackgroundColor, onClose } = this.props;
 | 
			
		||||
 | 
			
		||||
      history.push(history.location.pathname, previewState);
 | 
			
		||||
    if (router) {
 | 
			
		||||
      router.history.push(router.history.location.pathname, previewState);
 | 
			
		||||
      this.unlistenHistory = router.history.listen(() => onClose());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      this.unlistenHistory = history.listen(() => {
 | 
			
		||||
        this.props.onClose();
 | 
			
		||||
      });
 | 
			
		||||
    const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
 | 
			
		||||
 | 
			
		||||
    if (backgroundColor) {
 | 
			
		||||
      onChangeBackgroundColor(backgroundColor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    if (this.context.router) {
 | 
			
		||||
    const { router } = this.context;
 | 
			
		||||
 | 
			
		||||
    if (router) {
 | 
			
		||||
      this.unlistenHistory();
 | 
			
		||||
 | 
			
		||||
      if (this.context.router.history.location.state === previewState) {
 | 
			
		||||
        this.context.router.history.goBack();
 | 
			
		||||
      if (router.history.location.state === previewState) {
 | 
			
		||||
        router.history.goBack();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, onClose } = this.props;
 | 
			
		||||
    const { media, statusId, onClose } = this.props;
 | 
			
		||||
    const options = this.props.options || {};
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -65,6 +74,10 @@ export default class VideoModal extends ImmutablePureComponent {
 | 
			
		||||
            alt={media.get('description')}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='media-modal__overlay'>
 | 
			
		||||
          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { fromJS, is } from 'immutable';
 | 
			
		||||
import { is } from 'immutable';
 | 
			
		||||
import { throttle, debounce } from 'lodash';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
			
		||||
@@ -495,25 +495,13 @@ class Video extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpenVideo = () => {
 | 
			
		||||
    const { src, preview, width, height, alt } = this.props;
 | 
			
		||||
    this.video.pause();
 | 
			
		||||
 | 
			
		||||
    const media = fromJS({
 | 
			
		||||
      type: 'video',
 | 
			
		||||
      url: src,
 | 
			
		||||
      preview_url: preview,
 | 
			
		||||
      description: alt,
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const options = {
 | 
			
		||||
    this.props.onOpenVideo({
 | 
			
		||||
      startTime: this.video.currentTime,
 | 
			
		||||
      autoPlay: !this.state.paused,
 | 
			
		||||
      defaultVolume: this.state.volume,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.video.pause();
 | 
			
		||||
    this.props.onOpenVideo(media, options);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCloseVideo = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { List as ImmutableList, is } from 'immutable';
 | 
			
		||||
import { List as ImmutableList, Map as ImmutableMap, is } from 'immutable';
 | 
			
		||||
import { me } from '../initial_state';
 | 
			
		||||
 | 
			
		||||
const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 | 
			
		||||
@@ -121,6 +121,16 @@ export const makeGetStatus = () => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const makeGetPictureInPicture = () => {
 | 
			
		||||
  return createSelector([
 | 
			
		||||
    (state, { id }) => state.get('picture_in_picture').statusId === id,
 | 
			
		||||
    (state) => state.getIn(['meta', 'layout']) !== 'mobile',
 | 
			
		||||
  ], (inUse, available) => ImmutableMap({
 | 
			
		||||
    inUse: inUse && available,
 | 
			
		||||
    available,
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getAlertsBase = state => state.get('alerts');
 | 
			
		||||
 | 
			
		||||
export const getAlerts = createSelector([getAlertsBase], (base) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user