[NEW] Send multiple attachments (#2162)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-06-26 17:22:56 -03:00 committed by GitHub
parent a992c51698
commit 07e9bcb776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
224 changed files with 23596 additions and 21112 deletions

View File

@ -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>

View File

@ -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()
);
}

View File

@ -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'
}
};

View File

@ -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();

View File

@ -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,

View File

@ -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>
));

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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));

View File

@ -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>
);

View File

@ -103,5 +103,8 @@ export default StyleSheet.create({
},
scrollViewMention: {
maxHeight: SCROLLVIEW_MENTION_HEIGHT
},
buttonsWhitespace: {
width: 15
}
});

View File

@ -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
]}

View File

@ -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;

View File

@ -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

View File

@ -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',

View File

@ -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',

View File

@ -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;
};

View File

@ -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);

View File

@ -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
};

View File

@ -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;

View File

@ -0,0 +1,3 @@
export * from './ImageViewer';
export * from './types';
export * from './ImageComponent';

View File

@ -0,0 +1,4 @@
export const types = {
FAST_IMAGE: 'FAST_IMAGE',
REACT_NATIVE_IMAGE: 'REACT_NATIVE'
};

View File

@ -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;

View File

@ -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>

View File

@ -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}

View File

@ -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>
);
});

View File

@ -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;
};

View File

@ -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 };
}

View File

@ -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))));

View File

@ -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 {

View File

@ -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>

View File

@ -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() {

View File

@ -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>
);
}

View File

@ -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
}
});

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1 @@
export const THUMBS_HEIGHT = 74;

View File

@ -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));

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -0,0 +1 @@
../../../../../node_modules/expo-video-thumbnails/ios/EXVideoThumbnails/EXVideoThumbnailsModule.h

View File

@ -0,0 +1 @@
../../../../../node_modules/react-native-image-crop-picker/ios/src/UIImage+Extension.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/CGGeometry+RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController+Protected.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageScrollView.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKInternalUtility.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKTouchView.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/UIApplication+RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/UIImage+RSKImageCropper.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropOverlayView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropScrollView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropToolbar.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Constants/TOCropViewConstants.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/TOCropViewController.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h

View File

@ -0,0 +1 @@
../../../../../node_modules/expo-video-thumbnails/ios/EXVideoThumbnails/EXVideoThumbnailsModule.h

View File

@ -0,0 +1 @@
../../../../../node_modules/react-native-image-crop-picker/ios/src/UIImage+Extension.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/CGGeometry+RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController+Protected.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropViewController.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKImageScrollView.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKInternalUtility.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/RSKTouchView.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/UIApplication+RSKImageCropper.h

View File

@ -1 +0,0 @@
../../../RSKImageCropper/RSKImageCropper/UIImage+RSKImageCropper.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropOverlayView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropScrollView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropToolbar.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Views/TOCropView.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Constants/TOCropViewConstants.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/TOCropViewController.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCropViewControllerTransitioning.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h

View File

@ -0,0 +1 @@
../../../TOCropViewController/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h

View File

@ -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": [
]
}
}

View File

@ -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": [

View File

@ -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": {

29
ios/Pods/Manifest.lock generated
View File

@ -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

View File

@ -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.

View File

@ -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);

View File

@ -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));
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -1,6 +0,0 @@
framework module RSKImageCropper {
umbrella header "RSKImageCropper.h"
export *
module * { export * }
}

View File

@ -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" = "تحديد";

View File

@ -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