Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/pages/settings/Report/DynamicNotificationPreferencePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {DYNAMIC_ROUTES} from '@src/ROUTES';

import type {ValueOf} from 'type-fest';

import React, {useCallback} from 'react';
import React, {useCallback, useState} from 'react';

type DynamicNotificationPreferencePageProps = WithReportOrNotFoundProps;

Expand All @@ -32,6 +32,9 @@ function DynamicNotificationPreferencePage({report}: DynamicNotificationPreferen
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const isMoneyRequest = isMoneyRequestReport(report);
const currentNotificationPreference = getReportNotificationPreference(report);

const [draftNotificationPreference, setDraftNotificationPreference] = useState<ValueOf<typeof CONST.REPORT.NOTIFICATION_PREFERENCE> | undefined>(undefined);
const selectedNotificationPreference = draftNotificationPreference ?? currentNotificationPreference;
const shouldDisableNotificationPreferences =
isArchivedNonExpenseReport(report, isReportArchived) || isSelfDM(report) || (!isMoneyRequest && isHiddenForCurrentUser(currentNotificationPreference));
const notificationPreferenceOptions = Object.values(CONST.REPORT.NOTIFICATION_PREFERENCE)
Expand All @@ -40,21 +43,25 @@ function DynamicNotificationPreferencePage({report}: DynamicNotificationPreferen
value: preference,
text: translate(`notificationPreferencesPage.notificationPreferences.${preference}`),
keyForList: preference,
isSelected: preference === currentNotificationPreference,
isSelected: preference === selectedNotificationPreference,
}));
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.NOTIFICATION_PREFERENCES.path);

const goBack = useCallback(() => {
Navigation.goBack(backPath);
}, [backPath]);

const updateNotificationPreferenceForReportAction = useCallback(
(value: ValueOf<typeof CONST.REPORT.NOTIFICATION_PREFERENCE>) => {
updateNotificationPreference(report.reportID, currentNotificationPreference, value, currentUserAccountID, undefined, undefined);
goBack();
},
[report.reportID, currentNotificationPreference, currentUserAccountID, goBack],
);
const saveNotificationPreference = () => {
updateNotificationPreference(report.reportID, currentNotificationPreference, selectedNotificationPreference, currentUserAccountID, undefined, undefined);
goBack();
};

const confirmButtonOptions = {
showButton: true,
text: translate('common.save'),
onConfirm: saveNotificationPreference,
isDisabled: selectedNotificationPreference === currentNotificationPreference,
};

