Skip to content

Commit 8b9dedb

Browse files
committed
Add option to disable real-time updates in web UI
Fix #9031 Fix #7913
1 parent 5bf67ca commit 8b9dedb

20 files changed

Lines changed: 178 additions & 70 deletions

File tree

app/controllers/settings/preferences_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def user_settings_params
5555
:setting_show_application,
5656
:setting_advanced_layout,
5757
:setting_use_blurhash,
58+
:setting_use_pending_items,
5859
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
5960
interactions: %i(must_be_follower must_be_following must_be_following_dm)
6061
)

app/javascript/mastodon/actions/notifications.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl';
1212
import { List as ImmutableList } from 'immutable';
1313
import { unescapeHTML } from '../utils/html';
1414
import { getFiltersRegex } from '../selectors';
15+
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
16+
import compareId from 'mastodon/compare_id';
1517

1618
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
1719
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@@ -22,8 +24,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
2224

2325
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
2426

25-
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
26-
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
27+
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
28+
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
29+
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
2730

2831
defineMessages({
2932
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
@@ -38,6 +41,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
3841
}
3942
};
4043

44+
export const loadPending = () => ({
45+
type: NOTIFICATIONS_LOAD_PENDING,
46+
});
47+
4148
export function updateNotifications(notification, intlMessages, intlLocale) {
4249
return (dispatch, getState) => {
4350
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
@@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
6976
dispatch({
7077
type: NOTIFICATIONS_UPDATE,
7178
notification,
79+
usePendingItems: preferPendingItems,
7280
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
7381
});
7482

@@ -122,10 +130,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
122130
: excludeTypesFromFilter(activeFilter),
123131
};
124132

125-
if (!maxId && notifications.get('items').size > 0) {
126-
params.since_id = notifications.getIn(['items', 0, 'id']);
133+
if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
134+
const a = notifications.getIn(['pendingItems', 0, 'id']);
135+
const b = notifications.getIn(['items', 0, 'id']);
136+
137+
if (a && b && compareId(a, b) > 0) {
138+
params.since_id = a;
139+
} else {
140+
params.since_id = b || a;
141+
}
127142
}
128143

144+
const isLoadingRecent = !!params.since_id;
145+
129146
dispatch(expandNotificationsRequest(isLoadingMore));
130147

131148
api(getState).get('/api/v1/notifications', { params }).then(response => {
@@ -134,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
134151
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
135152
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
136153

137-
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
154+
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems));
138155
fetchRelatedRelationships(dispatch, response.data);
139156
done();
140157
}).catch(error => {
@@ -151,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) {
151168
};
152169
};
153170

154-
export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
171+
export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) {
155172
return {
156173
type: NOTIFICATIONS_EXPAND_SUCCESS,
157174
notifications,
158175
next,
176+
usePendingItems,
159177
skipLoading: !isLoadingMore,
160178
};
161179
};

app/javascript/mastodon/actions/timelines.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { importFetchedStatus, importFetchedStatuses } from './importer';
2-
import api, { getLinks } from '../api';
2+
import api, { getLinks } from 'mastodon/api';
33
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
4+
import compareId from 'mastodon/compare_id';
5+
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
46

57
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
68
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
1012
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
1113
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
1214

13-
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
15+
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
16+
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
17+
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
18+
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
1419

15-
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
16-
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
20+
export const loadPending = timeline => ({
21+
type: TIMELINE_LOAD_PENDING,
22+
timeline,
23+
});
1724

1825
export function updateTimeline(timeline, status, accept) {
1926
return dispatch => {
@@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
2734
type: TIMELINE_UPDATE,
2835
timeline,
2936
status,
37+
usePendingItems: preferPendingItems,
3038
});
3139
};
3240
};
@@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
7179
return;
7280
}
7381

74-
if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
75-
params.since_id = timeline.getIn(['items', 0]);
82+
if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
83+
const a = timeline.getIn(['pendingItems', 0]);
84+
const b = timeline.getIn(['items', 0]);
85+
86+
if (a && b && compareId(a, b) > 0) {
87+
params.since_id = a;
88+
} else {
89+
params.since_id = b || a;
90+
}
7691
}
7792

