[Glitch] Add option to disable real-time updates in web UI

Port 729723f857 to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
Eugen Rochko 2019-07-16 06:30:47 +02:00 committed by Thibaut Girka
parent c8a47595fb
commit e91bf82083
12 changed files with 154 additions and 51 deletions

View file

@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from 'flavours/glitch/util/html'; import { unescapeHTML } from 'flavours/glitch/util/html';
import { getFiltersRegex } from 'flavours/glitch/selectors'; 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'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
@ -32,8 +34,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; 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_MOUNT = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; 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) { export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => { return (dispatch, getState) => {
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
@ -83,6 +90,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch({ dispatch({
type: NOTIFICATIONS_UPDATE, type: NOTIFICATIONS_UPDATE,
notification, notification,
usePendingItems: preferPendingItems,
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
}); });
@ -136,10 +144,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
: excludeTypesFromFilter(activeFilter), : excludeTypesFromFilter(activeFilter),
}; };
if (!maxId && notifications.get('items').size > 0) { if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
params.since_id = notifications.getIn(['items', 0, 'id']); 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)); dispatch(expandNotificationsRequest(isLoadingMore));
api(getState).get('/api/v1/notifications', { params }).then(response => { api(getState).get('/api/v1/notifications', { params }).then(response => {
@ -148,7 +165,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); 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); fetchRelatedRelationships(dispatch, response.data);
done(); done();
}).catch(error => { }).catch(error => {
@ -165,13 +182,12 @@ export function expandNotificationsRequest(isLoadingMore) {
}; };
}; };
export function expandNotificationsSuccess(notifications, next, isLoadingMore) { export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) {
return { return {
type: NOTIFICATIONS_EXPAND_SUCCESS, type: NOTIFICATIONS_EXPAND_SUCCESS,
notifications, notifications,
accounts: notifications.map(item => item.account),
statuses: notifications.map(item => item.status).filter(status => !!status),
next, next,
usePendingItems,
skipLoading: !isLoadingMore, skipLoading: !isLoadingMore,
}; };
}; };

View file

@ -1,6 +1,8 @@
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from 'flavours/glitch/util/api'; import api, { getLinks } from 'flavours/glitch/util/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; 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_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; 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 loadPending = timeline => ({
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; type: TIMELINE_LOAD_PENDING,
timeline,
});
export function updateTimeline(timeline, status, accept) { export function updateTimeline(timeline, status, accept) {
return dispatch => { return dispatch => {
@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) {
type: TIMELINE_UPDATE, type: TIMELINE_UPDATE,
timeline, timeline,
status, status,
usePendingItems: preferPendingItems,
}); });
}; };
}; };
@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return; return;
} }
if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
params.since_id = timeline.getIn(['items', 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; const isLoadingRecent = !!params.since_id;
@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
api(getState).get(path, { params }).then(response => { api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); 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(); done();
}).catch(error => { }).catch(error => {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); 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 { return {
type: TIMELINE_EXPAND_SUCCESS, type: TIMELINE_EXPAND_SUCCESS,
timeline, timeline,
@ -125,6 +140,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi
next, next,
partial, partial,
isLoadingRecent, isLoadingRecent,
usePendingItems,
skipLoading: !isLoadingMore, skipLoading: !isLoadingMore,
}; };
}; };
@ -153,9 +169,8 @@ export function connectTimeline(timeline) {
}; };
}; };
export function disconnectTimeline(timeline) { export const disconnectTimeline = timeline => ({
return { type: TIMELINE_DISCONNECT,
type: TIMELINE_DISCONNECT, timeline,
timeline, usePendingItems: preferPendingItems,
}; });
};

View 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>
);
}
}

View file

