From c182295980b962916814c815ca81ef4cd224628d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Mar 2025 13:30:27 +0000 Subject: [PATCH 1/4] feat: add notifications widget in the navbar --- site/src/api/api.ts | 107 +++++++++++++----- .../modules/dashboard/Navbar/NavbarView.tsx | 14 +++ .../NotificationsInbox/InboxButton.tsx | 2 +- .../NotificationsInbox/InboxItem.stories.tsx | 9 +- .../NotificationsInbox/InboxItem.tsx | 8 +- .../NotificationsInbox/InboxPopover.tsx | 4 +- .../NotificationsInbox/NotificationsInbox.tsx | 51 ++++++--- .../notifications/NotificationsInbox/types.ts | 12 -- site/src/testHelpers/entities.ts | 20 ++-- 9 files changed, 155 insertions(+), 72 deletions(-) delete mode 100644 site/src/modules/notifications/NotificationsInbox/types.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 627ede80976c6..2f1df01468910 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -124,6 +124,33 @@ export const watchWorkspace = (workspaceId: string): EventSource => { ); }; +type WatchInboxNotificationsParams = { + read_status?: "read" | "unread" | "all"; +}; + +export const watchInboxNotifications = ( + onNewNotification: (res: TypesGen.GetInboxNotificationResponse) => void, + params?: WatchInboxNotificationsParams, +) => { + const searchParams = new URLSearchParams(params); + const socket = createWebSocket( + "/api/v2/notifications/inbox/watch", + searchParams, + ); + + socket.addEventListener("message", (event) => { + const res = JSON.parse(event.data) as TypesGen.GetInboxNotificationResponse; + onNewNotification(res); + }); + + socket.addEventListener("error", (event) => { + console.log("Watch inbox notifications error: ", event); + socket.close(); + }); + + return socket; +}; + export const getURLWithSearchParams = ( basePath: string, options?: SearchParamOptions, @@ -184,15 +211,11 @@ export const watchBuildLogsByTemplateVersionId = ( searchParams.append("after", after.toString()); } - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${ - location.host - }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, + const socket = createWebSocket( + `/api/v2/templateversions/${versionId}/logs`, + searchParams, ); - socket.binaryType = "blob"; - socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); @@ -214,21 +237,21 @@ export const watchWorkspaceAgentLogs = ( agentId: string, { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions, ) => { - // WebSocket compression in Safari (confirmed in 16.5) is broken when - // the server sends large messages. The following error is seen: - // - // WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error - // - const noCompression = - userAgentParser(navigator.userAgent).browser.name === "Safari" - ? "&no_compression" - : ""; + const searchParams = new URLSearchParams({ after: after.toString() }); - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`, + /** + * WebSocket compression in Safari (confirmed in 16.5) is broken when + * the server sends large messages. The following error is seen: + * WebSocket connection to 'wss://...' failed: The operation couldn’t be completed. + */ + if (userAgentParser(navigator.userAgent).browser.name === "Safari") { + searchParams.set("no_compression", ""); + } + + const socket = createWebSocket( + `/api/v2/workspaceagents/${agentId}/logs`, + searchParams, ); - socket.binaryType = "blob"; socket.addEventListener("message", (event) => { const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; @@ -267,13 +290,11 @@ export const watchBuildLogsByBuildId = ( if (after !== undefined) { searchParams.append("after", after.toString()); } - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${ - location.host - }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, + + const socket = createWebSocket( + `/api/v2/workspacebuilds/${buildId}/logs`, + searchParams, ); - socket.binaryType = "blob"; socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), @@ -2388,6 +2409,25 @@ class ApiMethods { ); return res.data; }; + + getInboxNotifications = async () => { + const res = await this.axios.get( + "/api/v2/notifications/inbox", + ); + return res.data; + }; + + updateInboxNotificationReadStatus = async ( + notificationId: string, + req: TypesGen.UpdateInboxNotificationReadStatusRequest, + ) => { + const res = + await this.axios.put( + `/api/v2/notifications/inbox/${notificationId}/read-status`, + req, + ); + return res.data; + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, @@ -2439,6 +2479,21 @@ function getConfiguredAxiosInstance(): AxiosInstance { return instance; } +/** + * Utility function to help create a WebSocket connection with Coder's API. + */ +function createWebSocket( + path: string, + params: URLSearchParams = new URLSearchParams(), +) { + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket( + `${protocol}//${location.host}${path}?${params.toString()}`, + ); + socket.binaryType = "blob"; + return socket; +} + // Other non-API methods defined here to make it a little easier to find them. interface ClientApi extends ApiMethods { getCsrfToken: () => string; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index d5ee661025f47..dd635a7c5c3c2 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -9,6 +9,8 @@ import { DeploymentDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; +import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; +import { API } from "api/api"; export interface NavbarViewProps { logo_url?: string; @@ -65,6 +67,18 @@ export const NavbarView: FC = ({ canViewHealth={canViewHealth} /> + => { + throw new Error("Function not implemented."); + }} + markNotificationAsRead={(notificationId) => + API.updateInboxNotificationReadStatus(notificationId, { + is_read: true, + }) + } + /> + {user && ( = { title: "modules/notifications/NotificationsInbox/InboxItem", @@ -22,7 +23,7 @@ export const Read: Story = { args: { notification: { ...MockNotification, - read_status: "read", + read_at: daysAgo(1), }, }, }; @@ -31,7 +32,7 @@ export const Unread: Story = { args: { notification: { ...MockNotification, - read_status: "unread", + read_at: null, }, }, }; @@ -40,7 +41,7 @@ export const UnreadFocus: Story = { args: { notification: { ...MockNotification, - read_status: "unread", + read_at: null, }, }, play: async ({ canvasElement }) => { @@ -54,7 +55,7 @@ export const OnMarkNotificationAsRead: Story = { args: { notification: { ...MockNotification, - read_status: "unread", + read_at: null, }, onMarkNotificationAsRead: fn(), }, diff --git a/site/src/modules/notifications/NotificationsInbox/InboxItem.tsx b/site/src/modules/notifications/NotificationsInbox/InboxItem.tsx index 2086a5f0a7fed..1279fa914fbbb 100644 --- a/site/src/modules/notifications/NotificationsInbox/InboxItem.tsx +++ b/site/src/modules/notifications/NotificationsInbox/InboxItem.tsx @@ -1,13 +1,13 @@ +import type { InboxNotification } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; import { SquareCheckBig } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink } from "react-router-dom"; import { relativeTime } from "utils/time"; -import type { Notification } from "./types"; type InboxItemProps = { - notification: Notification; + notification: InboxNotification; onMarkNotificationAsRead: (notificationId: string) => void; }; @@ -25,7 +25,7 @@ export const InboxItem: FC = ({ -
+
{notification.content} @@ -41,7 +41,7 @@ export const InboxItem: FC = ({
- {notification.read_status === "unread" && ( + {notification.read_at === null && ( <>
Unread diff --git a/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx b/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx index 2b94380ef7e7a..ae6ede30f0766 100644 --- a/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx +++ b/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx @@ -13,10 +13,10 @@ import { cn } from "utils/cn"; import { InboxButton } from "./InboxButton"; import { InboxItem } from "./InboxItem"; import { UnreadBadge } from "./UnreadBadge"; -import type { Notification } from "./types"; +import type { InboxNotification } from "api/typesGenerated"; type InboxPopoverProps = { - notifications: Notification[] | undefined; + notifications: readonly InboxNotification[] | undefined; unreadCount: number; error: unknown; onRetry: () => void; diff --git a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx index cbd573e155956..ede919ba9d550 100644 --- a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx +++ b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx @@ -1,22 +1,23 @@ import { getErrorDetail, getErrorMessage } from "api/errors"; import { displayError } from "components/GlobalSnackbar/utils"; -import type { FC } from "react"; +import { useEffect, useRef, type FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { InboxPopover } from "./InboxPopover"; -import type { Notification } from "./types"; +import type { + ListInboxNotificationsResponse, + UpdateInboxNotificationReadStatusResponse, +} from "api/typesGenerated"; +import { API, watchInboxNotifications } from "api/api"; const NOTIFICATIONS_QUERY_KEY = ["notifications"]; -type NotificationsResponse = { - notifications: Notification[]; - unread_count: number; -}; - type NotificationsInboxProps = { defaultOpen?: boolean; - fetchNotifications: () => Promise; + fetchNotifications: () => Promise; markAllAsRead: () => Promise; - markNotificationAsRead: (notificationId: string) => Promise; + markNotificationAsRead: ( + notificationId: string, + ) => Promise; }; export const NotificationsInbox: FC = ({ @@ -36,6 +37,24 @@ export const NotificationsInbox: FC = ({ queryFn: fetchNotifications, }); + useEffect(() => { + const socket = watchInboxNotifications( + (res) => { + safeUpdateNotificationsCache((prev) => { + return { + unread_count: res.unread_count, + notifications: [res.notification, ...prev.notifications], + }; + }); + }, + { read_status: "unread" }, + ); + + return () => { + socket.close(); + }; + }, []); + const markAllAsReadMutation = useMutation({ mutationFn: markAllAsRead, onSuccess: () => { @@ -59,15 +78,15 @@ export const NotificationsInbox: FC = ({ const markNotificationAsReadMutation = useMutation({ mutationFn: markNotificationAsRead, - onSuccess: (_, notificationId) => { + onSuccess: (res) => { safeUpdateNotificationsCache((prev) => { return { - unread_count: prev.unread_count - 1, + unread_count: res.unread_count, notifications: prev.notifications.map((n) => { - if (n.id !== notificationId) { + if (n.id !== res.notification.id) { return n; } - return { ...n, read_status: "read" }; + return res.notification; }), }; }); @@ -81,10 +100,12 @@ export const NotificationsInbox: FC = ({ }); async function safeUpdateNotificationsCache( - callback: (res: NotificationsResponse) => NotificationsResponse, + callback: ( + res: ListInboxNotificationsResponse, + ) => ListInboxNotificationsResponse, ) { await queryClient.cancelQueries(NOTIFICATIONS_QUERY_KEY); - queryClient.setQueryData( + queryClient.setQueryData( NOTIFICATIONS_QUERY_KEY, (prev) => { if (!prev) { diff --git a/site/src/modules/notifications/NotificationsInbox/types.ts b/site/src/modules/notifications/NotificationsInbox/types.ts deleted file mode 100644 index 168d81485791f..0000000000000 --- a/site/src/modules/notifications/NotificationsInbox/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: Remove this file when the types from API are available - -export type Notification = { - id: string; - read_status: "read" | "unread"; - content: string; - created_at: string; - actions: { - label: string; - url: string; - }[]; -}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ef18611caeb8a..bec72a7514f9e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -7,7 +7,6 @@ import type { FieldError } from "api/errors"; import type * as TypesGen from "api/typesGenerated"; import type { ProxyLatencyReport } from "contexts/useProxyLatency"; import range from "lodash/range"; -import type { Notification } from "modules/notifications/NotificationsInbox/types"; import type { Permissions } from "modules/permissions"; import type { OrganizationPermissions } from "modules/permissions/organizations"; import type { FileTree } from "utils/filetree"; @@ -4245,9 +4244,9 @@ export const MockNotificationTemplates: TypesGen.NotificationTemplate[] = [ export const MockNotificationMethodsResponse: TypesGen.NotificationMethodsResponse = { available: ["smtp", "webhook"], default: "smtp" }; -export const MockNotification: Notification = { +export const MockNotification: TypesGen.InboxNotification = { id: "1", - read_status: "unread", + read_at: null, content: "New user account testuser has been created. This new user account was created for Test User by Kira Pilot.", created_at: mockTwoDaysAgo(), @@ -4257,14 +4256,19 @@ export const MockNotification: Notification = { url: "https://dev.coder.com/templates/coder/coder", }, ], + user_id: MockUser.id, + template_id: MockTemplate.id, + targets: [], + title: "User account created", + icon: "user", }; -export const MockNotifications: Notification[] = [ +export const MockNotifications: TypesGen.InboxNotification[] = [ MockNotification, - { ...MockNotification, id: "2", read_status: "unread" }, - { ...MockNotification, id: "3", read_status: "read" }, - { ...MockNotification, id: "4", read_status: "read" }, - { ...MockNotification, id: "5", read_status: "read" }, + { ...MockNotification, id: "2", read_at: null }, + { ...MockNotification, id: "3", read_at: mockTwoDaysAgo() }, + { ...MockNotification, id: "4", read_at: mockTwoDaysAgo() }, + { ...MockNotification, id: "5", read_at: mockTwoDaysAgo() }, ]; function mockTwoDaysAgo() { From a696bd7a45d4107e7616e1cba4d99e7e79edcf48 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Mar 2025 13:40:45 +0000 Subject: [PATCH 2/4] FMT --- site/src/api/api.ts | 2 +- site/src/modules/dashboard/Navbar/NavbarView.tsx | 4 ++-- .../NotificationsInbox/InboxItem.stories.tsx | 2 +- .../notifications/NotificationsInbox/InboxPopover.tsx | 2 +- .../NotificationsInbox/NotificationsInbox.tsx | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2f1df01468910..781ff6ccb3c99 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -144,7 +144,7 @@ export const watchInboxNotifications = ( }); socket.addEventListener("error", (event) => { - console.log("Watch inbox notifications error: ", event); + console.warn("Watch inbox notifications error: ", event); socket.close(); }); diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index dd635a7c5c3c2..4761c9f544c6e 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -1,7 +1,9 @@ +import { API } from "api/api"; import type * as TypesGen from "api/typesGenerated"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; +import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; import type { FC } from "react"; import { NavLink, useLocation } from "react-router-dom"; import { cn } from "utils/cn"; @@ -9,8 +11,6 @@ import { DeploymentDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; -import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; -import { API } from "api/api"; export interface NavbarViewProps { logo_url?: string; diff --git a/site/src/modules/notifications/NotificationsInbox/InboxItem.stories.tsx b/site/src/modules/notifications/NotificationsInbox/InboxItem.stories.tsx index 840f6ac305631..6f2f00937a670 100644 --- a/site/src/modules/notifications/NotificationsInbox/InboxItem.stories.tsx +++ b/site/src/modules/notifications/NotificationsInbox/InboxItem.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, fn, userEvent, within } from "@storybook/test"; import { MockNotification } from "testHelpers/entities"; -import { InboxItem } from "./InboxItem"; import { daysAgo } from "utils/time"; +import { InboxItem } from "./InboxItem"; const meta: Meta = { title: "modules/notifications/NotificationsInbox/InboxItem", diff --git a/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx b/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx index ae6ede30f0766..b1808918891cc 100644 --- a/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx +++ b/site/src/modules/notifications/NotificationsInbox/InboxPopover.tsx @@ -1,3 +1,4 @@ +import type { InboxNotification } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { Popover, @@ -13,7 +14,6 @@ import { cn } from "utils/cn"; import { InboxButton } from "./InboxButton"; import { InboxItem } from "./InboxItem"; import { UnreadBadge } from "./UnreadBadge"; -import type { InboxNotification } from "api/typesGenerated"; type InboxPopoverProps = { notifications: readonly InboxNotification[] | undefined; diff --git a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx index ede919ba9d550..736c3fa7aaa1f 100644 --- a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx +++ b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx @@ -1,13 +1,13 @@ +import { API, watchInboxNotifications } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { useEffect, useRef, type FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { InboxPopover } from "./InboxPopover"; import type { ListInboxNotificationsResponse, UpdateInboxNotificationReadStatusResponse, } from "api/typesGenerated"; -import { API, watchInboxNotifications } from "api/api"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { type FC, useEffect, useRef } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { InboxPopover } from "./InboxPopover"; const NOTIFICATIONS_QUERY_KEY = ["notifications"]; From 281dadfaf1bacd47adc39e5eed361e2f7f6eb727 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Mar 2025 18:03:58 +0000 Subject: [PATCH 3/4] Apply review comments and fix storybook tests --- site/src/api/api.ts | 10 +++- .../modules/dashboard/Navbar/NavbarView.tsx | 2 +- .../NotificationsInbox.stories.tsx | 8 +++- .../NotificationsInbox/NotificationsInbox.tsx | 48 ++++++++++--------- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b9c96b8cded1b..f3be2612b61f8 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -139,8 +139,14 @@ export const watchInboxNotifications = ( ); socket.addEventListener("message", (event) => { - const res = JSON.parse(event.data) as TypesGen.GetInboxNotificationResponse; - onNewNotification(res); + try { + const res = JSON.parse( + event.data, + ) as TypesGen.GetInboxNotificationResponse; + onNewNotification(res); + } catch (error) { + console.warn("Error parsing inbox notification: ", error); + } }); socket.addEventListener("error", (event) => { diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 4761c9f544c6e..56ce03f342118 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -69,7 +69,7 @@ export const NavbarView: FC = ({ => { + markAllAsRead={() => { throw new Error("Function not implemented."); }} markNotificationAsRead={(notificationId) => diff --git a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.stories.tsx b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.stories.tsx index 18663d521d8da..edc7edaa6d400 100644 --- a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.stories.tsx +++ b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.stories.tsx @@ -134,7 +134,13 @@ export const MarkNotificationAsRead: Story = { notifications: MockNotifications, unread_count: 2, })), - markNotificationAsRead: fn(), + markNotificationAsRead: fn(async () => ({ + unread_count: 1, + notification: { + ...MockNotifications[1], + read_at: new Date().toISOString(), + }, + })), }, play: async ({ canvasElement }) => { const body = within(canvasElement.ownerDocument.body); diff --git a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx index 736c3fa7aaa1f..c8cf5ee6e6eb3 100644 --- a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx +++ b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx @@ -8,6 +8,7 @@ import { displayError } from "components/GlobalSnackbar/utils"; import { type FC, useEffect, useRef } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { InboxPopover } from "./InboxPopover"; +import { useEffectEvent } from "hooks/hookPolyfills"; const NOTIFICATIONS_QUERY_KEY = ["notifications"]; @@ -37,10 +38,29 @@ export const NotificationsInbox: FC = ({ queryFn: fetchNotifications, }); + const updateNotificationsCache = useEffectEvent( + async ( + callback: ( + res: ListInboxNotificationsResponse, + ) => ListInboxNotificationsResponse, + ) => { + await queryClient.cancelQueries(NOTIFICATIONS_QUERY_KEY); + queryClient.setQueryData( + NOTIFICATIONS_QUERY_KEY, + (prev) => { + if (!prev) { + return { notifications: [], unread_count: 0 }; + } + return callback(prev); + }, + ); + }, + ); + useEffect(() => { const socket = watchInboxNotifications( (res) => { - safeUpdateNotificationsCache((prev) => { + updateNotificationsCache((prev) => { return { unread_count: res.unread_count, notifications: [res.notification, ...prev.notifications], @@ -53,17 +73,18 @@ export const NotificationsInbox: FC = ({ return () => { socket.close(); }; - }, []); + }, [updateNotificationsCache]); const markAllAsReadMutation = useMutation({ mutationFn: markAllAsRead, onSuccess: () => { - safeUpdateNotificationsCache((prev) => { + updateNotificationsCache((prev) => { + console.log("PREV", prev); return { unread_count: 0, notifications: prev.notifications.map((n) => ({ ...n, - read_status: "read", + read_at: new Date().toISOString(), })), }; }); @@ -79,7 +100,7 @@ export const NotificationsInbox: FC = ({ const markNotificationAsReadMutation = useMutation({ mutationFn: markNotificationAsRead, onSuccess: (res) => { - safeUpdateNotificationsCache((prev) => { + updateNotificationsCache((prev) => { return { unread_count: res.unread_count, notifications: prev.notifications.map((n) => { @@ -99,23 +120,6 @@ export const NotificationsInbox: FC = ({ }, }); - async function safeUpdateNotificationsCache( - callback: ( - res: ListInboxNotificationsResponse, - ) => ListInboxNotificationsResponse, - ) { - await queryClient.cancelQueries(NOTIFICATIONS_QUERY_KEY); - queryClient.setQueryData( - NOTIFICATIONS_QUERY_KEY, - (prev) => { - if (!prev) { - return { notifications: [], unread_count: 0 }; - } - return callback(prev); - }, - ); - } - return ( Date: Tue, 18 Mar 2025 18:05:05 +0000 Subject: [PATCH 4/4] Fix lint --- .../notifications/NotificationsInbox/NotificationsInbox.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx index c8cf5ee6e6eb3..bf8d3622e35f1 100644 --- a/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx +++ b/site/src/modules/notifications/NotificationsInbox/NotificationsInbox.tsx @@ -5,10 +5,10 @@ import type { UpdateInboxNotificationReadStatusResponse, } from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; +import { useEffectEvent } from "hooks/hookPolyfills"; import { type FC, useEffect, useRef } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { InboxPopover } from "./InboxPopover"; -import { useEffectEvent } from "hooks/hookPolyfills"; const NOTIFICATIONS_QUERY_KEY = ["notifications"]; @@ -79,7 +79,6 @@ export const NotificationsInbox: FC = ({ mutationFn: markAllAsRead, onSuccess: () => { updateNotificationsCache((prev) => { - console.log("PREV", prev); return { unread_count: 0, notifications: prev.notifications.map((n) => ({