From 40954ae2dc91d3633863ceb7589e9d9858ccb769 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Thu, 7 May 2026 10:22:40 +0200 Subject: [PATCH] Merge pull request #32949 from overleaf/kh-default-invitees-to-replies-only [web] default invitees to replies only GitOrigin-RevId: e3198403917e2679e49e27aaa87ae111675dc974 --- .../use-project-notification-preferences.ts | 48 ++++++++++-------- .../project-notifications-setting.test.tsx | 50 ++++++++++++++++++- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts b/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts index e49f23aed7..a82eed216e 100644 --- a/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts +++ b/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts @@ -8,6 +8,7 @@ import type { } from '../../../../../modules/notifications/app/src/types.js' import { sendMB } from '@/infrastructure/event-tracking' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' +import { type PermissionsLevel } from '@/features/ide-react/types/permissions' export type SettableNotificationLevel = 'all' | 'replies' | 'off' export type NotificationLevel = SettableNotificationLevel | 'global-off' @@ -56,38 +57,43 @@ function levelToPreferences( } /** - * Map backend preferences to UI notification level + * Map backend preferences to UI notification level, considering the user's + * role so that only the relevant key variant is inspected. */ function preferencesToLevel( - preferences: GlobalNotificationPreferencesSchema + preferences: GlobalNotificationPreferencesSchema, + permissionsLevel: PermissionsLevel ): NotificationLevel { if (preferences.muteAllNotifications) { return 'global-off' } - // If all notifications are off - if ( - !preferences.commentOnOwnProject && - !preferences.commentOnInvitedProject && - !preferences.repliesOnOwnProject && - !preferences.repliesOnInvitedProject && - !preferences.repliesOnAuthoredThread && - !preferences.repliesOnParticipatingThread - ) { + const isOwner = permissionsLevel === 'owner' + + const projectComments = isOwner + ? preferences.commentOnOwnProject + : preferences.commentOnInvitedProject + const projectTrackedChanges = isOwner + ? preferences.trackedChangesOnOwnProject + : preferences.trackedChangesOnInvitedProject + const projectReplies = isOwner + ? preferences.repliesOnOwnProject + : preferences.repliesOnInvitedProject + + const anyProjectNotifications = + projectComments || projectTrackedChanges || projectReplies + const anyParticipantNotifications = + preferences.repliesOnAuthoredThread || + preferences.repliesOnParticipatingThread + + if (!anyProjectNotifications && !anyParticipantNotifications) { return 'off' } - // If only reply-related notifications are on - if ( - !preferences.commentOnOwnProject && - !preferences.commentOnInvitedProject && - (preferences.repliesOnAuthoredThread || - preferences.repliesOnParticipatingThread) - ) { + if (!anyProjectNotifications && anyParticipantNotifications) { return 'replies' } - // Default to 'all' for any other combination return 'all' } @@ -104,11 +110,11 @@ export function useProjectNotificationPreferences() { `/notifications/preferences/project/${projectId}` ) .then(prefs => { - setNotificationLevel(preferencesToLevel(prefs)) + setNotificationLevel(preferencesToLevel(prefs, permissionsLevel)) }) .catch(debugConsole.error) .finally(() => setIsLoading(false)) - }, [projectId]) + }, [projectId, permissionsLevel]) const setLevel = useCallback( (level: SettableNotificationLevel) => { diff --git a/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx b/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx index fbf6ea7131..ab5ab40a40 100644 --- a/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx +++ b/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx @@ -53,9 +53,21 @@ const allNotificationsOff = { repliesOnParticipatingThread: false, } -function renderComponent() { +const defaultPreferences = { + trackedChangesOnOwnProject: true, + trackedChangesOnInvitedProject: false, + commentOnOwnProject: true, + commentOnInvitedProject: false, + repliesOnOwnProject: false, + repliesOnInvitedProject: false, + repliesOnAuthoredThread: true, + repliesOnParticipatingThread: true, + muteAllNotifications: false, +} + +function renderComponent(props: { permissionsLevel?: string } = {}) { return render( - + @@ -241,6 +253,40 @@ describe('', function () { ).to.be.true }) + it('shows "all" for owner with default preferences', async function () { + fetchMock.get(preferencesUrl, defaultPreferences) + + renderComponent({ permissionsLevel: 'owner' }) + + await waitFor( + () => + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + }) + + it('shows "replies" for invitee with default preferences', async function () { + fetchMock.get(preferencesUrl, defaultPreferences) + + renderComponent({ permissionsLevel: 'readAndWrite' }) + + await waitFor( + () => + expect( + ( + screen.getByLabelText('Replies to your activity only', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + }) + it('POSTs "all" preferences when "All project activity" is selected', async function () { fetchMock.get(preferencesUrl, allNotificationsOff) const saveMock = fetchMock.post(preferencesUrl, { status: 200 })