7893
const isLoadingRecent = !!params.since_id;
@@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
8297
api(getState).get(path, { params }).then(response => {
8398
const next = getLinks(response).refs.find(link => link.rel === 'next');
8499
dispatch(importFetchedStatuses(response.data));
85-
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
100+
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
86101
done();
87102
}).catch(error => {
88103
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
@@ -115,14 +130,15 @@ export function expandTimelineRequest(timeline, isLoadingMore) {
115130
};
116131
};
117132

118-
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
133+
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
119134
return {
120135
type: TIMELINE_EXPAND_SUCCESS,
121136
timeline,
122137
statuses,
123138
next,
124139
partial,
125140
isLoadingRecent,
141+
usePendingItems,
126142
skipLoading: !isLoadingMore,
127143
};
128144
};
@@ -151,9 +167,8 @@ export function connectTimeline(timeline) {
151167
};
152168
};
153169

154-
export function disconnectTimeline(timeline) {
155-
return {
156-
type: TIMELINE_DISCONNECT,
157-
timeline,
158-
};
159-
};
170+
export const disconnectTimeline = timeline => ({
171+
type: TIMELINE_DISCONNECT,
172+
timeline,
173+
usePendingItems: preferPendingItems,
174+
});
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
export default function compareId(id1, id2) {
1+
export default function compareId (id1, id2) {
22
if (id1 === id2) {
33
return 0;
44
}
5+
56
if (id1.length === id2.length) {
67
return id1 > id2 ? 1 : -1;
78
} else {
89
return id1.length > id2.length ? 1 : -1;
910
}
10-
}
11+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { FormattedMessage } from 'react-intl';
3+
import PropTypes from 'prop-types';
4+
5+
export default class LoadPending extends React.PureComponent {
6+
7+
static propTypes = {
8+
onClick: PropTypes.func,
9+
count: PropTypes.number,
10+
}
11+
12+
render() {
13+
const { count } = this.props;
14+
15+
return (
16+
<button className='load-more load-gap' onClick={this.props.onClick}>
17+
<FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
18+
</button>
19+
);
20+
}
21+
22+
}

app/javascript/mastodon/components/scrollable_list.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
33
import PropTypes from 'prop-types';
44
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
55
import LoadMore from './load_more';
6+
import LoadPending from './load_pending';
67
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
78
import { throttle } from 'lodash';
89
import { List as ImmutableList } from 'immutable';
@@ -21,13 +22,15 @@ export default class ScrollableList extends PureComponent {
2122
static propTypes = {
2223
scrollKey: PropTypes.string.isRequired,
2324
onLoadMore: PropTypes.func,
25+
onLoadPending: PropTypes.func,
2426
onScrollToTop: PropTypes.func,
2527
onScroll: PropTypes.func,
2628
trackScroll: PropTypes.bool,
2729
shouldUpdateScroll: PropTypes.func,
2830
isLoading: PropTypes.bool,
2931
showLoading: PropTypes.bool,
3032
hasMore: PropTypes.bool,
33+
numPending: PropTypes.number,
3134
prepend: PropTypes.node,
3235
alwaysPrepend: PropTypes.bool,
3336
emptyMessage: PropTypes.node,
@@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent {
225228
this.props.onLoadMore();
226229
}
227230

231+
handleLoadPending = e => {
232+
e.preventDefault();
233+
this.props.onLoadPending();
234+
}
235+
228236
render () {
229-
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
237+
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
230238
const { fullscreen } = this.state;
231239
const childrenCount = React.Children.count(children);
232240

233241
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
242+
const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
234243
let scrollableArea = null;
235244

236245
if (showLoading) {
@@ -251,6 +260,8 @@ export default class ScrollableList extends PureComponent {
251260
<div role='feed' className='item-list'>
252261
{prepend}
253262

263+
{loadPending}
264+
254265
{React.Children.map(this.props.children, (child, index) => (
255266
<IntersectionObserverArticleContainer
256267
key={child.key}

app/javascript/mastodon/features/community_timeline/components/column_settings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ColumnSettings extends React.PureComponent {
2020
return (
2121
<div>
2222
<div className='column-settings__row'>
23-
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
23+
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
2424
</div>
2525
</div>
2626
);

app/javascript/mastodon/features/notifications/components/setting_toggle.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,20 @@ export default class SettingToggle extends React.PureComponent {
1111
settingPath: PropTypes.array.isRequired,
1212
label: PropTypes.node.isRequired,
1313
onChange: PropTypes.func.isRequired,
14+
defaultValue: PropTypes.bool,
1415
}
1516

1617
onChange = ({ target }) => {
1718
this.props.onChange(this.props.settingPath, target.checked);
1819
}
1920

2021
render () {
21-
const { prefix, settings, settingPath, label } = this.props;
22+
const { prefix, settings, settingPath, label, defaultValue } = this.props;
2223
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
2324

2425
return (
2526
<div className='setting-toggle'>
26-
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
27+
<Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
2728
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
2829
</div>
2930
);

app/javascript/mastodon/features/notifications/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
44
import ImmutablePropTypes from 'react-immutable-proptypes';
55
import Column from '../../components/column';
66
import ColumnHeader from '../../components/column_header';
7-
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
7+
import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications';
88
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
99
import NotificationContainer from './containers/notification_container';
1010
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -41,6 +41,7 @@ const mapStateToProps = state => ({
4141
isLoading: state.getIn(['notifications', 'isLoading'], true),
4242
isUnread: state.getIn(['notifications', 'unread']) > 0,
4343
hasMore: state.getIn(['notifications', 'hasMore']),
44+
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
4445
});
4546

4647
export default @connect(mapStateToProps)
@@ -58,6 +59,7 @@ class Notifications extends React.PureComponent {
5859
isUnread: PropTypes.bool,
5960
multiColumn: PropTypes.bool,
6061
hasMore: PropTypes.bool,
62+
numPending: PropTypes.number,
6163
};
6264

6365
static defaultProps = {
@@ -80,6 +82,10 @@ class Notifications extends React.PureComponent {
8082
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
8183
}, 300, { leading: true });
8284

85+
handleLoadPending = () => {
86+
this.props.dispatch(loadPending());
87+
};
88+
8389
handleScrollToTop = debounce(() => {
8490
this.props.dispatch(scrollTopNotifications(true));
8591
}, 100);
@@ -136,7 +142,7 @@ class Notifications extends React.PureComponent {
136142
}
137143

138144
render () {
139-
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
145+
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
140146
const pinned = !!columnId;
141147
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
142148

@@ -178,8 +184,10 @@ class Notifications extends React.PureComponent {
178184
isLoading={isLoading}
179185
showLoading={isLoading && notifications.size === 0}
180186
hasMore={hasMore}
187+
numPending={numPending}
181188
emptyMessage={emptyMessage}
182189
onLoadMore={this.handleLoadOlder}
190+
onLoadPending={this.handleLoadPending}
183191
onScrollToTop={this.handleScrollToTop}
184192
onScroll={this.handleScroll}
185193
shouldUpdateScroll={shouldUpdateScroll}

app/javascript/mastodon/features/ui/containers/status_list_container.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { connect } from 'react-redux';
22
import StatusList from '../../../components/status_list';
3-
import { scrollTopTimeline } from '../../../actions/timelines';
3+
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
44
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
55
import { createSelector } from 'reselect';
66
import { debounce } from 'lodash';
@@ -37,6 +37,7 @@ const makeMapStateToProps = () => {
3737
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
3838
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
3939
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
40+
numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size,
4041
});
4142

4243
return mapStateToProps;
@@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
5253
dispatch(scrollTopTimeline(timelineId, false));
5354
}, 100),
5455

56+
onLoadPending: () => dispatch(loadPending(timelineId)),
57+
5558
});
5659

5760
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);

0 commit comments

Comments
 (0)