Commit 663f46b1 authored by mgabdev's avatar mgabdev

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
parent 83696f80
......@@ -140,7 +140,7 @@ const excludeTypesFromFilter = filter => {
return allTypes.filterNot(item => item === filter).toJS();
};
const noOp = () => { };
const noOp = () => {}
export function expandNotifications({ maxId } = {}, done = noOp) {
return (dispatch, getState) => {
......@@ -252,17 +252,20 @@ export function setFilter(path, value) {
export function markReadNotifications() {
return (dispatch, getState) => {
if (!me) return;
const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
const last_read = getState().getIn(['notifications', 'lastRead']);
if (top_notification && top_notification > last_read) {
api(getState).post('/api/v1/notifications/mark_read', { id: top_notification }).then(response => {
if (!me) return
const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']))
const lastReadId = getState().getIn(['notifications', 'lastReadId'])
if (topNotification && topNotification > lastReadId && lastReadId !== -1) {
api(getState).post('/api/v1/notifications/mark_read', {
id: topNotification
}).then(() => {
dispatch({
type: NOTIFICATIONS_MARK_READ,
notification: top_notification,
});
});
notification: topNotification,
})
})
}
}
};
\ No newline at end of file
}
\ No newline at end of file
......@@ -2,13 +2,19 @@ import { Fragment } from 'react'
import { NavLink } from 'react-router-dom'
import { injectIntl, defineMessages } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { HotKeys } from 'react-hotkeys'
import ImmutablePropTypes from 'react-immutable-proptypes'
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 Avatar from './avatar'
import Icon from './icon'
import Text from './text'
import DotTextSeperator from './dot_text_seperator'
import RelativeTimestamp from './relative_timestamp'
import DisplayName from './display_name'
const messages = defineMessages({
......@@ -24,15 +30,6 @@ const messages = defineMessages({
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
@injectIntl
class Notification extends ImmutablePureComponent {
......@@ -48,6 +45,7 @@ class Notification extends ImmutablePureComponent {
statusId: PropTypes.string,
type: PropTypes.string.isRequired,
isHidden: PropTypes.bool,
isUnread: PropTypes.bool,
}
render() {
......@@ -58,12 +56,14 @@ class Notification extends ImmutablePureComponent {
type,
statusId,
isHidden,
isUnread,
} = this.props
const count = !!accounts ? accounts.size : 0
let message
let icon
switch (type) {
case 'follow':
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 (
<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.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.flexRow].join(' ')}>
<div className={[_s.default, _s.flexNormal].join(' ')}>
<div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>
{
accounts && accounts.slice(0, 8).map((account, i) => (
accounts && accounts.map((account, i) => (
<NavLink
to={`/${account.get('acct')}`}
key={`fav-avatar-${i}`}
className={_s.mr5}
className={[_s.mr5, _s.mb5].join(' ')}
>
<Avatar size={30} account={account} />
<Avatar size={34} account={account} />
</NavLink>
))
}
</div>
<div className={[_s.default, _s.pt10].join(' ')}>
<div className={[_s.default, _s.flexRow].join(' ')}>
<div className={[_s.default, _s.pt5].join(' ')}>
<div className={[_s.default, _s.flexRow, _s.alignItemsEnd].join(' ')}>
<div className={_s.text}>
{
accounts && accounts.slice(0, 1).map((account, i) => (
......@@ -148,6 +162,15 @@ class Notification extends ImmutablePureComponent {
{' '}
{message}
</Text>
{
!!createdAt &&
<Fragment>
<DotTextSeperator />
<Text size='small' color='tertiary' className={_s.ml5}>
<RelativeTimestamp timestamp={createdAt} />
</Text>
</Fragment>
}
</div>
</div>
{
......
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 Notification from '../components/notification'
......@@ -27,25 +14,29 @@ const makeMapStateToProps = () => {
const isLikes = !!props.notification.get('like')
const isReposts = !!props.notification.get('repost')
const isGrouped = isFollows || isLikes || isReposts
const lastReadId = state.getIn(['notifications', 'lastReadId'])
if (isFollows) {
let lastUpdated
const list = props.notification.get('follow')
let accounts = ImmutableList()
list.forEach((item) => {
const account = getAccountFromState(state, item.get('account'))
accounts = accounts.set(accounts.size, account)
if (!lastUpdated) lastUpdated = item.get('created_at')
})
return {
type: 'follow',
accounts: accounts,
createdAt: undefined,
accounts: accounts,
createdAt: lastUpdated,
isUnread: false,
statusId: undefined,
}
} else if (isLikes || isReposts) {
const theType = isLikes ? 'like' : 'repost'
const list = props.notification.get(theType)
let lastUpdated = list.get('lastUpdated')
let accounts = ImmutableList()
const accountIdArr = list.get('accounts')
......@@ -59,7 +50,8 @@ const makeMapStateToProps = () => {
return {
type: theType,
accounts: accounts,
createdAt: undefined,
createdAt: lastUpdated,
isUnread: false,
statusId: list.get('status'),
}
} else if (!isGrouped) {
......@@ -68,9 +60,10 @@ const makeMapStateToProps = () => {
const statusId = notification.get('status')
return {
accounts: !!account ? ImmutableList([account]) : ImmutableList(),
type: notification.get('type'),
accounts: !!account ? ImmutableList([account]) : ImmutableList(),
createdAt: notification.get('created_at'),
isUnread: lastReadId < notification.get('id'),
statusId: statusId || undefined,
}
}
......@@ -79,42 +72,4 @@ const makeMapStateToProps = () => {
return mapStateToProps
}
const mapDispatchToProps = (dispatch) => ({
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)
export default connect(makeMapStateToProps)(Notification)
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
import { createSelector } from 'reselect'
import { List as ImmutableList } from 'immutable'
import { FormattedMessage } from 'react-intl'
import debounce from 'lodash.debounce'
import {
expandNotifications,
......@@ -10,37 +9,33 @@ import {
dequeueNotifications,
} from '../actions/notifications'
import NotificationContainer from '../containers/notification_container'
// import ColumnSettingsContainer from './containers/column_settings_container'
import ScrollableList from '../components/scrollable_list'
import LoadMore from '../components/load_more'
import TimelineQueueButtonHeader from '../components/timeline_queue_button_header'
import Block from '../components/block'
import TimelineQueueButtonHeader from '../components/timeline_queue_button_header'
import Block from '../components/block'
const mapStateToProps = (state) => ({
notifications: state.getIn(['notifications', 'items']),
sortedNotifications: state.getIn(['notifications', 'sortedItems']),
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)
@injectIntl
class Notifications extends ImmutablePureComponent {
static propTypes = {
sortedNotifications: ImmutablePropTypes.list.isRequired,
notifications: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
}
componentWillUnmount () {
componentWillUnmount() {
this.handleLoadOlder.cancel()
this.handleScrollToTop.cancel()
this.handleScroll.cancel()
......@@ -52,14 +47,9 @@ class Notifications extends ImmutablePureComponent {
this.props.dispatch(scrollTopNotifications(true))
}
handleLoadGap = (maxId) => {
// maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
// this.props.dispatch(expandNotifications({ maxId }))
}
handleLoadOlder = debounce(() => {
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 })
handleScrollToTop = debounce(() => {
......@@ -70,97 +60,57 @@ class Notifications extends ImmutablePureComponent {
this.props.dispatch(scrollTopNotifications(false))
}, 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 = () => {
this.props.dispatch(dequeueNotifications())
}
render () {
render() {
const {
intl,
notifications,
sortedNotifications,
isLoading,
isUnread,
hasMore,
totalQueuedNotificationsCount
totalQueuedNotificationsCount,
} = this.props
let scrollableContent = null
// : todo : include follow requests
// console.log('--0--notifications:', hasMore)
if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent
} else if (notifications.size > 0 || hasMore) {
scrollableContent = notifications.map((item, index) => item === null ? (
<LoadMore disabled={isLoading} onClick={this.handleLoadGap} />
) : (
} else if (sortedNotifications.size > 0 || hasMore) {
scrollableContent = sortedNotifications.map((item, index) => (
<NotificationContainer
key={`notification-${index}`}
notification={item}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
))
} else {
scrollableContent = null
}
this.scrollableContent = scrollableContent
return (
<div ref={this.setColumnRef}>
<Fragment>
<TimelineQueueButtonHeader
onClick={this.handleDequeueNotifications}
count={totalQueuedNotificationsCount}
itemType='notification'
/>
<Block>
<ScrollableList
scrollKey='notifications'
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
showLoading={isLoading && sortedNotifications.size === 0}
hasMore={hasMore}
emptyMessage={<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />}
onLoadMore={this.handleLoadOlder}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
>
{ scrollableContent }
{scrollableContent}
</ScrollableList>
</Block>
</div>
</Fragment>
)
}
......
......@@ -24,6 +24,7 @@ export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout');
export const promotions = initialState && initialState.promotions;
export const unreadCount = getMeta('unread_count');
export const lastReadNotificationId = getMeta('last_read_notification_id');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
export const favouritesCount = getMeta('favourites_count');
export const compactMode = false;
......
......@@ -497,6 +497,7 @@ body {
.maxWidth100PC42PX { max-width: calc(100% - 42px); }
.minWidth330PX { min-width: 330px; }
.minWidth20PX { min-width: 20px; }
.minWidth14PX { min-width: 14px; }
.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[:is_staff] = object.current_account.user.staff?
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[:favourites_count] = object.current_account.favourites.count.to_s
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment