diff --git a/coderd/coderd.go b/coderd/coderd.go index f5956d7457fe8..ccfa059294b49 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1391,6 +1391,7 @@ func New(options *Options) *API { r.Get("/", api.listInboxNotifications) r.Get("/watch", api.watchInboxNotifications) r.Put("/{id}/read-status", api.updateInboxNotificationReadStatus) + r.Put("/mark-all-read", api.updateAllInboxNotificationsReadStatus) }) r.Get("/settings", api.notificationsSettings) r.Put("/settings", api.putNotificationsSettings) diff --git a/coderd/database/queries/notificationsinbox.sql b/coderd/database/queries/notificationsinbox.sql index 43ab63ae83652..73bb2e8b7cb69 100644 --- a/coderd/database/queries/notificationsinbox.sql +++ b/coderd/database/queries/notificationsinbox.sql @@ -57,3 +57,13 @@ SET read_at = $1 WHERE id = $2; + +-- name: UpdateAllInboxNotificationsReadStatusByUserID :exec +-- Marks all unread notifications as read for a user +UPDATE + inbox_notifications +SET + read_at = $1 +WHERE + user_id = $2 AND + read_at IS NULL; diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 5437165bb71a6..bf94bcd7e8332 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -345,3 +345,47 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt UnreadCount: int(unreadCount), }) } + +// updateAllInboxNotificationsReadStatus marks all notifications as read for the user. +// @Summary Mark all notifications as read +// @ID mark-all-notifications-as-read +// @Security CoderSessionToken +// @Produce json +// @Tags Notifications +// @Success 200 {object} codersdk.Response +// @Router /notifications/inbox/mark-all-read [put] +func (api *API) updateAllInboxNotificationsReadStatus(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + apikey = httpmw.APIKey(r) + ) + + err := api.Database.UpdateAllInboxNotificationsReadStatusByUserID(ctx, database.UpdateAllInboxNotificationsReadStatusByUserIDParams{ + ReadAt: sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + }, + UserID: apikey.UserID, + }) + if err != nil { + api.Logger.Error(ctx, "failed to update all inbox notifications read status", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to update all inbox notifications read status.", + }) + return + } + + // Get the updated unread count + unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID) + if err != nil { + api.Logger.Error(ctx, "failed to call count unread inbox notifications", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to call count unread inbox notifications.", + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.MarkAllInboxNotificationsReadResponse{ + UnreadCount: int(unreadCount), + }) +} diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index 81e119381d281..38bf99538487d 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -723,3 +723,64 @@ func TestInboxNotifications_ReadStatus(t *testing.T) { require.Empty(t, updatedNotif.Notification) }) } + +func TestInboxNotifications_MarkAllRead(t *testing.T) { + t.Parallel() + + // I skip these tests specifically on windows as for now they are flaky - only on Windows. + // For now the idea is that the runner takes too long to insert the entries, could be worth + // investigating a manual Tx. + if runtime.GOOS == "windows" { + t.Skip("our runners are randomly taking too long to insert entries") + } + + t.Run("ok", func(t *testing.T) { + t.Parallel() + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{}) + firstUser := coderdtest.CreateFirstUser(t, client) + client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 0, notifs.UnreadCount) + require.Empty(t, notifs.Notifications) + + for i := range 20 { + dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{ + ID: uuid.New(), + UserID: member.ID, + TemplateID: notifications.TemplateWorkspaceOutOfMemory, + Title: fmt.Sprintf("Notification %d", i), + Actions: json.RawMessage("[]"), + Content: fmt.Sprintf("Content of the notif %d", i), + CreatedAt: dbtime.Now(), + }) + } + + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 20, notifs.UnreadCount) + require.Len(t, notifs.Notifications, 20) + + // Mark all as read + response, err := client.MarkAllInboxNotificationsRead(ctx) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, 0, response.UnreadCount) + + // Check that all notifications are marked as read + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.Equal(t, 0, notifs.UnreadCount) + + // All notifications should be marked as read + for _, notif := range notifs.Notifications { + require.NotNil(t, notif.ReadAt) + } + }) +} diff --git a/codersdk/inboxnotification.go b/codersdk/inboxnotification.go index 845140ea658c7..4d9749c2adb66 100644 --- a/codersdk/inboxnotification.go +++ b/codersdk/inboxnotification.go @@ -109,3 +109,26 @@ func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID var resp UpdateInboxNotificationReadStatusResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } + +type MarkAllInboxNotificationsReadResponse struct { + UnreadCount int `json:"unread_count"` +} + +func (c *Client) MarkAllInboxNotificationsRead(ctx context.Context) (MarkAllInboxNotificationsReadResponse, error) { + res, err := c.Request( + ctx, http.MethodPut, + "/api/v2/notifications/inbox/mark-all-read", + nil, + ) + if err != nil { + return MarkAllInboxNotificationsReadResponse{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return MarkAllInboxNotificationsReadResponse{}, ReadBodyAsError(res) + } + + var resp MarkAllInboxNotificationsReadResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +}