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:
parent
83696f8098
commit
663f46b166
@ -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,
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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)
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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:
|
||||||
|
@ -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%; }
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user