@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
import LoadMore from './load_more'; import LoadMore from './load_more';
import LoadPending from './load_pending';
import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent {
static propTypes = { static propTypes = {
scrollKey: PropTypes.string.isRequired, scrollKey: PropTypes.string.isRequired,
onLoadMore: PropTypes.func, onLoadMore: PropTypes.func,
onLoadPending: PropTypes.func,
onScrollToTop: PropTypes.func, onScrollToTop: PropTypes.func,
onScroll: PropTypes.func, onScroll: PropTypes.func,
trackScroll: PropTypes.bool, trackScroll: PropTypes.bool,
@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
showLoading: PropTypes.bool, showLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
numPending: PropTypes.number,
prepend: PropTypes.node, prepend: PropTypes.node,
alwaysPrepend: PropTypes.bool, alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node, emptyMessage: PropTypes.node,
@ -222,12 +225,18 @@ export default class ScrollableList extends PureComponent {
return !(location.state && location.state.mastodonModalOpen); return !(location.state && location.state.mastodonModalOpen);
} }
handleLoadPending = e => {
e.preventDefault();
this.props.onLoadPending();
}
render () { 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 { fullscreen } = this.state;
const childrenCount = React.Children.count(children); const childrenCount = React.Children.count(children);
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; 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; let scrollableArea = null;
if (showLoading) { if (showLoading) {
@ -248,6 +257,8 @@ export default class ScrollableList extends PureComponent {
<div role='feed' className='item-list'> <div role='feed' className='item-list'>
{prepend} {prepend}
{loadPending}
{React.Children.map(this.props.children, (child, index) => ( {React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticleContainer <IntersectionObserverArticleContainer
key={child.key} key={child.key}

View file

@ -26,7 +26,7 @@ export default class ColumnSettings extends React.PureComponent {
return ( return (
<div> <div>
<div className='column-settings__row'> <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>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>

View file

@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent {
label: PropTypes.node.isRequired, label: PropTypes.node.isRequired,
meta: PropTypes.node, meta: PropTypes.node,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.bool,
} }
onChange = ({ target }) => { onChange = ({ target }) => {
@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent {
} }
render () { 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('-'); const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
return ( return (
<div className='setting-toggle'> <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> <label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>} {meta && <span className='setting-meta__label'>{meta}</span>}
</div> </div>

View file

@ -10,6 +10,7 @@ import {
scrollTopNotifications, scrollTopNotifications,
mountNotifications, mountNotifications,
unmountNotifications, unmountNotifications,
loadPending,
} from 'flavours/glitch/actions/notifications'; } from 'flavours/glitch/actions/notifications';
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
import NotificationContainer from './containers/notification_container'; import NotificationContainer from './containers/notification_container';
@ -48,6 +49,7 @@ const mapStateToProps = state => ({
isLoading: state.getIn(['notifications', 'isLoading'], true), isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0, isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']), hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
}); });
@ -80,6 +82,7 @@ export default class Notifications extends React.PureComponent {
isUnread: PropTypes.bool, isUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
numPending: PropTypes.number,
localSettings: ImmutablePropTypes.map, localSettings: ImmutablePropTypes.map,
notifCleaningActive: PropTypes.bool, notifCleaningActive: PropTypes.bool,
onEnterCleaningMode: PropTypes.func, onEnterCleaningMode: PropTypes.func,
@ -100,6 +103,10 @@ export default class Notifications extends React.PureComponent {
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
}, 300, { leading: true }); }, 300, { leading: true });
handleLoadPending = () => {
this.props.dispatch(loadPending());
};
handleScrollToTop = debounce(() => { handleScrollToTop = debounce(() => {
this.props.dispatch(scrollTopNotifications(true)); this.props.dispatch(scrollTopNotifications(true));
}, 100); }, 100);
@ -170,7 +177,7 @@ export default class Notifications extends React.PureComponent {
} }
render () { 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 pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />; 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} isLoading={isLoading}
showLoading={isLoading && notifications.size === 0} showLoading={isLoading && notifications.size === 0}
hasMore={hasMore} hasMore={hasMore}
numPending={numPending}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
onLoadMore={this.handleLoadOlder} onLoadMore={this.handleLoadOlder}
onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop} onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import StatusList from 'flavours/glitch/components/status_list'; 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 { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
@ -62,6 +62,7 @@ const makeMapStateToProps = () => {
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']), hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size,
}); });
return mapStateToProps; return mapStateToProps;
@ -77,6 +78,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
dispatch(scrollTopTimeline(timelineId, false)); dispatch(scrollTopTimeline(timelineId, false));
}, 100), }, 100),
onLoadPending: () => dispatch(loadPending(timelineId)),
}); });
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);

View file

