+
{
accounts && accounts.slice(0, 1).map((account, i) => (
@@ -148,6 +162,15 @@ class Notification extends ImmutablePureComponent {
{' '}
{message}
+ {
+ !!createdAt &&
+
+
+
+
+
+
+ }
{
diff --git a/app/javascript/gabsocial/containers/notification_container.js b/app/javascript/gabsocial/containers/notification_container.js
index 41e53e17..2da1826d 100644
--- a/app/javascript/gabsocial/containers/notification_container.js
+++ b/app/javascript/gabsocial/containers/notification_container.js
@@ -1,17 +1,4 @@
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)
diff --git a/app/javascript/gabsocial/features/notifications.js b/app/javascript/gabsocial/features/notifications.js
index 7a1a7f9c..dd236606 100644
--- a/app/javascript/gabsocial/features/notifications.js
+++ b/app/javascript/gabsocial/features/notifications.js
@@ -1,8 +1,7 @@
+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 ? (
-
- ) : (
+ } else if (sortedNotifications.size > 0 || hasMore) {
+ scrollableContent = sortedNotifications.map((item, index) => (
))
- } else {
- scrollableContent = null
}
this.scrollableContent = scrollableContent
return (
-
-
+
-
}
onLoadMore={this.handleLoadOlder}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
>
- { scrollableContent }
+ {scrollableContent}
-
+
)
}
diff --git a/app/javascript/gabsocial/initial_state.js b/app/javascript/gabsocial/initial_state.js
index bb840b14..b4ac7bc3 100644
--- a/app/javascript/gabsocial/initial_state.js
+++ b/app/javascript/gabsocial/initial_state.js
@@ -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;
diff --git a/app/javascript/gabsocial/reducers/notifications.js b/app/javascript/gabsocial/reducers/notifications.js
index d8b7b914..9a7d36e6 100644
--- a/app/javascript/gabsocial/reducers/notifications.js
+++ b/app/javascript/gabsocial/reducers/notifications.js
@@ -11,25 +11,29 @@ import {
NOTIFICATIONS_DEQUEUE,
MAX_QUEUED_NOTIFICATIONS,
NOTIFICATIONS_MARK_READ,
-} from '../actions/notifications';
+} from '../actions/notifications'
import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
-} from '../actions/accounts';
-import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
-import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+} from '../actions/accounts'
+import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'
+import { Range, Map as ImmutableMap, List as ImmutableList } from 'immutable'
+import { unreadCount, lastReadNotificationId } from '../initial_state'
import compareId from '../utils/compare_id';
-import { unreadCount } from '../initial_state';
+
+const DEFAULT_NOTIFICATIONS_LIMIT = 20
const initialState = ImmutableMap({
items: ImmutableList(),
+ sortedItems: ImmutableList(),
+ lastId: null,
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
- lastRead: -1,
+ lastReadId: -1,
filter: ImmutableMap({
active: 'all',
onlyVerified: false,
@@ -37,7 +41,7 @@ const initialState = ImmutableMap({
}),
});
-const notificationToMap = notification => ImmutableMap({
+const notificationToMap = (notification) => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
@@ -45,98 +49,137 @@ const notificationToMap = notification => ImmutableMap({
status: notification.status ? notification.status.id : null,
});
-const normalizeNotification = (state, notification) => {
- const top = state.get('top');
+const makeSortedNotifications = (state) => {
+ let finalSortedItems = ImmutableList()
+ const items = state.get('items')
- if (!top) {
- state = state.update('unread', unread => unread + 1);
- }
-
- 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 = {}
+ const chunks = Range(0, items.count(), DEFAULT_NOTIFICATIONS_LIMIT)
+ .map((chunkStart) => items.slice(chunkStart, chunkStart + DEFAULT_NOTIFICATIONS_LIMIT))
- let items = ImmutableList()
+ chunks.forEach((chunk) => {
+ let sortedItems = ImmutableList()
- notifications.forEach((n) => {
- const notification = notificationToMap(n)
- const statusId = notification.get('status')
- const type = notification.get('type')
+ let follows = ImmutableList()
+ let likes = {}
+ let reposts = {}
+
+ let followIndex = -1
+ let indexesForStatusesForReposts = {}
+ let indexesForStatusesForFavorites = {}
- switch (type) {
- case 'follow': {
- follows = follows.set(follows.size, notification)
- break
+ chunk.forEach((notification) => {
+ const statusId = notification.get('status')
+ const type = notification.get('type')
+
+ 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] = []
- likes[statusId].push({
- account: notification.get('account'),
- })
- break
+
+ if (follows.size > 0) {
+ sortedItems = sortedItems.set(followIndex, ImmutableMap({
+ follow: follows,
+ }))
}
- case 'reblog': {
- if (reposts[statusId] === undefined) reposts[statusId] = []
- reposts[statusId].push({
- account: notification.get('account'),
- })
- break
+ if (Object.keys(likes).length > 0) {
+ for (const statusId in likes) {
+ if (likes.hasOwnProperty(statusId)) {
+ const likeArr = likes[statusId]
+ const accounts = likeArr.map((l) => l.account)
+ const lastUpdated = likeArr[0]['created_at']
+ sortedItems = sortedItems.set(indexesForStatusesForFavorites[statusId], ImmutableMap({
+ like: ImmutableMap({
+ accounts,
+ lastUpdated,
+ status: statusId,
+ })
+ }))
+ }
+ }
}
- default: {
- items = items.set(items.size, notification)
- break
+ 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)
+ 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) {
- items = items.set(items.size, ImmutableMap({
- follow: follows,
- }))
- }
- if (Object.keys(likes).length > 0) {
- for (const statusId in likes) {
- if (likes.hasOwnProperty(statusId)) {
- 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.set('sortedItems', finalSortedItems)
+}
+
+const normalizeNotification = (state, notification) => {
+ const top = state.get('top')
+
+ if (!top) {
+ state = state.update('unread', (unread) => unread + 1)
}
- 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()) {
- mutable.update('items', list => {
+ mutable.update('items', (list) => {
const lastIndex = 1 + list.findLastIndex(
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
)
- const pop = list.take(firstIndex).concat(items, list.skip(lastIndex))
-
- return pop
+ return list.take(firstIndex).concat(items, list.skip(lastIndex))
})
}
- if (!next) {
- mutable.set('hasMore', false);
- }
+ if (!next) mutable.set('hasMore', false)
+ mutable.set('isLoading', false)
+ })
- mutable.set('isLoading', false);
- });
-};
-
-const filterNotifications = (state, relationship) => {
- return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
-};
+ return makeSortedNotifications(state)
+}
const updateTop = (state, top) => {
- if (top) {
- state = state.set('unread', 0);
- }
+ return state.withMutations((mutable) => {
+ 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) => {
- 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 queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
@@ -181,9 +224,10 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
const unread = state.getIn(['unread'], 0)
- let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id);
- if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('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) {
return state;
}
@@ -206,9 +250,12 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_INITIALIZE:
- return state.set('unread', unreadCount);
+ return state.withMutations((mutable) => {
+ mutable.set('unread', unreadCount)
+ mutable.set('lastReadId', lastReadNotificationId)
+ })
case NOTIFICATIONS_MARK_READ:
- return state.set('lastRead', action.notification);
+ return state.set('lastReadId', action.lastReadId);
case NOTIFICATIONS_EXPAND_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_EXPAND_FAIL:
diff --git a/app/javascript/styles/global.css b/app/javascript/styles/global.css
index b92b41e3..ecc5a959 100644
--- a/app/javascript/styles/global.css
+++ b/app/javascript/styles/global.css
@@ -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%; }
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 956dfbe5..f50d0502 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -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