Skip to content

Commit

Permalink
Merge pull request Expensify#38755 from suneox/35380-virtual-viewport…
Browse files Browse the repository at this point in the history
…-on-report-screen

35380 virtual viewport on report screen
  • Loading branch information
iwiznia authored Mar 26, 2024
2 parents b0db06d + 1f8265c commit 4eb0705
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const useEmojiPickerMenu = () => {
const [preferredSkinTone] = usePreferredEmojiSkinTone();
const {windowHeight} = useWindowDimensions();
const StyleUtils = useStyleUtils();
const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight);
/**
* At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer
* to avoid the list style to be cut off due to the list height being larger than the container height
* so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker
*/
const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95);

useEffect(() => {
setFilteredEmojis(allEmojis);
Expand Down
7 changes: 7 additions & 0 deletions src/hooks/useViewportOffsetTop/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Native doesn't support DOM so default value is 0
*/

export default function useViewportOffsetTop(): number {
return 0;
}
47 changes: 47 additions & 0 deletions src/hooks/useViewportOffsetTop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {useEffect, useRef, useState} from 'react';
import addViewportResizeListener from '@libs/VisualViewport';

/**
* A hook that returns the offset of the top edge of the visual viewport
*/
export default function useViewportOffsetTop(shouldAdjustScrollView = false): number {
const [viewportOffsetTop, setViewportOffsetTop] = useState(0);
const initialHeight = useRef(window.visualViewport?.height ?? window.innerHeight).current;
const cachedDefaultOffsetTop = useRef<number>(0);
useEffect(() => {
const updateOffsetTop = (event?: Event) => {
let targetOffsetTop = window.visualViewport?.offsetTop ?? 0;
if (event?.target instanceof VisualViewport) {
targetOffsetTop = event.target.offsetTop;
}

if (shouldAdjustScrollView && window.visualViewport) {
const adjustScrollY = Math.round(initialHeight - window.visualViewport.height);
if (cachedDefaultOffsetTop.current === 0) {
cachedDefaultOffsetTop.current = targetOffsetTop;
}

if (adjustScrollY > targetOffsetTop) {
setViewportOffsetTop(adjustScrollY);
} else if (targetOffsetTop !== 0 && adjustScrollY === targetOffsetTop) {
setViewportOffsetTop(cachedDefaultOffsetTop.current);
} else {
setViewportOffsetTop(targetOffsetTop);
}
} else {
setViewportOffsetTop(targetOffsetTop);
}
};
updateOffsetTop();
return addViewportResizeListener(updateOffsetTop);
}, [initialHeight, shouldAdjustScrollView]);

useEffect(() => {
if (!shouldAdjustScrollView) {
return;
}
window.scrollTo({top: viewportOffsetTop});
}, [shouldAdjustScrollView, viewportOffsetTop]);

return viewportOffsetTop;
}
160 changes: 84 additions & 76 deletions src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import withCurrentReportID from '@components/withCurrentReportID';
import type {CurrentReportIDContextValue} from '@components/withCurrentReportID';
import withViewportOffsetTop from '@components/withViewportOffsetTop';
import type {ViewportOffsetTopProps} from '@components/withViewportOffsetTop';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Timing from '@libs/actions/Timing';
import * as Browser from '@libs/Browser';
import Navigation from '@libs/Navigation/Navigation';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector';
Expand All @@ -55,6 +55,9 @@ import {ActionListContext, ReactionListContext} from './ReportScreenContext';
import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext';

type ReportScreenOnyxProps = {
/** Get modal status */
modal: OnyxEntry<OnyxTypes.Modal>;

/** Tells us if the sidebar has rendered */
isSidebarLoaded: OnyxEntry<boolean>;

Expand Down Expand Up @@ -93,7 +96,7 @@ type OnyxHOCProps = {

type ReportScreenNavigationProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.REPORT>;

type ReportScreenProps = OnyxHOCProps & ViewportOffsetTopProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps;
type ReportScreenProps = OnyxHOCProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps;

/** Get the currently viewed report ID as number */
function getReportID(route: ReportScreenNavigationProps['route']): string {
Expand Down Expand Up @@ -131,7 +134,7 @@ function ReportScreen({
markReadyForHydration,
policies = {},
isSidebarLoaded = false,
viewportOffsetTop,
modal,
isComposerFullSize = false,
userLeavingStatus = false,
currentReportID = '',
Expand Down Expand Up @@ -259,6 +262,8 @@ function ReportScreen({
Timing.start(CONST.TIMING.CHAT_RENDER);
Performance.markStart(CONST.TIMING.CHAT_RENDER);
}
const [isComposerFocus, setIsComposerFocus] = useState(false);
const viewportOffsetTop = useViewportOffsetTop(Browser.isMobileSafari() && isComposerFocus && !modal?.willAlertModalBecomeVisible);

const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
Expand Down Expand Up @@ -644,6 +649,8 @@ function ReportScreen({

{isCurrentReportLoadedFromOnyx ? (
<ReportFooter
onComposerFocus={() => setIsComposerFocus(true)}
onComposerBlur={() => setIsComposerFocus(false)}
report={report}
pendingAction={reportPendingAction}
isComposerFullSize={!!isComposerFullSize}
Expand All @@ -663,81 +670,82 @@ function ReportScreen({

ReportScreen.displayName = 'ReportScreen';

export default withViewportOffsetTop(
withCurrentReportID(
withOnyx<ReportScreenProps, ReportScreenOnyxProps>(
{
isSidebarLoaded: {
key: ONYXKEYS.IS_SIDEBAR_LOADED,
},
sortedAllReportActions: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,
canEvict: false,
selector: (allReportActions: OnyxEntry<OnyxTypes.ReportActions>) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
},
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`,
allowStaleData: true,
selector: reportWithoutHasDraftSelector,
},
reportMetadata: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`,
initialValue: {
isLoadingInitialReportActions: true,
isLoadingOlderReportActions: false,
isLoadingNewerReportActions: false,
},
},
isComposerFullSize: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`,
initialValue: false,
},
betas: {
key: ONYXKEYS.BETAS,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
allowStaleData: true,
},
accountManagerReportID: {
key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID,
initialValue: null,
},
userLeavingStatus: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
initialValue: false,
export default withCurrentReportID(
withOnyx<ReportScreenProps, ReportScreenOnyxProps>(
{
modal: {
key: ONYXKEYS.MODAL,
},
isSidebarLoaded: {
key: ONYXKEYS.IS_SIDEBAR_LOADED,
},
sortedAllReportActions: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,
canEvict: false,
selector: (allReportActions: OnyxEntry<OnyxTypes.ReportActions>) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
},
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`,
allowStaleData: true,
selector: reportWithoutHasDraftSelector,
},
reportMetadata: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`,
initialValue: {
isLoadingInitialReportActions: true,
isLoadingOlderReportActions: false,
isLoadingNewerReportActions: false,
},
parentReportAction: {
key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`,
selector: (parentReportActions: OnyxEntry<OnyxTypes.ReportActions>, props: WithOnyxInstanceState<ReportScreenOnyxProps>): OnyxEntry<OnyxTypes.ReportAction> => {
const parentReportActionID = props?.report?.parentReportActionID;
if (!parentReportActionID) {
return null;
}
return parentReportActions?.[parentReportActionID] ?? null;
},
canEvict: false,
},
isComposerFullSize: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`,
initialValue: false,
},
betas: {
key: ONYXKEYS.BETAS,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
allowStaleData: true,
},
accountManagerReportID: {
key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID,
initialValue: null,
},
userLeavingStatus: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
initialValue: false,
},
parentReportAction: {
key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`,
selector: (parentReportActions: OnyxEntry<OnyxTypes.ReportActions>, props: WithOnyxInstanceState<ReportScreenOnyxProps>): OnyxEntry<OnyxTypes.ReportAction> => {
const parentReportActionID = props?.report?.parentReportActionID;
if (!parentReportActionID) {
return null;
}
return parentReportActions?.[parentReportActionID] ?? null;
},
canEvict: false,
},
true,
)(
memo(
ReportScreen,
(prevProps, nextProps) =>
prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) &&
lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
lodashIsEqual(prevProps.betas, nextProps.betas) &&
lodashIsEqual(prevProps.policies, nextProps.policies) &&
prevProps.accountManagerReportID === nextProps.accountManagerReportID &&
prevProps.userLeavingStatus === nextProps.userLeavingStatus &&
prevProps.currentReportID === nextProps.currentReportID &&
prevProps.viewportOffsetTop === nextProps.viewportOffsetTop &&
lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) &&
lodashIsEqual(prevProps.route, nextProps.route) &&
lodashIsEqual(prevProps.report, nextProps.report),
),
},
true,
)(
memo(
ReportScreen,
(prevProps, nextProps) =>
prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) &&
lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
prevProps.isComposerFullSize === nextProps.isComposerFullSize &&
lodashIsEqual(prevProps.betas, nextProps.betas) &&
lodashIsEqual(prevProps.policies, nextProps.policies) &&
prevProps.accountManagerReportID === nextProps.accountManagerReportID &&
prevProps.userLeavingStatus === nextProps.userLeavingStatus &&
prevProps.currentReportID === nextProps.currentReportID &&
lodashIsEqual(prevProps.modal, nextProps.modal) &&
lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) &&
lodashIsEqual(prevProps.route, nextProps.route) &&
lodashIsEqual(prevProps.report, nextProps.report),
),
),
);
35 changes: 24 additions & 11 deletions src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps &

/** Whether the report is ready for display */
isReportReadyForDisplay?: boolean;

/** A method to call when the input is focus */
onComposerFocus?: () => void;

/** A method to call when the input is blur */
onComposerBlur?: () => void;
};

// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
Expand All @@ -107,6 +113,8 @@ function ReportActionCompose({
isReportReadyForDisplay = true,
isEmptyChat,
lastReportAction,
onComposerFocus,
onComposerBlur,
}: ReportActionComposeProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand Down Expand Up @@ -293,20 +301,25 @@ function ReportActionCompose({
isKeyboardVisibleWhenShowingModalRef.current = true;
}, []);

const onBlur = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
const webEvent = event as unknown as FocusEvent;
setIsFocused(false);
if (suggestionsRef.current) {
suggestionsRef.current.resetSuggestions();
}
if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) {
isKeyboardVisibleWhenShowingModalRef.current = true;
}
}, []);
const onBlur = useCallback(
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
const webEvent = event as unknown as FocusEvent;
setIsFocused(false);
onComposerBlur?.();
if (suggestionsRef.current) {
suggestionsRef.current.resetSuggestions();
}
if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) {
isKeyboardVisibleWhenShowingModalRef.current = true;
}
},
[onComposerBlur],
);

const onFocus = useCallback(() => {
setIsFocused(true);
}, []);
onComposerFocus?.();
}, [onComposerFocus]);

// resets the composer to normal size when
// the send button is pressed.
Expand Down
10 changes: 10 additions & 0 deletions src/pages/home/report/ReportFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ type ReportFooterProps = ReportFooterOnyxProps & {

/** Whether the composer is in full size */
isComposerFullSize?: boolean;

/** A method to call when the input is focus */
onComposerFocus: () => void;

/** A method to call when the input is blur */
onComposerBlur: () => void;
};

function ReportFooter({
Expand All @@ -63,6 +69,8 @@ function ReportFooter({
isReportReadyForDisplay = true,
listHeight = 0,
isComposerFullSize = false,
onComposerBlur,
onComposerFocus,
}: ReportFooterProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
Expand Down Expand Up @@ -137,6 +145,8 @@ function ReportFooter({
<ReportActionCompose
// @ts-expect-error TODO: Remove this once ReportActionCompose (https://github.com/Expensify/App/issues/31984) is migrated to TypeScript.
onSubmit={onSubmitComment}
onComposerFocus={onComposerFocus}
onComposerBlur={onComposerBlur}
reportID={report.reportID}
report={report}
isEmptyChat={isEmptyChat}
Expand Down

0 comments on commit 4eb0705

Please sign in to comment.