Merge pull request #1164 from ThibG/glitch-soc/cherry-pick-upstream
Cherry-pick changes from upstream
This commit is contained in:
		
						commit
						e9cc17bbea
					
				@ -53,6 +53,7 @@ class Settings::PreferencesController < Settings::BaseController
 | 
			
		||||
      :setting_advanced_layout,
 | 
			
		||||
      :setting_default_content_type,
 | 
			
		||||
      :setting_use_blurhash,
 | 
			
		||||
      :setting_use_pending_items,
 | 
			
		||||
      notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
 | 
			
		||||
      interactions: %i(must_be_follower must_be_following must_be_following_dm)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import { unescapeHTML } from 'flavours/glitch/util/html';
 | 
			
		||||
import { getFiltersRegex } from 'flavours/glitch/selectors';
 | 
			
		||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
import compareId from 'flavours/glitch/util/compare_id';
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
 | 
			
		||||
 | 
			
		||||
@ -34,6 +36,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
 | 
			
		||||
export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
 | 
			
		||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT';
 | 
			
		||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
 | 
			
		||||
@ -52,6 +55,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const loadPending = () => ({
 | 
			
		||||
  type: NOTIFICATIONS_LOAD_PENDING,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
 | 
			
		||||
@ -83,6 +90,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
			
		||||
      dispatch({
 | 
			
		||||
        type: NOTIFICATIONS_UPDATE,
 | 
			
		||||
        notification,
 | 
			
		||||
        usePendingItems: preferPendingItems,
 | 
			
		||||
        meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -136,9 +144,18 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 | 
			
		||||
        : excludeTypesFromFilter(activeFilter),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!maxId && notifications.get('items').size > 0) {
 | 
			
		||||
      params.since_id = notifications.getIn(['items', 0, 'id']);
 | 
			
		||||
    if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
 | 
			
		||||
      const a = notifications.getIn(['pendingItems', 0, 'id']);
 | 
			
		||||
      const b = notifications.getIn(['items', 0, 'id']);
 | 
			
		||||
 | 
			
		||||
      if (a && b && compareId(a, b) > 0) {
 | 
			
		||||
        params.since_id = a;
 | 
			
		||||
      } else {
 | 
			
		||||
        params.since_id = b || a;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isLoadingRecent = !!params.since_id;
 | 
			
		||||
 | 
			
		||||
    dispatch(expandNotificationsRequest(isLoadingMore));
 | 
			
		||||
 | 
			
		||||
@ -148,7 +165,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 | 
			
		||||
      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
 | 
			
		||||
      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
 | 
			
		||||
 | 
			
		||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
 | 
			
		||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems));
 | 
			
		||||
      fetchRelatedRelationships(dispatch, response.data);
 | 
			
		||||
      done();
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
@ -165,13 +182,12 @@ export function expandNotificationsRequest(isLoadingMore) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
 | 
			
		||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: NOTIFICATIONS_EXPAND_SUCCESS,
 | 
			
		||||
    notifications,
 | 
			
		||||
    accounts: notifications.map(item => item.account),
 | 
			
		||||
    statuses: notifications.map(item => item.status).filter(status => !!status),
 | 
			
		||||
    next,
 | 
			
		||||
    usePendingItems,
 | 
			
		||||
    skipLoading: !isLoadingMore,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
 | 
			
		||||
import api, { getLinks } from 'flavours/glitch/util/api';
 | 
			
		||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import compareId from 'flavours/glitch/util/compare_id';
 | 
			
		||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
 | 
			
		||||
export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
 | 
			
		||||
@ -11,9 +13,14 @@ export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
 | 
			
		||||
export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_SCROLL_TOP   = 'TIMELINE_SCROLL_TOP';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT';
 | 
			
		||||
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
 | 
			
		||||
export const TIMELINE_DISCONNECT   = 'TIMELINE_DISCONNECT';
 | 
			
		||||
export const TIMELINE_CONNECT      = 'TIMELINE_CONNECT';
 | 
			
		||||
 | 
			
		||||
export const loadPending = timeline => ({
 | 
			
		||||
  type: TIMELINE_LOAD_PENDING,
 | 
			
		||||
  timeline,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function updateTimeline(timeline, status, accept) {
 | 
			
		||||
  return dispatch => {
 | 
			
		||||
@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
 | 
			
		||||
      type: TIMELINE_UPDATE,
 | 
			
		||||
      timeline,
 | 
			
		||||
      status,
 | 
			
		||||
      usePendingItems: preferPendingItems,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
 | 
			
		||||
      params.since_id = timeline.getIn(['items', 0]);
 | 
			
		||||
    if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
 | 
			
		||||
      const a = timeline.getIn(['pendingItems', 0]);
 | 
			
		||||
      const b = timeline.getIn(['items', 0]);
 | 
			
		||||
 | 
			
		||||
      if (a && b && compareId(a, b) > 0) {
 | 
			
		||||
        params.since_id = a;
 | 
			
		||||
      } else {
 | 
			
		||||
        params.since_id = b || a;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isLoadingRecent = !!params.since_id;
 | 
			
		||||
@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 | 
			
		||||
    api(getState).get(path, { params }).then(response => {
 | 
			
		||||
      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
			
		||||
      dispatch(importFetchedStatuses(response.data));
 | 
			
		||||
      dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
 | 
			
		||||
      dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
 | 
			
		||||
      done();
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
 | 
			
		||||
@ -117,7 +132,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
 | 
			
		||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: TIMELINE_EXPAND_SUCCESS,
 | 
			
		||||
    timeline,
 | 
			
		||||
@ -125,6 +140,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi
 | 
			
		||||
    next,
 | 
			
		||||
    partial,
 | 
			
		||||
    isLoadingRecent,
 | 
			
		||||
    usePendingItems,
 | 
			
		||||
    skipLoading: !isLoadingMore,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -153,9 +169,8 @@ export function connectTimeline(timeline) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function disconnectTimeline(timeline) {
 | 
			
		||||
  return {
 | 
			
		||||
export const disconnectTimeline = timeline => ({
 | 
			
		||||
  type: TIMELINE_DISCONNECT,
 | 
			
		||||
  timeline,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
  usePendingItems: preferPendingItems,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								app/javascript/flavours/glitch/components/load_pending.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/javascript/flavours/glitch/components/load_pending.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
export default class LoadPending extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
    count: PropTypes.number,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { count } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button className='load-more load-gap' onClick={this.props.onClick}>
 | 
			
		||||
        <FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
 | 
			
		||||
      </button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
 | 
			
		||||
import LoadMore from './load_more';
 | 
			
		||||
import LoadPending from './load_pending';
 | 
			
		||||
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
 | 
			
		||||
import { throttle } from 'lodash';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    scrollKey: PropTypes.string.isRequired,
 | 
			
		||||
    onLoadMore: PropTypes.func,
 | 
			
		||||
    onLoadPending: PropTypes.func,
 | 
			
		||||
    onScrollToTop: PropTypes.func,
 | 
			
		||||
    onScroll: PropTypes.func,
 | 
			
		||||
    trackScroll: PropTypes.bool,
 | 
			
		||||
@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
    isLoading: PropTypes.bool,
 | 
			
		||||
    showLoading: PropTypes.bool,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    numPending: PropTypes.number,
 | 
			
		||||
    prepend: PropTypes.node,
 | 
			
		||||
    alwaysPrepend: PropTypes.bool,
 | 
			
		||||
    emptyMessage: PropTypes.node,
 | 
			
		||||
@ -222,12 +225,18 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
    return !(location.state && location.state.mastodonModalOpen);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadPending = e => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.props.onLoadPending();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { fullscreen } = this.state;
 | 
			
		||||
    const childrenCount = React.Children.count(children);
 | 
			
		||||
 | 
			
		||||
    const loadMore     = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
 | 
			
		||||
    const loadPending  = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
 | 
			
		||||
    let scrollableArea = null;
 | 
			
		||||
 | 
			
		||||
    if (showLoading) {
 | 
			
		||||
@ -248,6 +257,8 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
          <div role='feed' className='item-list'>
 | 
			
		||||
            {prepend}
 | 
			
		||||
 | 
			
		||||
            {loadPending}
 | 
			
		||||
 | 
			
		||||
            {React.Children.map(this.props.children, (child, index) => (
 | 
			
		||||
              <IntersectionObserverArticleContainer
 | 
			
		||||
                key={child.key}
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ export default class ColumnSettings extends React.PureComponent {
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <div className='column-settings__row'>
 | 
			
		||||
          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
 | 
			
		||||
          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent {
 | 
			
		||||
    label: PropTypes.node.isRequired,
 | 
			
		||||
    meta: PropTypes.node,
 | 
			
		||||
    onChange: PropTypes.func.isRequired,
 | 
			
		||||
    defaultValue: PropTypes.bool,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChange = ({ target }) => {
 | 
			
		||||
@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { prefix, settings, settingPath, label, meta } = this.props;
 | 
			
		||||
    const { prefix, settings, settingPath, label, meta, defaultValue } = this.props;
 | 
			
		||||
    const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='setting-toggle'>
 | 
			
		||||
        <Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
 | 
			
		||||
        <Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
 | 
			
		||||
        <label htmlFor={id} className='setting-toggle__label'>{label}</label>
 | 
			
		||||
        {meta && <span className='setting-meta__label'>{meta}</span>}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import {
 | 
			
		||||
  scrollTopNotifications,
 | 
			
		||||
  mountNotifications,
 | 
			
		||||
  unmountNotifications,
 | 
			
		||||
  loadPending,
 | 
			
		||||
} from 'flavours/glitch/actions/notifications';
 | 
			
		||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 | 
			
		||||
import NotificationContainer from './containers/notification_container';
 | 
			
		||||
@ -48,6 +49,7 @@ const mapStateToProps = state => ({
 | 
			
		||||
  isLoading: state.getIn(['notifications', 'isLoading'], true),
 | 
			
		||||
  isUnread: state.getIn(['notifications', 'unread']) > 0,
 | 
			
		||||
  hasMore: state.getIn(['notifications', 'hasMore']),
 | 
			
		||||
  numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
 | 
			
		||||
  notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -80,6 +82,7 @@ export default class Notifications extends React.PureComponent {
 | 
			
		||||
    isUnread: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    numPending: PropTypes.number,
 | 
			
		||||
    localSettings: ImmutablePropTypes.map,
 | 
			
		||||
    notifCleaningActive: PropTypes.bool,
 | 
			
		||||
    onEnterCleaningMode: PropTypes.func,
 | 
			
		||||
@ -100,6 +103,10 @@ export default class Notifications extends React.PureComponent {
 | 
			
		||||
    this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
 | 
			
		||||
  }, 300, { leading: true });
 | 
			
		||||
 | 
			
		||||
  handleLoadPending = () => {
 | 
			
		||||
    this.props.dispatch(loadPending());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleScrollToTop = debounce(() => {
 | 
			
		||||
    this.props.dispatch(scrollTopNotifications(true));
 | 
			
		||||
  }, 100);
 | 
			
		||||
@ -170,7 +177,7 @@ export default class Notifications extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
 | 
			
		||||
    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
 | 
			
		||||
    const pinned = !!columnId;
 | 
			
		||||
    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
 | 
			
		||||
 | 
			
		||||
@ -212,8 +219,10 @@ export default class Notifications extends React.PureComponent {
 | 
			
		||||
        isLoading={isLoading}
 | 
			
		||||
        showLoading={isLoading && notifications.size === 0}
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        numPending={numPending}
 | 
			
		||||
        emptyMessage={emptyMessage}
 | 
			
		||||
        onLoadMore={this.handleLoadOlder}
 | 
			
		||||
        onLoadPending={this.handleLoadPending}
 | 
			
		||||
        onScrollToTop={this.handleScrollToTop}
 | 
			
		||||
        onScroll={this.handleScroll}
 | 
			
		||||
        shouldUpdateScroll={shouldUpdateScroll}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import StatusList from 'flavours/glitch/components/status_list';
 | 
			
		||||
import { scrollTopTimeline } from 'flavours/glitch/actions/timelines';
 | 
			
		||||
import { scrollTopTimeline, loadPending } from 'flavours/glitch/actions/timelines';
 | 
			
		||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
@ -62,6 +62,7 @@ const makeMapStateToProps = () => {
 | 
			
		||||
    isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
 | 
			
		||||
    isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
 | 
			
		||||
    hasMore:   state.getIn(['timelines', timelineId, 'hasMore']),
 | 
			
		||||
    numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mapStateToProps;
 | 
			
		||||
@ -77,6 +78,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
 | 
			
		||||
    dispatch(scrollTopTimeline(timelineId, false));
 | 
			
		||||
  }, 100),
 | 
			
		||||
 | 
			
		||||
  onLoadPending: () => dispatch(loadPending(timelineId)),
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import {
 | 
			
		||||
  NOTIFICATIONS_FILTER_SET,
 | 
			
		||||
  NOTIFICATIONS_CLEAR,
 | 
			
		||||
  NOTIFICATIONS_SCROLL_TOP,
 | 
			
		||||
  NOTIFICATIONS_LOAD_PENDING,
 | 
			
		||||
  NOTIFICATIONS_DELETE_MARKED_REQUEST,
 | 
			
		||||
  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
 | 
			
		||||
  NOTIFICATION_MARK_FOR_DELETE,
 | 
			
		||||
@ -25,6 +26,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import compareId from 'flavours/glitch/util/compare_id';
 | 
			
		||||
 | 
			
		||||
const initialState = ImmutableMap({
 | 
			
		||||
  pendingItems: ImmutableList(),
 | 
			
		||||
  items: ImmutableList(),
 | 
			
		||||
  hasMore: true,
 | 
			
		||||
  top: false,
 | 
			
		||||
@ -46,7 +48,11 @@ const notificationToMap = (state, notification) => ImmutableMap({
 | 
			
		||||
  status: notification.status ? notification.status.id : null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const normalizeNotification = (state, notification) => {
 | 
			
		||||
const normalizeNotification = (state, notification, usePendingItems) => {
 | 
			
		||||
  if (usePendingItems) {
 | 
			
		||||
    return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const top = !shouldCountUnreadNotifications(state);
 | 
			
		||||
 | 
			
		||||
  if (top) {
 | 
			
		||||
@ -64,7 +70,7 @@ const normalizeNotification = (state, notification) => {
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => {
 | 
			
		||||
  const top = !(shouldCountUnreadNotifications(state));
 | 
			
		||||
  const lastReadId = state.get('lastReadId');
 | 
			
		||||
  let items = ImmutableList();
 | 
			
		||||
@ -75,7 +81,7 @@ const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
 | 
			
		||||
  return state.withMutations(mutable => {
 | 
			
		||||
    if (!items.isEmpty()) {
 | 
			
		||||
      mutable.update('items', list => {
 | 
			
		||||
      mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
 | 
			
		||||
        const lastIndex = 1 + list.findLastIndex(
 | 
			
		||||
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
 | 
			
		||||
        );
 | 
			
		||||
@ -105,7 +111,8 @@ const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterNotifications = (state, relationship) => {
 | 
			
		||||
  return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
 | 
			
		||||
  const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id);
 | 
			
		||||
  return state.update('items', helper).update('pendingItems', helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const clearUnread = (state) => {
 | 
			
		||||
@ -131,7 +138,8 @@ const deleteByStatus = (state, statusId) => {
 | 
			
		||||
    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
 | 
			
		||||
    state = state.update('unread', unread => unread - deletedUnread.size);
 | 
			
		||||
  }
 | 
			
		||||
  return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
 | 
			
		||||
  const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
 | 
			
		||||
  return state.update('items', helper).update('pendingItems', helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const markForDelete = (state, notificationId, yes) => {
 | 
			
		||||
@ -192,6 +200,8 @@ export default function notifications(state = initialState, action) {
 | 
			
		||||
    return state.update('mounted', count => count - 1);
 | 
			
		||||
  case NOTIFICATIONS_SET_VISIBILITY:
 | 
			
		||||
    return updateVisibility(state, action.visibility);
 | 
			
		||||
  case NOTIFICATIONS_LOAD_PENDING:
 | 
			
		||||
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
 | 
			
		||||
  case NOTIFICATIONS_EXPAND_REQUEST:
 | 
			
		||||
  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
 | 
			
		||||
    return state.set('isLoading', true);
 | 
			
		||||
@ -203,20 +213,20 @@ export default function notifications(state = initialState, action) {
 | 
			
		||||
  case NOTIFICATIONS_SCROLL_TOP:
 | 
			
		||||
    return updateTop(state, action.top);
 | 
			
		||||
  case NOTIFICATIONS_UPDATE:
 | 
			
		||||
    return normalizeNotification(state, action.notification);
 | 
			
		||||
    return normalizeNotification(state, action.notification, action.usePendingItems);
 | 
			
		||||
  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
			
		||||
    return expandNormalizedNotifications(state, action.notifications, action.next);
 | 
			
		||||
    return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems);
 | 
			
		||||
  case ACCOUNT_BLOCK_SUCCESS:
 | 
			
		||||
    return filterNotifications(state, action.relationship);
 | 
			
		||||
  case ACCOUNT_MUTE_SUCCESS:
 | 
			
		||||
    return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
 | 
			
		||||
  case NOTIFICATIONS_CLEAR:
 | 
			
		||||
    return state.set('items', ImmutableList()).set('hasMore', false);
 | 
			
		||||
    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
 | 
			
		||||
  case TIMELINE_DELETE:
 | 
			
		||||
    return deleteByStatus(state, action.id);
 | 
			
		||||
  case TIMELINE_DISCONNECT:
 | 
			
		||||
    return action.timeline === 'home' ?
 | 
			
		||||
      state.update('items', items => items.first() ? items.unshift(null) : items) :
 | 
			
		||||
      state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
 | 
			
		||||
      state;
 | 
			
		||||
 | 
			
		||||
  case NOTIFICATION_MARK_FOR_DELETE:
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import {
 | 
			
		||||
  TIMELINE_SCROLL_TOP,
 | 
			
		||||
  TIMELINE_CONNECT,
 | 
			
		||||
  TIMELINE_DISCONNECT,
 | 
			
		||||
  TIMELINE_LOAD_PENDING,
 | 
			
		||||
} from 'flavours/glitch/actions/timelines';
 | 
			
		||||
import {
 | 
			
		||||
  ACCOUNT_BLOCK_SUCCESS,
 | 
			
		||||
@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({
 | 
			
		||||
  top: true,
 | 
			
		||||
  isLoading: false,
 | 
			
		||||
  hasMore: true,
 | 
			
		||||
  pendingItems: ImmutableList(),
 | 
			
		||||
  items: ImmutableList(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
 | 
			
		||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
 | 
			
		||||
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
 | 
			
		||||
    mMap.set('isLoading', false);
 | 
			
		||||
    mMap.set('isPartial', isPartial);
 | 
			
		||||
@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 | 
			
		||||
    if (timeline.endsWith(':pinned')) {
 | 
			
		||||
      mMap.set('items', statuses.map(status => status.get('id')));
 | 
			
		||||
    } else if (!statuses.isEmpty()) {
 | 
			
		||||
      mMap.update('items', ImmutableList(), oldIds => {
 | 
			
		||||
      mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
 | 
			
		||||
        const newIds = statuses.map(status => status.get('id'));
 | 
			
		||||
        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
 | 
			
		||||
        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
 | 
			
		||||
@ -56,7 +58,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTimeline = (state, timeline, status) => {
 | 
			
		||||
const updateTimeline = (state, timeline, status, usePendingItems) => {
 | 
			
		||||
  if (usePendingItems) {
 | 
			
		||||
    if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
 | 
			
		||||
      return state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const top        = state.getIn([timeline, 'top']);
 | 
			
		||||
  const ids        = state.getIn([timeline, 'items'], ImmutableList());
 | 
			
		||||
  const includesId = ids.includes(status.get('id'));
 | 
			
		||||
@ -77,8 +87,10 @@ const updateTimeline = (state, timeline, status) => {
 | 
			
		||||
 | 
			
		||||
const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
 | 
			
		||||
  state.keySeq().forEach(timeline => {
 | 
			
		||||
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
 | 
			
		||||
      state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
 | 
			
		||||
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
 | 
			
		||||
      const helper = list => list.filterNot(item => item === id);
 | 
			
		||||
      state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Remove reblogs of deleted status
 | 
			
		||||
@ -108,11 +120,10 @@ const filterTimelines = (state, relationship, statuses) => {
 | 
			
		||||
  return state;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterTimeline = (timeline, state, relationship, statuses) =>
 | 
			
		||||
  state.updateIn([timeline, 'items'], ImmutableList(), list =>
 | 
			
		||||
    list.filterNot(statusId =>
 | 
			
		||||
      statuses.getIn([statusId, 'account']) === relationship.id
 | 
			
		||||
    ));
 | 
			
		||||
const filterTimeline = (timeline, state, relationship, statuses) => {
 | 
			
		||||
  const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id);
 | 
			
		||||
  return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTop = (state, timeline, top) => {
 | 
			
		||||
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
 | 
			
		||||
@ -123,14 +134,17 @@ const updateTop = (state, timeline, top) => {
 | 
			
		||||
 | 
			
		||||
export default function timelines(state = initialState, action) {
 | 
			
		||||
  switch(action.type) {
 | 
			
		||||
  case TIMELINE_LOAD_PENDING:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map =>
 | 
			
		||||
      map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0));
 | 
			
		||||
  case TIMELINE_EXPAND_REQUEST:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
 | 
			
		||||
  case TIMELINE_EXPAND_FAIL:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
 | 
			
		||||
  case TIMELINE_EXPAND_SUCCESS:
 | 
			
		||||
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
 | 
			
		||||
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
 | 
			
		||||
  case TIMELINE_UPDATE:
 | 
			
		||||
    return updateTimeline(state, action.timeline, fromJS(action.status));
 | 
			
		||||
    return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
 | 
			
		||||
  case TIMELINE_DELETE:
 | 
			
		||||
    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
 | 
			
		||||
  case TIMELINE_CLEAR:
 | 
			
		||||
@ -148,7 +162,7 @@ export default function timelines(state = initialState, action) {
 | 
			
		||||
    return state.update(
 | 
			
		||||
      action.timeline,
 | 
			
		||||
      initialTimeline,
 | 
			
		||||
      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
 | 
			
		||||
      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
 | 
			
		||||
    );
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
export default function compareId(id1, id2) {
 | 
			
		||||
export default function compareId (id1, id2) {
 | 
			
		||||
  if (id1 === id2) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (id1.length === id2.length) {
 | 
			
		||||
    return id1 > id2 ? 1 : -1;
 | 
			
		||||
  } else {
 | 
			
		||||
    return id1.length > id2.length ? 1 : -1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -30,5 +30,6 @@ export const isStaff = getMeta('is_staff');
 | 
			
		||||
export const defaultContentType = getMeta('default_content_type');
 | 
			
		||||
export const forceSingleColumn = getMeta('advanced_layout') === false;
 | 
			
		||||
export const useBlurhash = getMeta('use_blurhash');
 | 
			
		||||
export const usePendingItems = getMeta('use_pending_items');
 | 
			
		||||
 | 
			
		||||
export default initialState;
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import { unescapeHTML } from '../utils/html';
 | 
			
		||||
import { getFiltersRegex } from '../selectors';
 | 
			
		||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
 | 
			
		||||
import compareId from 'mastodon/compare_id';
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_UPDATE      = 'NOTIFICATIONS_UPDATE';
 | 
			
		||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
 | 
			
		||||
@ -24,6 +26,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
 | 
			
		||||
 | 
			
		||||
export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
 | 
			
		||||
export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
 | 
			
		||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
 | 
			
		||||
 | 
			
		||||
defineMessages({
 | 
			
		||||
  mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
 | 
			
		||||
@ -38,6 +41,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const loadPending = () => ({
 | 
			
		||||
  type: NOTIFICATIONS_LOAD_PENDING,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
 | 
			
		||||
@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
			
		||||
      dispatch({
 | 
			
		||||
        type: NOTIFICATIONS_UPDATE,
 | 
			
		||||
        notification,
 | 
			
		||||
        usePendingItems: preferPendingItems,
 | 
			
		||||
        meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -122,9 +130,18 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 | 
			
		||||
        : excludeTypesFromFilter(activeFilter),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!maxId && notifications.get('items').size > 0) {
 | 
			
		||||
      params.since_id = notifications.getIn(['items', 0, 'id']);
 | 
			
		||||
    if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
 | 
			
		||||
      const a = notifications.getIn(['pendingItems', 0, 'id']);
 | 
			
		||||
      const b = notifications.getIn(['items', 0, 'id']);
 | 
			
		||||
 | 
			
		||||
      if (a && b && compareId(a, b) > 0) {
 | 
			
		||||
        params.since_id = a;
 | 
			
		||||
      } else {
 | 
			
		||||
        params.since_id = b || a;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isLoadingRecent = !!params.since_id;
 | 
			
		||||
 | 
			
		||||
    dispatch(expandNotificationsRequest(isLoadingMore));
 | 
			
		||||
 | 
			
		||||
@ -134,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 | 
			
		||||
      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
 | 
			
		||||
      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
 | 
			
		||||
 | 
			
		||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
 | 
			
		||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems));
 | 
			
		||||
      fetchRelatedRelationships(dispatch, response.data);
 | 
			
		||||
      done();
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
@ -151,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
 | 
			
		||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: NOTIFICATIONS_EXPAND_SUCCESS,
 | 
			
		||||
    notifications,
 | 
			
		||||
    next,
 | 
			
		||||
    usePendingItems,
 | 
			
		||||
    skipLoading: !isLoadingMore,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
 | 
			
		||||
import api, { getLinks } from '../api';
 | 
			
		||||
import api, { getLinks } from 'mastodon/api';
 | 
			
		||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import compareId from 'mastodon/compare_id';
 | 
			
		||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
 | 
			
		||||
export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
 | 
			
		||||
@ -11,9 +13,14 @@ export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
 | 
			
		||||
export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_SCROLL_TOP   = 'TIMELINE_SCROLL_TOP';
 | 
			
		||||
 | 
			
		||||
export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT';
 | 
			
		||||
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
 | 
			
		||||
export const TIMELINE_DISCONNECT   = 'TIMELINE_DISCONNECT';
 | 
			
		||||
export const TIMELINE_CONNECT      = 'TIMELINE_CONNECT';
 | 
			
		||||
 | 
			
		||||
export const loadPending = timeline => ({
 | 
			
		||||
  type: TIMELINE_LOAD_PENDING,
 | 
			
		||||
  timeline,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function updateTimeline(timeline, status, accept) {
 | 
			
		||||
  return dispatch => {
 | 
			
		||||
@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
 | 
			
		||||
      type: TIMELINE_UPDATE,
 | 
			
		||||
      timeline,
 | 
			
		||||
      status,
 | 
			
		||||
      usePendingItems: preferPendingItems,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
 | 
			
		||||
      params.since_id = timeline.getIn(['items', 0]);
 | 
			
		||||
    if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
 | 
			
		||||
      const a = timeline.getIn(['pendingItems', 0]);
 | 
			
		||||
      const b = timeline.getIn(['items', 0]);
 | 
			
		||||
 | 
			
		||||
      if (a && b && compareId(a, b) > 0) {
 | 
			
		||||
        params.since_id = a;
 | 
			
		||||
      } else {
 | 
			
		||||
        params.since_id = b || a;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isLoadingRecent = !!params.since_id;
 | 
			
		||||
@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 | 
			
		||||
    api(getState).get(path, { params }).then(response => {
 | 
			
		||||
      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
			
		||||
      dispatch(importFetchedStatuses(response.data));
 | 
			
		||||
      dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
 | 
			
		||||
      dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
 | 
			
		||||
      done();
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
 | 
			
		||||
@ -115,7 +130,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
 | 
			
		||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: TIMELINE_EXPAND_SUCCESS,
 | 
			
		||||
    timeline,
 | 
			
		||||
@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi
 | 
			
		||||
    next,
 | 
			
		||||
    partial,
 | 
			
		||||
    isLoadingRecent,
 | 
			
		||||
    usePendingItems,
 | 
			
		||||
    skipLoading: !isLoadingMore,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -151,9 +167,8 @@ export function connectTimeline(timeline) {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function disconnectTimeline(timeline) {
 | 
			
		||||
  return {
 | 
			
		||||
export const disconnectTimeline = timeline => ({
 | 
			
		||||
  type: TIMELINE_DISCONNECT,
 | 
			
		||||
  timeline,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
  usePendingItems: preferPendingItems,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
export default function compareId(id1, id2) {
 | 
			
		||||
export default function compareId (id1, id2) {
 | 
			
		||||
  if (id1 === id2) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (id1.length === id2.length) {
 | 
			
		||||
    return id1 > id2 ? 1 : -1;
 | 
			
		||||
  } else {
 | 
			
		||||
    return id1.length > id2.length ? 1 : -1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								app/javascript/mastodon/components/load_pending.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/javascript/mastodon/components/load_pending.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
export default class LoadPending extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
    count: PropTypes.number,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { count } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button className='load-more load-gap' onClick={this.props.onClick}>
 | 
			
		||||
        <FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
 | 
			
		||||
      </button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
 | 
			
		||||
import LoadMore from './load_more';
 | 
			
		||||
import LoadPending from './load_pending';
 | 
			
		||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
 | 
			
		||||
import { throttle } from 'lodash';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    scrollKey: PropTypes.string.isRequired,
 | 
			
		||||
    onLoadMore: PropTypes.func,
 | 
			
		||||
    onLoadPending: PropTypes.func,
 | 
			
		||||
    onScrollToTop: PropTypes.func,
 | 
			
		||||
    onScroll: PropTypes.func,
 | 
			
		||||
    trackScroll: PropTypes.bool,
 | 
			
		||||
@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
    isLoading: PropTypes.bool,
 | 
			
		||||
    showLoading: PropTypes.bool,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    numPending: PropTypes.number,
 | 
			
		||||
    prepend: PropTypes.node,
 | 
			
		||||
    alwaysPrepend: PropTypes.bool,
 | 
			
		||||
    emptyMessage: PropTypes.node,
 | 
			
		||||
@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
    this.props.onLoadMore();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadPending = e => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.props.onLoadPending();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { fullscreen } = this.state;
 | 
			
		||||
    const childrenCount = React.Children.count(children);
 | 
			
		||||
 | 
			
		||||
    const loadMore     = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
 | 
			
		||||
    const loadPending  = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
 | 
			
		||||
    let scrollableArea = null;
 | 
			
		||||
 | 
			
		||||
    if (showLoading) {
 | 
			
		||||
@ -251,6 +260,8 @@ export default class ScrollableList extends PureComponent {
 | 
			
		||||
          <div role='feed' className='item-list'>
 | 
			
		||||
            {prepend}
 | 
			
		||||
 | 
			
		||||
            {loadPending}
 | 
			
		||||
 | 
			
		||||
            {React.Children.map(this.props.children, (child, index) => (
 | 
			
		||||
              <IntersectionObserverArticleContainer
 | 
			
		||||
                key={child.key}
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ class ColumnSettings extends React.PureComponent {
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <div className='column-settings__row'>
 | 
			
		||||
          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
 | 
			
		||||
          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent {
 | 
			
		||||
    settingPath: PropTypes.array.isRequired,
 | 
			
		||||
    label: PropTypes.node.isRequired,
 | 
			
		||||
    onChange: PropTypes.func.isRequired,
 | 
			
		||||
    defaultValue: PropTypes.bool,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChange = ({ target }) => {
 | 
			
		||||
@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { prefix, settings, settingPath, label } = this.props;
 | 
			
		||||
    const { prefix, settings, settingPath, label, defaultValue } = this.props;
 | 
			
		||||
    const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='setting-toggle'>
 | 
			
		||||
        <Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
 | 
			
		||||
        <Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
 | 
			
		||||
        <label htmlFor={id} className='setting-toggle__label'>{label}</label>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import Column from '../../components/column';
 | 
			
		||||
import ColumnHeader from '../../components/column_header';
 | 
			
		||||
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
 | 
			
		||||
import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications';
 | 
			
		||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 | 
			
		||||
import NotificationContainer from './containers/notification_container';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
@ -41,6 +41,7 @@ const mapStateToProps = state => ({
 | 
			
		||||
  isLoading: state.getIn(['notifications', 'isLoading'], true),
 | 
			
		||||
  isUnread: state.getIn(['notifications', 'unread']) > 0,
 | 
			
		||||
  hasMore: state.getIn(['notifications', 'hasMore']),
 | 
			
		||||
  numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
@ -58,6 +59,7 @@ class Notifications extends React.PureComponent {
 | 
			
		||||
    isUnread: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    numPending: PropTypes.number,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
@ -80,6 +82,10 @@ class Notifications extends React.PureComponent {
 | 
			
		||||
    this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
 | 
			
		||||
  }, 300, { leading: true });
 | 
			
		||||
 | 
			
		||||
  handleLoadPending = () => {
 | 
			
		||||
    this.props.dispatch(loadPending());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleScrollToTop = debounce(() => {
 | 
			
		||||
    this.props.dispatch(scrollTopNotifications(true));
 | 
			
		||||
  }, 100);
 | 
			
		||||
@ -136,7 +142,7 @@ class Notifications extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
 | 
			
		||||
    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
 | 
			
		||||
    const pinned = !!columnId;
 | 
			
		||||
    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
 | 
			
		||||
 | 
			
		||||
@ -178,8 +184,10 @@ class Notifications extends React.PureComponent {
 | 
			
		||||
        isLoading={isLoading}
 | 
			
		||||
        showLoading={isLoading && notifications.size === 0}
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        numPending={numPending}
 | 
			
		||||
        emptyMessage={emptyMessage}
 | 
			
		||||
        onLoadMore={this.handleLoadOlder}
 | 
			
		||||
        onLoadPending={this.handleLoadPending}
 | 
			
		||||
        onScrollToTop={this.handleScrollToTop}
 | 
			
		||||
        onScroll={this.handleScroll}
 | 
			
		||||
        shouldUpdateScroll={shouldUpdateScroll}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import StatusList from '../../../components/status_list';
 | 
			
		||||
import { scrollTopTimeline } from '../../../actions/timelines';
 | 
			
		||||
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
 | 
			
		||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
@ -37,6 +37,7 @@ const makeMapStateToProps = () => {
 | 
			
		||||
    isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
 | 
			
		||||
    isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
 | 
			
		||||
    hasMore:   state.getIn(['timelines', timelineId, 'hasMore']),
 | 
			
		||||
    numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mapStateToProps;
 | 
			
		||||
@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
 | 
			
		||||
    dispatch(scrollTopTimeline(timelineId, false));
 | 
			
		||||
  }, 100),
 | 
			
		||||
 | 
			
		||||
  onLoadPending: () => dispatch(loadPending(timelineId)),
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
 | 
			
		||||
 | 
			
		||||
@ -22,5 +22,6 @@ export const profile_directory = getMeta('profile_directory');
 | 
			
		||||
export const isStaff = getMeta('is_staff');
 | 
			
		||||
export const forceSingleColumn = !getMeta('advanced_layout');
 | 
			
		||||
export const useBlurhash = getMeta('use_blurhash');
 | 
			
		||||
export const usePendingItems = getMeta('use_pending_items');
 | 
			
		||||
 | 
			
		||||
export default initialState;
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import {
 | 
			
		||||
  NOTIFICATIONS_FILTER_SET,
 | 
			
		||||
  NOTIFICATIONS_CLEAR,
 | 
			
		||||
  NOTIFICATIONS_SCROLL_TOP,
 | 
			
		||||
  NOTIFICATIONS_LOAD_PENDING,
 | 
			
		||||
} from '../actions/notifications';
 | 
			
		||||
import {
 | 
			
		||||
  ACCOUNT_BLOCK_SUCCESS,
 | 
			
		||||
@ -16,6 +17,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
import compareId from '../compare_id';
 | 
			
		||||
 | 
			
		||||
const initialState = ImmutableMap({
 | 
			
		||||
  pendingItems: ImmutableList(),
 | 
			
		||||
  items: ImmutableList(),
 | 
			
		||||
  hasMore: true,
 | 
			
		||||
  top: false,
 | 
			
		||||
@ -31,7 +33,11 @@ const notificationToMap = notification => ImmutableMap({
 | 
			
		||||
  status: notification.status ? notification.status.id : null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const normalizeNotification = (state, notification) => {
 | 
			
		||||
const normalizeNotification = (state, notification, usePendingItems) => {
 | 
			
		||||
  if (usePendingItems) {
 | 
			
		||||
    return state.update('pendingItems', list => list.unshift(notificationToMap(notification)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const top = state.get('top');
 | 
			
		||||
 | 
			
		||||
  if (!top) {
 | 
			
		||||
@ -47,7 +53,7 @@ const normalizeNotification = (state, notification) => {
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => {
 | 
			
		||||
  let items = ImmutableList();
 | 
			
		||||
 | 
			
		||||
  notifications.forEach((n, i) => {
 | 
			
		||||
@ -56,7 +62,7 @@ const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
 | 
			
		||||
  return state.withMutations(mutable => {
 | 
			
		||||
    if (!items.isEmpty()) {
 | 
			
		||||
      mutable.update('items', list => {
 | 
			
		||||
      mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
 | 
			
		||||
        const lastIndex = 1 + list.findLastIndex(
 | 
			
		||||
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
 | 
			
		||||
        );
 | 
			
		||||
@ -78,7 +84,8 @@ const expandNormalizedNotifications = (state, notifications, next) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterNotifications = (state, relationship) => {
 | 
			
		||||
  return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
 | 
			
		||||
  const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id);
 | 
			
		||||
  return state.update('items', helper).update('pendingItems', helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTop = (state, top) => {
 | 
			
		||||
@ -90,34 +97,37 @@ const updateTop = (state, top) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteByStatus = (state, statusId) => {
 | 
			
		||||
  return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
 | 
			
		||||
  const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
 | 
			
		||||
  return state.update('items', helper).update('pendingItems', helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function notifications(state = initialState, action) {
 | 
			
		||||
  switch(action.type) {
 | 
			
		||||
  case NOTIFICATIONS_LOAD_PENDING:
 | 
			
		||||
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
 | 
			
		||||
  case NOTIFICATIONS_EXPAND_REQUEST:
 | 
			
		||||
    return state.set('isLoading', true);
 | 
			
		||||
  case NOTIFICATIONS_EXPAND_FAIL:
 | 
			
		||||
    return state.set('isLoading', false);
 | 
			
		||||
  case NOTIFICATIONS_FILTER_SET:
 | 
			
		||||
    return state.set('items', ImmutableList()).set('hasMore', true);
 | 
			
		||||
    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
 | 
			
		||||
  case NOTIFICATIONS_SCROLL_TOP:
 | 
			
		||||
    return updateTop(state, action.top);
 | 
			
		||||
  case NOTIFICATIONS_UPDATE:
 | 
			
		||||
    return normalizeNotification(state, action.notification);
 | 
			
		||||
    return normalizeNotification(state, action.notification, action.usePendingItems);
 | 
			
		||||
  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
			
		||||
    return expandNormalizedNotifications(state, action.notifications, action.next);
 | 
			
		||||
    return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems);
 | 
			
		||||
  case ACCOUNT_BLOCK_SUCCESS:
 | 
			
		||||
    return filterNotifications(state, action.relationship);
 | 
			
		||||
  case ACCOUNT_MUTE_SUCCESS:
 | 
			
		||||
    return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
 | 
			
		||||
  case NOTIFICATIONS_CLEAR:
 | 
			
		||||
    return state.set('items', ImmutableList()).set('hasMore', false);
 | 
			
		||||
    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
 | 
			
		||||
  case TIMELINE_DELETE:
 | 
			
		||||
    return deleteByStatus(state, action.id);
 | 
			
		||||
  case TIMELINE_DISCONNECT:
 | 
			
		||||
    return action.timeline === 'home' ?
 | 
			
		||||
      state.update('items', items => items.first() ? items.unshift(null) : items) :
 | 
			
		||||
      state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
 | 
			
		||||
      state;
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
 | 
			
		||||
@ -10,8 +10,6 @@ import uuid from '../uuid';
 | 
			
		||||
const initialState = ImmutableMap({
 | 
			
		||||
  saved: true,
 | 
			
		||||
 | 
			
		||||
  onboarded: false,
 | 
			
		||||
 | 
			
		||||
  skinTone: 1,
 | 
			
		||||
 | 
			
		||||
  home: ImmutableMap({
 | 
			
		||||
@ -74,10 +72,6 @@ const initialState = ImmutableMap({
 | 
			
		||||
      body: '',
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
  trends: ImmutableMap({
 | 
			
		||||
    show: true,
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const defaultColumns = fromJS([
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import {
 | 
			
		||||
  TIMELINE_SCROLL_TOP,
 | 
			
		||||
  TIMELINE_CONNECT,
 | 
			
		||||
  TIMELINE_DISCONNECT,
 | 
			
		||||
  TIMELINE_LOAD_PENDING,
 | 
			
		||||
} from '../actions/timelines';
 | 
			
		||||
import {
 | 
			
		||||
  ACCOUNT_BLOCK_SUCCESS,
 | 
			
		||||
@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({
 | 
			
		||||
  top: true,
 | 
			
		||||
  isLoading: false,
 | 
			
		||||
  hasMore: true,
 | 
			
		||||
  pendingItems: ImmutableList(),
 | 
			
		||||
  items: ImmutableList(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
 | 
			
		||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
 | 
			
		||||
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
 | 
			
		||||
    mMap.set('isLoading', false);
 | 
			
		||||
    mMap.set('isPartial', isPartial);
 | 
			
		||||
@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 | 
			
		||||
    if (timeline.endsWith(':pinned')) {
 | 
			
		||||
      mMap.set('items', statuses.map(status => status.get('id')));
 | 
			
		||||
    } else if (!statuses.isEmpty()) {
 | 
			
		||||
      mMap.update('items', ImmutableList(), oldIds => {
 | 
			
		||||
      mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
 | 
			
		||||
        const newIds = statuses.map(status => status.get('id'));
 | 
			
		||||
 | 
			
		||||
        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
 | 
			
		||||
@ -57,7 +59,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTimeline = (state, timeline, status) => {
 | 
			
		||||
const updateTimeline = (state, timeline, status, usePendingItems) => {
 | 
			
		||||
  if (usePendingItems) {
 | 
			
		||||
    if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
 | 
			
		||||
      return state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const top        = state.getIn([timeline, 'top']);
 | 
			
		||||
  const ids        = state.getIn([timeline, 'items'], ImmutableList());
 | 
			
		||||
  const includesId = ids.includes(status.get('id'));
 | 
			
		||||
@ -78,8 +88,10 @@ const updateTimeline = (state, timeline, status) => {
 | 
			
		||||
 | 
			
		||||
const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
 | 
			
		||||
  state.keySeq().forEach(timeline => {
 | 
			
		||||
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
 | 
			
		||||
      state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
 | 
			
		||||
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
 | 
			
		||||
      const helper = list => list.filterNot(item => item === id);
 | 
			
		||||
      state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Remove reblogs of deleted status
 | 
			
		||||
@ -109,11 +121,10 @@ const filterTimelines = (state, relationship, statuses) => {
 | 
			
		||||
  return state;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterTimeline = (timeline, state, relationship, statuses) =>
 | 
			
		||||
  state.updateIn([timeline, 'items'], ImmutableList(), list =>
 | 
			
		||||
    list.filterNot(statusId =>
 | 
			
		||||
      statuses.getIn([statusId, 'account']) === relationship.id
 | 
			
		||||
    ));
 | 
			
		||||
const filterTimeline = (timeline, state, relationship, statuses) => {
 | 
			
		||||
  const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id);
 | 
			
		||||
  return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTop = (state, timeline, top) => {
 | 
			
		||||
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
 | 
			
		||||
@ -124,14 +135,17 @@ const updateTop = (state, timeline, top) => {
 | 
			
		||||
 | 
			
		||||
export default function timelines(state = initialState, action) {
 | 
			
		||||
  switch(action.type) {
 | 
			
		||||
  case TIMELINE_LOAD_PENDING:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map =>
 | 
			
		||||
      map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0));
 | 
			
		||||
  case TIMELINE_EXPAND_REQUEST:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
 | 
			
		||||
  case TIMELINE_EXPAND_FAIL:
 | 
			
		||||
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
 | 
			
		||||
  case TIMELINE_EXPAND_SUCCESS:
 | 
			
		||||
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
 | 
			
		||||
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
 | 
			
		||||
  case TIMELINE_UPDATE:
 | 
			
		||||
    return updateTimeline(state, action.timeline, fromJS(action.status));
 | 
			
		||||
    return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
 | 
			
		||||
  case TIMELINE_DELETE:
 | 
			
		||||
    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
 | 
			
		||||
  case TIMELINE_CLEAR:
 | 
			
		||||
@ -149,7 +163,7 @@ export default function timelines(state = initialState, action) {
 | 
			
		||||
    return state.update(
 | 
			
		||||
      action.timeline,
 | 
			
		||||
      initialTimeline,
 | 
			
		||||
      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
 | 
			
		||||
      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
 | 
			
		||||
    );
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,7 @@ class UserSettingsDecorator
 | 
			
		||||
    user.settings['advanced_layout']     = advanced_layout_preference if change?('setting_advanced_layout')
 | 
			
		||||
    user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
 | 
			
		||||
    user.settings['use_blurhash']        = use_blurhash_preference if change?('setting_use_blurhash')
 | 
			
		||||
    user.settings['use_pending_items']   = use_pending_items_preference if change?('setting_use_pending_items')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def merged_notification_emails
 | 
			
		||||
@ -137,6 +138,10 @@ class UserSettingsDecorator
 | 
			
		||||
    boolean_cast_setting 'setting_use_blurhash'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def use_pending_items_preference
 | 
			
		||||
    boolean_cast_setting 'setting_use_pending_items'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def boolean_cast_setting(key)
 | 
			
		||||
    ActiveModel::Type::Boolean.new.cast(settings[key])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ class User < ApplicationRecord
 | 
			
		||||
  delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
 | 
			
		||||
           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
 | 
			
		||||
           :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
 | 
			
		||||
           :advanced_layout, :default_content_type, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false
 | 
			
		||||
           :advanced_layout, :default_content_type, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
 | 
			
		||||
 | 
			
		||||
  attr_reader :invite_code
 | 
			
		||||
  attr_writer :external
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		||||
      store[:reduce_motion]     = object.current_account.user.setting_reduce_motion
 | 
			
		||||
      store[:advanced_layout]   = object.current_account.user.setting_advanced_layout
 | 
			
		||||
      store[:use_blurhash]      = object.current_account.user.setting_use_blurhash
 | 
			
		||||
      store[:use_pending_items] = object.current_account.user.setting_use_pending_items
 | 
			
		||||
      store[:is_staff]          = object.current_account.user.staff?
 | 
			
		||||
      store[:default_content_type] = object.current_account.user.setting_default_content_type
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,9 @@
 | 
			
		||||
 | 
			
		||||
  %h4= t 'appearance.animations_and_accessibility'
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true
 | 
			
		||||
    = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ en:
 | 
			
		||||
        setting_show_application: The application you use to toot will be displayed in the detailed view of your toots
 | 
			
		||||
        setting_skin: Reskins the selected Mastodon flavour
 | 
			
		||||
        setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
 | 
			
		||||
        setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
 | 
			
		||||
        username: Your username will be unique on %{domain}
 | 
			
		||||
        whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
 | 
			
		||||
      featured_tag:
 | 
			
		||||
@ -122,6 +123,7 @@ en:
 | 
			
		||||
        setting_system_font_ui: Use system's default font
 | 
			
		||||
        setting_unfollow_modal: Show confirmation dialog before unfollowing someone
 | 
			
		||||
        setting_use_blurhash: Show colorful gradients for hidden media
 | 
			
		||||
        setting_use_pending_items: Slow mode
 | 
			
		||||
        severity: Severity
 | 
			
		||||
        type: Import type
 | 
			
		||||
        username: Username
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,9 @@ threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i
 | 
			
		||||
threads threads_count, threads_count
 | 
			
		||||
 | 
			
		||||
if ENV['SOCKET']
 | 
			
		||||
  bind 'unix://' + ENV['SOCKET']
 | 
			
		||||
  bind "unix://#{ENV['SOCKET']}"
 | 
			
		||||
else
 | 
			
		||||
  port ENV.fetch('PORT') { 3000 }
 | 
			
		||||
  bind "tcp://#{ENV.fetch('BIND', '127.0.0.1')}:#{ENV.fetch('PORT', 3000)}"
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
environment ENV.fetch('RAILS_ENV') { 'development' }
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,7 @@ defaults: &defaults
 | 
			
		||||
  aggregate_reblogs: true
 | 
			
		||||
  advanced_layout: false
 | 
			
		||||
  use_blurhash: true
 | 
			
		||||
  use_pending_items: false
 | 
			
		||||
  notification_emails:
 | 
			
		||||
    follow: false
 | 
			
		||||
    reblog: false
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@ services:
 | 
			
		||||
    image: tootsuite/mastodon
 | 
			
		||||
    restart: always
 | 
			
		||||
    env_file: .env.production
 | 
			
		||||
    command: yarn start
 | 
			
		||||
    command: BIND=0.0.0.0 node ./streaming
 | 
			
		||||
    networks:
 | 
			
		||||
      - external_network
 | 
			
		||||
      - internal_network
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@ -71,7 +71,7 @@
 | 
			
		||||
    "@babel/plugin-transform-runtime": "^7.4.4",
 | 
			
		||||
    "@babel/preset-env": "^7.4.5",
 | 
			
		||||
    "@babel/preset-react": "^7.0.0",
 | 
			
		||||
    "@babel/runtime": "^7.4.5",
 | 
			
		||||
    "@babel/runtime": "^7.5.4",
 | 
			
		||||
    "@clusterws/cws": "^0.14.0",
 | 
			
		||||
    "array-includes": "^3.0.3",
 | 
			
		||||
    "atrament": "^0.2.3",
 | 
			
		||||
@ -109,7 +109,7 @@
 | 
			
		||||
    "intl-relativeformat": "^6.4.2",
 | 
			
		||||
    "is-nan": "^1.2.1",
 | 
			
		||||
    "js-yaml": "^3.13.1",
 | 
			
		||||
    "lodash": "^4.17.13",
 | 
			
		||||
    "lodash": "^4.17.14",
 | 
			
		||||
    "mark-loader": "^0.1.6",
 | 
			
		||||
    "marky": "^1.2.1",
 | 
			
		||||
    "mini-css-extract-plugin": "^0.7.0",
 | 
			
		||||
@ -161,7 +161,7 @@
 | 
			
		||||
    "throng": "^4.0.0",
 | 
			
		||||
    "tiny-queue": "^0.2.1",
 | 
			
		||||
    "uuid": "^3.1.0",
 | 
			
		||||
    "webpack": "^4.34.0",
 | 
			
		||||
    "webpack": "^4.35.3",
 | 
			
		||||
    "webpack-assets-manifest": "^3.1.1",
 | 
			
		||||
    "webpack-bundle-analyzer": "^3.3.2",
 | 
			
		||||
    "webpack-cli": "^3.3.5",
 | 
			
		||||
@ -174,8 +174,8 @@
 | 
			
		||||
    "enzyme": "^3.10.0",
 | 
			
		||||
    "enzyme-adapter-react-16": "^1.14.0",
 | 
			
		||||
    "eslint": "^5.16.0",
 | 
			
		||||
    "eslint-plugin-import": "~2.17.3",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "~6.2.1",
 | 
			
		||||
    "eslint-plugin-import": "~2.18.0",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "~6.2.3",
 | 
			
		||||
    "eslint-plugin-promise": "~4.2.1",
 | 
			
		||||
    "eslint-plugin-react": "~7.14.2",
 | 
			
		||||
    "jest": "^24.8.0",
 | 
			
		||||
 | 
			
		||||
@ -678,7 +678,7 @@ const attachServerWithConfig = (server, onSuccess) => {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    server.listen(+process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => {
 | 
			
		||||
    server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => {
 | 
			
		||||
      if (onSuccess) {
 | 
			
		||||
        onSuccess(`${server.address().address}:${server.address().port}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								yarn.lock
									
									
									
									
									
								
							@ -768,10 +768,10 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    regenerator-runtime "^0.12.0"
 | 
			
		||||
 | 
			
		||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5":
 | 
			
		||||
  version "7.4.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
 | 
			
		||||
  integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
 | 
			
		||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4":
 | 
			
		||||
  version "7.5.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
 | 
			
		||||
  integrity sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    regenerator-runtime "^0.13.2"
 | 
			
		||||
 | 
			
		||||
@ -1344,11 +1344,6 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
 | 
			
		||||
    mime-types "~2.1.24"
 | 
			
		||||
    negotiator "0.6.2"
 | 
			
		||||
 | 
			
		||||
acorn-dynamic-import@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
 | 
			
		||||
  integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
 | 
			
		||||
 | 
			
		||||
acorn-globals@^4.1.0:
 | 
			
		||||
  version "4.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103"
 | 
			
		||||
@ -1384,10 +1379,10 @@ acorn@^5.5.0, acorn@^5.5.3:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
 | 
			
		||||
  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
 | 
			
		||||
 | 
			
		||||
acorn@^6.0.1, acorn@^6.0.5, acorn@^6.0.7:
 | 
			
		||||
  version "6.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
 | 
			
		||||
  integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
 | 
			
		||||
acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.0:
 | 
			
		||||
  version "6.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3"
 | 
			
		||||
  integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==
 | 
			
		||||
 | 
			
		||||
airbnb-prop-types@^2.13.2:
 | 
			
		||||
  version "2.13.2"
 | 
			
		||||
@ -3654,10 +3649,10 @@ eslint-module-utils@^2.4.0:
 | 
			
		||||
    debug "^2.6.8"
 | 
			
		||||
    pkg-dir "^2.0.0"
 | 
			
		||||
 | 
			
		||||
eslint-plugin-import@~2.17.3:
 | 
			
		||||
  version "2.17.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189"
 | 
			
		||||
  integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q==
 | 
			
		||||
eslint-plugin-import@~2.18.0:
 | 
			
		||||
  version "2.18.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678"
 | 
			
		||||
  integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    array-includes "^3.0.3"
 | 
			
		||||
    contains-path "^0.1.0"
 | 
			
		||||
@ -3671,11 +3666,12 @@ eslint-plugin-import@~2.17.3:
 | 
			
		||||
    read-pkg-up "^2.0.0"
 | 
			
		||||
    resolve "^1.11.0"
 | 
			
		||||
 | 
			
		||||
eslint-plugin-jsx-a11y@~6.2.1:
 | 
			
		||||
  version "6.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz#4ebba9f339b600ff415ae4166e3e2e008831cf0c"
 | 
			
		||||
  integrity sha512-cjN2ObWrRz0TTw7vEcGQrx+YltMvZoOEx4hWU8eEERDnBIU00OTq7Vr+jA7DFKxiwLNv4tTh5Pq2GUNEa8b6+w==
 | 
			
		||||
eslint-plugin-jsx-a11y@~6.2.3:
 | 
			
		||||
  version "6.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa"
 | 
			
		||||
  integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.4.5"
 | 
			
		||||
    aria-query "^3.0.0"
 | 
			
		||||
    array-includes "^3.0.3"
 | 
			
		||||
    ast-types-flow "^0.0.7"
 | 
			
		||||
@ -3683,7 +3679,7 @@ eslint-plugin-jsx-a11y@~6.2.1:
 | 
			
		||||
    damerau-levenshtein "^1.0.4"
 | 
			
		||||
    emoji-regex "^7.0.2"
 | 
			
		||||
    has "^1.0.3"
 | 
			
		||||
    jsx-ast-utils "^2.0.1"
 | 
			
		||||
    jsx-ast-utils "^2.2.1"
 | 
			
		||||
 | 
			
		||||
eslint-plugin-promise@~4.2.1:
 | 
			
		||||
  version "4.2.1"
 | 
			
		||||
@ -6045,7 +6041,7 @@ jsprim@^1.2.2:
 | 
			
		||||
    json-schema "0.2.3"
 | 
			
		||||
    verror "1.10.0"
 | 
			
		||||
 | 
			
		||||
jsx-ast-utils@^2.0.1, jsx-ast-utils@^2.1.0:
 | 
			
		||||
jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1:
 | 
			
		||||
  version "2.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb"
 | 
			
		||||
  integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==
 | 
			
		||||
@ -6264,10 +6260,10 @@ lodash.uniq@^4.5.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 | 
			
		||||
  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 | 
			
		||||
 | 
			
		||||
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
 | 
			
		||||
  version "4.17.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
 | 
			
		||||
  integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
 | 
			
		||||
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10:
 | 
			
		||||
  version "4.17.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
 | 
			
		||||
  integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
 | 
			
		||||
 | 
			
		||||
loglevel@^1.6.3:
 | 
			
		||||
  version "1.6.3"
 | 
			
		||||
@ -10312,17 +10308,16 @@ webpack-sources@^1.0.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-
 | 
			
		||||
    source-list-map "^2.0.0"
 | 
			
		||||
    source-map "~0.6.1"
 | 
			
		||||
 | 
			
		||||
webpack@^4.34.0:
 | 
			
		||||
  version "4.34.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.34.0.tgz#a4c30129482f7b4ece4c0842002dedf2b56fab58"
 | 
			
		||||
  integrity sha512-ry2IQy1wJjOefLe1uJLzn5tG/DdIKzQqNlIAd2L84kcaADqNvQDTBlo8UcCNyDaT5FiaB+16jhAkb63YeG3H8Q==
 | 
			
		||||
webpack@^4.35.3:
 | 
			
		||||
  version "4.35.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.3.tgz#66bc35ef215a7b75e8790f84d560013ffecf0ca3"
 | 
			
		||||
  integrity sha512-xggQPwr9ILlXzz61lHzjvgoqGU08v5+Wnut19Uv3GaTtzN4xBTcwnobodrXE142EL1tOiS5WVEButooGzcQzTA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@webassemblyjs/ast" "1.8.5"
 | 
			
		||||
    "@webassemblyjs/helper-module-context" "1.8.5"
 | 
			
		||||
    "@webassemblyjs/wasm-edit" "1.8.5"
 | 
			
		||||
    "@webassemblyjs/wasm-parser" "1.8.5"
 | 
			
		||||
    acorn "^6.0.5"
 | 
			
		||||
    acorn-dynamic-import "^4.0.0"
 | 
			
		||||
    acorn "^6.2.0"
 | 
			
		||||
    ajv "^6.1.0"
 | 
			
		||||
    ajv-keywords "^3.1.0"
 | 
			
		||||
    chrome-trace-event "^1.0.0"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user