Updated frontend notification filtering and configuration

• Updated:
- frontend notification filtering and configuration

• Added:
- lastReadNotificationId to initial_state
- minWidth20PX for responsive notification icon hiding on xs

• Removed:
- unused code
This commit is contained in:
mgabdev 2020-05-21 15:37:40 -04:00
parent 83696f8098
commit 663f46b166
8 changed files with 245 additions and 264 deletions

View File

@ -140,7 +140,7 @@ const excludeTypesFromFilter = filter => {
return allTypes.filterNot(item => item === filter).toJS(); return allTypes.filterNot(item => item === filter).toJS();
}; };
const noOp = () => { }; const noOp = () => {}
export function expandNotifications({ maxId } = {}, done = noOp) { export function expandNotifications({ maxId } = {}, done = noOp) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -252,17 +252,20 @@ export function setFilter(path, value) {
export function markReadNotifications() { export function markReadNotifications() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!me) return; if (!me) return
const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
const last_read = getState().getIn(['notifications', 'lastRead']); const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']))
const lastReadId = getState().getIn(['notifications', 'lastReadId'])
if (top_notification && top_notification > last_read) { if (topNotification && topNotification > lastReadId && lastReadId !== -1) {
api(getState).post('/api/v1/notifications/mark_read', { id: top_notification }).then(response => { api(getState).post('/api/v1/notifications/mark_read', {
id: topNotification
}).then(() => {
dispatch({ dispatch({
type: NOTIFICATIONS_MARK_READ, type: NOTIFICATIONS_MARK_READ,
notification: top_notification, notification: topNotification,
}); })
}); })
} }
} }
}; }

View File

