Skip to content

Commit 1593861

Browse files
feat: add load more notifications on inbox (coder#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.
1 parent 82e3773 commit 1593861

File tree

4 files changed

+76
-11
lines changed

4 files changed

+76
-11
lines changed

site/src/api/api.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2434,9 +2434,13 @@ class ApiMethods {
24342434
return res.data;
24352435
};
24362436

2437-
getInboxNotifications = async () => {
2437+
getInboxNotifications = async (startingBeforeId?: string) => {
2438+
const params = new URLSearchParams();
2439+
if (startingBeforeId) {
2440+
params.append("starting_before", startingBeforeId);
2441+
}
24382442
const res = await this.axios.get<TypesGen.ListInboxNotificationsResponse>(
2439-
"/api/v2/notifications/inbox",
2443+
`/api/v2/notifications/inbox?${params.toString()}`,
24402444
);
24412445
return res.data;
24422446
};

site/src/components/ScrollArea/ScrollArea.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const ScrollArea = React.forwardRef<
1818
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
1919
{children}
2020
</ScrollAreaPrimitive.Viewport>
21-
<ScrollBar />
21+
<ScrollBar className="z-10" />
2222
<ScrollAreaPrimitive.Corner />
2323
</ScrollAreaPrimitive.Root>
2424
));

site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx

+29-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ type InboxPopoverProps = {
1919
notifications: readonly InboxNotification[] | undefined;
2020
unreadCount: number;
2121
error: unknown;
22+
isLoadingMoreNotifications: boolean;
23+
hasMoreNotifications: boolean;
2224
onRetry: () => void;
2325
onMarkAllAsRead: () => void;
2426
onMarkNotificationAsRead: (notificationId: string) => void;
27+
onLoadMoreNotifications: () => void;
2528
defaultOpen?: boolean;
2629
};
2730

@@ -30,9 +33,12 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
3033
unreadCount,
3134
notifications,
3235
error,
36+
isLoadingMoreNotifications,
37+
hasMoreNotifications,
3338
onRetry,
3439
onMarkAllAsRead,
3540
onMarkNotificationAsRead,
41+
onLoadMoreNotifications,
3642
}) => {
3743
const [isOpen, setIsOpen] = useState(defaultOpen);
3844

@@ -41,12 +47,21 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
4147
<PopoverTrigger asChild>
4248
<InboxButton unreadCount={unreadCount} />
4349
</PopoverTrigger>
44-
<PopoverContent className="w-[466px]" align="end">
50+
<PopoverContent
51+
className="w-[var(--radix-popper-available-width)] max-w-[466px]"
52+
align="end"
53+
>
4554
{/*
4655
* data-radix-scroll-area-viewport is used to set the max-height of the ScrollArea
4756
* https://github.com/shadcn-ui/ui/issues/542#issuecomment-2339361283
4857
*/}
49-
<ScrollArea className="[&>[data-radix-scroll-area-viewport]]:max-h-[calc(var(--radix-popover-content-available-height)-24px)]">
58+
<ScrollArea
59+
className={cn([
60+
"[--bottom-offset:48px]",
61+
"[--max-height:calc(var(--radix-popover-content-available-height)-var(--bottom-offset))]",
62+
"[&>[data-radix-scroll-area-viewport]]:max-h-[var(--max-height)]",
63+
])}
64+
>
5065
<div
5166
className={cn([
5267
"flex items-center justify-between p-3 border-0 border-b border-solid border-border",
@@ -94,6 +109,18 @@ export const InboxPopover: FC<InboxPopoverProps> = ({
94109
onMarkNotificationAsRead={onMarkNotificationAsRead}
95110
/>
96111
))}
112+
{hasMoreNotifications && (
113+
<Button
114+
variant="subtle"
115+
size="sm"
116+
disabled={isLoadingMoreNotifications}
117+
onClick={onLoadMoreNotifications}
118+
className="w-full"
119+
>
120+
<Spinner loading={isLoadingMoreNotifications} size="sm" />
121+
Load more
122+
</Button>
123+
)}
97124
</div>
98125
) : (
99126
<div className="p-6 flex items-center justify-center min-h-48">

site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx

+40-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { API, watchInboxNotifications } from "api/api";
1+
import { watchInboxNotifications } from "api/api";
22
import { getErrorDetail, getErrorMessage } from "api/errors";
33
import type {
44
ListInboxNotificationsResponse,
@@ -11,10 +11,13 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
1111
import { InboxPopover } from "./InboxPopover";
1212

1313
const NOTIFICATIONS_QUERY_KEY = ["notifications"];
14+
const NOTIFICATIONS_LIMIT = 25; // This is hard set in the API
1415

1516
type NotificationsInboxProps = {
1617
defaultOpen?: boolean;
17-
fetchNotifications: () => Promise<ListInboxNotificationsResponse>;
18+
fetchNotifications: (
19+
startingBeforeId?: string,
20+
) => Promise<ListInboxNotificationsResponse>;
1821
markAllAsRead: () => Promise<void>;
1922
markNotificationAsRead: (
2023
notificationId: string,
@@ -30,12 +33,12 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
3033
const queryClient = useQueryClient();
3134

3235
const {
33-
data: res,
36+
data: inboxRes,
3437
error,
3538
refetch,
3639
} = useQuery({
3740
queryKey: NOTIFICATIONS_QUERY_KEY,
38-
queryFn: fetchNotifications,
41+
queryFn: () => fetchNotifications(),
3942
});
4043

4144
const updateNotificationsCache = useEffectEvent(
@@ -75,6 +78,32 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
7578
};
7679
}, [updateNotificationsCache]);
7780

81+
const {
82+
mutate: loadMoreNotifications,
83+
isLoading: isLoadingMoreNotifications,
84+
} = useMutation({
85+
mutationFn: async () => {
86+
if (!inboxRes || inboxRes.notifications.length === 0) {
87+
return;
88+
}
89+
const lastNotification =
90+
inboxRes.notifications[inboxRes.notifications.length - 1];
91+
const newRes = await fetchNotifications(lastNotification.id);
92+
updateNotificationsCache((prev) => {
93+
return {
94+
unread_count: newRes.unread_count,
95+
notifications: [...prev.notifications, ...newRes.notifications],
96+
};
97+
});
98+
},
99+
onError: (error) => {
100+
displayError(
101+
getErrorMessage(error, "Error loading more notifications"),
102+
getErrorDetail(error),
103+
);
104+
},
105+
});
106+
78107
const markAllAsReadMutation = useMutation({
79108
mutationFn: markAllAsRead,
80109
onSuccess: () => {
@@ -122,12 +151,17 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
122151
return (
123152
<InboxPopover
124153
defaultOpen={defaultOpen}
125-
notifications={res?.notifications}
126-
unreadCount={res?.unread_count ?? 0}
154+
notifications={inboxRes?.notifications}
155+
unreadCount={inboxRes?.unread_count ?? 0}
127156
error={error}
157+
isLoadingMoreNotifications={isLoadingMoreNotifications}
158+
hasMoreNotifications={Boolean(
159+
inboxRes && inboxRes.notifications.length === NOTIFICATIONS_LIMIT,
160+
)}
128161
onRetry={refetch}
129162
onMarkAllAsRead={markAllAsReadMutation.mutate}
130163
onMarkNotificationAsRead={markNotificationAsReadMutation.mutate}
164+
onLoadMoreNotifications={loadMoreNotifications}
131165
/>
132166
);
133167
};

0 commit comments

Comments
 (0)