Added notification queueing functionality
updated streaming functionality to load notifications into a queue (if currently on notitications page) and to display TimelineQueueButtonHeader with outstanding notification count. (if not on notifications page, it behaves as normal, adding/updating notification state). Max 40 are saved to queuedNotifications state and all are tallied into the totalQueuedNotificationsCount state. On click of TimelineQueueButtonHeader it dequeues the queuedNotifications and loads on page if <= max, otherwise it refreshes the page and shows latest 20 (default count) and clears/resets the state for queuedNotifications and totalQueuedNotificationsCount.
This commit is contained in:
parent
6ad747a609
commit
c8e8618f64
@ -16,6 +16,8 @@ import { me } from 'gabsocial/initial_state';
|
|||||||
|
|
||||||
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';
|
||||||
|
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
||||||
|
export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
|
||||||
|
|
||||||
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
||||||
@ -26,6 +28,8 @@ 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 MAX_QUEUED_NOTIFICATIONS = 40;
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
|||||||
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);
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
|
||||||
const filters = getFilters(getState(), { contextType: 'notifications' });
|
|
||||||
|
|
||||||
let filtered = false;
|
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
|
||||||
const regex = regexFromFilters(filters);
|
|
||||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
|
||||||
|
|
||||||
filtered = regex && regex.test(searchIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showInColumn) {
|
if (showInColumn) {
|
||||||
dispatch(importFetchedAccount(notification.account));
|
dispatch(importFetchedAccount(notification.account));
|
||||||
@ -65,15 +57,27 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
type: NOTIFICATIONS_UPDATE,
|
||||||
notification,
|
notification,
|
||||||
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchRelatedRelationships(dispatch, [notification]);
|
fetchRelatedRelationships(dispatch, [notification]);
|
||||||
} else if (playSound && !filtered) {
|
}
|
||||||
dispatch({
|
};
|
||||||
type: NOTIFICATIONS_UPDATE_NOOP,
|
};
|
||||||
meta: { sound: 'ribbit' },
|
|
||||||
});
|
export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
|
const filters = getFilters(getState(), { contextType: 'notifications' });
|
||||||
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
|
|
||||||
|
let filtered = false;
|
||||||
|
|
||||||
|
const isOnNotificationsPage = curPath === '/notifications';
|
||||||
|
|
||||||
|
if (notification.type === 'mention') {
|
||||||
|
const regex = regexFromFilters(filters);
|
||||||
|
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||||
|
filtered = regex && regex.test(searchIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop notifications
|
// Desktop notifications
|
||||||
@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
notify.close();
|
notify.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playSound && !filtered) {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_UPDATE_NOOP,
|
||||||
|
meta: { sound: 'ribbit' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnNotificationsPage) {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_UPDATE_QUEUE,
|
||||||
|
notification,
|
||||||
|
intlMessages,
|
||||||
|
intlLocale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dispatch(updateNotifications(notification, intlMessages, intlLocale));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function dequeueNotifications() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
|
||||||
|
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
|
||||||
|
|
||||||
|
if (totalQueuedNotificationsCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||||
|
queuedNotifications.forEach(block => {
|
||||||
|
dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dispatch(expandNotifications());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_DEQUEUE,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotificationsQueue, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
@ -36,7 +36,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
case 'notification':
|
case 'notification':
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
|
||||||
break;
|
break;
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
@ -4,7 +4,11 @@ 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,
|
||||||
|
dequeueNotifications,
|
||||||
|
} from '../../actions/notifications';
|
||||||
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';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
@ -14,6 +18,7 @@ import { List as ImmutableList } from 'immutable';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import LoadGap from '../../components/load_gap';
|
import LoadGap from '../../components/load_gap';
|
||||||
|
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
@ -40,6 +45,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']),
|
||||||
|
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@ -54,6 +60,8 @@ class Notifications extends React.PureComponent {
|
|||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool,
|
isUnread: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
|
dequeueNotifications: PropTypes.func,
|
||||||
|
totalQueuedNotificationsCount: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -61,6 +69,7 @@ class Notifications extends React.PureComponent {
|
|||||||
this.handleScrollToTop.cancel();
|
this.handleScrollToTop.cancel();
|
||||||
this.handleScroll.cancel();
|
this.handleScroll.cancel();
|
||||||
this.props.dispatch(scrollTopNotifications(false));
|
this.props.dispatch(scrollTopNotifications(false));
|
||||||
|
this.handleDequeueNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -112,8 +121,12 @@ class Notifications extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDequeueNotifications = () => {
|
||||||
|
this.props.dispatch(dequeueNotifications());
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, notifications, isLoading, isUnread, columnId, hasMore, showFilterBar } = this.props;
|
const { intl, notifications, isLoading, isUnread, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
|
||||||
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." />;
|
||||||
|
|
||||||
let scrollableContent = null;
|
let scrollableContent = null;
|
||||||
@ -168,6 +181,7 @@ class Notifications extends React.PureComponent {
|
|||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
|
||||||
{scrollContainer}
|
{scrollContainer}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,9 @@ import {
|
|||||||
NOTIFICATIONS_FILTER_SET,
|
NOTIFICATIONS_FILTER_SET,
|
||||||
NOTIFICATIONS_CLEAR,
|
NOTIFICATIONS_CLEAR,
|
||||||
NOTIFICATIONS_SCROLL_TOP,
|
NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
NOTIFICATIONS_UPDATE_QUEUE,
|
||||||
|
NOTIFICATIONS_DEQUEUE,
|
||||||
|
MAX_QUEUED_NOTIFICATIONS,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
@ -21,6 +24,8 @@ const initialState = ImmutableMap({
|
|||||||
top: false,
|
top: false,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||||
|
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
const notificationToMap = notification => ImmutableMap({
|
||||||
@ -93,6 +98,32 @@ const deleteByStatus = (state, statusId) => {
|
|||||||
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
|
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
|
||||||
|
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
|
||||||
|
const listedNotifications = state.getIn(['items'], ImmutableList());
|
||||||
|
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
|
||||||
|
|
||||||
|
let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id);
|
||||||
|
if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id);
|
||||||
|
|
||||||
|
if (alreadyExists) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newQueuedNotifications = queuedNotifications;
|
||||||
|
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||||
|
mutable.set('queuedNotifications', newQueuedNotifications.push({
|
||||||
|
notification,
|
||||||
|
intlMessages,
|
||||||
|
intlLocale,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function notifications(state = initialState, action) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
@ -105,6 +136,13 @@ export default function notifications(state = initialState, action) {
|
|||||||
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);
|
||||||
|
case NOTIFICATIONS_UPDATE_QUEUE:
|
||||||
|
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
|
||||||
|
case NOTIFICATIONS_DEQUEUE:
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
mutable.set('queuedNotifications', ImmutableList())
|
||||||
|
mutable.set('totalQueuedNotificationsCount', 0)
|
||||||
|
});
|
||||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
return expandNormalizedNotifications(state, action.notifications, action.next);
|
return expandNormalizedNotifications(state, action.notifications, action.next);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user