[Gekidou] Use localPath when available (#6058)

* Use localPath when available

* Revert changes to replace space for dash in the filename

* Rename other action to external and always call onDownloadSuccess if defined

* add missing localization strings
This commit is contained in:
Elias Nahum
2022-03-17 08:58:49 -03:00
committed by GitHub
parent ff952ced2a
commit 088aa193ab
14 changed files with 156 additions and 40 deletions

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {getFileById} from '@queries/servers/file';
export const updateLocalFile = async (serverUrl: string, file: FileInfo) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
@@ -11,3 +12,25 @@ export const updateLocalFile = async (serverUrl: string, file: FileInfo) => {
return operator.handleFiles({files: [file], prepareRecordsOnly: false});
};
export const updateLocalFilePath = async (serverUrl: string, fileId: string, localPath: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
try {
const file = await getFileById(database, fileId);
if (file) {
await database.write(async () => {
await file.update((r) => {
r.localPath = localPath;
});
});
}
return {error: undefined};
} catch (error) {
return {error};
}
};

View File

@@ -17,13 +17,10 @@ import {preventDoubleTap} from '@utils/tap';
import File from './file';
import type FileModel from '@typings/database/models/servers/file';
type FilesProps = {
authorId: string;
canDownloadFiles: boolean;
failed?: boolean;
files: FileModel[];
filesInfo: FileInfo[];
layoutWidth?: number;
location: string;
isReplyPost: boolean;
@@ -50,12 +47,11 @@ const styles = StyleSheet.create({
},
});
const Files = ({authorId, canDownloadFiles, failed, files, isReplyPost, layoutWidth, location, postId, publicLinkEnabled, theme}: FilesProps) => {
const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, location, postId, publicLinkEnabled, theme}: FilesProps) => {
const galleryIdentifier = `${postId}-fileAttachments-${location}`;
const [inViewPort, setInViewPort] = useState(false);
const serverUrl = useServerUrl();
const isTablet = useIsTablet();
const filesInfo: FileInfo[] = useMemo(() => files.map((f) => f.toFileInfo(authorId)), [authorId, files]);
const {images: imageAttachments, nonImages: nonImageAttachments} = useMemo(() => {
return filesInfo.reduce(({images, nonImages}: {images: FileInfo[]; nonImages: FileInfo[]}, file) => {
@@ -79,7 +75,7 @@ const Files = ({authorId, canDownloadFiles, failed, files, isReplyPost, layoutWi
}
return {images, nonImages};
}, {images: [], nonImages: []});
}, [files, publicLinkEnabled, serverUrl]);
}, [filesInfo, publicLinkEnabled, serverUrl]);
const filesForGallery = useDerivedValue(() => imageAttachments.concat(nonImageAttachments),
[imageAttachments, nonImageAttachments]);
@@ -89,7 +85,7 @@ const Files = ({authorId, canDownloadFiles, failed, files, isReplyPost, layoutWi
};
const handlePreviewPress = preventDoubleTap((idx: number) => {
const items = filesForGallery.value.map((f) => fileToGalleryItem(f, authorId));
const items = filesForGallery.value.map((f) => fileToGalleryItem(f, f.user_id));
openGalleryAtIndex(galleryIdentifier, idx, items);
});
@@ -99,7 +95,7 @@ const Files = ({authorId, canDownloadFiles, failed, files, isReplyPost, layoutWi
filesForGallery.value[idx] = file;
};
const isSingleImage = () => (files.length === 1 && isImage(files[0]));
const isSingleImage = () => (filesInfo.length === 1 && isImage(filesInfo[0]));
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => {
const singleImage = isSingleImage();

View File

@@ -3,18 +3,40 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {combineLatest, of as of$, from as from$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import {fileExists} from '@utils/file';
import Files from './files';
import type {WithDatabaseArgs} from '@typings/database/database';
import type FileModel from '@typings/database/models/servers/file';
import type PostModel from '@typings/database/models/servers/post';
import type SystemModel from '@typings/database/models/servers/system';
const enhance = withObservables(['post'], ({database, post}: {post: PostModel} & WithDatabaseArgs) => {
type EnhanceProps = WithDatabaseArgs & {
post: PostModel;
}
const filesLocalPathValidation = async (files: FileModel[], authorId: string) => {
const filesInfo: FileInfo[] = [];
for await (const f of files) {
const info = f.toFileInfo(authorId);
if (info.localPath) {
const exists = await fileExists(info.localPath);
if (!exists) {
info.localPath = '';
}
}
filesInfo.push(info);
}
return filesInfo;
};
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
const config = database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG);
const enableMobileFileDownload = config.pipe(
switchMap(({value}: {value: ClientConfig}) => of$(value.EnableMobileFileDownload !== 'false')),
@@ -32,11 +54,15 @@ const enhance = withObservables(['post'], ({database, post}: {post: PostModel} &
map(([download, compliance]) => compliance || download),
);
const filesInfo = post.files.observeWithColumns(['local_path']).pipe(
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
);
return {
authorId: of$(post.userId),
canDownloadFiles,
postId: of$(post.id),
publicLinkEnabled,
filesInfo,
};
});

View File

@@ -89,13 +89,14 @@ const VideoFile = ({
// library
const publicUri = await fetchPublicLink(serverUrl, data.id!);
if (('link') in publicUri) {
const {uri, height, width} = await getThumbnailAsync(publicUri.link, {time: 2000});
const {uri, height, width} = await getThumbnailAsync(data.localPath || publicUri.link, {time: 2000});
data.mini_preview = uri;
data.height = height;
data.width = width;
updateLocalFile(serverUrl, data);
if (mounted.current) {
setVideo(data);
setFailed(false);
}
}
}

View File

@@ -18,12 +18,11 @@ import Files from './files';
import Message from './message';
import Reactions from './reactions';
import type FileModel from '@typings/database/models/servers/file';
import type PostModel from '@typings/database/models/servers/post';
type BodyProps = {
appsEnabled: boolean;
files: FileModel[];
filesCount: number;
hasReactions: boolean;
highlight: boolean;
highlightReplyBar: boolean;
@@ -73,7 +72,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const Body = ({
appsEnabled, files, hasReactions, highlight, highlightReplyBar,
appsEnabled, filesCount, hasReactions, highlight, highlightReplyBar,
isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAddChannelMember,
location, post, showAddReaction, theme,
}: BodyProps) => {
@@ -166,10 +165,9 @@ const Body = ({
theme={theme}
/>
}
{files.length > 0 &&
{filesCount > 0 &&
<Files
failed={post.props?.failed}
files={files}
layoutWidth={layoutWidth}
location={location}
post={post}

View File

@@ -143,7 +143,7 @@ const withPost = withObservables(
appsEnabled: of$(appsEnabled(partialConfig)),
canDelete,
differentThreadSequence: of$(differentThreadSequence),
files: post.files.observe(),
filesCount: post.files.observeCount(),
hasReplies,
highlightReplyBar,
isConsecutivePost,

View File

@@ -25,7 +25,6 @@ import Header from './header';
import PreHeader from './pre_header';
import SystemMessage from './system_message';
import type FileModel from '@typings/database/models/servers/file';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
@@ -34,7 +33,7 @@ type PostProps = {
canDelete: boolean;
currentUser: UserModel;
differentThreadSequence: boolean;
files: FileModel[];
filesCount: number;
hasReplies: boolean;
highlight?: boolean;
highlightPinnedOrSaved?: boolean;
@@ -96,7 +95,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const Post = ({
appsEnabled, canDelete, currentUser, differentThreadSequence, files, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
appsEnabled, canDelete, currentUser, differentThreadSequence, filesCount, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember,
location, post, reactionsCount, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
testID, previousPost,
@@ -247,7 +246,7 @@ const Post = ({
body = (
<Body
appsEnabled={appsEnabled}
files={files}
filesCount={filesCount}
hasReactions={reactionsCount > 0}
highlight={Boolean(highlightedStyle)}
highlightReplyBar={highlightReplyBar}

View File

@@ -112,7 +112,7 @@ export const transformFileRecord = ({action, database, value}: TransformerArgs):
file.width = raw?.width || record?.width || 0;
file.height = raw?.height || record?.height || 0;
file.imageThumbnail = raw?.mini_preview || record?.imageThumbnail || '';
file.localPath = raw?.localPath ?? '';
file.localPath = raw?.localPath || record?.localPath || '';
};
return prepareBaseRecord({

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import type {Database} from '@nozbe/watermelondb';
import type FileModel from '@typings/database/models/servers/file';
const {SERVER: {FILE}} = MM_TABLES;
export const getFileById = async (database: Database, fileId: string) => {
try {
const record = (await database.get<FileModel>(FILE).find(fileId));
return record;
} catch {
return undefined;
}
};

View File

@@ -29,6 +29,7 @@ type Props = {
action: GalleryAction;
item: GalleryItemType;
setAction: (action: GalleryAction) => void;
onDownloadSuccess?: (path: string) => void;
}
const styles = StyleSheet.create({
@@ -62,7 +63,7 @@ const styles = StyleSheet.create({
},
});
const DownloadWithAction = ({action, item, setAction}: Props) => {
const DownloadWithAction = ({action, item, onDownloadSuccess, setAction}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const [showToast, setShowToast] = useState<boolean|undefined>();
@@ -129,10 +130,18 @@ const DownloadWithAction = ({action, item, setAction}: Props) => {
}
};
const externalAction = async (response: ClientResponse) => {
if (response.data?.path && onDownloadSuccess) {
onDownloadSuccess(response.data.path as string);
}
setShowToast(false);
};
const openFile = async (response: ClientResponse) => {
if (mounted.current) {
if (response.data?.path) {
const path = response.data.path as string;
onDownloadSuccess?.(path);
FileViewer.open(path, {
displayName: item.name,
showAppsSuggestions: true,
@@ -187,6 +196,7 @@ const DownloadWithAction = ({action, item, setAction}: Props) => {
const save = async (response: ClientResponse) => {
if (response.data?.path) {
const path = response.data.path as string;
onDownloadSuccess?.(path);
const hasPermission = await hasWriteStoragePermission(intl);
if (hasPermission) {
@@ -206,6 +216,7 @@ const DownloadWithAction = ({action, item, setAction}: Props) => {
if (mounted.current) {
if (response.data?.path) {
const path = response.data.path as string;
onDownloadSuccess?.(path);
Share.open({
message: '',
title: '',
@@ -224,7 +235,7 @@ const DownloadWithAction = ({action, item, setAction}: Props) => {
const path = getLocalFilePathFromFile(serverUrl, galleryItemToFileInfo(item));
if (path) {
const exists = await fileExists(path);
let actionToExecute: (request: ClientResponse) => Promise<void>;
let actionToExecute: (response: ClientResponse) => Promise<void>;
switch (action) {
case 'sharing':
actionToExecute = shareFile;
@@ -232,6 +243,9 @@ const DownloadWithAction = ({action, item, setAction}: Props) => {
case 'opening':
actionToExecute = openFile;
break;
case 'external':
actionToExecute = externalAction;
break;
default:
actionToExecute = save;
break;

View File

@@ -2,16 +2,22 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Platform, StyleSheet, useWindowDimensions} from 'react-native';
import {useIntl} from 'react-intl';
import {Alert, 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, {LoadError, OnPlaybackRateData} from 'react-native-video';
import Video, {OnPlaybackRateData} from 'react-native-video';
import {updateLocalFilePath} from '@actions/local/file';
import CompassIcon from '@components/compass_icon';
import {Events} from '@constants';
import {GALLERY_FOOTER_HEIGHT, VIDEO_INSET} from '@constants/gallery';
import {useServerUrl} from '@context/server';
import {changeOpacity} from '@utils/theme';
import {ImageRendererProps} from '../image_renderer';
import DownloadWithAction from '../footer/download_with_action';
import type {ImageRendererProps} from '../image_renderer';
interface VideoRendererProps extends ImageRendererProps {
index: number;
@@ -50,15 +56,24 @@ 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 [paused, setPaused] = useState(!(initialIndex === index));
const [videoReady, setVideoReady] = useState(false);
const source = useMemo(() => ({uri: item.uri}), [item.uri]);
const [videoUri, setVideoUri] = useState(item.uri);
const [downloading, setDownloading] = useState(false);
const source = useMemo(() => ({uri: videoUri}), [videoUri]);
const setFullscreen = (value: boolean) => {
fullscreen.value = value;
};
const onDownloadSuccess = (path: string) => {
setVideoUri(path);
updateLocalFilePath(serverUrl, item.id, path);
};
const onEnd = useCallback(() => {
setFullscreen(false);
onShouldHideControls(true);
@@ -66,11 +81,18 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
videoRef.current?.dismissFullscreenPlayer();
}, [onShouldHideControls]);
const onError = useCallback((error: LoadError) => {
// eslint-disable-next-line no-console
console.log(
'Error loading, figure out what to do here... give the option to download?',
error,
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'}),
}],
);
}, []);
@@ -100,6 +122,13 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
setVideoReady(true);
}, []);
const setGalleryAction = useCallback((action: GalleryAction) => {
DeviceEventEmitter.emit(Events.GALLERY_ACTIONS, action);
if (action === 'none') {
setDownloading(false);
}
}, []);
const animatedStyle = useAnimatedStyle(() => {
let w = width;
let h = height - (VIDEO_INSET + GALLERY_FOOTER_HEIGHT + bottom);
@@ -156,6 +185,14 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
/>
</Animated.View>
}
{downloading &&
<DownloadWithAction
action='external'
setAction={setGalleryAction}
onDownloadSuccess={onDownloadSuccess}
item={item}
/>
}
</>
);
};

View File

@@ -483,5 +483,8 @@
"user.settings.general.nickname": "Nickname",
"user.settings.general.position": "Position",
"user.settings.general.username": "Username",
"video.download": "Download video",
"video.failed_description": "An error occurred while trying to play the video.\n",
"video.failed_title": "Video playback failed",
"your.servers": "Your servers"
}

View File

@@ -1,10 +1,11 @@
--- SessionDelegate.swifts 2021-08-10 16:26:55.000000000 -0400
+++ SessionDelegate.swift 2021-08-10 16:27:33.000000000 -0400
@@ -286,6 +286,15 @@
--- SessionDelegate.swift.orig 2022-03-12 17:34:07.000000000 -0300
+++ SessionDelegate.swift 2022-03-16 15:51:22.000000000 -0300
@@ -292,6 +292,16 @@
return
}
+ let sizeString = ((downloadTask.response as? HTTPURLResponse)?.allHeaderFields["X-Uncompressed-Content-Length"]) as? String
+ let allHeaders = (downloadRequest.response)?.allHeaderFields as? NSDictionary ?? NSDictionary()
+ let sizeString = (allHeaders["X-Uncompressed-Content-Length"] ?? allHeaders["x-uncompressed-content-length"]) as? String
+ if (sizeString != nil) {
+ let size = Int64(sizeString!)
+ downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten,

View File

@@ -69,4 +69,4 @@ type GalleryItemType = {
postId?: string;
};
type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening';
type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening' | 'external';