@ -9,6 +9,7 @@ import {
NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR, NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP, NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_LOAD_PENDING,
NOTIFICATIONS_DELETE_MARKED_REQUEST, NOTIFICATIONS_DELETE_MARKED_REQUEST,
NOTIFICATIONS_DELETE_MARKED_SUCCESS, NOTIFICATIONS_DELETE_MARKED_SUCCESS,
NOTIFICATION_MARK_FOR_DELETE, 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'; import compareId from 'flavours/glitch/util/compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
pendingItems: ImmutableList(),
items: ImmutableList(), items: ImmutableList(),
hasMore: true, hasMore: true,
top: false, top: false,
@ -46,7 +48,11 @@ const notificationToMap = (state, notification) => ImmutableMap({
status: notification.status ? notification.status.id : null, 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); const top = !shouldCountUnreadNotifications(state);
if (top) { 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 top = !(shouldCountUnreadNotifications(state));
const lastReadId = state.get('lastReadId'); const lastReadId = state.get('lastReadId');
let items = ImmutableList(); let items = ImmutableList();
@ -75,7 +81,7 @@ const expandNormalizedNotifications = (state, notifications, next) => {
return state.withMutations(mutable => { return state.withMutations(mutable => {
if (!items.isEmpty()) { if (!items.isEmpty()) {
mutable.update('items', list => { mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
const lastIndex = 1 + list.findLastIndex( const lastIndex = 1 + list.findLastIndex(
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) 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) => { 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) => { 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); 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); 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) => { const markForDelete = (state, notificationId, yes) => {
@ -192,6 +200,8 @@ export default function notifications(state = initialState, action) {
return state.update('mounted', count => count - 1); return state.update('mounted', count => count - 1);
case NOTIFICATIONS_SET_VISIBILITY: case NOTIFICATIONS_SET_VISIBILITY:
return updateVisibility(state, action.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_EXPAND_REQUEST:
case NOTIFICATIONS_DELETE_MARKED_REQUEST: case NOTIFICATIONS_DELETE_MARKED_REQUEST:
return state.set('isLoading', true); return state.set('isLoading', true);
@ -203,20 +213,20 @@ export default function notifications(state = initialState, action) {
case NOTIFICATIONS_SCROLL_TOP: case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top); return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE: case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification); return normalizeNotification(state, action.notification, action.usePendingItems);
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next); return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems);
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
return filterNotifications(state, action.relationship); return filterNotifications(state, action.relationship);
case ACCOUNT_MUTE_SUCCESS: case ACCOUNT_MUTE_SUCCESS:
return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
case NOTIFICATIONS_CLEAR: 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: case TIMELINE_DELETE:
return deleteByStatus(state, action.id); return deleteByStatus(state, action.id);
case TIMELINE_DISCONNECT: case TIMELINE_DISCONNECT:
return action.timeline === 'home' ? 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; state;
case NOTIFICATION_MARK_FOR_DELETE: case NOTIFICATION_MARK_FOR_DELETE:

View file

@ -8,6 +8,7 @@ import {
TIMELINE_SCROLL_TOP, TIMELINE_SCROLL_TOP,
TIMELINE_CONNECT, TIMELINE_CONNECT,
TIMELINE_DISCONNECT, TIMELINE_DISCONNECT,
TIMELINE_LOAD_PENDING,
} from 'flavours/glitch/actions/timelines'; } from 'flavours/glitch/actions/timelines';
import { import {
ACCOUNT_BLOCK_SUCCESS, ACCOUNT_BLOCK_SUCCESS,
@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({
top: true, top: true,
isLoading: false, isLoading: false,
hasMore: true, hasMore: true,
pendingItems: ImmutableList(),
items: 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 => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
mMap.set('isLoading', false); mMap.set('isLoading', false);
mMap.set('isPartial', isPartial); mMap.set('isPartial', isPartial);
@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
if (timeline.endsWith(':pinned')) { if (timeline.endsWith(':pinned')) {
mMap.set('items', statuses.map(status => status.get('id'))); mMap.set('items', statuses.map(status => status.get('id')));
} else if (!statuses.isEmpty()) { } 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 newIds = statuses.map(status => status.get('id'));
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; 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); 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 top = state.getIn([timeline, 'top']);
const ids = state.getIn([timeline, 'items'], ImmutableList()); const ids = state.getIn([timeline, 'items'], ImmutableList());
const includesId = ids.includes(status.get('id')); 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) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
state.keySeq().forEach(timeline => { state.keySeq().forEach(timeline => {
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); const helper = list => list.filterNot(item => item === id);
state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
}
}); });
// Remove reblogs of deleted status // Remove reblogs of deleted status
@ -108,11 +120,10 @@ const filterTimelines = (state, relationship, statuses) => {
return state; return state;
}; };
const filterTimeline = (timeline, state, relationship, statuses) => const filterTimeline = (timeline, state, relationship, statuses) => {
state.updateIn([timeline, 'items'], ImmutableList(), list => const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id);
list.filterNot(statusId => return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper);
statuses.getIn([statusId, 'account']) === relationship.id };
));
const updateTop = (state, timeline, top) => { const updateTop = (state, timeline, top) => {
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { 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) { export default function timelines(state = initialState, action) {
switch(action.type) { 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: case TIMELINE_EXPAND_REQUEST:
return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
case TIMELINE_EXPAND_FAIL: case TIMELINE_EXPAND_FAIL:
return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
case TIMELINE_EXPAND_SUCCESS: 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: case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status)); return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
case TIMELINE_CLEAR: case TIMELINE_CLEAR:
@ -148,7 +162,7 @@ export default function timelines(state = initialState, action) {
return state.update( return state.update(
action.timeline, action.timeline,
initialTimeline, 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: default:
return state; return state;

View file

@ -1,10 +1,11 @@
export default function compareId(id1, id2) { export default function compareId (id1, id2) {
if (id1 === id2) { if (id1 === id2) {
return 0; return 0;
} }
if (id1.length === id2.length) { if (id1.length === id2.length) {
return id1 > id2 ? 1 : -1; return id1 > id2 ? 1 : -1;
} else { } else {
return id1.length > id2.length ? 1 : -1; return id1.length > id2.length ? 1 : -1;
} }
} };

View file

@ -30,5 +30,6 @@ export const isStaff = getMeta('is_staff');
export const defaultContentType = getMeta('default_content_type'); export const defaultContentType = getMeta('default_content_type');
export const forceSingleColumn = getMeta('advanced_layout') === false; export const forceSingleColumn = getMeta('advanced_layout') === false;
export const useBlurhash = getMeta('use_blurhash'); export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export default initialState; export default initialState;