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:
Elias Nahum 2022-12-19 21:29:11 +02:00 committed by GitHub
parent f7e7ffdc2c
commit 88fde2cc5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 188 additions and 28 deletions

View file

@ -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}

View file

@ -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}>

View file

@ -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>

View file

@ -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: '',

View 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;

View file

@ -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'

View file

@ -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"
}