[NEW] Send multiple attachments (#2162)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
a992c51698
commit
07e9bcb776
|
@ -3,13 +3,8 @@
|
||||||
package="chat.rocket.reactnative">
|
package="chat.rocket.reactnative">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- <uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
|
|
||||||
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
@ -65,6 +60,7 @@
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -15,6 +15,7 @@ public class BasePackageList {
|
||||||
new expo.modules.keepawake.KeepAwakePackage(),
|
new expo.modules.keepawake.KeepAwakePackage(),
|
||||||
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
||||||
new expo.modules.permissions.PermissionsPackage(),
|
new expo.modules.permissions.PermissionsPackage(),
|
||||||
|
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
|
||||||
new expo.modules.webbrowser.WebBrowserPackage()
|
new expo.modules.webbrowser.WebBrowserPackage()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,9 @@ export const themes = {
|
||||||
passcodePrimary: '#2F343D',
|
passcodePrimary: '#2F343D',
|
||||||
passcodeSecondary: '#6C727A',
|
passcodeSecondary: '#6C727A',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#1F2329',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: '#030b1b',
|
backgroundColor: '#030b1b',
|
||||||
|
@ -95,7 +97,9 @@ export const themes = {
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#030b1b',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
},
|
},
|
||||||
black: {
|
black: {
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
|
@ -137,6 +141,8 @@ export const themes = {
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#000000',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useRef, useContext } from 'react';
|
import React, { useRef, useContext, forwardRef } from 'react';
|
||||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import ActionSheet from './ActionSheet';
|
import ActionSheet from './ActionSheet';
|
||||||
|
@ -14,15 +13,11 @@ export const useActionSheet = () => useContext(context);
|
||||||
|
|
||||||
const { Provider, Consumer } = context;
|
const { Provider, Consumer } = context;
|
||||||
|
|
||||||
export const withActionSheet = (Component) => {
|
export const withActionSheet = Component => forwardRef((props, ref) => (
|
||||||
const ConnectedActionSheet = props => (
|
<Consumer>
|
||||||
<Consumer>
|
{contexts => <Component {...props} {...contexts} ref={ref} />}
|
||||||
{contexts => <Component {...props} {...contexts} />}
|
</Consumer>
|
||||||
</Consumer>
|
));
|
||||||
);
|
|
||||||
hoistNonReactStatics(ConnectedActionSheet, Component);
|
|
||||||
return ConnectedActionSheet;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActionSheetProvider = React.memo(({ children }) => {
|
export const ActionSheetProvider = React.memo(({ children }) => {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
|
@ -4,11 +4,22 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { View, StyleSheet } from 'react-native';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { themedHeader } from '../../utils/navigation';
|
import { themedHeader } from '../../utils/navigation';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||||
|
|
||||||
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
|
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
|
||||||
export const headerHeight = isIOS ? 44 : 56;
|
export const headerHeight = isIOS ? 44 : 56;
|
||||||
|
|
||||||
|
export const getHeaderHeight = (isLandscape) => {
|
||||||
|
if (isIOS) {
|
||||||
|
if (isLandscape && !isTablet) {
|
||||||
|
return 32;
|
||||||
|
} else {
|
||||||
|
return 44;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 56;
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
height: headerHeight,
|
height: headerHeight,
|
||||||
|
|
|
@ -36,9 +36,11 @@ export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) =
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const CloseModalButton = React.memo(({ navigation, testID, onPress = () => navigation.pop() }) => (
|
export const CloseModalButton = React.memo(({
|
||||||
|
navigation, testID, onPress = () => navigation.pop(), ...props
|
||||||
|
}) => (
|
||||||
<CustomHeaderButtons left>
|
<CustomHeaderButtons left>
|
||||||
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} />
|
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -57,9 +59,9 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const SaveButton = React.memo(({ onPress, testID }) => (
|
export const SaveButton = React.memo(({ onPress, testID, ...props }) => (
|
||||||
<CustomHeaderButtons>
|
<CustomHeaderButtons>
|
||||||
<Item title='save' iconName='download' onPress={onPress} testID={testID} />
|
<Item title='save' iconName='download' onPress={onPress} testID={testID} {...props} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import { CancelEditingButton, ActionsButton } from './buttons';
|
import { CancelEditingButton, ActionsButton } from './buttons';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
const LeftButtons = React.memo(({
|
const LeftButtons = React.memo(({
|
||||||
theme, showMessageBoxActions, editing, editCancel
|
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
|
||||||
}) => {
|
}) => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||||
}
|
}
|
||||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
if (isActionsEnabled) {
|
||||||
|
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||||
|
}
|
||||||
|
return <View style={styles.buttonsWhitespace} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
LeftButtons.propTypes = {
|
LeftButtons.propTypes = {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
showMessageBoxActions: PropTypes.func.isRequired,
|
showMessageBoxActions: PropTypes.func.isRequired,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
editCancel: PropTypes.func.isRequired
|
editCancel: PropTypes.func.isRequired,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeftButtons;
|
export default LeftButtons;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||||
import RNFetchBlob from 'rn-fetch-blob';
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
@ -113,7 +113,7 @@ export default class extends React.PureComponent {
|
||||||
this.recording = false;
|
this.recording = false;
|
||||||
const filePath = await AudioRecorder.stopRecording();
|
const filePath = await AudioRecorder.stopRecording();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
const data = await RNFetchBlob.fs.stat(decodeURIComponent(filePath));
|
const data = await FileSystem.getInfoAsync(decodeURIComponent(filePath), { size: true });
|
||||||
this.finishRecording(true, filePath, data.size);
|
this.finishRecording(true, filePath, data.size);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
const RightButtons = React.memo(({
|
const RightButtons = React.memo(({
|
||||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions
|
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled
|
||||||
}) => {
|
}) => {
|
||||||
if (showSend) {
|
if (showSend) {
|
||||||
return <SendButton onPress={submit} theme={theme} />;
|
return <SendButton onPress={submit} theme={theme} />;
|
||||||
}
|
}
|
||||||
if (recordAudioMessageEnabled) {
|
if (recordAudioMessageEnabled || isActionsEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AudioButton onPress={recordAudioMessage} theme={theme} />
|
{recordAudioMessageEnabled ? <AudioButton onPress={recordAudioMessage} theme={theme} /> : null}
|
||||||
<ActionsButton onPress={showMessageBoxActions} theme={theme} />
|
{isActionsEnabled ? <ActionsButton onPress={showMessageBoxActions} theme={theme} /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
return <View style={styles.buttonsWhitespace} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
RightButtons.propTypes = {
|
RightButtons.propTypes = {
|
||||||
|
@ -26,7 +28,8 @@ RightButtons.propTypes = {
|
||||||
submit: PropTypes.func.isRequired,
|
submit: PropTypes.func.isRequired,
|
||||||
recordAudioMessage: PropTypes.func.isRequired,
|
recordAudioMessage: PropTypes.func.isRequired,
|
||||||
recordAudioMessageEnabled: PropTypes.bool,
|
recordAudioMessageEnabled: PropTypes.bool,
|
||||||
showMessageBoxActions: PropTypes.func.isRequired
|
showMessageBoxActions: PropTypes.func.isRequired,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RightButtons;
|
export default RightButtons;
|
||||||
|
|
|
@ -1,249 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import {
|
|
||||||
View, Text, StyleSheet, Image, ScrollView, TouchableHighlight
|
|
||||||
} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Modal from 'react-native-modal';
|
|
||||||
import equal from 'deep-equal';
|
|
||||||
|
|
||||||
import TextInput from '../TextInput';
|
|
||||||
import Button from '../Button';
|
|
||||||
import I18n from '../../i18n';
|
|
||||||
import sharedStyles from '../../views/Styles';
|
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
|
||||||
import { themes } from '../../constants/colors';
|
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
|
||||||
import { withTheme } from '../../theme';
|
|
||||||
import { withDimensions } from '../../dimensions';
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
modal: {
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
margin: 0
|
|
||||||
},
|
|
||||||
titleContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingTop: 16
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 14,
|
|
||||||
...sharedStyles.textBold
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
height: 430,
|
|
||||||
flexDirection: 'column'
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
padding: 16
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
height: 150,
|
|
||||||
flex: 1,
|
|
||||||
marginBottom: 16,
|
|
||||||
resizeMode: 'contain'
|
|
||||||
},
|
|
||||||
bigPreview: {
|
|
||||||
height: 250
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: 16
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginBottom: 0
|
|
||||||
},
|
|
||||||
androidButton: {
|
|
||||||
paddingHorizontal: 15,
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 2
|
|
||||||
},
|
|
||||||
androidButtonText: {
|
|
||||||
fontSize: 18,
|
|
||||||
textAlign: 'center'
|
|
||||||
},
|
|
||||||
fileIcon: {
|
|
||||||
margin: 20,
|
|
||||||
flex: 1,
|
|
||||||
textAlign: 'center'
|
|
||||||
},
|
|
||||||
video: {
|
|
||||||
flex: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
height: 150,
|
|
||||||
marginBottom: 6,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
class UploadModal extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
isVisible: PropTypes.bool,
|
|
||||||
file: PropTypes.object,
|
|
||||||
close: PropTypes.func,
|
|
||||||
submit: PropTypes.func,
|
|
||||||
width: PropTypes.number,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
isMasterDetail: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
file: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (!equal(props.file, state.file) && props.file && props.file.path) {
|
|
||||||
return {
|
|
||||||
file: props.file,
|
|
||||||
name: props.file.filename || 'Filename',
|
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
const { name, description, file } = this.state;
|
|
||||||
const { width, isVisible, theme } = this.props;
|
|
||||||
|
|
||||||
if (nextState.name !== name) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextProps.theme !== theme) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.description !== description) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextProps.isVisible !== isVisible) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextProps.width !== width) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!equal(nextState.file, file)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = () => {
|
|
||||||
const { file, submit } = this.props;
|
|
||||||
const { name, description } = this.state;
|
|
||||||
submit({ ...file, name, description });
|
|
||||||
}
|
|
||||||
|
|
||||||
renderButtons = () => {
|
|
||||||
const { close, theme } = this.props;
|
|
||||||
if (isIOS) {
|
|
||||||
return (
|
|
||||||
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
|
||||||
<Button
|
|
||||||
title={I18n.t('Cancel')}
|
|
||||||
type='secondary'
|
|
||||||
backgroundColor={themes[theme].chatComponentBackground}
|
|
||||||
style={styles.button}
|
|
||||||
onPress={close}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title={I18n.t('Send')}
|
|
||||||
type='primary'
|
|
||||||
style={styles.button}
|
|
||||||
onPress={this.submit}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// FIXME: RNGH don't work well on Android modals: https://github.com/kmagiera/react-native-gesture-handler/issues/139
|
|
||||||
return (
|
|
||||||
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
|
||||||
<TouchableHighlight
|
|
||||||
onPress={close}
|
|
||||||
style={[styles.androidButton, { backgroundColor: themes[theme].chatComponentBackground }]}
|
|
||||||
underlayColor={themes[theme].chatComponentBackground}
|
|
||||||
activeOpacity={0.5}
|
|
||||||
>
|
|
||||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: themes[theme].tintColor }]}>{I18n.t('Cancel')}</Text>
|
|
||||||
</TouchableHighlight>
|
|
||||||
<TouchableHighlight
|
|
||||||
onPress={this.submit}
|
|
||||||
style={[styles.androidButton, { backgroundColor: themes[theme].tintColor }]}
|
|
||||||
underlayColor={themes[theme].tintColor}
|
|
||||||
activeOpacity={0.5}
|
|
||||||
>
|
|
||||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: themes[theme].buttonText }]}>{I18n.t('Send')}</Text>
|
|
||||||
</TouchableHighlight>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPreview() {
|
|
||||||
const { file, theme, isMasterDetail } = this.props;
|
|
||||||
if (file.mime && file.mime.match(/image/)) {
|
|
||||||
return (<Image source={{ isStatic: true, uri: file.path }} style={[styles.image, isMasterDetail && styles.bigPreview]} />);
|
|
||||||
}
|
|
||||||
if (file.mime && file.mime.match(/video/)) {
|
|
||||||
return (
|
|
||||||
<View style={[styles.video, { backgroundColor: themes[theme].bannerBackground }]}>
|
|
||||||
<CustomIcon name='play' size={72} color={themes[theme].buttonText} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (<CustomIcon name='clip' size={72} style={[styles.fileIcon, { color: themes[theme].tintColor }]} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
width, isVisible, close, isMasterDetail, theme
|
|
||||||
} = this.props;
|
|
||||||
const { name, description } = this.state;
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isVisible={isVisible}
|
|
||||||
style={styles.modal}
|
|
||||||
onBackdropPress={close}
|
|
||||||
onBackButtonPress={close}
|
|
||||||
animationIn='fadeIn'
|
|
||||||
animationOut='fadeOut'
|
|
||||||
useNativeDriver
|
|
||||||
hideModalContentWhileAnimating
|
|
||||||
avoidKeyboard
|
|
||||||
>
|
|
||||||
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, isMasterDetail && sharedStyles.modalFormSheet]}>
|
|
||||||
<View style={styles.titleContainer}>
|
|
||||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView style={styles.scrollView}>
|
|
||||||
{this.renderPreview()}
|
|
||||||
<TextInput
|
|
||||||
placeholder={I18n.t('File_name')}
|
|
||||||
value={name}
|
|
||||||
onChangeText={value => this.setState({ name: value })}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder={I18n.t('File_description')}
|
|
||||||
value={description}
|
|
||||||
onChangeText={value => this.setState({ description: value })}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
{this.renderButtons()}
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withDimensions(withTheme(UploadModal));
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, Alert, Keyboard } from 'react-native';
|
import {
|
||||||
|
View, Alert, Keyboard, NativeModules
|
||||||
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||||
import ImagePicker from 'react-native-image-crop-picker';
|
import ImagePicker from 'react-native-image-crop-picker';
|
||||||
|
@ -16,7 +18,6 @@ import styles from './styles';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { emojis } from '../../emojis';
|
import { emojis } from '../../emojis';
|
||||||
import Recording from './Recording';
|
import Recording from './Recording';
|
||||||
import UploadModal from './UploadModal';
|
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import ReplyPreview from './ReplyPreview';
|
import ReplyPreview from './ReplyPreview';
|
||||||
|
@ -42,7 +43,6 @@ import {
|
||||||
MENTIONS_TRACKING_TYPE_USERS
|
MENTIONS_TRACKING_TYPE_USERS
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import CommandsPreview from './CommandsPreview';
|
import CommandsPreview from './CommandsPreview';
|
||||||
import { Review } from '../../utils/review';
|
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Navigation from '../../lib/Navigation';
|
import Navigation from '../../lib/Navigation';
|
||||||
import { withActionSheet } from '../ActionSheet';
|
import { withActionSheet } from '../ActionSheet';
|
||||||
|
@ -54,6 +54,7 @@ const imagePickerConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const libraryPickerConfig = {
|
const libraryPickerConfig = {
|
||||||
|
multiple: true,
|
||||||
mediaType: 'any'
|
mediaType: 'any'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,9 +89,24 @@ class MessageBox extends Component {
|
||||||
typing: PropTypes.func,
|
typing: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
replyCancel: PropTypes.func,
|
replyCancel: PropTypes.func,
|
||||||
isMasterDetail: PropTypes.bool,
|
showSend: PropTypes.bool,
|
||||||
navigation: PropTypes.object,
|
navigation: PropTypes.object,
|
||||||
showActionSheet: PropTypes.func
|
children: PropTypes.node,
|
||||||
|
isMasterDetail: PropTypes.bool,
|
||||||
|
showActionSheet: PropTypes.func,
|
||||||
|
iOSScrollBehavior: PropTypes.number,
|
||||||
|
sharing: PropTypes.bool,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
message: {
|
||||||
|
id: ''
|
||||||
|
},
|
||||||
|
sharing: false,
|
||||||
|
iOSScrollBehavior: NativeModules.KeyboardTrackingViewManager?.KeyboardTrackingScrollBehaviorFixedOffset,
|
||||||
|
isActionsEnabled: true,
|
||||||
|
getCustomEmoji: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -98,12 +114,9 @@ class MessageBox extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
mentions: [],
|
mentions: [],
|
||||||
showEmojiKeyboard: false,
|
showEmojiKeyboard: false,
|
||||||
showSend: false,
|
showSend: props.showSend,
|
||||||
recording: false,
|
recording: false,
|
||||||
trackingType: '',
|
trackingType: '',
|
||||||
file: {
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
commandPreview: [],
|
commandPreview: [],
|
||||||
showCommandPreview: false,
|
showCommandPreview: false,
|
||||||
command: {}
|
command: {}
|
||||||
|
@ -161,27 +174,29 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const { rid, tmid, navigation } = this.props;
|
const {
|
||||||
|
rid, tmid, navigation, sharing
|
||||||
|
} = this.props;
|
||||||
let msg;
|
let msg;
|
||||||
try {
|
try {
|
||||||
const threadsCollection = db.collections.get('threads');
|
const threadsCollection = db.collections.get('threads');
|
||||||
const subsCollection = db.collections.get('subscriptions');
|
const subsCollection = db.collections.get('subscriptions');
|
||||||
|
try {
|
||||||
|
this.room = await subsCollection.find(rid);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Messagebox.didMount: Room not found');
|
||||||
|
}
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
try {
|
try {
|
||||||
const thread = await threadsCollection.find(tmid);
|
this.thread = await threadsCollection.find(tmid);
|
||||||
if (thread) {
|
if (this.thread && !sharing) {
|
||||||
msg = thread.draftMessage;
|
msg = this.thread.draftMessage;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Messagebox.didMount: Thread not found');
|
console.log('Messagebox.didMount: Thread not found');
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!sharing) {
|
||||||
try {
|
msg = this.room.draftMessage;
|
||||||
this.room = await subsCollection.find(rid);
|
|
||||||
msg = this.room.draftMessage;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Messagebox.didMount: Room not found');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
|
@ -211,8 +226,14 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
const { isFocused, editing, replying } = this.props;
|
const {
|
||||||
if (!isFocused()) {
|
isFocused, editing, replying, sharing
|
||||||
|
} = this.props;
|
||||||
|
if (!isFocused?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sharing) {
|
||||||
|
this.setInput(nextProps.message.msg ?? '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editing !== nextProps.editing && nextProps.editing) {
|
if (editing !== nextProps.editing && nextProps.editing) {
|
||||||
|
@ -230,11 +251,11 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const {
|
const {
|
||||||
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
|
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
roomType, replying, editing, isFocused, message, theme
|
roomType, replying, editing, isFocused, message, theme, children
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -266,10 +287,10 @@ class MessageBox extends Component {
|
||||||
if (!equal(nextState.commandPreview, commandPreview)) {
|
if (!equal(nextState.commandPreview, commandPreview)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!equal(nextState.file, file)) {
|
if (!equal(nextProps.message, message)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!equal(nextProps.message, message)) {
|
if (!equal(nextProps.children, children)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -312,22 +333,26 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
// eslint-disable-next-line react/sort-comp
|
||||||
debouncedOnChangeText = debounce(async(text) => {
|
debouncedOnChangeText = debounce(async(text) => {
|
||||||
|
const { sharing } = this.props;
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const isTextEmpty = text.length === 0;
|
const isTextEmpty = text.length === 0;
|
||||||
// this.setShowSend(!isTextEmpty);
|
// this.setShowSend(!isTextEmpty);
|
||||||
this.handleTyping(!isTextEmpty);
|
this.handleTyping(!isTextEmpty);
|
||||||
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
|
||||||
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
|
if (!sharing) {
|
||||||
if (slashCommand) {
|
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
||||||
const [, name, params] = slashCommand;
|
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
|
||||||
const commandsCollection = db.collections.get('slash_commands');
|
if (slashCommand) {
|
||||||
try {
|
const [, name, params] = slashCommand;
|
||||||
const command = await commandsCollection.find(name);
|
const commandsCollection = db.collections.get('slash_commands');
|
||||||
if (command.providesPreview) {
|
try {
|
||||||
return this.setCommandPreview(command, name, params);
|
const command = await commandsCollection.find(name);
|
||||||
|
if (command.providesPreview) {
|
||||||
|
return this.setCommandPreview(command, name, params);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Slash command not found');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.log('Slash command not found');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,12 +362,20 @@ class MessageBox extends Component {
|
||||||
const cursor = Math.max(start, end);
|
const cursor = Math.max(start, end);
|
||||||
const lastNativeText = this.component?.lastNativeText || '';
|
const lastNativeText = this.component?.lastNativeText || '';
|
||||||
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
||||||
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
||||||
|
|
||||||
|
// if sharing, track #|@|:
|
||||||
|
if (sharing) {
|
||||||
|
regexp = /(#|@|:)([a-z0-9._-]+)$/im;
|
||||||
|
}
|
||||||
|
|
||||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
if (!sharing) {
|
||||||
if (slash) {
|
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
||||||
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
if (slash) {
|
||||||
|
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.stopTrackingMention();
|
return this.stopTrackingMention();
|
||||||
}
|
}
|
||||||
|
@ -482,7 +515,10 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTyping = (isTyping) => {
|
handleTyping = (isTyping) => {
|
||||||
const { typing, rid } = this.props;
|
const { typing, rid, sharing } = this.props;
|
||||||
|
if (sharing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isTyping) {
|
if (!isTyping) {
|
||||||
if (this.typingTimeout) {
|
if (this.typingTimeout) {
|
||||||
clearTimeout(this.typingTimeout);
|
clearTimeout(this.typingTimeout);
|
||||||
|
@ -522,7 +558,8 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
setShowSend = (showSend) => {
|
setShowSend = (showSend) => {
|
||||||
const { showSend: prevShowSend } = this.state;
|
const { showSend: prevShowSend } = this.state;
|
||||||
if (prevShowSend !== showSend) {
|
const { showSend: propShowSend } = this.props;
|
||||||
|
if (prevShowSend !== showSend && !propShowSend) {
|
||||||
this.setState({ showSend });
|
this.setState({ showSend });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -534,7 +571,7 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
canUploadFile = (file) => {
|
canUploadFile = (file) => {
|
||||||
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
|
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
|
||||||
const result = canUploadFile(file, { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize });
|
const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -542,33 +579,11 @@ class MessageBox extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMediaMessage = async(file) => {
|
|
||||||
const {
|
|
||||||
rid, tmid, baseUrl: server, user, message: { id: messageTmid }, replyCancel
|
|
||||||
} = this.props;
|
|
||||||
this.setState({ file: { isVisible: false } });
|
|
||||||
const fileInfo = {
|
|
||||||
name: file.name,
|
|
||||||
description: file.description,
|
|
||||||
size: file.size,
|
|
||||||
type: file.mime,
|
|
||||||
store: 'Uploads',
|
|
||||||
path: file.path
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
replyCancel();
|
|
||||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid || messageTmid, server, user);
|
|
||||||
Review.pushPositiveEvent();
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
takePhoto = async() => {
|
takePhoto = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
||||||
if (this.canUploadFile(image)) {
|
if (this.canUploadFile(image)) {
|
||||||
this.showUploadModal(image);
|
this.openShareView([image]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -579,7 +594,7 @@ class MessageBox extends Component {
|
||||||
try {
|
try {
|
||||||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||||
if (this.canUploadFile(video)) {
|
if (this.canUploadFile(video)) {
|
||||||
this.showUploadModal(video);
|
this.openShareView([video]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -588,10 +603,8 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
chooseFromLibrary = async() => {
|
chooseFromLibrary = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||||
if (this.canUploadFile(image)) {
|
this.openShareView(attachments);
|
||||||
this.showUploadModal(image);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
@ -609,7 +622,7 @@ class MessageBox extends Component {
|
||||||
path: res.uri
|
path: res.uri
|
||||||
};
|
};
|
||||||
if (this.canUploadFile(file)) {
|
if (this.canUploadFile(file)) {
|
||||||
this.showUploadModal(file);
|
this.openShareView([file]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!DocumentPicker.isCancel(e)) {
|
if (!DocumentPicker.isCancel(e)) {
|
||||||
|
@ -618,6 +631,10 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openShareView = (attachments) => {
|
||||||
|
Navigation.navigate('ShareView', { room: this.room, thread: this.thread, attachments });
|
||||||
|
}
|
||||||
|
|
||||||
createDiscussion = () => {
|
createDiscussion = () => {
|
||||||
const { isMasterDetail } = this.props;
|
const { isMasterDetail } = this.props;
|
||||||
const params = { channel: this.room, showCloseModal: true };
|
const params = { channel: this.room, showCloseModal: true };
|
||||||
|
@ -628,10 +645,6 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showUploadModal = (file) => {
|
|
||||||
this.setState({ file: { ...file, isVisible: true } });
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessageBoxActions = () => {
|
showMessageBoxActions = () => {
|
||||||
const { showActionSheet } = this.props;
|
const { showActionSheet } = this.props;
|
||||||
showActionSheet({ options: this.options });
|
showActionSheet({ options: this.options });
|
||||||
|
@ -679,16 +692,22 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
submit = async() => {
|
submit = async() => {
|
||||||
const {
|
const {
|
||||||
onSubmit, rid: roomId, tmid
|
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const message = this.text;
|
const message = this.text;
|
||||||
|
|
||||||
|
// if sharing, only execute onSubmit prop
|
||||||
|
if (sharing) {
|
||||||
|
onSubmit(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
this.debouncedOnChangeText.stop();
|
this.debouncedOnChangeText.stop();
|
||||||
this.closeEmoji();
|
this.closeEmoji();
|
||||||
this.stopTrackingMention();
|
this.stopTrackingMention();
|
||||||
this.handleTyping(false);
|
this.handleTyping(false);
|
||||||
if (message.trim() === '') {
|
if (message.trim() === '' && !showSend) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -809,7 +828,7 @@ class MessageBox extends Component {
|
||||||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled
|
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled, children, isActionsEnabled
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isAndroidTablet = isTablet && isAndroid ? {
|
const isAndroidTablet = isTablet && isAndroid ? {
|
||||||
|
@ -846,6 +865,7 @@ class MessageBox extends Component {
|
||||||
showEmojiKeyboard={showEmojiKeyboard}
|
showEmojiKeyboard={showEmojiKeyboard}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
showMessageBoxActions={this.showMessageBoxActions}
|
showMessageBoxActions={this.showMessageBoxActions}
|
||||||
|
isActionsEnabled={isActionsEnabled}
|
||||||
editCancel={this.editCancel}
|
editCancel={this.editCancel}
|
||||||
openEmoji={this.openEmoji}
|
openEmoji={this.openEmoji}
|
||||||
closeEmoji={this.closeEmoji}
|
closeEmoji={this.closeEmoji}
|
||||||
|
@ -872,18 +892,20 @@ class MessageBox extends Component {
|
||||||
recordAudioMessage={this.recordAudioMessage}
|
recordAudioMessage={this.recordAudioMessage}
|
||||||
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
||||||
showMessageBoxActions={this.showMessageBoxActions}
|
showMessageBoxActions={this.showMessageBoxActions}
|
||||||
|
isActionsEnabled={isActionsEnabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const { showEmojiKeyboard, file } = this.state;
|
const { showEmojiKeyboard } = this.state;
|
||||||
const {
|
const {
|
||||||
user, baseUrl, theme, isMasterDetail
|
user, baseUrl, theme, iOSScrollBehavior
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<MessageboxContext.Provider
|
<MessageboxContext.Provider
|
||||||
|
@ -906,13 +928,7 @@ class MessageBox extends Component {
|
||||||
requiresSameParentToManageScrollView
|
requiresSameParentToManageScrollView
|
||||||
addBottomView
|
addBottomView
|
||||||
bottomViewColor={themes[theme].messageboxBackground}
|
bottomViewColor={themes[theme].messageboxBackground}
|
||||||
/>
|
iOSScrollBehavior={iOSScrollBehavior}
|
||||||
<UploadModal
|
|
||||||
isVisible={(file && file.isVisible)}
|
|
||||||
file={file}
|
|
||||||
close={() => this.setState({ file: {} })}
|
|
||||||
submit={this.sendMediaMessage}
|
|
||||||
isMasterDetail={isMasterDetail}
|
|
||||||
/>
|
/>
|
||||||
</MessageboxContext.Provider>
|
</MessageboxContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -103,5 +103,8 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
scrollViewMention: {
|
scrollViewMention: {
|
||||||
maxHeight: SCROLLVIEW_MENTION_HEIGHT
|
maxHeight: SCROLLVIEW_MENTION_HEIGHT
|
||||||
|
},
|
||||||
|
buttonsWhitespace: {
|
||||||
|
width: 15
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,7 +40,7 @@ const RoomTypeIcon = React.memo(({
|
||||||
name={icon}
|
name={icon}
|
||||||
size={size}
|
size={size}
|
||||||
style={[
|
style={[
|
||||||
type === 'l' ? { color: STATUS_COLORS[status] } : { color },
|
type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
|
||||||
styles.icon,
|
styles.icon,
|
||||||
style
|
style
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -5,16 +5,20 @@ import PropTypes from 'prop-types';
|
||||||
import { isIOS } from '../utils/deviceInfo';
|
import { isIOS } from '../utils/deviceInfo';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
const StatusBar = React.memo(({ theme }) => {
|
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => {
|
||||||
let barStyle = 'light-content';
|
if (!barStyle) {
|
||||||
if (theme === 'light' && isIOS) {
|
barStyle = 'light-content';
|
||||||
barStyle = 'dark-content';
|
if (theme === 'light' && isIOS) {
|
||||||
|
barStyle = 'dark-content';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <StatusBarRN backgroundColor={themes[theme].headerBackground} barStyle={barStyle} animated />;
|
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
|
||||||
});
|
});
|
||||||
|
|
||||||
StatusBar.propTypes = {
|
StatusBar.propTypes = {
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
barStyle: PropTypes.string,
|
||||||
|
backgroundColor: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatusBar;
|
export default StatusBar;
|
||||||
|
|
|
@ -112,7 +112,8 @@ export default StyleSheet.create({
|
||||||
// maxWidth: 400,
|
// maxWidth: 400,
|
||||||
minHeight: isTablet ? 300 : 200,
|
minHeight: isTablet ? 300 : 200,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
borderWidth: 1
|
borderWidth: 1,
|
||||||
|
overflow: 'hidden'
|
||||||
},
|
},
|
||||||
imagePressed: {
|
imagePressed: {
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
|
|
|
@ -379,6 +379,8 @@ export default {
|
||||||
Reactions_are_enabled: 'Reactions are enabled',
|
Reactions_are_enabled: 'Reactions are enabled',
|
||||||
Reactions: 'Reactions',
|
Reactions: 'Reactions',
|
||||||
Read: 'Read',
|
Read: 'Read',
|
||||||
|
Read_External_Permission_Message: 'Rocket Chat needs to access photos, media, and files on your device',
|
||||||
|
Read_External_Permission: 'Read Media Permission',
|
||||||
Read_Only_Channel: 'Read Only Channel',
|
Read_Only_Channel: 'Read Only Channel',
|
||||||
Read_Only: 'Read Only',
|
Read_Only: 'Read Only',
|
||||||
Read_Receipt: 'Read Receipt',
|
Read_Receipt: 'Read Receipt',
|
||||||
|
@ -444,6 +446,7 @@ export default {
|
||||||
Send_message: 'Send message',
|
Send_message: 'Send message',
|
||||||
Send_me_the_code_again: 'Send me the code again',
|
Send_me_the_code_again: 'Send me the code again',
|
||||||
Send_to: 'Send to...',
|
Send_to: 'Send to...',
|
||||||
|
Sending_to: 'Sending to',
|
||||||
Sent_an_attachment: 'Sent an attachment',
|
Sent_an_attachment: 'Sent an attachment',
|
||||||
Server: 'Server',
|
Server: 'Server',
|
||||||
Servers: 'Servers',
|
Servers: 'Servers',
|
||||||
|
|
|
@ -344,6 +344,8 @@ export default {
|
||||||
Reactions_are_disabled: 'Reagir está desabilitado',
|
Reactions_are_disabled: 'Reagir está desabilitado',
|
||||||
Reactions_are_enabled: 'Reagir está habilitado',
|
Reactions_are_enabled: 'Reagir está habilitado',
|
||||||
Reactions: 'Reações',
|
Reactions: 'Reações',
|
||||||
|
Read_External_Permission_Message: 'Rocket Chat precisa acessar fotos, mídia e arquivos no seu dispositivo',
|
||||||
|
Read_External_Permission: 'Permissão de acesso à arquivos',
|
||||||
Read_Only_Channel: 'Canal Somente Leitura',
|
Read_Only_Channel: 'Canal Somente Leitura',
|
||||||
Read_Only: 'Somente Leitura',
|
Read_Only: 'Somente Leitura',
|
||||||
Register: 'Registrar',
|
Register: 'Registrar',
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { types } from './types';
|
||||||
|
|
||||||
|
export const ImageComponent = (type) => {
|
||||||
|
let Component;
|
||||||
|
if (type === types.REACT_NATIVE_IMAGE) {
|
||||||
|
const { Image } = require('react-native');
|
||||||
|
Component = Image;
|
||||||
|
} else {
|
||||||
|
const FastImage = require('react-native-fast-image').default;
|
||||||
|
Component = FastImage;
|
||||||
|
}
|
||||||
|
return Component;
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
|
@ -6,14 +6,12 @@ import {
|
||||||
State,
|
State,
|
||||||
PinchGestureHandler
|
PinchGestureHandler
|
||||||
} from 'react-native-gesture-handler';
|
} from 'react-native-gesture-handler';
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
import Animated, { Easing } from 'react-native-reanimated';
|
import Animated, { Easing } from 'react-native-reanimated';
|
||||||
import { withDimensions } from '../../dimensions';
|
import { ImageComponent } from './ImageComponent';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
const AnimatedFastImage = Animated.createAnimatedComponent(FastImage);
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
wrapper: {
|
flex: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
|
@ -264,10 +262,13 @@ const HEIGHT = 300;
|
||||||
|
|
||||||
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
|
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
|
||||||
// and changed to use FastImage animated component
|
// and changed to use FastImage animated component
|
||||||
class ImageViewer extends Component {
|
export class ImageViewer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
uri: PropTypes.string,
|
uri: PropTypes.string,
|
||||||
width: PropTypes.number
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
imageComponentType: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -384,15 +385,22 @@ class ImageViewer extends Component {
|
||||||
panRef = React.createRef();
|
panRef = React.createRef();
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { uri, width, ...props } = this.props;
|
const {
|
||||||
|
uri, width, height, theme, imageComponentType, ...props
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const Component = ImageComponent(imageComponentType);
|
||||||
|
const AnimatedFastImage = Animated.createAnimatedComponent(Component);
|
||||||
|
|
||||||
// The below two animated values makes it so that scale appears to be done
|
// The below two animated values makes it so that scale appears to be done
|
||||||
// from the top left corner of the image view instead of its center. This
|
// from the top left corner of the image view instead of its center. This
|
||||||
// is required for the "scale focal point" math to work correctly
|
// is required for the "scale focal point" math to work correctly
|
||||||
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2);
|
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2);
|
||||||
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2);
|
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2);
|
||||||
|
const backgroundColor = themes[theme].previewBackground;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.wrapper}>
|
<View style={[styles.flex, { width, height, backgroundColor }]}>
|
||||||
<PinchGestureHandler
|
<PinchGestureHandler
|
||||||
ref={this.pinchRef}
|
ref={this.pinchRef}
|
||||||
simultaneousHandlers={this.panRef}
|
simultaneousHandlers={this.panRef}
|
||||||
|
@ -438,5 +446,3 @@ class ImageViewer extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withDimensions(ImageViewer);
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ScrollView, StyleSheet } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { ImageComponent } from './ImageComponent';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
scrollContent: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ImageViewer = ({
|
||||||
|
uri, imageComponentType, theme, width, height, ...props
|
||||||
|
}) => {
|
||||||
|
const backgroundColor = themes[theme].previewBackground;
|
||||||
|
const Component = ImageComponent(imageComponentType);
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={{ backgroundColor }}
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.scrollContent,
|
||||||
|
width && { width },
|
||||||
|
height && { height }
|
||||||
|
]}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
maximumZoomScale={2}
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
style={styles.image}
|
||||||
|
resizeMode='contain'
|
||||||
|
source={{ uri }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImageViewer.propTypes = {
|
||||||
|
uri: PropTypes.string,
|
||||||
|
imageComponentType: PropTypes.string,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
|
@ -1,38 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { ScrollView, StyleSheet } from 'react-native';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
scrollContent: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%'
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
flex: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const ImageViewer = ({
|
|
||||||
uri, ...props
|
|
||||||
}) => (
|
|
||||||
<ScrollView
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
maximumZoomScale={2}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
style={styles.image}
|
|
||||||
resizeMode='contain'
|
|
||||||
source={{ uri }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
|
|
||||||
ImageViewer.propTypes = {
|
|
||||||
uri: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageViewer;
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './ImageViewer';
|
||||||
|
export * from './types';
|
||||||
|
export * from './ImageComponent';
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const types = {
|
||||||
|
FAST_IMAGE: 'FAST_IMAGE',
|
||||||
|
REACT_NATIVE_IMAGE: 'REACT_NATIVE'
|
||||||
|
};
|
|
@ -1,6 +1,12 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
const getUser = state => state.login.user || {};
|
const getUser = (state) => {
|
||||||
|
if (!isEmpty(state.share?.user)) {
|
||||||
|
return state.share.user;
|
||||||
|
}
|
||||||
|
return state.login?.user;
|
||||||
|
};
|
||||||
const getLoginServices = state => state.login.services || {};
|
const getLoginServices = state => state.login.services || {};
|
||||||
const getShowFormLoginSetting = state => state.settings.Accounts_ShowFormLogin || false;
|
const getShowFormLoginSetting = state => state.settings.Accounts_ShowFormLogin || false;
|
||||||
|
|
||||||
|
|
61
app/share.js
61
app/share.js
|
@ -1,4 +1,5 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { Dimensions } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { AppearanceProvider } from 'react-native-appearance';
|
import { AppearanceProvider } from 'react-native-appearance';
|
||||||
|
@ -32,6 +33,8 @@ import ShareView from './views/ShareView';
|
||||||
import SelectServerView from './views/SelectServerView';
|
import SelectServerView from './views/SelectServerView';
|
||||||
import { setCurrentScreen } from './utils/log';
|
import { setCurrentScreen } from './utils/log';
|
||||||
import AuthLoadingView from './views/AuthLoadingView';
|
import AuthLoadingView from './views/AuthLoadingView';
|
||||||
|
import { DimensionsContext } from './dimensions';
|
||||||
|
import debounce from './utils/debounce';
|
||||||
|
|
||||||
const Inside = createStackNavigator();
|
const Inside = createStackNavigator();
|
||||||
const InsideStack = () => {
|
const InsideStack = () => {
|
||||||
|
@ -43,7 +46,6 @@ const InsideStack = () => {
|
||||||
};
|
};
|
||||||
screenOptions.headerStyle = {
|
screenOptions.headerStyle = {
|
||||||
...screenOptions.headerStyle,
|
...screenOptions.headerStyle,
|
||||||
// TODO: fix on multiple files PR :)
|
|
||||||
height: 57
|
height: 57
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,7 +86,7 @@ const OutsideStack = () => {
|
||||||
// App
|
// App
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
export const App = ({ root }) => (
|
export const App = ({ root }) => (
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
||||||
<>
|
<>
|
||||||
{!root ? (
|
{!root ? (
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
@ -115,13 +117,17 @@ App.propTypes = {
|
||||||
class Root extends React.Component {
|
class Root extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const { width, height, scale } = Dimensions.get('screen');
|
||||||
this.state = {
|
this.state = {
|
||||||
theme: defaultTheme(),
|
theme: defaultTheme(),
|
||||||
themePreferences: {
|
themePreferences: {
|
||||||
currentTheme: supportSystemTheme() ? 'automatic' : 'light',
|
currentTheme: supportSystemTheme() ? 'automatic' : 'light',
|
||||||
darkLevel: 'dark'
|
darkLevel: 'dark'
|
||||||
},
|
},
|
||||||
root: ''
|
root: '',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scale
|
||||||
};
|
};
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
@ -159,28 +165,49 @@ class Root extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dimensions update fires twice
|
||||||
|
onDimensionsChange = debounce(({ window: { width, height, scale } }) => {
|
||||||
|
this.setDimensions({ width, height, scale });
|
||||||
|
this.setMasterDetail(width);
|
||||||
|
})
|
||||||
|
|
||||||
|
setDimensions = ({ width, height, scale }) => {
|
||||||
|
this.setState({ width, height, scale });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme, root } = this.state;
|
const {
|
||||||
|
theme, root, width, height, scale
|
||||||
|
} = this.state;
|
||||||
const navTheme = navigationTheme(theme);
|
const navTheme = navigationTheme(theme);
|
||||||
return (
|
return (
|
||||||
<AppearanceProvider>
|
<AppearanceProvider>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeContext.Provider value={{ theme }}>
|
<ThemeContext.Provider value={{ theme }}>
|
||||||
<NavigationContainer
|
<DimensionsContext.Provider
|
||||||
theme={navTheme}
|
value={{
|
||||||
ref={Navigation.navigationRef}
|
width,
|
||||||
onStateChange={(state) => {
|
height,
|
||||||
const previousRouteName = Navigation.routeNameRef.current;
|
scale,
|
||||||
const currentRouteName = getActiveRouteName(state);
|
setDimensions: this.setDimensions
|
||||||
if (previousRouteName !== currentRouteName) {
|
|
||||||
setCurrentScreen(currentRouteName);
|
|
||||||
}
|
|
||||||
Navigation.routeNameRef.current = currentRouteName;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<App root={root} />
|
<NavigationContainer
|
||||||
</NavigationContainer>
|
theme={navTheme}
|
||||||
<ScreenLockedView />
|
ref={Navigation.navigationRef}
|
||||||
|
onStateChange={(state) => {
|
||||||
|
const previousRouteName = Navigation.routeNameRef.current;
|
||||||
|
const currentRouteName = getActiveRouteName(state);
|
||||||
|
if (previousRouteName !== currentRouteName) {
|
||||||
|
setCurrentScreen(currentRouteName);
|
||||||
|
}
|
||||||
|
Navigation.routeNameRef.current = currentRouteName;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<App root={root} />
|
||||||
|
</NavigationContainer>
|
||||||
|
<ScreenLockedView />
|
||||||
|
</DimensionsContext.Provider>
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</AppearanceProvider>
|
</AppearanceProvider>
|
||||||
|
|
|
@ -53,6 +53,7 @@ import AttachmentView from '../views/AttachmentView';
|
||||||
import ModalBlockView from '../views/ModalBlockView';
|
import ModalBlockView from '../views/ModalBlockView';
|
||||||
import JitsiMeetView from '../views/JitsiMeetView';
|
import JitsiMeetView from '../views/JitsiMeetView';
|
||||||
import StatusView from '../views/StatusView';
|
import StatusView from '../views/StatusView';
|
||||||
|
import ShareView from '../views/ShareView';
|
||||||
import CreateDiscussionView from '../views/CreateDiscussionView';
|
import CreateDiscussionView from '../views/CreateDiscussionView';
|
||||||
|
|
||||||
// ChatsStackNavigator
|
// ChatsStackNavigator
|
||||||
|
@ -303,6 +304,10 @@ const InsideStackNavigator = () => {
|
||||||
name='StatusView'
|
name='StatusView'
|
||||||
component={StatusView}
|
component={StatusView}
|
||||||
/>
|
/>
|
||||||
|
<InsideStack.Screen
|
||||||
|
name='ShareView'
|
||||||
|
component={ShareView}
|
||||||
|
/>
|
||||||
<InsideStack.Screen
|
<InsideStack.Screen
|
||||||
name='ModalBlockView'
|
name='ModalBlockView'
|
||||||
component={ModalBlockView}
|
component={ModalBlockView}
|
||||||
|
|
|
@ -50,6 +50,7 @@ import StatusView from '../../views/StatusView';
|
||||||
import CreateDiscussionView from '../../views/CreateDiscussionView';
|
import CreateDiscussionView from '../../views/CreateDiscussionView';
|
||||||
|
|
||||||
import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
||||||
|
import ShareView from '../../views/ShareView';
|
||||||
|
|
||||||
// ChatsStackNavigator
|
// ChatsStackNavigator
|
||||||
const ChatsStack = createStackNavigator();
|
const ChatsStack = createStackNavigator();
|
||||||
|
@ -286,6 +287,10 @@ const InsideStackNavigator = React.memo(() => {
|
||||||
component={JitsiMeetView}
|
component={JitsiMeetView}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
<InsideStack.Screen
|
||||||
|
name='ShareView'
|
||||||
|
component={ShareView}
|
||||||
|
/>
|
||||||
</InsideStack.Navigator>
|
</InsideStack.Navigator>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,9 +16,15 @@ export const isReadOnly = async(room, user) => {
|
||||||
if (room.archived) {
|
if (room.archived) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const allowPost = await canPost(room);
|
if (isMuted(room, user)) {
|
||||||
if (allowPost) {
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return (room && room.ro) || isMuted(room, user);
|
if (room?.ro) {
|
||||||
|
const allowPost = await canPost(room);
|
||||||
|
if (allowPost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
export const canUploadFile = (file, serverInfo) => {
|
export const canUploadFile = (file, allowList, maxFileSize) => {
|
||||||
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = serverInfo;
|
|
||||||
if (!(file && file.path)) {
|
if (!(file && file.path)) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
if (FileUpload_MaxFileSize > -1 && file.size > FileUpload_MaxFileSize) {
|
if (maxFileSize > -1 && file.size > maxFileSize) {
|
||||||
return { success: false, error: 'error-file-too-large' };
|
return { success: false, error: 'error-file-too-large' };
|
||||||
}
|
}
|
||||||
// if white list is empty, all media types are enabled
|
// if white list is empty, all media types are enabled
|
||||||
if (!FileUpload_MediaTypeWhiteList || FileUpload_MediaTypeWhiteList === '*') {
|
if (!allowList || allowList === '*') {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
const allowedMime = FileUpload_MediaTypeWhiteList.split(',');
|
const allowedMime = allowList.split(',');
|
||||||
if (allowedMime.includes(file.mime)) {
|
if (allowedMime.includes(file.mime)) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,22 @@ import * as mime from 'react-native-mime-types';
|
||||||
import { FileSystem } from 'react-native-unimodules';
|
import { FileSystem } from 'react-native-unimodules';
|
||||||
import { Video } from 'expo-av';
|
import { Video } from 'expo-av';
|
||||||
import SHA256 from 'js-sha256';
|
import SHA256 from 'js-sha256';
|
||||||
|
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { LISTENER } from '../containers/Toast';
|
import { LISTENER } from '../containers/Toast';
|
||||||
import EventEmitter from '../utils/events';
|
import EventEmitter from '../utils/events';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
import ImageViewer from '../presentation/ImageViewer';
|
import { ImageViewer } from '../presentation/ImageViewer';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import { formatAttachmentUrl } from '../lib/utils';
|
import { formatAttachmentUrl } from '../lib/utils';
|
||||||
import RCActivityIndicator from '../containers/ActivityIndicator';
|
import RCActivityIndicator from '../containers/ActivityIndicator';
|
||||||
import { SaveButton, CloseModalButton } from '../containers/HeaderButton';
|
import { SaveButton, CloseModalButton } from '../containers/HeaderButton';
|
||||||
import { isAndroid } from '../utils/deviceInfo';
|
import { isAndroid } from '../utils/deviceInfo';
|
||||||
import { getUserSelector } from '../selectors/login';
|
import { getUserSelector } from '../selectors/login';
|
||||||
|
import { withDimensions } from '../dimensions';
|
||||||
|
import { getHeaderHeight } from '../containers/Header';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -32,6 +36,9 @@ class AttachmentView extends React.Component {
|
||||||
route: PropTypes.object,
|
route: PropTypes.object,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
insets: PropTypes.object,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
token: PropTypes.string
|
token: PropTypes.string
|
||||||
|
@ -61,13 +68,16 @@ class AttachmentView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { route, navigation } = this.props;
|
const { route, navigation, theme } = this.props;
|
||||||
const attachment = route.params?.attachment;
|
const attachment = route.params?.attachment;
|
||||||
const { title } = attachment;
|
const { title } = attachment;
|
||||||
const options = {
|
const options = {
|
||||||
headerLeft: () => <CloseModalButton testID='close-attachment-view' navigation={navigation} />,
|
headerLeft: () => <CloseModalButton testID='close-attachment-view' navigation={navigation} buttonStyle={{ color: themes[theme].previewTintColor }} />,
|
||||||
title: decodeURI(title),
|
title: decodeURI(title),
|
||||||
headerRight: () => <SaveButton testID='save-image' onPress={this.handleSave} />
|
headerRight: () => <SaveButton testID='save-image' onPress={this.handleSave} buttonStyle={{ color: themes[theme].previewTintColor }} />,
|
||||||
|
headerBackground: () => <View style={{ flex: 1, backgroundColor: themes[theme].previewBackground }} />,
|
||||||
|
headerTintColor: themes[theme].previewTintColor,
|
||||||
|
headerTitleStyle: { color: themes[theme].previewTintColor }
|
||||||
};
|
};
|
||||||
navigation.setOptions(options);
|
navigation.setOptions(options);
|
||||||
}
|
}
|
||||||
|
@ -107,12 +117,21 @@ class AttachmentView extends React.Component {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderImage = uri => (
|
renderImage = (uri) => {
|
||||||
<ImageViewer
|
const {
|
||||||
uri={uri}
|
theme, width, height, insets
|
||||||
onLoadEnd={() => this.setState({ loading: false })}
|
} = this.props;
|
||||||
/>
|
const headerHeight = getHeaderHeight(width > height);
|
||||||
);
|
return (
|
||||||
|
<ImageViewer
|
||||||
|
uri={uri}
|
||||||
|
onLoadEnd={() => this.setState({ loading: false })}
|
||||||
|
theme={theme}
|
||||||
|
width={width}
|
||||||
|
height={height - insets.top - insets.bottom - headerHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderVideo = uri => (
|
renderVideo = uri => (
|
||||||
<Video
|
<Video
|
||||||
|
@ -146,6 +165,7 @@ class AttachmentView extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
|
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||||
|
<StatusBar barStyle='light-content' backgroundColor={themes[theme].previewBackground} />
|
||||||
{content}
|
{content}
|
||||||
{loading ? <RCActivityIndicator absolute size='large' theme={theme} /> : null}
|
{loading ? <RCActivityIndicator absolute size='large' theme={theme} /> : null}
|
||||||
</View>
|
</View>
|
||||||
|
@ -158,4 +178,4 @@ const mapStateToProps = state => ({
|
||||||
user: getUserSelector(state)
|
user: getUserSelector(state)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(AttachmentView));
|
export default connect(mapStateToProps)(withTheme(withDimensions(withSafeAreaInsets(AttachmentView))));
|
||||||
|
|
|
@ -43,7 +43,7 @@ const Icon = React.memo(({
|
||||||
} else if (type === 'c') {
|
} else if (type === 'c') {
|
||||||
icon = 'hash';
|
icon = 'hash';
|
||||||
} else if (type === 'l') {
|
} else if (type === 'l') {
|
||||||
icon = 'omnichannel';
|
icon = 'livechat';
|
||||||
} else if (type === 'd') {
|
} else if (type === 'd') {
|
||||||
icon = 'team';
|
icon = 'team';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -173,7 +173,7 @@ class UploadProgress extends Component {
|
||||||
[
|
[
|
||||||
<View key='row' style={styles.row}>
|
<View key='row' style={styles.row}>
|
||||||
<CustomIcon name='clip' size={20} color={themes[theme].auxiliaryText} />
|
<CustomIcon name='clip' size={20} color={themes[theme].auxiliaryText} />
|
||||||
<Text style={[styles.descriptionContainer, styles.descriptionText, { color: themes[theme].auxiliaryText }]} ellipsizeMode='tail' numberOfLines={1}>
|
<Text style={[styles.descriptionContainer, styles.descriptionText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||||
{I18n.t('Uploading')} {item.name}
|
{I18n.t('Uploading')} {item.name}
|
||||||
</Text>
|
</Text>
|
||||||
<CustomIcon name='Cross' size={20} color={themes[theme].auxiliaryText} onPress={() => this.cancelUpload(item)} />
|
<CustomIcon name='Cross' size={20} color={themes[theme].auxiliaryText} onPress={() => this.cancelUpload(item)} />
|
||||||
|
@ -186,7 +186,7 @@ class UploadProgress extends Component {
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<CustomIcon name='warning' size={20} color={themes[theme].dangerColor} />
|
<CustomIcon name='warning' size={20} color={themes[theme].dangerColor} />
|
||||||
<View style={styles.descriptionContainer}>
|
<View style={styles.descriptionContainer}>
|
||||||
<Text style={[styles.descriptionText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Error_uploading')} {item.name}</Text>
|
<Text style={[styles.descriptionText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{I18n.t('Error_uploading')} {item.name}</Text>
|
||||||
<TouchableOpacity onPress={() => this.tryAgain(item)}>
|
<TouchableOpacity onPress={() => this.tryAgain(item)}>
|
||||||
<Text style={[styles.tryAgainButtonText, { color: themes[theme].tintColor }]}>{I18n.t('Try_again')}</Text>
|
<Text style={[styles.tryAgainButtonText, { color: themes[theme].tintColor }]}>{I18n.t('Try_again')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -212,9 +212,6 @@ class RoomView extends React.Component {
|
||||||
if (roomUpdate.topic !== prevState.roomUpdate.topic) {
|
if (roomUpdate.topic !== prevState.roomUpdate.topic) {
|
||||||
this.setHeader();
|
this.setHeader();
|
||||||
}
|
}
|
||||||
if (!isEqual(prevState.roomUpdate.roles, roomUpdate.roles)) {
|
|
||||||
this.setReadOnly();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If it's a livechat room
|
// If it's a livechat room
|
||||||
if (this.t === 'l') {
|
if (this.t === 'l') {
|
||||||
|
@ -225,6 +222,7 @@ class RoomView extends React.Component {
|
||||||
if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
|
if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
|
||||||
this.setHeader();
|
this.setHeader();
|
||||||
}
|
}
|
||||||
|
this.setReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillUnmount() {
|
async componentWillUnmount() {
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, Text, FlatList, Keyboard, BackHandler
|
View, Text, FlatList, Keyboard, BackHandler, PermissionsAndroid, ScrollView
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import ShareExtension from 'rn-extensions-share';
|
import ShareExtension from 'rn-extensions-share';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import RNFetchBlob from 'rn-fetch-blob';
|
|
||||||
import * as mime from 'react-native-mime-types';
|
import * as mime from 'react-native-mime-types';
|
||||||
import { isEqual, orderBy } from 'lodash';
|
import { isEqual, orderBy } from 'lodash';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
|
||||||
import log from '../../utils/log';
|
|
||||||
import { canUploadFile } from '../../utils/media';
|
|
||||||
import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem';
|
import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem';
|
||||||
import ServerItem from '../../presentation/ServerItem';
|
import ServerItem from '../../presentation/ServerItem';
|
||||||
import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||||
|
@ -28,6 +25,12 @@ import { themes } from '../../constants/colors';
|
||||||
import { animateNextTransition } from '../../utils/layoutAnimation';
|
import { animateNextTransition } from '../../utils/layoutAnimation';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
|
||||||
|
const permission = {
|
||||||
|
title: I18n.t('Read_External_Permission'),
|
||||||
|
message: I18n.t('Read_External_Permission_Message')
|
||||||
|
};
|
||||||
|
|
||||||
const LIMIT = 50;
|
const LIMIT = 50;
|
||||||
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
|
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
|
||||||
|
@ -46,52 +49,47 @@ class ShareListView extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.data = [];
|
this.data = [];
|
||||||
this.state = {
|
this.state = {
|
||||||
showError: false,
|
|
||||||
searching: false,
|
searching: false,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
value: '',
|
|
||||||
isMedia: false,
|
|
||||||
mediaLoading: false,
|
|
||||||
fileInfo: null,
|
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
chats: [],
|
chats: [],
|
||||||
servers: [],
|
servers: [],
|
||||||
|
attachments: [],
|
||||||
|
text: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
serverInfo: null
|
serverInfo: null,
|
||||||
|
needsPermission: isAndroid || false
|
||||||
};
|
};
|
||||||
this.setHeader();
|
this.setHeader();
|
||||||
this.unsubscribeFocus = props.navigation.addListener('focus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress));
|
this.unsubscribeFocus = props.navigation.addListener('focus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress));
|
||||||
this.unsubscribeBlur = props.navigation.addListener('blur', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress));
|
this.unsubscribeBlur = props.navigation.addListener('blur', () => BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
const { server } = this.props;
|
const { server } = this.props;
|
||||||
setTimeout(async() => {
|
try {
|
||||||
try {
|
const data = await ShareExtension.data();
|
||||||
const { value, type } = await ShareExtension.data();
|
if (isAndroid) {
|
||||||
let fileInfo = null;
|
await this.askForPermission(data);
|
||||||
const isMedia = (type === 'media');
|
|
||||||
if (isMedia) {
|
|
||||||
this.setState({ mediaLoading: true });
|
|
||||||
const data = await RNFetchBlob.fs.stat(this.uriToPath(value));
|
|
||||||
fileInfo = {
|
|
||||||
name: data.filename,
|
|
||||||
description: '',
|
|
||||||
size: data.size,
|
|
||||||
mime: mime.lookup(data.path),
|
|
||||||
path: isIOS ? data.path : `file://${ data.path }`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
value, fileInfo, isMedia, mediaLoading: false
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
this.setState({ mediaLoading: false });
|
|
||||||
}
|
}
|
||||||
|
const info = await Promise.all(data.filter(item => item.type === 'media').map(file => FileSystem.getInfoAsync(this.uriToPath(file.value), { size: true })));
|
||||||
|
const attachments = info.map(file => ({
|
||||||
|
filename: file.uri.substring(file.uri.lastIndexOf('/') + 1),
|
||||||
|
description: '',
|
||||||
|
size: file.size,
|
||||||
|
mime: mime.lookup(file.uri),
|
||||||
|
path: file.uri
|
||||||
|
}));
|
||||||
|
const text = data.filter(item => item.type === 'text').reduce((acc, item) => `${ item.value }\n${ acc }`, '');
|
||||||
|
this.setState({
|
||||||
|
text,
|
||||||
|
attachments
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
this.getSubscriptions(server);
|
this.getSubscriptions(server);
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
@ -102,14 +100,11 @@ class ShareListView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { searching } = this.state;
|
const { searching, needsPermission } = this.state;
|
||||||
if (nextState.searching !== searching) {
|
if (nextState.searching !== searching) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextState.needsPermission !== needsPermission) {
|
||||||
const { isMedia } = this.state;
|
|
||||||
if (nextState.isMedia !== isMedia) {
|
|
||||||
this.getSubscriptions(nextProps.server, nextState.fileInfo);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,8 +186,7 @@ class ShareListView extends React.Component {
|
||||||
this.setState(...args);
|
this.setState(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscriptions = async(server, fileInfo) => {
|
getSubscriptions = async(server) => {
|
||||||
const { fileInfo: fileData } = this.state;
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const serversDB = database.servers;
|
const serversDB = database.servers;
|
||||||
|
|
||||||
|
@ -215,20 +209,29 @@ class ShareListView extends React.Component {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const canUploadFileResult = canUploadFile(fileInfo || fileData, serverInfo);
|
|
||||||
|
|
||||||
this.internalSetState({
|
this.internalSetState({
|
||||||
chats: this.chats ? this.chats.slice() : [],
|
chats: this.chats ? this.chats.slice() : [],
|
||||||
servers: this.servers ? this.servers.slice() : [],
|
servers: this.servers ? this.servers.slice() : [],
|
||||||
loading: false,
|
loading: false,
|
||||||
showError: !canUploadFileResult.success,
|
|
||||||
error: canUploadFileResult.error,
|
|
||||||
serverInfo
|
serverInfo
|
||||||
});
|
});
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
askForPermission = async(data) => {
|
||||||
|
const mediaIndex = data.findIndex(item => item.type === 'media');
|
||||||
|
if (mediaIndex !== -1) {
|
||||||
|
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, permission);
|
||||||
|
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
||||||
|
this.setState({ needsPermission: true });
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ needsPermission: false });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri);
|
uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri);
|
||||||
|
|
||||||
getRoomTitle = (item) => {
|
getRoomTitle = (item) => {
|
||||||
|
@ -237,16 +240,16 @@ class ShareListView extends React.Component {
|
||||||
return ((item.prid || useRealName) && item.fname) || item.name;
|
return ((item.prid || useRealName) && item.fname) || item.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
shareMessage = (item) => {
|
shareMessage = (room) => {
|
||||||
const { value, isMedia, fileInfo } = this.state;
|
const { attachments, text, serverInfo } = this.state;
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
|
|
||||||
navigation.navigate('ShareView', {
|
navigation.navigate('ShareView', {
|
||||||
rid: item.rid,
|
room,
|
||||||
value,
|
text,
|
||||||
isMedia,
|
attachments,
|
||||||
fileInfo,
|
serverInfo,
|
||||||
name: this.getRoomTitle(item)
|
isShareExtension: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,13 +308,13 @@ class ShareListView extends React.Component {
|
||||||
}}
|
}}
|
||||||
title={this.getRoomTitle(item)}
|
title={this.getRoomTitle(item)}
|
||||||
baseUrl={server}
|
baseUrl={server}
|
||||||
avatar={this.getRoomTitle(item)}
|
avatar={RocketChat.getRoomAvatar(item)}
|
||||||
description={
|
description={
|
||||||
item.t === 'c'
|
item.t === 'c'
|
||||||
? (item.topic || item.description)
|
? (item.topic || item.description)
|
||||||
: item.fname
|
: item.fname
|
||||||
}
|
}
|
||||||
type={item.t}
|
type={item.prid ? 'discussion' : item.t}
|
||||||
onPress={() => this.shareMessage(item)}
|
onPress={() => this.shareMessage(item)}
|
||||||
testID={`share-extension-item-${ item.name }`}
|
testID={`share-extension-item-${ item.name }`}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
@ -384,14 +387,26 @@ class ShareListView extends React.Component {
|
||||||
|
|
||||||
renderContent = () => {
|
renderContent = () => {
|
||||||
const {
|
const {
|
||||||
chats, mediaLoading, loading, searchResults, searching, searchText
|
chats, loading, searchResults, searching, searchText, needsPermission
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
|
||||||
if (mediaLoading || loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator theme={theme} />;
|
return <ActivityIndicator theme={theme} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsPermission) {
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
|
||||||
|
contentContainerStyle={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.permissionTitle, { color: themes[theme].titleText }]}>{permission.title}</Text>
|
||||||
|
<Text style={[styles.permissionMessage, { color: themes[theme].bodyText }]}>{permission.message}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={searching ? searchResults : chats}
|
data={searching ? searchResults : chats}
|
||||||
|
@ -414,42 +429,12 @@ class ShareListView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError = () => {
|
|
||||||
const {
|
|
||||||
fileInfo: file, loading, searching, error
|
|
||||||
} = this.state;
|
|
||||||
const { theme } = this.props;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <ActivityIndicator theme={theme} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
|
||||||
{ !searching
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
{this.renderSelectServer()}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<View style={[styles.container, styles.centered, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
|
||||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t(error)}</Text>
|
|
||||||
<CustomIcon name='cancel' size={120} color={themes[theme].dangerColor} />
|
|
||||||
<Text style={[styles.fileMime, { color: themes[theme].titleText }]}>{ file.mime }</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { showError } = this.state;
|
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<SafeAreaView theme={theme}>
|
<SafeAreaView theme={theme}>
|
||||||
<StatusBar theme={theme} />
|
<StatusBar theme={theme} />
|
||||||
{ showError ? this.renderError() : this.renderContent() }
|
{this.renderContent()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,5 +53,17 @@ export default StyleSheet.create({
|
||||||
title: {
|
title: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
...sharedStyles.textBold
|
...sharedStyles.textBold
|
||||||
|
},
|
||||||
|
permissionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginHorizontal: 30,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
permissionMessage: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginHorizontal: 30,
|
||||||
|
...sharedStyles.textRegular
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { withTheme } from '../../theme';
|
||||||
|
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
|
||||||
|
const androidMarginLeft = isTablet ? 0 : 4;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
marginRight: isAndroid ? 15 : 5,
|
||||||
|
marginLeft: isAndroid ? androidMarginLeft : -10,
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
inner: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textRegular,
|
||||||
|
marginRight: 4
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Header = React.memo(({ room, thread, theme }) => {
|
||||||
|
let type;
|
||||||
|
if (thread?.tmid) {
|
||||||
|
type = 'thread';
|
||||||
|
} else if (room?.prid) {
|
||||||
|
type = 'discussion';
|
||||||
|
} else {
|
||||||
|
type = room?.t;
|
||||||
|
}
|
||||||
|
let icon;
|
||||||
|
if (type === 'discussion') {
|
||||||
|
icon = 'chat';
|
||||||
|
} else if (type === 'thread') {
|
||||||
|
icon = 'threads';
|
||||||
|
} else if (type === 'c') {
|
||||||
|
icon = 'hash';
|
||||||
|
} else if (type === 'l') {
|
||||||
|
icon = 'livechat';
|
||||||
|
} else if (type === 'd') {
|
||||||
|
if (RocketChat.isGroupChat(room)) {
|
||||||
|
icon = 'team';
|
||||||
|
} else {
|
||||||
|
icon = 'at';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
icon = 'lock';
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColor = themes[theme].previewTintColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.inner}>
|
||||||
|
<Text numberOfLines={1} style={styles.text}>
|
||||||
|
<Text style={[styles.text, { color: textColor }]} numberOfLines={1}>{I18n.t('Sending_to')} </Text>
|
||||||
|
<CustomIcon
|
||||||
|
name={icon}
|
||||||
|
size={16}
|
||||||
|
color={textColor}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[styles.name, { color: textColor }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{thread?.msg ?? RocketChat.getRoomTitle(room)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Header.propTypes = {
|
||||||
|
room: PropTypes.object,
|
||||||
|
thread: PropTypes.object,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme(Header);
|
|
@ -0,0 +1,136 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Video } from 'expo-av';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { ScrollView, Text, StyleSheet } from 'react-native';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
import { ImageViewer, types } from '../../presentation/ImageViewer';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { useDimensions, useOrientation } from '../../dimensions';
|
||||||
|
import { getHeaderHeight } from '../../containers/Header';
|
||||||
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
|
import { THUMBS_HEIGHT } from './constants';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
import { allowPreview } from './utils';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
fileContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
fileName: {
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginHorizontal: 10,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
fileSize: {
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconPreview = React.memo(({
|
||||||
|
iconName, title, description, theme, width, height, danger
|
||||||
|
}) => (
|
||||||
|
<ScrollView
|
||||||
|
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
|
||||||
|
contentContainerStyle={[styles.fileContainer, { width, height }]}
|
||||||
|
>
|
||||||
|
<CustomIcon
|
||||||
|
name={iconName}
|
||||||
|
size={56}
|
||||||
|
color={danger ? themes[theme].dangerColor : themes[theme].tintColor}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.fileName, { color: themes[theme].titleText }]}>{title}</Text>
|
||||||
|
{description ? <Text style={[styles.fileSize, { color: themes[theme].bodyText }]}>{description}</Text> : null}
|
||||||
|
</ScrollView>
|
||||||
|
));
|
||||||
|
|
||||||
|
const Preview = React.memo(({
|
||||||
|
item, theme, isShareExtension, length
|
||||||
|
}) => {
|
||||||
|
const type = item?.mime;
|
||||||
|
const { width, height } = useDimensions();
|
||||||
|
const { isLandscape } = useOrientation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const headerHeight = getHeaderHeight(isLandscape);
|
||||||
|
const messageboxHeight = isIOS ? 56 : 0;
|
||||||
|
const thumbsHeight = (length > 1) ? THUMBS_HEIGHT : 0;
|
||||||
|
const calculatedHeight = height - insets.top - insets.bottom - messageboxHeight - thumbsHeight - headerHeight;
|
||||||
|
|
||||||
|
if (item?.canUpload) {
|
||||||
|
if (type?.match(/video/)) {
|
||||||
|
return (
|
||||||
|
<Video
|
||||||
|
source={{ uri: item.path }}
|
||||||
|
rate={1.0}
|
||||||
|
volume={1.0}
|
||||||
|
isMuted={false}
|
||||||
|
resizeMode={Video.RESIZE_MODE_CONTAIN}
|
||||||
|
isLooping={false}
|
||||||
|
style={{ width, height: calculatedHeight }}
|
||||||
|
useNativeControls
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow preview of images too big in order to prevent memory issues on iOS share extension
|
||||||
|
if (allowPreview(isShareExtension, item?.size)) {
|
||||||
|
if (type?.match(/image/)) {
|
||||||
|
return (
|
||||||
|
<ImageViewer
|
||||||
|
uri={item.path}
|
||||||
|
imageComponentType={isShareExtension ? types.REACT_NATIVE_IMAGE : types.FAST_IMAGE}
|
||||||
|
width={width}
|
||||||
|
height={calculatedHeight}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<IconPreview
|
||||||
|
iconName={type?.match(/image/) ? 'Camera' : 'clip'}
|
||||||
|
title={item?.filename}
|
||||||
|
description={prettyBytes(item?.size ?? 0)}
|
||||||
|
theme={theme}
|
||||||
|
width={width}
|
||||||
|
height={calculatedHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconPreview
|
||||||
|
iconName='warning'
|
||||||
|
title={I18n.t(item?.error)}
|
||||||
|
description={prettyBytes(item?.size ?? 0)}
|
||||||
|
theme={theme}
|
||||||
|
width={width}
|
||||||
|
height={calculatedHeight}
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Preview.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isShareExtension: PropTypes.bool,
|
||||||
|
length: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
IconPreview.propTypes = {
|
||||||
|
iconName: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
danger: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Preview;
|
|
@ -0,0 +1,191 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
FlatList, Image, View, StyleSheet
|
||||||
|
} from 'react-native';
|
||||||
|
import { RectButton, TouchableOpacity, TouchableNativeFeedback } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import { BUTTON_HIT_SLOP } from '../../containers/message/utils';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
|
import { THUMBS_HEIGHT } from './constants';
|
||||||
|
import { allowPreview } from './utils';
|
||||||
|
|
||||||
|
const THUMB_SIZE = 64;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
list: {
|
||||||
|
height: THUMBS_HEIGHT,
|
||||||
|
paddingHorizontal: 8
|
||||||
|
},
|
||||||
|
videoThumbIcon: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
bottom: 0
|
||||||
|
},
|
||||||
|
dangerIcon: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 16,
|
||||||
|
bottom: 0
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 6,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
removeView: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
paddingTop: 8
|
||||||
|
},
|
||||||
|
thumb: {
|
||||||
|
width: THUMB_SIZE,
|
||||||
|
height: THUMB_SIZE,
|
||||||
|
borderRadius: 2,
|
||||||
|
marginRight: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ThumbButton = isIOS ? TouchableOpacity : TouchableNativeFeedback;
|
||||||
|
|
||||||
|
const ThumbContent = React.memo(({ item, theme, isShareExtension }) => {
|
||||||
|
const type = item?.mime;
|
||||||
|
|
||||||
|
if (type?.match(/image/)) {
|
||||||
|
// Disallow preview of images too big in order to prevent memory issues on iOS share extension
|
||||||
|
if (allowPreview(isShareExtension, item?.size)) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.path }}
|
||||||
|
style={[styles.thumb, { borderColor: themes[theme].borderColor }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<View style={[styles.thumb, { borderColor: themes[theme].borderColor }]}>
|
||||||
|
<CustomIcon
|
||||||
|
name='Camera'
|
||||||
|
size={30}
|
||||||
|
color={themes[theme].tintColor}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type?.match(/video/)) {
|
||||||
|
const { uri } = item;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Image source={{ uri }} style={styles.thumb} />
|
||||||
|
<CustomIcon
|
||||||
|
name='video-1'
|
||||||
|
size={20}
|
||||||
|
color={themes[theme].buttonText}
|
||||||
|
style={styles.videoThumbIcon}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple files upload of files different than image/video is not implemented, so there's no thumb
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Thumb = ({
|
||||||
|
item, theme, isShareExtension, onPress, onRemove
|
||||||
|
}) => (
|
||||||
|
<ThumbButton style={styles.item} onPress={() => onPress(item)} activeOpacity={0.7}>
|
||||||
|
<>
|
||||||
|
<ThumbContent
|
||||||
|
item={item}
|
||||||
|
theme={theme}
|
||||||
|
isShareExtension={isShareExtension}
|
||||||
|
/>
|
||||||
|
<RectButton
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
style={[styles.removeButton, { backgroundColor: themes[theme].bodyText, borderColor: themes[theme].auxiliaryBackground }]}
|
||||||
|
activeOpacity={1}
|
||||||
|
rippleColor={themes[theme].bannerBackground}
|
||||||
|
onPress={() => onRemove(item)}
|
||||||
|
>
|
||||||
|
<View style={[styles.removeView, { borderColor: themes[theme].auxiliaryBackground }]}>
|
||||||
|
<CustomIcon
|
||||||
|
name='Cross'
|
||||||
|
color={themes[theme].backgroundColor}
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</RectButton>
|
||||||
|
{!item?.canUpload ? (
|
||||||
|
<CustomIcon
|
||||||
|
name='warning'
|
||||||
|
size={20}
|
||||||
|
color={themes[theme].dangerColor}
|
||||||
|
style={styles.dangerIcon}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
</ThumbButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Thumbs = React.memo(({
|
||||||
|
attachments, theme, isShareExtension, onPress, onRemove
|
||||||
|
}) => {
|
||||||
|
if (attachments?.length > 1) {
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
data={attachments}
|
||||||
|
keyExtractor={item => item.path}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Thumb
|
||||||
|
item={item}
|
||||||
|
theme={theme}
|
||||||
|
isShareExtension={isShareExtension}
|
||||||
|
onPress={() => onPress(item)}
|
||||||
|
onRemove={() => onRemove(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
style={[styles.list, { backgroundColor: themes[theme].messageboxBackground }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Thumbs.propTypes = {
|
||||||
|
attachments: PropTypes.array,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isShareExtension: PropTypes.bool,
|
||||||
|
onPress: PropTypes.func,
|
||||||
|
onRemove: PropTypes.func
|
||||||
|
};
|
||||||
|
Thumb.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isShareExtension: PropTypes.bool,
|
||||||
|
onPress: PropTypes.func,
|
||||||
|
onRemove: PropTypes.func
|
||||||
|
};
|
||||||
|
ThumbContent.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isShareExtension: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Thumbs;
|
|
@ -0,0 +1 @@
|
||||||
|
export const THUMBS_HEIGHT = 74;
|
|
@ -1,307 +1,358 @@
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, Text, Image } from 'react-native';
|
import { View, Text, NativeModules } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ShareExtension from 'rn-extensions-share';
|
import ShareExtension from 'rn-extensions-share';
|
||||||
|
import * as VideoThumbnails from 'expo-video-thumbnails';
|
||||||
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
|
||||||
import log from '../../utils/log';
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import TextInput from '../../containers/TextInput';
|
import Loading from '../../containers/Loading';
|
||||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
import {
|
||||||
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
Item,
|
||||||
|
CloseModalButton,
|
||||||
|
CustomHeaderButtons
|
||||||
|
} from '../../containers/HeaderButton';
|
||||||
import { isBlocked } from '../../utils/room';
|
import { isBlocked } from '../../utils/room';
|
||||||
import { isReadOnly } from '../../utils/isReadOnly';
|
import { isReadOnly } from '../../utils/isReadOnly';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import Header from './Header';
|
||||||
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
import TextInput from '../../containers/TextInput';
|
||||||
|
import Preview from './Preview';
|
||||||
|
import Thumbs from './Thumbs';
|
||||||
|
import MessageBox from '../../containers/MessageBox';
|
||||||
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import { getUserSelector } from '../../selectors/login';
|
||||||
|
import StatusBar from '../../containers/StatusBar';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
import { canUploadFile } from '../../utils/media';
|
||||||
|
|
||||||
class ShareView extends React.Component {
|
class ShareView extends Component {
|
||||||
static propTypes = {
|
|
||||||
navigation: PropTypes.object,
|
|
||||||
route: PropTypes.object,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
user: PropTypes.shape({
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
token: PropTypes.string.isRequired
|
|
||||||
}),
|
|
||||||
server: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { route } = this.props;
|
this.messagebox = React.createRef();
|
||||||
const rid = route.params?.rid;
|
this.files = props.route.params?.attachments ?? [];
|
||||||
const name = route.params?.name;
|
this.isShareExtension = props.route.params?.isShareExtension;
|
||||||
const value = route.params?.value;
|
this.serverInfo = props.route.params?.serverInfo ?? {};
|
||||||
const isMedia = route.params?.isMedia ?? false;
|
|
||||||
const fileInfo = route.params?.fileInfo ?? {};
|
|
||||||
const room = route.params?.room ?? { rid };
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
rid,
|
selected: {},
|
||||||
value,
|
|
||||||
isMedia,
|
|
||||||
name,
|
|
||||||
fileInfo,
|
|
||||||
room,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
file: {
|
attachments: [],
|
||||||
name: fileInfo ? fileInfo.name : '',
|
text: props.route.params?.text ?? '',
|
||||||
description: ''
|
room: props.route.params?.room ?? {},
|
||||||
},
|
thread: props.route.params?.thread ?? {},
|
||||||
canSend: false
|
maxFileSize: this.isShareExtension ? this.serverInfo?.FileUpload_MaxFileSize : props.FileUpload_MaxFileSize,
|
||||||
|
mediaAllowList: this.isShareExtension ? this.serverInfo?.FileUpload_MediaTypeWhiteList : props.FileUpload_MediaTypeWhiteList
|
||||||
};
|
};
|
||||||
|
this.getServerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
this.setReadOnly();
|
componentDidMount = async() => {
|
||||||
this.setHeader();
|
const readOnly = await this.getReadOnly();
|
||||||
|
const { attachments, selected } = await this.getAttachments();
|
||||||
|
this.setState({ readOnly, attachments, selected }, () => this.setHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { canSend } = this.state;
|
const {
|
||||||
const { navigation } = this.props;
|
room, thread, readOnly, attachments
|
||||||
|
} = this.state;
|
||||||
|
const { navigation, theme } = this.props;
|
||||||
|
|
||||||
navigation.setOptions({
|
const options = {
|
||||||
title: I18n.t('Share'),
|
headerTitle: () => <Header room={room} thread={thread} />,
|
||||||
headerRight:
|
headerTitleAlign: 'left',
|
||||||
() => (canSend
|
headerTintColor: themes[theme].previewTintColor
|
||||||
? (
|
};
|
||||||
<CustomHeaderButtons>
|
|
||||||
<Item
|
// if is share extension show default back button
|
||||||
title={I18n.t('Send')}
|
if (!this.isShareExtension) {
|
||||||
onPress={this.sendMessage}
|
options.headerLeft = () => <CloseModalButton navigation={navigation} buttonStyle={{ color: themes[theme].previewTintColor }} />;
|
||||||
testID='send-message-share-view'
|
}
|
||||||
buttonStyle={styles.send}
|
|
||||||
/>
|
if (!attachments.length && !readOnly) {
|
||||||
</CustomHeaderButtons>
|
options.headerRight = () => (
|
||||||
)
|
<CustomHeaderButtons>
|
||||||
: null)
|
<Item
|
||||||
});
|
title={I18n.t('Send')}
|
||||||
|
onPress={this.send}
|
||||||
|
buttonStyle={[styles.send, { color: themes[theme].previewTintColor }]}
|
||||||
|
/>
|
||||||
|
</CustomHeaderButtons>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headerBackground = () => <View style={[styles.container, { backgroundColor: themes[theme].previewBackground }]} />;
|
||||||
|
|
||||||
|
navigation.setOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReadOnly = async() => {
|
// fetch server info
|
||||||
|
getServerInfo = async() => {
|
||||||
|
const { server } = this.props;
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const serversCollection = serversDB.collections.get('servers');
|
||||||
|
try {
|
||||||
|
this.serverInfo = await serversCollection.find(server);
|
||||||
|
} catch (error) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getReadOnly = async() => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
const { username } = user;
|
const readOnly = await isReadOnly(room, user);
|
||||||
const readOnly = await isReadOnly(room, { username });
|
return readOnly;
|
||||||
|
|
||||||
this.setState({ readOnly, canSend: !(readOnly || isBlocked(room)) }, () => this.setHeader());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bytesToSize = bytes => `${ (bytes / 1048576).toFixed(2) }MB`;
|
getAttachments = async() => {
|
||||||
|
const { mediaAllowList, maxFileSize } = this.state;
|
||||||
|
const items = await Promise.all(this.files.map(async(item) => {
|
||||||
|
// Check server settings
|
||||||
|
const { success: canUpload, error } = canUploadFile(item, mediaAllowList, maxFileSize);
|
||||||
|
item.canUpload = canUpload;
|
||||||
|
item.error = error;
|
||||||
|
|
||||||
sendMessage = async() => {
|
// get video thumbnails
|
||||||
const { isMedia, loading } = this.state;
|
if (item.mime?.match(/video/)) {
|
||||||
|
try {
|
||||||
|
const { uri } = await VideoThumbnails.getThumbnailAsync(item.path);
|
||||||
|
item.uri = uri;
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a filename, if there isn't any
|
||||||
|
if (!item.filename) {
|
||||||
|
item.filename = new Date().toISOString();
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
attachments: items,
|
||||||
|
selected: items[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
send = async() => {
|
||||||
|
const { loading, selected } = this.state;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ loading: true });
|
// update state
|
||||||
if (isMedia) {
|
await this.selectFile(selected);
|
||||||
await this.sendMediaMessage();
|
|
||||||
|
const {
|
||||||
|
attachments, room, text, thread
|
||||||
|
} = this.state;
|
||||||
|
const { navigation, server, user } = this.props;
|
||||||
|
|
||||||
|
// if it's share extension this should show loading
|
||||||
|
if (this.isShareExtension) {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
// if it's not share extension this can close
|
||||||
} else {
|
} else {
|
||||||
await this.sendTextMessage();
|
navigation.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ loading: false });
|
try {
|
||||||
ShareExtension.close();
|
// Send attachment
|
||||||
}
|
if (attachments.length) {
|
||||||
|
await Promise.all(attachments.map(({
|
||||||
|
filename: name,
|
||||||
|
mime: type,
|
||||||
|
description,
|
||||||
|
size,
|
||||||
|
path,
|
||||||
|
canUpload
|
||||||
|
}) => {
|
||||||
|
if (canUpload) {
|
||||||
|
return RocketChat.sendFileMessage(
|
||||||
|
room.rid,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
store: 'Uploads'
|
||||||
|
},
|
||||||
|
thread?.tmid,
|
||||||
|
server,
|
||||||
|
{ id: user.id, token: user.token }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}));
|
||||||
|
|
||||||
sendMediaMessage = async() => {
|
// Send text message
|
||||||
const { rid, fileInfo, file } = this.state;
|
} else if (text.length) {
|
||||||
const { server, user } = this.props;
|
await RocketChat.sendMessage(room.rid, text, thread?.tmid, { id: user.id, token: user.token });
|
||||||
const { name, description } = file;
|
|
||||||
const fileMessage = {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
size: fileInfo.size,
|
|
||||||
type: fileInfo.mime,
|
|
||||||
store: 'Uploads',
|
|
||||||
path: fileInfo.path
|
|
||||||
};
|
|
||||||
if (fileInfo && rid !== '') {
|
|
||||||
try {
|
|
||||||
await RocketChat.sendFileMessage(rid, fileMessage, undefined, server, user);
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
sendTextMessage = async() => {
|
// if it's share extension this should close
|
||||||
const { value, rid } = this.state;
|
if (this.isShareExtension) {
|
||||||
const { user } = this.props;
|
ShareExtension.close();
|
||||||
if (value !== '' && rid !== '') {
|
|
||||||
try {
|
|
||||||
await RocketChat.sendMessage(rid, value, undefined, user);
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderPreview = () => {
|
selectFile = (item) => {
|
||||||
const { fileInfo } = this.state;
|
const { attachments, selected } = this.state;
|
||||||
const { theme } = this.props;
|
if (attachments.length > 0) {
|
||||||
|
const { text } = this.messagebox.current;
|
||||||
|
const newAttachments = attachments.map((att) => {
|
||||||
|
if (att.path === selected.path) {
|
||||||
|
att.description = text;
|
||||||
|
}
|
||||||
|
return att;
|
||||||
|
});
|
||||||
|
return this.setState({ attachments: newAttachments, selected: item });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const icon = fileInfo.mime.match(/image/)
|
removeFile = (item) => {
|
||||||
? <Image source={{ isStatic: true, uri: fileInfo.path }} style={styles.mediaImage} />
|
const { selected, attachments } = this.state;
|
||||||
: (
|
let newSelected;
|
||||||
<View style={styles.mediaIconContainer}>
|
if (item.path === selected.path) {
|
||||||
<CustomIcon name='clip' style={styles.mediaIcon} />
|
const selectedIndex = attachments.findIndex(att => att.path === selected.path);
|
||||||
|
// Selects the next one, if available
|
||||||
|
if (attachments[selectedIndex + 1]?.path) {
|
||||||
|
newSelected = attachments[selectedIndex + 1];
|
||||||
|
// If it's the last thumb, selects the previous one
|
||||||
|
} else {
|
||||||
|
newSelected = attachments[selectedIndex - 1] || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ attachments: attachments.filter(att => att.path !== item.path), selected: newSelected ?? selected });
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeText = (text) => {
|
||||||
|
this.setState({ text });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent = () => {
|
||||||
|
const {
|
||||||
|
attachments, selected, room, text
|
||||||
|
} = this.state;
|
||||||
|
const { theme, navigation } = this.props;
|
||||||
|
|
||||||
|
if (attachments.length) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Preview
|
||||||
|
// using key just to reset zoom/move after change selected
|
||||||
|
key={selected?.path}
|
||||||
|
item={selected}
|
||||||
|
length={attachments.length}
|
||||||
|
theme={theme}
|
||||||
|
isShareExtension={this.isShareExtension}
|
||||||
|
/>
|
||||||
|
<MessageBox
|
||||||
|
showSend
|
||||||
|
sharing
|
||||||
|
ref={this.messagebox}
|
||||||
|
rid={room.rid}
|
||||||
|
roomType={room.t}
|
||||||
|
theme={theme}
|
||||||
|
onSubmit={this.send}
|
||||||
|
message={{ msg: selected?.description ?? '' }}
|
||||||
|
navigation={navigation}
|
||||||
|
isFocused={navigation.isFocused}
|
||||||
|
iOSScrollBehavior={NativeModules.KeyboardTrackingViewManager?.KeyboardTrackingScrollBehaviorNone}
|
||||||
|
isActionsEnabled={false}
|
||||||
|
>
|
||||||
|
<Thumbs
|
||||||
|
attachments={attachments}
|
||||||
|
theme={theme}
|
||||||
|
isShareExtension={this.isShareExtension}
|
||||||
|
onPress={this.selectFile}
|
||||||
|
onRemove={this.removeFile}
|
||||||
|
/>
|
||||||
|
</MessageBox>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.mediaContent,
|
|
||||||
{
|
|
||||||
borderColor: themes[theme].separatorColor,
|
|
||||||
backgroundColor: themes[theme].auxiliaryBackground
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<View style={styles.mediaInfo}>
|
|
||||||
<Text style={[styles.mediaText, { color: themes[theme].titleText }]} numberOfLines={1}>{fileInfo.name}</Text>
|
|
||||||
<Text style={[styles.mediaText, { color: themes[theme].titleText }]}>{this.bytesToSize(fileInfo.size)}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMediaContent = () => {
|
|
||||||
const { fileInfo, file } = this.state;
|
|
||||||
const { theme } = this.props;
|
|
||||||
const inputStyle = {
|
|
||||||
backgroundColor: themes[theme].focusedBackground,
|
|
||||||
borderColor: themes[theme].separatorColor
|
|
||||||
};
|
|
||||||
return fileInfo ? (
|
|
||||||
<View style={styles.mediaContainer}>
|
|
||||||
{this.renderPreview()}
|
|
||||||
<View style={styles.mediaInputContent}>
|
|
||||||
<TextInput
|
|
||||||
inputStyle={[
|
|
||||||
styles.mediaNameInput,
|
|
||||||
styles.input,
|
|
||||||
styles.firstInput,
|
|
||||||
inputStyle
|
|
||||||
]}
|
|
||||||
placeholder={I18n.t('File_name')}
|
|
||||||
onChangeText={name => this.setState({ file: { ...file, name } })}
|
|
||||||
defaultValue={file.name}
|
|
||||||
containerStyle={styles.inputContainer}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
inputStyle={[
|
|
||||||
styles.mediaDescriptionInput,
|
|
||||||
styles.input,
|
|
||||||
inputStyle
|
|
||||||
]}
|
|
||||||
placeholder={I18n.t('File_description')}
|
|
||||||
onChangeText={description => this.setState({ file: { ...file, description } })}
|
|
||||||
defaultValue={file.description}
|
|
||||||
multiline
|
|
||||||
textAlignVertical='top'
|
|
||||||
autoFocus
|
|
||||||
containerStyle={styles.inputContainer}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderInput = () => {
|
|
||||||
const { value } = this.state;
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
containerStyle={[styles.content, styles.inputContainer]}
|
containerStyle={styles.inputContainer}
|
||||||
inputStyle={[
|
inputStyle={[
|
||||||
styles.input,
|
styles.input,
|
||||||
styles.textInput,
|
styles.textInput,
|
||||||
{
|
{ backgroundColor: themes[theme].focusedBackground }
|
||||||
borderColor: themes[theme].separatorColor,
|
|
||||||
backgroundColor: themes[theme].focusedBackground
|
|
||||||
}
|
|
||||||
]}
|
]}
|
||||||
placeholder=''
|
placeholder=''
|
||||||
onChangeText={handleText => this.setState({ value: handleText })}
|
onChangeText={this.onChangeText}
|
||||||
defaultValue={value}
|
defaultValue=''
|
||||||
multiline
|
multiline
|
||||||
textAlignVertical='top'
|
textAlignVertical='top'
|
||||||
autoFocus
|
autoFocus
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
value={text}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderError = () => {
|
|
||||||
const { room } = this.state;
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}>
|
|
||||||
<Text style={styles.title}>
|
|
||||||
{
|
|
||||||
isBlocked(room) ? I18n.t('This_room_is_blocked') : I18n.t('This_room_is_read_only')
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
|
const { readOnly, room, loading } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
const {
|
|
||||||
name, loading, isMedia, room, readOnly
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (readOnly || isBlocked(room)) {
|
if (readOnly || isBlocked(room)) {
|
||||||
return this.renderError();
|
return (
|
||||||
}
|
<View style={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||||
|
<Text style={[styles.title, { color: themes[theme].titleText }]}>
|
||||||
return (
|
{isBlocked(room) ? I18n.t('This_room_is_blocked') : I18n.t('This_room_is_read_only')}
|
||||||
<View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
isMedia
|
|
||||||
? styles.toContent
|
|
||||||
: styles.toContentText,
|
|
||||||
{
|
|
||||||
backgroundColor: isMedia
|
|
||||||
? themes[theme].focusedBackground
|
|
||||||
: themes[theme].auxiliaryBackground
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.text} numberOfLines={1}>
|
|
||||||
<Text style={[styles.to, { color: themes[theme].auxiliaryText }]}>{`${ I18n.t('To') }: `}</Text>
|
|
||||||
<Text style={[styles.name, { color: themes[theme].titleText }]}>{`${ name }`}</Text>
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.content, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
);
|
||||||
{isMedia ? this.renderMediaContent() : this.renderInput()}
|
}
|
||||||
</View>
|
return (
|
||||||
{ loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null }
|
<SafeAreaView
|
||||||
</View>
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
|
<StatusBar barStyle='light-content' backgroundColor={themes[theme].previewBackground} />
|
||||||
|
{this.renderContent()}
|
||||||
|
<Loading visible={loading} />
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (({ share }) => ({
|
ShareView.propTypes = {
|
||||||
user: {
|
navigation: PropTypes.object,
|
||||||
id: share.user && share.user.id,
|
route: PropTypes.object,
|
||||||
username: share.user && share.user.username,
|
theme: PropTypes.string,
|
||||||
token: share.user && share.user.token
|
user: PropTypes.shape({
|
||||||
},
|
id: PropTypes.string.isRequired,
|
||||||
server: share.server
|
username: PropTypes.string.isRequired,
|
||||||
}));
|
token: PropTypes.string.isRequired
|
||||||
|
}),
|
||||||
|
server: PropTypes.string,
|
||||||
|
FileUpload_MediaTypeWhiteList: PropTypes.string,
|
||||||
|
FileUpload_MaxFileSize: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
user: getUserSelector(state),
|
||||||
|
server: state.share.server || state.server.server,
|
||||||
|
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
|
||||||
|
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(ShareView));
|
export default connect(mapStateToProps)(withTheme(ShareView));
|
||||||
|
|
|
@ -6,6 +6,16 @@ export default StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
|
input: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: 0
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
centered: {
|
centered: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
|
@ -15,82 +25,6 @@ export default StyleSheet.create({
|
||||||
...sharedStyles.textBold,
|
...sharedStyles.textBold,
|
||||||
...sharedStyles.textAlignCenter
|
...sharedStyles.textAlignCenter
|
||||||
},
|
},
|
||||||
text: {
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 8,
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
toContent: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
toContentText: {
|
|
||||||
width: '100%',
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
mediaContainer: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
mediaContent: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
padding: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
...sharedStyles.separatorTop
|
|
||||||
},
|
|
||||||
mediaImage: {
|
|
||||||
height: 64,
|
|
||||||
width: 64
|
|
||||||
},
|
|
||||||
mediaIcon: {
|
|
||||||
fontSize: 64
|
|
||||||
},
|
|
||||||
mediaIconContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
mediaInfo: {
|
|
||||||
marginLeft: 16,
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
mediaText: {
|
|
||||||
fontSize: 16,
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
mediaInputContent: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
fontSize: 16,
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
inputContainer: {
|
|
||||||
marginBottom: 0
|
|
||||||
},
|
|
||||||
firstInput: {
|
|
||||||
borderBottomWidth: 0
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
height: '100%'
|
|
||||||
},
|
|
||||||
mediaNameInput: {
|
|
||||||
paddingLeft: 16,
|
|
||||||
paddingRight: 16,
|
|
||||||
paddingVertical: 8
|
|
||||||
},
|
|
||||||
mediaDescriptionInput: {
|
|
||||||
paddingLeft: 16,
|
|
||||||
paddingRight: 16,
|
|
||||||
paddingVertical: 8,
|
|
||||||
height: 100
|
|
||||||
},
|
|
||||||
send: {
|
send: {
|
||||||
...sharedStyles.textSemibold,
|
...sharedStyles.textSemibold,
|
||||||
fontSize: 16
|
fontSize: 16
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { isAndroid } from '../../utils/deviceInfo';
|
||||||
|
|
||||||
|
// Limit preview to 3MB on iOS share extension
|
||||||
|
export const allowPreview = (isShareExtension, size) => isAndroid || !isShareExtension || size < 3000000;
|
|
@ -34,6 +34,9 @@ PODS:
|
||||||
- EXPermissions (8.1.0):
|
- EXPermissions (8.1.0):
|
||||||
- UMCore
|
- UMCore
|
||||||
- UMPermissionsInterface
|
- UMPermissionsInterface
|
||||||
|
- EXVideoThumbnails (4.1.1):
|
||||||
|
- UMCore
|
||||||
|
- UMFileSystemInterface
|
||||||
- EXWebBrowser (8.2.1):
|
- EXWebBrowser (8.2.1):
|
||||||
- UMCore
|
- UMCore
|
||||||
- Fabric (1.10.2)
|
- Fabric (1.10.2)
|
||||||
|
@ -431,7 +434,7 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||||
- React
|
- React
|
||||||
- rn-extensions-share (2.3.10):
|
- rn-extensions-share (2.4.0):
|
||||||
- React
|
- React
|
||||||
- rn-fetch-blob (0.12.0):
|
- rn-fetch-blob (0.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -462,15 +465,15 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNGestureHandler (1.6.1):
|
- RNGestureHandler (1.6.1):
|
||||||
- React
|
- React
|
||||||
- RNImageCropPicker (0.30.0):
|
- RNImageCropPicker (0.31.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RNImageCropPicker/QBImagePickerController (= 0.30.0)
|
- RNImageCropPicker/QBImagePickerController (= 0.31.1)
|
||||||
- RSKImageCropper
|
- TOCropViewController
|
||||||
- RNImageCropPicker/QBImagePickerController (0.30.0):
|
- RNImageCropPicker/QBImagePickerController (0.31.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RSKImageCropper
|
- TOCropViewController
|
||||||
- RNLocalize (1.4.0):
|
- RNLocalize (1.4.0):
|
||||||
- React
|
- React
|
||||||
- RNReanimated (1.8.0):
|
- RNReanimated (1.8.0):
|
||||||
|
@ -483,13 +486,13 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNVectorIcons (6.6.0):
|
- RNVectorIcons (6.6.0):
|
||||||
- React
|
- React
|
||||||
- RSKImageCropper (2.2.3)
|
|
||||||
- SDWebImage (5.7.4):
|
- SDWebImage (5.7.4):
|
||||||
- SDWebImage/Core (= 5.7.4)
|
- SDWebImage/Core (= 5.7.4)
|
||||||
- SDWebImage/Core (5.7.4)
|
- SDWebImage/Core (5.7.4)
|
||||||
- SDWebImageWebPCoder (0.4.1):
|
- SDWebImageWebPCoder (0.4.1):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.5)
|
- SDWebImage/Core (~> 5.5)
|
||||||
|
- TOCropViewController (2.5.2)
|
||||||
- UMAppLoader (1.0.2)
|
- UMAppLoader (1.0.2)
|
||||||
- UMBarCodeScannerInterface (5.1.0)
|
- UMBarCodeScannerInterface (5.1.0)
|
||||||
- UMCameraInterface (5.1.0)
|
- UMCameraInterface (5.1.0)
|
||||||
|
@ -522,6 +525,7 @@ DEPENDENCIES:
|
||||||
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||||
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
||||||
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
||||||
|
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||||
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
||||||
|
@ -644,9 +648,9 @@ SPEC REPOS:
|
||||||
- nanopb
|
- nanopb
|
||||||
- OpenSSL-Universal
|
- OpenSSL-Universal
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- RSKImageCropper
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
|
- TOCropViewController
|
||||||
- YogaKit
|
- YogaKit
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
@ -670,6 +674,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-local-authentication/ios"
|
:path: "../node_modules/expo-local-authentication/ios"
|
||||||
EXPermissions:
|
EXPermissions:
|
||||||
:path: "../node_modules/expo-permissions/ios"
|
:path: "../node_modules/expo-permissions/ios"
|
||||||
|
EXVideoThumbnails:
|
||||||
|
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||||
EXWebBrowser:
|
EXWebBrowser:
|
||||||
:path: "../node_modules/expo-web-browser/ios"
|
:path: "../node_modules/expo-web-browser/ios"
|
||||||
FBLazyVector:
|
FBLazyVector:
|
||||||
|
@ -833,6 +839,7 @@ SPEC CHECKSUMS:
|
||||||
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
||||||
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
||||||
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
||||||
|
EXVideoThumbnails: be6984a3cda1e44c45b5c6278244e99855f99a0a
|
||||||
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
||||||
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
||||||
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
||||||
|
@ -894,7 +901,7 @@ SPEC CHECKSUMS:
|
||||||
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
||||||
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
||||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||||
rn-extensions-share: 4bfee75806ad54aadeff1dfa535697a6345a50b8
|
rn-extensions-share: 8db79372089567cbc5aefe8444869bbc808578d3
|
||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
||||||
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
||||||
|
@ -905,16 +912,16 @@ SPEC CHECKSUMS:
|
||||||
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
||||||
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
||||||
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
||||||
RNImageCropPicker: a606d65f71c6c05caa3c850c16fb1ba2a4718608
|
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
|
||||||
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
||||||
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
||||||
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
||||||
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
||||||
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
||||||
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
||||||
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
|
|
||||||
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
||||||
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
||||||
|
TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
|
||||||
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
||||||
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
||||||
UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d
|
UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/expo-video-thumbnails/ios/EXVideoThumbnails/EXVideoThumbnailsModule.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-image-crop-picker/ios/src/UIImage+Extension.h
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/CGGeometry+RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController+Protected.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageScrollView.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKInternalUtility.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKTouchView.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/UIApplication+RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/UIImage+RSKImageCropper.h
|
|
1
ios/Pods/Headers/Private/TOCropViewController/TOActivityCroppedImageProvider.h
generated
Symbolic link
1
ios/Pods/Headers/Private/TOCropViewController/TOActivityCroppedImageProvider.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropOverlayView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropScrollView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropToolbar.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Constants/TOCropViewConstants.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/TOCropViewController.h
|
1
ios/Pods/Headers/Private/TOCropViewController/TOCropViewControllerTransitioning.h
generated
Symbolic link
1
ios/Pods/Headers/Private/TOCropViewController/TOCropViewControllerTransitioning.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/expo-video-thumbnails/ios/EXVideoThumbnails/EXVideoThumbnailsModule.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-image-crop-picker/ios/src/UIImage+Extension.h
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/CGGeometry+RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController+Protected.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKImageScrollView.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKInternalUtility.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/RSKTouchView.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/UIApplication+RSKImageCropper.h
|
|
|
@ -1 +0,0 @@
|
||||||
../../../RSKImageCropper/RSKImageCropper/UIImage+RSKImageCropper.h
|
|
1
ios/Pods/Headers/Public/TOCropViewController/TOActivityCroppedImageProvider.h
generated
Symbolic link
1
ios/Pods/Headers/Public/TOCropViewController/TOActivityCroppedImageProvider.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropOverlayView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropScrollView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropToolbar.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropView.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Constants/TOCropViewConstants.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/TOCropViewController.h
|
1
ios/Pods/Headers/Public/TOCropViewController/TOCropViewControllerTransitioning.h
generated
Symbolic link
1
ios/Pods/Headers/Public/TOCropViewController/TOCropViewControllerTransitioning.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../TOCropViewController/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "EXVideoThumbnails",
|
||||||
|
"version": "4.1.1",
|
||||||
|
"summary": "ExpoVideoThumbnails standalone module",
|
||||||
|
"description": "ExpoVideoThumbnails standalone module",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": "650 Industries, Inc.",
|
||||||
|
"homepage": "https://github.com/expo/expo/tree/master/packages/expo-video-thumbnails",
|
||||||
|
"platforms": {
|
||||||
|
"ios": "10.0"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"git": "https://github.com/expo/expo.git"
|
||||||
|
},
|
||||||
|
"source_files": "EXVideoThumbnails/**/*.{h,m}",
|
||||||
|
"preserve_paths": "EXVideoThumbnails/**/*.{h,m}",
|
||||||
|
"requires_arc": true,
|
||||||
|
"dependencies": {
|
||||||
|
"UMCore": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"UMFileSystemInterface": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "RNImageCropPicker",
|
"name": "RNImageCropPicker",
|
||||||
"version": "0.30.0",
|
"version": "0.31.1",
|
||||||
"summary": "Select single or multiple images, with cropping option",
|
"summary": "Select single or multiple images, with cropping option",
|
||||||
"requires_arc": true,
|
"requires_arc": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -10,21 +10,21 @@
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"git": "https://github.com/ivpusic/react-native-image-crop-picker",
|
"git": "https://github.com/ivpusic/react-native-image-crop-picker",
|
||||||
"tag": "v0.30.0"
|
"tag": "v0.31.1"
|
||||||
},
|
},
|
||||||
"source_files": "ios/src/*.{h,m}",
|
"source_files": "ios/src/*.{h,m}",
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"ios": "8.0"
|
"ios": "8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"RSKImageCropper": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"React-Core": [
|
"React-Core": [
|
||||||
|
|
||||||
],
|
],
|
||||||
"React-RCTImage": [
|
"React-RCTImage": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"TOCropViewController": [
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subspecs": [
|
"subspecs": [
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "rn-extensions-share",
|
"name": "rn-extensions-share",
|
||||||
"version": "2.3.10",
|
"version": "2.4.0",
|
||||||
"summary": "Share-Extension using react-native for both ios and android",
|
"summary": "Share-Extension using react-native for both ios and android",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": {
|
"authors": {
|
||||||
|
|
|
@ -34,6 +34,9 @@ PODS:
|
||||||
- EXPermissions (8.1.0):
|
- EXPermissions (8.1.0):
|
||||||
- UMCore
|
- UMCore
|
||||||
- UMPermissionsInterface
|
- UMPermissionsInterface
|
||||||
|
- EXVideoThumbnails (4.1.1):
|
||||||
|
- UMCore
|
||||||
|
- UMFileSystemInterface
|
||||||
- EXWebBrowser (8.2.1):
|
- EXWebBrowser (8.2.1):
|
||||||
- UMCore
|
- UMCore
|
||||||
- Fabric (1.10.2)
|
- Fabric (1.10.2)
|
||||||
|
@ -431,7 +434,7 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||||
- React
|
- React
|
||||||
- rn-extensions-share (2.3.10):
|
- rn-extensions-share (2.4.0):
|
||||||
- React
|
- React
|
||||||
- rn-fetch-blob (0.12.0):
|
- rn-fetch-blob (0.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -462,15 +465,15 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNGestureHandler (1.6.1):
|
- RNGestureHandler (1.6.1):
|
||||||
- React
|
- React
|
||||||
- RNImageCropPicker (0.30.0):
|
- RNImageCropPicker (0.31.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RNImageCropPicker/QBImagePickerController (= 0.30.0)
|
- RNImageCropPicker/QBImagePickerController (= 0.31.1)
|
||||||
- RSKImageCropper
|
- TOCropViewController
|
||||||
- RNImageCropPicker/QBImagePickerController (0.30.0):
|
- RNImageCropPicker/QBImagePickerController (0.31.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RSKImageCropper
|
- TOCropViewController
|
||||||
- RNLocalize (1.4.0):
|
- RNLocalize (1.4.0):
|
||||||
- React
|
- React
|
||||||
- RNReanimated (1.8.0):
|
- RNReanimated (1.8.0):
|
||||||
|
@ -483,13 +486,13 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNVectorIcons (6.6.0):
|
- RNVectorIcons (6.6.0):
|
||||||
- React
|
- React
|
||||||
- RSKImageCropper (2.2.3)
|
|
||||||
- SDWebImage (5.7.4):
|
- SDWebImage (5.7.4):
|
||||||
- SDWebImage/Core (= 5.7.4)
|
- SDWebImage/Core (= 5.7.4)
|
||||||
- SDWebImage/Core (5.7.4)
|
- SDWebImage/Core (5.7.4)
|
||||||
- SDWebImageWebPCoder (0.4.1):
|
- SDWebImageWebPCoder (0.4.1):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.5)
|
- SDWebImage/Core (~> 5.5)
|
||||||
|
- TOCropViewController (2.5.2)
|
||||||
- UMAppLoader (1.0.2)
|
- UMAppLoader (1.0.2)
|
||||||
- UMBarCodeScannerInterface (5.1.0)
|
- UMBarCodeScannerInterface (5.1.0)
|
||||||
- UMCameraInterface (5.1.0)
|
- UMCameraInterface (5.1.0)
|
||||||
|
@ -522,6 +525,7 @@ DEPENDENCIES:
|
||||||
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||||
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
||||||
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
||||||
|
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||||
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
||||||
|
@ -644,9 +648,9 @@ SPEC REPOS:
|
||||||
- nanopb
|
- nanopb
|
||||||
- OpenSSL-Universal
|
- OpenSSL-Universal
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- RSKImageCropper
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
|
- TOCropViewController
|
||||||
- YogaKit
|
- YogaKit
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
@ -670,6 +674,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-local-authentication/ios"
|
:path: "../node_modules/expo-local-authentication/ios"
|
||||||
EXPermissions:
|
EXPermissions:
|
||||||
:path: "../node_modules/expo-permissions/ios"
|
:path: "../node_modules/expo-permissions/ios"
|
||||||
|
EXVideoThumbnails:
|
||||||
|
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||||
EXWebBrowser:
|
EXWebBrowser:
|
||||||
:path: "../node_modules/expo-web-browser/ios"
|
:path: "../node_modules/expo-web-browser/ios"
|
||||||
FBLazyVector:
|
FBLazyVector:
|
||||||
|
@ -833,6 +839,7 @@ SPEC CHECKSUMS:
|
||||||
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
||||||
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
||||||
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
||||||
|
EXVideoThumbnails: be6984a3cda1e44c45b5c6278244e99855f99a0a
|
||||||
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
||||||
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
||||||
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
||||||
|
@ -894,7 +901,7 @@ SPEC CHECKSUMS:
|
||||||
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
||||||
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
||||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||||
rn-extensions-share: 4bfee75806ad54aadeff1dfa535697a6345a50b8
|
rn-extensions-share: 8db79372089567cbc5aefe8444869bbc808578d3
|
||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
||||||
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
||||||
|
@ -905,16 +912,16 @@ SPEC CHECKSUMS:
|
||||||
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
||||||
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
||||||
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
||||||
RNImageCropPicker: a606d65f71c6c05caa3c850c16fb1ba2a4718608
|
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
|
||||||
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
||||||
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
||||||
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
||||||
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
||||||
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
||||||
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
||||||
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
|
|
||||||
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
||||||
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
||||||
|
TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
|
||||||
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
||||||
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
||||||
UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d
|
UMCameraInterface: 625878bbf2ba188a8548675e1d1d2e438a653e6d
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,195 +0,0 @@
|
||||||
## RSKImageCropper [![Build Status](https://travis-ci.org/ruslanskorb/RSKImageCropper.svg)](https://travis-ci.org/ruslanskorb/RSKImageCropper) [![Coverage Status](https://coveralls.io/repos/ruslanskorb/RSKImageCropper/badge.svg)](https://coveralls.io/r/ruslanskorb/RSKImageCropper) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/ruslanskorb/RSKImageCropper)
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="Screenshot.png" alt="Sample">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
An image cropper for iOS like in the Contacts app with support for landscape orientation.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
*RSKImageCropper requires iOS 6.0 or later.*
|
|
||||||
|
|
||||||
### Using [CocoaPods](http://cocoapods.org)
|
|
||||||
|
|
||||||
1. Add the pod `RSKImageCropper` to your [Podfile](http://guides.cocoapods.org/using/the-podfile.html).
|
|
||||||
|
|
||||||
pod 'RSKImageCropper'
|
|
||||||
|
|
||||||
2. Run `pod install` from Terminal, then open your app's `.xcworkspace` file to launch Xcode.
|
|
||||||
3. Import the `RSKImageCropper.h` header. Typically, this should be written as `#import <RSKImageCropper/RSKImageCropper.h>`
|
|
||||||
|
|
||||||
### Using [Carthage](https://github.com/Carthage/Carthage)
|
|
||||||
|
|
||||||
1. Add the `ruslanskorb/RSKImageCropper` project to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile).
|
|
||||||
|
|
||||||
github "ruslanskorb/RSKImageCropper"
|
|
||||||
|
|
||||||
2. Run `carthage update`, then follow the [additional steps required](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) to add the iOS and/or Mac frameworks into your project.
|
|
||||||
3. Import the RSKImageCropper framework/module.
|
|
||||||
* Using Modules: `@import RSKImageCropper`
|
|
||||||
* Without Modules: `#import <RSKImageCropper/RSKImageCropper.h>`
|
|
||||||
|
|
||||||
## Basic Usage
|
|
||||||
|
|
||||||
Import the class header.
|
|
||||||
|
|
||||||
``` objective-c
|
|
||||||
#import <RSKImageCropper/RSKImageCropper.h>
|
|
||||||
```
|
|
||||||
|
|
||||||
Just create a view controller for image cropping and set the delegate.
|
|
||||||
|
|
||||||
``` objective-c
|
|
||||||
- (IBAction)onButtonTouch:(UIButton *)sender
|
|
||||||
{
|
|
||||||
UIImage *image = [UIImage imageNamed:@"image"];
|
|
||||||
RSKImageCropViewController *imageCropVC = [[RSKImageCropViewController alloc] initWithImage:image];
|
|
||||||
imageCropVC.delegate = self;
|
|
||||||
[self.navigationController pushViewController:imageCropVC animated:YES];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Delegate
|
|
||||||
|
|
||||||
`RSKImageCropViewControllerDelegate` provides three delegate methods. To use them, implement the delegate in your view controller.
|
|
||||||
|
|
||||||
```objective-c
|
|
||||||
@interface ViewController () <RSKImageCropViewControllerDelegate>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then implement the delegate functions.
|
|
||||||
|
|
||||||
```objective-c
|
|
||||||
// Crop image has been canceled.
|
|
||||||
- (void)imageCropViewControllerDidCancelCrop:(RSKImageCropViewController *)controller
|
|
||||||
{
|
|
||||||
[self.navigationController popViewControllerAnimated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The original image has been cropped. Additionally provides a rotation angle used to produce image.
|
|
||||||
- (void)imageCropViewController:(RSKImageCropViewController *)controller
|
|
||||||
didCropImage:(UIImage *)croppedImage
|
|
||||||
usingCropRect:(CGRect)cropRect
|
|
||||||
rotationAngle:(CGFloat)rotationAngle
|
|
||||||
{
|
|
||||||
self.imageView.image = croppedImage;
|
|
||||||
[self.navigationController popViewControllerAnimated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The original image will be cropped.
|
|
||||||
- (void)imageCropViewController:(RSKImageCropViewController *)controller
|
|
||||||
willCropImage:(UIImage *)originalImage
|
|
||||||
{
|
|
||||||
// Use when `applyMaskToCroppedImage` set to YES.
|
|
||||||
[SVProgressHUD show];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## DataSource
|
|
||||||
|
|
||||||
`RSKImageCropViewControllerDataSource` provides three data source methods. The method `imageCropViewControllerCustomMaskRect:` asks the data source a custom rect for the mask. The method `imageCropViewControllerCustomMaskPath:` asks the data source a custom path for the mask. The method `imageCropViewControllerCustomMovementRect:` asks the data source a custom rect in which the image can be moved. To use them, implement the data source in your view controller.
|
|
||||||
|
|
||||||
```objective-c
|
|
||||||
@interface ViewController () <RSKImageCropViewControllerDataSource>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then implement the data source functions.
|
|
||||||
|
|
||||||
```objective-c
|
|
||||||
// Returns a custom rect for the mask.
|
|
||||||
- (CGRect)imageCropViewControllerCustomMaskRect:(RSKImageCropViewController *)controller
|
|
||||||
{
|
|
||||||
CGSize aspectRatio = CGSizeMake(16.0f, 9.0f);
|
|
||||||
|
|
||||||
CGFloat viewWidth = CGRectGetWidth(controller.view.frame);
|
|
||||||
CGFloat viewHeight = CGRectGetHeight(controller.view.frame);
|
|
||||||
|
|
||||||
CGFloat maskWidth;
|
|
||||||
if ([controller isPortraitInterfaceOrientation]) {
|
|
||||||
maskWidth = viewWidth;
|
|
||||||
} else {
|
|
||||||
maskWidth = viewHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat maskHeight;
|
|
||||||
do {
|
|
||||||
maskHeight = maskWidth * aspectRatio.height / aspectRatio.width;
|
|
||||||
maskWidth -= 1.0f;
|
|
||||||
} while (maskHeight != floor(maskHeight));
|
|
||||||
maskWidth += 1.0f;
|
|
||||||
|
|
||||||
CGSize maskSize = CGSizeMake(maskWidth, maskHeight);
|
|
||||||
|
|
||||||
CGRect maskRect = CGRectMake((viewWidth - maskSize.width) * 0.5f,
|
|
||||||
(viewHeight - maskSize.height) * 0.5f,
|
|
||||||
maskSize.width,
|
|
||||||
maskSize.height);
|
|
||||||
|
|
||||||
return maskRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a custom path for the mask.
|
|
||||||
- (UIBezierPath *)imageCropViewControllerCustomMaskPath:(RSKImageCropViewController *)controller
|
|
||||||
{
|
|
||||||
CGRect rect = controller.maskRect;
|
|
||||||
CGPoint point1 = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect));
|
|
||||||
CGPoint point2 = CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect));
|
|
||||||
CGPoint point3 = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
|
|
||||||
CGPoint point4 = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
|
|
||||||
|
|
||||||
UIBezierPath *rectangle = [UIBezierPath bezierPath];
|
|
||||||
[rectangle moveToPoint:point1];
|
|
||||||
[rectangle addLineToPoint:point2];
|
|
||||||
[rectangle addLineToPoint:point3];
|
|
||||||
[rectangle addLineToPoint:point4];
|
|
||||||
[rectangle closePath];
|
|
||||||
|
|
||||||
return rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a custom rect in which the image can be moved.
|
|
||||||
- (CGRect)imageCropViewControllerCustomMovementRect:(RSKImageCropViewController *)controller
|
|
||||||
{
|
|
||||||
if (controller.rotationAngle == 0) {
|
|
||||||
return controller.maskRect;
|
|
||||||
} else {
|
|
||||||
CGRect maskRect = controller.maskRect;
|
|
||||||
CGFloat rotationAngle = controller.rotationAngle;
|
|
||||||
|
|
||||||
CGRect movementRect = CGRectZero;
|
|
||||||
|
|
||||||
movementRect.size.width = CGRectGetWidth(maskRect) * fabs(cos(rotationAngle)) + CGRectGetHeight(maskRect) * fabs(sin(rotationAngle));
|
|
||||||
movementRect.size.height = CGRectGetHeight(maskRect) * fabs(cos(rotationAngle)) + CGRectGetWidth(maskRect) * fabs(sin(rotationAngle));
|
|
||||||
|
|
||||||
movementRect.origin.x = CGRectGetMinX(maskRect) + (CGRectGetWidth(maskRect) - CGRectGetWidth(movementRect)) * 0.5f;
|
|
||||||
movementRect.origin.y = CGRectGetMinY(maskRect) + (CGRectGetHeight(maskRect) - CGRectGetHeight(movementRect)) * 0.5f;
|
|
||||||
|
|
||||||
movementRect.origin.x = floor(CGRectGetMinX(movementRect));
|
|
||||||
movementRect.origin.y = floor(CGRectGetMinY(movementRect));
|
|
||||||
movementRect = CGRectIntegral(movementRect);
|
|
||||||
|
|
||||||
return movementRect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coming Soon
|
|
||||||
|
|
||||||
- If you would like to request a new feature, feel free to raise as an issue.
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
Build and run the `RSKImageCropperExample` project in Xcode to see `RSKImageCropper` in action.
|
|
||||||
Have fun. Fork and send pull requests. Figure out hooks for customization.
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
Ruslan Skorb
|
|
||||||
|
|
||||||
- http://github.com/ruslanskorb
|
|
||||||
- http://twitter.com/ruslanskorb
|
|
||||||
- ruslan.skorb@gmail.com
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is is available under the MIT license. See the LICENSE file for more info. Attribution by linking to the [project page](https://github.com/ruslanskorb/RSKImageCropper) is appreciated.
|
|
|
@ -1,114 +0,0 @@
|
||||||
//
|
|
||||||
// CGGeometry+RSKImageCropper.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2015 Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
|
||||||
#import <tgmath.h>
|
|
||||||
|
|
||||||
// tgmath functions aren't used on iOS when modules are enabled.
|
|
||||||
// Open Radar - http://www.openradar.me/16744288
|
|
||||||
// Work around this by redeclaring things here.
|
|
||||||
|
|
||||||
#ifdef __tg_promote1
|
|
||||||
|
|
||||||
#undef cos
|
|
||||||
#define cos(__x) __tg_cos(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef sin
|
|
||||||
#define sin(__x) __tg_sin(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef sqrt
|
|
||||||
#define sqrt(__x) __tg_sqrt(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef fabs
|
|
||||||
#define fabs(__x) __tg_fabs(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef ceil
|
|
||||||
#define ceil(__x) __tg_ceil(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef floor
|
|
||||||
#define floor(__x) __tg_floor(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#undef round
|
|
||||||
#define round(__x) __tg_round(__tg_promote1((__x))(__x))
|
|
||||||
|
|
||||||
#endif /* __tg_promote1 */
|
|
||||||
|
|
||||||
#ifdef __tg_promote2
|
|
||||||
|
|
||||||
#undef atan2
|
|
||||||
#define atan2(__x, __y) __tg_atan2(__tg_promote2((__x), (__y))(__x), \
|
|
||||||
__tg_promote2((__x), (__y))(__y))
|
|
||||||
|
|
||||||
#undef pow
|
|
||||||
#define pow(__x, __y) __tg_pow(__tg_promote2((__x), (__y))(__x), \
|
|
||||||
__tg_promote2((__x), (__y))(__y))
|
|
||||||
|
|
||||||
#endif /* __tg_promote2 */
|
|
||||||
|
|
||||||
#ifdef CGFLOAT_IS_DOUBLE
|
|
||||||
#define RSK_EPSILON DBL_EPSILON
|
|
||||||
#define RSK_MIN DBL_MIN
|
|
||||||
#else
|
|
||||||
#define RSK_EPSILON FLT_EPSILON
|
|
||||||
#define RSK_MIN FLT_MIN
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Line segments.
|
|
||||||
struct RSKLineSegment {
|
|
||||||
CGPoint start;
|
|
||||||
CGPoint end;
|
|
||||||
};
|
|
||||||
typedef struct RSKLineSegment RSKLineSegment;
|
|
||||||
|
|
||||||
// The "empty" point. This is the point returned when, for example, we
|
|
||||||
// intersect two disjoint line segments. Note that the null point is not the
|
|
||||||
// same as the zero point.
|
|
||||||
CG_EXTERN const CGPoint RSKPointNull;
|
|
||||||
|
|
||||||
// Returns the exact center point of the given rectangle.
|
|
||||||
CGPoint RSKRectCenterPoint(CGRect rect);
|
|
||||||
|
|
||||||
// Returns the `rect` with normalized values.
|
|
||||||
CGRect RSKRectNormalize(CGRect rect);
|
|
||||||
|
|
||||||
// Returns the `rect` scaled around the `point` by `sx` and `sy`.
|
|
||||||
CGRect RSKRectScaleAroundPoint(CGRect rect, CGPoint point, CGFloat sx, CGFloat sy);
|
|
||||||
|
|
||||||
// Returns true if `point' is the null point, false otherwise.
|
|
||||||
bool RSKPointIsNull(CGPoint point);
|
|
||||||
|
|
||||||
// Returns the `point` rotated around the `pivot` by `angle`.
|
|
||||||
CGPoint RSKPointRotateAroundPoint(CGPoint point, CGPoint pivot, CGFloat angle);
|
|
||||||
|
|
||||||
// Returns the distance between two points.
|
|
||||||
CGFloat RSKPointDistance(CGPoint p1, CGPoint p2);
|
|
||||||
|
|
||||||
// Make a line segment from two points `start` and `end`.
|
|
||||||
RSKLineSegment RSKLineSegmentMake(CGPoint start, CGPoint end);
|
|
||||||
|
|
||||||
// Returns the line segment rotated around the `pivot` by `angle`.
|
|
||||||
RSKLineSegment RSKLineSegmentRotateAroundPoint(RSKLineSegment lineSegment, CGPoint pivot, CGFloat angle);
|
|
||||||
|
|
||||||
// Returns the intersection of `ls1' and `ls2'. This may return a null point.
|
|
||||||
CGPoint RSKLineSegmentIntersection(RSKLineSegment ls1, RSKLineSegment ls2);
|
|
|
@ -1,203 +0,0 @@
|
||||||
//
|
|
||||||
// CGGeometry+RSKImageCropper.m
|
|
||||||
//
|
|
||||||
// Copyright (c) 2015 Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "CGGeometry+RSKImageCropper.h"
|
|
||||||
|
|
||||||
// K is a constant such that the accumulated error of our floating-point computations is definitely bounded by K units in the last place.
|
|
||||||
#ifdef CGFLOAT_IS_DOUBLE
|
|
||||||
static const CGFloat kK = 9;
|
|
||||||
#else
|
|
||||||
static const CGFloat kK = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const CGPoint RSKPointNull = { INFINITY, INFINITY };
|
|
||||||
|
|
||||||
CGPoint RSKRectCenterPoint(CGRect rect)
|
|
||||||
{
|
|
||||||
return CGPointMake(CGRectGetMinX(rect) + CGRectGetWidth(rect) / 2,
|
|
||||||
CGRectGetMinY(rect) + CGRectGetHeight(rect) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
CGRect RSKRectNormalize(CGRect rect)
|
|
||||||
{
|
|
||||||
CGPoint origin = rect.origin;
|
|
||||||
|
|
||||||
CGFloat x = origin.x;
|
|
||||||
CGFloat y = origin.y;
|
|
||||||
CGFloat ceilX = ceil(x);
|
|
||||||
CGFloat ceilY = ceil(y);
|
|
||||||
|
|
||||||
if (fabs(ceilX - x) < pow(10, kK) * RSK_EPSILON * fabs(ceilX + x) || fabs(ceilX - x) < RSK_MIN ||
|
|
||||||
fabs(ceilY - y) < pow(10, kK) * RSK_EPSILON * fabs(ceilY + y) || fabs(ceilY - y) < RSK_MIN) {
|
|
||||||
|
|
||||||
origin.x = ceilX;
|
|
||||||
origin.y = ceilY;
|
|
||||||
} else {
|
|
||||||
origin.x = floor(x);
|
|
||||||
origin.y = floor(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
CGSize size = rect.size;
|
|
||||||
|
|
||||||
CGFloat width = size.width;
|
|
||||||
CGFloat height = size.height;
|
|
||||||
CGFloat ceilWidth = ceil(width);
|
|
||||||
CGFloat ceilHeight = ceil(height);
|
|
||||||
|
|
||||||
if (fabs(ceilWidth - width) < pow(10, kK) * RSK_EPSILON * fabs(ceilWidth + width) || fabs(ceilWidth - width) < RSK_MIN ||
|
|
||||||
fabs(ceilHeight - height) < pow(10, kK) * RSK_EPSILON * fabs(ceilHeight + height) || fabs(ceilHeight - height) < RSK_MIN) {
|
|
||||||
|
|
||||||
size.width = ceilWidth;
|
|
||||||
size.height = ceilHeight;
|
|
||||||
} else {
|
|
||||||
size.width = floor(width);
|
|
||||||
size.height = floor(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CGRectMake(origin.x, origin.y, size.width, size.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
CGRect RSKRectScaleAroundPoint(CGRect rect, CGPoint point, CGFloat sx, CGFloat sy)
|
|
||||||
{
|
|
||||||
CGAffineTransform translationTransform, scaleTransform;
|
|
||||||
translationTransform = CGAffineTransformMakeTranslation(-point.x, -point.y);
|
|
||||||
rect = CGRectApplyAffineTransform(rect, translationTransform);
|
|
||||||
scaleTransform = CGAffineTransformMakeScale(sx, sy);
|
|
||||||
rect = CGRectApplyAffineTransform(rect, scaleTransform);
|
|
||||||
translationTransform = CGAffineTransformMakeTranslation(point.x, point.y);
|
|
||||||
rect = CGRectApplyAffineTransform(rect, translationTransform);
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RSKPointIsNull(CGPoint point)
|
|
||||||
{
|
|
||||||
return CGPointEqualToPoint(point, RSKPointNull);
|
|
||||||
}
|
|
||||||
|
|
||||||
CGPoint RSKPointRotateAroundPoint(CGPoint point, CGPoint pivot, CGFloat angle)
|
|
||||||
{
|
|
||||||
CGAffineTransform translationTransform, rotationTransform;
|
|
||||||
translationTransform = CGAffineTransformMakeTranslation(-pivot.x, -pivot.y);
|
|
||||||
point = CGPointApplyAffineTransform(point, translationTransform);
|
|
||||||
rotationTransform = CGAffineTransformMakeRotation(angle);
|
|
||||||
point = CGPointApplyAffineTransform(point, rotationTransform);
|
|
||||||
translationTransform = CGAffineTransformMakeTranslation(pivot.x, pivot.y);
|
|
||||||
point = CGPointApplyAffineTransform(point, translationTransform);
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat RSKPointDistance(CGPoint p1, CGPoint p2)
|
|
||||||
{
|
|
||||||
CGFloat dx = p1.x - p2.x;
|
|
||||||
CGFloat dy = p1.y - p2.y;
|
|
||||||
return sqrt(pow(dx, 2) + pow(dy, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
RSKLineSegment RSKLineSegmentMake(CGPoint start, CGPoint end)
|
|
||||||
{
|
|
||||||
return (RSKLineSegment){ start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
RSKLineSegment RSKLineSegmentRotateAroundPoint(RSKLineSegment line, CGPoint pivot, CGFloat angle)
|
|
||||||
{
|
|
||||||
return RSKLineSegmentMake(RSKPointRotateAroundPoint(line.start, pivot, angle),
|
|
||||||
RSKPointRotateAroundPoint(line.end, pivot, angle));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Equations of line segments:
|
|
||||||
|
|
||||||
pA = ls1.start + uA * (ls1.end - ls1.start)
|
|
||||||
pB = ls2.start + uB * (ls2.end - ls2.start)
|
|
||||||
|
|
||||||
In the case when `pA` is equal `pB` we have:
|
|
||||||
|
|
||||||
x1 + uA * (x2 - x1) = x3 + uB * (x4 - x3)
|
|
||||||
y1 + uA * (y2 - y1) = y3 + uB * (y4 - y3)
|
|
||||||
|
|
||||||
uA = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) / (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
|
||||||
uB = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) / (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
|
||||||
|
|
||||||
numeratorA = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
|
|
||||||
denominatorA = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
|
||||||
|
|
||||||
numeratorA = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
|
|
||||||
denominatorB = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
|
||||||
|
|
||||||
[1] Denominators are equal.
|
|
||||||
[2] If numerators and denominator are zero, then the line segments are coincident. The point of intersection is the midpoint of the line segment.
|
|
||||||
|
|
||||||
x = (x1 + x2) * 0.5
|
|
||||||
y = (y1 + y2) * 0.5
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
x = (x3 + x4) * 0.5
|
|
||||||
y = (y3 + y4) * 0.5
|
|
||||||
|
|
||||||
[3] If denominator is zero, then the line segments are parallel. There is no point of intersection.
|
|
||||||
[4] If `uA` and `uB` is included into the interval [0, 1], then the line segments intersects in the point (x, y).
|
|
||||||
|
|
||||||
x = x1 + uA * (x2 - x1)
|
|
||||||
y = y1 + uA * (y2 - y1)
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
x = x3 + uB * (x4 - x3)
|
|
||||||
y = y3 + uB * (y4 - y3)
|
|
||||||
*/
|
|
||||||
CGPoint RSKLineSegmentIntersection(RSKLineSegment ls1, RSKLineSegment ls2)
|
|
||||||
{
|
|
||||||
CGFloat x1 = ls1.start.x;
|
|
||||||
CGFloat y1 = ls1.start.y;
|
|
||||||
CGFloat x2 = ls1.end.x;
|
|
||||||
CGFloat y2 = ls1.end.y;
|
|
||||||
CGFloat x3 = ls2.start.x;
|
|
||||||
CGFloat y3 = ls2.start.y;
|
|
||||||
CGFloat x4 = ls2.end.x;
|
|
||||||
CGFloat y4 = ls2.end.y;
|
|
||||||
|
|
||||||
CGFloat numeratorA = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
|
|
||||||
CGFloat numeratorB = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
|
|
||||||
CGFloat denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
|
||||||
|
|
||||||
// Check the coincidence.
|
|
||||||
if (fabs(numeratorA) < RSK_EPSILON && fabs(numeratorB) < RSK_EPSILON && fabs(denominator) < RSK_EPSILON) {
|
|
||||||
return CGPointMake((x1 + x2) * 0.5, (y1 + y2) * 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the parallelism.
|
|
||||||
if (fabs(denominator) < RSK_EPSILON) {
|
|
||||||
return RSKPointNull;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the intersection.
|
|
||||||
CGFloat uA = numeratorA / denominator;
|
|
||||||
CGFloat uB = numeratorB / denominator;
|
|
||||||
if (uA < 0 || uA > 1 || uB < 0 || uB > 1) {
|
|
||||||
return RSKPointNull;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CGPointMake(x1 + uA * (x2 - x1), y1 + uA * (y2 - y1));
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
//
|
|
||||||
// RSKImageCropViewController+Protected.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
The methods in the RSKImageCropViewControllerProtectedMethods category
|
|
||||||
typically should only be called by subclasses which are implementing new
|
|
||||||
image crop view controllers. They may be overridden but must call super.
|
|
||||||
*/
|
|
||||||
@interface RSKImageCropViewController (RSKImageCropViewControllerProtectedMethods)
|
|
||||||
|
|
||||||
/**
|
|
||||||
Asynchronously crops the original image in accordance with the current settings and tells the delegate that the original image will be / has been cropped.
|
|
||||||
*/
|
|
||||||
- (void)cropImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Tells the delegate that the crop has been canceled.
|
|
||||||
*/
|
|
||||||
- (void)cancelCrop;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Resets the rotation angle, the position and the zoom scale of the original image to the default values.
|
|
||||||
|
|
||||||
@param animated Set this value to YES to animate the reset.
|
|
||||||
*/
|
|
||||||
- (void)reset:(BOOL)animated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets the current rotation angle of the image in radians.
|
|
||||||
|
|
||||||
@param rotationAngle The rotation angle of the image in radians.
|
|
||||||
*/
|
|
||||||
- (void)setRotationAngle:(CGFloat)rotationAngle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets the current scale factor for the image.
|
|
||||||
|
|
||||||
@param zoomScale The scale factor for the image.
|
|
||||||
*/
|
|
||||||
- (void)setZoomScale:(CGFloat)zoomScale;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
|
@ -1,352 +0,0 @@
|
||||||
//
|
|
||||||
// RSKImageCropViewController.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@protocol RSKImageCropViewControllerDataSource;
|
|
||||||
@protocol RSKImageCropViewControllerDelegate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Types of supported crop modes.
|
|
||||||
*/
|
|
||||||
typedef NS_ENUM(NSUInteger, RSKImageCropMode) {
|
|
||||||
RSKImageCropModeCircle,
|
|
||||||
RSKImageCropModeSquare,
|
|
||||||
RSKImageCropModeCustom
|
|
||||||
};
|
|
||||||
|
|
||||||
@interface RSKImageCropViewController : UIViewController
|
|
||||||
|
|
||||||
/**
|
|
||||||
Designated initializer. Initializes and returns a newly allocated view controller object with the specified image.
|
|
||||||
|
|
||||||
@param originalImage The image for cropping.
|
|
||||||
*/
|
|
||||||
- (instancetype)initWithImage:(UIImage *)originalImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Initializes and returns a newly allocated view controller object with the specified image and the specified crop mode.
|
|
||||||
|
|
||||||
@param originalImage The image for cropping.
|
|
||||||
@param cropMode The mode for cropping.
|
|
||||||
*/
|
|
||||||
- (instancetype)initWithImage:(UIImage *)originalImage cropMode:(RSKImageCropMode)cropMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Zooms to a specific area of the image so that it is visible.
|
|
||||||
|
|
||||||
@param rect A rectangle defining an area of the image.
|
|
||||||
@param animated YES if the scrolling should be animated, NO if it should be immediate.
|
|
||||||
*/
|
|
||||||
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated;
|
|
||||||
|
|
||||||
///-----------------------------
|
|
||||||
/// @name Accessing the Delegate
|
|
||||||
///-----------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The receiver's delegate.
|
|
||||||
|
|
||||||
@discussion A `RSKImageCropViewControllerDelegate` delegate responds to messages sent by completing / canceling crop the image in the image crop view controller.
|
|
||||||
*/
|
|
||||||
@property (weak, nonatomic, nullable) id<RSKImageCropViewControllerDelegate> delegate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The receiver's data source.
|
|
||||||
|
|
||||||
@discussion A `RSKImageCropViewControllerDataSource` data source provides a custom rect and a custom path for the mask and a custom movement rect for the image.
|
|
||||||
*/
|
|
||||||
@property (weak, nonatomic, nullable) id<RSKImageCropViewControllerDataSource> dataSource;
|
|
||||||
|
|
||||||
///--------------------------
|
|
||||||
/// @name Accessing the Image
|
|
||||||
///--------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The image for cropping.
|
|
||||||
*/
|
|
||||||
@property (strong, nonatomic) UIImage *originalImage;
|
|
||||||
|
|
||||||
/// -----------------------------------
|
|
||||||
/// @name Accessing the Mask Attributes
|
|
||||||
/// -----------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The color of the layer with the mask. Default value is [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.7f].
|
|
||||||
*/
|
|
||||||
@property (copy, nonatomic) UIColor *maskLayerColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The line width used when stroking the path of the mask layer. Default value is 1.0.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat maskLayerLineWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The color to fill the stroked outline of the path of the mask layer, or nil for no stroking. Default valus is nil.
|
|
||||||
*/
|
|
||||||
@property (copy, nonatomic, nullable) UIColor *maskLayerStrokeColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The rect of the mask.
|
|
||||||
|
|
||||||
@discussion Updating each time before the crop view lays out its subviews.
|
|
||||||
*/
|
|
||||||
@property (assign, readonly, nonatomic) CGRect maskRect;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The path of the mask.
|
|
||||||
|
|
||||||
@discussion Updating each time before the crop view lays out its subviews.
|
|
||||||
*/
|
|
||||||
@property (copy, readonly, nonatomic) UIBezierPath *maskPath;
|
|
||||||
|
|
||||||
/// -----------------------------------
|
|
||||||
/// @name Accessing the Crop Attributes
|
|
||||||
/// -----------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The mode for cropping. Default value is `RSKImageCropModeCircle`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) RSKImageCropMode cropMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The crop rectangle.
|
|
||||||
|
|
||||||
@discussion The value is calculated at run time.
|
|
||||||
*/
|
|
||||||
@property (readonly, nonatomic) CGRect cropRect;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A value that specifies the current rotation angle of the image in radians.
|
|
||||||
|
|
||||||
@discussion The value is calculated at run time.
|
|
||||||
*/
|
|
||||||
@property (readonly, nonatomic) CGFloat rotationAngle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A floating-point value that specifies the current scale factor applied to the image.
|
|
||||||
|
|
||||||
@discussion The value is calculated at run time.
|
|
||||||
*/
|
|
||||||
@property (readonly, nonatomic) CGFloat zoomScale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A Boolean value that determines whether the image will always fill the mask space. Default value is `NO`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) BOOL avoidEmptySpaceAroundImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A Boolean value that determines whether the image will always bounce horizontally. Default value is `NO`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) BOOL alwaysBounceHorizontal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A Boolean value that determines whether the image will always bounce vertically. Default value is `NO`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) BOOL alwaysBounceVertical;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A Boolean value that determines whether the mask applies to the image after cropping. Default value is `NO`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) BOOL applyMaskToCroppedImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A Boolean value that controls whether the rotaion gesture is enabled. Default value is `NO`.
|
|
||||||
|
|
||||||
@discussion To support the rotation when `cropMode` is `RSKImageCropModeCustom` you must implement the data source method `imageCropViewControllerCustomMovementRect:`.
|
|
||||||
*/
|
|
||||||
@property (assign, getter=isRotationEnabled, nonatomic) BOOL rotationEnabled;
|
|
||||||
|
|
||||||
/// -------------------------------
|
|
||||||
/// @name Accessing the UI Elements
|
|
||||||
/// -------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The Title Label.
|
|
||||||
*/
|
|
||||||
@property (strong, nonatomic, readonly) UILabel *moveAndScaleLabel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The Cancel Button.
|
|
||||||
*/
|
|
||||||
@property (strong, nonatomic, readonly) UIButton *cancelButton;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The Choose Button.
|
|
||||||
*/
|
|
||||||
@property (strong, nonatomic, readonly) UIButton *chooseButton;
|
|
||||||
|
|
||||||
/// -------------------------------------------
|
|
||||||
/// @name Checking of the Interface Orientation
|
|
||||||
/// -------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns a Boolean value indicating whether the user interface is currently presented in a portrait orientation.
|
|
||||||
|
|
||||||
@return YES if the interface orientation is portrait, otherwise returns NO.
|
|
||||||
*/
|
|
||||||
- (BOOL)isPortraitInterfaceOrientation;
|
|
||||||
|
|
||||||
/// -------------------------------------
|
|
||||||
/// @name Accessing the Layout Attributes
|
|
||||||
/// -------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
The inset of the circle mask rect's area within the crop view's area in portrait orientation. Default value is `15.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitCircleMaskRectInnerEdgeInset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The inset of the square mask rect's area within the crop view's area in portrait orientation. Default value is `20.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitSquareMaskRectInnerEdgeInset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the top of the 'Move and Scale' label and the top of the crop view in portrait orientation. Default value is `64.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitMoveAndScaleLabelTopAndCropViewTopVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the bottom of the crop view and the bottom of the 'Cancel' button in portrait orientation. Default value is `21.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitCropViewBottomAndCancelButtonBottomVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the bottom of the crop view and the bottom of the 'Choose' button in portrait orientation. Default value is `21.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitCropViewBottomAndChooseButtonBottomVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The horizontal space between the leading of the 'Cancel' button and the leading of the crop view in portrait orientation. Default value is `13.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitCancelButtonLeadingAndCropViewLeadingHorizontalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The horizontal space between the trailing of the crop view and the trailing of the 'Choose' button in portrait orientation. Default value is `13.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat portraitCropViewTrailingAndChooseButtonTrailingHorizontalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The inset of the circle mask rect's area within the crop view's area in landscape orientation. Default value is `45.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeCircleMaskRectInnerEdgeInset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The inset of the square mask rect's area within the crop view's area in landscape orientation. Default value is `45.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeSquareMaskRectInnerEdgeInset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the top of the 'Move and Scale' label and the top of the crop view in landscape orientation. Default value is `12.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeMoveAndScaleLabelTopAndCropViewTopVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the bottom of the crop view and the bottom of the 'Cancel' button in landscape orientation. Default value is `12.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeCropViewBottomAndCancelButtonBottomVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The vertical space between the bottom of the crop view and the bottom of the 'Choose' button in landscape orientation. Default value is `12.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeCropViewBottomAndChooseButtonBottomVerticalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The horizontal space between the leading of the 'Cancel' button and the leading of the crop view in landscape orientation. Default value is `13.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeCancelButtonLeadingAndCropViewLeadingHorizontalSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
The horizontal space between the trailing of the crop view and the trailing of the 'Choose' button in landscape orientation. Default value is `13.0f`.
|
|
||||||
*/
|
|
||||||
@property (assign, nonatomic) CGFloat landscapeCropViewTrailingAndChooseButtonTrailingHorizontalSpace;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
/**
|
|
||||||
The `RSKImageCropViewControllerDataSource` protocol is adopted by an object that provides a custom rect and a custom path for the mask and a custom movement rect for the image.
|
|
||||||
*/
|
|
||||||
@protocol RSKImageCropViewControllerDataSource <NSObject>
|
|
||||||
|
|
||||||
/**
|
|
||||||
Asks the data source a custom rect for the mask.
|
|
||||||
|
|
||||||
@param controller The crop view controller object to whom a rect is provided.
|
|
||||||
|
|
||||||
@return A custom rect for the mask.
|
|
||||||
*/
|
|
||||||
- (CGRect)imageCropViewControllerCustomMaskRect:(RSKImageCropViewController *)controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Asks the data source a custom path for the mask.
|
|
||||||
|
|
||||||
@param controller The crop view controller object to whom a path is provided.
|
|
||||||
|
|
||||||
@return A custom path for the mask.
|
|
||||||
*/
|
|
||||||
- (UIBezierPath *)imageCropViewControllerCustomMaskPath:(RSKImageCropViewController *)controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Asks the data source a custom rect in which the image can be moved.
|
|
||||||
|
|
||||||
@param controller The crop view controller object to whom a rect is provided.
|
|
||||||
|
|
||||||
@return A custom rect in which the image can be moved.
|
|
||||||
*/
|
|
||||||
- (CGRect)imageCropViewControllerCustomMovementRect:(RSKImageCropViewController *)controller;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
/**
|
|
||||||
The `RSKImageCropViewControllerDelegate` protocol defines messages sent to a image crop view controller delegate when crop image was canceled or the original image was cropped.
|
|
||||||
*/
|
|
||||||
@protocol RSKImageCropViewControllerDelegate <NSObject>
|
|
||||||
|
|
||||||
/**
|
|
||||||
Tells the delegate that crop image has been canceled.
|
|
||||||
*/
|
|
||||||
- (void)imageCropViewControllerDidCancelCrop:(RSKImageCropViewController *)controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Tells the delegate that the original image has been cropped. Additionally provides a crop rect and a rotation angle used to produce image.
|
|
||||||
*/
|
|
||||||
- (void)imageCropViewController:(RSKImageCropViewController *)controller didCropImage:(UIImage *)croppedImage usingCropRect:(CGRect)cropRect rotationAngle:(CGFloat)rotationAngle;
|
|
||||||
|
|
||||||
@optional
|
|
||||||
|
|
||||||
/**
|
|
||||||
Tells the delegate that the image has been displayed.
|
|
||||||
*/
|
|
||||||
- (void)imageCropViewControllerDidDisplayImage:(RSKImageCropViewController *)controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Tells the delegate that the original image will be cropped.
|
|
||||||
*/
|
|
||||||
- (void)imageCropViewController:(RSKImageCropViewController *)controller willCropImage:(UIImage *)originalImage;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,44 +0,0 @@
|
||||||
//
|
|
||||||
// RSKImageCropper.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
`RSKImageCropper` is an image cropper for iOS like in the Contacts app with support for landscape orientation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
//! Project version number for RSKImageCropper.
|
|
||||||
FOUNDATION_EXPORT double RSKImageCropperVersionNumber;
|
|
||||||
|
|
||||||
//! Project version string for RSKImageCropper.
|
|
||||||
FOUNDATION_EXPORT const unsigned char RSKImageCropperVersionString[];
|
|
||||||
|
|
||||||
#import <RSKImageCropper/CGGeometry+RSKImageCropper.h>
|
|
||||||
#import <RSKImageCropper/RSKImageCropViewController.h>
|
|
||||||
#import <RSKImageCropper/RSKImageCropViewController+Protected.h>
|
|
||||||
#import <RSKImageCropper/RSKImageScrollView.h>
|
|
||||||
#import <RSKImageCropper/RSKInternalUtility.h>
|
|
||||||
#import <RSKImageCropper/RSKTouchView.h>
|
|
||||||
#import <RSKImageCropper/UIApplication+RSKImageCropper.h>
|
|
||||||
#import <RSKImageCropper/UIImage+RSKImageCropper.h>
|
|
|
@ -1,6 +0,0 @@
|
||||||
framework module RSKImageCropper {
|
|
||||||
umbrella header "RSKImageCropper.h"
|
|
||||||
|
|
||||||
export *
|
|
||||||
module * { export * }
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
//
|
|
||||||
// RSKImageCropper.strings
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
/* Move and Scale label */
|
|
||||||
"Move and Scale" = "نقل وتحجيم";
|
|
||||||
/* Cancel button */
|
|
||||||
"Cancel" = "إلغاء";
|
|
||||||
/* Choose button */
|
|
||||||
"Choose" = "تحديد";
|
|
|
@ -1,30 +0,0 @@
|
||||||
//
|
|
||||||
// RSKImageCropper.strings
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
//
|
|
||||||
|
|
||||||
/* Move and Scale label */
|
|
||||||
"Move and Scale" = "Преместване и мащабиране";
|
|
||||||
/* Cancel button */
|
|
||||||
"Cancel" = "Отмяна";
|
|
||||||
/* Choose button */
|
|
||||||
"Choose" = "Изберете";
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue