feat: add media auto-download (#5076)
* feat: media auto-download view * media auto download view completed and saving the settings in mmkv * audio download preference * audio auto download when the user who sent the audio is the same logged on mobile * creation of isAutoDownloadEnabled, evaluate hist hook, Image Full Size preload done * minor tweak audio show play button after download * refactor audioFile to handleMediaDownload and fixed the audio download * desestructured params to download too * image download and autoDownload, algo fix the formatAttachmentUrl to show the image from local * add the possibility to cancel image download and clear local images * refactor blur component * video download and auto download, also keeped the behavior to download unsuportted videos to the gallery * add the possibility to start downloading a video, then exit the room, back again to room and cancel the video previously downloading * remove the custom hook for autoDownload * remove blurcomponent, fix the blur style in image.tsx, minor tweak video function name * send messageId to video * introducing the reducer to keep the downloads in progress * create a media download selector * remove all the redux stuff and do the same as file upload * video download behavior * done for image and audio * fix the try catch download media * clean up * image container uiKit * fix lint * change rn-fetch-blob to expo-filesystem * add pt-br * pass the correct message id when there is an attachment on reply * refactor some changes requested * fix audio and move the netInfo from autoDownloadPreference to redux * variable isAutoDownloadEnable name and handleMediaDownload getExtension * message/Image refactored, change the component to show the image from FastImage to Image * refactor handleMediaDownload and deleteMedia * minor tweak * refactor audio * refactor video * fix the type on the messagesView(the view of files) * minor tweak * fix the name of searchMediaFIleAsync's result * minor tweak, add the default behavior, add the OFF as label * minor tweaks * verify if the media auto download exists on settings view * fix media auto download view layout and minor tweak wifi * avoid auto download from reply * minor tweak at comment * tweak list.section * change the name to netInfoState and Local_document_directory * remove mediaType and refactor audio and image * separate blurview * thumbnail video and video behavior * add Audio to i18n and minor tweak * set the blur as always dark and add the possibility to overlay * don't need to controle the filepath in the view * fix the loading in image and video at begin * save the file with a similar filename as expected * removed the necessity of messageId or id * minor tweak * switch useLayoutEffect to useEffect * avoid onpress do some edge case because of cached at video * minor tweak * tweak at audio comment extension * minor tweak type userpreferences * remove test id from mediaAutoDownloadView * change action's name to SET_NET_INFO_STATE * caching and deleting video's thumbnails * remove generate thumbnail * minor tweak in image * update camera-roll and save the file from local url * remove local_cache_directory and deleteThumbnail * update blur to fix error on android * fix blur is hiding the file description * avoid download unsupported video * return void when it is loading the audio
This commit is contained in:
parent
278ed91f9a
commit
c9f4ca1197
|
@ -46,7 +46,8 @@ export const APP = createRequestTypes('APP', [
|
|||
'INIT',
|
||||
'INIT_LOCAL_SETTINGS',
|
||||
'SET_MASTER_DETAIL',
|
||||
'SET_NOTIFICATION_PRESENCE_CAP'
|
||||
'SET_NOTIFICATION_PRESENCE_CAP',
|
||||
'SET_NET_INFO_STATE'
|
||||
]);
|
||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Action } from 'redux';
|
||||
import { NetInfoStateType } from '@react-native-community/netinfo';
|
||||
|
||||
import { RootEnum } from '../definitions';
|
||||
import { APP } from './actionsTypes';
|
||||
|
@ -16,7 +17,11 @@ interface ISetNotificationPresenceCap extends Action {
|
|||
show: boolean;
|
||||
}
|
||||
|
||||
export type TActionApp = IAppStart & ISetMasterDetail & ISetNotificationPresenceCap;
|
||||
interface ISetNetInfoState extends Action {
|
||||
netInfoState: NetInfoStateType;
|
||||
}
|
||||
|
||||
export type TActionApp = IAppStart & ISetMasterDetail & ISetNotificationPresenceCap & ISetNetInfoState;
|
||||
|
||||
interface Params {
|
||||
root: RootEnum;
|
||||
|
@ -62,3 +67,10 @@ export function setNotificationPresenceCap(show: boolean): ISetNotificationPrese
|
|||
show
|
||||
};
|
||||
}
|
||||
|
||||
export function setNetInfoState(netInfoState: NetInfoStateType): ISetNetInfoState {
|
||||
return {
|
||||
type: APP.SET_NET_INFO_STATE,
|
||||
netInfoState
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,10 +3,17 @@ import { Image } from 'react-native';
|
|||
import { FastImageProps } from 'react-native-fast-image';
|
||||
|
||||
import { types } from './types';
|
||||
import { LOCAL_DOCUMENT_DIRECTORY } from '../../lib/methods/handleMediaDownload';
|
||||
|
||||
export const ImageComponent = (type?: string): React.ComponentType<Partial<Image> | FastImageProps> => {
|
||||
export function ImageComponent({
|
||||
type,
|
||||
uri
|
||||
}: {
|
||||
type?: string;
|
||||
uri: string;
|
||||
}): React.ComponentType<Partial<Image> | FastImageProps> {
|
||||
let Component;
|
||||
if (type === types.REACT_NATIVE_IMAGE) {
|
||||
if (type === types.REACT_NATIVE_IMAGE || (LOCAL_DOCUMENT_DIRECTORY && uri.startsWith(LOCAL_DOCUMENT_DIRECTORY))) {
|
||||
const { Image } = require('react-native');
|
||||
Component = Image;
|
||||
} else {
|
||||
|
|
|
@ -109,7 +109,7 @@ export const ImageViewer = ({ uri = '', imageComponentType, width, height, ...pr
|
|||
|
||||
const gesture = Gesture.Simultaneous(pinchGesture, panGesture, doubleTapGesture);
|
||||
|
||||
const Component = ImageComponent(imageComponentType);
|
||||
const Component = ImageComponent({ type: imageComponentType, uri });
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme
|
|||
};
|
||||
|
||||
const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, id }: IMessageAttachments) => {
|
||||
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, author }: IMessageAttachments) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
if (!attachments || attachments.length === 0) {
|
||||
|
@ -71,6 +71,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
|||
getCustomEmoji={getCustomEmoji}
|
||||
style={style}
|
||||
isReply={isReply}
|
||||
author={author}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
|||
isReply={isReply}
|
||||
style={style}
|
||||
theme={theme}
|
||||
messageId={id}
|
||||
author={author}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -111,16 +112,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Reply
|
||||
key={index}
|
||||
index={index}
|
||||
attachment={file}
|
||||
timeFormat={timeFormat}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
messageId={id}
|
||||
/>
|
||||
);
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />;
|
||||
});
|
||||
return <>{attachmentsElements}</>;
|
||||
},
|
||||
|
|
|
@ -17,18 +17,19 @@ import MessageContext from './Context';
|
|||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { IAttachment } from '../../definitions';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { downloadAudioFile } from '../../lib/methods/audioFile';
|
||||
import { IAttachment, IUserMessage } from '../../definitions';
|
||||
import { TSupportedThemes, useTheme } from '../../theme';
|
||||
import { downloadMediaFile, getMediaCache } from '../../lib/methods/handleMediaDownload';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { PAUSE_AUDIO } from './constants';
|
||||
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
|
||||
|
||||
interface IButton {
|
||||
loading: boolean;
|
||||
paused: boolean;
|
||||
theme: TSupportedThemes;
|
||||
disabled?: boolean;
|
||||
onPress: () => void;
|
||||
cached: boolean;
|
||||
}
|
||||
|
||||
interface IMessageAudioProps {
|
||||
|
@ -38,7 +39,7 @@ interface IMessageAudioProps {
|
|||
theme: TSupportedThemes;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
scale?: number;
|
||||
messageId: string;
|
||||
author?: IUserMessage;
|
||||
}
|
||||
|
||||
interface IMessageAudioState {
|
||||
|
@ -46,6 +47,7 @@ interface IMessageAudioState {
|
|||
currentTime: number;
|
||||
duration: number;
|
||||
paused: boolean;
|
||||
cached: boolean;
|
||||
}
|
||||
|
||||
const mode = {
|
||||
|
@ -90,25 +92,29 @@ const formatTime = (seconds: number) => moment.utc(seconds * 1000).format('mm:ss
|
|||
|
||||
const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
|
||||
|
||||
const Button = React.memo(({ loading, paused, onPress, disabled, theme }: IButton) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} />
|
||||
) : (
|
||||
<CustomIcon
|
||||
name={paused ? 'play-filled' : 'pause-filled'}
|
||||
size={36}
|
||||
color={disabled ? themes[theme].tintDisabled : themes[theme].tintColor}
|
||||
/>
|
||||
)}
|
||||
</Touchable>
|
||||
));
|
||||
const Button = React.memo(({ loading, paused, onPress, disabled, cached }: IButton) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
let customIconName: 'arrow-down-circle' | 'play-filled' | 'pause-filled' = 'arrow-down-circle';
|
||||
if (cached) {
|
||||
customIconName = paused ? 'play-filled' : 'pause-filled';
|
||||
}
|
||||
return (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} />
|
||||
) : (
|
||||
<CustomIcon name={customIconName} size={36} color={disabled ? colors.tintDisabled : colors.tintColor} />
|
||||
)}
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
|
||||
Button.displayName = 'MessageAudioButton';
|
||||
|
||||
|
@ -119,10 +125,11 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
constructor(props: IMessageAudioProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
loading: true,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
paused: true
|
||||
paused: true,
|
||||
cached: false
|
||||
};
|
||||
|
||||
this.sound = new Audio.Sound();
|
||||
|
@ -135,29 +142,26 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { file, messageId } = this.props;
|
||||
// @ts-ignore can't use declare to type this
|
||||
const { baseUrl, user } = this.context;
|
||||
|
||||
let url = file.audio_url;
|
||||
if (url && !url.startsWith('http')) {
|
||||
url = `${baseUrl}${file.audio_url}`;
|
||||
const { file, isReply } = this.props;
|
||||
const cachedAudioResult = await getMediaCache({
|
||||
type: 'audio',
|
||||
mimeType: file.audio_type,
|
||||
urlToCache: this.getUrl()
|
||||
});
|
||||
if (cachedAudioResult?.exists) {
|
||||
await this.sound.loadAsync({ uri: cachedAudioResult.uri });
|
||||
this.setState({ loading: false, cached: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
if (url) {
|
||||
const audio = await downloadAudioFile(`${url}?rc_uid=${user.id}&rc_token=${user.token}`, url, messageId);
|
||||
await this.sound.loadAsync({ uri: audio });
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
if (isReply) {
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
this.setState({ loading: false });
|
||||
await this.handleAutoDownload();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: IMessageAudioProps, nextState: IMessageAudioState) {
|
||||
const { currentTime, duration, paused, loading } = this.state;
|
||||
const { currentTime, duration, paused, loading, cached } = this.state;
|
||||
const { file, theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
|
@ -177,6 +181,9 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.cached !== cached) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -198,6 +205,39 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
}
|
||||
}
|
||||
|
||||
getUrl = () => {
|
||||
const { file } = this.props;
|
||||
// @ts-ignore can't use declare to type this
|
||||
const { baseUrl } = this.context;
|
||||
|
||||
let url = file.audio_url;
|
||||
if (url && !url.startsWith('http')) {
|
||||
url = `${baseUrl}${file.audio_url}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
handleAutoDownload = async () => {
|
||||
const { author } = this.props;
|
||||
// @ts-ignore can't use declare to type this
|
||||
const { user } = this.context;
|
||||
const url = this.getUrl();
|
||||
try {
|
||||
if (url) {
|
||||
const isCurrentUserAuthor = author?._id === user.id;
|
||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('audioPreferenceDownload');
|
||||
if (isAutoDownloadEnabled || isCurrentUserAuthor) {
|
||||
await this.handleDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: false, cached: false });
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
onPlaybackStatusUpdate = (status: AVPlaybackStatus) => {
|
||||
if (status) {
|
||||
this.onLoad(status);
|
||||
|
@ -247,6 +287,39 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
this.setState({ paused: !paused }, this.playPause);
|
||||
};
|
||||
|
||||
handleDownload = async () => {
|
||||
const { file } = this.props;
|
||||
// @ts-ignore can't use declare to type this
|
||||
const { user } = this.context;
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
const url = this.getUrl();
|
||||
if (url) {
|
||||
const audio = await downloadMediaFile({
|
||||
downloadUrl: `${url}?rc_uid=${user.id}&rc_token=${user.token}`,
|
||||
type: 'audio',
|
||||
mimeType: file.audio_type
|
||||
});
|
||||
await this.sound.loadAsync({ uri: audio });
|
||||
this.setState({ loading: false, cached: true });
|
||||
}
|
||||
} catch {
|
||||
this.setState({ loading: false, cached: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
const { cached, loading } = this.state;
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (cached) {
|
||||
this.togglePlayPause();
|
||||
return;
|
||||
}
|
||||
this.handleDownload();
|
||||
};
|
||||
|
||||
playPause = async () => {
|
||||
const { paused } = this.state;
|
||||
try {
|
||||
|
@ -274,7 +347,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
};
|
||||
|
||||
render() {
|
||||
const { loading, paused, currentTime, duration } = this.state;
|
||||
const { loading, paused, currentTime, duration, cached } = this.state;
|
||||
const { file, getCustomEmoji, theme, scale, isReply, style } = this.props;
|
||||
const { description } = file;
|
||||
// @ts-ignore can't use declare to type this
|
||||
|
@ -306,7 +379,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
|
||||
]}
|
||||
>
|
||||
<Button disabled={isReply} loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Button disabled={isReply} loading={loading} paused={paused} cached={cached} onPress={this.onPress} />
|
||||
<Slider
|
||||
disabled={isReply}
|
||||
style={styles.slider}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import { StyleProp, View, ViewStyle } from 'react-native';
|
||||
import { BlurView } from '@react-native-community/blur';
|
||||
|
||||
import styles from '../../styles';
|
||||
import { useTheme } from '../../../../theme';
|
||||
import RCActivityIndicator from '../../../ActivityIndicator';
|
||||
import { CustomIcon, TIconsName } from '../../../CustomIcon';
|
||||
|
||||
const BlurComponent = ({
|
||||
loading = false,
|
||||
style = {},
|
||||
iconName,
|
||||
showOverlay = false
|
||||
}: {
|
||||
loading: boolean;
|
||||
style: StyleProp<ViewStyle>;
|
||||
iconName: TIconsName;
|
||||
showOverlay?: boolean;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!showOverlay ? (
|
||||
<BlurView style={[style, styles.blurView]} blurType={'dark'} blurAmount={2} />
|
||||
) : (
|
||||
<View style={[style, styles.blurView, { backgroundColor: colors.overlayColor }]} />
|
||||
)}
|
||||
<View style={[style, styles.blurIndicator]}>
|
||||
{loading ? <RCActivityIndicator size={54} /> : <CustomIcon color={colors.buttonText} name={iconName} size={54} />}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlurComponent;
|
|
@ -1,25 +1,24 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { StyleProp, TextStyle, View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { dequal } from 'dequal';
|
||||
import { createImageProgress } from 'react-native-image-progress';
|
||||
import * as Progress from 'react-native-progress';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import styles from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import MessageContext from './Context';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { IAttachment } from '../../definitions';
|
||||
import { TSupportedThemes, useTheme } from '../../theme';
|
||||
import { IAttachment, IUserMessage } from '../../definitions';
|
||||
import { useTheme } from '../../theme';
|
||||
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
|
||||
import { cancelDownload, downloadMediaFile, isDownloadActive, getMediaCache } from '../../lib/methods/handleMediaDownload';
|
||||
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
|
||||
import BlurComponent from './Components/BlurComponent';
|
||||
|
||||
interface IMessageButton {
|
||||
children: React.ReactElement;
|
||||
disabled?: boolean;
|
||||
onPress: () => void;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
interface IMessageImage {
|
||||
|
@ -29,71 +28,152 @@ interface IMessageImage {
|
|||
style?: StyleProp<TextStyle>[];
|
||||
isReply?: boolean;
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
author?: IUserMessage;
|
||||
}
|
||||
|
||||
const ImageProgress = createImageProgress(FastImage);
|
||||
const Button = React.memo(({ children, onPress, disabled }: IMessageButton) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Touchable
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={[styles.imageContainer, styles.mustWrapBlur]}
|
||||
background={Touchable.Ripple(colors.bannerBackground)}
|
||||
>
|
||||
{children}
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
|
||||
const Button = React.memo(({ children, onPress, disabled, theme }: IMessageButton) => (
|
||||
<Touchable
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
{children}
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
export const MessageImage = React.memo(({ imgUri, theme }: { imgUri: string; theme: TSupportedThemes }) => (
|
||||
<ImageProgress
|
||||
style={[styles.image, { borderColor: themes[theme].borderColor }]}
|
||||
source={{ uri: encodeURI(imgUri) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
indicator={Progress.Pie}
|
||||
indicatorProps={{
|
||||
color: themes[theme].actionTintColor
|
||||
}}
|
||||
/>
|
||||
));
|
||||
export const MessageImage = React.memo(({ imgUri, cached, loading }: { imgUri: string; cached: boolean; loading: boolean }) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<>
|
||||
<FastImage
|
||||
style={[styles.image, { borderColor: colors.borderColor }]}
|
||||
source={{ uri: encodeURI(imgUri) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{!cached ? (
|
||||
<BlurComponent loading={loading} style={[styles.image, styles.imageBlurContainer]} iconName='arrow-down-circle' />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const ImageContainer = React.memo(
|
||||
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply }: IMessageImage) => {
|
||||
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, author }: IMessageImage) => {
|
||||
const [imageCached, setImageCached] = useState(file);
|
||||
const [cached, setCached] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
const getUrl = (link?: string) => imageUrl || formatAttachmentUrl(link, user.id, user.token, baseUrl);
|
||||
const img = getUrl(file.image_url);
|
||||
// The param file.title_link is the one that point to image with best quality, however we still need to test the imageUrl
|
||||
// And we cannot be certain whether the file.title_link actually exists.
|
||||
const imgUrlToCache = getUrl(imageCached.title_link || imageCached.image_url);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCache = async () => {
|
||||
if (img) {
|
||||
const cachedImageResult = await getMediaCache({
|
||||
type: 'image',
|
||||
mimeType: imageCached.image_type,
|
||||
urlToCache: imgUrlToCache
|
||||
});
|
||||
if (cachedImageResult?.exists) {
|
||||
setImageCached(prev => ({
|
||||
...prev,
|
||||
title_link: cachedImageResult?.uri
|
||||
}));
|
||||
setLoading(false);
|
||||
setCached(true);
|
||||
return;
|
||||
}
|
||||
if (isReply) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (isDownloadActive(imgUrlToCache)) {
|
||||
return;
|
||||
}
|
||||
setLoading(false);
|
||||
await handleAutoDownload();
|
||||
}
|
||||
};
|
||||
handleCache();
|
||||
}, []);
|
||||
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleAutoDownload = async () => {
|
||||
const isCurrentUserAuthor = author?._id === user.id;
|
||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('imagesPreferenceDownload');
|
||||
if (isAutoDownloadEnabled || isCurrentUserAuthor) {
|
||||
await handleDownload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const imageUri = await downloadMediaFile({
|
||||
downloadUrl: imgUrlToCache,
|
||||
type: 'image',
|
||||
mimeType: imageCached.image_type
|
||||
});
|
||||
setImageCached(prev => ({
|
||||
...prev,
|
||||
title_link: imageUri
|
||||
}));
|
||||
setCached(true);
|
||||
} catch (e) {
|
||||
setCached(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPress = () => {
|
||||
if (loading && isDownloadActive(imgUrlToCache)) {
|
||||
cancelDownload(imgUrlToCache);
|
||||
setLoading(false);
|
||||
setCached(false);
|
||||
return;
|
||||
}
|
||||
if (!cached && !loading) {
|
||||
handleDownload();
|
||||
return;
|
||||
}
|
||||
if (!showAttachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
return showAttachment(file);
|
||||
showAttachment(imageCached);
|
||||
};
|
||||
|
||||
if (file.description) {
|
||||
if (imageCached.description) {
|
||||
return (
|
||||
<Button disabled={isReply} theme={theme} onPress={onPress}>
|
||||
<View>
|
||||
<Markdown
|
||||
msg={file.description}
|
||||
style={[isReply && style]}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<MessageImage imgUri={img} theme={theme} />
|
||||
</View>
|
||||
</Button>
|
||||
<View>
|
||||
<Markdown
|
||||
msg={imageCached.description}
|
||||
style={[isReply && style]}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<Button disabled={isReply} onPress={onPress}>
|
||||
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button disabled={isReply} theme={theme} onPress={onPress}>
|
||||
<MessageImage imgUri={img} theme={theme} />
|
||||
<Button disabled={isReply} onPress={onPress}>
|
||||
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -91,7 +91,6 @@ interface IMessageReply {
|
|||
timeFormat?: string;
|
||||
index: number;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
const Title = React.memo(
|
||||
|
@ -198,7 +197,7 @@ const Fields = React.memo(
|
|||
);
|
||||
|
||||
const Reply = React.memo(
|
||||
({ attachment, timeFormat, index, getCustomEmoji, messageId }: IMessageReply) => {
|
||||
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
|
||||
|
@ -257,7 +256,6 @@ const Reply = React.memo(
|
|||
timeFormat={timeFormat}
|
||||
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14, marginBottom: 8 }]}
|
||||
isReply
|
||||
id={messageId}
|
||||
/>
|
||||
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
{loading ? (
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { StyleProp, StyleSheet, TextStyle } from 'react-native';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { StyleProp, StyleSheet, TextStyle, View, Text } from 'react-native';
|
||||
import { dequal } from 'dequal';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import messageStyles from './styles';
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import { themes } from '../../lib/constants';
|
||||
import MessageContext from './Context';
|
||||
import { fileDownload } from './helpers/fileDownload';
|
||||
|
@ -13,10 +14,13 @@ import EventEmitter from '../../lib/methods/helpers/events';
|
|||
import { LISTENER } from '../Toast';
|
||||
import I18n from '../../i18n';
|
||||
import { IAttachment } from '../../definitions/IAttachment';
|
||||
import RCActivityIndicator from '../ActivityIndicator';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { useTheme } from '../../theme';
|
||||
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
|
||||
import { cancelDownload, downloadMediaFile, isDownloadActive, getMediaCache } from '../../lib/methods/handleMediaDownload';
|
||||
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import BlurComponent from './Components/BlurComponent';
|
||||
|
||||
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
|
||||
const isTypeSupported = (type: string) => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||
|
@ -29,6 +33,20 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
cancelContainer: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8
|
||||
},
|
||||
text: {
|
||||
...sharedStyles.textRegular,
|
||||
fontSize: 12
|
||||
},
|
||||
thumbnailImage: {
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -40,30 +58,130 @@ interface IMessageVideo {
|
|||
isReply?: boolean;
|
||||
}
|
||||
|
||||
const CancelIndicator = () => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={styles.cancelContainer}>
|
||||
<Text style={[styles.text, { color: colors.auxiliaryText }]}>{I18n.t('Cancel')}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Wait backend send the thumbnailUrl as prop
|
||||
const Thumbnail = ({ loading, thumbnailUrl, cached }: { loading: boolean; thumbnailUrl?: string; cached: boolean }) => (
|
||||
<>
|
||||
{thumbnailUrl ? <FastImage style={styles.thumbnailImage} source={{ uri: thumbnailUrl }} /> : null}
|
||||
<BlurComponent
|
||||
iconName={cached ? 'play-filled' : 'arrow-down-circle'}
|
||||
loading={loading}
|
||||
style={styles.button}
|
||||
showOverlay={cached}
|
||||
/>
|
||||
{loading ? <CancelIndicator /> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const Video = React.memo(
|
||||
({ file, showAttachment, getCustomEmoji, style, isReply }: IMessageVideo) => {
|
||||
const [videoCached, setVideoCached] = useState(file);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cached, setCached] = useState(false);
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
const video = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVideoSearchAndDownload = async () => {
|
||||
if (video) {
|
||||
const cachedVideoResult = await getMediaCache({
|
||||
type: 'video',
|
||||
mimeType: file.video_type,
|
||||
urlToCache: video
|
||||
});
|
||||
const downloadActive = isDownloadActive(video);
|
||||
if (cachedVideoResult?.exists) {
|
||||
setVideoCached(prev => ({
|
||||
...prev,
|
||||
video_url: cachedVideoResult?.uri
|
||||
}));
|
||||
setLoading(false);
|
||||
setCached(true);
|
||||
if (downloadActive) {
|
||||
cancelDownload(video);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isReply) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await handleAutoDownload();
|
||||
}
|
||||
};
|
||||
handleVideoSearchAndDownload();
|
||||
}, []);
|
||||
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPress = async () => {
|
||||
if (file.video_type && isTypeSupported(file.video_type) && showAttachment) {
|
||||
return showAttachment(file);
|
||||
const handleAutoDownload = async () => {
|
||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
|
||||
if (isAutoDownloadEnabled && file.video_type && isTypeSupported(file.video_type)) {
|
||||
await handleDownload();
|
||||
return;
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const videoUri = await downloadMediaFile({
|
||||
downloadUrl: video,
|
||||
type: 'video',
|
||||
mimeType: file.video_type
|
||||
});
|
||||
setVideoCached(prev => ({
|
||||
...prev,
|
||||
video_url: videoUri
|
||||
}));
|
||||
setCached(true);
|
||||
} catch {
|
||||
setCached(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPress = async () => {
|
||||
if (file.video_type && cached && isTypeSupported(file.video_type) && showAttachment) {
|
||||
showAttachment(videoCached);
|
||||
return;
|
||||
}
|
||||
if (!loading && !cached && file.video_type && isTypeSupported(file.video_type)) {
|
||||
handleDownload();
|
||||
return;
|
||||
}
|
||||
if (loading && !cached) {
|
||||
handleCancelDownload();
|
||||
return;
|
||||
}
|
||||
if (!isIOS && file.video_url) {
|
||||
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||
await downloadVideo(uri);
|
||||
await downloadVideoToGallery(video);
|
||||
return;
|
||||
}
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('Unsupported_format') });
|
||||
};
|
||||
|
||||
const downloadVideo = async (uri: string) => {
|
||||
const handleCancelDownload = () => {
|
||||
if (loading) {
|
||||
cancelDownload(video);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadVideoToGallery = async (uri: string) => {
|
||||
setLoading(true);
|
||||
const fileDownloaded = await fileDownload(uri, file);
|
||||
setLoading(false);
|
||||
|
@ -87,10 +205,10 @@ const Video = React.memo(
|
|||
<Touchable
|
||||
disabled={isReply}
|
||||
onPress={onPress}
|
||||
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
|
||||
style={[styles.button, messageStyles.mustWrapBlur, { backgroundColor: themes[theme].videoBackground }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
{loading ? <RCActivityIndicator /> : <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />}
|
||||
<Thumbnail loading={loading} cached={cached} />
|
||||
</Touchable>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ export interface IMessageAttachments {
|
|||
isReply?: boolean;
|
||||
showAttachment?: (file: IAttachment) => void;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
id: string;
|
||||
author?: IUserMessage;
|
||||
}
|
||||
|
||||
export interface IMessageAvatar {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { isTablet } from '../../lib/methods/helpers';
|
||||
import { isAndroid, isTablet } from '../../lib/methods/helpers';
|
||||
|
||||
export default StyleSheet.create({
|
||||
root: {
|
||||
|
@ -105,6 +105,9 @@ export default StyleSheet.create({
|
|||
borderWidth: 1,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
imageBlurContainer: {
|
||||
height: '100%'
|
||||
},
|
||||
imagePressed: {
|
||||
opacity: 0.5
|
||||
},
|
||||
|
@ -168,5 +171,22 @@ export default StyleSheet.create({
|
|||
threadDetails: {
|
||||
flex: 1,
|
||||
marginLeft: 12
|
||||
},
|
||||
blurView: {
|
||||
position: 'absolute',
|
||||
borderWidth: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
},
|
||||
blurIndicator: {
|
||||
position: 'absolute',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
mustWrapBlur: {
|
||||
// https://github.com/Kureev/react-native-blur/issues/520#issuecomment-1378339192 Fix BlurView
|
||||
overflow: isAndroid ? 'hidden' : 'visible'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface IAttachment {
|
|||
color?: string;
|
||||
thumb_url?: string;
|
||||
collapsed?: boolean;
|
||||
audio_type?: string;
|
||||
}
|
||||
|
||||
export interface IServerAttachment {
|
||||
|
|
|
@ -726,6 +726,13 @@
|
|||
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
|
||||
"Learn_more": "Learn more",
|
||||
"and_N_more": "and {{count}} more",
|
||||
"Media_auto_download": "Media auto-download",
|
||||
"Images": "Images",
|
||||
"Video": "Video",
|
||||
"Wi_Fi_and_mobile_data":"Wi-Fi and mobile data",
|
||||
"Wi_Fi": "Wi-Fi",
|
||||
"Off": "Off",
|
||||
"Audio": "Audio",
|
||||
"Forward_message": "Forward message",
|
||||
"Person_or_channel": "Person or channel",
|
||||
"Select": "Select",
|
||||
|
|
|
@ -711,16 +711,23 @@
|
|||
"Discard_changes_description": "Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
||||
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace.",
|
||||
"Learn_more": "Saiba mais",
|
||||
"and_N_more": "e mais {{count}}",
|
||||
"Media_auto_download": "Download automático de mídia",
|
||||
"Images": "Imagens",
|
||||
"Video": "Vídeo",
|
||||
"Wi_Fi_and_mobile_data":"Wi-Fi e dados móveis",
|
||||
"Wi_Fi": "Wi-Fi",
|
||||
"Off": "Desativado",
|
||||
"Audio": "Áudio",
|
||||
"decline": "Recusar",
|
||||
"accept": "Aceitar",
|
||||
"Incoming_call_from": "Chamada recebida de",
|
||||
"Call_started": "Chamada Iniciada",
|
||||
"Forward_message": "Encaminhar mensagem",
|
||||
"Person_or_channel": "Pessoa ou canal",
|
||||
"Select": "Selecionar",
|
||||
"Nickname": "Apelido",
|
||||
"Bio": "Biografia",
|
||||
"decline": "Recusar",
|
||||
"accept": "Aceitar",
|
||||
"Incoming_call_from": "Chamada recebida de",
|
||||
"Call_started": "Chamada Iniciada",
|
||||
"Learn_more": "Saiba mais",
|
||||
"and_N_more": "e mais {{count}}",
|
||||
"Message_has_been_shared":"Menssagem foi compartilhada"
|
||||
}
|
|
@ -99,6 +99,7 @@ export const colors = {
|
|||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#1F2329',
|
||||
overlayColor: '#1F2329B2',
|
||||
...mentions,
|
||||
...callButtons
|
||||
},
|
||||
|
@ -175,6 +176,7 @@ export const colors = {
|
|||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#FFFFFF',
|
||||
overlayColor: '#1F2329B2',
|
||||
...mentions,
|
||||
...callButtons
|
||||
},
|
||||
|
@ -251,6 +253,7 @@ export const colors = {
|
|||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#FFFFFF',
|
||||
overlayColor: '#1F2329B2',
|
||||
...mentions,
|
||||
...callButtons
|
||||
}
|
||||
|
|
|
@ -11,4 +11,5 @@ export * from './messageTypeLoad';
|
|||
export * from './notifications';
|
||||
export * from './defaultSettings';
|
||||
export * from './tablet';
|
||||
export * from './mediaAutoDownload';
|
||||
export * from './userAgent';
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export type MediaDownloadOption = 'never' | 'wifi_mobile_data' | 'wifi';
|
||||
|
||||
export const IMAGES_PREFERENCE_DOWNLOAD = 'imagesPreferenceDownload';
|
||||
export const VIDEO_PREFERENCE_DOWNLOAD = 'videoPreferenceDownload';
|
||||
export const AUDIO_PREFERENCE_DOWNLOAD = 'audioPreferenceDownload';
|
|
@ -1,59 +0,0 @@
|
|||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
import { sanitizeLikeString } from '../database/utils';
|
||||
import { store } from '../store/auxStore';
|
||||
import log from './helpers/log';
|
||||
|
||||
const DEFAULT_EXTENSION = 'mp3';
|
||||
|
||||
const sanitizeString = (value: string) => sanitizeLikeString(value.substring(value.lastIndexOf('/') + 1));
|
||||
|
||||
const getExtension = (value: string) => {
|
||||
let extension = DEFAULT_EXTENSION;
|
||||
const filename = value.split('/').pop();
|
||||
if (filename?.includes('.')) {
|
||||
extension = value.substring(value.lastIndexOf('.') + 1);
|
||||
}
|
||||
return extension;
|
||||
};
|
||||
|
||||
const ensureDirAsync = async (dir: string, intermediates = true): Promise<void> => {
|
||||
const info = await FileSystem.getInfoAsync(dir);
|
||||
if (info.exists && info.isDirectory) {
|
||||
return;
|
||||
}
|
||||
await FileSystem.makeDirectoryAsync(dir, { intermediates });
|
||||
return ensureDirAsync(dir, intermediates);
|
||||
};
|
||||
|
||||
export const downloadAudioFile = async (url: string, fileUrl: string, messageId: string): Promise<string> => {
|
||||
let path = '';
|
||||
try {
|
||||
const serverUrl = store.getState().server.server;
|
||||
const serverUrlParsed = sanitizeString(serverUrl);
|
||||
const folderPath = `${FileSystem.documentDirectory}audios/${serverUrlParsed}`;
|
||||
const filename = `${messageId}.${getExtension(fileUrl)}`;
|
||||
const filePath = `${folderPath}/${filename}`;
|
||||
await ensureDirAsync(folderPath);
|
||||
const file = await FileSystem.getInfoAsync(filePath);
|
||||
if (!file.exists) {
|
||||
const downloadedFile = await FileSystem.downloadAsync(url, filePath);
|
||||
path = downloadedFile.uri;
|
||||
} else {
|
||||
path = file.uri;
|
||||
}
|
||||
} catch (error) {
|
||||
log(error);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
export const deleteAllAudioFiles = async (serverUrl: string): Promise<void> => {
|
||||
try {
|
||||
const serverUrlParsed = sanitizeString(serverUrl);
|
||||
const path = `${FileSystem.documentDirectory}audios/${serverUrlParsed}`;
|
||||
await FileSystem.deleteAsync(path, { idempotent: true });
|
||||
} catch (error) {
|
||||
log(error);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import { NetInfoStateType } from '@react-native-community/netinfo';
|
||||
|
||||
import {
|
||||
IMAGES_PREFERENCE_DOWNLOAD,
|
||||
AUDIO_PREFERENCE_DOWNLOAD,
|
||||
VIDEO_PREFERENCE_DOWNLOAD,
|
||||
MediaDownloadOption
|
||||
} from '../constants';
|
||||
import userPreferences from './userPreferences';
|
||||
import { store } from '../store/auxStore';
|
||||
|
||||
type TMediaType = typeof IMAGES_PREFERENCE_DOWNLOAD | typeof AUDIO_PREFERENCE_DOWNLOAD | typeof VIDEO_PREFERENCE_DOWNLOAD;
|
||||
|
||||
export const fetchAutoDownloadEnabled = (mediaType: TMediaType) => {
|
||||
const { netInfoState } = store.getState().app;
|
||||
const mediaDownloadPreference = userPreferences.getString(mediaType) as MediaDownloadOption;
|
||||
|
||||
if (mediaDownloadPreference === 'wifi_mobile_data') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mediaDownloadPreference === 'wifi' && netInfoState === NetInfoStateType.wifi) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mediaDownloadPreference === null) {
|
||||
if (mediaType === 'imagesPreferenceDownload') {
|
||||
return true;
|
||||
}
|
||||
if (mediaType === 'audioPreferenceDownload' || mediaType === 'videoPreferenceDownload') {
|
||||
return netInfoState === NetInfoStateType.wifi;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import { getFilename } from './handleMediaDownload';
|
||||
|
||||
describe('Test the getFilename', () => {
|
||||
it('returns the title without changes', () => {
|
||||
const { image_type, image_url, title } = {
|
||||
title: 'help-image.png',
|
||||
image_url: '/file-upload/oTQmb2zRCsYF4pdHv/help-image-url.png',
|
||||
image_type: 'image/png'
|
||||
};
|
||||
|
||||
const filename = getFilename({ type: 'image', mimeType: image_type, title, url: image_url });
|
||||
expect(filename).toBe(title);
|
||||
});
|
||||
|
||||
it("returns the title with correct extension based on image_type when the title's extension is wrong", () => {
|
||||
const { image_type, image_url, title } = {
|
||||
title: 'help-image.MOV',
|
||||
image_url: '/file-upload/oTQmb2zRCsYF4pdHv/help-image-url.MOV',
|
||||
image_type: 'image/png'
|
||||
};
|
||||
|
||||
const filename = getFilename({ type: 'image', mimeType: image_type, title, url: image_url });
|
||||
expect(filename).toBe('help-image.png');
|
||||
});
|
||||
|
||||
it("returns the filename from image_url when there isn't extension at title", () => {
|
||||
const { image_type, image_url, title } = {
|
||||
title: 'help-image',
|
||||
image_url: '/file-upload/oTQmb2zRCsYF4pdHv/help-image-url.png',
|
||||
image_type: 'image/png'
|
||||
};
|
||||
|
||||
const filename = getFilename({ type: 'image', mimeType: image_type, title, url: image_url });
|
||||
expect(filename).toBe('help-image-url.png');
|
||||
});
|
||||
|
||||
it("returns the filename from image_url with correct extension based on image_type when there isn't extension at title and the image_url's extension is wrong", () => {
|
||||
const { image_type, image_url, title } = {
|
||||
title: 'help-image',
|
||||
image_url: '/file-upload/oTQmb2zRCsYF4pdHv/help-image-url.MOV',
|
||||
image_type: 'image/png'
|
||||
};
|
||||
|
||||
const filename = getFilename({ type: 'image', mimeType: image_type, title, url: image_url });
|
||||
expect(filename).toBe('help-image-url.png');
|
||||
});
|
||||
|
||||
it("returns the filename from image_url and based on the image_type when there isn't extension either at title and image_url", () => {
|
||||
const { image_type, image_url, title } = {
|
||||
title: 'help-image',
|
||||
image_url: '/file-upload/oTQmb2zRCsYF4pdHv/help-image-url.png',
|
||||
image_type: 'image/png'
|
||||
};
|
||||
|
||||
const filename = getFilename({ type: 'image', mimeType: image_type, title, url: image_url });
|
||||
expect(filename).toBe('help-image-url.png');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
import * as FileSystem from 'expo-file-system';
|
||||
import * as mime from 'react-native-mime-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { sanitizeLikeString } from '../database/utils';
|
||||
import { store } from '../store/auxStore';
|
||||
import log from './helpers/log';
|
||||
|
||||
export type MediaTypes = 'audio' | 'image' | 'video';
|
||||
|
||||
const defaultType = {
|
||||
audio: 'mp3',
|
||||
image: 'jpg',
|
||||
video: 'mp4'
|
||||
};
|
||||
|
||||
export const LOCAL_DOCUMENT_DIRECTORY = FileSystem.documentDirectory;
|
||||
|
||||
const sanitizeString = (value: string) => {
|
||||
const urlWithoutQueryString = value.split('?')[0];
|
||||
return sanitizeLikeString(urlWithoutQueryString.substring(urlWithoutQueryString.lastIndexOf('/') + 1));
|
||||
};
|
||||
const serverUrlParsedAsPath = (serverURL: string) => `${sanitizeString(serverURL)}/`;
|
||||
|
||||
export const getFilename = ({
|
||||
title,
|
||||
url,
|
||||
type,
|
||||
mimeType
|
||||
}: {
|
||||
title?: string;
|
||||
url?: string;
|
||||
type: MediaTypes;
|
||||
mimeType?: string;
|
||||
}) => {
|
||||
const isTitleTyped = mime.lookup(title);
|
||||
const extension = getExtension(type, mimeType);
|
||||
if (isTitleTyped && title) {
|
||||
if (isTitleTyped === mimeType) {
|
||||
return title;
|
||||
}
|
||||
// removing any character sequence after the last dot
|
||||
const filenameWithoutWrongExtension = title.replace(/\.\w+$/, '');
|
||||
return `${filenameWithoutWrongExtension}.${extension}`;
|
||||
}
|
||||
|
||||
const filenameFromUrl = url?.substring(url.lastIndexOf('/') + 1);
|
||||
const isFileNameFromUrlTyped = mime.lookup(filenameFromUrl);
|
||||
if (isFileNameFromUrlTyped && filenameFromUrl) {
|
||||
if (isFileNameFromUrlTyped === mimeType) {
|
||||
return filenameFromUrl;
|
||||
}
|
||||
// removing any character sequence after the last dot
|
||||
const filenameWithoutWrongExtension = filenameFromUrl.replace(/\.\w+$/, '');
|
||||
return `${filenameWithoutWrongExtension}.${extension}`;
|
||||
}
|
||||
|
||||
return `${filenameFromUrl}.${extension}`;
|
||||
};
|
||||
|
||||
const getExtension = (type: MediaTypes, mimeType?: string) => {
|
||||
if (!mimeType) {
|
||||
return defaultType[type];
|
||||
}
|
||||
// The library is returning mpag instead of mp3 for audio/mpeg
|
||||
if (mimeType === 'audio/mpeg') {
|
||||
return 'mp3';
|
||||
}
|
||||
// Audios sent by Android devices are in the audio/aac format, which cannot be converted to mp3 by iOS.
|
||||
// However, both platforms support the m4a format, so they can maintain the same behavior.
|
||||
if (mimeType === 'audio/aac') {
|
||||
return 'm4a';
|
||||
}
|
||||
// The return of mime.extension('video/quicktime') is .qt,
|
||||
// this format the iOS isn't recognize and can't save on gallery
|
||||
if (mimeType === 'video/quicktime') {
|
||||
return 'mov';
|
||||
}
|
||||
const extension = mime.extension(mimeType);
|
||||
// The mime.extension can return false when there aren't any extension
|
||||
if (!extension) {
|
||||
return defaultType[type];
|
||||
}
|
||||
return extension;
|
||||
};
|
||||
|
||||
const ensureDirAsync = async (dir: string, intermediates = true): Promise<void> => {
|
||||
const info = await FileSystem.getInfoAsync(dir);
|
||||
if (info.exists && info.isDirectory) {
|
||||
return;
|
||||
}
|
||||
await FileSystem.makeDirectoryAsync(dir, { intermediates });
|
||||
return ensureDirAsync(dir, intermediates);
|
||||
};
|
||||
|
||||
const getFilePath = ({ type, mimeType, urlToCache }: { type: MediaTypes; mimeType?: string; urlToCache?: string }) => {
|
||||
if (!urlToCache) {
|
||||
return;
|
||||
}
|
||||
const folderPath = getFolderPath();
|
||||
const fileUrlSanitized = sanitizeString(urlToCache);
|
||||
const filename = `${fileUrlSanitized}.${getExtension(type, mimeType)}`;
|
||||
const filePath = `${folderPath}${filename}`;
|
||||
return filePath;
|
||||
};
|
||||
|
||||
const getFolderPath = () => {
|
||||
const serverUrl = store.getState().server.server;
|
||||
const serverUrlParsed = serverUrlParsedAsPath(serverUrl);
|
||||
const folderPath = `${LOCAL_DOCUMENT_DIRECTORY}${serverUrlParsed}`;
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
export const getFileInfoAsync = async (filePath: string) => {
|
||||
const file = await FileSystem.getInfoAsync(filePath);
|
||||
return file;
|
||||
};
|
||||
|
||||
export const getMediaCache = async ({
|
||||
type,
|
||||
mimeType,
|
||||
urlToCache
|
||||
}: {
|
||||
type: MediaTypes;
|
||||
mimeType?: string;
|
||||
urlToCache?: string;
|
||||
}) => {
|
||||
if (!urlToCache) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const folderPath = getFolderPath();
|
||||
const filePath = getFilePath({ type, mimeType, urlToCache });
|
||||
if (!filePath) {
|
||||
return null;
|
||||
}
|
||||
await ensureDirAsync(folderPath);
|
||||
const file = await getFileInfoAsync(filePath);
|
||||
return file;
|
||||
} catch (error) {
|
||||
log(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMediaFiles = async (serverUrl: string): Promise<void> => {
|
||||
try {
|
||||
const serverUrlParsed = serverUrlParsedAsPath(serverUrl);
|
||||
const path = `${LOCAL_DOCUMENT_DIRECTORY}${serverUrlParsed}`;
|
||||
await FileSystem.deleteAsync(path, { idempotent: true });
|
||||
} catch (error) {
|
||||
log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadQueue: { [index: string]: FileSystem.DownloadResumable } = {};
|
||||
|
||||
export const mediaDownloadKey = (messageUrl: string) => `${sanitizeString(messageUrl)}`;
|
||||
|
||||
export function isDownloadActive(messageUrl: string): boolean {
|
||||
return !!downloadQueue[mediaDownloadKey(messageUrl)];
|
||||
}
|
||||
|
||||
export async function cancelDownload(messageUrl: string): Promise<void> {
|
||||
const downloadKey = mediaDownloadKey(messageUrl);
|
||||
if (!isEmpty(downloadQueue[downloadKey])) {
|
||||
try {
|
||||
await downloadQueue[downloadKey].cancelAsync();
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
delete downloadQueue[downloadKey];
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadMediaFile({
|
||||
type,
|
||||
mimeType,
|
||||
downloadUrl
|
||||
}: {
|
||||
type: MediaTypes;
|
||||
mimeType?: string;
|
||||
downloadUrl: string;
|
||||
}): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const path = getFilePath({ type, mimeType, urlToCache: downloadUrl });
|
||||
if (!path) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
const downloadKey = mediaDownloadKey(downloadUrl);
|
||||
downloadQueue[downloadKey] = FileSystem.createDownloadResumable(downloadUrl, path);
|
||||
const result = await downloadQueue[downloadKey].downloadAsync();
|
||||
if (result?.uri) {
|
||||
return resolve(result.uri);
|
||||
}
|
||||
reject();
|
||||
} catch {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
import { LOCAL_DOCUMENT_DIRECTORY } from '../handleMediaDownload';
|
||||
|
||||
export const formatAttachmentUrl = (attachmentUrl: string | undefined, userId: string, token: string, server: string): string => {
|
||||
if (LOCAL_DOCUMENT_DIRECTORY && attachmentUrl?.startsWith(LOCAL_DOCUMENT_DIRECTORY)) {
|
||||
return attachmentUrl;
|
||||
}
|
||||
if (attachmentUrl && attachmentUrl.startsWith('http')) {
|
||||
if (attachmentUrl.includes('rc_token')) {
|
||||
return encodeURI(attachmentUrl);
|
||||
|
|
|
@ -4,6 +4,7 @@ import createSagaMiddleware from 'redux-saga';
|
|||
import reducers from '../../reducers';
|
||||
import sagas from '../../sagas';
|
||||
import applyAppStateMiddleware from './appStateMiddleware';
|
||||
import applyInternetStateMiddleware from './internetStateMiddleware';
|
||||
|
||||
let sagaMiddleware;
|
||||
let enhancers;
|
||||
|
@ -17,6 +18,7 @@ if (__DEV__) {
|
|||
|
||||
enhancers = compose(
|
||||
applyAppStateMiddleware(),
|
||||
applyInternetStateMiddleware(),
|
||||
applyMiddleware(reduxImmutableStateInvariant),
|
||||
applyMiddleware(sagaMiddleware),
|
||||
Reactotron.createEnhancer()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import NetInfo, { NetInfoState, NetInfoStateType } from '@react-native-community/netinfo';
|
||||
|
||||
import { setNetInfoState } from '../../actions/app';
|
||||
|
||||
export default () =>
|
||||
(createStore: any) =>
|
||||
(...args: any) => {
|
||||
const store = createStore(...args);
|
||||
let currentType: NetInfoStateType | undefined;
|
||||
const handleInternetStateChange = (nextState: NetInfoState) => {
|
||||
if (nextState.type !== currentType) {
|
||||
store.dispatch(setNetInfoState(nextState.type));
|
||||
currentType = nextState.type;
|
||||
}
|
||||
};
|
||||
NetInfo.addEventListener(handleInternetStateChange);
|
||||
return store;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { NetInfoStateType } from '@react-native-community/netinfo';
|
||||
|
||||
import { TActionApp } from '../actions/app';
|
||||
import { RootEnum } from '../definitions';
|
||||
import { APP, APP_STATE } from '../actions/actionsTypes';
|
||||
|
@ -10,6 +12,7 @@ export interface IApp {
|
|||
foreground: boolean;
|
||||
background: boolean;
|
||||
notificationPresenceCap: boolean;
|
||||
netInfoState?: NetInfoStateType | null;
|
||||
}
|
||||
|
||||
export const initialState: IApp = {
|
||||
|
@ -19,7 +22,8 @@ export const initialState: IApp = {
|
|||
ready: false,
|
||||
foreground: true,
|
||||
background: false,
|
||||
notificationPresenceCap: false
|
||||
notificationPresenceCap: false,
|
||||
netInfoState: null
|
||||
};
|
||||
|
||||
export default function app(state = initialState, action: TActionApp): IApp {
|
||||
|
@ -62,6 +66,11 @@ export default function app(state = initialState, action: TActionApp): IApp {
|
|||
...state,
|
||||
notificationPresenceCap: action.show
|
||||
};
|
||||
case APP.SET_NET_INFO_STATE:
|
||||
return {
|
||||
...state,
|
||||
netInfoState: action.netInfoState
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import LanguageView from '../views/LanguageView';
|
|||
import ThemeView from '../views/ThemeView';
|
||||
import DefaultBrowserView from '../views/DefaultBrowserView';
|
||||
import ScreenLockConfigView from '../views/ScreenLockConfigView';
|
||||
import MediaAutoDownloadView from '../views/MediaAutoDownloadView';
|
||||
// Admin Stack
|
||||
import AdminPanelView from '../views/AdminPanelView';
|
||||
// NewMessage Stack
|
||||
|
@ -179,6 +180,7 @@ const SettingsStackNavigator = () => {
|
|||
<SettingsStack.Screen name='LanguageView' component={LanguageView} />
|
||||
<SettingsStack.Screen name='ThemeView' component={ThemeView} />
|
||||
<SettingsStack.Screen name='DefaultBrowserView' component={DefaultBrowserView} />
|
||||
<SettingsStack.Screen name='MediaAutoDownloadView' component={MediaAutoDownloadView} />
|
||||
<SettingsStack.Screen
|
||||
name='ScreenLockConfigView'
|
||||
component={ScreenLockConfigView}
|
||||
|
|
|
@ -50,6 +50,7 @@ import CreateChannelView from '../../views/CreateChannelView';
|
|||
import UserPreferencesView from '../../views/UserPreferencesView';
|
||||
import UserNotificationPrefView from '../../views/UserNotificationPreferencesView';
|
||||
import SecurityPrivacyView from '../../views/SecurityPrivacyView';
|
||||
import MediaAutoDownloadView from '../../views/MediaAutoDownloadView';
|
||||
import E2EEncryptionSecurityView from '../../views/E2EEncryptionSecurityView';
|
||||
// InsideStackNavigator
|
||||
import AttachmentView from '../../views/AttachmentView';
|
||||
|
@ -190,6 +191,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
|||
<ModalStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||
<ModalStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||
<ModalStack.Screen name='SecurityPrivacyView' component={SecurityPrivacyView} />
|
||||
<ModalStack.Screen name='MediaAutoDownloadView' component={MediaAutoDownloadView} />
|
||||
<ModalStack.Screen name='E2EEncryptionSecurityView' component={E2EEncryptionSecurityView} />
|
||||
</ModalStack.Navigator>
|
||||
</ModalContainer>
|
||||
|
|
|
@ -195,6 +195,7 @@ export type ModalStackParamList = {
|
|||
UserPreferencesView: undefined;
|
||||
UserNotificationPrefView: undefined;
|
||||
SecurityPrivacyView: undefined;
|
||||
MediaAutoDownloadView: undefined;
|
||||
E2EEncryptionSecurityView: undefined;
|
||||
};
|
||||
|
||||
|
|
|
@ -209,6 +209,7 @@ export type SettingsStackParamList = {
|
|||
ScreenLockConfigView: undefined;
|
||||
ProfileView: undefined;
|
||||
DisplayPrefsView: undefined;
|
||||
MediaAutoDownloadView: undefined;
|
||||
};
|
||||
|
||||
export type AdminPanelStackParamList = {
|
||||
|
|
|
@ -2,10 +2,8 @@ import { CameraRoll } from '@react-native-camera-roll/camera-roll';
|
|||
import { HeaderBackground, useHeaderHeight } from '@react-navigation/elements';
|
||||
import { StackNavigationOptions } from '@react-navigation/stack';
|
||||
import { ResizeMode, Video } from 'expo-av';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import React from 'react';
|
||||
import { PermissionsAndroid, View, useWindowDimensions } from 'react-native';
|
||||
import * as mime from 'react-native-mime-types';
|
||||
import { PermissionsAndroid, useWindowDimensions, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
@ -24,6 +22,7 @@ import EventEmitter from '../lib/methods/helpers/events';
|
|||
import { getUserSelector } from '../selectors/login';
|
||||
import { TNavigation } from '../stacks/stackType';
|
||||
import { useTheme } from '../theme';
|
||||
import { LOCAL_DOCUMENT_DIRECTORY, getFilename } from '../lib/methods/handleMediaDownload';
|
||||
|
||||
const RenderContent = ({
|
||||
setLoading,
|
||||
|
@ -145,13 +144,13 @@ const AttachmentView = (): React.ReactElement => {
|
|||
|
||||
const handleSave = async () => {
|
||||
const { title_link, image_url, image_type, video_url, video_type } = attachment;
|
||||
const url = title_link || image_url || video_url;
|
||||
// When the attachment is a video, the video_url refers to local file and the title_link to the link
|
||||
const url = video_url || title_link || image_url;
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaAttachment = formatAttachmentUrl(url, user.id, user.token, baseUrl);
|
||||
if (isAndroid) {
|
||||
const rationale = {
|
||||
title: I18n.t('Write_External_Permission'),
|
||||
|
@ -166,16 +165,22 @@ const AttachmentView = (): React.ReactElement => {
|
|||
|
||||
setLoading(true);
|
||||
try {
|
||||
const extension = image_url
|
||||
? `.${mime.extension(image_type) || 'jpg'}`
|
||||
: `.${(video_type === 'video/quicktime' && 'mov') || mime.extension(video_type) || 'mp4'}`;
|
||||
// The return of mime.extension('video/quicktime') is .qt,
|
||||
// this format the iOS isn't recognize and can't save on gallery
|
||||
const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`;
|
||||
const path = `${documentDir + sha256(url) + extension}`;
|
||||
const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment);
|
||||
await CameraRoll.save(path, { album: 'Rocket.Chat' });
|
||||
file.flush();
|
||||
if (LOCAL_DOCUMENT_DIRECTORY && url.startsWith(LOCAL_DOCUMENT_DIRECTORY)) {
|
||||
await CameraRoll.save(url, { album: 'Rocket.Chat' });
|
||||
} else {
|
||||
const mediaAttachment = formatAttachmentUrl(url, user.id, user.token, baseUrl);
|
||||
let filename = '';
|
||||
if (image_url) {
|
||||
filename = getFilename({ title: attachment.title, type: 'image', mimeType: image_type, url });
|
||||
} else {
|
||||
filename = getFilename({ title: attachment.title, type: 'video', mimeType: video_type, url });
|
||||
}
|
||||
const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`;
|
||||
const path = `${documentDir + filename}`;
|
||||
const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment);
|
||||
await CameraRoll.save(path, { album: 'Rocket.Chat' });
|
||||
file.flush();
|
||||
}
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
|
||||
} catch (e) {
|
||||
EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') });
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
|
||||
import { CustomIcon } from '../../containers/CustomIcon';
|
||||
import * as List from '../../containers/List';
|
||||
import I18n from '../../i18n';
|
||||
import { useTheme } from '../../theme';
|
||||
import sharedStyles from '../Styles';
|
||||
import { MediaDownloadOption } from '../../lib/constants';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: { ...sharedStyles.textRegular, fontSize: 16 }
|
||||
});
|
||||
|
||||
type TOPTIONS = { label: string; value: MediaDownloadOption }[];
|
||||
|
||||
const OPTIONS: TOPTIONS = [
|
||||
{
|
||||
label: 'Wi_Fi_and_mobile_data',
|
||||
value: 'wifi_mobile_data'
|
||||
},
|
||||
{
|
||||
label: 'Wi_Fi',
|
||||
value: 'wifi'
|
||||
},
|
||||
{
|
||||
label: 'Never',
|
||||
value: 'never'
|
||||
}
|
||||
];
|
||||
|
||||
interface IBaseParams {
|
||||
value: string;
|
||||
onChangeValue: (value: MediaDownloadOption) => void;
|
||||
}
|
||||
|
||||
const ListPicker = ({
|
||||
value,
|
||||
title,
|
||||
onChangeValue
|
||||
}: {
|
||||
title: string;
|
||||
} & IBaseParams) => {
|
||||
const { showActionSheet, hideActionSheet } = useActionSheet();
|
||||
const { colors } = useTheme();
|
||||
const option = OPTIONS.find(option => option.value === value) || OPTIONS[2];
|
||||
|
||||
const getOptions = (): TActionSheetOptionsItem[] =>
|
||||
OPTIONS.map(i => ({
|
||||
title: I18n.t(i.label, { defaultValue: i.label }),
|
||||
onPress: () => {
|
||||
hideActionSheet();
|
||||
onChangeValue(i.value);
|
||||
},
|
||||
right: option.value === i.value ? () => <CustomIcon name={'check'} size={20} color={colors.tintActive} /> : undefined
|
||||
}));
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
onPress={() => showActionSheet({ options: getOptions() })}
|
||||
right={() => (
|
||||
<Text style={[styles.title, { color: colors.actionTintColor }]}>
|
||||
{/* when picking an option the label should be Never
|
||||
but when showing among the other settings the label should be Off */}
|
||||
{option.label === 'Never' ? I18n.t('Off') : I18n.t(option.label)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPicker;
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useLayoutEffect } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
|
||||
import * as List from '../../containers/List';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import ListPicker from './ListPicker';
|
||||
import { useUserPreferences } from '../../lib/methods/userPreferences';
|
||||
import {
|
||||
AUDIO_PREFERENCE_DOWNLOAD,
|
||||
IMAGES_PREFERENCE_DOWNLOAD,
|
||||
MediaDownloadOption,
|
||||
VIDEO_PREFERENCE_DOWNLOAD
|
||||
} from '../../lib/constants';
|
||||
import i18n from '../../i18n';
|
||||
import { SettingsStackParamList } from '../../stacks/types';
|
||||
|
||||
const MediaAutoDownload = () => {
|
||||
const [imagesPreference, setImagesPreference] = useUserPreferences<MediaDownloadOption>(
|
||||
IMAGES_PREFERENCE_DOWNLOAD,
|
||||
'wifi_mobile_data'
|
||||
);
|
||||
const [videoPreference, setVideoPreference] = useUserPreferences<MediaDownloadOption>(VIDEO_PREFERENCE_DOWNLOAD, 'wifi');
|
||||
const [audioPreference, setAudioPreference] = useUserPreferences<MediaDownloadOption>(AUDIO_PREFERENCE_DOWNLOAD, 'wifi');
|
||||
const navigation = useNavigation<StackNavigationProp<SettingsStackParamList, 'MediaAutoDownloadView'>>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: i18n.t('Media_auto_download')
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<StatusBar />
|
||||
<List.Container>
|
||||
<List.Section>
|
||||
<ListPicker onChangeValue={setImagesPreference} value={imagesPreference} title='Images' />
|
||||
<List.Separator />
|
||||
<ListPicker onChangeValue={setVideoPreference} value={videoPreference} title='Video' />
|
||||
<List.Separator />
|
||||
<ListPicker onChangeValue={setAudioPreference} value={audioPreference} title='Audio' />
|
||||
</List.Section>
|
||||
</List.Container>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaAutoDownload;
|
|
@ -1,12 +1,12 @@
|
|||
export default function (message: { type: string; url: string }) {
|
||||
if (/image/.test(message.type)) {
|
||||
return { image_url: message.url };
|
||||
return { image_url: message.url, image_type: message.type };
|
||||
}
|
||||
if (/audio/.test(message.type)) {
|
||||
return { audio_url: message.url };
|
||||
return { audio_url: message.url, audio_type: message.type };
|
||||
}
|
||||
if (/video/.test(message.type)) {
|
||||
return { video_url: message.url };
|
||||
return { video_url: message.url, video_type: message.type };
|
||||
}
|
||||
return {
|
||||
title_link: message.url,
|
|
@ -10,7 +10,7 @@ import Message from '../../containers/message';
|
|||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||
import I18n from '../../i18n';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import getFileUrlFromMessage from './getFileUrlFromMessage';
|
||||
import getFileUrlAndTypeFromMessage from './getFileUrlAndTypeFromMessage';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TSupportedThemes, withTheme } from '../../theme';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
|
@ -204,7 +204,7 @@ class MessagesView extends React.Component<IMessagesViewProps, IMessagesViewStat
|
|||
{
|
||||
title: item.name,
|
||||
description: item.description,
|
||||
...getFileUrlFromMessage(item)
|
||||
...getFileUrlAndTypeFromMessage(item)
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { APP_STORE_LINK, FDROID_MARKET_LINK, isFDroidBuild, LICENSE_LINK, PLAY_M
|
|||
import database from '../../lib/database';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
import { clearCache } from '../../lib/methods';
|
||||
import { deleteAllAudioFiles } from '../../lib/methods/audioFile';
|
||||
import { deleteMediaFiles } from '../../lib/methods/handleMediaDownload';
|
||||
import { getDeviceModel, getReadableVersion, isAndroid } from '../../lib/methods/helpers';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
|
||||
|
@ -99,7 +99,7 @@ const SettingsView = (): React.ReactElement => {
|
|||
confirmationText: I18n.t('Clear'),
|
||||
onPress: async () => {
|
||||
dispatch(appStart({ root: RootEnum.ROOT_LOADING, text: I18n.t('Clear_cache_loading') }));
|
||||
await deleteAllAudioFiles(server);
|
||||
await deleteMediaFiles(server);
|
||||
await clearCache({ server });
|
||||
await FastImage.clearMemoryCache();
|
||||
await FastImage.clearDiskCache();
|
||||
|
@ -224,6 +224,13 @@ const SettingsView = (): React.ReactElement => {
|
|||
testID='settings-view-theme'
|
||||
/>
|
||||
<List.Separator />
|
||||
<List.Item
|
||||
title='Media_auto_download'
|
||||
showActionIndicator
|
||||
onPress={() => navigateToScreen('MediaAutoDownloadView')}
|
||||
testID='settings-view-media-auto-download'
|
||||
/>
|
||||
<List.Separator />
|
||||
<List.Item
|
||||
title='Security_and_privacy'
|
||||
showActionIndicator
|
||||
|
|
|
@ -61,6 +61,10 @@ describe('Settings screen', () => {
|
|||
await expect(element(by.id('settings-view-security-privacy'))).toExist();
|
||||
});
|
||||
|
||||
it('should have media auto-download', async () => {
|
||||
await expect(element(by.id('settings-view-media-auto-download'))).toExist();
|
||||
});
|
||||
|
||||
it('should have licence', async () => {
|
||||
await expect(element(by.id('settings-view-license'))).toExist();
|
||||
});
|
||||
|
|
|
@ -383,9 +383,9 @@ PODS:
|
|||
- glog
|
||||
- react-native-background-timer (2.4.1):
|
||||
- React-Core
|
||||
- react-native-blur (4.1.0):
|
||||
- react-native-blur (4.3.2):
|
||||
- React-Core
|
||||
- react-native-cameraroll (5.6.0):
|
||||
- react-native-cameraroll (5.7.2):
|
||||
- React-Core
|
||||
- react-native-cookies (6.2.1):
|
||||
- React-Core
|
||||
|
@ -943,8 +943,8 @@ SPEC CHECKSUMS:
|
|||
React-jsinspector: 9885f6f94d231b95a739ef7bb50536fb87ce7539
|
||||
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
|
||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||
react-native-blur: ba2f37268542f8a26d809f48c5162705a3261fc6
|
||||
react-native-cameraroll: 755bcc628148a90a7c9cf3f817a252be3a601bc5
|
||||
react-native-blur: cfdad7b3c01d725ab62a8a729f42ea463998afa2
|
||||
react-native-cameraroll: 134805127580aed23403b8c2cb1548920dd77b3a
|
||||
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
|
||||
react-native-document-picker: f5ec1a712ca2a975c233117f044817bb8393cad4
|
||||
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
|
||||
|
|
|
@ -43,10 +43,10 @@
|
|||
"@hookform/resolvers": "^2.9.10",
|
||||
"@nozbe/watermelondb": "^0.25.5",
|
||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||
"@react-native-camera-roll/camera-roll": "^5.6.0",
|
||||
"@react-native-camera-roll/camera-roll": "^5.7.2",
|
||||
"@react-native-clipboard/clipboard": "^1.8.5",
|
||||
"@react-native-community/art": "^1.2.0",
|
||||
"@react-native-community/blur": "^4.1.0",
|
||||
"@react-native-community/blur": "^4.3.2",
|
||||
"@react-native-community/datetimepicker": "^6.7.5",
|
||||
"@react-native-community/hooks": "3.0.0",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -4628,10 +4628,10 @@
|
|||
dependencies:
|
||||
merge-options "^3.0.4"
|
||||
|
||||
"@react-native-camera-roll/camera-roll@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.6.0.tgz#385082d57d694f3fd5ae386f8b8ce24b0969c5f9"
|
||||
integrity sha512-a/GYwnBTxj1yKWB9m/qy8GzjowSocML8NbLT81wdMh0JzZYXCLze51BR2cb8JNDgRPzA9xe7KpD3j9qQOSOjag==
|
||||
"@react-native-camera-roll/camera-roll@^5.7.2":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz#db11525ae26c8a61630c424aebd323a7c784a921"
|
||||
integrity sha512-s8VAUG1Kvi+tEJkLHObmOJdXAL/uclnXJ/IdnJtx2fCKiWA3Ho0ln9gDQqCYHHHHu+sXk7wovsH/I2/AYy0brg==
|
||||
|
||||
"@react-native-clipboard/clipboard@^1.8.5":
|
||||
version "1.8.5"
|
||||
|
@ -4647,10 +4647,10 @@
|
|||
invariant "^2.2.4"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@react-native-community/blur@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.1.0.tgz#ed1361a569150c2249aae9b734e278fd262b70cd"
|
||||
integrity sha512-esfuAjbAoeysfI3RhmCHlYwlXobXzcsVGZEHgDhVGB88aO9RktY6b13mYbo2FXZ8XnntcccuvXlgckvoIsggWg==
|
||||
"@react-native-community/blur@^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.2.tgz#185a2c7dd03ba168cc95069bc4742e9505fd6c6c"
|
||||
integrity sha512-0ID+pyZKdC4RdgC7HePxUQ6JmsbNrgz03u+6SgqYpmBoK/rE+7JffqIw7IEsfoKitLEcRNLGekIBsfwCqiEkew==
|
||||
|
||||
"@react-native-community/cli-clean@^10.1.1":
|
||||
version "10.1.1"
|
||||
|
|
Loading…
Reference in New Issue