Restyle video playback error (#6871)
* Restyle video playback error * video not shown when attached with multiple files and thumb fails * update local path when saving and sharing a file * feedback review
This commit is contained in:
parent
f7e7ffdc2c
commit
88fde2cc5e
7 changed files with 188 additions and 28 deletions
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
|
||||
import Animated, {useDerivedValue} from 'react-native-reanimated';
|
||||
|
||||
|
|
@ -69,10 +69,9 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
filesForGallery.value[idx] = file;
|
||||
};
|
||||
|
||||
const isSingleImage = () => (filesInfo.length === 1 && (isImage(filesInfo[0]) || isVideo(filesInfo[0])));
|
||||
const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]);
|
||||
|
||||
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => {
|
||||
const singleImage = isSingleImage();
|
||||
let nonVisibleImagesCount: number;
|
||||
let container: StyleProp<ViewStyle> = items.length > 1 ? styles.container : undefined;
|
||||
const containerWithGutter = [container, styles.gutter];
|
||||
|
|
@ -97,7 +96,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
file={file}
|
||||
index={attachmentIndex(file.id!)}
|
||||
onPress={handlePreviewPress}
|
||||
isSingleImage={singleImage}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
updateFileForGallery={updateFileForGallery}
|
||||
|
|
|
|||
|
|
@ -76,9 +76,8 @@ const VideoFile = ({
|
|||
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
|
||||
return calculateDimensions(video.height || wrapperWidth, video.width || wrapperWidth, wrapperWidth, viewPortHeight);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth]);
|
||||
}, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth, isSingleImage]);
|
||||
|
||||
const getThumbnail = async () => {
|
||||
const data = {...file};
|
||||
|
|
@ -161,7 +160,12 @@ const VideoFile = ({
|
|||
|
||||
if (failed) {
|
||||
thumbnail = (
|
||||
<View style={[isSingleImage ? null : style.imagePreview, style.failed, imageDimensions]}>
|
||||
<View
|
||||
|
||||
// @ts-expect-error ref of type unknown
|
||||
ref={forwardRef}
|
||||
style={[isSingleImage ? null : {height: '100%'}, style.failed, imageDimensions]}
|
||||
>
|
||||
<FileIcon
|
||||
failed={failed}
|
||||
file={file}
|
||||
|
|
@ -174,7 +178,7 @@ const VideoFile = ({
|
|||
<View
|
||||
style={style.fileImageWrapper}
|
||||
>
|
||||
{!isSingleImage && <View style={style.boxPlaceholder}/>}
|
||||
{!isSingleImage && !failed && <View style={style.boxPlaceholder}/>}
|
||||
{thumbnail}
|
||||
<View style={style.playContainer}>
|
||||
<View style={style.play}>
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ const DocumentRenderer = ({canDownloadFiles, item, onShouldHideControls}: Props)
|
|||
onPress={handleOpenFile}
|
||||
rippleColor={changeOpacity('#fff', 0.16)}
|
||||
>
|
||||
<View style={buttonBackgroundStyle(Preferences.THEMES.denim, 'lg', 'primary', enabled ? 'default' : 'disabled')}>
|
||||
<Text style={buttonTextStyle(Preferences.THEMES.denim, 'lg', 'primary', enabled ? 'default' : 'disabled')} >{optionText}</Text>
|
||||
<View style={buttonBackgroundStyle(Preferences.THEMES.onyx, 'lg', 'primary', enabled ? 'default' : 'disabled')}>
|
||||
<Text style={buttonTextStyle(Preferences.THEMES.onyx, 'lg', 'primary', enabled ? 'default' : 'disabled')} >{optionText}</Text>
|
||||
</View>
|
||||
</RectButton>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
|||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import Share from 'react-native-share';
|
||||
|
||||
import {updateLocalFilePath} from '@actions/local/file';
|
||||
import {downloadFile} from '@actions/remote/file';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProgressBar from '@components/progress_bar';
|
||||
|
|
@ -173,6 +174,8 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery
|
|||
return;
|
||||
}
|
||||
|
||||
updateLocalFilePath(serverUrl, item.id, path);
|
||||
|
||||
Share.open({
|
||||
url: path,
|
||||
saveToFiles: true,
|
||||
|
|
@ -193,6 +196,7 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery
|
|||
album: applicationName,
|
||||
});
|
||||
setSaved(true);
|
||||
updateLocalFilePath(serverUrl, item.id, path);
|
||||
} catch {
|
||||
setError(intl.formatMessage({id: 'gallery.save_failed', defaultMessage: 'Unable to save the file'}));
|
||||
}
|
||||
|
|
@ -223,6 +227,7 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery
|
|||
if (response.data?.path) {
|
||||
const path = response.data.path as string;
|
||||
onDownloadSuccess?.(path);
|
||||
updateLocalFilePath(serverUrl, item.id, path);
|
||||
Share.open({
|
||||
message: '',
|
||||
title: '',
|
||||
|
|
|
|||
145
app/screens/gallery/video_renderer/error.tsx
Normal file
145
app/screens/gallery/video_renderer/error.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Dispatch, SetStateAction, useCallback, useState} from 'react';
|
||||
import {StyleSheet, Text, useWindowDimensions, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {RectButton, TouchableWithoutFeedback} from 'react-native-gesture-handler';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Preferences} from '@constants';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {calculateDimensions} from '@utils/images';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
maxWidth: 600,
|
||||
},
|
||||
filename: {
|
||||
color: '#FFF',
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
marginVertical: 8,
|
||||
paddingHorizontal: 25,
|
||||
textAlign: 'center',
|
||||
},
|
||||
unsupported: {
|
||||
color: '#FFF',
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 25,
|
||||
opacity: 0.64,
|
||||
textAlign: 'center',
|
||||
},
|
||||
marginBottom: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
marginTop: {
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
filename: string;
|
||||
height: number;
|
||||
isDownloading: boolean;
|
||||
isRemote: boolean;
|
||||
onShouldHideControls: () => void;
|
||||
posterUri?: string;
|
||||
setDownloading: Dispatch<SetStateAction<boolean>>;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const VideoError = ({filename, height, isDownloading, isRemote, onShouldHideControls, posterUri, setDownloading, width}: Props) => {
|
||||
const [hasPoster, setHasPoster] = useState(false);
|
||||
const [loadPosterError, setLoadPosterError] = useState(false);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
setDownloading(true);
|
||||
}, []);
|
||||
|
||||
const handlePosterSet = useCallback(() => {
|
||||
setHasPoster(true);
|
||||
}, []);
|
||||
|
||||
const handlePosterError = useCallback(() => {
|
||||
setLoadPosterError(true);
|
||||
}, []);
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const imageDimensions = calculateDimensions(height, width, dimensions.width);
|
||||
|
||||
let poster;
|
||||
if (posterUri && !loadPosterError) {
|
||||
poster = (
|
||||
<FastImage
|
||||
source={{uri: posterUri}}
|
||||
style={hasPoster && imageDimensions}
|
||||
onLoad={handlePosterSet}
|
||||
onError={handlePosterError}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
poster = (
|
||||
<CompassIcon
|
||||
color='#338AFF' // yes this is hardcoded
|
||||
name='file-video-outline-large'
|
||||
size={120}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={onShouldHideControls}>
|
||||
<Animated.View style={styles.container}>
|
||||
{poster}
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={styles.filename}
|
||||
>
|
||||
{filename}
|
||||
</Text>
|
||||
{isRemote &&
|
||||
<View style={styles.marginTop}>
|
||||
<View style={styles.marginBottom}>
|
||||
<FormattedText
|
||||
defaultMessage='This video must be downloaded to play it.'
|
||||
id='video.download_description'
|
||||
style={styles.unsupported}
|
||||
/>
|
||||
</View>
|
||||
<RectButton
|
||||
enabled={!isDownloading}
|
||||
exclusive={true}
|
||||
onPress={handleDownload}
|
||||
rippleColor={changeOpacity('#fff', 0.16)}
|
||||
>
|
||||
<View style={buttonBackgroundStyle(Preferences.THEMES.onyx, 'lg', 'primary', isDownloading ? 'disabled' : 'default')}>
|
||||
<FormattedText
|
||||
defaultMessage='Download'
|
||||
id='video.download'
|
||||
style={buttonTextStyle(Preferences.THEMES.onyx, 'lg', 'primary', isDownloading ? 'disabled' : 'default')}
|
||||
/>
|
||||
</View>
|
||||
</RectButton>
|
||||
</View>
|
||||
}
|
||||
{!isRemote &&
|
||||
<FormattedText
|
||||
defaultMessage='An error occurred while trying to play the video.'
|
||||
id='video.failed_description'
|
||||
style={styles.unsupported}
|
||||
/>
|
||||
}
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoError;
|
||||
|
|
@ -2,8 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions} from 'react-native';
|
||||
import {DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions} from 'react-native';
|
||||
import Animated, {Easing, useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming, WithTimingConfig} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import Video, {OnPlaybackRateData} from 'react-native-video';
|
||||
|
|
@ -17,6 +16,8 @@ import {changeOpacity} from '@utils/theme';
|
|||
|
||||
import DownloadWithAction from '../footer/download_with_action';
|
||||
|
||||
import VideoError from './error';
|
||||
|
||||
import type {ImageRendererProps} from '../image_renderer';
|
||||
import type {GalleryAction} from '@typings/screens/gallery';
|
||||
|
||||
|
|
@ -55,7 +56,6 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
const dimensions = useWindowDimensions();
|
||||
const fullscreen = useSharedValue(false);
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const {formatMessage} = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const videoRef = useAnimatedRef<Video>();
|
||||
const showControls = useRef(!(initialIndex === index));
|
||||
|
|
@ -63,6 +63,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
const [videoReady, setVideoReady] = useState(false);
|
||||
const [videoUri, setVideoUri] = useState(item.uri);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const source = useMemo(() => ({uri: videoUri}), [videoUri]);
|
||||
|
||||
const setFullscreen = (value: boolean) => {
|
||||
|
|
@ -71,6 +72,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
|
||||
const onDownloadSuccess = (path: string) => {
|
||||
setVideoUri(path);
|
||||
setHasError(false);
|
||||
updateLocalFilePath(serverUrl, item.id, path);
|
||||
};
|
||||
|
||||
|
|
@ -83,18 +85,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
}, [onShouldHideControls]);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
Alert.alert(
|
||||
formatMessage({id: 'video.failed_title', defaultMessage: 'Video playback failed'}),
|
||||
formatMessage({id: 'video.failed_description', defaultMessage: 'An error occurred while trying to play the video.\n'}),
|
||||
[{
|
||||
text: formatMessage({id: 'video.download', defaultMessage: 'Download video'}),
|
||||
onPress: () => {
|
||||
setDownloading(true);
|
||||
},
|
||||
}, {
|
||||
text: formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
}],
|
||||
);
|
||||
setHasError(true);
|
||||
}, []);
|
||||
|
||||
const onFullscreenPlayerWillDismiss = useCallback(() => {
|
||||
|
|
@ -124,6 +115,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
|
||||
const onReadyForDisplay = useCallback(() => {
|
||||
setVideoReady(true);
|
||||
setHasError(false);
|
||||
}, []);
|
||||
|
||||
const handleTouchStart = useCallback(() => {
|
||||
|
|
@ -142,6 +134,10 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
let w = width;
|
||||
let h = height - (VIDEO_INSET + GALLERY_FOOTER_HEIGHT + bottom);
|
||||
|
||||
if (hasError) {
|
||||
return {height: 0};
|
||||
}
|
||||
|
||||
if (fullscreen.value) {
|
||||
w = dimensions.width;
|
||||
h = dimensions.height;
|
||||
|
|
@ -154,7 +150,7 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
width: withTiming(w, timingConfig),
|
||||
height: withTiming(h, timingConfig),
|
||||
};
|
||||
}, [dimensions]);
|
||||
}, [dimensions, hasError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialIndex === index && videoReady) {
|
||||
|
|
@ -199,6 +195,18 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
|
|||
/>
|
||||
</Animated.View>
|
||||
}
|
||||
{hasError &&
|
||||
<VideoError
|
||||
filename={item.name}
|
||||
isDownloading={downloading}
|
||||
isRemote={videoUri.startsWith('http')}
|
||||
onShouldHideControls={handleTouchStart}
|
||||
posterUri={item.posterUri}
|
||||
setDownloading={setDownloading}
|
||||
height={item.height}
|
||||
width={item.width}
|
||||
/>
|
||||
}
|
||||
{downloading &&
|
||||
<DownloadWithAction
|
||||
action='external'
|
||||
|
|
|
|||
|
|
@ -929,7 +929,6 @@
|
|||
"user.settings.notifications.email_threads.description": "Notify me about all replies to threads I'm following",
|
||||
"user.tutorial.long_press": "Long-press on an item to view a user's profile",
|
||||
"video.download": "Download video",
|
||||
"video.failed_description": "An error occurred while trying to play the video.\n",
|
||||
"video.failed_title": "Video playback failed",
|
||||
"video.failed_description": "An error occurred while trying to play the video.",
|
||||
"your.servers": "Your servers"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue