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:
mgabdev 2019-07-11 00:02:18 -04:00
parent 6ad747a609
commit c8e8618f64
4 changed files with 123 additions and 25 deletions

@ -16,6 +16,8 @@ import { me } from 'gabsocial/initial_state';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
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_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_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const MAX_QUEUED_NOTIFICATIONS = 40;
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
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) {
dispatch(importFetchedAccount(notification.account));
@ -65,21 +57,33 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
});
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
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
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();
@ -169,7 +215,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
export function clearNotifications() {
return (dispatch, getState) => {
if (!me) return;
dispatch({
type: NOTIFICATIONS_CLEAR,
});

@ -6,7 +6,7 @@ import {
connectTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
import { getLocale } from '../locales';
@ -36,7 +36,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));

@ -4,7 +4,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
import {
expandNotifications,
scrollTopNotifications,
dequeueNotifications,
} from '../../actions/notifications';
import NotificationContainer from './containers/notification_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
@ -14,6 +18,7 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
import LoadGap from '../../components/load_gap';
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@ -40,6 +45,7 @@ const mapStateToProps = state => ({
isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
});
export default @connect(mapStateToProps)
@ -54,6 +60,8 @@ class Notifications extends React.PureComponent {
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
};
componentWillUnmount () {
@ -61,6 +69,7 @@ class Notifications extends React.PureComponent {
this.handleScrollToTop.cancel();
this.handleScroll.cancel();
this.props.dispatch(scrollTopNotifications(false));
this.handleDequeueNotifications();
}
componentDidMount() {
@ -112,8 +121,12 @@ class Notifications extends React.PureComponent {
}
}
handleDequeueNotifications = () => {
this.props.dispatch(dequeueNotifications());
};
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." />;
let scrollableContent = null;
@ -168,6 +181,7 @@ class Notifications extends React.PureComponent {
<ColumnSettingsContainer />
</ColumnHeader>
{filterBarContainer}
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
{scrollContainer}
</Column>
);

@ -6,6 +6,9 @@ import {
NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_UPDATE_QUEUE,
NOTIFICATIONS_DEQUEUE,
MAX_QUEUED_NOTIFICATIONS,
} from '../actions/notifications';
import {
ACCOUNT_BLOCK_SUCCESS,
@ -21,6 +24,8 @@ const initialState = ImmutableMap({
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
});
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));
};
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) {
switch(action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
@ -105,6 +136,13 @@ export default function notifications(state = initialState, action) {
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
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:
return expandNormalizedNotifications(state, action.notifications, action.next);
case ACCOUNT_BLOCK_SUCCESS: