[MM-16263] UI/UX Improvements to the mobile post draft area (#3807)
* Adding base button functionality Moving file upload previews to be under textbox * Ensuring textbox is scrollable when in landscape mode * Updated image picker to use mixed camera option * Added unit tests, fixed other tests affected by dependency update * Updated patch for react-native-image-picker to 1.1.0 * Fixing incorrect import of DocumentPicker * MM-20989: Ensuring keyboard doesn't dismiss while submitting post (#3758) * Ensuring keyboard doesn't dismiss while submitting post * Update snapshot * Preventing the @ icon from being repeatedly tappable (#3777) * Fix snapshot from merge * MM-21736 Select/Take images and videos for Android * MM-21737 Fix attachment error message position on iOS * Remove FileUploadPreview from the iOS Thread screen * Fix android camera permissions * Fix post input box sizing and disable scrollview * Fix iOS photo gallery videos Co-authored-by: Andre Vasconcelos <andre.onogoro@gmail.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
ac048428cb
commit
ef0274cad8
28 changed files with 1373 additions and 298 deletions
|
|
@ -16,11 +16,11 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
|||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 28,
|
||||
"width": 72,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "rgba(22,109,224,0.3)",
|
||||
|
|
@ -54,11 +54,11 @@ exports[`SendButton should match snapshot 1`] = `
|
|||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 28,
|
||||
"width": 72,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
@ -88,11 +88,11 @@ exports[`SendButton should render theme backgroundColor 1`] = `
|
|||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 28,
|
||||
"width": 72,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import {PermissionTypes} from 'app/constants';
|
|||
import AttachmentButton from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AttachmentButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
|
|
|
|||
|
|
@ -186,6 +186,12 @@ const style = StyleSheet.create({
|
|||
smallImageOverlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
},
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
singleSmallImageWrapper: {
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ export default class FileUploadItem extends PureComponent {
|
|||
}
|
||||
</View>
|
||||
<FileUploadRemove
|
||||
theme={this.props.theme}
|
||||
channelId={channelId}
|
||||
clientId={file.clientId}
|
||||
onPress={this.handleRemoveFile}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
|
|
@ -20,7 +21,6 @@ export default class FileUploadPreview extends PureComponent {
|
|||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
|
|
@ -75,10 +75,10 @@ export default class FileUploadPreview extends PureComponent {
|
|||
const {
|
||||
channelIsLoading,
|
||||
filesUploadingForCurrentChannel,
|
||||
deviceHeight,
|
||||
files,
|
||||
} = this.props;
|
||||
const {fileSizeWarning, showFileMaxWarning} = this.state;
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
if (
|
||||
!fileSizeWarning && !showFileMaxWarning &&
|
||||
(channelIsLoading || (!files.length && !filesUploadingForCurrentChannel))
|
||||
|
|
@ -87,8 +87,8 @@ export default class FileUploadPreview extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[style.container, {height: deviceHeight}]}>
|
||||
<View style={style.previewContainer}>
|
||||
<View style={style.fileContainer}>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
style={style.scrollView}
|
||||
|
|
@ -97,6 +97,8 @@ export default class FileUploadPreview extends PureComponent {
|
|||
>
|
||||
{this.buildFilePreviews()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<View style={style.errorContainer}>
|
||||
{showFileMaxWarning && (
|
||||
<FormattedText
|
||||
style={style.warning}
|
||||
|
|
@ -115,25 +117,34 @@ export default class FileUploadPreview extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollViewContent: {
|
||||
alignItems: 'flex-end',
|
||||
marginLeft: 14,
|
||||
},
|
||||
warning: {
|
||||
color: 'white',
|
||||
marginLeft: 14,
|
||||
marginBottom: 10,
|
||||
},
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
fileContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
errorContainer: {
|
||||
height: 18,
|
||||
},
|
||||
previewContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollViewContent: {
|
||||
alignItems: 'flex-end',
|
||||
marginLeft: 14,
|
||||
},
|
||||
warning: {
|
||||
color: theme.errorTextColor,
|
||||
marginLeft: 14,
|
||||
marginBottom: Platform.select({
|
||||
android: 14,
|
||||
ios: 0,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, StyleSheet} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {Platform} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ export default class FileUploadRemove extends PureComponent {
|
|||
clientId: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOnPress = () => {
|
||||
|
|
@ -23,6 +25,7 @@ export default class FileUploadRemove extends PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={style.removeButtonWrapper}
|
||||
|
|
@ -30,9 +33,9 @@ export default class FileUploadRemove extends PureComponent {
|
|||
type={'opacity'}
|
||||
>
|
||||
<Icon
|
||||
name='md-close'
|
||||
color='#fff'
|
||||
size={18}
|
||||
name='close-circle'
|
||||
color={this.props.theme.centerChannelColor}
|
||||
size={20}
|
||||
style={style.removeButtonIcon}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
|
|
@ -40,28 +43,27 @@ export default class FileUploadRemove extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
removeButtonIcon: Platform.select({
|
||||
ios: {
|
||||
marginTop: 2,
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
removeButtonIcon: Platform.select({
|
||||
ios: {
|
||||
marginTop: 2,
|
||||
},
|
||||
}),
|
||||
removeButtonWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
elevation: 11,
|
||||
top: 7,
|
||||
right: 7,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.centerChannelBg,
|
||||
},
|
||||
android: {
|
||||
marginLeft: 1,
|
||||
},
|
||||
}),
|
||||
removeButtonWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
elevation: 11,
|
||||
top: 7,
|
||||
right: 7,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#000',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {ViewTypes} from 'app/constants';
|
|||
const {OnPasteEventManager} = NativeModules;
|
||||
const OnPasteEventEmitter = new NativeEventEmitter(OnPasteEventManager);
|
||||
|
||||
export class PasteableTextInput extends React.Component {
|
||||
export class PasteableTextInput extends React.PureComponent {
|
||||
static propTypes = {
|
||||
...TextInput.PropTypes,
|
||||
onPaste: PropTypes.func,
|
||||
|
|
@ -42,8 +42,8 @@ export class PasteableTextInput extends React.Component {
|
|||
const {height} = event.nativeEvent.contentSize;
|
||||
const {style} = this.props;
|
||||
const {inputHeight} = this.state;
|
||||
const newHeight = height > style.maxHeight ? inputHeight : height + ViewTypes.INPUT_VERTICAL_PADDING;
|
||||
const transitionSpeed = height === ViewTypes.INPUT_LINE_HEIGHT ? 500 : 100;
|
||||
const newHeight = Math.min(style.maxHeight, height + ViewTypes.INPUT_VERTICAL_PADDING);
|
||||
const transitionSpeed = height === ViewTypes.INPUT_LINE_HEIGHT ? 500 : 1;
|
||||
|
||||
Animated.timing(inputHeight, {
|
||||
toValue: newHeight,
|
||||
|
|
|
|||
|
|
@ -20,60 +20,26 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<AttachmentButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
"alignItems": "stretch",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<View
|
||||
disableScrollViewPanResponder={true}
|
||||
keyboardShouldPersistTaps="always"
|
||||
overScrollMode="never"
|
||||
pinchGestureEnabled={false}
|
||||
scrollEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
},
|
||||
]
|
||||
|
|
@ -95,9 +61,8 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
|||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"maxHeight": 100,
|
||||
"maxHeight": 150,
|
||||
"paddingBottom": 8,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 12,
|
||||
|
|
@ -107,9 +72,209 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
|||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<Fade
|
||||
visible={false}
|
||||
<Connect(FileUploadPreview)
|
||||
files={Array []}
|
||||
rootId=""
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 10,
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#3d3c40"
|
||||
name="at"
|
||||
size={20}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 10,
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/icons/slash-forward-box.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"height": 20,
|
||||
"opacity": 1,
|
||||
"tintColor": "#3d3c40",
|
||||
"width": 20,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<FileUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<ImageUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<AttachmentButton
|
||||
blurTextBox={[Function]}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={true}
|
||||
handleSendMessage={[Function]}
|
||||
|
|
@ -143,8 +308,8 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
/>
|
||||
</Fade>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
|
|
|||
207
app/components/post_textbox/components/cameraButton.js
Normal file
207
app/components/post_textbox/components/cameraButton.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
validMimeTypes: [],
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
maxFileCount: 5,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your camera',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_description',
|
||||
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
attachFileFromCamera = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
noData: true,
|
||||
mediaType: 'mixed',
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true,
|
||||
},
|
||||
permissionDenied: {
|
||||
title,
|
||||
text,
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
|
||||
},
|
||||
};
|
||||
|
||||
const hasCameraPermission = await this.hasCameraPermission();
|
||||
|
||||
if (hasCameraPermission) {
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hasCameraPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = 'camera';
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.attachFileFromCamera}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
name='camera-outline'
|
||||
size={20}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
204
app/components/post_textbox/components/fileUploadButton.js
Normal file
204
app/components/post_textbox/components/fileUploadButton.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
NativeModules,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export default class FileUploadButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
validMimeTypes: [],
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
maxFileCount: 5,
|
||||
extraOptions: null,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.storage_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your files',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.storage_permission_denied_description',
|
||||
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
attachFileFromFiles = async () => {
|
||||
const {browseFileTypes} = this.props;
|
||||
const hasPermission = await this.hasStoragePermission();
|
||||
|
||||
if (hasPermission) {
|
||||
try {
|
||||
const res = await DocumentPicker.pick({type: [browseFileTypes]});
|
||||
if (Platform.OS === 'android') {
|
||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||
const newUri = await ShareExtension.getFilePath(res.uri);
|
||||
if (newUri.filePath) {
|
||||
res.uri = newUri.filePath;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode file uri to get the actual path
|
||||
res.uri = decodeURIComponent(res.uri);
|
||||
|
||||
this.uploadFiles([res]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
hasStoragePermission = async () => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check('storage');
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request('storage');
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => AndroidOpenSettings.appDetailsSettings(),
|
||||
},
|
||||
]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
this.props.blurTextBox();
|
||||
this.attachFileFromFiles();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handleButtonPress}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
name='file-document-outline'
|
||||
size={20}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
224
app/components/post_textbox/components/imageUploadButton.js
Normal file
224
app/components/post_textbox/components/imageUploadButton.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
export default class ImageUploadButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
validMimeTypes: [],
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
maxFileCount: 5,
|
||||
extraOptions: null,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
if (Platform.OS === 'android') {
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
attachFileFromLibrary = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
mediaType: 'mixed',
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
title,
|
||||
text,
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
|
||||
},
|
||||
};
|
||||
|
||||
const hasPhotoPermission = await this.hasPhotoPermission();
|
||||
|
||||
if (hasPhotoPermission) {
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hasPhotoPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = 'photo';
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.blurTextBox();
|
||||
this.attachFileFromLibrary();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handleButtonPress}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
name='image-outline'
|
||||
size={20}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
|
||||
import Typing from './components/typing';
|
||||
import PostTextBoxBase from './post_textbox_base';
|
||||
|
|
@ -16,7 +15,6 @@ export default class PostTextBoxAndroid extends PostTextBoxBase {
|
|||
render() {
|
||||
const {
|
||||
deactivatedChannel,
|
||||
files,
|
||||
rootId,
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -29,10 +27,6 @@ export default class PostTextBoxAndroid extends PostTextBoxBase {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Typing/>
|
||||
<FileUploadPreview
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
/>
|
||||
<Autocomplete
|
||||
cursorPosition={cursorPosition}
|
||||
maxHeight={Math.min(top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,28 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert} from 'react-native';
|
||||
import {Alert, Image} from 'react-native';
|
||||
import assert from 'assert';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import Fade from 'app/components/fade';
|
||||
import SendButton from 'app/components/send_button';
|
||||
import PasteableTextInput from 'app/components/pasteable_text_input';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import FileUploadButton from './components/fileUploadButton';
|
||||
import ImageUploadButton from './components/imageUploadButton';
|
||||
import CameraButton from './components/cameraButton';
|
||||
|
||||
import PostTextbox from './post_textbox.ios';
|
||||
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('PostTextBox', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
|
|
@ -270,7 +278,6 @@ describe('PostTextBox', () => {
|
|||
<PostTextbox {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(false);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -284,7 +291,6 @@ describe('PostTextBox', () => {
|
|||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -298,7 +304,6 @@ describe('PostTextBox', () => {
|
|||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -312,7 +317,6 @@ describe('PostTextBox', () => {
|
|||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -328,7 +332,6 @@ describe('PostTextBox', () => {
|
|||
|
||||
wrapper.setState({sendingMessage: true});
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -464,12 +467,58 @@ describe('PostTextBox', () => {
|
|||
expect(baseProps.actions.initUploadFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should change state value on props change', () => {
|
||||
test('should render all quick action icons', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
expect(wrapper.state('value')).toEqual('');
|
||||
wrapper.setProps({value: 'value', channelId: 'channel-id2'});
|
||||
expect(wrapper.state('value')).toEqual('value');
|
||||
|
||||
// @ button
|
||||
expect(wrapper.find(MaterialCommunityIcons).exists()).toBe(true);
|
||||
|
||||
// slash command button
|
||||
expect(wrapper.find(Image).exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find(FileUploadButton).exists()).toBe(true);
|
||||
expect(wrapper.find(ImageUploadButton).exists()).toBe(true);
|
||||
expect(wrapper.find(CameraButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should trigger text change when @ icon is tapped', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.handleTextChange = jest.fn();
|
||||
|
||||
wrapper.find(MaterialCommunityIcons).parent().props().onPress();
|
||||
expect(instance.handleTextChange).toHaveBeenCalledWith(`${instance.state.value}@`, true);
|
||||
});
|
||||
|
||||
test('should disable slash icon if textbox value is NOT empty', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.setState({value: 'Test'});
|
||||
expect(wrapper.find(Image).parent().props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should NOT render file upload icons when server forbids it', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
canUploadFiles: false,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...props}/>);
|
||||
|
||||
expect(wrapper.find(MaterialCommunityIcons).exists()).toBe(true);
|
||||
expect(wrapper.find(Image).exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find(FileUploadButton).exists()).toBe(false);
|
||||
expect(wrapper.find(ImageUploadButton).exists()).toBe(false);
|
||||
expect(wrapper.find(CameraButton).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should change state value on props change', () => {
|
||||
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
|
||||
expect(wrapper.state('value')).toEqual('');
|
||||
wrapper.setProps({value: 'value', channelId: 'channel-id2'});
|
||||
expect(wrapper.state('value')).toEqual('value');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,28 +8,37 @@ import {
|
|||
AppState,
|
||||
BackHandler,
|
||||
findNodeHandle,
|
||||
Image,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import Button from 'react-native-button';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import slashForwardBoxIcon from 'assets/images/icons/slash-forward-box.png';
|
||||
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import AttachmentButton from 'app/components/attachment_button';
|
||||
import Fade from 'app/components/fade';
|
||||
import FileUploadButton from './components/fileUploadButton';
|
||||
import ImageUploadButton from './components/imageUploadButton';
|
||||
import CameraButton from './components/cameraButton';
|
||||
import FormattedMarkdownText from 'app/components/formatted_markdown_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import PasteableTextInput from 'app/components/pasteable_text_input';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import SendButton from 'app/components/send_button';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_CONTENT_HEIGHT, MAX_FILE_COUNT} from 'app/constants/post_textbox';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_FILE_COUNT} from 'app/constants/post_textbox';
|
||||
import {NOTIFY_ALL_MEMBERS} from 'app/constants/view';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
|
||||
|
|
@ -159,6 +168,12 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
focus = () => {
|
||||
if (this.input.current) {
|
||||
this.input.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
numberOfTimezones = async () => {
|
||||
const {data} = await this.props.actions.getChannelTimezones(this.props.channelId);
|
||||
return data?.length || 0;
|
||||
|
|
@ -224,37 +239,109 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
getAttachmentButton = () => {
|
||||
getTextInputButton = (actionType) => {
|
||||
const {channelIsReadOnly, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let button = null;
|
||||
const buttonStyle = [];
|
||||
let iconColor = theme.centerChannelColor;
|
||||
let isDisabled = false;
|
||||
|
||||
if (!channelIsReadOnly) {
|
||||
switch (actionType) {
|
||||
case 'at':
|
||||
isDisabled = this.state.value[this.state.value.length - 1] === '@';
|
||||
if (isDisabled) {
|
||||
iconColor = changeOpacity(theme.centerChannelColor, 0.6);
|
||||
}
|
||||
button = (
|
||||
<TouchableOpacity
|
||||
disabled={isDisabled}
|
||||
onPress={() => {
|
||||
this.handleTextChange(`${this.state.value}@`, true);
|
||||
this.focus();
|
||||
}}
|
||||
style={style.iconWrapper}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={iconColor}
|
||||
name='at'
|
||||
size={20}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
break;
|
||||
case 'slash':
|
||||
isDisabled = this.state.value.length > 0;
|
||||
buttonStyle.push(style.slashIcon);
|
||||
if (isDisabled) {
|
||||
buttonStyle.push(style.iconDisabled);
|
||||
}
|
||||
|
||||
button = (
|
||||
<TouchableOpacity
|
||||
disabled={isDisabled}
|
||||
onPress={() => {
|
||||
this.handleTextChange('/', true);
|
||||
this.focus();
|
||||
}}
|
||||
style={style.iconWrapper}
|
||||
>
|
||||
<Image
|
||||
source={slashForwardBoxIcon}
|
||||
style={buttonStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
getMediaButton = (actionType) => {
|
||||
const {canUploadFiles, channelIsReadOnly, files, maxFileSize, theme} = this.props;
|
||||
let attachmentButton = null;
|
||||
let button = null;
|
||||
const props = {
|
||||
blurTextBox: this.blur,
|
||||
fileCount: files.length,
|
||||
maxFileCount: MAX_FILE_COUNT,
|
||||
onShowFileMaxWarning: this.onShowFileMaxWarning,
|
||||
onShowFileSizeWarning: this.onShowFileSizeWarning,
|
||||
uploadFiles: this.handleUploadFiles,
|
||||
maxFileSize,
|
||||
theme,
|
||||
};
|
||||
|
||||
if (canUploadFiles && !channelIsReadOnly) {
|
||||
attachmentButton = (
|
||||
<AttachmentButton
|
||||
blurTextBox={this.blur}
|
||||
theme={theme}
|
||||
fileCount={files.length}
|
||||
maxFileSize={maxFileSize}
|
||||
maxFileCount={MAX_FILE_COUNT}
|
||||
onShowFileMaxWarning={this.onShowFileMaxWarning}
|
||||
onShowFileSizeWarning={this.onShowFileSizeWarning}
|
||||
uploadFiles={this.handleUploadFiles}
|
||||
/>
|
||||
);
|
||||
switch (actionType) {
|
||||
case 'file':
|
||||
button = (
|
||||
<FileUploadButton {...props}/>
|
||||
);
|
||||
break;
|
||||
case 'image':
|
||||
button = (
|
||||
<ImageUploadButton {...props}/>
|
||||
);
|
||||
break;
|
||||
case 'camera':
|
||||
button = (
|
||||
<CameraButton {...props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return attachmentButton;
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
getInputContainerStyle = () => {
|
||||
const {canUploadFiles, channelIsReadOnly, theme} = this.props;
|
||||
const {channelIsReadOnly, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const inputContainerStyle = [style.inputContainer];
|
||||
|
||||
if (!canUploadFiles) {
|
||||
inputContainerStyle.push(style.inputContainerWithoutFileUpload);
|
||||
}
|
||||
|
||||
if (channelIsReadOnly) {
|
||||
inputContainerStyle.push(style.readonlyContainer);
|
||||
}
|
||||
|
|
@ -737,7 +824,7 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
|
||||
renderTextBox = () => {
|
||||
const {intl} = this.context;
|
||||
const {channelDisplayName, channelIsArchived, channelIsLoading, channelIsReadOnly, theme, isLandscape} = this.props;
|
||||
const {channelDisplayName, channelIsArchived, channelIsLoading, channelIsReadOnly, theme, isLandscape, files, rootId} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (channelIsArchived) {
|
||||
|
|
@ -753,8 +840,17 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
style={[style.inputWrapper, padding(isLandscape)]}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
{this.getAttachmentButton()}
|
||||
<View style={this.getInputContainerStyle()}>
|
||||
<ScrollView
|
||||
style={this.getInputContainerStyle()}
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
pinchGestureEnabled={false}
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
<PasteableTextInput
|
||||
ref={this.input}
|
||||
value={textValue}
|
||||
|
|
@ -773,14 +869,33 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
onPaste={this.handlePasteFiles}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
<Fade visible={this.isSendButtonVisible()}>
|
||||
|
||||
<FileUploadPreview
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
/>
|
||||
|
||||
<View style={style.buttonsContainer}>
|
||||
<View style={style.quickActionsContainer}>
|
||||
|
||||
{this.getTextInputButton('at')}
|
||||
|
||||
{this.getTextInputButton('slash')}
|
||||
|
||||
{this.getMediaButton('file')}
|
||||
|
||||
{this.getMediaButton('image')}
|
||||
|
||||
{this.getMediaButton('camera')}
|
||||
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={!this.isSendButtonEnabled()}
|
||||
handleSendMessage={this.handleSendMessage}
|
||||
theme={theme}
|
||||
/>
|
||||
</Fade>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -788,34 +903,48 @@ export default class PostTextBoxBase extends PureComponent {
|
|||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
buttonsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
slashIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
opacity: 1,
|
||||
tintColor: theme.centerChannelColor,
|
||||
},
|
||||
iconDisabled: {
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
iconWrapper: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
quickActionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
maxHeight: MAX_CONTENT_HEIGHT,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
paddingTop: 8,
|
||||
},
|
||||
hidden: {
|
||||
position: 'absolute',
|
||||
top: 10000, // way off screen
|
||||
left: 10000, // way off screen
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
color: 'transparent',
|
||||
maxHeight: 150,
|
||||
},
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
alignItems: 'stretch',
|
||||
marginRight: 10,
|
||||
},
|
||||
inputContainerWithoutFileUpload: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
inputContentContainer: {
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
inputWrapper: {
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import ProfilePictureButton from './profile_picture_button.js';
|
|||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('profile_picture_button', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
},
|
||||
sendButton: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderRadius: 18,
|
||||
borderRadius: 4,
|
||||
height: 28,
|
||||
width: 28,
|
||||
width: 72,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 3,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MAX_CONTENT_HEIGHT = 100;
|
||||
export const MAX_FILE_COUNT = 5;
|
||||
export const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
export const INSERT_TO_DRAFT = 'insert_to_draft';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
|||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
import NetworkIndicator from 'app/components/network_indicator';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
|
|
@ -57,7 +56,6 @@ export default class ChannelIOS extends ChannelBase {
|
|||
updateNativeScrollView={this.updateNativeScrollView}
|
||||
/>
|
||||
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
|
||||
<FileUploadPreview/>
|
||||
<Autocomplete
|
||||
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
|
||||
onChangeText={this.handleAutoComplete}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ jest.mock('app/utils/theme', () => {
|
|||
changeOpacity: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('edit_profile', () => {
|
||||
const actions = {
|
||||
|
|
|
|||
|
|
@ -42,9 +42,6 @@ exports[`thread should match snapshot, has root post 1`] = `
|
|||
<View
|
||||
nativeID="threadAccessoriesContainer"
|
||||
>
|
||||
<Connect(FileUploadPreview)
|
||||
rootId="root_id"
|
||||
/>
|
||||
<ForwardRef(forwardConnectRef)
|
||||
cursorPositionEvent="onThreadTextBoxCursorChange"
|
||||
maxHeight={200}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
|||
import {getLastPostIndex} from 'mattermost-redux/utils/post_list';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
|
|
@ -52,9 +51,6 @@ export default class ThreadIOS extends ThreadBase {
|
|||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
/>
|
||||
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
|
||||
<FileUploadPreview
|
||||
rootId={rootId}
|
||||
/>
|
||||
<Autocomplete
|
||||
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
|
||||
onChangeText={this.handleAutoComplete}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import * as NavigationActions from 'app/actions/navigation';
|
|||
import ThreadIOS from './thread.ios';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('thread', () => {
|
||||
const baseProps = {
|
||||
|
|
|
|||
BIN
assets/base/images/icons/slash-forward-box.png
Normal file
BIN
assets/base/images/icons/slash-forward-box.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 894 B |
|
|
@ -206,16 +206,12 @@ PODS:
|
|||
- React
|
||||
- react-native-document-picker (3.2.4):
|
||||
- React
|
||||
- react-native-image-picker (0.28.1):
|
||||
- React
|
||||
- react-native-local-auth (1.5.0):
|
||||
- react-native-image-picker (2.0.0):
|
||||
- React
|
||||
- react-native-netinfo (4.4.0):
|
||||
- React
|
||||
- react-native-notifications (2.0.6):
|
||||
- React
|
||||
- react-native-passcode-status (1.1.2):
|
||||
- React
|
||||
- react-native-safe-area (0.5.1):
|
||||
- React
|
||||
- react-native-video (5.0.2):
|
||||
|
|
@ -330,10 +326,8 @@ DEPENDENCIES:
|
|||
- react-native-cookies (from `../node_modules/react-native-cookies/ios`)
|
||||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
- react-native-local-auth (from `../node_modules/react-native-local-auth`)
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-notifications (from `../node_modules/react-native-notifications`)
|
||||
- react-native-passcode-status (from `../node_modules/react-native-passcode-status`)
|
||||
- react-native-safe-area (from `../node_modules/react-native-safe-area`)
|
||||
- react-native-video (from `../node_modules/react-native-video`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
|
|
@ -422,14 +416,10 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-document-picker"
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
react-native-local-auth:
|
||||
:path: "../node_modules/react-native-local-auth"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-notifications:
|
||||
:path: "../node_modules/react-native-notifications"
|
||||
react-native-passcode-status:
|
||||
:path: "../node_modules/react-native-passcode-status"
|
||||
react-native-safe-area:
|
||||
:path: "../node_modules/react-native-safe-area"
|
||||
react-native-video:
|
||||
|
|
@ -511,11 +501,9 @@ SPEC CHECKSUMS:
|
|||
react-native-cameraroll: ad20f5a93c25cb83a76455df57a2c62fbb63aaed
|
||||
react-native-cookies: 854d59c4135c70b92a02ca4930e68e4e2eb58150
|
||||
react-native-document-picker: c36bf5f067a581657ecaf7124dcd921a8be19061
|
||||
react-native-image-picker: fd93361c666f397bdf72f9c6c23f13d2685b9173
|
||||
react-native-local-auth: 5081a70211643de74bb207e007401a0c81b37a20
|
||||
react-native-image-picker: ba7fe85b3373ff33d4827210d989dfcbbd68f7f9
|
||||
react-native-netinfo: 892a5130be97ff8bb69c523739c424a2ffc296d1
|
||||
react-native-notifications: d5cb54ef8bf3004dcb56c887650dea08ecbddee7
|
||||
react-native-passcode-status: 88c4f6e074328bc278bd127646b6c694ad5a530a
|
||||
react-native-safe-area: e8230b0017d76c00de6b01e2412dcf86b127c6a3
|
||||
react-native-video: 961749da457e73bf0b5565edfbaffc25abfb8974
|
||||
react-native-webview: 0d1c2b4e7ffb0543a74fa0512f2f8dc5fb0e49e2
|
||||
|
|
|
|||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -9937,9 +9937,9 @@
|
|||
}
|
||||
},
|
||||
"react-native-image-picker": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-0.28.1.tgz",
|
||||
"integrity": "sha512-CW2dm+cjsdW2fjBW2WD/cSufNG0x0UpljwGHrjSzyB0TckoW+tjYv44UWtckCWxr1JtCg+QrYDO/MzlRyFcjwQ=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.0.0.tgz",
|
||||
"integrity": "sha512-svmQJ5bBuCgX9RG5DU3A38udgemze1/HtYUXgUAvP2i3LzrzW5ub5GT3L8n87pg/DNj2rRHi1wLtNaNEoV4ZAw=="
|
||||
},
|
||||
"react-native-iphone-x-helper": {
|
||||
"version": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
"react-native-fast-image": "7.0.2",
|
||||
"react-native-haptic-feedback": "1.8.2",
|
||||
"react-native-image-gallery": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
|
||||
"react-native-image-picker": "0.28.1",
|
||||
"react-native-image-picker": "2.0.0",
|
||||
"react-native-keyboard-aware-scroll-view": "0.9.1",
|
||||
"react-native-keyboard-tracking-view": "github:mattermost/react-native-keyboard-tracking-view#169676811a32f82cb674a52d7857a14043f7b8f7",
|
||||
"react-native-keychain": "4.0.1",
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
index b4311eb..9d3f3d2 100644
|
||||
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
@@ -47,6 +47,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
+import java.util.ArrayList;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
import com.facebook.react.modules.core.PermissionAwareActivity;
|
||||
@@ -542,14 +543,23 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
@NonNull final Callback callback,
|
||||
@NonNull final int requestCode)
|
||||
{
|
||||
- final int writePermission = ActivityCompat
|
||||
- .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
- final int cameraPermission = ActivityCompat
|
||||
+ int selfCheckResult = 0;
|
||||
+ switch (requestCode) {
|
||||
+ case REQUEST_PERMISSIONS_FOR_CAMERA:
|
||||
+ selfCheckResult = ActivityCompat
|
||||
.checkSelfPermission(activity, Manifest.permission.CAMERA);
|
||||
+ if (selfCheckResult == PackageManager.PERMISSION_GRANTED) {
|
||||
+ selfCheckResult = ActivityCompat
|
||||
+ .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ }
|
||||
+ break;
|
||||
+ case REQUEST_PERMISSIONS_FOR_LIBRARY:
|
||||
+ selfCheckResult = ActivityCompat
|
||||
+ .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ break;
|
||||
+ }
|
||||
|
||||
- final boolean permissionsGrated = writePermission == PackageManager.PERMISSION_GRANTED &&
|
||||
- cameraPermission == PackageManager.PERMISSION_GRANTED;
|
||||
-
|
||||
+ final boolean permissionsGrated = selfCheckResult == PackageManager.PERMISSION_GRANTED;
|
||||
if (!permissionsGrated)
|
||||
{
|
||||
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
|
||||
@@ -598,7 +608,18 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
}
|
||||
else
|
||||
{
|
||||
- String[] PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
|
||||
+ List<String> permissions = new ArrayList<String>();
|
||||
+ if (requestCode == REQUEST_PERMISSIONS_FOR_CAMERA )
|
||||
+ {
|
||||
+ permissions.add(Manifest.permission.CAMERA);
|
||||
+ permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ }
|
||||
+ if (requestCode == REQUEST_PERMISSIONS_FOR_LIBRARY )
|
||||
+ {
|
||||
+ permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ }
|
||||
+
|
||||
+ String[] PERMISSIONS = permissions.toArray(new String[0]);
|
||||
if (activity instanceof ReactActivity)
|
||||
{
|
||||
((ReactActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
|
||||
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
index 28d5870..3f70983 100644
|
||||
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
@@ -455,12 +455,19 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking
|
||||
}
|
||||
|
||||
if (videoURL) { // Protect against reported crash
|
||||
- NSError *error = nil;
|
||||
- [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
- if (error) {
|
||||
- self.callback(@[@{@"error": error.localizedFailureReason}]);
|
||||
- return;
|
||||
- }
|
||||
+ NSError *error = nil;
|
||||
+
|
||||
+ // If we have write access to the source file, move it. Otherwise use copy.
|
||||
+ if ([fileManager isWritableFileAtPath:[videoURL path]]) {
|
||||
+ [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
+ } else {
|
||||
+ [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
+ }
|
||||
+
|
||||
+ if (error) {
|
||||
+ self.callback(@[@{@"error": error.localizedFailureReason}]);
|
||||
+ return;
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
184
patches/react-native-image-picker+2.0.0.patch
Normal file
184
patches/react-native-image-picker+2.0.0.patch
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
index ef62bed..7379605 100644
|
||||
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
@@ -49,6 +49,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
+import java.util.ArrayList;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
import com.facebook.react.modules.core.PermissionAwareActivity;
|
||||
@@ -69,6 +70,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
public static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002;
|
||||
public static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003;
|
||||
public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004;
|
||||
+ public static final int REQUEST_LAUNCH_MIXED_CAPTURE = 13005;
|
||||
public static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001;
|
||||
public static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002;
|
||||
|
||||
@@ -266,26 +268,23 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, videoDurationLimit);
|
||||
}
|
||||
}
|
||||
+ else if (pickBoth) {
|
||||
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
+ this.setImageCaptureUri(takePictureIntent);
|
||||
+ Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
+ cameraIntent = new Intent(Intent.ACTION_CHOOSER);
|
||||
+ Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
+ Intent[] intentArray = new Intent[]{takePictureIntent,takeVideoIntent};
|
||||
+ cameraIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
|
||||
+ cameraIntent.putExtra(Intent.EXTRA_TITLE, "Choose an action");
|
||||
+ cameraIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
|
||||
+ requestCode = REQUEST_LAUNCH_MIXED_CAPTURE;
|
||||
+ }
|
||||
else
|
||||
{
|
||||
requestCode = REQUEST_LAUNCH_IMAGE_CAPTURE;
|
||||
cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
-
|
||||
- final File original = createNewFile(reactContext, this.options, false);
|
||||
- imageConfig = imageConfig.withOriginalFile(original);
|
||||
-
|
||||
- if (imageConfig.original != null) {
|
||||
- cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
|
||||
- }else {
|
||||
- responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
- return;
|
||||
- }
|
||||
- if (cameraCaptureURI == null)
|
||||
- {
|
||||
- responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
- return;
|
||||
- }
|
||||
- cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI);
|
||||
+ this.setImageCaptureUri(cameraIntent);
|
||||
}
|
||||
|
||||
if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null)
|
||||
@@ -444,14 +443,20 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
callback = null;
|
||||
return;
|
||||
|
||||
+ case REQUEST_LAUNCH_MIXED_CAPTURE:
|
||||
case REQUEST_LAUNCH_VIDEO_CAPTURE:
|
||||
- final String path = getRealPathFromURI(data.getData());
|
||||
- responseHelper.putString("uri", data.getData().toString());
|
||||
- responseHelper.putString("path", path);
|
||||
- fileScan(reactContext, path);
|
||||
- responseHelper.invokeResponse(callback);
|
||||
- callback = null;
|
||||
- return;
|
||||
+ if (data == null || data.getData() == null) {
|
||||
+ uri = cameraCaptureURI;
|
||||
+ break;
|
||||
+ } else {
|
||||
+ final String path = getRealPathFromURI(data.getData());
|
||||
+ responseHelper.putString("uri", data.getData().toString());
|
||||
+ responseHelper.putString("path", path);
|
||||
+ fileScan(reactContext, path);
|
||||
+ responseHelper.invokeResponse(callback);
|
||||
+ callback = null;
|
||||
+ return;
|
||||
+ }
|
||||
}
|
||||
|
||||
final ReadExifResult result = readExifInterface(responseHelper, imageConfig);
|
||||
@@ -551,7 +556,8 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
{
|
||||
return callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|
||||
|| (requestCode != REQUEST_LAUNCH_IMAGE_CAPTURE && requestCode != REQUEST_LAUNCH_IMAGE_LIBRARY
|
||||
- && requestCode != REQUEST_LAUNCH_VIDEO_LIBRARY && requestCode != REQUEST_LAUNCH_VIDEO_CAPTURE);
|
||||
+ && requestCode != REQUEST_LAUNCH_VIDEO_LIBRARY && requestCode != REQUEST_LAUNCH_VIDEO_CAPTURE
|
||||
+ && requestCode != REQUEST_LAUNCH_MIXED_CAPTURE);
|
||||
}
|
||||
|
||||
private void updatedResultResponse(@Nullable final Uri uri,
|
||||
@@ -571,22 +577,23 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
@NonNull final Callback callback,
|
||||
@NonNull final int requestCode)
|
||||
{
|
||||
- final int writePermission = ActivityCompat
|
||||
- .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
- final int cameraPermission = ActivityCompat
|
||||
- .checkSelfPermission(activity, Manifest.permission.CAMERA);
|
||||
-
|
||||
- boolean permissionsGranted = false;
|
||||
-
|
||||
+ int selfCheckResult = 0;
|
||||
switch (requestCode) {
|
||||
case REQUEST_PERMISSIONS_FOR_LIBRARY:
|
||||
- permissionsGranted = writePermission == PackageManager.PERMISSION_GRANTED;
|
||||
- break;
|
||||
+ selfCheckResult = ActivityCompat
|
||||
+ .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ break;
|
||||
case REQUEST_PERMISSIONS_FOR_CAMERA:
|
||||
- permissionsGranted = cameraPermission == PackageManager.PERMISSION_GRANTED;
|
||||
+ selfCheckResult = ActivityCompat
|
||||
+ .checkSelfPermission(activity, Manifest.permission.CAMERA);
|
||||
+ if (selfCheckResult == PackageManager.PERMISSION_GRANTED) {
|
||||
+ selfCheckResult = ActivityCompat
|
||||
+ .checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
+ }
|
||||
break;
|
||||
}
|
||||
|
||||
+ final boolean permissionsGranted = selfCheckResult == PackageManager.PERMISSION_GRANTED;
|
||||
if (!permissionsGranted)
|
||||
{
|
||||
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
|
||||
@@ -641,7 +648,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
PERMISSIONS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
|
||||
break;
|
||||
case REQUEST_PERMISSIONS_FOR_CAMERA:
|
||||
- PERMISSIONS = new String[]{Manifest.permission.CAMERA};
|
||||
+ PERMISSIONS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
|
||||
break;
|
||||
default:
|
||||
PERMISSIONS = new String[]{};
|
||||
@@ -781,4 +788,22 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
videoDurationLimit = options.getInt("durationLimit");
|
||||
}
|
||||
}
|
||||
+
|
||||
+ private void setImageCaptureUri(Intent cameraIntent) {
|
||||
+ final File original = createNewFile(reactContext, this.options, false);
|
||||
+ imageConfig = imageConfig.withOriginalFile(original);
|
||||
+
|
||||
+ if (imageConfig.original != null) {
|
||||
+ cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
|
||||
+ }else {
|
||||
+ responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
+ return;
|
||||
+ }
|
||||
+ if (cameraCaptureURI == null)
|
||||
+ {
|
||||
+ responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
+ return;
|
||||
+ }
|
||||
+ cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI);
|
||||
+ }
|
||||
}
|
||||
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
index 46b2c11..70bb8a5 100644
|
||||
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
@@ -460,7 +460,14 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking
|
||||
|
||||
if (videoURL) { // Protect against reported crash
|
||||
NSError *error = nil;
|
||||
- [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
+
|
||||
+ // If we have write access to the source file, move it. Otherwise use copy.
|
||||
+ if ([fileManager isWritableFileAtPath:[videoURL path]]) {
|
||||
+ [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
+ } else {
|
||||
+ [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
||||
+ }
|
||||
+
|
||||
if (error) {
|
||||
self.callback(@[@{@"error": error.localizedFailureReason}]);
|
||||
return;
|
||||
Loading…
Reference in a new issue