@ -2,13 +2,19 @@ import { Fragment } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { injectIntl, defineMessages } from 'react-intl' import { injectIntl, defineMessages } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import { HotKeys } from 'react-hotkeys'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import { me } from '../initial_state' import { me } from '../initial_state'
import {
CX,
BREAKPOINT_EXTRA_SMALL,
} from '../constants'
import Responsive from '../features/ui/util/responsive_component'
import StatusContainer from '../containers/status_container' import StatusContainer from '../containers/status_container'
import Avatar from './avatar' import Avatar from './avatar'
import Icon from './icon' import Icon from './icon'
import Text from './text' import Text from './text'
import DotTextSeperator from './dot_text_seperator'
import RelativeTimestamp from './relative_timestamp'
import DisplayName from './display_name' import DisplayName from './display_name'
const messages = defineMessages({ const messages = defineMessages({
@ -24,15 +30,6 @@ const messages = defineMessages({
repostedStatusMultiple: { id: 'reposted_status_multiple', defaultMessage: 'and {count} others reposted your status' }, repostedStatusMultiple: { id: 'reposted_status_multiple', defaultMessage: 'and {count} others reposted your status' },
}) })
// : todo :
const notificationForScreenReader = (intl, message, timestamp) => {
const output = [message]
output.push(intl.formatDate(timestamp, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }))
return output.join(', ')
}
export default export default
@injectIntl @injectIntl
class Notification extends ImmutablePureComponent { class Notification extends ImmutablePureComponent {
@ -48,6 +45,7 @@ class Notification extends ImmutablePureComponent {
statusId: PropTypes.string, statusId: PropTypes.string,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
isHidden: PropTypes.bool, isHidden: PropTypes.bool,
isUnread: PropTypes.bool,
} }
render() { render() {
@ -58,12 +56,14 @@ class Notification extends ImmutablePureComponent {
type, type,
statusId, statusId,
isHidden, isHidden,
isUnread,
} = this.props } = this.props
const count = !!accounts ? accounts.size : 0 const count = !!accounts ? accounts.size : 0
let message let message
let icon let icon
switch (type) { switch (type) {
case 'follow': case 'follow':
icon = 'group' icon = 'group'
@ -114,29 +114,43 @@ class Notification extends ImmutablePureComponent {
) )
} }
const containerClasses = CX({
default: 1,
px10: 1,
cursorPointer: 1,
bgSubtle_onHover: !isUnread,
highlightedComment: isUnread,
})
return ( return (
<div className={[_s.default, _s.px10, _s.cursorPointer, _s.bgSubtle_onHover].join(' ')}> <div
className={containerClasses}
tabIndex='0'
aria-label={`${message} ${createdAt}`}
>
<div className={[_s.default, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}> <div className={[_s.default, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<div className={[_s.default, _s.flexRow, _s.my10, _s.py10, _s.px10].join(' ')}> <div className={[_s.default, _s.flexRow, _s.my10, _s.py10, _s.px10].join(' ')}>
<Icon id={icon} size='20px' className={[_s.fillPrimary, _s.mt5].join(' ')} /> <Responsive min={BREAKPOINT_EXTRA_SMALL}>
<Icon id={icon} size='20px' className={[_s.fillPrimary, _s.minWidth20PX, _s.mt5, _s.mr15].join(' ')} />
</Responsive>
<div className={[_s.default, _s.ml15, _s.flexNormal].join(' ')}> <div className={[_s.default, _s.flexNormal].join(' ')}>
<div className={[_s.default, _s.flexRow].join(' ')}> <div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>
{ {
accounts && accounts.slice(0, 8).map((account, i) => ( accounts && accounts.map((account, i) => (
<NavLink <NavLink
to={`/${account.get('acct')}`} to={`/${account.get('acct')}`}
key={`fav-avatar-${i}`} key={`fav-avatar-${i}`}
className={_s.mr5} className={[_s.mr5, _s.mb5].join(' ')}
> >
<Avatar size={30} account={account} /> <Avatar size={34} account={account} />
</NavLink> </NavLink>
)) ))
} }
</div> </div>
<div className={[_s.default, _s.pt10].join(' ')}> <div className={[_s.default, _s.pt5].join(' ')}>
<div className={[_s.default, _s.flexRow].join(' ')}> <div className={[_s.default, _s.flexRow, _s.alignItemsEnd].join(' ')}>
<div className={_s.text}> <div className={_s.text}>
{ {
accounts && accounts.slice(0, 1).map((account, i) => ( accounts && accounts.slice(0, 1).map((account, i) => (
@ -148,6 +162,15 @@ class Notification extends ImmutablePureComponent {
{' '} {' '}
{message} {message}
</Text> </Text>
{
!!createdAt &&
<Fragment>
<DotTextSeperator />
<Text size='small' color='tertiary' className={_s.ml5}>
<RelativeTimestamp timestamp={createdAt} />
</Text>
</Fragment>
}
</div> </div>
</div> </div>
{ {

View File

@ -1,17 +1,4 @@
import { List as ImmutableList } from 'immutable' import { List as ImmutableList } from 'immutable'
import { openModal } from '../actions/modal'
import { mentionCompose } from '../actions/compose'
import {
reblog,
favorite,
unreblog,
unfavorite,
} from '../actions/interactions'
import {
hideStatus,
revealStatus,
} from '../actions/statuses'
import { boostModal } from '../initial_state'
import { makeGetNotification } from '../selectors' import { makeGetNotification } from '../selectors'
import Notification from '../components/notification' import Notification from '../components/notification'
@ -27,25 +14,29 @@ const makeMapStateToProps = () => {
const isLikes = !!props.notification.get('like') const isLikes = !!props.notification.get('like')
const isReposts = !!props.notification.get('repost') const isReposts = !!props.notification.get('repost')
const isGrouped = isFollows || isLikes || isReposts const isGrouped = isFollows || isLikes || isReposts
const lastReadId = state.getIn(['notifications', 'lastReadId'])
if (isFollows) { if (isFollows) {
let lastUpdated
const list = props.notification.get('follow') const list = props.notification.get('follow')
let accounts = ImmutableList() let accounts = ImmutableList()
list.forEach((item) => { list.forEach((item) => {
const account = getAccountFromState(state, item.get('account')) const account = getAccountFromState(state, item.get('account'))
accounts = accounts.set(accounts.size, account) accounts = accounts.set(accounts.size, account)
if (!lastUpdated) lastUpdated = item.get('created_at')
}) })
return { return {
type: 'follow', type: 'follow',
accounts: accounts, accounts: accounts,
createdAt: undefined, createdAt: lastUpdated,
isUnread: false,
statusId: undefined, statusId: undefined,
} }
} else if (isLikes || isReposts) { } else if (isLikes || isReposts) {
const theType = isLikes ? 'like' : 'repost' const theType = isLikes ? 'like' : 'repost'
const list = props.notification.get(theType) const list = props.notification.get(theType)
let lastUpdated = list.get('lastUpdated')
let accounts = ImmutableList() let accounts = ImmutableList()
const accountIdArr = list.get('accounts') const accountIdArr = list.get('accounts')
@ -59,7 +50,8 @@ const makeMapStateToProps = () => {
return { return {
type: theType, type: theType,
accounts: accounts, accounts: accounts,
createdAt: undefined, createdAt: lastUpdated,
isUnread: false,
statusId: list.get('status'), statusId: list.get('status'),
} }
} else if (!isGrouped) { } else if (!isGrouped) {
@ -68,9 +60,10 @@ const makeMapStateToProps = () => {
const statusId = notification.get('status') const statusId = notification.get('status')
return { return {
accounts: !!account ? ImmutableList([account]) : ImmutableList(),
type: notification.get('type'), type: notification.get('type'),
accounts: !!account ? ImmutableList([account]) : ImmutableList(),
createdAt: notification.get('created_at'), createdAt: notification.get('created_at'),
isUnread: lastReadId < notification.get('id'),
statusId: statusId || undefined, statusId: statusId || undefined,
} }
} }
@ -79,42 +72,4 @@ const makeMapStateToProps = () => {
return mapStateToProps return mapStateToProps
} }
const mapDispatchToProps = (dispatch) => ({ export default connect(makeMapStateToProps)(Notification)
onMention: (account, router) => {
dispatch(mentionCompose(account, router))
},
onModalRepost (status) {
dispatch(repost(status))
},
onRepost (status, e) {
if (status.get('reblogged')) {
dispatch(unrepost(status))
} else {
if (e.shiftKey || !boostModal) {
this.onModalRepost(status)
} else {
dispatch(openModal('BOOST', { status, onRepost: this.onModalRepost }))
}
}
},
onFavorite (status) {
if (status.get('favourited')) {
dispatch(unfavorite(status))
} else {
dispatch(favorite(status))
}
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')))
} else {
dispatch(hideStatus(status.get('id')))
}
},
})
export default connect(makeMapStateToProps, mapDispatchToProps)(Notification)

View File

@ -1,8 +1,7 @@
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
import { createSelector } from 'reselect'
import { List as ImmutableList } from 'immutable'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import { import {
expandNotifications, expandNotifications,
@ -10,37 +9,33 @@ import {
dequeueNotifications, dequeueNotifications,
} from '../actions/notifications' } from '../actions/notifications'
import NotificationContainer from '../containers/notification_container' import NotificationContainer from '../containers/notification_container'
// import ColumnSettingsContainer from './containers/column_settings_container'
import ScrollableList from '../components/scrollable_list' import ScrollableList from '../components/scrollable_list'
import LoadMore from '../components/load_more' import TimelineQueueButtonHeader from '../components/timeline_queue_button_header'
import TimelineQueueButtonHeader from '../components/timeline_queue_button_header' import Block from '../components/block'
import Block from '../components/block'
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
notifications: state.getIn(['notifications', 'items']), notifications: state.getIn(['notifications', 'items']),
sortedNotifications: state.getIn(['notifications', 'sortedItems']),
isLoading: state.getIn(['notifications', 'isLoading'], true), isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']), hasMore: state.getIn(['notifications', 'hasMore']),
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0), totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
}) })
export default export default
@connect(mapStateToProps) @connect(mapStateToProps)
@injectIntl
class Notifications extends ImmutablePureComponent { class Notifications extends ImmutablePureComponent {
static propTypes = { static propTypes = {
sortedNotifications: ImmutablePropTypes.list.isRequired,
notifications: ImmutablePropTypes.list.isRequired, notifications: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func, dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number, totalQueuedNotificationsCount: PropTypes.number,
} }
componentWillUnmount () { componentWillUnmount() {
this.handleLoadOlder.cancel() this.handleLoadOlder.cancel()
this.handleScrollToTop.cancel() this.handleScrollToTop.cancel()
this.handleScroll.cancel() this.handleScroll.cancel()
@ -52,14 +47,9 @@ class Notifications extends ImmutablePureComponent {
this.props.dispatch(scrollTopNotifications(true)) this.props.dispatch(scrollTopNotifications(true))
} }
handleLoadGap = (maxId) => {
// maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
// this.props.dispatch(expandNotifications({ maxId }))
}
handleLoadOlder = debounce(() => { handleLoadOlder = debounce(() => {
const last = this.props.notifications.last() const last = this.props.notifications.last()
// this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })) this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }))
}, 300, { leading: true }) }, 300, { leading: true })
handleScrollToTop = debounce(() => { handleScrollToTop = debounce(() => {
@ -70,97 +60,57 @@ class Notifications extends ImmutablePureComponent {
this.props.dispatch(scrollTopNotifications(false)) this.props.dispatch(scrollTopNotifications(false))
}, 100) }, 100)
setColumnRef = c => {
this.column = c
}
handleMoveUp = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1
this._selectChild(elementIndex, true)
}
handleMoveDown = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1
this._selectChild(elementIndex, false)
}
_selectChild (index, align_top) {
const container = this.column.node
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`)
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true)
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false)
}
element.focus()
}
}
handleDequeueNotifications = () => { handleDequeueNotifications = () => {
this.props.dispatch(dequeueNotifications()) this.props.dispatch(dequeueNotifications())
} }
render () { render() {
const { const {
intl, sortedNotifications,
notifications,
isLoading, isLoading,
isUnread,
hasMore, hasMore,
totalQueuedNotificationsCount totalQueuedNotificationsCount,
} = this.props } = this.props
let scrollableContent = null let scrollableContent = null
// : todo : include follow requests // : todo : include follow requests
// console.log('--0--notifications:', hasMore)
if (isLoading && this.scrollableContent) { if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent scrollableContent = this.scrollableContent
} else if (notifications.size > 0 || hasMore) { } else if (sortedNotifications.size > 0 || hasMore) {
scrollableContent = notifications.map((item, index) => item === null ? ( scrollableContent = sortedNotifications.map((item, index) => (
<LoadMore disabled={isLoading} onClick={this.handleLoadGap} />
) : (
<NotificationContainer <NotificationContainer
key={`notification-${index}`} key={`notification-${index}`}
notification={item} notification={item}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/> />
)) ))
} else {
scrollableContent = null
} }
this.scrollableContent = scrollableContent this.scrollableContent = scrollableContent
return ( return (
<div ref={this.setColumnRef}> <Fragment>
<TimelineQueueButtonHeader <TimelineQueueButtonHeader
onClick={this.handleDequeueNotifications} onClick={this.handleDequeueNotifications}
count={totalQueuedNotificationsCount} count={totalQueuedNotificationsCount}
itemType='notification' itemType='notification'
/> />
<Block> <Block>
<ScrollableList <ScrollableList
scrollKey='notifications' scrollKey='notifications'
isLoading={isLoading} isLoading={isLoading}
showLoading={isLoading && notifications.size === 0} showLoading={isLoading && sortedNotifications.size === 0}
hasMore={hasMore} hasMore={hasMore}
emptyMessage={<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />} emptyMessage={<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />}
onLoadMore={this.handleLoadOlder} onLoadMore={this.handleLoadOlder}
onScrollToTop={this.handleScrollToTop} onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
> >
{ scrollableContent } {scrollableContent}
</ScrollableList> </ScrollableList>
</Block> </Block>
</div> </Fragment>
) )
} }

View File

@ -24,6 +24,7 @@ export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout'); export const forceSingleColumn = !getMeta('advanced_layout');
export const promotions = initialState && initialState.promotions; export const promotions = initialState && initialState.promotions;
export const unreadCount = getMeta('unread_count'); export const unreadCount = getMeta('unread_count');
export const lastReadNotificationId = getMeta('last_read_notification_id');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete'); export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
export const favouritesCount = getMeta('favourites_count'); export const favouritesCount = getMeta('favourites_count');
export const compactMode = false; export const compactMode = false;

View File

@ -11,25 +11,29 @@ import {
NOTIFICATIONS_DEQUEUE, NOTIFICATIONS_DEQUEUE,
MAX_QUEUED_NOTIFICATIONS, MAX_QUEUED_NOTIFICATIONS,
NOTIFICATIONS_MARK_READ, NOTIFICATIONS_MARK_READ,
} from '../actions/notifications'; } from '../actions/notifications'
import { import {
ACCOUNT_BLOCK_SUCCESS, ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS, ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts'; } from '../actions/accounts'
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Range, Map as ImmutableMap, List as ImmutableList } from 'immutable'
import { unreadCount, lastReadNotificationId } from '../initial_state'
import compareId from '../utils/compare_id'; import compareId from '../utils/compare_id';
import { unreadCount } from '../initial_state';
const DEFAULT_NOTIFICATIONS_LIMIT = 20
const initialState = ImmutableMap({ const initialState = ImmutableMap({
items: ImmutableList(), items: ImmutableList(),
sortedItems: ImmutableList(),
lastId: null,
hasMore: true, hasMore: true,
top: false, top: false,
unread: 0, unread: 0,
isLoading: false, isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+ totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
lastRead: -1, lastReadId: -1,
filter: ImmutableMap({ filter: ImmutableMap({
active: 'all', active: 'all',
onlyVerified: false, onlyVerified: false,
@ -37,7 +41,7 @@ const initialState = ImmutableMap({
}), }),
}); });
const notificationToMap = notification => ImmutableMap({ const notificationToMap = (notification) => ImmutableMap({
id: notification.id, id: notification.id,
type: notification.type, type: notification.type,
account: notification.account.id, account: notification.account.id,
@ -45,98 +49,137 @@ const notificationToMap = notification => ImmutableMap({
status: notification.status ? notification.status.id : null, status: notification.status ? notification.status.id : null,
}); });
const normalizeNotification = (state, notification) => { const makeSortedNotifications = (state) => {
const top = state.get('top'); let finalSortedItems = ImmutableList()
const items = state.get('items')
if (!top) { const chunks = Range(0, items.count(), DEFAULT_NOTIFICATIONS_LIMIT)
state = state.update('unread', unread => unread + 1); .map((chunkStart) => items.slice(chunkStart, chunkStart + DEFAULT_NOTIFICATIONS_LIMIT))
}
return state.update('items', list => {
if (top && list.size > 40) {
list = list.take(20);
}
return list.unshift(notificationToMap(notification));
});
};
const expandNormalizedNotifications = (state, notifications, next) => {
//Grouped items
let follows = ImmutableList()
let likes = {}
let reposts = {}
let items = ImmutableList() chunks.forEach((chunk) => {
let sortedItems = ImmutableList()
notifications.forEach((n) => { let follows = ImmutableList()
const notification = notificationToMap(n) let likes = {}
const statusId = notification.get('status') let reposts = {}
const type = notification.get('type')
let followIndex = -1
let indexesForStatusesForReposts = {}
let indexesForStatusesForFavorites = {}
switch (type) { chunk.forEach((notification) => {
case 'follow': { const statusId = notification.get('status')
follows = follows.set(follows.size, notification) const type = notification.get('type')
break
switch (type) {
case 'follow': {
if (followIndex === -1) followIndex = sortedItems.size
sortedItems = sortedItems.set(followIndex, ImmutableMap())
follows = follows.set(follows.size, notification)
break
}
case 'favourite': {
if (likes[statusId] === undefined) {
let size = sortedItems.size
sortedItems = sortedItems.set(size, ImmutableMap())
indexesForStatusesForFavorites[statusId] = size
likes[statusId] = []
}
likes[statusId].push({
account: notification.get('account'),
created_at: notification.get('created_at'),
})
break
}
case 'reblog': {
if (reposts[statusId] === undefined) {
let size = sortedItems.size
sortedItems = sortedItems.set(size, ImmutableMap())
indexesForStatusesForReposts[statusId] = size
reposts[statusId] = []
}
reposts[statusId].push({
account: notification.get('account'),
created_at: notification.get('created_at'),
})
break
}
default: {
sortedItems = sortedItems.set(sortedItems.size, notification)
break
}
} }
case 'favourite': {
if (likes[statusId] === undefined) likes[statusId] = [] if (follows.size > 0) {
likes[statusId].push({ sortedItems = sortedItems.set(followIndex, ImmutableMap({
account: notification.get('account'), follow: follows,
}) }))
break
} }
case 'reblog': { if (Object.keys(likes).length > 0) {
if (reposts[statusId] === undefined) reposts[statusId] = [] for (const statusId in likes) {
reposts[statusId].push({ if (likes.hasOwnProperty(statusId)) {
account: notification.get('account'), const likeArr = likes[statusId]
}) const accounts = likeArr.map((l) => l.account)
break const lastUpdated = likeArr[0]['created_at']
sortedItems = sortedItems.set(indexesForStatusesForFavorites[statusId], ImmutableMap({
like: ImmutableMap({
accounts,
lastUpdated,
status: statusId,
})
}))
}
}
} }
default: { if (Object.keys(reposts).length > 0) {
items = items.set(items.size, notification) for (const statusId in reposts) {
break if (reposts.hasOwnProperty(statusId)) {
const repostArr = reposts[statusId]
const accounts = repostArr.map((l) => l.account)
const lastUpdated = repostArr[0]['created_at']
sortedItems = sortedItems.set(indexesForStatusesForReposts[statusId], ImmutableMap({
repost: ImmutableMap({
accounts,
lastUpdated,
status: statusId,
})
}))
}
}
} }
} })
finalSortedItems = finalSortedItems.concat(sortedItems)
}) })
if (follows.size > 0) { return state.set('sortedItems', finalSortedItems)
items = items.set(items.size, ImmutableMap({ }
follow: follows,
})) const normalizeNotification = (state, notification) => {
} const top = state.get('top')
if (Object.keys(likes).length > 0) {
for (const statusId in likes) { if (!top) {
if (likes.hasOwnProperty(statusId)) { state = state.update('unread', (unread) => unread + 1)
const likeArr = likes[statusId]
const accounts = likeArr.map((l) => l.account)
items = items.set(items.size, ImmutableMap({
like: ImmutableMap({
status: statusId,
accounts: accounts,
})
}))
}
}
}
if (Object.keys(reposts).length > 0) {
for (const statusId in reposts) {
if (reposts.hasOwnProperty(statusId)) {
const repostArr = reposts[statusId]
const accounts = repostArr.map((l) => l.account)
items = items.set(items.size, ImmutableMap({
repost: ImmutableMap({
status: statusId,
accounts: accounts,
})
}))
}
}
} }
return state.withMutations(mutable => { state = state.update('items', (list) => {
if (top && list.size > 40) list = list.take(20)
return list.unshift(notificationToMap(notification))
})
return makeSortedNotifications(state)
}
const expandNormalizedNotifications = (state, notifications, next) => {
let items = ImmutableList()
notifications.forEach((n, i) => {
items = items.set(i, notificationToMap(n))
})
state = state.withMutations((mutable) => {
if (!items.isEmpty()) { if (!items.isEmpty()) {
mutable.update('items', list => { mutable.update('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'))
) )
@ -145,35 +188,35 @@ const expandNormalizedNotifications = (state, notifications, next) => {
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0 item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
) )
const pop = list.take(firstIndex).concat(items, list.skip(lastIndex)) return list.take(firstIndex).concat(items, list.skip(lastIndex))
return pop
}) })
} }
if (!next) { if (!next) mutable.set('hasMore', false)
mutable.set('hasMore', false); mutable.set('isLoading', false)
} })
mutable.set('isLoading', false); return makeSortedNotifications(state)
}); }
};
const filterNotifications = (state, relationship) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
};
const updateTop = (state, top) => { const updateTop = (state, top) => {
if (top) { return state.withMutations((mutable) => {
state = state.set('unread', 0); if (top) mutable.set('unread', 0)
} mutable.set('top', top)
})
}
return state.set('top', top); const filterNotifications = (state, relationship) => {
}; const filterer = (list) => list.filterNot((item) => !!item && item.get('account') === relationship.id)
state = state.update('items', filterer)
return makeSortedNotifications(state)
}
const deleteByStatus = (state, statusId) => { const deleteByStatus = (state, statusId) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); const filterer = (list) => list.filterNot((item) => !!item && item.get('status') === statusId)
}; state = state.update('items', filterer)
return makeSortedNotifications(state)
}
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => { const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList()); const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
@ -181,9 +224,10 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0); const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
const unread = state.getIn(['unread'], 0) const unread = state.getIn(['unread'], 0)
let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id); let alreadyExists = queuedNotifications.find((existingQueuedNotification) => existingQueuedNotification.id === notification.id);
if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id); if (!alreadyExists) {
alreadyExists = listedNotifications.find((existingListedNotification) => existingListedNotification.get('id') === notification.id);
}
if (alreadyExists) { if (alreadyExists) {
return state; return state;
} }
@ -206,9 +250,12 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
export default function notifications(state = initialState, action) { export default function notifications(state = initialState, action) {
switch(action.type) { switch(action.type) {
case NOTIFICATIONS_INITIALIZE: case NOTIFICATIONS_INITIALIZE:
return state.set('unread', unreadCount); return state.withMutations((mutable) => {
mutable.set('unread', unreadCount)
mutable.set('lastReadId', lastReadNotificationId)
})
case NOTIFICATIONS_MARK_READ: case NOTIFICATIONS_MARK_READ:
return state.set('lastRead', action.notification); return state.set('lastReadId', action.lastReadId);
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:

View File

@ -497,6 +497,7 @@ body {
.maxWidth100PC42PX { max-width: calc(100% - 42px); } .maxWidth100PC42PX { max-width: calc(100% - 42px); }
.minWidth330PX { min-width: 330px; } .minWidth330PX { min-width: 330px; }
.minWidth20PX { min-width: 20px; }
.minWidth14PX { min-width: 14px; } .minWidth14PX { min-width: 14px; }
.width100PC { width: 100%; } .width100PC { width: 100%; }

View File

@ -37,6 +37,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:group_in_home_feed] = object.current_account.user.setting_group_in_home_feed store[:group_in_home_feed] = object.current_account.user.setting_group_in_home_feed
store[:is_staff] = object.current_account.user.staff? store[:is_staff] = object.current_account.user.staff?
store[:unread_count] = unread_count object.current_account store[:unread_count] = unread_count object.current_account
store[:last_read_notification_id] = object.current_account.user.last_read_notification
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0 store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
store[:favourites_count] = object.current_account.favourites.count.to_s store[:favourites_count] = object.current_account.favourites.count.to_s
end end