From 8f150cc77c3475bad14b406db0582de2d6571437 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 11:29:45 +0200 Subject: [PATCH 01/12] Fix video player in search view --- .../VideoPlayerContexts/PlaybackContext.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index d926c52e0f549..43caa064cf1fd 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -1,4 +1,5 @@ -import type {NavigationState} from '@react-navigation/native'; +import type {NavigationState, Route} from '@react-navigation/native'; +import {findFocusedRoute} from '@react-navigation/native'; import type {AVPlaybackStatus, AVPlaybackStatusToSet} from 'expo-av'; import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {View} from 'react-native'; @@ -7,11 +8,22 @@ import usePrevious from '@hooks/usePrevious'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import Navigation from '@libs/Navigation/Navigation'; import Visibility from '@libs/Visibility'; +import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; +import SCREENS from '@src/SCREENS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {PlaybackContext, StatusCallback} from './types'; const Context = React.createContext(null); +type SearchRoute = Omit, 'key'> | undefined; +type MoneyRequestReportState = { + params: SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT]; +} & SearchRoute; + +function isMoneyRequestReportRouteWithReportIDInParams(route: SearchRoute): route is MoneyRequestReportState { + return !!route && !!route.params && route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT && 'reportID' in route.params; +} + function PlaybackContextProvider({children}: ChildrenProps) { const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null); const [currentlyPlayingURLReportID, setCurrentlyPlayingURLReportID] = useState(); @@ -58,11 +70,9 @@ function PlaybackContextProvider({children}: ChildrenProps) { */ const updateCurrentPlayingReportID = useCallback( (state: NavigationState) => { - if (!isReportTopmostSplitNavigator()) { - setCurrentReportID(undefined); - return; - } - const reportID = Navigation.getTopmostReportId(state); + const focusedRoute = findFocusedRoute(state); + const reportID = isMoneyRequestReportRouteWithReportIDInParams(focusedRoute) ? focusedRoute.params.reportID : Navigation.getTopmostReportId(state); + setCurrentReportID(reportID); }, [setCurrentReportID], From 94d72fbe7863aa5b7c683c0da10d30a3a77fb809 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 15:01:41 +0200 Subject: [PATCH 02/12] Fix video play in chat threads --- .../VideoPlayerContexts/PlaybackContext.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 43caa064cf1fd..26aee776a770e 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -5,8 +5,11 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} fr import type {View} from 'react-native'; import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types'; import usePrevious from '@hooks/usePrevious'; +import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import Navigation from '@libs/Navigation/Navigation'; +import {getAllReportActions, getReportActionHtml} from '@libs/ReportActionsUtils'; +import {getReportOrDraftReport, isChatThread} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; import SCREENS from '@src/SCREENS'; @@ -20,6 +23,14 @@ type MoneyRequestReportState = { params: SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT]; } & SearchRoute; +function isUrlInAnyReportMessagesAttachments(url: string | null, reportID?: string): boolean { + const reportActions = getAllReportActions(reportID); + return Object.values(reportActions).some((action) => { + const {sourceURL, previewSourceURL} = getAttachmentDetails(getReportActionHtml(action)); + return sourceURL === url || previewSourceURL === url; + }); +} + function isMoneyRequestReportRouteWithReportIDInParams(route: SearchRoute): route is MoneyRequestReportState { return !!route && !!route.params && route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT && 'reportID' in route.params; } @@ -71,7 +82,11 @@ function PlaybackContextProvider({children}: ChildrenProps) { const updateCurrentPlayingReportID = useCallback( (state: NavigationState) => { const focusedRoute = findFocusedRoute(state); - const reportID = isMoneyRequestReportRouteWithReportIDInParams(focusedRoute) ? focusedRoute.params.reportID : Navigation.getTopmostReportId(state); + let {reportID} = getReportOrDraftReport(Navigation.getTopmostReportId()) ?? {}; + + if (isMoneyRequestReportRouteWithReportIDInParams(focusedRoute)) { + reportID = focusedRoute.params.reportID; + } setCurrentReportID(reportID); }, @@ -83,7 +98,15 @@ function PlaybackContextProvider({children}: ChildrenProps) { if (currentlyPlayingURL && url !== currentlyPlayingURL) { pauseVideo(); } - setCurrentlyPlayingURLReportID(currentReportID); + + const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); + const {reportID, chatReportID, parentReportID} = topMostReport ?? {}; + + const isTopMostReportAChatThread = isChatThread(topMostReport); + const isAttachmentInTopMostReport = isUrlInAnyReportMessagesAttachments(url, reportID); + const chatThreadID = isAttachmentInTopMostReport ? reportID : chatReportID ?? parentReportID; + + setCurrentlyPlayingURLReportID(isTopMostReportAChatThread ? chatThreadID : currentReportID); setCurrentlyPlayingURL(url); }, [currentlyPlayingURL, currentReportID, pauseVideo], From 67e7722eb50c9c8600b70ef485fff92f1e18cb47 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 15:50:20 +0200 Subject: [PATCH 03/12] Add comments to PlaybackContext --- .../VideoPlayerContexts/PlaybackContext.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 26aee776a770e..02b3c9ea89ed2 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -84,6 +84,8 @@ function PlaybackContextProvider({children}: ChildrenProps) { const focusedRoute = findFocusedRoute(state); let {reportID} = getReportOrDraftReport(Navigation.getTopmostReportId()) ?? {}; + // We need to handle a case where a report is selected via search and is therefore not a topmost report, + // but we still want to be able to play videos in it if (isMoneyRequestReportRouteWithReportIDInParams(focusedRoute)) { reportID = focusedRoute.params.reportID; } @@ -103,8 +105,16 @@ function PlaybackContextProvider({children}: ChildrenProps) { const {reportID, chatReportID, parentReportID} = topMostReport ?? {}; const isTopMostReportAChatThread = isChatThread(topMostReport); - const isAttachmentInTopMostReport = isUrlInAnyReportMessagesAttachments(url, reportID); - const chatThreadID = isAttachmentInTopMostReport ? reportID : chatReportID ?? parentReportID; + + /* + This code solves the problem of 2 types of videos in a chat thread: + - video that is in a first message of thread, which makes it have a reportID equal to topmost report parent ID + - any other video in chat thread that has a reportID equal to topmost report ID + we need to find out which reportID is assigned to the video + and do that by checking if selected url is in topmost report messages attachments (then we use said report) + or if it is a first message, then we take parent report ID since it is associated with parent */ + const isUrlInTopMostReportAttachments = isUrlInAnyReportMessagesAttachments(url, reportID); + const chatThreadID = isUrlInTopMostReportAttachments ? reportID : chatReportID ?? parentReportID; setCurrentlyPlayingURLReportID(isTopMostReportAChatThread ? chatThreadID : currentReportID); setCurrentlyPlayingURL(url); From 633938f336b85ce9aa414e78619977212e760c8d Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 17:45:12 +0200 Subject: [PATCH 04/12] Play video only on the original page --- src/components/VideoPlayerPreview/index.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 3831bb117fa36..8e6de09889d73 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -1,7 +1,8 @@ +import {useNavigation} from '@react-navigation/native'; import type {VideoReadyForDisplayEvent} from 'expo-av'; -import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; +import React, {useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; +import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import VideoPlayer from '@components/VideoPlayer'; import IconButton from '@components/VideoPlayer/IconButton'; @@ -10,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; +import Navigation from '@libs/Navigation/Navigation'; import VideoPlayerThumbnail from './VideoPlayerThumbnail'; type VideoDimensions = { @@ -51,6 +53,9 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi const [isThumbnail, setIsThumbnail] = useState(true); const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); + const renderRoute = useRef(null); + const navigation = useNavigation(); + const isFocused = () => Navigation.getActiveRouteWithoutParams() === renderRoute.current; // `onVideoLoaded` is passed to VideoPlayerPreview's `Video` element which is displayed only on web. // VideoReadyForDisplayEvent type is lacking srcElement, that's why it's added here @@ -65,8 +70,17 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi } }; + // We want to play the video only when the user is on the page where it was rendered + useEffect(() => { + renderRoute.current = Navigation.getActiveRouteWithoutParams(); + }, []); + + useEffect(() => { + return navigation.addListener('blur', () => !isFocused() && setIsThumbnail(true)); + }, [navigation]); + useEffect(() => { - if (videoUrl !== currentlyPlayingURL || reportID !== currentlyPlayingURLReportID) { + if (videoUrl !== currentlyPlayingURL || reportID !== currentlyPlayingURLReportID || !isFocused()) { return; } setIsThumbnail(false); From 52488f08176b74509ccdee0b85f28fe33b81489c Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 18:17:25 +0200 Subject: [PATCH 05/12] Add exception for attachments modal --- src/components/VideoPlayerPreview/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 8e6de09889d73..09ebc2051362f 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -12,6 +12,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; import VideoPlayerThumbnail from './VideoPlayerThumbnail'; type VideoDimensions = { @@ -55,7 +56,10 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); const renderRoute = useRef(null); const navigation = useNavigation(); - const isFocused = () => Navigation.getActiveRouteWithoutParams() === renderRoute.current; + const isFocused = () => { + const currentRoute = Navigation.getActiveRouteWithoutParams(); + return currentRoute === `/${ROUTES.ATTACHMENTS.route}` || currentRoute === renderRoute.current; + }; // `onVideoLoaded` is passed to VideoPlayerPreview's `Video` element which is displayed only on web. // VideoReadyForDisplayEvent type is lacking srcElement, that's why it's added here From 2b7f1b028b173a9c2d43d70ee0441afbdc3673c6 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 1 Apr 2025 19:17:12 +0200 Subject: [PATCH 06/12] Add fallback for Attachment route --- src/components/VideoPlayerContexts/PlaybackContext.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 02b3c9ea89ed2..8e5e029854d4c 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -101,6 +101,9 @@ function PlaybackContextProvider({children}: ChildrenProps) { pauseVideo(); } + // Used for /attachment route + const reportIDFromParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; + const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); const {reportID, chatReportID, parentReportID} = topMostReport ?? {}; @@ -116,7 +119,7 @@ function PlaybackContextProvider({children}: ChildrenProps) { const isUrlInTopMostReportAttachments = isUrlInAnyReportMessagesAttachments(url, reportID); const chatThreadID = isUrlInTopMostReportAttachments ? reportID : chatReportID ?? parentReportID; - setCurrentlyPlayingURLReportID(isTopMostReportAChatThread ? chatThreadID : currentReportID); + setCurrentlyPlayingURLReportID(isTopMostReportAChatThread ? chatThreadID : currentReportID ?? reportIDFromParams); setCurrentlyPlayingURL(url); }, [currentlyPlayingURL, currentReportID, pauseVideo], From 893a0a1176224f292bb349eabcd8bb7ad11f6e54 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Wed, 2 Apr 2025 15:57:09 +0200 Subject: [PATCH 07/12] Fix first video load in carousel on native apps --- .../VideoPlayerContexts/PlaybackContext.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 8e5e029854d4c..710325b529aab 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -12,6 +12,7 @@ import {getAllReportActions, getReportActionHtml} from '@libs/ReportActionsUtils import {getReportOrDraftReport, isChatThread} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {PlaybackContext, StatusCallback} from './types'; @@ -90,9 +91,11 @@ function PlaybackContextProvider({children}: ChildrenProps) { reportID = focusedRoute.params.reportID; } + reportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportID; + setCurrentReportID(reportID); }, - [setCurrentReportID], + [setCurrentReportID, prevCurrentReportID], ); const updateCurrentlyPlayingURL = useCallback( @@ -103,6 +106,7 @@ function PlaybackContextProvider({children}: ChildrenProps) { // Used for /attachment route const reportIDFromParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; + const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportIDFromParams; const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); const {reportID, chatReportID, parentReportID} = topMostReport ?? {}; @@ -119,10 +123,12 @@ function PlaybackContextProvider({children}: ChildrenProps) { const isUrlInTopMostReportAttachments = isUrlInAnyReportMessagesAttachments(url, reportID); const chatThreadID = isUrlInTopMostReportAttachments ? reportID : chatReportID ?? parentReportID; - setCurrentlyPlayingURLReportID(isTopMostReportAChatThread ? chatThreadID : currentReportID ?? reportIDFromParams); + const currentPlayReportID = isTopMostReportAChatThread ? chatThreadID : currentReportID ?? attachmentReportID; + + setCurrentlyPlayingURLReportID(currentPlayReportID); setCurrentlyPlayingURL(url); }, - [currentlyPlayingURL, currentReportID, pauseVideo], + [currentlyPlayingURL, currentReportID, prevCurrentReportID, pauseVideo], ); const shareVideoPlayerElements = useCallback( From dc260095876b4e4b2b7401ec14897b1621325bc8 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 3 Apr 2025 10:17:34 +0200 Subject: [PATCH 08/12] Fix for nested chat threads --- .../VideoPlayerContexts/PlaybackContext.tsx | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 710325b529aab..3b5c1c7e5d926 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -24,18 +24,39 @@ type MoneyRequestReportState = { params: SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT]; } & SearchRoute; -function isUrlInAnyReportMessagesAttachments(url: string | null, reportID?: string): boolean { - const reportActions = getAllReportActions(reportID); - return Object.values(reportActions).some((action) => { - const {sourceURL, previewSourceURL} = getAttachmentDetails(getReportActionHtml(action)); - return sourceURL === url || previewSourceURL === url; - }); -} - function isMoneyRequestReportRouteWithReportIDInParams(route: SearchRoute): route is MoneyRequestReportState { return !!route && !!route.params && route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT && 'reportID' in route.params; } +function findUrlInReportOrAncestorAttachments(url: string | null, reportID: string | undefined): string | undefined { + const reportFromParams = getReportOrDraftReport(reportID); + if (!isChatThread(reportFromParams)) { + return undefined; + } + + function searchReportAndAncestors(currentID: string | undefined): string | undefined { + const {parentReportID} = getReportOrDraftReport(currentID) ?? {}; + + const reportActions = getAllReportActions(currentID); + const hasUrlInAttachments = Object.values(reportActions).some((action) => { + const {sourceURL, previewSourceURL} = getAttachmentDetails(getReportActionHtml(action)); + return sourceURL === url || previewSourceURL === url; + }); + + if (hasUrlInAttachments) { + return currentID; + } + + if (parentReportID) { + return searchReportAndAncestors(parentReportID); + } + + return undefined; + } + + return searchReportAndAncestors(reportID); +} + function PlaybackContextProvider({children}: ChildrenProps) { const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null); const [currentlyPlayingURLReportID, setCurrentlyPlayingURLReportID] = useState(); @@ -105,25 +126,14 @@ function PlaybackContextProvider({children}: ChildrenProps) { } // Used for /attachment route - const reportIDFromParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; - const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportIDFromParams; - - const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); - const {reportID, chatReportID, parentReportID} = topMostReport ?? {}; - - const isTopMostReportAChatThread = isChatThread(topMostReport); - - /* - This code solves the problem of 2 types of videos in a chat thread: - - video that is in a first message of thread, which makes it have a reportID equal to topmost report parent ID - - any other video in chat thread that has a reportID equal to topmost report ID - we need to find out which reportID is assigned to the video - and do that by checking if selected url is in topmost report messages attachments (then we use said report) - or if it is a first message, then we take parent report ID since it is associated with parent */ - const isUrlInTopMostReportAttachments = isUrlInAnyReportMessagesAttachments(url, reportID); - const chatThreadID = isUrlInTopMostReportAttachments ? reportID : chatReportID ?? parentReportID; - - const currentPlayReportID = isTopMostReportAChatThread ? chatThreadID : currentReportID ?? attachmentReportID; + const reportIDFromUrlParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; + const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportIDFromUrlParams; + const reportIDWithUrl = findUrlInReportOrAncestorAttachments(url, Navigation.getTopmostReportId()); + + // - if it is a chat thread, use chat thread ID or any ascentor ID since the video could have originally been sent on report many levels up + // - report ID in which we are currently, if it is not a chat thread + // - if it is an attachment route, then we take report ID from the URL params + const currentPlayReportID = [reportIDWithUrl, currentReportID, attachmentReportID].find((id) => id !== undefined); setCurrentlyPlayingURLReportID(currentPlayReportID); setCurrentlyPlayingURL(url); From 4b890805b27a6c7952465ed287bb89bdbd9312de Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 4 Apr 2025 16:50:23 +0200 Subject: [PATCH 09/12] Fix code according to PR comments --- .../VideoPlayerContexts/PlaybackContext.tsx | 41 ++++++++----------- src/components/VideoPlayerPreview/index.tsx | 25 ++++------- src/hooks/useFirstRenderRoute.ts | 23 +++++++++++ 3 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 src/hooks/useFirstRenderRoute.ts diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 3b5c1c7e5d926..c20b04b243101 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -3,6 +3,7 @@ import {findFocusedRoute} from '@react-navigation/native'; import type {AVPlaybackStatus, AVPlaybackStatusToSet} from 'expo-av'; import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types'; import usePrevious from '@hooks/usePrevious'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; @@ -14,6 +15,7 @@ import Visibility from '@libs/Visibility'; import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type {Report} from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {PlaybackContext, StatusCallback} from './types'; @@ -28,33 +30,25 @@ function isMoneyRequestReportRouteWithReportIDInParams(route: SearchRoute): rout return !!route && !!route.params && route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT && 'reportID' in route.params; } -function findUrlInReportOrAncestorAttachments(url: string | null, reportID: string | undefined): string | undefined { - const reportFromParams = getReportOrDraftReport(reportID); - if (!isChatThread(reportFromParams)) { - return undefined; - } - - function searchReportAndAncestors(currentID: string | undefined): string | undefined { - const {parentReportID} = getReportOrDraftReport(currentID) ?? {}; +function findUrlInReportOrAncestorAttachments(currentReport: OnyxEntry, url: string | null): string | undefined { + const {parentReportID, reportID} = currentReport ?? {}; - const reportActions = getAllReportActions(currentID); - const hasUrlInAttachments = Object.values(reportActions).some((action) => { - const {sourceURL, previewSourceURL} = getAttachmentDetails(getReportActionHtml(action)); - return sourceURL === url || previewSourceURL === url; - }); + const reportActions = getAllReportActions(reportID); + const hasUrlInAttachments = Object.values(reportActions).some((action) => { + const {sourceURL, previewSourceURL} = getAttachmentDetails(getReportActionHtml(action)); + return sourceURL === url || previewSourceURL === url; + }); - if (hasUrlInAttachments) { - return currentID; - } - - if (parentReportID) { - return searchReportAndAncestors(parentReportID); - } + if (hasUrlInAttachments) { + return reportID; + } - return undefined; + if (parentReportID) { + const parentReport = getReportOrDraftReport(parentReportID); + return findUrlInReportOrAncestorAttachments(parentReport, url); } - return searchReportAndAncestors(reportID); + return undefined; } function PlaybackContextProvider({children}: ChildrenProps) { @@ -126,9 +120,10 @@ function PlaybackContextProvider({children}: ChildrenProps) { } // Used for /attachment route + const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); const reportIDFromUrlParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportIDFromUrlParams; - const reportIDWithUrl = findUrlInReportOrAncestorAttachments(url, Navigation.getTopmostReportId()); + const reportIDWithUrl = isChatThread(topMostReport) ? findUrlInReportOrAncestorAttachments(topMostReport, url) : undefined; // - if it is a chat thread, use chat thread ID or any ascentor ID since the video could have originally been sent on report many levels up // - report ID in which we are currently, if it is not a chat thread diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 09ebc2051362f..cdf28c208f297 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -1,17 +1,17 @@ import {useNavigation} from '@react-navigation/native'; import type {VideoReadyForDisplayEvent} from 'expo-av'; -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import VideoPlayer from '@components/VideoPlayer'; import IconButton from '@components/VideoPlayer/IconButton'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import useFirstRenderRoute from '@hooks/useFirstRenderRoute'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; -import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import VideoPlayerThumbnail from './VideoPlayerThumbnail'; @@ -54,12 +54,10 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi const [isThumbnail, setIsThumbnail] = useState(true); const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); - const renderRoute = useRef(null); const navigation = useNavigation(); - const isFocused = () => { - const currentRoute = Navigation.getActiveRouteWithoutParams(); - return currentRoute === `/${ROUTES.ATTACHMENTS.route}` || currentRoute === renderRoute.current; - }; + + // We want to play the video only when the user is on the page where it was rendered + const firstRenderRoute = useFirstRenderRoute([`/${ROUTES.ATTACHMENTS.route}`]); // `onVideoLoaded` is passed to VideoPlayerPreview's `Video` element which is displayed only on web. // VideoReadyForDisplayEvent type is lacking srcElement, that's why it's added here @@ -74,21 +72,16 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi } }; - // We want to play the video only when the user is on the page where it was rendered - useEffect(() => { - renderRoute.current = Navigation.getActiveRouteWithoutParams(); - }, []); - useEffect(() => { - return navigation.addListener('blur', () => !isFocused() && setIsThumbnail(true)); - }, [navigation]); + return navigation.addListener('blur', () => !firstRenderRoute.isFocused && setIsThumbnail(true)); + }, [navigation, firstRenderRoute]); useEffect(() => { - if (videoUrl !== currentlyPlayingURL || reportID !== currentlyPlayingURLReportID || !isFocused()) { + if (videoUrl !== currentlyPlayingURL || reportID !== currentlyPlayingURLReportID || !firstRenderRoute.isFocused) { return; } setIsThumbnail(false); - }, [currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL, videoUrl, reportID]); + }, [currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL, videoUrl, reportID, firstRenderRoute]); return ( diff --git a/src/hooks/useFirstRenderRoute.ts b/src/hooks/useFirstRenderRoute.ts new file mode 100644 index 0000000000000..6a30dd5b01705 --- /dev/null +++ b/src/hooks/useFirstRenderRoute.ts @@ -0,0 +1,23 @@ +import {useEffect, useRef} from 'react'; +import Navigation from '@navigation/Navigation'; + +function useFirstRenderRoute(focusExceptionRoutes?: string[]) { + const initialRoute = useRef(null); + + useEffect(() => { + initialRoute.current = Navigation.getActiveRouteWithoutParams(); + }, []); + + return { + get isFocused() { + const activeRoute = Navigation.getActiveRouteWithoutParams(); + const allRoutesToConsider = [initialRoute.current, ...(focusExceptionRoutes ?? [])]; + return allRoutesToConsider.includes(activeRoute); + }, + get firstRenderRoute() { + return initialRoute.current; + }, + }; +} + +export default useFirstRenderRoute; From f5bf193a6793e729080f2a610683dc768390f179 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 4 Apr 2025 16:51:12 +0200 Subject: [PATCH 10/12] Run prettier --- src/hooks/useFirstRenderRoute.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/hooks/useFirstRenderRoute.ts b/src/hooks/useFirstRenderRoute.ts index 6a30dd5b01705..57380f3622eb9 100644 --- a/src/hooks/useFirstRenderRoute.ts +++ b/src/hooks/useFirstRenderRoute.ts @@ -2,22 +2,22 @@ import {useEffect, useRef} from 'react'; import Navigation from '@navigation/Navigation'; function useFirstRenderRoute(focusExceptionRoutes?: string[]) { - const initialRoute = useRef(null); + const initialRoute = useRef(null); - useEffect(() => { - initialRoute.current = Navigation.getActiveRouteWithoutParams(); - }, []); + useEffect(() => { + initialRoute.current = Navigation.getActiveRouteWithoutParams(); + }, []); - return { - get isFocused() { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - const allRoutesToConsider = [initialRoute.current, ...(focusExceptionRoutes ?? [])]; - return allRoutesToConsider.includes(activeRoute); - }, - get firstRenderRoute() { - return initialRoute.current; - }, - }; + return { + get isFocused() { + const activeRoute = Navigation.getActiveRouteWithoutParams(); + const allRoutesToConsider = [initialRoute.current, ...(focusExceptionRoutes ?? [])]; + return allRoutesToConsider.includes(activeRoute); + }, + get firstRenderRoute() { + return initialRoute.current; + }, + }; } export default useFirstRenderRoute; From 703ea6c027385e8b954f34336d1076ba01a8c777 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 4 Apr 2025 16:56:39 +0200 Subject: [PATCH 11/12] Change useFirstRenderRoute value getter name --- src/hooks/useFirstRenderRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useFirstRenderRoute.ts b/src/hooks/useFirstRenderRoute.ts index 57380f3622eb9..f64629f659482 100644 --- a/src/hooks/useFirstRenderRoute.ts +++ b/src/hooks/useFirstRenderRoute.ts @@ -14,7 +14,7 @@ function useFirstRenderRoute(focusExceptionRoutes?: string[]) { const allRoutesToConsider = [initialRoute.current, ...(focusExceptionRoutes ?? [])]; return allRoutesToConsider.includes(activeRoute); }, - get firstRenderRoute() { + get value() { return initialRoute.current; }, }; From 5c45bf5c3cedef1b1006e05d6ad9c8d93b6edef8 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 4 Apr 2025 19:26:06 +0200 Subject: [PATCH 12/12] Fix attachment view after reload --- src/components/VideoPlayerContexts/PlaybackContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index c20b04b243101..0c456940407be 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -122,13 +122,13 @@ function PlaybackContextProvider({children}: ChildrenProps) { // Used for /attachment route const topMostReport = getReportOrDraftReport(Navigation.getTopmostReportId()); const reportIDFromUrlParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID') ?? undefined; - const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID : reportIDFromUrlParams; + const attachmentReportID = Navigation.getActiveRouteWithoutParams() === `/${ROUTES.ATTACHMENTS.route}` ? prevCurrentReportID ?? reportIDFromUrlParams : undefined; const reportIDWithUrl = isChatThread(topMostReport) ? findUrlInReportOrAncestorAttachments(topMostReport, url) : undefined; // - if it is a chat thread, use chat thread ID or any ascentor ID since the video could have originally been sent on report many levels up // - report ID in which we are currently, if it is not a chat thread // - if it is an attachment route, then we take report ID from the URL params - const currentPlayReportID = [reportIDWithUrl, currentReportID, attachmentReportID].find((id) => id !== undefined); + const currentPlayReportID = [attachmentReportID, reportIDWithUrl, currentReportID].find((id) => id !== undefined); setCurrentlyPlayingURLReportID(currentPlayReportID); setCurrentlyPlayingURL(url);