mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+6
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user