[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">
|
||||
|
||||
<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.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
|
||||
android:name=".MainApplication"
|
||||
|
@ -65,6 +60,7 @@
|
|||
android:theme="@style/AppTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
|
|
@ -15,6 +15,7 @@ public class BasePackageList {
|
|||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,9 @@ export const themes = {
|
|||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#1F2329',
|
||||
previewTintColor: '#ffffff'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: '#030b1b',
|
||||
|
@ -95,7 +97,9 @@ export const themes = {
|
|||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#030b1b',
|
||||
previewTintColor: '#ffffff'
|
||||
},
|
||||
black: {
|
||||
backgroundColor: '#000000',
|
||||
|
@ -137,6 +141,8 @@ export const themes = {
|
|||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#000000',
|
||||
previewTintColor: '#ffffff'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useRef, useContext } from 'react';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import React, { useRef, useContext, forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActionSheet from './ActionSheet';
|
||||
|
@ -14,15 +13,11 @@ export const useActionSheet = () => useContext(context);
|
|||
|
||||
const { Provider, Consumer } = context;
|
||||
|
||||
export const withActionSheet = (Component) => {
|
||||
const ConnectedActionSheet = props => (
|
||||
export const withActionSheet = Component => forwardRef((props, ref) => (
|
||||
<Consumer>
|
||||
{contexts => <Component {...props} {...contexts} />}
|
||||
{contexts => <Component {...props} {...contexts} ref={ref} />}
|
||||
</Consumer>
|
||||
);
|
||||
hoistNonReactStatics(ConnectedActionSheet, Component);
|
||||
return ConnectedActionSheet;
|
||||
};
|
||||
));
|
||||
|
||||
export const ActionSheetProvider = React.memo(({ children }) => {
|
||||
const ref = useRef();
|
||||
|
|
|
@ -4,11 +4,22 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||
import { View, StyleSheet } from 'react-native';
|
||||
import { themes } from '../../constants/colors';
|
||||
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
|
||||
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({
|
||||
container: {
|
||||
height: headerHeight,
|
||||
|
|
|
@ -36,9 +36,11 @@ export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) =
|
|||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
export const CloseModalButton = React.memo(({ navigation, testID, onPress = () => navigation.pop() }) => (
|
||||
export const CloseModalButton = React.memo(({
|
||||
navigation, testID, onPress = () => navigation.pop(), ...props
|
||||
}) => (
|
||||
<CustomHeaderButtons left>
|
||||
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} />
|
||||
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
@ -57,9 +59,9 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
|
|||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
export const SaveButton = React.memo(({ onPress, testID }) => (
|
||||
export const SaveButton = React.memo(({ onPress, testID, ...props }) => (
|
||||
<CustomHeaderButtons>
|
||||
<Item title='save' iconName='download' onPress={onPress} testID={testID} />
|
||||
<Item title='save' iconName='download' onPress={onPress} testID={testID} {...props} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { CancelEditingButton, ActionsButton } from './buttons';
|
||||
import styles from './styles';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
theme, showMessageBoxActions, editing, editCancel
|
||||
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||
}
|
||||
if (isActionsEnabled) {
|
||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||
}
|
||||
return <View style={styles.buttonsWhitespace} />;
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showMessageBoxActions: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
editCancel: PropTypes.func.isRequired
|
||||
editCancel: PropTypes.func.isRequired,
|
||||
isActionsEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LeftButtons;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
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 I18n from '../../i18n';
|
||||
|
@ -113,7 +113,7 @@ export default class extends React.PureComponent {
|
|||
this.recording = false;
|
||||
const filePath = await AudioRecorder.stopRecording();
|
||||
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);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
||||
import styles from './styles';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions
|
||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} theme={theme} />;
|
||||
}
|
||||
if (recordAudioMessageEnabled) {
|
||||
if (recordAudioMessageEnabled || isActionsEnabled) {
|
||||
return (
|
||||
<>
|
||||
<AudioButton onPress={recordAudioMessage} theme={theme} />
|
||||
<ActionsButton onPress={showMessageBoxActions} theme={theme} />
|
||||
{recordAudioMessageEnabled ? <AudioButton onPress={recordAudioMessage} theme={theme} /> : null}
|
||||
{isActionsEnabled ? <ActionsButton onPress={showMessageBoxActions} theme={theme} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||
return <View style={styles.buttonsWhitespace} />;
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
|
@ -26,7 +28,8 @@ RightButtons.propTypes = {
|
|||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired,
|
||||
recordAudioMessageEnabled: PropTypes.bool,
|
||||
showMessageBoxActions: PropTypes.func.isRequired
|
||||
showMessageBoxActions: PropTypes.func.isRequired,
|
||||
isActionsEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
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 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 { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||
import ImagePicker from 'react-native-image-crop-picker';
|
||||
|
@ -16,7 +18,6 @@ import styles from './styles';
|
|||
import database from '../../lib/database';
|
||||
import { emojis } from '../../emojis';
|
||||
import Recording from './Recording';
|
||||
import UploadModal from './UploadModal';
|
||||
import log from '../../utils/log';
|
||||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
|
@ -42,7 +43,6 @@ import {
|
|||
MENTIONS_TRACKING_TYPE_USERS
|
||||
} from './constants';
|
||||
import CommandsPreview from './CommandsPreview';
|
||||
import { Review } from '../../utils/review';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { withActionSheet } from '../ActionSheet';
|
||||
|
@ -54,6 +54,7 @@ const imagePickerConfig = {
|
|||
};
|
||||
|
||||
const libraryPickerConfig = {
|
||||
multiple: true,
|
||||
mediaType: 'any'
|
||||
};
|
||||
|
||||
|
@ -88,9 +89,24 @@ class MessageBox extends Component {
|
|||
typing: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
replyCancel: PropTypes.func,
|
||||
isMasterDetail: PropTypes.bool,
|
||||
showSend: PropTypes.bool,
|
||||
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) {
|
||||
|
@ -98,12 +114,9 @@ class MessageBox extends Component {
|
|||
this.state = {
|
||||
mentions: [],
|
||||
showEmojiKeyboard: false,
|
||||
showSend: false,
|
||||
showSend: props.showSend,
|
||||
recording: false,
|
||||
trackingType: '',
|
||||
file: {
|
||||
isVisible: false
|
||||
},
|
||||
commandPreview: [],
|
||||
showCommandPreview: false,
|
||||
command: {}
|
||||
|
@ -161,27 +174,29 @@ class MessageBox extends Component {
|
|||
|
||||
async componentDidMount() {
|
||||
const db = database.active;
|
||||
const { rid, tmid, navigation } = this.props;
|
||||
const {
|
||||
rid, tmid, navigation, sharing
|
||||
} = this.props;
|
||||
let msg;
|
||||
try {
|
||||
const threadsCollection = db.collections.get('threads');
|
||||
const subsCollection = db.collections.get('subscriptions');
|
||||
try {
|
||||
this.room = await subsCollection.find(rid);
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Room not found');
|
||||
}
|
||||
if (tmid) {
|
||||
try {
|
||||
const thread = await threadsCollection.find(tmid);
|
||||
if (thread) {
|
||||
msg = thread.draftMessage;
|
||||
this.thread = await threadsCollection.find(tmid);
|
||||
if (this.thread && !sharing) {
|
||||
msg = this.thread.draftMessage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Thread not found');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.room = await subsCollection.find(rid);
|
||||
} else if (!sharing) {
|
||||
msg = this.room.draftMessage;
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Room not found');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
|
@ -211,8 +226,14 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { isFocused, editing, replying } = this.props;
|
||||
if (!isFocused()) {
|
||||
const {
|
||||
isFocused, editing, replying, sharing
|
||||
} = this.props;
|
||||
if (!isFocused?.()) {
|
||||
return;
|
||||
}
|
||||
if (sharing) {
|
||||
this.setInput(nextProps.message.msg ?? '');
|
||||
return;
|
||||
}
|
||||
if (editing !== nextProps.editing && nextProps.editing) {
|
||||
|
@ -230,11 +251,11 @@ class MessageBox extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
|
||||
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
roomType, replying, editing, isFocused, message, theme
|
||||
roomType, replying, editing, isFocused, message, theme, children
|
||||
} = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
|
@ -266,10 +287,10 @@ class MessageBox extends Component {
|
|||
if (!equal(nextState.commandPreview, commandPreview)) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.file, file)) {
|
||||
if (!equal(nextProps.message, message)) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.message, message)) {
|
||||
if (!equal(nextProps.children, children)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -312,10 +333,13 @@ class MessageBox extends Component {
|
|||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
debouncedOnChangeText = debounce(async(text) => {
|
||||
const { sharing } = this.props;
|
||||
const db = database.active;
|
||||
const isTextEmpty = text.length === 0;
|
||||
// this.setShowSend(!isTextEmpty);
|
||||
this.handleTyping(!isTextEmpty);
|
||||
|
||||
if (!sharing) {
|
||||
// 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 (slashCommand) {
|
||||
|
@ -330,6 +354,7 @@ class MessageBox extends Component {
|
|||
console.log('Slash command not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTextEmpty) {
|
||||
try {
|
||||
|
@ -337,13 +362,21 @@ class MessageBox extends Component {
|
|||
const cursor = Math.max(start, end);
|
||||
const lastNativeText = this.component?.lastNativeText || '';
|
||||
// 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);
|
||||
if (!result) {
|
||||
if (!sharing) {
|
||||
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
||||
if (slash) {
|
||||
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
||||
}
|
||||
}
|
||||
return this.stopTrackingMention();
|
||||
}
|
||||
const [, lastChar, name] = result;
|
||||
|
@ -482,7 +515,10 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
handleTyping = (isTyping) => {
|
||||
const { typing, rid } = this.props;
|
||||
const { typing, rid, sharing } = this.props;
|
||||
if (sharing) {
|
||||
return;
|
||||
}
|
||||
if (!isTyping) {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
|
@ -522,7 +558,8 @@ class MessageBox extends Component {
|
|||
|
||||
setShowSend = (showSend) => {
|
||||
const { showSend: prevShowSend } = this.state;
|
||||
if (prevShowSend !== showSend) {
|
||||
const { showSend: propShowSend } = this.props;
|
||||
if (prevShowSend !== showSend && !propShowSend) {
|
||||
this.setState({ showSend });
|
||||
}
|
||||
}
|
||||
|
@ -534,7 +571,7 @@ class MessageBox extends Component {
|
|||
|
||||
canUploadFile = (file) => {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
@ -542,33 +579,11 @@ class MessageBox extends Component {
|
|||
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() => {
|
||||
try {
|
||||
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
||||
if (this.canUploadFile(image)) {
|
||||
this.showUploadModal(image);
|
||||
this.openShareView([image]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
|
@ -579,7 +594,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||
if (this.canUploadFile(video)) {
|
||||
this.showUploadModal(video);
|
||||
this.openShareView([video]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
|
@ -588,10 +603,8 @@ class MessageBox extends Component {
|
|||
|
||||
chooseFromLibrary = async() => {
|
||||
try {
|
||||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||
if (this.canUploadFile(image)) {
|
||||
this.showUploadModal(image);
|
||||
}
|
||||
const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||
this.openShareView(attachments);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
@ -609,7 +622,7 @@ class MessageBox extends Component {
|
|||
path: res.uri
|
||||
};
|
||||
if (this.canUploadFile(file)) {
|
||||
this.showUploadModal(file);
|
||||
this.openShareView([file]);
|
||||
}
|
||||
} catch (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 = () => {
|
||||
const { isMasterDetail } = this.props;
|
||||
const params = { channel: this.room, showCloseModal: true };
|
||||
|
@ -628,10 +645,6 @@ class MessageBox extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
showUploadModal = (file) => {
|
||||
this.setState({ file: { ...file, isVisible: true } });
|
||||
}
|
||||
|
||||
showMessageBoxActions = () => {
|
||||
const { showActionSheet } = this.props;
|
||||
showActionSheet({ options: this.options });
|
||||
|
@ -679,16 +692,22 @@ class MessageBox extends Component {
|
|||
|
||||
submit = async() => {
|
||||
const {
|
||||
onSubmit, rid: roomId, tmid
|
||||
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||
} = this.props;
|
||||
const message = this.text;
|
||||
|
||||
// if sharing, only execute onSubmit prop
|
||||
if (sharing) {
|
||||
onSubmit(message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearInput();
|
||||
this.debouncedOnChangeText.stop();
|
||||
this.closeEmoji();
|
||||
this.stopTrackingMention();
|
||||
this.handleTyping(false);
|
||||
if (message.trim() === '') {
|
||||
if (message.trim() === '' && !showSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -809,7 +828,7 @@ class MessageBox extends Component {
|
|||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||
} = this.state;
|
||||
const {
|
||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled
|
||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled, children, isActionsEnabled
|
||||
} = this.props;
|
||||
|
||||
const isAndroidTablet = isTablet && isAndroid ? {
|
||||
|
@ -846,6 +865,7 @@ class MessageBox extends Component {
|
|||
showEmojiKeyboard={showEmojiKeyboard}
|
||||
editing={editing}
|
||||
showMessageBoxActions={this.showMessageBoxActions}
|
||||
isActionsEnabled={isActionsEnabled}
|
||||
editCancel={this.editCancel}
|
||||
openEmoji={this.openEmoji}
|
||||
closeEmoji={this.closeEmoji}
|
||||
|
@ -872,18 +892,20 @@ class MessageBox extends Component {
|
|||
recordAudioMessage={this.recordAudioMessage}
|
||||
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
||||
showMessageBoxActions={this.showMessageBoxActions}
|
||||
isActionsEnabled={isActionsEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { showEmojiKeyboard, file } = this.state;
|
||||
const { showEmojiKeyboard } = this.state;
|
||||
const {
|
||||
user, baseUrl, theme, isMasterDetail
|
||||
user, baseUrl, theme, iOSScrollBehavior
|
||||
} = this.props;
|
||||
return (
|
||||
<MessageboxContext.Provider
|
||||
|
@ -906,13 +928,7 @@ class MessageBox extends Component {
|
|||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
bottomViewColor={themes[theme].messageboxBackground}
|
||||
/>
|
||||
<UploadModal
|
||||
isVisible={(file && file.isVisible)}
|
||||
file={file}
|
||||
close={() => this.setState({ file: {} })}
|
||||
submit={this.sendMediaMessage}
|
||||
isMasterDetail={isMasterDetail}
|
||||
iOSScrollBehavior={iOSScrollBehavior}
|
||||
/>
|
||||
</MessageboxContext.Provider>
|
||||
);
|
||||
|
|
|
@ -103,5 +103,8 @@ export default StyleSheet.create({
|
|||
},
|
||||
scrollViewMention: {
|
||||
maxHeight: SCROLLVIEW_MENTION_HEIGHT
|
||||
},
|
||||
buttonsWhitespace: {
|
||||
width: 15
|
||||
}
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ const RoomTypeIcon = React.memo(({
|
|||
name={icon}
|
||||
size={size}
|
||||
style={[
|
||||
type === 'l' ? { color: STATUS_COLORS[status] } : { color },
|
||||
type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
|
||||
styles.icon,
|
||||
style
|
||||
]}
|
||||
|
|
|
@ -5,16 +5,20 @@ import PropTypes from 'prop-types';
|
|||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const StatusBar = React.memo(({ theme }) => {
|
||||
let barStyle = 'light-content';
|
||||
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => {
|
||||
if (!barStyle) {
|
||||
barStyle = 'light-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 = {
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
barStyle: PropTypes.string,
|
||||
backgroundColor: PropTypes.string
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
|
|
|
@ -112,7 +112,8 @@ export default StyleSheet.create({
|
|||
// maxWidth: 400,
|
||||
minHeight: isTablet ? 300 : 200,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
imagePressed: {
|
||||
opacity: 0.5
|
||||
|
|
|
@ -379,6 +379,8 @@ export default {
|
|||
Reactions_are_enabled: 'Reactions are enabled',
|
||||
Reactions: 'Reactions',
|
||||
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: 'Read Only',
|
||||
Read_Receipt: 'Read Receipt',
|
||||
|
@ -444,6 +446,7 @@ export default {
|
|||
Send_message: 'Send message',
|
||||
Send_me_the_code_again: 'Send me the code again',
|
||||
Send_to: 'Send to...',
|
||||
Sending_to: 'Sending to',
|
||||
Sent_an_attachment: 'Sent an attachment',
|
||||
Server: 'Server',
|
||||
Servers: 'Servers',
|
||||
|
|
|
@ -344,6 +344,8 @@ export default {
|
|||
Reactions_are_disabled: 'Reagir está desabilitado',
|
||||
Reactions_are_enabled: 'Reagir está habilitado',
|
||||
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: 'Somente Leitura',
|
||||
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 PropTypes from 'prop-types';
|
||||
import {
|
||||
|
@ -6,14 +6,12 @@ import {
|
|||
State,
|
||||
PinchGestureHandler
|
||||
} from 'react-native-gesture-handler';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Animated, { Easing } from 'react-native-reanimated';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
|
||||
const AnimatedFastImage = Animated.createAnimatedComponent(FastImage);
|
||||
import { ImageComponent } from './ImageComponent';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: {
|
||||
flex: 1
|
||||
},
|
||||
image: {
|
||||
|
@ -264,10 +262,13 @@ const HEIGHT = 300;
|
|||
|
||||
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
|
||||
// and changed to use FastImage animated component
|
||||
class ImageViewer extends Component {
|
||||
export class ImageViewer extends React.Component {
|
||||
static propTypes = {
|
||||
uri: PropTypes.string,
|
||||
width: PropTypes.number
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
theme: PropTypes.string,
|
||||
imageComponentType: PropTypes.string
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -384,15 +385,22 @@ class ImageViewer extends Component {
|
|||
panRef = React.createRef();
|
||||
|
||||
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
|
||||
// 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
|
||||
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2);
|
||||
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2);
|
||||
const backgroundColor = themes[theme].previewBackground;
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={[styles.flex, { width, height, backgroundColor }]}>
|
||||
<PinchGestureHandler
|
||||
ref={this.pinchRef}
|
||||
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 { 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 getShowFormLoginSetting = state => state.settings.Accounts_ShowFormLogin || false;
|
||||
|
||||
|
|
35
app/share.js
35
app/share.js
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { AppearanceProvider } from 'react-native-appearance';
|
||||
|
@ -32,6 +33,8 @@ import ShareView from './views/ShareView';
|
|||
import SelectServerView from './views/SelectServerView';
|
||||
import { setCurrentScreen } from './utils/log';
|
||||
import AuthLoadingView from './views/AuthLoadingView';
|
||||
import { DimensionsContext } from './dimensions';
|
||||
import debounce from './utils/debounce';
|
||||
|
||||
const Inside = createStackNavigator();
|
||||
const InsideStack = () => {
|
||||
|
@ -43,7 +46,6 @@ const InsideStack = () => {
|
|||
};
|
||||
screenOptions.headerStyle = {
|
||||
...screenOptions.headerStyle,
|
||||
// TODO: fix on multiple files PR :)
|
||||
height: 57
|
||||
};
|
||||
|
||||
|
@ -84,7 +86,7 @@ const OutsideStack = () => {
|
|||
// App
|
||||
const Stack = createStackNavigator();
|
||||
export const App = ({ root }) => (
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
||||
<>
|
||||
{!root ? (
|
||||
<Stack.Screen
|
||||
|
@ -115,13 +117,17 @@ App.propTypes = {
|
|||
class Root extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { width, height, scale } = Dimensions.get('screen');
|
||||
this.state = {
|
||||
theme: defaultTheme(),
|
||||
themePreferences: {
|
||||
currentTheme: supportSystemTheme() ? 'automatic' : 'light',
|
||||
darkLevel: 'dark'
|
||||
},
|
||||
root: ''
|
||||
root: '',
|
||||
width,
|
||||
height,
|
||||
scale
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
@ -159,13 +165,33 @@ 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() {
|
||||
const { theme, root } = this.state;
|
||||
const {
|
||||
theme, root, width, height, scale
|
||||
} = this.state;
|
||||
const navTheme = navigationTheme(theme);
|
||||
return (
|
||||
<AppearanceProvider>
|
||||
<Provider store={store}>
|
||||
<ThemeContext.Provider value={{ theme }}>
|
||||
<DimensionsContext.Provider
|
||||
value={{
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
setDimensions: this.setDimensions
|
||||
}}
|
||||
>
|
||||
<NavigationContainer
|
||||
theme={navTheme}
|
||||
ref={Navigation.navigationRef}
|
||||
|
@ -181,6 +207,7 @@ class Root extends React.Component {
|
|||
<App root={root} />
|
||||
</NavigationContainer>
|
||||
<ScreenLockedView />
|
||||
</DimensionsContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
</Provider>
|
||||
</AppearanceProvider>
|
||||
|
|
|
@ -53,6 +53,7 @@ import AttachmentView from '../views/AttachmentView';
|
|||
import ModalBlockView from '../views/ModalBlockView';
|
||||
import JitsiMeetView from '../views/JitsiMeetView';
|
||||
import StatusView from '../views/StatusView';
|
||||
import ShareView from '../views/ShareView';
|
||||
import CreateDiscussionView from '../views/CreateDiscussionView';
|
||||
|
||||
// ChatsStackNavigator
|
||||
|
@ -303,6 +304,10 @@ const InsideStackNavigator = () => {
|
|||
name='StatusView'
|
||||
component={StatusView}
|
||||
/>
|
||||
<InsideStack.Screen
|
||||
name='ShareView'
|
||||
component={ShareView}
|
||||
/>
|
||||
<InsideStack.Screen
|
||||
name='ModalBlockView'
|
||||
component={ModalBlockView}
|
||||
|
|
|
@ -50,6 +50,7 @@ import StatusView from '../../views/StatusView';
|
|||
import CreateDiscussionView from '../../views/CreateDiscussionView';
|
||||
|
||||
import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
||||
import ShareView from '../../views/ShareView';
|
||||
|
||||
// ChatsStackNavigator
|
||||
const ChatsStack = createStackNavigator();
|
||||
|
@ -286,6 +287,10 @@ const InsideStackNavigator = React.memo(() => {
|
|||
component={JitsiMeetView}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<InsideStack.Screen
|
||||
name='ShareView'
|
||||
component={ShareView}
|
||||
/>
|
||||
</InsideStack.Navigator>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -16,9 +16,15 @@ export const isReadOnly = async(room, user) => {
|
|||
if (room.archived) {
|
||||
return true;
|
||||
}
|
||||
if (isMuted(room, user)) {
|
||||
return true;
|
||||
}
|
||||
if (room?.ro) {
|
||||
const allowPost = await canPost(room);
|
||||
if (allowPost) {
|
||||
return false;
|
||||
}
|
||||
return (room && room.ro) || isMuted(room, user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
export const canUploadFile = (file, serverInfo) => {
|
||||
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = serverInfo;
|
||||
export const canUploadFile = (file, allowList, maxFileSize) => {
|
||||
if (!(file && file.path)) {
|
||||
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' };
|
||||
}
|
||||
// if white list is empty, all media types are enabled
|
||||
if (!FileUpload_MediaTypeWhiteList || FileUpload_MediaTypeWhiteList === '*') {
|
||||
if (!allowList || allowList === '*') {
|
||||
return { success: true };
|
||||
}
|
||||
const allowedMime = FileUpload_MediaTypeWhiteList.split(',');
|
||||
const allowedMime = allowList.split(',');
|
||||
if (allowedMime.includes(file.mime)) {
|
||||
return { success: true };
|
||||
}
|
||||
|
|
|
@ -7,18 +7,22 @@ import * as mime from 'react-native-mime-types';
|
|||
import { FileSystem } from 'react-native-unimodules';
|
||||
import { Video } from 'expo-av';
|
||||
import SHA256 from 'js-sha256';
|
||||
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { LISTENER } from '../containers/Toast';
|
||||
import EventEmitter from '../utils/events';
|
||||
import I18n from '../i18n';
|
||||
import { withTheme } from '../theme';
|
||||
import ImageViewer from '../presentation/ImageViewer';
|
||||
import { ImageViewer } from '../presentation/ImageViewer';
|
||||
import { themes } from '../constants/colors';
|
||||
import { formatAttachmentUrl } from '../lib/utils';
|
||||
import RCActivityIndicator from '../containers/ActivityIndicator';
|
||||
import { SaveButton, CloseModalButton } from '../containers/HeaderButton';
|
||||
import { isAndroid } from '../utils/deviceInfo';
|
||||
import { getUserSelector } from '../selectors/login';
|
||||
import { withDimensions } from '../dimensions';
|
||||
import { getHeaderHeight } from '../containers/Header';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -32,6 +36,9 @@ class AttachmentView extends React.Component {
|
|||
route: PropTypes.object,
|
||||
theme: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
insets: PropTypes.object,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
|
@ -61,13 +68,16 @@ class AttachmentView extends React.Component {
|
|||
}
|
||||
|
||||
setHeader = () => {
|
||||
const { route, navigation } = this.props;
|
||||
const { route, navigation, theme } = this.props;
|
||||
const attachment = route.params?.attachment;
|
||||
const { title } = attachment;
|
||||
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),
|
||||
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);
|
||||
}
|
||||
|
@ -107,12 +117,21 @@ class AttachmentView extends React.Component {
|
|||
this.setState({ loading: false });
|
||||
};
|
||||
|
||||
renderImage = uri => (
|
||||
renderImage = (uri) => {
|
||||
const {
|
||||
theme, width, height, insets
|
||||
} = 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 => (
|
||||
<Video
|
||||
|
@ -146,6 +165,7 @@ class AttachmentView extends React.Component {
|
|||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<StatusBar barStyle='light-content' backgroundColor={themes[theme].previewBackground} />
|
||||
{content}
|
||||
{loading ? <RCActivityIndicator absolute size='large' theme={theme} /> : null}
|
||||
</View>
|
||||
|
@ -158,4 +178,4 @@ const mapStateToProps = 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') {
|
||||
icon = 'hash';
|
||||
} else if (type === 'l') {
|
||||
icon = 'omnichannel';
|
||||
icon = 'livechat';
|
||||
} else if (type === 'd') {
|
||||
icon = 'team';
|
||||
} else {
|
||||
|
|
|
@ -173,7 +173,7 @@ class UploadProgress extends Component {
|
|||
[
|
||||
<View key='row' style={styles.row}>
|
||||
<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}
|
||||
</Text>
|
||||
<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}>
|
||||
<CustomIcon name='warning' size={20} color={themes[theme].dangerColor} />
|
||||
<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)}>
|
||||
<Text style={[styles.tryAgainButtonText, { color: themes[theme].tintColor }]}>{I18n.t('Try_again')}</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -212,9 +212,6 @@ class RoomView extends React.Component {
|
|||
if (roomUpdate.topic !== prevState.roomUpdate.topic) {
|
||||
this.setHeader();
|
||||
}
|
||||
if (!isEqual(prevState.roomUpdate.roles, roomUpdate.roles)) {
|
||||
this.setReadOnly();
|
||||
}
|
||||
}
|
||||
// If it's a livechat room
|
||||
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) {
|
||||
this.setHeader();
|
||||
}
|
||||
this.setReadOnly();
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Text, FlatList, Keyboard, BackHandler
|
||||
View, Text, FlatList, Keyboard, BackHandler, PermissionsAndroid, ScrollView
|
||||
} from 'react-native';
|
||||
import ShareExtension from 'rn-extensions-share';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { connect } from 'react-redux';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import * as mime from 'react-native-mime-types';
|
||||
import { isEqual, orderBy } from 'lodash';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import database from '../../lib/database';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||
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 ServerItem from '../../presentation/ServerItem';
|
||||
import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||
|
@ -28,6 +25,12 @@ import { themes } from '../../constants/colors';
|
|||
import { animateNextTransition } from '../../utils/layoutAnimation';
|
||||
import { withTheme } from '../../theme';
|
||||
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 getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
|
||||
|
@ -46,52 +49,47 @@ class ShareListView extends React.Component {
|
|||
super(props);
|
||||
this.data = [];
|
||||
this.state = {
|
||||
showError: false,
|
||||
searching: false,
|
||||
searchText: '',
|
||||
value: '',
|
||||
isMedia: false,
|
||||
mediaLoading: false,
|
||||
fileInfo: null,
|
||||
searchResults: [],
|
||||
chats: [],
|
||||
servers: [],
|
||||
attachments: [],
|
||||
text: '',
|
||||
loading: true,
|
||||
serverInfo: null
|
||||
serverInfo: null,
|
||||
needsPermission: isAndroid || false
|
||||
};
|
||||
this.setHeader();
|
||||
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;
|
||||
setTimeout(async() => {
|
||||
try {
|
||||
const { value, type } = await ShareExtension.data();
|
||||
let fileInfo = null;
|
||||
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 }`
|
||||
};
|
||||
const data = await ShareExtension.data();
|
||||
if (isAndroid) {
|
||||
await this.askForPermission(data);
|
||||
}
|
||||
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({
|
||||
value, fileInfo, isMedia, mediaLoading: false
|
||||
text,
|
||||
attachments
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
this.setState({ mediaLoading: false });
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
this.getSubscriptions(server);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
|
@ -102,14 +100,11 @@ class ShareListView extends React.Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { searching } = this.state;
|
||||
const { searching, needsPermission } = this.state;
|
||||
if (nextState.searching !== searching) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { isMedia } = this.state;
|
||||
if (nextState.isMedia !== isMedia) {
|
||||
this.getSubscriptions(nextProps.server, nextState.fileInfo);
|
||||
if (nextState.needsPermission !== needsPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -191,8 +186,7 @@ class ShareListView extends React.Component {
|
|||
this.setState(...args);
|
||||
}
|
||||
|
||||
getSubscriptions = async(server, fileInfo) => {
|
||||
const { fileInfo: fileData } = this.state;
|
||||
getSubscriptions = async(server) => {
|
||||
const db = database.active;
|
||||
const serversDB = database.servers;
|
||||
|
||||
|
@ -215,20 +209,29 @@ class ShareListView extends React.Component {
|
|||
// Do nothing
|
||||
}
|
||||
|
||||
const canUploadFileResult = canUploadFile(fileInfo || fileData, serverInfo);
|
||||
|
||||
this.internalSetState({
|
||||
chats: this.chats ? this.chats.slice() : [],
|
||||
servers: this.servers ? this.servers.slice() : [],
|
||||
loading: false,
|
||||
showError: !canUploadFileResult.success,
|
||||
error: canUploadFileResult.error,
|
||||
serverInfo
|
||||
});
|
||||
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);
|
||||
|
||||
getRoomTitle = (item) => {
|
||||
|
@ -237,16 +240,16 @@ class ShareListView extends React.Component {
|
|||
return ((item.prid || useRealName) && item.fname) || item.name;
|
||||
}
|
||||
|
||||
shareMessage = (item) => {
|
||||
const { value, isMedia, fileInfo } = this.state;
|
||||
shareMessage = (room) => {
|
||||
const { attachments, text, serverInfo } = this.state;
|
||||
const { navigation } = this.props;
|
||||
|
||||
navigation.navigate('ShareView', {
|
||||
rid: item.rid,
|
||||
value,
|
||||
isMedia,
|
||||
fileInfo,
|
||||
name: this.getRoomTitle(item)
|
||||
room,
|
||||
text,
|
||||
attachments,
|
||||
serverInfo,
|
||||
isShareExtension: true
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -305,13 +308,13 @@ class ShareListView extends React.Component {
|
|||
}}
|
||||
title={this.getRoomTitle(item)}
|
||||
baseUrl={server}
|
||||
avatar={this.getRoomTitle(item)}
|
||||
avatar={RocketChat.getRoomAvatar(item)}
|
||||
description={
|
||||
item.t === 'c'
|
||||
? (item.topic || item.description)
|
||||
: item.fname
|
||||
}
|
||||
type={item.t}
|
||||
type={item.prid ? 'discussion' : item.t}
|
||||
onPress={() => this.shareMessage(item)}
|
||||
testID={`share-extension-item-${ item.name }`}
|
||||
theme={theme}
|
||||
|
@ -384,14 +387,26 @@ class ShareListView extends React.Component {
|
|||
|
||||
renderContent = () => {
|
||||
const {
|
||||
chats, mediaLoading, loading, searchResults, searching, searchText
|
||||
chats, loading, searchResults, searching, searchText, needsPermission
|
||||
} = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
if (mediaLoading || loading) {
|
||||
if (loading) {
|
||||
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 (
|
||||
<FlatList
|
||||
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() {
|
||||
const { showError } = this.state;
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<SafeAreaView theme={theme}>
|
||||
<StatusBar theme={theme} />
|
||||
{ showError ? this.renderError() : this.renderContent() }
|
||||
{this.renderContent()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,5 +53,17 @@ export default StyleSheet.create({
|
|||
title: {
|
||||
fontSize: 14,
|
||||
...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,24 +1,340 @@
|
|||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
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 ShareExtension from 'rn-extensions-share';
|
||||
import * as VideoThumbnails from 'expo-video-thumbnails';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import I18n from '../../i18n';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import log from '../../utils/log';
|
||||
import styles from './styles';
|
||||
import TextInput from '../../containers/TextInput';
|
||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||
import Loading from '../../containers/Loading';
|
||||
import {
|
||||
Item,
|
||||
CloseModalButton,
|
||||
CustomHeaderButtons
|
||||
} from '../../containers/HeaderButton';
|
||||
import { isBlocked } from '../../utils/room';
|
||||
import { isReadOnly } from '../../utils/isReadOnly';
|
||||
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 {
|
||||
static propTypes = {
|
||||
class ShareView extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.messagebox = React.createRef();
|
||||
this.files = props.route.params?.attachments ?? [];
|
||||
this.isShareExtension = props.route.params?.isShareExtension;
|
||||
this.serverInfo = props.route.params?.serverInfo ?? {};
|
||||
|
||||
this.state = {
|
||||
selected: {},
|
||||
loading: false,
|
||||
readOnly: false,
|
||||
attachments: [],
|
||||
text: props.route.params?.text ?? '',
|
||||
room: props.route.params?.room ?? {},
|
||||
thread: props.route.params?.thread ?? {},
|
||||
maxFileSize: this.isShareExtension ? this.serverInfo?.FileUpload_MaxFileSize : props.FileUpload_MaxFileSize,
|
||||
mediaAllowList: this.isShareExtension ? this.serverInfo?.FileUpload_MediaTypeWhiteList : props.FileUpload_MediaTypeWhiteList
|
||||
};
|
||||
this.getServerInfo();
|
||||
}
|
||||
|
||||
componentDidMount = async() => {
|
||||
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 = () => {
|
||||
const {
|
||||
room, thread, readOnly, attachments
|
||||
} = this.state;
|
||||
const { navigation, theme } = this.props;
|
||||
|
||||
const options = {
|
||||
headerTitle: () => <Header room={room} thread={thread} />,
|
||||
headerTitleAlign: 'left',
|
||||
headerTintColor: themes[theme].previewTintColor
|
||||
};
|
||||
|
||||
// if is share extension show default back button
|
||||
if (!this.isShareExtension) {
|
||||
options.headerLeft = () => <CloseModalButton navigation={navigation} buttonStyle={{ color: themes[theme].previewTintColor }} />;
|
||||
}
|
||||
|
||||
if (!attachments.length && !readOnly) {
|
||||
options.headerRight = () => (
|
||||
<CustomHeaderButtons>
|
||||
<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);
|
||||
}
|
||||
|
||||
// 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 { user } = this.props;
|
||||
const readOnly = await isReadOnly(room, user);
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// get video thumbnails
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update state
|
||||
await this.selectFile(selected);
|
||||
|
||||
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 {
|
||||
navigation.pop();
|
||||
}
|
||||
|
||||
try {
|
||||
// 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();
|
||||
}));
|
||||
|
||||
// Send text message
|
||||
} else if (text.length) {
|
||||
await RocketChat.sendMessage(room.rid, text, thread?.tmid, { id: user.id, token: user.token });
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// if it's share extension this should close
|
||||
if (this.isShareExtension) {
|
||||
ShareExtension.close();
|
||||
}
|
||||
};
|
||||
|
||||
selectFile = (item) => {
|
||||
const { attachments, selected } = this.state;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
removeFile = (item) => {
|
||||
const { selected, attachments } = this.state;
|
||||
let newSelected;
|
||||
if (item.path === selected.path) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
containerStyle={styles.inputContainer}
|
||||
inputStyle={[
|
||||
styles.input,
|
||||
styles.textInput,
|
||||
{ backgroundColor: themes[theme].focusedBackground }
|
||||
]}
|
||||
placeholder=''
|
||||
onChangeText={this.onChangeText}
|
||||
defaultValue=''
|
||||
multiline
|
||||
textAlignVertical='top'
|
||||
autoFocus
|
||||
theme={theme}
|
||||
value={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { readOnly, room, loading } = this.state;
|
||||
const { theme } = this.props;
|
||||
if (readOnly || isBlocked(room)) {
|
||||
return (
|
||||
<View style={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>
|
||||
{isBlocked(room) ? I18n.t('This_room_is_blocked') : I18n.t('This_room_is_read_only')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||
theme={theme}
|
||||
>
|
||||
<StatusBar barStyle='light-content' backgroundColor={themes[theme].previewBackground} />
|
||||
{this.renderContent()}
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShareView.propTypes = {
|
||||
navigation: PropTypes.object,
|
||||
route: PropTypes.object,
|
||||
theme: PropTypes.string,
|
||||
|
@ -27,281 +343,16 @@ class ShareView extends React.Component {
|
|||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
server: PropTypes.string
|
||||
};
|
||||
server: PropTypes.string,
|
||||
FileUpload_MediaTypeWhiteList: PropTypes.string,
|
||||
FileUpload_MaxFileSize: PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { route } = this.props;
|
||||
const rid = route.params?.rid;
|
||||
const name = route.params?.name;
|
||||
const value = route.params?.value;
|
||||
const isMedia = route.params?.isMedia ?? false;
|
||||
const fileInfo = route.params?.fileInfo ?? {};
|
||||
const room = route.params?.room ?? { rid };
|
||||
|
||||
this.state = {
|
||||
rid,
|
||||
value,
|
||||
isMedia,
|
||||
name,
|
||||
fileInfo,
|
||||
room,
|
||||
loading: false,
|
||||
readOnly: false,
|
||||
file: {
|
||||
name: fileInfo ? fileInfo.name : '',
|
||||
description: ''
|
||||
},
|
||||
canSend: false
|
||||
};
|
||||
|
||||
this.setReadOnly();
|
||||
this.setHeader();
|
||||
}
|
||||
|
||||
setHeader = () => {
|
||||
const { canSend } = this.state;
|
||||
const { navigation } = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
title: I18n.t('Share'),
|
||||
headerRight:
|
||||
() => (canSend
|
||||
? (
|
||||
<CustomHeaderButtons>
|
||||
<Item
|
||||
title={I18n.t('Send')}
|
||||
onPress={this.sendMessage}
|
||||
testID='send-message-share-view'
|
||||
buttonStyle={styles.send}
|
||||
/>
|
||||
</CustomHeaderButtons>
|
||||
)
|
||||
: null)
|
||||
});
|
||||
}
|
||||
|
||||
setReadOnly = async() => {
|
||||
const { room } = this.state;
|
||||
const { user } = this.props;
|
||||
const { username } = user;
|
||||
const readOnly = await isReadOnly(room, { username });
|
||||
|
||||
this.setState({ readOnly, canSend: !(readOnly || isBlocked(room)) }, () => this.setHeader());
|
||||
}
|
||||
|
||||
bytesToSize = bytes => `${ (bytes / 1048576).toFixed(2) }MB`;
|
||||
|
||||
sendMessage = async() => {
|
||||
const { isMedia, loading } = this.state;
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
if (isMedia) {
|
||||
await this.sendMediaMessage();
|
||||
} else {
|
||||
await this.sendTextMessage();
|
||||
}
|
||||
|
||||
this.setState({ loading: false });
|
||||
ShareExtension.close();
|
||||
}
|
||||
|
||||
sendMediaMessage = async() => {
|
||||
const { rid, fileInfo, file } = this.state;
|
||||
const { server, user } = this.props;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendTextMessage = async() => {
|
||||
const { value, rid } = this.state;
|
||||
const { user } = this.props;
|
||||
if (value !== '' && rid !== '') {
|
||||
try {
|
||||
await RocketChat.sendMessage(rid, value, undefined, user);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderPreview = () => {
|
||||
const { fileInfo } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
const icon = fileInfo.mime.match(/image/)
|
||||
? <Image source={{ isStatic: true, uri: fileInfo.path }} style={styles.mediaImage} />
|
||||
: (
|
||||
<View style={styles.mediaIconContainer}>
|
||||
<CustomIcon name='clip' style={styles.mediaIcon} />
|
||||
</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 (
|
||||
<TextInput
|
||||
containerStyle={[styles.content, styles.inputContainer]}
|
||||
inputStyle={[
|
||||
styles.input,
|
||||
styles.textInput,
|
||||
{
|
||||
borderColor: themes[theme].separatorColor,
|
||||
backgroundColor: themes[theme].focusedBackground
|
||||
}
|
||||
]}
|
||||
placeholder=''
|
||||
onChangeText={handleText => this.setState({ value: handleText })}
|
||||
defaultValue={value}
|
||||
multiline
|
||||
textAlignVertical='top'
|
||||
autoFocus
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
const { theme } = this.props;
|
||||
const {
|
||||
name, loading, isMedia, room, readOnly
|
||||
} = this.state;
|
||||
|
||||
if (readOnly || isBlocked(room)) {
|
||||
return this.renderError();
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
</View>
|
||||
<View style={[styles.content, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||
{isMedia ? this.renderMediaContent() : this.renderInput()}
|
||||
</View>
|
||||
{ loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (({ share }) => ({
|
||||
user: {
|
||||
id: share.user && share.user.id,
|
||||
username: share.user && share.user.username,
|
||||
token: share.user && share.user.token
|
||||
},
|
||||
server: share.server
|
||||
}));
|
||||
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));
|
||||
|
|
|
@ -6,6 +6,16 @@ export default StyleSheet.create({
|
|||
container: {
|
||||
flex: 1
|
||||
},
|
||||
input: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 0
|
||||
},
|
||||
textInput: {
|
||||
height: '100%'
|
||||
},
|
||||
centered: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
|
@ -15,82 +25,6 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textBold,
|
||||
...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: {
|
||||
...sharedStyles.textSemibold,
|
||||
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):
|
||||
- UMCore
|
||||
- UMPermissionsInterface
|
||||
- EXVideoThumbnails (4.1.1):
|
||||
- UMCore
|
||||
- UMFileSystemInterface
|
||||
- EXWebBrowser (8.2.1):
|
||||
- UMCore
|
||||
- Fabric (1.10.2)
|
||||
|
@ -431,7 +434,7 @@ PODS:
|
|||
- React
|
||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||
- React
|
||||
- rn-extensions-share (2.3.10):
|
||||
- rn-extensions-share (2.4.0):
|
||||
- React
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
|
@ -462,15 +465,15 @@ PODS:
|
|||
- React
|
||||
- RNGestureHandler (1.6.1):
|
||||
- React
|
||||
- RNImageCropPicker (0.30.0):
|
||||
- RNImageCropPicker (0.31.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.30.0)
|
||||
- RSKImageCropper
|
||||
- RNImageCropPicker/QBImagePickerController (0.30.0):
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.31.1)
|
||||
- TOCropViewController
|
||||
- RNImageCropPicker/QBImagePickerController (0.31.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RSKImageCropper
|
||||
- TOCropViewController
|
||||
- RNLocalize (1.4.0):
|
||||
- React
|
||||
- RNReanimated (1.8.0):
|
||||
|
@ -483,13 +486,13 @@ PODS:
|
|||
- React
|
||||
- RNVectorIcons (6.6.0):
|
||||
- React
|
||||
- RSKImageCropper (2.2.3)
|
||||
- SDWebImage (5.7.4):
|
||||
- SDWebImage/Core (= 5.7.4)
|
||||
- SDWebImage/Core (5.7.4)
|
||||
- SDWebImageWebPCoder (0.4.1):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.5)
|
||||
- TOCropViewController (2.5.2)
|
||||
- UMAppLoader (1.0.2)
|
||||
- UMBarCodeScannerInterface (5.1.0)
|
||||
- UMCameraInterface (5.1.0)
|
||||
|
@ -522,6 +525,7 @@ DEPENDENCIES:
|
|||
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
||||
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
||||
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
||||
|
@ -644,9 +648,9 @@ SPEC REPOS:
|
|||
- nanopb
|
||||
- OpenSSL-Universal
|
||||
- PromisesObjC
|
||||
- RSKImageCropper
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- TOCropViewController
|
||||
- YogaKit
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
|
@ -670,6 +674,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/expo-local-authentication/ios"
|
||||
EXPermissions:
|
||||
:path: "../node_modules/expo-permissions/ios"
|
||||
EXVideoThumbnails:
|
||||
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||
EXWebBrowser:
|
||||
:path: "../node_modules/expo-web-browser/ios"
|
||||
FBLazyVector:
|
||||
|
@ -833,6 +839,7 @@ SPEC CHECKSUMS:
|
|||
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
||||
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
||||
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
||||
EXVideoThumbnails: be6984a3cda1e44c45b5c6278244e99855f99a0a
|
||||
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
||||
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
||||
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
||||
|
@ -894,7 +901,7 @@ SPEC CHECKSUMS:
|
|||
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
||||
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||
rn-extensions-share: 4bfee75806ad54aadeff1dfa535697a6345a50b8
|
||||
rn-extensions-share: 8db79372089567cbc5aefe8444869bbc808578d3
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
||||
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
||||
|
@ -905,16 +912,16 @@ SPEC CHECKSUMS:
|
|||
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
||||
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
||||
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
||||
RNImageCropPicker: a606d65f71c6c05caa3c850c16fb1ba2a4718608
|
||||
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
|
||||
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
||||
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
||||
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
||||
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
||||
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
||||
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
||||
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
|
||||
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
||||
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
||||
TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
|
||||
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
||||
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
||||
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",
|
||||
"version": "0.30.0",
|
||||
"version": "0.31.1",
|
||||
"summary": "Select single or multiple images, with cropping option",
|
||||
"requires_arc": true,
|
||||
"license": "MIT",
|
||||
|
@ -10,21 +10,21 @@
|
|||
},
|
||||
"source": {
|
||||
"git": "https://github.com/ivpusic/react-native-image-crop-picker",
|
||||
"tag": "v0.30.0"
|
||||
"tag": "v0.31.1"
|
||||
},
|
||||
"source_files": "ios/src/*.{h,m}",
|
||||
"platforms": {
|
||||
"ios": "8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"RSKImageCropper": [
|
||||
|
||||
],
|
||||
"React-Core": [
|
||||
|
||||
],
|
||||
"React-RCTImage": [
|
||||
|
||||
],
|
||||
"TOCropViewController": [
|
||||
|
||||
]
|
||||
},
|
||||
"subspecs": [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rn-extensions-share",
|
||||
"version": "2.3.10",
|
||||
"version": "2.4.0",
|
||||
"summary": "Share-Extension using react-native for both ios and android",
|
||||
"license": "MIT",
|
||||
"authors": {
|
||||
|
|
|
@ -34,6 +34,9 @@ PODS:
|
|||
- EXPermissions (8.1.0):
|
||||
- UMCore
|
||||
- UMPermissionsInterface
|
||||
- EXVideoThumbnails (4.1.1):
|
||||
- UMCore
|
||||
- UMFileSystemInterface
|
||||
- EXWebBrowser (8.2.1):
|
||||
- UMCore
|
||||
- Fabric (1.10.2)
|
||||
|
@ -431,7 +434,7 @@ PODS:
|
|||
- React
|
||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||
- React
|
||||
- rn-extensions-share (2.3.10):
|
||||
- rn-extensions-share (2.4.0):
|
||||
- React
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
|
@ -462,15 +465,15 @@ PODS:
|
|||
- React
|
||||
- RNGestureHandler (1.6.1):
|
||||
- React
|
||||
- RNImageCropPicker (0.30.0):
|
||||
- RNImageCropPicker (0.31.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.30.0)
|
||||
- RSKImageCropper
|
||||
- RNImageCropPicker/QBImagePickerController (0.30.0):
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.31.1)
|
||||
- TOCropViewController
|
||||
- RNImageCropPicker/QBImagePickerController (0.31.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RSKImageCropper
|
||||
- TOCropViewController
|
||||
- RNLocalize (1.4.0):
|
||||
- React
|
||||
- RNReanimated (1.8.0):
|
||||
|
@ -483,13 +486,13 @@ PODS:
|
|||
- React
|
||||
- RNVectorIcons (6.6.0):
|
||||
- React
|
||||
- RSKImageCropper (2.2.3)
|
||||
- SDWebImage (5.7.4):
|
||||
- SDWebImage/Core (= 5.7.4)
|
||||
- SDWebImage/Core (5.7.4)
|
||||
- SDWebImageWebPCoder (0.4.1):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.5)
|
||||
- TOCropViewController (2.5.2)
|
||||
- UMAppLoader (1.0.2)
|
||||
- UMBarCodeScannerInterface (5.1.0)
|
||||
- UMCameraInterface (5.1.0)
|
||||
|
@ -522,6 +525,7 @@ DEPENDENCIES:
|
|||
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
||||
- EXPermissions (from `../node_modules/expo-permissions/ios`)
|
||||
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
|
||||
|
@ -644,9 +648,9 @@ SPEC REPOS:
|
|||
- nanopb
|
||||
- OpenSSL-Universal
|
||||
- PromisesObjC
|
||||
- RSKImageCropper
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- TOCropViewController
|
||||
- YogaKit
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
|
@ -670,6 +674,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/expo-local-authentication/ios"
|
||||
EXPermissions:
|
||||
:path: "../node_modules/expo-permissions/ios"
|
||||
EXVideoThumbnails:
|
||||
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||
EXWebBrowser:
|
||||
:path: "../node_modules/expo-web-browser/ios"
|
||||
FBLazyVector:
|
||||
|
@ -833,6 +839,7 @@ SPEC CHECKSUMS:
|
|||
EXKeepAwake: d045bc2cf1ad5a04f0323cc7c894b95b414042e0
|
||||
EXLocalAuthentication: bbf1026cc289d729da4f29240dd7a8f6a14e4b20
|
||||
EXPermissions: 24b97f734ce9172d245a5be38ad9ccfcb6135964
|
||||
EXVideoThumbnails: be6984a3cda1e44c45b5c6278244e99855f99a0a
|
||||
EXWebBrowser: 5902f99ac5ac551e5c82ff46f13a337b323aa9ea
|
||||
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
|
||||
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
|
||||
|
@ -894,7 +901,7 @@ SPEC CHECKSUMS:
|
|||
ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
|
||||
ReactNativeKeyboardInput: c37e26821519869993b3b61844350feb9177ff37
|
||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||
rn-extensions-share: 4bfee75806ad54aadeff1dfa535697a6345a50b8
|
||||
rn-extensions-share: 8db79372089567cbc5aefe8444869bbc808578d3
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNAudio: cae2991f2dccb75163f260b60da8051717b959fa
|
||||
RNBootSplash: 7cb9b4fe7e94177edc0d11010f7631d79db2f5e9
|
||||
|
@ -905,16 +912,16 @@ SPEC CHECKSUMS:
|
|||
RNFastImage: 35ae972d6727c84ee3f5c6897e07f84d0a3445e9
|
||||
RNFirebase: 37daa9a346d070f9f6ee1f3b4aaf4c8e3b1d5d1c
|
||||
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
||||
RNImageCropPicker: a606d65f71c6c05caa3c850c16fb1ba2a4718608
|
||||
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
|
||||
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
|
||||
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
||||
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
|
||||
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
||||
RNUserDefaults: c421fd97ad06b35c16608c5d0fe675db353f632d
|
||||
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
||||
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
|
||||
SDWebImage: 48b88379b798fd1e4298f95bb25d2cdabbf4deb3
|
||||
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
|
||||
TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
|
||||
UMAppLoader: ee77a072f9e15128f777ccd6d2d00f52ab4387e6
|
||||
UMBarCodeScannerInterface: 9dc692b87e5f20fe277fa57aa47f45d418c3cc6c
|
||||
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