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) => ({ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy