Skip to content

Commit

Permalink
Add option to disable real-time updates in web UI
Browse files Browse the repository at this point in the history
Fix #9031
Fix #7913
  • Loading branch information
Gargron committed Jul 14, 2019
1 parent 5bf67ca commit 8b9dedb
Show file tree
Hide file tree
Showing 20 changed files with 178 additions and 70 deletions.
1 change: 1 addition & 0 deletions app/controllers/settings/preferences_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def user_settings_params
:setting_show_application,
:setting_advanced_layout,
: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)
)
Expand Down
30 changes: 24 additions & 6 deletions app/javascript/mastodon/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,8 +24,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';

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_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' },
Expand All @@ -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);
Expand Down Expand Up @@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
usePendingItems: preferPendingItems,
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
});

Expand Down Expand Up @@ -122,10 +130,19 @@ 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));

api(getState).get('/api/v1/notifications', { params }).then(response => {
Expand All @@ -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 => {
Expand All @@ -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,
};
};
Expand Down
43 changes: 29 additions & 14 deletions app/javascript/mastodon/actions/timelines.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
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_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';

export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const loadPending = timeline => ({
type: TIMELINE_LOAD_PENDING,
timeline,
});

export function updateTimeline(timeline, status, accept) {
return dispatch => {
Expand All @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
type: TIMELINE_UPDATE,
timeline,
status,
usePendingItems: preferPendingItems,
});
};
};
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -115,14 +130,15 @@ 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,
statuses,
next,
partial,
isLoadingRecent,
usePendingItems,
skipLoading: !isLoadingMore,
};
};
Expand Down Expand Up @@ -151,9 +167,8 @@ export function connectTimeline(timeline) {
};
};

export function disconnectTimeline(timeline) {
return {
type: TIMELINE_DISCONNECT,
timeline,
};
};
export const disconnectTimeline = timeline => ({
type: TIMELINE_DISCONNECT,
timeline,
usePendingItems: preferPendingItems,
});
5 changes: 3 additions & 2 deletions app/javascript/mastodon/compare_id.js
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions app/javascript/mastodon/components/load_pending.js
Original file line number Diff line number Diff line change
@@ -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>
);
}

}
13 changes: 12 additions & 1 deletion app/javascript/mastodon/components/scrollable_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,13 +22,15 @@ 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,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
showLoading: PropTypes.bool,
hasMore: PropTypes.bool,
numPending: PropTypes.number,
prepend: PropTypes.node,
alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node,
Expand Down Expand Up @@ -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) {
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ export default class SettingToggle extends React.PureComponent {
settingPath: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.bool,
}

onChange = ({ target }) => {
this.props.onChange(this.props.settingPath, target.checked);
}

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>
);
Expand Down
12 changes: 10 additions & 2 deletions app/javascript/mastodon/features/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand All @@ -58,6 +59,7 @@ class Notifications extends React.PureComponent {
isUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
numPending: PropTypes.number,
};

static defaultProps = {
Expand All @@ -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);
Expand Down Expand Up @@ -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." />;

Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
dispatch(scrollTopTimeline(timelineId, false));
}, 100),

onLoadPending: () => dispatch(loadPending(timelineId)),

});

export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
1 change: 1 addition & 0 deletions app/javascript/mastodon/initial_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,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;
Loading

0 comments on commit 8b9dedb

Please sign in to comment.