Add option to disable real-time updates in web UI (#9984)
Fix #9031 Fix #7913
This commit is contained in:
parent
4562c3cb7e
commit
9b1d3e4acb
|
@ -55,6 +55,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
:setting_show_application,
|
:setting_show_application,
|
||||||
:setting_advanced_layout,
|
:setting_advanced_layout,
|
||||||
:setting_use_blurhash,
|
:setting_use_blurhash,
|
||||||
|
:setting_use_pending_items,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
|
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)
|
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 { List as ImmutableList } from 'immutable';
|
||||||
import { unescapeHTML } from '../utils/html';
|
import { unescapeHTML } from '../utils/html';
|
||||||
import { getFiltersRegex } from '../selectors';
|
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 = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
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_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';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
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) {
|
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);
|
||||||
|
@ -69,6 +76,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,9 +130,18 @@ 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));
|
||||||
|
|
||||||
|
@ -134,7 +151,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 => {
|
||||||
|
@ -151,11 +168,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,
|
||||||
next,
|
next,
|
||||||
|
usePendingItems,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
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 { 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_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
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_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_CONNECT = 'TIMELINE_CONNECT';
|
|
||||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
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) {
|
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));
|
||||||
|
@ -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 {
|
return {
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
|
@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi
|
||||||
next,
|
next,
|
||||||
partial,
|
partial,
|
||||||
isLoadingRecent,
|
isLoadingRecent,
|
||||||
|
usePendingItems,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -151,9 +167,8 @@ export function connectTimeline(timeline) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function disconnectTimeline(timeline) {
|
export const disconnectTimeline = timeline => ({
|
||||||
return {
|
|
||||||
type: TIMELINE_DISCONNECT,
|
type: TIMELINE_DISCONNECT,
|
||||||
timeline,
|
timeline,
|
||||||
};
|
usePendingItems: preferPendingItems,
|
||||||
};
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -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 PropTypes from 'prop-types';
|
||||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
|
import LoadPending from './load_pending';
|
||||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
import IntersectionObserverWrapper from '../features/ui/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,
|
||||||
|
@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent {
|
||||||
this.props.onLoadMore();
|
this.props.onLoadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -251,6 +260,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}
|
||||||
|
|
|
@ -20,7 +20,7 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent {
|
||||||
settingPath: PropTypes.array.isRequired,
|
settingPath: PropTypes.array.isRequired,
|
||||||
label: PropTypes.node.isRequired,
|
label: PropTypes.node.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
defaultValue: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = ({ target }) => {
|
onChange = ({ target }) => {
|
||||||
|
@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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('-');
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
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 { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import NotificationContainer from './containers/notification_container';
|
import NotificationContainer from './containers/notification_container';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -41,6 +41,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -58,6 +59,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -80,6 +82,10 @@ 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);
|
||||||
|
@ -136,7 +142,7 @@ 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." />;
|
||||||
|
|
||||||
|
@ -178,8 +184,10 @@ 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}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StatusList from '../../../components/status_list';
|
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 { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
@ -37,6 +37,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;
|
||||||
|
@ -52,6 +53,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);
|
||||||
|
|
|
@ -21,5 +21,6 @@ export const profile_directory = getMeta('profile_directory');
|
||||||
export const isStaff = getMeta('is_staff');
|
export const isStaff = getMeta('is_staff');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
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;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
NOTIFICATIONS_FILTER_SET,
|
NOTIFICATIONS_FILTER_SET,
|
||||||
NOTIFICATIONS_CLEAR,
|
NOTIFICATIONS_CLEAR,
|
||||||
NOTIFICATIONS_SCROLL_TOP,
|
NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
NOTIFICATIONS_LOAD_PENDING,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
@ -16,6 +17,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import compareId from '../compare_id';
|
import compareId from '../compare_id';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
pendingItems: ImmutableList(),
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
top: false,
|
top: false,
|
||||||
|
@ -31,7 +33,11 @@ const notificationToMap = 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(notification)));
|
||||||
|
}
|
||||||
|
|
||||||
const top = state.get('top');
|
const top = state.get('top');
|
||||||
|
|
||||||
if (!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();
|
let items = ImmutableList();
|
||||||
|
|
||||||
notifications.forEach((n, i) => {
|
notifications.forEach((n, i) => {
|
||||||
|
@ -56,7 +62,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'))
|
||||||
);
|
);
|
||||||
|
@ -78,7 +84,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 updateTop = (state, top) => {
|
const updateTop = (state, top) => {
|
||||||
|
@ -90,34 +97,37 @@ const updateTop = (state, top) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteByStatus = (state, statusId) => {
|
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) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
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:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case NOTIFICATIONS_EXPAND_FAIL:
|
case NOTIFICATIONS_EXPAND_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case NOTIFICATIONS_FILTER_SET:
|
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:
|
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;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -10,8 +10,6 @@ import uuid from '../uuid';
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
saved: true,
|
saved: true,
|
||||||
|
|
||||||
onboarded: false,
|
|
||||||
|
|
||||||
skinTone: 1,
|
skinTone: 1,
|
||||||
|
|
||||||
home: ImmutableMap({
|
home: ImmutableMap({
|
||||||
|
@ -74,10 +72,6 @@ const initialState = ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trends: ImmutableMap({
|
|
||||||
show: true,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultColumns = fromJS([
|
const defaultColumns = fromJS([
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
TIMELINE_SCROLL_TOP,
|
TIMELINE_SCROLL_TOP,
|
||||||
TIMELINE_CONNECT,
|
TIMELINE_CONNECT,
|
||||||
TIMELINE_DISCONNECT,
|
TIMELINE_DISCONNECT,
|
||||||
|
TIMELINE_LOAD_PENDING,
|
||||||
} from '../actions/timelines';
|
} from '../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;
|
||||||
|
@ -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 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'));
|
||||||
|
@ -78,8 +88,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
|
||||||
|
@ -109,11 +121,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 => {
|
||||||
|
@ -124,14 +135,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:
|
||||||
|
@ -149,7 +163,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;
|
||||||
|
|
|
@ -35,6 +35,7 @@ class UserSettingsDecorator
|
||||||
user.settings['show_application'] = show_application_preference if change?('setting_show_application')
|
user.settings['show_application'] = show_application_preference if change?('setting_show_application')
|
||||||
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
|
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
|
||||||
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
|
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
|
end
|
||||||
|
|
||||||
def merged_notification_emails
|
def merged_notification_emails
|
||||||
|
@ -117,6 +118,10 @@ class UserSettingsDecorator
|
||||||
boolean_cast_setting 'setting_use_blurhash'
|
boolean_cast_setting 'setting_use_blurhash'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_pending_items_preference
|
||||||
|
boolean_cast_setting 'setting_use_pending_items'
|
||||||
|
end
|
||||||
|
|
||||||
def boolean_cast_setting(key)
|
def boolean_cast_setting(key)
|
||||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||||
end
|
end
|
||||||
|
|
|
@ -106,7 +106,7 @@ class User < ApplicationRecord
|
||||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||||
:advanced_layout, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false
|
:advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
|
||||||
|
|
||||||
attr_reader :invite_code
|
attr_reader :invite_code
|
||||||
attr_writer :external
|
attr_writer :external
|
||||||
|
|
|
@ -33,6 +33,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
||||||
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
||||||
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
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[:is_staff] = object.current_account.user.staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
|
|
||||||
%h4= t 'appearance.animations_and_accessibility'
|
%h4= t 'appearance.animations_and_accessibility'
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true
|
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true
|
||||||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||||
|
|
|
@ -35,6 +35,7 @@ en:
|
||||||
setting_noindex: Affects your public profile and status pages
|
setting_noindex: Affects your public profile and status pages
|
||||||
setting_show_application: The application you use to toot will be displayed in the detailed view of your toots
|
setting_show_application: The application you use to toot will be displayed in the detailed view of your toots
|
||||||
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
|
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}
|
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
|
whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
|
||||||
featured_tag:
|
featured_tag:
|
||||||
|
@ -111,6 +112,7 @@ en:
|
||||||
setting_theme: Site theme
|
setting_theme: Site theme
|
||||||
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||||
setting_use_blurhash: Show colorful gradients for hidden media
|
setting_use_blurhash: Show colorful gradients for hidden media
|
||||||
|
setting_use_pending_items: Slow mode
|
||||||
severity: Severity
|
severity: Severity
|
||||||
type: Import type
|
type: Import type
|
||||||
username: Username
|
username: Username
|
||||||
|
|
|
@ -33,6 +33,7 @@ defaults: &defaults
|
||||||
aggregate_reblogs: true
|
aggregate_reblogs: true
|
||||||
advanced_layout: false
|
advanced_layout: false
|
||||||
use_blurhash: true
|
use_blurhash: true
|
||||||
|
use_pending_items: false
|
||||||
notification_emails:
|
notification_emails:
|
||||||
follow: false
|
follow: false
|
||||||
reblog: false
|
reblog: false
|
||||||
|
|
Loading…
Reference in New Issue