Skip to content
Open
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
2 changes: 0 additions & 2 deletions components/content/QueryState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const QueryState: React.FunctionComponent<QueryStateProps> = ({results, t
const isResultNotFound = (result: UseQueryResult): boolean => result.isError && result.error instanceof NotFoundError;

export const InternalError: React.FunctionComponent<{inline?: boolean}> = ({inline}) => {
// const navigation = useNavigation<TabNavigationProps>();
return (
<Outcome
headline={'Oh no!'}
Expand All @@ -63,7 +62,6 @@ export const InternalError: React.FunctionComponent<{inline?: boolean}> = ({inli
illustrationBottomMargin={-64}
illustrationLeftMargin={-16}
inline={inline}
// onClose={() => navigation.navigate('Home')} // TODO(skuznets): figure out how to navigate home here, as we don't have the props needed to go home - can we go to defaults for tab navigator?
/>
);
};
Expand Down
36 changes: 25 additions & 11 deletions components/content/carousel/MediaPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {MediaViewerModal} from 'components/content/carousel/MediaViewerModal/MediaViewerModal';
import {NetworkImage} from 'components/content/carousel/NetworkImage';
import {imageToThumbnailListItem, ThumbnailListItem, videoToThumbnailListItem} from 'components/content/carousel/ThumbnailList';
import {PDFThumbnail} from 'components/content/carousel/PDFThumbnail';
import {imageToThumbnailListItem, pdfToThumbnailListItem, ThumbnailListItem, videoToThumbnailListItem} from 'components/content/carousel/ThumbnailList';
import {InternalError} from 'components/content/QueryState';
import {View, ViewProps, VStack} from 'components/core';
import {HTML, HTMLRendererConfig} from 'components/text/HTML';
import React, {useCallback, useMemo, useState} from 'react';
Expand All @@ -16,6 +18,10 @@ const thumbnailListItem = (mediaItem: MediaItem): ThumbnailListItem | undefined
return imageToThumbnailListItem(mediaItem);
}

if (mediaItem.type === MediaType.PDF) {
return pdfToThumbnailListItem(mediaItem);
}

return undefined;
};

Expand All @@ -40,21 +46,29 @@ export const MediaPreview: React.FunctionComponent<MediaPreviewProps> = ({thumbn
const thumbnailItem = useMemo(() => thumbnailListItem(mediaItem), [mediaItem]);

if (!thumbnailItem) {
return <View />;
return (
<View width={thumbnailWidth} height={thumbnailHeight}>
<InternalError />
</View>
);
}

return (
<View>
<VStack justifyContent="center" alignItems="center" space={8}>
<NetworkImage
width={thumbnailWidth}
height={thumbnailHeight}
uri={thumbnailItem.uri}
index={0}
showVideoIndicator={thumbnailItem.isVideo}
onPress={onPress}
imageStyle={{borderRadius: 4}}
/>
{thumbnailItem.kind === 'pdf' ? (
<PDFThumbnail width={thumbnailWidth} height={thumbnailHeight} index={0} onPress={onPress} imageStyle={{borderRadius: 4}} />
) : (
<NetworkImage
width={thumbnailWidth}
height={thumbnailHeight}
uri={thumbnailItem.uri}
index={0}
showVideoIndicator={thumbnailItem.kind === 'video'}
onPress={onPress}
imageStyle={{borderRadius: 4}}
/>
)}
{thumbnailItem.caption && (
<View px={32}>
<HTMLRendererConfig baseStyle={{fontSize: 12, textAlign: 'center', fontStyle: 'italic'}}>
Expand Down
2 changes: 1 addition & 1 deletion components/content/carousel/MediaViewerModal/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export const ImageView: React.FunctionComponent<ImageViewProps> = ({item, native
return (
<GestureDetector gesture={composedGesture}>
<Animated.View style={[{flex: 1}, animatedStyle]}>
<Image style={[{flex: 1}, animatedStyle]} contentFit="contain" contentPosition={'center'} source={item.url.original} />
<Image style={{flex: 1}} contentFit="contain" contentPosition={'center'} source={item.url.original} />
</Animated.View>
</GestureDetector>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import {ImageView} from 'components/content/carousel/MediaViewerModal/ImageView';
import {PDFView} from 'components/content/carousel/MediaViewerModal/PDFView';
import {WebVideoView} from 'components/content/carousel/MediaViewerModal/WebVideoView';
import {View} from 'components/core';
import {BodySm} from 'components/text';
import {usePostHog} from 'posthog-react-native';
import React, {useEffect} from 'react';
import {Dimensions} from 'react-native';
import {useWindowDimensions} from 'react-native';
import {NativeGesture} from 'react-native-gesture-handler';
import {MediaItem, MediaType} from 'types/nationalAvalancheCenter';

const SCREEN = Dimensions.get('screen');
const SCREEN_WIDTH = SCREEN.width;
const SCREEN_HEIGHT = SCREEN.height;

interface MediaContentProps {
item: MediaItem;
isVisible: boolean;
Expand All @@ -20,14 +17,17 @@ interface MediaContentProps {

export const MediaContentView: React.FunctionComponent<MediaContentProps> = ({item, isVisible, nativeGesture}) => {
const postHog = usePostHog();
const dimensions = useWindowDimensions();

let content: React.JSX.Element;
let isMediaSupported = true;

if (item.type === MediaType.Image) {
content = <ImageView item={item} nativeGesture={nativeGesture} fullScreenWidth={SCREEN_WIDTH} />;
content = <ImageView item={item} nativeGesture={nativeGesture} fullScreenWidth={dimensions.width} />;
} else if (item.type === MediaType.Video) {
content = <WebVideoView item={item} isVisible={isVisible} />;
} else if (item.type === MediaType.PDF) {
content = <PDFView item={item} />;
} else {
isMediaSupported = false;
content = <BodySm>{'Unsupported Media Type'}</BodySm>;
Expand All @@ -39,5 +39,5 @@ export const MediaContentView: React.FunctionComponent<MediaContentProps> = ({it
}
}, [postHog, isMediaSupported, item]);

return <View style={{width: SCREEN_WIDTH, height: SCREEN_HEIGHT}}>{content}</View>;
return <View style={{width: dimensions.width, flex: 1}}>{content}</View>;
};
28 changes: 28 additions & 0 deletions components/content/carousel/MediaViewerModal/PDFView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {WebMediaView} from 'components/content/carousel/MediaViewerModal/WebMediaView';
import {usePostHog} from 'posthog-react-native';
import React, {useEffect} from 'react';
import {Platform} from 'react-native';
import {WebViewSource} from 'react-native-webview/lib/WebViewTypes';
import {PDFMediaItem} from 'types/nationalAvalancheCenter';

interface PDFViewProps {
item: PDFMediaItem;
}

const googleDocsViewer = (url: string) => `https://docs.google.com/gview?embedded=true&url=${encodeURIComponent(url)}`;

const getPdfSource = (item: PDFMediaItem): WebViewSource => ({
uri: Platform.OS === 'android' ? googleDocsViewer(item.url.original) : item.url.original,
});

export const PDFView: React.FunctionComponent<PDFViewProps> = ({item}: PDFViewProps) => {
const postHog = usePostHog();

useEffect(() => {
if (postHog) {
postHog.capture('pdfView-Opened', {url: item.url.original});
}
}, [postHog, item.url.original]);

return <WebMediaView source={getPdfSource(item)} heightFraction={0.7} errorMessage="An error occurred loading the PDF. Please try again" scalesPageToFit />;
};
104 changes: 104 additions & 0 deletions components/content/carousel/MediaViewerModal/WebMediaView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {onlineManager} from '@tanstack/react-query';
import {MediaLoadErrorView} from 'components/content/carousel/MediaViewerModal/MediaLoadErrorView';
import {isOfflineErrorCode} from 'components/content/carousel/MediaViewerModal/webViewOfflineError';
import {View} from 'components/core';
import React, {Ref, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {ActivityIndicator, LayoutChangeEvent, StyleSheet} from 'react-native';
import WebView from 'react-native-webview';
import {WebViewErrorEvent, WebViewSource} from 'react-native-webview/lib/WebViewTypes';
import {colorLookup} from 'theme';

export interface WebMediaViewHandle {
reload: () => void;
injectJavaScript: (script: string) => void;
}

export interface WebMediaViewProps {
ref?: Ref<WebMediaViewHandle>;
source: WebViewSource;
heightFraction: number;
errorMessage: string;
allowsInlineMediaPlayback?: boolean;
allowsFullscreenVideo?: boolean;
scalesPageToFit?: boolean;
}

export const WebMediaView: React.FunctionComponent<WebMediaViewProps> = ({
ref,
source,
heightFraction,
errorMessage,
allowsInlineMediaPlayback,
allowsFullscreenVideo,
scalesPageToFit,
}) => {
const webRef = useRef<WebView>(null);

const [containerHeight, setContainerHeight] = useState(0);
const [loadError, setLoadError] = useState<WebViewErrorEvent | null>(null);

const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
setContainerHeight(event.nativeEvent.layout.height);
}, []);

useImperativeHandle(
ref,
() => ({
reload: () => webRef.current?.reload(),
injectJavaScript: (script: string) => webRef.current?.injectJavaScript(script),
}),
[],
);

useEffect(() => {
return onlineManager.subscribe(() => {
if (loadError && isOfflineErrorCode(loadError.nativeEvent.code) && onlineManager.isOnline()) {
webRef.current?.reload();
setLoadError(null);
}
});
}, [loadError]);

const maxViewHeight = containerHeight * heightFraction;

const onRenderLoading = useCallback(() => {
return (
<View style={{...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', backgroundColor: colorLookup('modal.background')}}>
<ActivityIndicator size={'large'} />
</View>
);
}, []);

const onError = useCallback((error: WebViewErrorEvent) => {
setLoadError(error);
}, []);

const onRetry = useCallback(() => {
webRef.current?.reload();
setLoadError(null);
}, []);

if (loadError) {
const message = isOfflineErrorCode(loadError.nativeEvent.code) ? "It appears you're not connected to the internet. Please reconnect and try again." : errorMessage;
return <MediaLoadErrorView message={message} onRetry={onRetry} />;
}

return (
<View style={{flex: 1, justifyContent: 'center', backgroundColor: colorLookup('modal.background')}} onLayout={onContainerLayout}>
<View style={{height: maxViewHeight, width: '100%'}}>
<WebView
ref={webRef}
bounces={false}
style={{flex: 1, backgroundColor: colorLookup('modal.background')}}
source={source}
renderLoading={onRenderLoading}
onError={onError}
startInLoadingState
allowsInlineMediaPlayback={allowsInlineMediaPlayback}
allowsFullscreenVideo={allowsFullscreenVideo}
scalesPageToFit={scalesPageToFit}
/>
</View>
</View>
);
};
91 changes: 11 additions & 80 deletions components/content/carousel/MediaViewerModal/WebVideoView.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import {onlineManager} from '@tanstack/react-query';
import {MediaLoadErrorView} from 'components/content/carousel/MediaViewerModal/MediaLoadErrorView';
import {Center, View} from 'components/core';
import {WebMediaView, WebMediaViewHandle} from 'components/content/carousel/MediaViewerModal/WebMediaView';
import {Center} from 'components/core';
import {BodySm} from 'components/text';
import Constants from 'expo-constants';
import {usePostHog} from 'posthog-react-native';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, Dimensions, Platform} from 'react-native';
import WebView from 'react-native-webview';
import {WebViewErrorEvent, WebViewSource} from 'react-native-webview/lib/WebViewTypes';
import {colorLookup} from 'theme';
import React, {useEffect, useRef} from 'react';
import {Platform} from 'react-native';
import {WebViewSource} from 'react-native-webview/lib/WebViewTypes';
import {VideoMediaItem} from 'types/nationalAvalancheCenter';

const SCREEN = Dimensions.get('screen');
const SCREEN_HEIGHT = SCREEN.height;

interface WebVideoViewProps {
item: VideoMediaItem;
isVisible: boolean;
Expand Down Expand Up @@ -51,47 +45,16 @@ const getBundleID = () => {
const youtubeLink = (videoId: string) => `https://youtube.com/embed/${videoId}`;
const refererValue = (bundleID: string) => `https://${bundleID}`;

const ANDROID_OFFLINE_ERROR_CODE = -2;
const IOS_OFFLINE_ERROR_CODE = -1009;

const isOfflineErrorCode = (errorCode: number) => {
return (Platform.OS === 'android' && errorCode == ANDROID_OFFLINE_ERROR_CODE) || (Platform.OS === 'ios' && errorCode == IOS_OFFLINE_ERROR_CODE);
};

export const WebVideoView: React.FunctionComponent<WebVideoViewProps> = ({item, isVisible}: WebVideoViewProps) => {
const webRef = useRef<WebView>(null);
const handleRef = useRef<WebMediaViewHandle>(null);
const postHog = usePostHog();

const [loadError, setLoadError] = useState<WebViewErrorEvent | null>(null);

useEffect(() => {
return onlineManager.subscribe(() => {
if (loadError && isOfflineErrorCode(loadError.nativeEvent.code) && onlineManager.isOnline()) {
webRef.current?.reload();
setLoadError(null);
}
});
}, [loadError, webRef, setLoadError]);

useEffect(() => {
if (!isVisible) {
const jsCode = `document.querySelector('video').pause();`;
webRef.current?.injectJavaScript(jsCode);
handleRef.current?.injectJavaScript(`document.querySelector('video').pause();`);
}
}, [isVisible]);

// This centers the video within the modal
const maxHeight = SCREEN_HEIGHT * 0.33;
const yOffset = (SCREEN_HEIGHT - maxHeight) / 2;

const onRenderLoading = useCallback(() => {
return (
<View flex={1} style={{transform: [{translateY: maxHeight / 2}], backgroundColor: colorLookup('modal.background')}}>
<ActivityIndicator size={'large'} />
</View>
);
}, [maxHeight]);

useEffect(() => {
if (postHog) {
let properties: {[key: string]: string} = {};
Expand All @@ -104,18 +67,6 @@ export const WebVideoView: React.FunctionComponent<WebVideoViewProps> = ({item,
}
}, [postHog, item.url]);

const onError = useCallback(
(error: WebViewErrorEvent) => {
setLoadError(error);
},
[setLoadError],
);

const onRetry = useCallback(() => {
webRef.current?.reload();
setLoadError(null);
}, [webRef, setLoadError]);

let sourceData: WebViewSource;
try {
sourceData = getSourceData(item);
Expand All @@ -127,34 +78,14 @@ export const WebVideoView: React.FunctionComponent<WebVideoViewProps> = ({item,
);
}

if (loadError) {
return <WebViewLoadError error={loadError} onRetry={onRetry} />;
}

return (
<WebView
ref={webRef}
bounces={false}
style={{maxHeight: maxHeight, transform: [{translateY: yOffset}], backgroundColor: colorLookup('modal.background')}}
<WebMediaView
ref={handleRef}
source={sourceData}
renderLoading={onRenderLoading}
onError={onError}
startInLoadingState
heightFraction={0.33}
errorMessage="An error occured loading the video. Please try again"
allowsInlineMediaPlayback
allowsFullscreenVideo
/>
);
};

interface LoadErrorViewProps {
error: WebViewErrorEvent;
onRetry: () => void;
}

const WebViewLoadError: React.FunctionComponent<LoadErrorViewProps> = ({error, onRetry}) => {
const message = isOfflineErrorCode(error.nativeEvent.code)
? "It appears you're not connected to the internet. Please reconnect and try again."
: 'An error occured loading the video. Please try again';

return <MediaLoadErrorView message={message} onRetry={onRetry} />;
};
Loading
Loading