diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index fd9b57511cc4..f1b9d16de654 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -26,6 +26,7 @@ type AttachmentCarouselPagerContextValue = { isScrollEnabled: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; + onSwipeDown: () => void; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 8704584c3e18..33d9f20b5e57 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -42,9 +42,15 @@ type AttachmentCarouselPagerProps = { * @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows. */ onRequestToggleArrows: (showArrows?: boolean) => void; + + /** A callback that is called when swipe-down-to-close gesture happens */ + onClose: () => void; }; -function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager( + {items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const pagerRef = useRef(null); @@ -114,9 +120,10 @@ function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelect isScrollEnabled, pagerRef, onTap: handleTap, + onSwipeDown: onClose, onScaleChanged: handleScaleChange, }), - [pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange], + [pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, onClose, handleScaleChange], ); const animatedProps = useAnimatedProps(() => ({ diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 228f0d597a32..a4d3e1392095 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -102,6 +102,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [setShouldShowArrows], ); + const goBack = useCallback(() => { + Navigation.goBack(); + }, []); + return ( {page == null ? ( @@ -133,6 +137,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, activeSource={activeSource} onRequestToggleArrows={toggleArrows} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} + onClose={goBack} ref={pagerRef} /> diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 07c535ccd96c..fa8a6d71516f 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -6,6 +6,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import getImageResolution from '@libs/fileDownload/getImageResolution'; import type {AvatarSource} from '@libs/UserUtils'; @@ -54,7 +55,7 @@ type AvatarWithImagePickerProps = { disabledStyle?: StyleProp; /** Executed once an image has been selected */ - onImageSelected?: () => void; + onImageSelected?: (file: File | CustomRNImageManipulatorResult) => void; /** Execute when the user taps "remove" */ onImageRemoved?: () => void; @@ -87,7 +88,7 @@ type AvatarWithImagePickerProps = { pendingAction?: OnyxCommon.PendingAction; /** The errors to display */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Title for avatar preview modal */ headerTitle?: string; diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 36cb175e3c45..69fa0d5e6e41 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -59,6 +59,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan activePage, onTap, onScaleChanged: onScaleChangedContext, + onSwipeDown, pagerRef, } = useMemo(() => { if (attachmentCarouselPagerContext === null) { @@ -70,6 +71,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan activePage: 0, onTap: () => {}, onScaleChanged: () => {}, + onSwipeDown: () => {}, pagerRef: undefined, }; } @@ -212,6 +214,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan shouldDisableTransformationGestures={isPagerScrolling} onTap={onTap} onScaleChanged={scaleChange} + onSwipeDown={onSwipeDown} > void; /** Triggered when the canvas is tapped (single tap) */ type OnTapCallback = () => void; +/** Triggered when the swipe down gesture on canvas occurs */ +type OnSwipeDownCallback = () => void; + /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { canvasSize: CanvasSize; @@ -32,6 +35,7 @@ type MultiGestureCanvasVariables = { minContentScale: number; maxContentScale: number; shouldDisableTransformationGestures: SharedValue; + isSwipingDownToClose: SharedValue; zoomScale: SharedValue; totalScale: SharedValue; pinchScale: SharedValue; @@ -45,6 +49,7 @@ type MultiGestureCanvasVariables = { reset: (animated: boolean, callback: () => void) => void; onTap: OnTapCallback | undefined; onScaleChanged: OnScaleChangedCallback | undefined; + onSwipeDown: OnSwipeDownCallback | undefined; }; -export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables}; +export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables, OnSwipeDownCallback}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index a3f9c7d62df0..97843e118871 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,7 +1,8 @@ /* eslint-disable no-param-reassign */ +import {Dimensions} from 'react-native'; import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -10,10 +11,24 @@ import * as MultiGestureCanvasUtils from './utils'; // We're using a "withDecay" animation to smoothly phase out the pan animation // https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; +const SCREEN_HEIGHT = Dimensions.get('screen').height; +const SNAP_POINT = SCREEN_HEIGHT / 4; +const SNAP_POINT_HIDDEN = SCREEN_HEIGHT / 1.2; type UsePanGestureProps = Pick< MultiGestureCanvasVariables, - 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'shouldDisableTransformationGestures' | 'stopAnimation' + | 'canvasSize' + | 'contentSize' + | 'zoomScale' + | 'totalScale' + | 'offsetX' + | 'offsetY' + | 'panTranslateX' + | 'panTranslateY' + | 'shouldDisableTransformationGestures' + | 'stopAnimation' + | 'onSwipeDown' + | 'isSwipingDownToClose' >; const usePanGesture = ({ @@ -27,16 +42,24 @@ const usePanGesture = ({ panTranslateY, shouldDisableTransformationGestures, stopAnimation, + isSwipingDownToClose, + onSwipeDown, }: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + // Used to track previous touch position for the "swipe down to close" gesture + const previousTouch = useSharedValue<{x: number; y: number} | null>(null); + // Velocity of the pan gesture // We need to keep track of the velocity to properly phase out/decay the pan animation const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); + // Disable "swipe down to close" gesture when content is bigger than the canvas + const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]); + // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect @@ -113,8 +136,22 @@ const usePanGesture = ({ }); } } else { - // Animated back to the boundary - offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG); + const finalTranslateY = offsetY.value + panVelocityY.value * 0.2; + + if (finalTranslateY > SNAP_POINT && zoomScale.value <= 1) { + offsetY.value = withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => { + isSwipingDownToClose.value = false; + }); + + if (onSwipeDown) { + runOnJS(onSwipeDown)(); + } + } else { + // Animated back to the boundary + offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG, () => { + isSwipingDownToClose.value = false; + }); + } } // Reset velocity variables after we finished the pan gesture @@ -125,14 +162,36 @@ const usePanGesture = ({ const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) - // eslint-disable-next-line @typescript-eslint/naming-convention - .onTouchesMove((_evt, state) => { + .onTouchesUp(() => { + previousTouch.value = null; + }) + .onTouchesMove((evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value <= 1 || shouldDisableTransformationGestures.value) { - return; + if (zoomScale.value > 1 && !shouldDisableTransformationGestures.value) { + state.activate(); } - state.activate(); + // TODO: this needs tuning to work properly + if (!shouldDisableTransformationGestures.value && zoomScale.value === 1 && previousTouch.value !== null) { + const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); + const velocityY = evt.allTouches[0].y - previousTouch.value.y; + + if (Math.abs(velocityY) > velocityX && velocityY > 20) { + state.activate(); + + isSwipingDownToClose.value = true; + previousTouch.value = null; + + return; + } + } + + if (previousTouch.value === null) { + previousTouch.value = { + x: evt.allTouches[0].x, + y: evt.allTouches[0].y, + }; + } }) .onStart(() => { stopAnimation(); @@ -147,15 +206,23 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; - panTranslateX.value += evt.changeX; - panTranslateY.value += evt.changeY; + if (!isSwipingDownToClose.value) { + panTranslateX.value += evt.changeX; + } + + if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { + panTranslateY.value += evt.changeY; + } }) .onEnd(() => { // Add pan translation to total offset and reset gesture variables offsetX.value += panTranslateX.value; offsetY.value += panTranslateY.value; + + // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; + previousTouch.value = null; // If we are swiping (in the pager), we don't want to return to boundaries if (shouldDisableTransformationGestures.value) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bd314757e89c..5c2a25e1d328 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -703,8 +703,8 @@ function getAllReportActions(reportID: string): ReportActions { function isReportActionAttachment(reportAction: OnyxEntry): boolean { const message = reportAction?.message?.[0]; - if (reportAction && 'isAttachment' in reportAction) { - return reportAction.isAttachment ?? false; + if (reportAction && ('isAttachment' in reportAction || 'attachmentInfo' in reportAction)) { + return reportAction?.isAttachment ?? !!reportAction?.attachmentInfo ?? false; } if (message) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2daf337ddb80..8813501e2b3f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1617,22 +1617,6 @@ function getPersonalDetailsForAccountID(accountID: number): Partial): boolean { reportAction?.actorAccountID === currentUserAccountID && isCommentOrIOU && canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions - !isReportMessageAttachment(reportAction?.message?.[0] ?? {type: '', text: ''}) && + !ReportActionsUtils.isReportActionAttachment(reportAction) && !ReportActionsUtils.isDeletedAction(reportAction) && !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index f2507a28d576..2280f6cdc0f5 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -779,6 +779,13 @@ function generateStatementPDF(period: string) { function setContactMethodAsDefault(newDefaultContactMethod: string) { const oldDefaultContactMethod = currentEmail; const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + primaryLogin: newDefaultContactMethod, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, @@ -825,6 +832,13 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, ]; const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + primaryLogin: oldDefaultContactMethod, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index df8a589f7bdc..fd03adcffd93 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import CONST from '@src/CONST'; import type {Message} from '@src/types/onyx/ReportAction'; @@ -17,5 +18,5 @@ export default function isReportMessageAttachment({text, html, translationKey}: } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx index d4438d3141bf..a9adb5310e58 100755 --- a/src/pages/DetailsPage.tsx +++ b/src/pages/DetailsPage.tsx @@ -64,22 +64,13 @@ function DetailsPage({personalDetails, route, session}: DetailsPageProps) { let details = Object.values(personalDetails ?? {}).find((personalDetail) => personalDetail?.login === login.toLowerCase()); if (!details) { - if (login === CONST.EMAIL.CONCIERGE) { - details = { - accountID: CONST.ACCOUNT_ID.CONCIERGE, - login, - displayName: 'Concierge', - avatar: UserUtils.getDefaultAvatar(CONST.ACCOUNT_ID.CONCIERGE), - }; - } else { - const optimisticAccountID = UserUtils.generateAccountID(login); - details = { - accountID: optimisticAccountID, - login, - displayName: login, - avatar: UserUtils.getDefaultAvatar(optimisticAccountID), - }; - } + const optimisticAccountID = UserUtils.generateAccountID(login); + details = { + accountID: optimisticAccountID, + login, + displayName: login, + avatar: UserUtils.getDefaultAvatar(optimisticAccountID), + }; } const isSMSLogin = details.login ? Str.isSMSLogin(details.login) : false; diff --git a/src/pages/NewChatSelectorPage.js b/src/pages/NewChatSelectorPage.js index f41f2fa5bd3e..0e8e192db884 100755 --- a/src/pages/NewChatSelectorPage.js +++ b/src/pages/NewChatSelectorPage.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -6,7 +7,6 @@ import TabSelector from '@components/TabSelector/TabSelector'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,6 +26,8 @@ const defaultProps = { }; function NewChatSelectorPage(props) { + const navigation = useNavigation(); + return ( Navigation.dismissModal()} + onBackButtonPress={navigation.goBack} /> ( + tabBar={({state, navigation: tabNavigation, position}) => ( )} diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 97ebc7dee2fb..0a6a2659ffb6 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -88,7 +88,12 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {privateNotes.map((item) => getMenuItem(item))} + + {privateNotes.map((item) => getMenuItem(item))} + ); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 4ce5822dfaa0..48dfe10a2a0e 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -26,7 +26,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CurrencyList} from '@src/types/onyx'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; @@ -102,11 +101,11 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi fallbackIcon={Expensicons.FallbackWorkspaceAvatar} style={[styles.mb3, isSmallScreenWidth ? styles.mtn17 : styles.mtn20, styles.alignItemsStart, styles.sectionMenuItemTopDescription]} isUsingDefaultAvatar={!policy?.avatar ?? null} - onImageSelected={(file: File) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file)} + onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} editorMaskImage={Expensicons.ImageCropSquareMask} - pendingAction={policy?.pendingFields?.avatar ?? null} - errors={policy?.errorFields?.avatar ?? null} + pendingAction={policy?.pendingFields?.avatar} + errors={policy?.errorFields?.avatar} onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')} previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')} headerTitle={translate('workspace.common.workspaceAvatar')} @@ -115,7 +114,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi disabledStyle={styles.cursorDefault} errorRowStyles={undefined} /> - + {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( - + )} - + ), isActive: policy?.harvesting?.enabled ?? false, - pendingAction: policy?.pendingFields?.isAutoApprovalEnabled as PendingAction, + pendingAction: policy?.pendingFields?.isAutoApprovalEnabled, }, { icon: Illustrations.Approval, @@ -83,7 +82,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { /> ), isActive: policy?.isAutoApprovalEnabled ?? false, - pendingAction: policy?.pendingFields?.approvalMode as PendingAction, + pendingAction: policy?.pendingFields?.approvalMode, }, { icon: Illustrations.WalletAlt, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 87974619ffc8..2844f63ab433 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -130,7 +130,7 @@ type Policy = { makeMeAdmin?: boolean; /** Pending fields for the policy */ - pendingFields?: Record; + pendingFields?: Record; /** Original file name which is used for the policy avatar */ originalFileName?: string;