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' } from '../../../../../modules/notifications/app/src/types.js'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' 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 SettableNotificationLevel = 'all' | 'replies' | 'off'
export type NotificationLevel = SettableNotificationLevel | 'global-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( function preferencesToLevel(
preferences: GlobalNotificationPreferencesSchema preferences: GlobalNotificationPreferencesSchema,
permissionsLevel: PermissionsLevel
): NotificationLevel { ): NotificationLevel {
if (preferences.muteAllNotifications) { if (preferences.muteAllNotifications) {
return 'global-off' return 'global-off'
} }
// If all notifications are off const isOwner = permissionsLevel === 'owner'
if (
!preferences.commentOnOwnProject && const projectComments = isOwner
!preferences.commentOnInvitedProject && ? preferences.commentOnOwnProject
!preferences.repliesOnOwnProject && : preferences.commentOnInvitedProject
!preferences.repliesOnInvitedProject && const projectTrackedChanges = isOwner
!preferences.repliesOnAuthoredThread && ? preferences.trackedChangesOnOwnProject
!preferences.repliesOnParticipatingThread : 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' return 'off'
} }
// If only reply-related notifications are on if (!anyProjectNotifications && anyParticipantNotifications) {
if (
!preferences.commentOnOwnProject &&
!preferences.commentOnInvitedProject &&
(preferences.repliesOnAuthoredThread ||
preferences.repliesOnParticipatingThread)
) {
return 'replies' return 'replies'
} }
// Default to 'all' for any other combination
return 'all' return 'all'
} }
@@ -104,11 +110,11 @@ export function useProjectNotificationPreferences() {
`/notifications/preferences/project/${projectId}` `/notifications/preferences/project/${projectId}`
) )
.then(prefs => { .then(prefs => {
setNotificationLevel(preferencesToLevel(prefs)) setNotificationLevel(preferencesToLevel(prefs, permissionsLevel))
}) })
.catch(debugConsole.error) .catch(debugConsole.error)
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
}, [projectId]) }, [projectId, permissionsLevel])
const setLevel = useCallback( const setLevel = useCallback(
(level: SettableNotificationLevel) => { (level: SettableNotificationLevel) => {

View File

@@ -53,9 +53,21 @@ const allNotificationsOff = {
repliesOnParticipatingThread: false, 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( return render(
<EditorProviders> <EditorProviders permissionsLevel={props.permissionsLevel as any}>
<SettingsModalProvider> <SettingsModalProvider>
<ProjectNotificationsSetting /> <ProjectNotificationsSetting />
</SettingsModalProvider> </SettingsModalProvider>
@@ -241,6 +253,40 @@ describe('<ProjectNotificationsSetting />', function () {
).to.be.true ).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 () { it('POSTs "all" preferences when "All project activity" is selected', async function () {
fetchMock.get(preferencesUrl, allNotificationsOff) fetchMock.get(preferencesUrl, allNotificationsOff)
const saveMock = fetchMock.post(preferencesUrl, { status: 200 }) const saveMock = fetchMock.post(preferencesUrl, { status: 200 })