return (
<ScreenWrapper
Expand All @@ -69,9 +76,11 @@ function DynamicNotificationPreferencePage({report}: DynamicNotificationPreferen
<SelectionList
data={notificationPreferenceOptions}
ListItem={SingleSelectListItem}
onSelectRow={(option) => updateNotificationPreferenceForReportAction(option.value)}
onSelectRow={(option) => setDraftNotificationPreference(option.value)}
confirmButtonOptions={confirmButtonOptions}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add bottom safe-area padding to the new Save footers

On devices with a bottom gesture/home-indicator inset, this new fixed Save footer receives only its regular pb5 spacing because the surrounding ScreenWrapper explicitly disables bottom safe-area padding and the SelectionList does not set addBottomSafeAreaPadding. The same combination occurs in all three changed pages, so their Save buttons can intrude into the unsafe area; pass addBottomSafeAreaPadding to each list or let the wrapper provide the inset.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot it is a valid concern. Please resolve it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in 8fe8710. Each of the three pages sets includeSafeAreaPaddingBottom={false} on its ScreenWrapper, so the SelectionList's fixed Save footer (FixedFooter) was getting only its base pb5 spacing and no bottom inset. Passing addBottomSafeAreaPadding to each list makes FixedFooter add the device bottom inset (via useBottomSafeSafeAreaPaddingStyle → raw useSafeAreaPaddings), so the Save button no longer intrudes into the home-indicator/gesture area. Because the wrapper's bottom padding is disabled, there's no double-count, and since a footer is present the list's own container padding is skipped — the inset lands exactly on the footer. This matches the established pattern (e.g. CardTypeStep).

shouldSingleExecuteRowSelect
initiallyFocusedItemKey={notificationPreferenceOptions.find((locale) => locale.isSelected)?.keyForList}
initiallyFocusedItemKey={currentNotificationPreference}
addBottomSafeAreaPadding
/>
</FullPageNotFoundView>
</ScreenWrapper>
Expand Down
60 changes: 33 additions & 27 deletions src/pages/settings/Report/DynamicVisibilityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import CONST from '@src/CONST';
import {DYNAMIC_ROUTES} from '@src/ROUTES';
import type {RoomVisibility} from '@src/types/onyx/Report';

import React, {useMemo} from 'react';
import React, {useMemo, useState} from 'react';

type DynamicVisibilityProps = WithReportOrNotFoundProps;

Expand All @@ -35,6 +35,9 @@ function DynamicVisibilityPage({report}: DynamicVisibilityProps) {

const {showConfirmModal} = useConfirmModal();

const [draftVisibility, setDraftVisibility] = useState<RoomVisibility | undefined>(undefined);
const selectedVisibility = draftVisibility ?? report?.visibility;

const visibilityOptions = useMemo(
() =>
Object.values(CONST.REPORT.VISIBILITY)
Expand All @@ -44,36 +47,43 @@ function DynamicVisibilityPage({report}: DynamicVisibilityProps) {
value: visibilityOption,
alternateText: translate(`newRoomPage.${visibilityOption}Description`),
keyForList: visibilityOption,
isSelected: visibilityOption === report?.visibility,
isSelected: visibilityOption === selectedVisibility,
})),
[translate, report?.visibility],
[translate, selectedVisibility],
);

const goBack = () => {
Navigation.goBack(backPath);
};

const changeVisibility = (newVisibility: RoomVisibility) => {
if (!report) {
const saveVisibility = async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public confirmation modal triggers when nothing changed

If a room is already public and the user taps Save without selecting a different option, selectedVisibility falls back to report?.visibility which is PUBLIC, so the "Are you sure?" modal still pops up. Could we gate the modal on draftVisibility being set — or skip the confirmation when selectedVisibility === report?.visibility?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by the same change in d8abb61. Save is now disabled while selectedVisibility === report?.visibility (DynamicVisibilityPage.tsx:86), so the exact scenario you described — an already-public room where the user hasn't picked a different option — leaves the button disabled and the "Are you sure?" modal can't fire. The modal now only appears when Public is an actual change from the room's current visibility, which is when we do want the confirmation.

if (!report || !selectedVisibility) {
return;
}
updateRoomVisibility(report.reportID, report.visibility, newVisibility);

// Selecting Public is a sensitive change, so it still has to be confirmed before we persist it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Selecting Public is a sensitive change, so it still has to be confirmed before we persist it.
// Selecting Public is a sensitive change, so it still has to be confirmed before we persist it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0f72407 — added the blank line before the comment.

if (selectedVisibility === CONST.REPORT.VISIBILITY.PUBLIC) {
const result = await showConfirmModal({
title: translate('common.areYouSure'),
prompt: translate('newRoomPage.publicDescription'),
confirmText: translate('common.yes'),
cancelText: translate('common.no'),
shouldShowCancelButton: true,
danger: true,
});
if (result.action !== ModalActions.CONFIRM) {
return;
}
}
updateRoomVisibility(report.reportID, report.visibility, selectedVisibility);
setNavigationActionToMicrotaskQueue(goBack);
};

const showPublicVisibilityModal = async () => {
const result = await showConfirmModal({
title: translate('common.areYouSure'),
prompt: translate('newRoomPage.publicDescription'),
confirmText: translate('common.yes'),
cancelText: translate('common.no'),
shouldShowCancelButton: true,
danger: true,
});
if (result.action !== ModalActions.CONFIRM) {
return;
}
changeVisibility(CONST.REPORT.VISIBILITY.PUBLIC);
const confirmButtonOptions = {
showButton: true,
text: translate('common.save'),
onConfirm: saveVisibility,
isDisabled: selectedVisibility === report?.visibility,
};

return (
Expand All @@ -89,16 +99,12 @@ function DynamicVisibilityPage({report}: DynamicVisibilityProps) {
<SelectionList
shouldPreventDefaultFocusOnSelectRow
data={visibilityOptions}
onSelectRow={(option) => {
if (option.value === CONST.REPORT.VISIBILITY.PUBLIC) {
showPublicVisibilityModal();
return;
}
changeVisibility(option.value);
}}
onSelectRow={(option) => setDraftVisibility(option.value)}
confirmButtonOptions={confirmButtonOptions}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={visibilityOptions.find((visibility) => visibility.isSelected)?.keyForList}
initiallyFocusedItemKey={report?.visibility}
ListItem={SingleSelectListItem}
addBottomSafeAreaPadding
/>
</FullPageNotFoundView>
</ScreenWrapper>
Expand Down
36 changes: 22 additions & 14 deletions src/pages/settings/Report/DynamicWriteCapabilityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,43 @@ import type {WithReportOrNotFoundProps} from '@pages/inbox/report/withReportOrNo

import CONST from '@src/CONST';
import {DYNAMIC_ROUTES} from '@src/ROUTES';
import type {WriteCapability} from '@src/types/onyx/Report';

import type {ValueOf} from 'type-fest';

import React, {useCallback} from 'react';
import React, {useState} from 'react';

type DynamicWriteCapabilityPageProps = WithReportOrNotFoundProps;

function DynamicWriteCapabilityPage({report, policy}: DynamicWriteCapabilityPageProps) {
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY.path);
const {translate} = useLocalize();
const currentWriteCapability = report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL;

const [draftWriteCapability, setDraftWriteCapability] = useState<WriteCapability | undefined>(undefined);
const selectedWriteCapability = draftWriteCapability ?? currentWriteCapability;
const writeCapabilityOptions = Object.values(CONST.REPORT.WRITE_CAPABILITIES).map((value) => ({
value,
text: translate(`writeCapabilityPage.writeCapability.${value}`),
keyForList: value,
isSelected: value === (report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL),
isSelected: value === selectedWriteCapability,
}));
const selectedOptionKey = writeCapabilityOptions.find((locale) => locale.isSelected)?.keyForList;
const isReportArchived = useReportIsArchived(report.reportID);
const isAbleToEdit = canEditWriteCapability(report, policy, isReportArchived);

const goBack = () => {
Navigation.goBack(backPath);
};

const updateWriteCapability = useCallback(
(newValue: ValueOf<typeof CONST.REPORT.WRITE_CAPABILITIES>) => {
updateWriteCapabilityUtil(report, newValue);
goBack();
},
[report, goBack],
);
const saveWriteCapability = () => {
updateWriteCapabilityUtil(report, selectedWriteCapability);
goBack();
};

const confirmButtonOptions = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save enabled with no changes

All three pages let the user tap Save without selecting a different option, which fires the update API with old === new. Would it make sense to disable the button while draftWriteCapability is undefined (and same for the other two pages)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done in d8abb61. All three Save buttons are now disabled whenever the selected value equals the current live value, i.e. selected === current (which is exactly the case when the draft is still undefined and when the user re-picks the option that's already active), so the update API is never fired with old === new:

showButton: true,
text: translate('common.save'),
onConfirm: saveWriteCapability,
isDisabled: selectedWriteCapability === currentWriteCapability,
};

return (
<ScreenWrapper
Expand All @@ -63,9 +69,11 @@ function DynamicWriteCapabilityPage({report, policy}: DynamicWriteCapabilityPage
<SelectionList
data={writeCapabilityOptions}
ListItem={SingleSelectListItem}
onSelectRow={(option) => updateWriteCapability(option.value)}
onSelectRow={(option) => setDraftWriteCapability(option.value)}
confirmButtonOptions={confirmButtonOptions}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={selectedOptionKey}
initiallyFocusedItemKey={currentWriteCapability}
addBottomSafeAreaPadding
/>
</FullPageNotFoundView>
</ScreenWrapper>
Expand Down
Loading