forked from Ivasoft/mattermost-mobile
[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:
@@ -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};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
18
app/queries/servers/file.ts
Normal file
18
app/queries/servers/file.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
types/screens/gallery.d.ts
vendored
2
types/screens/gallery.d.ts
vendored
@@ -69,4 +69,4 @@ type GalleryItemType = {
|
||||
postId?: string;
|
||||
};
|
||||
|
||||
type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening';
|
||||
type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening' | 'external';
|
||||
|
||||
Reference in New Issue
Block a user