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
3 changes: 0 additions & 3 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,6 @@ const appConfig: ExpoConfig & { extra: AppEnv & { NODE_ENV: AppStage; RELEASE_ID
'react-native-svg',
'@shopify/flash-list',
'react-native-pdf',
'react-native-pdf-thumbnail',
'react-native-blob-util',
'react-native-create-thumbnail',
'jail-monkey',
],
backgroundColor: { red: 0, green: 0, blue: 0, alpha: 0 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ class ShareExtensionViewController: UIViewController {
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)

do {
try? fileManager.removeItem(atPath: persistentURL.path)
try fileManager.copyItem(atPath: tempFilePath, toPath: persistentURL.path)
let key = isImage ? "images" : "files"
if sharedItems[key] == nil {
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ target 'Internxt' do
end

target 'InternxtShareExtension' do
exclude = ["expo-updates", "expo-splash-screen", "expo-dev-client", "react-native-reanimated", "react-native-screens", "react-native-safe-area-context", "react-native-gesture-handler", "react-native-video", "react-native-webview", "react-native-fast-image", "react-native-svg", "@shopify/flash-list", "react-native-pdf", "react-native-pdf-thumbnail", "react-native-create-thumbnail", "jail-monkey"]
exclude = ["expo-updates", "expo-splash-screen", "expo-dev-client", "react-native-reanimated", "react-native-screens", "react-native-safe-area-context", "react-native-gesture-handler", "react-native-video", "react-native-webview", "react-native-fast-image", "react-native-svg", "@shopify/flash-list", "react-native-pdf", "jail-monkey"]
use_expo_modules!(exclude: exclude)

if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,6 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a

PODFILE CHECKSUM: c616d1ada5d84f015b84b48b4ee50b266ab8ecc6
PODFILE CHECKSUM: 1c3110710c3007a8b81ff873b578769e00b42ac1

COCOAPODS: 1.16.2
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"react-native": "0.81.5",
"react-native-blob-util": "^0.24.6",
"react-native-capture-protection": "2.4.0",
"react-native-create-thumbnail": "^2.0.0",
"react-native-create-thumbnail": "^2.2.0",
"react-native-crypto": "^2.2.1",
"react-native-device-info": "^8.4.8",
"react-native-fast-image": "^8.5.11",
Expand Down
372 changes: 372 additions & 0 deletions patches/react-native-create-thumbnail+2.2.0.patch

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { GeneratedThumbnail, imageService } from '@internxt-mobile/services/common';
import { GeneratedThumbnail, generateVideoThumbnail } from '@internxt-mobile/services/common';
import { time } from '@internxt-mobile/services/common/time';
import errorService from '@internxt-mobile/services/ErrorService';
import { fs } from '@internxt-mobile/services/FileSystemService';
import { notifications } from '@internxt-mobile/services/NotificationsService';
import { FileExtension, Thumbnail } from '@internxt-mobile/types/drive/file';
import { RootStackScreenProps } from '@internxt-mobile/types/navigation';
import strings from 'assets/lang/strings';
import { WarningCircle } from 'phosphor-react-native';
import { WarningCircleIcon } from 'phosphor-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { Animated, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
Expand Down Expand Up @@ -57,8 +57,7 @@ export const DrivePreviewScreen: React.FC<RootStackScreenProps<'DrivePreview'>>
VIDEO_PREVIEW_TYPES.has(downloadingFile.data.type as FileExtension) &&
!generatedThumbnail
) {
imageService
.generateVideoThumbnail(downloadingFile.downloadedFilePath)
generateVideoThumbnail(downloadingFile.downloadedFilePath)
.then((generatedThumbnail) => {
setGeneratedThumbnail(generatedThumbnail);
})
Expand Down Expand Up @@ -184,8 +183,8 @@ export const DrivePreviewScreen: React.FC<RootStackScreenProps<'DrivePreview'>>
{error ? (
<View style={tailwind('mt-1')}>
<View style={tailwind('flex flex-row items-center')}>
<WarningCircle weight="fill" size={20} color={tailwind('text-red').color as string} />
<AppText style={tailwind('text-gray-60 text-center text-red ml-1')}>{error}</AppText>
<WarningCircleIcon weight="fill" size={20} color={tailwind('text-red').color as string} />
<AppText style={tailwind('text-center text-red ml-1')}>{error}</AppText>
</View>
{downloadingFile && error !== strings.messages.downloadLimit && (
<AppButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { logger } from '@internxt-mobile/services/common';
import { isThumbnailSupported } from '@internxt-mobile/services/common/media/thumbnail.constants';
import { driveFileService } from '@internxt-mobile/services/drive/file';
import errorService from '@internxt-mobile/services/ErrorService';
import { FileExtension, Thumbnail } from '@internxt-mobile/types/drive/file';
import { Thumbnail } from '@internxt-mobile/types/drive/file';
import { useEffect, useRef, useState } from 'react';

const IMAGE_PREVIEW_TYPES = new Set([FileExtension.PNG, FileExtension.JPG, FileExtension.JPEG, FileExtension.HEIC]);
const VIDEO_PREVIEW_TYPES = new Set([FileExtension.MP4, FileExtension.MOV, FileExtension.AVI]);
const PDF_PREVIEW_TYPES = new Set([FileExtension.PDF]);

interface ThumbnailRegenerationParams {
downloadedFilePath?: string;
fileExtension?: string;
Expand All @@ -19,10 +16,7 @@ interface ThumbnailRegenerationCallbacks {
onSuccess: (thumbnail: Thumbnail) => void;
}

export const canGenerateThumbnail = (fileExtension: string): boolean => {
const extension = fileExtension.toLowerCase() as FileExtension;
return IMAGE_PREVIEW_TYPES.has(extension) || VIDEO_PREVIEW_TYPES.has(extension) || PDF_PREVIEW_TYPES.has(extension);
};
export const canGenerateThumbnail = isThumbnailSupported;

export const shouldRegenerateThumbnail = (params: ThumbnailRegenerationParams): boolean => {
const { downloadedFilePath, fileExtension, hasThumbnails } = params;
Expand Down
156 changes: 12 additions & 144 deletions src/services/common/media/image.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
import { ImageManipulator, SaveFormat } from 'expo-image-manipulator';

import * as RNFS from '@dr.pogodin/react-native-fs';
import fileSystemService, { fs } from '@internxt-mobile/services/FileSystemService';
import { FileExtension } from '@internxt-mobile/types/drive/file';

import * as RNFS from '@dr.pogodin/react-native-fs';
import { isThumbnailSupported } from './thumbnail.constants';
import { generateThumbnail as generateThumbnailShared } from './thumbnail.generation';
export type { GeneratedThumbnail } from './thumbnail.types';

import ReactNativeBlobUtil from 'react-native-blob-util';
import { createThumbnail } from 'react-native-create-thumbnail';
import PdfThumbnail from 'react-native-pdf-thumbnail';
import uuid from 'react-native-uuid';

export type GeneratedThumbnail = {
size: number;
type: string;
width: number;
height: number;
path: string;
};
export const PROFILE_PICTURE_CACHE_KEY = 'PROFILE_PICTURE';
const MAX_THUMBNAIL_WIDTH = 512;
export type ThumbnailGenerateConfig = {
outputPath: string;
quality?: number;
Expand All @@ -27,23 +16,6 @@ export type ThumbnailGenerateConfig = {
};

class ImageService {
private get thumbnailGenerators(): Record<
FileExtension,
(filePath: string, config: ThumbnailGenerateConfig) => Promise<GeneratedThumbnail>
> {
return {
[FileExtension.AVI]: this.generateVideoThumbnail,
[FileExtension.MP4]: this.generateVideoThumbnail,
[FileExtension.MOV]: this.generateVideoThumbnail,
[FileExtension.JPEG]: this.generateImageThumbnail,
[FileExtension.JPG]: this.generateImageThumbnail,
[FileExtension.PNG]: this.generateImageThumbnail,
[FileExtension.HEIC]: this.generateImageThumbnail,
[FileExtension.PDF]: this.generatePdfThumbnail,
};
}
public readonly BASE64_PREFIX = 'data:image/png;base64,';

public async resize({
uri,
width,
Expand All @@ -67,21 +39,10 @@ class ImageService {
}
};

const result = await manipulateAsync(
getRequiredUriFormat(),
[
{
resize: {
width,
height,
},
},
],
{
format: SaveFormat.JPEG,
compress: quality / 100,
},
);
const imageManipulatorContext = ImageManipulator.manipulate(getRequiredUriFormat());
imageManipulatorContext.resize({ width, height });
const imageRef = await imageManipulatorContext.renderAsync();
const result = await imageRef.saveAsync({ format: SaveFormat.JPEG, compress: quality / 100 });

const stat = await fileSystemService.statRNFS(result.uri);
if (outputPath && !(await fileSystemService.exists(outputPath))) {
Expand All @@ -95,10 +56,6 @@ class ImageService {
};
}

public async pathToBase64(uri: string): Promise<string> {
return await ReactNativeBlobUtil.fs.readFile(uri, 'base64');
}

/**
* Cache an image from an URL and stores it using a cacheKey, can be
* retrieved using getCachedImage() method
Expand Down Expand Up @@ -150,102 +107,13 @@ class ImageService {
public async generateThumbnail(
filePath: string,
config: { outputPath: string; quality?: number; extension: string; thumbnailFormat: SaveFormat },
): Promise<GeneratedThumbnail | null> {
const generator = this.thumbnailGenerators[config.extension.toLowerCase() as FileExtension];

if (!generator) {
) {
if (!isThumbnailSupported(config.extension)) {
// eslint-disable-next-line no-console
console.error(`Cannot generate thumbnail for extension ${config.extension}`);

return null;
}
return this.resizeThumbnail(await generator(filePath, config));
}

/**
* Generates a thumbnail for a video file
*/
public generateVideoThumbnail = async (filePath: string): Promise<GeneratedThumbnail> => {
const result = await createThumbnail({
url: fileSystemService.pathToUri(filePath),
dirSize: 100,
});

return {
size: result.size,
type: 'JPEG',
width: result.width,
height: result.height,
path: result.path,
};
};

/**
* Generates a thumbnail for an image
*/
public generateImageThumbnail = async (
filePath: string,
config: ThumbnailGenerateConfig,
): Promise<GeneratedThumbnail> => {
const result = await this.resize({
uri: filePath,
outputPath: config.outputPath,
width: config.width || MAX_THUMBNAIL_WIDTH,
height: config.height,
format: 'JPEG',
quality: 80,
});

return {
size: result.size,
type: 'JPEG',
width: result.width,
height: result.height,
path: result.path,
};
};

/** Generates a thumbnail from a PDF file */
public generatePdfThumbnail = async (
filePath: string,
config: ThumbnailGenerateConfig,
): Promise<GeneratedThumbnail> => {
const neededPath = filePath.startsWith('file:///') ? filePath : `file:///${filePath}`;
const result = await PdfThumbnail.generate(neededPath, 0, config.quality || 80);
// The library has some problems if the URI contains spaces
const outputPath = decodeURI(result.uri);
if (!(await fileSystemService.exists(config.outputPath))) {
await fileSystemService.moveFile(outputPath, config.outputPath);
}

const stat = await fileSystemService.statRNFS(config.outputPath);
return {
path: config.outputPath,
width: result.width,
height: result.height,
size: stat.size,
type: 'JPEG',
};
};

private async resizeThumbnail(originThumbnail: GeneratedThumbnail): Promise<GeneratedThumbnail> {
const destination = fileSystemService.tmpFilePath(uuid.v4().toString());

const result = await this.resize({
uri: originThumbnail.path,
width: MAX_THUMBNAIL_WIDTH,
quality: 80,
format: 'JPEG',
outputPath: destination,
});

return {
width: result.width,
height: result.height,
path: result.path,
size: result.size,
type: 'JPEG',
};
return generateThumbnailShared(filePath, config.extension);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/services/common/media/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './image.service';
export * from './thumbnail.constants';
export * from './thumbnail.types';
export * from './thumbnail.generation';
24 changes: 24 additions & 0 deletions src/services/common/media/thumbnail.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FileExtension } from '@internxt-mobile/types/drive/file';

export const THUMBNAIL_MAX_WIDTH = 512;
export const THUMBNAIL_JPEG_COMPRESS = 0.8;
export const VIDEO_THUMBNAIL_DIR_SIZE = 100;
export const PDF_THUMBNAIL_QUALITY = 80;

export const IMAGE_THUMBNAIL_EXTENSIONS = new Set<string>([
FileExtension.JPG,
FileExtension.JPEG,
FileExtension.PNG,
FileExtension.HEIC,
]);
export const VIDEO_THUMBNAIL_EXTENSIONS = new Set<string>([FileExtension.MP4, FileExtension.MOV, FileExtension.AVI]);
export const PDF_THUMBNAIL_EXTENSIONS = new Set<string>([FileExtension.PDF]);

export const isThumbnailSupported = (extension: string): boolean => {
const extensionLower = extension.toLowerCase();
return (
IMAGE_THUMBNAIL_EXTENSIONS.has(extensionLower) ||
VIDEO_THUMBNAIL_EXTENSIONS.has(extensionLower) ||
PDF_THUMBNAIL_EXTENSIONS.has(extensionLower)
);
};
63 changes: 63 additions & 0 deletions src/services/common/media/thumbnail.generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as RNFS from '@dr.pogodin/react-native-fs';
import { ImageManipulator, SaveFormat } from 'expo-image-manipulator';
import { Platform } from 'react-native';
import { createThumbnail } from 'react-native-create-thumbnail';
import PdfThumbnail from 'react-native-pdf-thumbnail';

import {
IMAGE_THUMBNAIL_EXTENSIONS,
PDF_THUMBNAIL_QUALITY,
THUMBNAIL_JPEG_COMPRESS,
THUMBNAIL_MAX_WIDTH,
VIDEO_THUMBNAIL_DIR_SIZE,
VIDEO_THUMBNAIL_EXTENSIONS,
} from './thumbnail.constants';
import type { GeneratedThumbnail } from './thumbnail.types';

const toFileUri = (path: string): string => (path.startsWith('file://') ? path : `file://${path}`);

const statSize = async (path: string): Promise<number> => Number((await RNFS.stat(path)).size);

const generateImageThumbnailAndroid = async (sourcePath: string): Promise<GeneratedThumbnail> => {
const imageManipulatorContext = ImageManipulator.manipulate(toFileUri(sourcePath));
imageManipulatorContext.resize({ width: THUMBNAIL_MAX_WIDTH });
const imageRef = await imageManipulatorContext.renderAsync();
const result = await imageRef.saveAsync({ format: SaveFormat.JPEG, compress: THUMBNAIL_JPEG_COMPRESS });
imageRef.release();
imageManipulatorContext.release();
const path = result.uri.replace('file://', '');
return { path, width: result.width, height: result.height, size: await statSize(path), type: 'JPEG' };
};

const generateMediaThumbnail = async (sourcePath: string): Promise<GeneratedThumbnail> => {
const result = await createThumbnail({
url: toFileUri(sourcePath),
dirSize: VIDEO_THUMBNAIL_DIR_SIZE,
maxWidth: THUMBNAIL_MAX_WIDTH,
maxHeight: THUMBNAIL_MAX_WIDTH,
});
const path = result.path.replace('file://', '');
return { path, width: result.width, height: result.height, size: await statSize(path), type: 'JPEG' };
};

// iOS uses the patched react-native-create-thumbnail
// instead of expo-image-manipulator: subsampled decode avoids loading the full bitmap
// into memory, preventing jetsam kills in the share extension
export const generateImageThumbnail = async (sourcePath: string): Promise<GeneratedThumbnail> =>
Platform.OS === 'android' ? generateImageThumbnailAndroid(sourcePath) : generateMediaThumbnail(sourcePath);

export const generateVideoThumbnail = (sourcePath: string): Promise<GeneratedThumbnail> =>
generateMediaThumbnail(sourcePath);

export const generatePdfThumbnail = async (sourcePath: string): Promise<GeneratedThumbnail> => {
const result = await PdfThumbnail.generate(toFileUri(sourcePath), 0, PDF_THUMBNAIL_QUALITY);
const path = result.uri.replace('file://', '');
return { path, width: result.width, height: result.height, size: await statSize(path), type: 'JPEG' };
};

export const generateThumbnail = async (sourcePath: string, extension: string): Promise<GeneratedThumbnail> => {
const extensionLower = extension.toLowerCase();
if (IMAGE_THUMBNAIL_EXTENSIONS.has(extensionLower)) return generateImageThumbnail(sourcePath);
if (VIDEO_THUMBNAIL_EXTENSIONS.has(extensionLower)) return generateVideoThumbnail(sourcePath);
return generatePdfThumbnail(sourcePath);
};
Loading
Loading