Merge pull request #32949 from overleaf/kh-default-invitees-to-replies-only

[web] default invitees to replies only

GitOrigin-RevId: e3198403917e2679e49e27aaa87ae111675dc974
This commit is contained in:
Kristina
2026-05-07 10:22:40 +02:00
committed by Copybot
parent 498af9b07b
commit 40954ae2dc
2 changed files with 75 additions and 23 deletions

View File

@@ -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) => {

View File

@@ -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(
<EditorProviders>
<EditorProviders permissionsLevel={props.permissionsLevel as any}>
<SettingsModalProvider>
<ProjectNotificationsSetting />
</SettingsModalProvider>
@@ -241,6 +253,40 @@ describe('<ProjectNotificationsSetting />', 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 })