Skip to content

Commit

Permalink
Merge pull request Expensify#37134 from software-mansion-labs/ts-migr…
Browse files Browse the repository at this point in the history
…ation-videoplayercontext

[TS migration] Migrate VideoPlayerContexts  component files to TypeScript
  • Loading branch information
Hayata Suenaga authored Mar 13, 2024
2 parents 92ad1cd + 4c579d9 commit b6a7f7e
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 117 deletions.
36 changes: 4 additions & 32 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type {ImageContentFit} from 'expo-image';
import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
Expand All @@ -10,48 +9,21 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type IconAsset from '@src/types/utils/IconAsset';
import * as Expensicons from './Icon/Expensicons';
import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
import Text from './Text';

type PopoverMenuItem = {
/** An icon element displayed on the left side */
icon?: IconAsset;

type PopoverMenuItem = MenuItemProps & {
/** Text label */
text: string;

/** A callback triggered when this item is selected */
onSelected: () => void;

/** A description text to show under the title */
description?: string;

/** The fill color to pass into the icon. */
iconFill?: string;

/** Icon Width */
iconWidth?: number;

/** Icon Height */
iconHeight?: number;

/** Icon should be displayed in its own color */
displayInDefaultIconColor?: boolean;

/** Determines how the icon should be resized to fit its container */
contentFit?: ImageContentFit;
onSelected?: () => void;

/** Sub menu items to be rendered after a menu item is selected */
subMenuItems?: PopoverMenuItem[];

/** Determines whether an icon should be displayed on the right side of the menu item. */
shouldShowRightIcon?: boolean;

/** Adds padding to the left of the text when there is no icon. */
shouldPutLeftPaddingWhenNoIcon?: boolean;
};

type PopoverModalProps = Pick<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming'>;
Expand Down Expand Up @@ -181,7 +153,7 @@ function PopoverMenu({
const onModalHide = () => {
setFocusedIndex(-1);
if (selectedItemIndex.current !== null) {
currentMenuItems[selectedItemIndex.current].onSelected();
currentMenuItems[selectedItemIndex.current].onSelected?.();
selectedItemIndex.current = null;
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,43 @@
import PropTypes from 'prop-types';
import type {AVPlaybackStatusToSet, Video} from 'expo-av';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import type {View} from 'react-native';
import useCurrentReportID from '@hooks/useCurrentReportID';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {PlaybackContext, StatusCallback} from './types';

const PlaybackContext = React.createContext(null);
const Context = React.createContext<PlaybackContext | null>(null);

function PlaybackContextProvider({children}) {
const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null);
const [sharedElement, setSharedElement] = useState(null);
const [originalParent, setOriginalParent] = useState(null);
const currentVideoPlayerRef = useRef(null);
const {currentReportID} = useCurrentReportID();
function PlaybackContextProvider({children}: ChildrenProps) {
const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState<string | null>(null);
const [sharedElement, setSharedElement] = useState<View | null>(null);
const [originalParent, setOriginalParent] = useState<View | null>(null);
const currentVideoPlayerRef = useRef<Video | null>(null);
const {currentReportID} = useCurrentReportID() ?? {};

const pauseVideo = useCallback(() => {
if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.setStatusAsync)) {
return;
}
currentVideoPlayerRef.current.setStatusAsync({shouldPlay: false});
currentVideoPlayerRef.current?.setStatusAsync({shouldPlay: false});
}, [currentVideoPlayerRef]);

const stopVideo = useCallback(() => {
if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.stopAsync)) {
return;
}
currentVideoPlayerRef.current.stopAsync({shouldPlay: false});
currentVideoPlayerRef.current?.stopAsync();
}, [currentVideoPlayerRef]);

const playVideo = useCallback(() => {
if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.setStatusAsync)) {
return;
}
currentVideoPlayerRef.current.getStatusAsync().then((status) => {
const newStatus = {shouldPlay: true};
if (status.durationMillis === status.positionMillis) {
currentVideoPlayerRef.current?.getStatusAsync().then((status) => {
const newStatus: AVPlaybackStatusToSet = {shouldPlay: true};
if ('durationMillis' in status && status.durationMillis === status.positionMillis) {
newStatus.positionMillis = 0;
}
currentVideoPlayerRef.current.setStatusAsync(newStatus);
currentVideoPlayerRef.current?.setStatusAsync(newStatus);
});
}, [currentVideoPlayerRef]);

const unloadVideo = useCallback(() => {
if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.unloadAsync)) {
return;
}
currentVideoPlayerRef.current.unloadAsync();
currentVideoPlayerRef.current?.unloadAsync();
}, [currentVideoPlayerRef]);

const updateCurrentlyPlayingURL = useCallback(
(url) => {
(url: string) => {
if (currentlyPlayingURL && url !== currentlyPlayingURL) {
pauseVideo();
}
Expand All @@ -56,7 +47,7 @@ function PlaybackContextProvider({children}) {
);

const shareVideoPlayerElements = useCallback(
(ref, parent, child, isUploading) => {
(ref: Video, parent: View, child: View, isUploading: boolean) => {
currentVideoPlayerRef.current = ref;
setOriginalParent(parent);
setSharedElement(child);
Expand All @@ -69,12 +60,9 @@ function PlaybackContextProvider({children}) {
);

const checkVideoPlaying = useCallback(
(statusCallback) => {
if (!(currentVideoPlayerRef && currentVideoPlayerRef.current && currentVideoPlayerRef.current.getStatusAsync)) {
return;
}
currentVideoPlayerRef.current.getStatusAsync().then((status) => {
statusCallback(status.isPlaying);
(statusCallback: StatusCallback) => {
currentVideoPlayerRef.current?.getStatusAsync().then((status) => {
statusCallback('isPlaying' in status && status.isPlaying);
});
},
[currentVideoPlayerRef],
Expand Down Expand Up @@ -110,21 +98,17 @@ function PlaybackContextProvider({children}) {
}),
[updateCurrentlyPlayingURL, currentlyPlayingURL, originalParent, sharedElement, shareVideoPlayerElements, playVideo, pauseVideo, checkVideoPlaying],
);
return <PlaybackContext.Provider value={contextValue}>{children}</PlaybackContext.Provider>;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

function usePlaybackContext() {
const context = useContext(PlaybackContext);
if (context === undefined) {
const playbackContext = useContext(Context);
if (!playbackContext) {
throw new Error('usePlaybackContext must be used within a PlaybackContextProvider');
}
return context;
return playbackContext;
}

PlaybackContextProvider.displayName = 'PlaybackContextProvider';
PlaybackContextProvider.propTypes = {
/** Actual content wrapped by this component */
children: PropTypes.node.isRequired,
};

export {PlaybackContextProvider, usePlaybackContext};
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import PropTypes from 'prop-types';
import React, {useCallback, useContext, useMemo, useState} from 'react';
import _ from 'underscore';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import fileDownload from '@libs/fileDownload';
import CONST from '@src/CONST';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {usePlaybackContext} from './PlaybackContext';
import type {PlaybackSpeed, VideoPopoverMenuContext} from './types';

const VideoPopoverMenuContext = React.createContext(null);
const Context = React.createContext<VideoPopoverMenuContext | null>(null);

function VideoPopoverMenuContextProvider({children}) {
function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
const {currentVideoPlayerRef, currentlyPlayingURL} = usePlaybackContext();
const {translate} = useLocalize();
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]);
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState<PlaybackSpeed>(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]);
const {isOffline} = useNetwork();
const isLocalFile = currentlyPlayingURL && _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => currentlyPlayingURL.startsWith(prefix));
const isLocalFile = currentlyPlayingURL && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => currentlyPlayingURL.startsWith(prefix));

const updatePlaybackSpeed = useCallback(
(speed) => {
(speed: PlaybackSpeed) => {
setCurrentPlaybackSpeed(speed);
currentVideoPlayerRef.current.setStatusAsync({rate: speed});
currentVideoPlayerRef.current?.setStatusAsync({rate: speed});
},
[currentVideoPlayerRef],
);

const downloadAttachment = useCallback(() => {
if (currentlyPlayingURL === null) {
return;
}
const sourceURI = addEncryptedAuthTokenToURL(currentlyPlayingURL);
fileDownload(sourceURI);
}, [currentlyPlayingURL]);

const menuItems = useMemo(() => {
const items = [];
const items: PopoverMenuItem[] = [];

if (!isOffline && !isLocalFile) {
items.push({
Expand All @@ -47,37 +51,30 @@ function VideoPopoverMenuContextProvider({children}) {
items.push({
icon: Expensicons.Meter,
text: translate('videoPlayer.playbackSpeed'),
subMenuItems: [
..._.map(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS, (speed) => ({
icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : null,
text: speed.toString(),
onSelected: () => {
updatePlaybackSpeed(speed);
},
shouldPutLeftPaddingWhenNoIcon: true,
})),
],
subMenuItems: CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS.map((speed) => ({
icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : undefined,
text: speed.toString(),
onSelected: () => {
updatePlaybackSpeed(speed);
},
shouldPutLeftPaddingWhenNoIcon: true,
})),
});

return items;
}, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline, isLocalFile]);

const contextValue = useMemo(() => ({menuItems, updatePlaybackSpeed}), [menuItems, updatePlaybackSpeed]);
return <VideoPopoverMenuContext.Provider value={contextValue}>{children}</VideoPopoverMenuContext.Provider>;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

function useVideoPopoverMenuContext() {
const context = useContext(VideoPopoverMenuContext);
if (context === undefined) {
const videoPopooverMenuContext = useContext(Context);
if (!videoPopooverMenuContext) {
throw new Error('useVideoPopoverMenuContext must be used within a VideoPopoverMenuContext');
}
return context;
return videoPopooverMenuContext;
}

VideoPopoverMenuContextProvider.displayName = 'VideoPopoverMenuContextProvider';
VideoPopoverMenuContextProvider.propTypes = {
/** Actual content wrapped by this component */
children: PropTypes.node.isRequired,
};

export {VideoPopoverMenuContextProvider, useVideoPopoverMenuContext};
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import PropTypes from 'prop-types';
import React, {useCallback, useContext, useEffect, useMemo} from 'react';
import {useSharedValue} from 'react-native-reanimated';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {usePlaybackContext} from './PlaybackContext';
import type {VolumeContext} from './types';

const VolumeContext = React.createContext(null);
const Context = React.createContext<VolumeContext | null>(null);

function VolumeContextProvider({children}) {
function VolumeContextProvider({children}: ChildrenProps) {
const {currentVideoPlayerRef, originalParent} = usePlaybackContext();
const volume = useSharedValue(0);

const updateVolume = useCallback(
(newVolume) => {
(newVolume: number) => {
if (!currentVideoPlayerRef.current) {
return;
}
Expand All @@ -30,21 +31,17 @@ function VolumeContextProvider({children}) {
}, [originalParent, updateVolume, volume.value]);

const contextValue = useMemo(() => ({updateVolume, volume}), [updateVolume, volume]);
return <VolumeContext.Provider value={contextValue}>{children}</VolumeContext.Provider>;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

function useVolumeContext() {
const context = useContext(VolumeContext);
if (context === undefined) {
const volumeContext = useContext(Context);
if (!volumeContext) {
throw new Error('useVolumeContext must be used within a VolumeContextProvider');
}
return context;
return volumeContext;
}

VolumeContextProvider.displayName = 'VolumeContextProvider';
VolumeContextProvider.propTypes = {
/** Actual content wrapped by this component */
children: PropTypes.node.isRequired,
};

export {VolumeContextProvider, useVolumeContext};
34 changes: 34 additions & 0 deletions src/components/VideoPlayerContexts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {Video} from 'expo-av';
import type {MutableRefObject} from 'react';
import type {View} from 'react-native';
import type {SharedValue} from 'react-native-reanimated';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type CONST from '@src/CONST';

type PlaybackContext = {
updateCurrentlyPlayingURL: (url: string) => void;
currentlyPlayingURL: string | null;
originalParent: View | null;
sharedElement: View | null;
currentVideoPlayerRef: MutableRefObject<Video | null>;
shareVideoPlayerElements: (ref: Video, parent: View, child: View, isUploading: boolean) => void;
playVideo: () => void;
pauseVideo: () => void;
checkVideoPlaying: (statusCallback: StatusCallback) => void;
};

type VolumeContext = {
updateVolume: (newVolume: number) => void;
volume: SharedValue<number>;
};

type VideoPopoverMenuContext = {
menuItems: PopoverMenuItem[];
updatePlaybackSpeed: (speed: PlaybackSpeed) => void;
};

type StatusCallback = (isPlaying: boolean) => void;

type PlaybackSpeed = (typeof CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS)[number];

export type {PlaybackContext, VolumeContext, VideoPopoverMenuContext, StatusCallback, PlaybackSpeed};
3 changes: 2 additions & 1 deletion src/libs/fileDownload/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ function hasAndroidPermission(): Promise<boolean> {
/**
* Handling the download
*/
function handleDownload(url: string, fileName: string, successMessage?: string): Promise<void> {
function handleDownload(url: string, fileName?: string, successMessage?: string): Promise<void> {
return new Promise((resolve) => {
const dirs = RNFetchBlob.fs.dirs;

// Android files will download to Download directory
const path = dirs.DownloadDir;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)`
const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url));

const isLocalFile = url.startsWith('file://');
Expand Down
1 change: 1 addition & 0 deletions src/libs/fileDownload/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const fileDownload: FileDownload = (fileUrl, fileName, successMessage) =>
new Promise((resolve) => {
let fileDownloadPromise;
const fileType = FileUtils.getFileType(fileUrl);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)`
const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(fileUrl));

switch (fileType) {
Expand Down
Loading

0 comments on commit b6a7f7e

Please sign in to comment.