From a72c16fa5bdc95e2b31daa7d4ecbd28524ea10f4 Mon Sep 17 00:00:00 2001 From: Kevin Schiffer Date: Tue, 16 Jan 2024 17:48:33 +0900 Subject: [PATCH] console: Fix autoscroll loading --- .../templates/api-key-created.js | 1 + .../console/containers/notifications/index.js | 168 ++++++++++++++---- .../notification-content/index.js | 21 +-- .../notifications/notification-list/index.js | 71 ++++---- .../notifications/notifications.styl | 5 +- pkg/webui/console/views/app/app.styl | 2 + pkg/webui/console/views/app/index.js | 6 +- 7 files changed, 179 insertions(+), 95 deletions(-) diff --git a/pkg/webui/console/components/notifications/templates/api-key-created.js b/pkg/webui/console/components/notifications/templates/api-key-created.js index ea8a86e0bb..557a9e14c0 100644 --- a/pkg/webui/console/components/notifications/templates/api-key-created.js +++ b/pkg/webui/console/components/notifications/templates/api-key-created.js @@ -89,6 +89,7 @@ const ApiKeyCreated = ({ notificationData }) => { action: { Link: msg => ( { + 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 ( +
+ +
+ ) + } return ( -
+
-
{ )}
diff --git a/pkg/webui/console/containers/notifications/notification-content/index.js b/pkg/webui/console/containers/notifications/notification-content/index.js index cbb05fcd92..a9c15945b1 100644 --- a/pkg/webui/console/containers/notifications/notification-content/index.js +++ b/pkg/webui/console/containers/notifications/notification-content/index.js @@ -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' @@ -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' @@ -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) @@ -99,7 +86,7 @@ const NotificationContent = ({ })} />
- - {({ onItemsRendered, ref }) => ( - - {Item} - - )} - +
+ + {({ height, width }) => ( + + {({ onItemsRendered, ref }) => ( + + {Item} + + )} + + )} + +
) } @@ -167,14 +164,14 @@ const NotificationList = ({ NotificationList.propTypes = { hasNextPage: PropTypes.bool.isRequired, isArchive: PropTypes.bool.isRequired, - isArchiving: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, + listRef: PropTypes.shape({ current: PropTypes.shape({}) }).isRequired, loadNextPage: PropTypes.func.isRequired, selectedNotification: PropTypes.shape({ id: PropTypes.string, }), - setIsArchiving: PropTypes.func.isRequired, setSelectedNotification: PropTypes.func.isRequired, + totalCount: PropTypes.number.isRequired, } NotificationList.defaultProps = { diff --git a/pkg/webui/console/containers/notifications/notifications.styl b/pkg/webui/console/containers/notifications/notifications.styl index 7ac22ca72d..869da044b3 100644 --- a/pkg/webui/console/containers/notifications/notifications.styl +++ b/pkg/webui/console/containers/notifications/notifications.styl @@ -16,6 +16,9 @@ display: flex flex-direction: column border-right: 1px solid rgba(0, 0, 0, 0.09) + min-width: 10rem + max-width: 30rem + width: 33% &-change-button display: flex @@ -144,7 +147,7 @@ left: -5px .notification-content - width: 100% + width: 66% +media-query($bp.s) display: none height: 88vh diff --git a/pkg/webui/console/views/app/app.styl b/pkg/webui/console/views/app/app.styl index 7bbb10c1ad..a0ace6e691 100644 --- a/pkg/webui/console/views/app/app.styl +++ b/pkg/webui/console/views/app/app.styl @@ -20,12 +20,14 @@ position: relative display: flex flex-direction: column + flex-grow: 1 .main overflow-y: auto .stage display: flex + flex-grow: 1 flex-direction: column .mobile-breadcrumbs diff --git a/pkg/webui/console/views/app/index.js b/pkg/webui/console/views/app/index.js index acca9db585..5724c3f935 100644 --- a/pkg/webui/console/views/app/index.js +++ b/pkg/webui/console/views/app/index.js @@ -132,9 +132,11 @@ const Layout = () => {