Skip to content

Commit

Permalink
console: Fix autoscroll loading
Browse files Browse the repository at this point in the history
  • Loading branch information
kschiffer committed Jan 16, 2024
1 parent 06aafa6 commit a72c16f
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const ApiKeyCreated = ({ notificationData }) => {
action: {
Link: msg => (
<Link
key={msg}
to={`/applications/${
entity_ids[`${getEntity(entity_ids)}_ids`][`${getEntity(entity_ids)}_id`]
}/api-keys`}
Expand Down
168 changes: 130 additions & 38 deletions pkg/webui/console/containers/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { useCallback, useState } from 'react'
import React, { useCallback, useState, useRef, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import classNames from 'classnames'
import { defineMessages } from 'react-intl'

import Button from '@ttn-lw/components/button'
import Spinner from '@ttn-lw/components/spinner'

import attachPromise from '@ttn-lw/lib/store/actions/attach-promise'
import useQueryState from '@ttn-lw/lib/hooks/use-query-state'

import { getNotifications } from '@console/store/actions/notifications'
import { getNotifications, updateNotificationStatus } from '@console/store/actions/notifications'

import { selectUserId } from '@console/store/selectors/logout'

Expand All @@ -31,82 +32,175 @@ import NotificationContent from './notification-content'

import style from './notifications.styl'

const BATCH_SIZE = 50

// Update a range of values in an array by using another array and a start index.
const fillIntoArray = (array, start, values, totalCount) => {
const newArray = [...array]
const end = Math.min(start + values.length, totalCount)
for (let i = start; i < end; i++) {
newArray[i] = values[i - start]
}
return newArray
}

const indicesToPage = (startIndex, stopIndex, limit) => {
const startPage = Math.floor(startIndex / limit) + 1
const stopPage = Math.floor(stopIndex / limit) + 1
return [startPage, stopPage]
}

const pageToIndices = (page, limit) => {
const startIndex = (page - 1) * limit
const stopIndex = page * limit - 1
return [startIndex, stopIndex]
}

const m = defineMessages({
seeArchived: 'See archived messages',
seeAll: 'See all messages',
})

const pageSize = 6
const DEFAULT_PAGE = 1

const Notifications = () => {
const listRef = useRef(null)
const userId = useSelector(selectUserId)
const dispatch = useDispatch()
const [selectedNotification, setSelectedNotification] = useState(undefined)
const [hasNextPage, setHasNextPage] = useState(true)
const [isNextPageLoading, setIsNextPageLoading] = useState(false)
const [items, setItems] = useState([])
const [page, setPage] = useState(DEFAULT_PAGE)
const [items, setItems] = useState(undefined)
const [showArchived, setShowArchived] = useQueryState('archived', 'false')
const [isArchiving, setIsArchiving] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [fetching, setFetching] = useState(false)

const loadNextPage = useCallback(
async filter => {
setIsNextPageLoading(true)
async (startIndex, stopIndex) => {
if (fetching) return
setFetching(true)

// Determine filters based on whether archived notifications should be shown.
const filters =
showArchived === 'true'
? ['NOTIFICATION_STATUS_ARCHIVED']
: typeof filter === 'string'
? filter
: ['NOTIFICATION_STATUS_UNSEEN', 'NOTIFICATION_STATUS_SEEN']

// Calculate the number of items to fetch.
const limit = Math.max(BATCH_SIZE, stopIndex - startIndex + 1)
const [startPage, stopPage] = indicesToPage(startIndex, stopIndex, limit)

// Fetch new notifications with a maximum of 1000 items.
const newItems = await dispatch(
attachPromise(
getNotifications(userId, filters, {
limit: pageSize,
page,
limit: Math.min((stopPage - startPage + 1) * BATCH_SIZE, 1000),
page: startPage,
}),
),
)
setPage(page => page + 1)
setItems(items => [...items, ...newItems.notifications])
setHasNextPage(items.length < newItems.totalCount)
setIsNextPageLoading(false)

// Update the total count of notifications.
setTotalCount(newItems.totalCount)

// Integrate the new items into the existing list.
const updatedItems = fillIntoArray(
items,
pageToIndices(startPage, limit)[0],
newItems.notifications,
newItems.totalCount,
)
setItems(updatedItems)

// Set the first notification as selected if none is currently selected.
if (!selectedNotification) {
setSelectedNotification(updatedItems[0])
}

// Determine if there are more pages to load.
setHasNextPage(updatedItems.length < newItems.totalCount)
setFetching(false)
},
[dispatch, userId, page, setPage, showArchived, items],
[fetching, showArchived, dispatch, userId, items, selectedNotification],
)

const handleShowArchived = useCallback(async () => {
setPage(DEFAULT_PAGE)
const handleShowArchived = useCallback(() => {
// Toggle the showArchived state.
setShowArchived(showArchived === 'false' ? 'true' : 'false')

// Reset items and selected notification.
setItems([])
setHasNextPage(true)
setSelectedNotification(undefined)
}, [setShowArchived, showArchived, setPage])

// Load the first page of archived notifications.
loadNextPage(0, BATCH_SIZE)
}, [loadNextPage, setShowArchived, showArchived])

const handleArchive = useCallback(
async (_, id) => {
// Determine the filter to apply based on the showArchived state.
const updateFilter =
showArchived === 'true' ? 'NOTIFICATION_STATUS_SEEN' : 'NOTIFICATION_STATUS_ARCHIVED'

// Update the status of the notification.
await dispatch(attachPromise(updateNotificationStatus(userId, [id], updateFilter)))

// Find the index of the archived notification.
const index = items.findIndex(item => item.id === id)

// Reload notifications starting from the archived one.
await loadNextPage(
index,
index + BATCH_SIZE > items.length - 1 ? items.length - 1 : index + BATCH_SIZE,
)

// Update the selected notification to the one above the archived one.
setSelectedNotification(items[Math.max(1, index - 1)])

// Reset the list cache if available so that old items are discarded.
if (listRef.current && listRef.current.resetloadMoreItemsCache) {
listRef.current.resetloadMoreItemsCache()
}
},
[showArchived, dispatch, userId, items, loadNextPage],
)

// Load the first page of notifications when the component mounts.
useEffect(() => {
loadNextPage(0, BATCH_SIZE)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

if (!items) {
return (
<div className="d-flex flex-grow">
<Spinner center />
</div>
)
}

return (
<div className="d-flex h-vh">
<div className="d-flex flex-grow">
<div
className={classNames(style.notificationList, {
className={classNames(style.notificationList, 'flex-grow', {
[style.notificationSelected]: selectedNotification,
})}
>
<NotificationList
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={loadNextPage}
items={items}
totalCount={totalCount}
setSelectedNotification={setSelectedNotification}
selectedNotification={selectedNotification}
isArchive={showArchived === 'true'}
isArchiving={isArchiving}
setIsArchiving={setIsArchiving}
/>
<Button
onClick={handleShowArchived}
naked
message={showArchived === 'true' ? m.seeAll : m.seeArchived}
className={style.notificationListChangeButton}
listRef={listRef}
/>
<div className="d-flex j-center">
<Button
onClick={handleShowArchived}
naked
message={showArchived === 'true' ? m.seeAll : m.seeArchived}
className={style.notificationListChangeButton}
/>
</div>
</div>
<div
className={classNames(style.notificationContent, {
Expand All @@ -117,10 +211,8 @@ const Notifications = () => {
<NotificationContent
setSelectedNotification={setSelectedNotification}
selectedNotification={selectedNotification}
fetchItems={loadNextPage}
isArchive={showArchived === 'true'}
setPage={setPage}
setIsArchiving={setIsArchiving}
onArchive={handleArchive}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import React, { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useSelector } from 'react-redux'
import classNames from 'classnames'
import { defineMessages } from 'react-intl'

Expand All @@ -23,11 +23,8 @@ import DateTime from '@ttn-lw/lib/components/date-time'

import Notification from '@console/components/notifications'

import attachPromise from '@ttn-lw/lib/store/actions/attach-promise'
import PropTypes from '@ttn-lw/lib/prop-types'

import { updateNotificationStatus } from '@console/store/actions/notifications'

import { selectUserId } from '@console/store/selectors/logout'

import style from '../notifications.styl'
Expand All @@ -38,22 +35,12 @@ const m = defineMessages({
})

const NotificationContent = ({
onArchive,
isArchive,
setSelectedNotification,
selectedNotification,
setIsArchiving,
}) => {
const userId = useSelector(selectUserId)
const dispatch = useDispatch()

const handleArchive = useCallback(
async (e, id) => {
setIsArchiving(true)
const updateFilter = isArchive ? 'NOTIFICATION_STATUS_SEEN' : 'NOTIFICATION_STATUS_ARCHIVED'
await dispatch(attachPromise(updateNotificationStatus(userId, [id], updateFilter)))
},
[dispatch, userId, isArchive, setIsArchiving],
)

const handleBack = useCallback(() => {
setSelectedNotification(undefined)
Expand Down Expand Up @@ -99,7 +86,7 @@ const NotificationContent = ({
})}
/>
<Button
onClick={handleArchive}
onClick={onArchive}
message={isArchive ? m.unarchive : m.archive}
icon="archive"
value={selectedNotification.id}
Expand All @@ -120,13 +107,13 @@ const NotificationContent = ({

NotificationContent.propTypes = {
isArchive: PropTypes.bool.isRequired,
onArchive: PropTypes.func.isRequired,
selectedNotification: PropTypes.shape({
id: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired,
notification_type: PropTypes.string.isRequired,
status: PropTypes.string,
}).isRequired,
setIsArchiving: PropTypes.func.isRequired,
setSelectedNotification: PropTypes.func.isRequired,
}

Expand Down
Loading

0 comments on commit a72c16f

Please sign in to comment.