feat: add load more notifications on inbox (#17030)

Users need to see older notifications, so to make that happen, we added
a load more button at the end of the notifications list.

**Demo:**


https://github.com/user-attachments/assets/bd3d7964-a8f5-4164-8da0-9ba89ae88c9c

**What is missing?**
As you can notice, I didn't add tests for this feature. I tried, but I
didn't find a good solution for testing scroll events. However I was
able to get it working, but it was too cumbersome that I decided to
remove because of its maintenence burden.
This commit is contained in:
Bruno Quaresma
2025-03-21 13:38:17 -03:00
committed by GitHub
parent 82e37732ea
commit 1593861d7c
4 changed files with 76 additions and 11 deletions
+6 -2
View File
@@ -2434,9 +2434,13 @@ class ApiMethods {
return res.data;
};
getInboxNotifications = async () => {
getInboxNotifications = async (startingBeforeId?: string) => {
const params = new URLSearchParams();
if (startingBeforeId) {
params.append("starting_before", startingBeforeId);
}
const res = await this.axios.get<TypesGen.ListInboxNotificationsResponse>(
"/api/v2/notifications/inbox",
`/api/v2/notifications/inbox?${params.toString()}`,
);
return res.data;
};
@@ -18,7 +18,7 @@ export const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollBar className="z-10" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
@@ -19,9 +19,12 @@ type InboxPopoverProps = {
notifications: readonly InboxNotification[] | undefined;
unreadCount: number;
error: unknown;
isLoadingMoreNotifications: boolean;
hasMoreNotifications: boolean;
onRetry: () => void;
onMarkAllAsRead: () => void;
onMarkNotificationAsRead: (notificationId: string) => void;
onLoadMoreNotifications: () => void;
defaultOpen?: boolean;
};
@@ -30,9 +33,12 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
unreadCount,
notifications,
error,
isLoadingMoreNotifications,
hasMoreNotifications,
onRetry,
onMarkAllAsRead,
onMarkNotificationAsRead,
onLoadMoreNotifications,
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
@@ -41,12 +47,21 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
<PopoverTrigger asChild>
<InboxButton unreadCount={unreadCount} />
</PopoverTrigger>
<PopoverContent className="w-[466px]" align="end">
<PopoverContent
className="w-[var(--radix-popper-available-width)] max-w-[466px]"
align="end"
>
{/*
* data-radix-scroll-area-viewport is used to set the max-height of the ScrollArea
* https://github.com/shadcn-ui/ui/issues/542#issuecomment-2339361283
*/}
<ScrollArea className="[&>[data-radix-scroll-area-viewport]]:max-h-[calc(var(--radix-popover-content-available-height)-24px)]">
<ScrollArea
className={cn([
"[--bottom-offset:48px]",
"[--max-height:calc(var(--radix-popover-content-available-height)-var(--bottom-offset))]",
"[&>[data-radix-scroll-area-viewport]]:max-h-[var(--max-height)]",
])}
>
<div
className={cn([
"flex items-center justify-between p-3 border-0 border-b border-solid border-border",
@@ -94,6 +109,18 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
onMarkNotificationAsRead={onMarkNotificationAsRead}
/>
))}
{hasMoreNotifications && (
<Button
variant="subtle"
size="sm"
disabled={isLoadingMoreNotifications}
onClick={onLoadMoreNotifications}
className="w-full"
>
<Spinner loading={isLoadingMoreNotifications} size="sm" />
Load more
</Button>
)}
</div>
) : (
<div className="p-6 flex items-center justify-center min-h-48">
@@ -1,4 +1,4 @@
import { API, watchInboxNotifications } from "api/api";
import { watchInboxNotifications } from "api/api";
import { getErrorDetail, getErrorMessage } from "api/errors";
import type {
ListInboxNotificationsResponse,
@@ -11,10 +11,13 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { InboxPopover } from "./InboxPopover";
const NOTIFICATIONS_QUERY_KEY = ["notifications"];
const NOTIFICATIONS_LIMIT = 25; // This is hard set in the API
type NotificationsInboxProps = {
defaultOpen?: boolean;
fetchNotifications: () => Promise<ListInboxNotificationsResponse>;
fetchNotifications: (
startingBeforeId?: string,
) => Promise<ListInboxNotificationsResponse>;
markAllAsRead: () => Promise<void>;
markNotificationAsRead: (
notificationId: string,
@@ -30,12 +33,12 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
const queryClient = useQueryClient();
const {
data: res,
data: inboxRes,
error,
refetch,
} = useQuery({
queryKey: NOTIFICATIONS_QUERY_KEY,
queryFn: fetchNotifications,
queryFn: () => fetchNotifications(),
});
const updateNotificationsCache = useEffectEvent(
@@ -75,6 +78,32 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
};
}, [updateNotificationsCache]);
const {
mutate: loadMoreNotifications,
isLoading: isLoadingMoreNotifications,
} = useMutation({
mutationFn: async () => {
if (!inboxRes || inboxRes.notifications.length === 0) {
return;
}
const lastNotification =
inboxRes.notifications[inboxRes.notifications.length - 1];
const newRes = await fetchNotifications(lastNotification.id);
updateNotificationsCache((prev) => {
return {
unread_count: newRes.unread_count,
notifications: [...prev.notifications, ...newRes.notifications],
};
});
},
onError: (error) => {
displayError(
getErrorMessage(error, "Error loading more notifications"),
getErrorDetail(error),
);
},
});
const markAllAsReadMutation = useMutation({
mutationFn: markAllAsRead,
onSuccess: () => {
@@ -122,12 +151,17 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
return (
<InboxPopover
defaultOpen={defaultOpen}
notifications={res?.notifications}
unreadCount={res?.unread_count ?? 0}
notifications={inboxRes?.notifications}
unreadCount={inboxRes?.unread_count ?? 0}
error={error}
isLoadingMoreNotifications={isLoadingMoreNotifications}
hasMoreNotifications={Boolean(
inboxRes && inboxRes.notifications.length === NOTIFICATIONS_LIMIT,
)}
onRetry={refetch}
onMarkAllAsRead={markAllAsReadMutation.mutate}
onMarkNotificationAsRead={markNotificationAsReadMutation.mutate}
onLoadMoreNotifications={loadMoreNotifications}
/>
);
};