diff --git a/NOTICE.txt b/NOTICE.txt
index ff4adacd4..693cda9bd 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -867,3 +867,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
+
+## react-native-image-crop-picker
+
+This product contains 'react-native-image-crop-picker', a library to provide access to the camera and photo library on both iOS and Android
+* HOMEPAGE
+ * https://github.com/ivpusic/react-native-image-crop-picker/
+
+* LICENSE
+
+MIT License
+
+Copyright (c) 2017 Ivan Pusic
+
+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.
+
+---
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2517dc935..b8d2ad582 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -97,6 +97,7 @@ android {
ndk {
abiFilters "armeabi-v7a", "x86"
}
+
}
signingConfigs {
release {
@@ -151,6 +152,7 @@ dependencies {
compile project(':react-native-device-info')
compile project(':react-native-cookies')
compile project(':react-native-linear-gradient')
+ compile project(':react-native-image-crop-picker')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"])
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b937917a1..1cca6da30 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
+
{
actions.push({
- type: ViewTypes.POST_DRAFT_CHANGED,
+ type: ViewTypes.SET_POST_DRAFT,
channelId: d,
- postDraft: otherStorage.postDrafts[d]
+ postDraft: otherStorage.postDrafts[d].draft,
+ files: otherStorage.postDrafts[d].files
});
});
}
@@ -51,9 +52,10 @@ export function loadStorage() {
if (otherStorage.threadDrafts) {
Object.keys(otherStorage.threadDrafts).forEach((d) => {
actions.push({
- type: ViewTypes.COMMENT_DRAFT_CHANGED,
+ type: ViewTypes.SET_COMMENT_DRAFT,
rootId: d,
- draft: otherStorage.threadDrafts[d]
+ draft: otherStorage.threadDrafts[d].draft,
+ files: otherStorage.threadDrafts[d].files
});
});
}
@@ -81,7 +83,7 @@ export function flushToStorage() {
// Can add other important items here.
const postDrafts = state.views.channel.drafts;
- const threadDrafts = state.views.thread.draft;
+ const threadDrafts = state.views.thread.drafts;
await updateStorage(null, {postDrafts, threadDrafts});
};
diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js
new file mode 100644
index 000000000..404c4e6e7
--- /dev/null
+++ b/app/actions/views/file_upload.js
@@ -0,0 +1,60 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import FormData from 'form-data';
+import {Platform} from 'react-native';
+import {uploadFile} from 'mattermost-redux/actions/files';
+import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
+
+import {generateId} from 'app/utils/file';
+import {ViewTypes} from 'app/constants';
+
+export function handleUploadFiles(files, rootId, requestId) {
+ return async (dispatch, getState) => {
+ const state = getState();
+
+ const teamId = state.entities.teams.currentTeamId;
+ const channelId = state.entities.channels.currentChannelId;
+ const formData = new FormData();
+
+ files.forEach((file) => {
+ const name = file.path.split('/').pop();
+ const mimeType = lookupMimeType(name);
+ const clientId = generateId();
+
+ const fileData = {
+ uri: file.path,
+ name,
+ type: mimeType
+ };
+
+ formData.append('files', fileData);
+ formData.append('channel_id', channelId);
+ formData.append('client_ids', clientId);
+ });
+
+ let formBoundary;
+ if (Platform.os === 'ios') {
+ formBoundary = '--mobile.client.file.upload';
+ }
+
+ await uploadFile(teamId, channelId, formData, formBoundary, rootId, requestId)(dispatch, getState);
+ };
+}
+
+export function handleClearFiles(channelId, rootId) {
+ return {
+ type: ViewTypes.CLEAR_FILES_FOR_POST_DRAFT,
+ channelId,
+ rootId
+ };
+}
+
+export function handleRemoveFile(fileId, channelId, rootId) {
+ return {
+ type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,
+ fileId,
+ channelId,
+ rootId
+ };
+}
diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js
index 341e820a5..bfafd7b39 100644
--- a/app/components/autocomplete/at_mention/index.js
+++ b/app/components/autocomplete/at_mention/index.js
@@ -16,9 +16,9 @@ function mapStateToProps(state, ownProps) {
let postDraft;
if (ownProps.rootId.length) {
- postDraft = state.views.thread.draft[ownProps.rootId];
+ postDraft = state.views.thread.drafts[ownProps.rootId].draft;
} else {
- postDraft = state.views.channel.drafts[currentChannelId];
+ postDraft = state.views.channel.drafts[currentChannelId].draft;
}
return {
diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js
index a16055dd9..774a63ffd 100644
--- a/app/components/autocomplete/channel_mention/index.js
+++ b/app/components/autocomplete/channel_mention/index.js
@@ -15,9 +15,9 @@ function mapStateToProps(state, ownProps) {
let postDraft;
if (ownProps.rootId.length) {
- postDraft = state.views.thread.draft[ownProps.rootId];
+ postDraft = state.views.thread.drafts[ownProps.rootId].draft;
} else {
- postDraft = state.views.channel.drafts[currentChannelId];
+ postDraft = state.views.channel.drafts[currentChannelId].draft;
}
return {
diff --git a/app/components/autocomplete/index.js b/app/components/autocomplete/index.js
index b94dcfcee..2310fa387 100644
--- a/app/components/autocomplete/index.js
+++ b/app/components/autocomplete/index.js
@@ -15,7 +15,7 @@ const style = StyleSheet.create({
position: 'absolute',
bottom: 0,
left: 0,
- right: 43,
+ right: 0,
maxHeight: 200,
overflow: 'hidden'
}
diff --git a/app/components/file_attachment_list/file_attachment_list_container.js b/app/components/file_attachment_list/file_attachment_list_container.js
index 146e8b676..176424d7b 100644
--- a/app/components/file_attachment_list/file_attachment_list_container.js
+++ b/app/components/file_attachment_list/file_attachment_list_container.js
@@ -16,7 +16,7 @@ function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
return {
...ownProps,
- files: getFilesForPost(state, ownProps),
+ files: getFilesForPost(state, ownProps.post),
theme: getTheme(state)
};
};
diff --git a/app/components/file_attachment_list/file_attachment_preview.js b/app/components/file_attachment_list/file_attachment_preview.js
index 7c2f80115..d5d9517ca 100644
--- a/app/components/file_attachment_list/file_attachment_preview.js
+++ b/app/components/file_attachment_list/file_attachment_preview.js
@@ -35,6 +35,22 @@ export default class FileAttachmentPreview extends PureComponent {
wrapperWidth: 100
};
+ state = {
+ retry: 0
+ }
+
+ // Sometimes the request after a file upload errors out.
+ // We'll up to three times to get the image.
+ // We have to add a timestamp so fetch will retry the call.
+ handleError = () => {
+ if (this.state.retry < 3) {
+ setTimeout(() => this.setState({
+ retry: this.state.retry++,
+ timestamp: Date.now()
+ }), 100);
+ }
+ }
+
render() {
const {
file,
@@ -46,16 +62,17 @@ export default class FileAttachmentPreview extends PureComponent {
wrapperWidth
} = this.props;
- const source = {uri: Client.getFilePreviewUrl(file.id)};
+ const source = {uri: Client.getFilePreviewUrl(file.id, this.state.timestamp)};
return (
);
diff --git a/app/components/file_upload_preview/file_upload_preview.js b/app/components/file_upload_preview/file_upload_preview.js
new file mode 100644
index 000000000..fe09207b3
--- /dev/null
+++ b/app/components/file_upload_preview/file_upload_preview.js
@@ -0,0 +1,156 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {
+ Dimensions,
+ KeyboardAvoidingView,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+import Font from 'react-native-vector-icons/FontAwesome';
+import {RequestStatus} from 'mattermost-redux/constants';
+
+import FileAttachmentPreview from 'app/components/file_attachment_list/file_attachment_preview';
+
+const {height: deviceHeight} = Dimensions.get('window');
+
+export default class FileUploadPreview extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ cancelUploadFileRequest: PropTypes.func.isRequired,
+ handleClearFiles: PropTypes.func.isRequired,
+ handleRemoveFile: PropTypes.func.isRequired
+ }).isRequired,
+ channelId: PropTypes.string.isRequired,
+ createPostRequestStatus: PropTypes.string.isRequired,
+ files: PropTypes.array.isRequired,
+ rootId: PropTypes.string,
+ uploadFileRequestStatus: PropTypes.string.isRequired,
+ uploadFileRequestId: PropTypes.number
+ };
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.createPostRequestStatus === RequestStatus.STARTED && nextProps.createPostRequestStatus === RequestStatus.SUCCESS) {
+ this.props.actions.handleClearFiles(this.props.channelId, this.props.rootId);
+ }
+ }
+
+ buildFilePreviews = () => {
+ return this.props.files.map((file) => {
+ return (
+
+
+ this.props.actions.handleRemoveFile(file.id, this.props.channelId, this.props.rootId)}
+ >
+
+
+
+ );
+ });
+ }
+
+ handleCancelUpload = () => {
+ this.props.actions.cancelUploadFileRequest(this.props.uploadFileRequestId);
+ }
+
+ render() {
+ if (!this.props.files.length && this.props.uploadFileRequestStatus !== RequestStatus.STARTED) {
+ return null;
+ }
+
+ let component;
+ if (this.props.uploadFileRequestStatus === RequestStatus.STARTED) {
+ component = (
+
+ {'Loading...'}
+
+ {'Cancel'}
+
+
+ );
+ } else {
+ component = (
+
+ {this.buildFilePreviews()}
+
+ );
+ }
+
+ return (
+
+ {component}
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ cancelText: {
+ padding: 15,
+ color: '#fff',
+ fontSize: 18
+ },
+ container: {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ bottom: 40,
+ height: deviceHeight,
+ left: 0,
+ position: 'absolute',
+ width: '100%'
+ },
+ loader: {
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
+ height: 124,
+ width: '100%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ bottom: 0,
+ position: 'absolute'
+ },
+ loaderText: {
+ fontSize: 24,
+ color: '#fff'
+ },
+ preview: {
+ alignItems: 'center',
+ height: 115,
+ justifyContent: 'center',
+ marginLeft: 24,
+ width: 115
+ },
+ removeButton: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ height: 20,
+ width: 20,
+ borderRadius: 10,
+ backgroundColor: '#000'
+ },
+ scrollView: {
+ flex: 1,
+ marginBottom: 24
+ },
+ scrollViewContent: {
+ alignItems: 'flex-end'
+ }
+});
diff --git a/app/components/file_upload_preview/index.js b/app/components/file_upload_preview/index.js
new file mode 100644
index 000000000..210077d23
--- /dev/null
+++ b/app/components/file_upload_preview/index.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+import {cancelUploadFileRequest} from 'mattermost-redux/actions/files';
+
+import {handleClearFiles, handleRemoveFile} from 'app/actions/views/file_upload';
+
+import FileUploadPreview from './file_upload_preview';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ createPostRequestStatus: state.requests.posts.createPost.status,
+ uploadFileRequestStatus: state.requests.files.uploadFiles.status
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ cancelUploadFileRequest,
+ handleClearFiles,
+ handleRemoveFile
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FileUploadPreview);
diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js
index b175bd309..f879880eb 100644
--- a/app/components/post_textbox/post_textbox.js
+++ b/app/components/post_textbox/post_textbox.js
@@ -3,18 +3,20 @@
import React, {PropTypes, PureComponent} from 'react';
import {
- Platform,
+ StyleSheet,
TouchableHighlight,
- View, Text
+ View,
+ Text
} from 'react-native';
-import Icon from 'react-native-vector-icons/FontAwesome';
+import Icon from 'react-native-vector-icons/Ionicons';
+import ImagePicker from 'react-native-image-crop-picker';
+import {RequestStatus} from 'mattermost-redux/constants';
import Autocomplete from 'app/components/autocomplete';
+import FileUploadPreview from 'app/components/file_upload_preview';
import FormattedText from 'app/components/formatted_text';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
-import {changeOpacity} from 'app/utils/theme';
-
-// import PaperClipIcon from './components/paper_clip_icon.js';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_CONTENT_HEIGHT = 100;
@@ -24,12 +26,17 @@ export default class PostTextbox extends PureComponent {
typing: PropTypes.array.isRequired,
teamId: PropTypes.string.isRequired,
channelId: PropTypes.string.isRequired,
+ files: PropTypes.array,
rootId: PropTypes.string,
value: PropTypes.string.isRequired,
onChangeText: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
+ uploadFileRequestStatus: PropTypes.string.isRequired,
actions: PropTypes.shape({
+ closeModal: PropTypes.func.isRequired,
createPost: PropTypes.func.isRequired,
+ handleUploadFiles: PropTypes.func.isRequired,
+ showOptionsModal: PropTypes.func.isRequired,
userTyping: PropTypes.func.isRequired
}).isRequired
};
@@ -58,7 +65,7 @@ export default class PostTextbox extends PureComponent {
};
sendMessage = () => {
- if (this.props.value.trim().length === 0) {
+ if ((this.props.value.trim().length === 0 && this.props.files.length === 0) || this.props.uploadFileRequestStatus === RequestStatus.STARTED) {
return;
}
@@ -70,7 +77,7 @@ export default class PostTextbox extends PureComponent {
message: this.props.value
};
- this.props.actions.createPost(this.props.teamId, post);
+ this.props.actions.createPost(this.props.teamId, post, this.props.files);
this.handleTextChange('');
};
@@ -96,6 +103,66 @@ export default class PostTextbox extends PureComponent {
this.autocomplete = c;
};
+ attachFileFromCamera = async () => {
+ try {
+ this.props.actions.closeModal();
+ const image = await ImagePicker.openCamera({
+ compressImageQuality: 0.5
+ });
+
+ this.uploadFiles([image]);
+ } catch (error) {
+ // If user cancels it's considered
+ // an error and we have to catch it.
+ }
+ }
+
+ attachFileFromLibrary = async () => {
+ try {
+ this.props.actions.closeModal();
+ const images = await ImagePicker.openPicker({
+ multiple: true,
+ compressImageQuality: 0.5
+ });
+
+ this.uploadFiles(images);
+ } catch (error) {
+ // If user cancels it's considered
+ // an error and we have to catch it.
+ }
+ }
+
+ uploadFiles = (images) => {
+ const uploadFileRequestId = Date.now();
+ this.setState({
+ uploadFileRequestId
+ });
+ this.props.actions.handleUploadFiles(images, this.props.rootId, uploadFileRequestId);
+ }
+
+ showFileAttachmentOptions = () => {
+ this.blur();
+ const options = {
+ items: [{
+ action: this.attachFileFromCamera,
+ text: {
+ id: 'mobile.file_upload.camera',
+ defaultMessage: 'Take Photo or Video'
+ },
+ icon: 'camera'
+ }, {
+ action: this.attachFileFromLibrary,
+ text: {
+ id: 'mobile.file_upload.library',
+ defaultMessage: 'Photo Library'
+ },
+ icon: 'photo'
+ }]
+ };
+
+ this.props.actions.showOptionsModal(options);
+ }
+
renderTyping = () => {
const {typing} = this.props;
const numUsers = typing.length;
@@ -132,6 +199,8 @@ export default class PostTextbox extends PureComponent {
render() {
const {theme} = this.props;
+ const style = getStyleSheet(theme);
+
let placeholder;
if (this.props.rootId) {
placeholder = {id: 'create_comment.addComment', defaultMessage: 'Add a comment...'};
@@ -140,92 +209,107 @@ export default class PostTextbox extends PureComponent {
}
return (
-
+
{this.renderTyping()}
+
- {/*
-
-
- */}
-
-
-
+
+
+
+
+
+
);
}
}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ buttonContainer: {
+ height: 36,
+ width: 36,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ input: {
+ color: '#000',
+ flex: 1,
+ fontSize: 14,
+ paddingBottom: 8,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingTop: 6
+ },
+ inputContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ backgroundColor: '#fff',
+ alignItems: 'flex-end'
+ },
+ inputWrapper: {
+ alignItems: 'flex-end',
+ flexDirection: 'row',
+ padding: 4,
+ backgroundColor: '#000'
+ },
+ typing: {
+ paddingLeft: 10,
+ fontSize: 11,
+ marginBottom: 5,
+ color: theme.centerChannelColor,
+ backgroundColor: theme.centerChannelBg
+ }
+ });
+});
diff --git a/app/components/post_textbox/post_textbox_container.js b/app/components/post_textbox/post_textbox_container.js
index 2b8805ceb..18a2f1e55 100644
--- a/app/components/post_textbox/post_textbox_container.js
+++ b/app/components/post_textbox/post_textbox_container.js
@@ -3,9 +3,11 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-
import {createPost} from 'mattermost-redux/actions/posts';
import {userTyping} from 'mattermost-redux/actions/websocket';
+
+import {showOptionsModal, requestCloseModal} from 'app/actions/navigation';
+import {handleUploadFiles} from 'app/actions/views/file_upload';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
@@ -17,7 +19,8 @@ function mapStateToProps(state, ownProps) {
...ownProps,
currentUserId: getCurrentUserId(state),
typing: getUsersTyping(state),
- theme: getTheme(state)
+ theme: getTheme(state),
+ uploadFileRequestStatus: state.requests.files.uploadFiles.status
};
}
@@ -25,6 +28,9 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
createPost,
+ closeModal: requestCloseModal,
+ handleUploadFiles,
+ showOptionsModal,
userTyping
}, dispatch)
};
diff --git a/app/constants/view.js b/app/constants/view.js
index 4fab923ad..515acd113 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -14,7 +14,14 @@ const ViewTypes = keyMirror({
OPTIONS_MODAL_CHANGED: null,
- NOTIFICATION_CHANGED: null
+ NOTIFICATION_CHANGED: null,
+
+ SET_POST_DRAFT: null,
+ SET_COMMENT_DRAFT: null,
+
+ CLEAR_FILES_FOR_POST_DRAFT: null,
+
+ REMOVE_FILE_FROM_POST_DRAFT: null
});
export default ViewTypes;
diff --git a/app/initial_state.js b/app/initial_state.js
index 1a86cc2e4..0c083128a 100644
--- a/app/initial_state.js
+++ b/app/initial_state.js
@@ -293,6 +293,9 @@ const state = {
},
selectServer: {
serverUrl: Config.DefaultServerUrl
+ },
+ thread: {
+ drafts: {}
}
}
};
diff --git a/app/navigation/router.js b/app/navigation/router.js
index 0b57bd3d9..d408d2911 100644
--- a/app/navigation/router.js
+++ b/app/navigation/router.js
@@ -123,7 +123,7 @@ class Router extends React.Component {
if (navigationProps.allowSceneSwipe) {
panHandlers = navigationPanResponder.forHorizontal({
...cardProps,
- gestureResponseDistance: (this.state.deviceWidth / 2), // sets the distance from the edge for swiping
+ gestureResponseDistance: (this.state.deviceWidth / 4), // sets the distance from the edge for swiping
onNavigateBack: this.props.actions.goBack
});
}
@@ -254,7 +254,7 @@ class Router extends React.Component {
tapToClose={true}
openDrawerOffset={42}
onRequestClose={this.props.actions.closeDrawers}
- panOpenMask={0.3}
+ panOpenMask={0.2}
panCloseMask={42}
panThreshold={0.2}
acceptPan={navigationProps.allowMenuSwipe}
@@ -271,7 +271,7 @@ class Router extends React.Component {
tapToClose={true}
openDrawerOffset={50}
onRequestClose={this.props.actions.closeDrawers}
- panOpenMask={0.3}
+ panOpenMask={0.2}
panCloseMask={50}
panThreshold={0.2}
acceptPan={navigationProps.allowMenuSwipe}
diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js
index 4f29ba1c8..a2c77e54d 100644
--- a/app/reducers/views/channel.js
+++ b/app/reducers/views/channel.js
@@ -2,17 +2,25 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
+import {ChannelTypes, FilesTypes} from 'mattermost-redux/constants';
import {ViewTypes} from 'app/constants';
-import {ChannelTypes} from 'mattermost-redux/constants';
-
function drafts(state = {}, action) {
switch (action.type) {
case ViewTypes.POST_DRAFT_CHANGED: {
return {
...state,
- [action.channelId]: action.postDraft
+ [action.channelId]: Object.assign({}, state[action.channelId], {draft: action.postDraft})
+ };
+ }
+ case ViewTypes.SET_POST_DRAFT: {
+ return {
+ ...state,
+ [action.channelId]: {
+ draft: action.postDraft,
+ files: action.files
+ }
};
}
case ChannelTypes.SELECT_CHANNEL: {
@@ -20,12 +28,52 @@ function drafts(state = {}, action) {
if (!data[action.data]) {
data = {
...state,
- [action.data]: ''
+ [action.data]: {
+ draft: '',
+ files: []
+ }
};
}
return data;
}
+ case FilesTypes.RECEIVED_UPLOAD_FILES: {
+ if (action.rootId) {
+ return state;
+ }
+
+ const files = [
+ ...state[action.channelId].files,
+ ...action.data
+ ];
+
+ return {
+ ...state,
+ [action.channelId]: Object.assign({}, state[action.channelId], {files})
+ };
+ }
+ case ViewTypes.CLEAR_FILES_FOR_POST_DRAFT: {
+ if (action.rootId) {
+ return state;
+ }
+
+ return {
+ ...state,
+ [action.channelId]: Object.assign({}, state[action.channelId], {files: []})
+ };
+ }
+ case ViewTypes.REMOVE_FILE_FROM_POST_DRAFT: {
+ if (action.rootId) {
+ return state;
+ }
+
+ const files = state[action.channelId].files.filter((file) => file.id !== action.fileId);
+
+ return {
+ ...state,
+ [action.channelId]: Object.assign({}, state[action.channelId], {files})
+ };
+ }
default:
return state;
}
diff --git a/app/reducers/views/thread.js b/app/reducers/views/thread.js
index 363001681..a7ef55f6e 100644
--- a/app/reducers/views/thread.js
+++ b/app/reducers/views/thread.js
@@ -2,21 +2,70 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
+import {FilesTypes, PostsTypes} from 'mattermost-redux/constants';
import {ViewTypes} from 'app/constants';
-function draft(state = {}, action) {
+function drafts(state = {}, action) {
switch (action.type) {
case ViewTypes.COMMENT_DRAFT_CHANGED:
return {
...state,
- [action.rootId]: action.draft
+ [action.rootId]: Object.assign({}, state[action.rootId], {draft: action.draft})
};
+ case ViewTypes.SET_COMMENT_DRAFT:
+ return {
+ ...state,
+ [action.rootId]: {
+ draft: action.draft,
+ files: action.files
+ }
+ };
+ case PostsTypes.RECEIVED_POST_SELECTED: {
+ let data = {...state};
+
+ if (!data[action.data]) {
+ data = {
+ ...state,
+ [action.data]: {
+ draft: '',
+ files: []
+ }
+ };
+ }
+
+ return data;
+ }
+ case FilesTypes.RECEIVED_UPLOAD_FILES: {
+ if (!action.rootId) {
+ return state;
+ }
+
+ const files = [
+ ...state[action.rootId].files,
+ ...action.data
+ ];
+
+ return {
+ ...state,
+ [action.rootId]: Object.assign({}, state[action.rootId], {files})
+ };
+ }
+ case ViewTypes.CLEAR_FILES_FOR_POST_DRAFT: {
+ if (!action.rootId) {
+ return state;
+ }
+
+ return {
+ ...state,
+ [action.rootId]: Object.assign({}, state[action.rootId], {files: []})
+ };
+ }
default:
return state;
}
}
export default combineReducers({
- draft
+ drafts
});
diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js
index d7f84a08a..9d0708655 100644
--- a/app/scenes/channel/channel.js
+++ b/app/scenes/channel/channel.js
@@ -136,6 +136,8 @@ export default class Channel extends React.PureComponent {
teamId = currentTeam.id;
}
+ const channelDraft = this.props.drafts[this.props.currentChannel.id];
+
return (
+
{
+ let textComponent;
+ if (item.text.hasOwnProperty('id')) {
+ textComponent = (
+
+ );
+ } else {
+ textComponent = {item.text};
+ }
+
return (
- {item.text}
+ {textComponent}
{item.icon &&
{
const {items} = this.props;
const options = items.map((item, index) => {
+ let textComponent;
+ if (item.text.hasOwnProperty('id')) {
+ textComponent = (
+
+ );
+ } else {
+ textComponent = {item.text};
+ }
+
return (
- {item.text}
+ {textComponent}
{item.icon &&
- {this.props.title}
-
- );
+ let title;
+ let titleComponent;
+ if (this.props.title) {
+ if (this.props.title.hasOwnProperty('id')) {
+ titleComponent = (
+
+ );
+ } else {
+ titleComponent = {this.props.title};
+ }
+
+ title = (
+
+ {titleComponent}
+
+ );
+ }
return [
title,
diff --git a/app/scenes/thread/thread.js b/app/scenes/thread/thread.js
index c9d2891e3..9ede59a92 100644
--- a/app/scenes/thread/thread.js
+++ b/app/scenes/thread/thread.js
@@ -27,6 +27,7 @@ export default class Thread extends PureComponent {
}).isRequired,
teamId: PropTypes.string.isRequired,
channelId: PropTypes.string.isRequired,
+ files: PropTypes.array,
rootId: PropTypes.string.isRequired,
draft: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
@@ -61,6 +62,7 @@ export default class Thread extends PureComponent {
{
+ const r = Math.floor(Math.random() * 16);
+
+ let v;
+ if (c === 'x') {
+ v = r;
+ } else {
+ v = (r & 0x3) | 0x8;
+ }
+
+ return v.toString(16);
+ });
+
+ return 'uid' + id;
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 33ee6b148..ecadb6221 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1516,6 +1516,9 @@
"mobile.create_channel": "Create",
"mobile.create_channel.public": "New Public Channel",
"mobile.create_channel.private": "New Private Group",
+ "mobile.file_upload.camera": "Take Photo or Video",
+ "mobile.file_upload.library": "Photo Library",
+ "mobile.file_upload.more": "More",
"mobile.loading_channels": "Loading Channels...",
"mobile.loading_members": "Loading Members...",
"mobile.loading_posts": "Loading Messages...",
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index 3f0e2d6d3..76943beb3 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -25,10 +25,15 @@
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
1BCA51319AC6442991C6A208 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0A091BF1A3D04650AD306A0D /* Zocial.ttf */; };
2B4C9B708010475DA575B81D /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */; };
+ 37DA4BA61E6F55D3002B058E /* RSKImageCropper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DA4BA51E6F55D3002B058E /* RSKImageCropper.framework */; };
+ 37DA4BA71E6F55D3002B058E /* RSKImageCropper.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 37DA4BA51E6F55D3002B058E /* RSKImageCropper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 37DA4BA91E6F55D3002B058E /* QBImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DA4BA81E6F55D3002B058E /* QBImagePicker.framework */; };
+ 37DA4BAA1E6F55D3002B058E /* QBImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 37DA4BA81E6F55D3002B058E /* QBImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
382D94CF15EE4FC292C3F341 /* Foundation.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51F5A483EA9F4A9A87ACFB59 /* Foundation.ttf */; };
3D38ABA732A34A9BB3294F90 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; };
420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EDC04CBCF81642219D199CBB /* Octicons.ttf */; };
584837D6B55F405F908A2053 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ACC6B9FDC0AD45A6BFA4FBCD /* libBVLinearGradient.a */; };
+ 587E8EEB59DB4EC38F6940BF /* libimageCropPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37A98FBBA38D48E786B32BAD /* libimageCropPicker.a */; };
5E1AF7B72B8D4A4E9E53FF9D /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; };
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
7F63D2841E6C9585001FAE12 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; };
@@ -108,6 +113,27 @@
remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
remoteInfo = React;
};
+ 37D8FEC11E80B5230091F3BD /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = BVLinearGradient;
+ };
+ 37DA4B791E6F5419002B058E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = F9678BD770064C20ACFA154A /* imageCropPicker.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3400A8081CEB54A6008A0BC7;
+ remoteInfo = imageCropPicker;
+ };
+ 37DD11271E79EBE1004111BA /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = DA5891D81BA9A9FC002B4DB2;
+ remoteInfo = RNDeviceInfo;
+ };
3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
@@ -241,13 +267,6 @@
remoteGlobalIDString = 3D05745F1DE6004600184BB4;
remoteInfo = "RCTPushNotification-tvOS";
};
- 7F6877A51E7835AD0094B63F /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = DA5891D81BA9A9FC002B4DB2;
- remoteInfo = RNDeviceInfo;
- };
7F6877AF1E7835E50094B63F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7F6877AA1E7835E50094B63F /* ToolTipMenu.xcodeproj */;
@@ -283,13 +302,6 @@
remoteGlobalIDString = 5DBEB1501B18CEA900B34395;
remoteInfo = RNVectorIcons;
};
- 7FED0C0E1E7C4A94001A7CCA /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 134814201AA4EA6300B7C361;
- remoteInfo = BVLinearGradient;
- };
832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
@@ -299,6 +311,21 @@
};
/* End PBXContainerItemProxy section */
+/* Begin PBXCopyFilesBuildPhase section */
+ 37DA4BA41E6F55AD002B058E /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 37DA4BA71E6F55D3002B058E /* RSKImageCropper.framework in Embed Frameworks */,
+ 37DA4BAA1E6F55D3002B058E /* QBImagePicker.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
005346E5C0E542BFABAE1411 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; };
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; };
@@ -327,6 +354,9 @@
31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNCookieManagerIOS.xcodeproj; path = "../node_modules/react-native-cookies/RNCookieManagerIOS.xcodeproj"; sourceTree = ""; };
349FBA7338E74D9BBD709528 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; };
356C9186FA374641A00EB2EA /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = ""; };
+ 37A98FBBA38D48E786B32BAD /* libimageCropPicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libimageCropPicker.a; sourceTree = ""; };
+ 37DA4BA51E6F55D3002B058E /* RSKImageCropper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSKImageCropper.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 37DA4BA81E6F55D3002B058E /* QBImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = QBImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; };
51F5A483EA9F4A9A87ACFB59 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; };
59954D479A89488091EB588F /* RNSearchBar.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSearchBar.xcodeproj; path = "../node_modules/react-native-search-bar/RNSearchBar.xcodeproj"; sourceTree = ""; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; };
@@ -343,6 +373,7 @@
EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; };
F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; };
F81F6DC42D394831B4549928 /* libRNSearchBar.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSearchBar.a; sourceTree = ""; };
+ F9678BD770064C20ACFA154A /* imageCropPicker.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = imageCropPicker.xcodeproj; path = "../node_modules/react-native-image-crop-picker/ios/imageCropPicker.xcodeproj"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -361,6 +392,7 @@
7FBB5E9A1E1F5A4B000DE18A /* libRNSVG.a in Frameworks */,
7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */,
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */,
+ 37DA4BA61E6F55D3002B058E /* RSKImageCropper.framework in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
@@ -377,6 +409,8 @@
10CD747CE4304BD6AB38B4CD /* libRNDeviceInfo.a in Frameworks */,
584837D6B55F405F908A2053 /* libBVLinearGradient.a in Frameworks */,
7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */,
+ 37DA4BA91E6F55D3002B058E /* QBImagePicker.framework in Frameworks */,
+ 587E8EEB59DB4EC38F6940BF /* libimageCropPicker.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -506,6 +540,30 @@
name = Products;
sourceTree = "";
};
+ 37D8FEBE1E80B5230091F3BD /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 37DA4B761E6F5419002B058E /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 37DA4B7A1E6F5419002B058E /* libimageCropPicker.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 37DD11071E79EBE1004111BA /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
5E91572E1DD0AC6500FF2AA8 /* Products */ = {
isa = PBXGroup;
children = (
@@ -541,14 +599,6 @@
name = Products;
sourceTree = "";
};
- 7F6877861E7835AD0094B63F /* Products */ = {
- isa = PBXGroup;
- children = (
- 7F6877A61E7835AD0094B63F /* libRNDeviceInfo.a */,
- );
- name = Products;
- sourceTree = "";
- };
7F6877AB1E7835E50094B63F /* Products */ = {
isa = PBXGroup;
children = (
@@ -582,14 +632,6 @@
name = Products;
sourceTree = "";
};
- 7FED0C0B1E7C4A94001A7CCA /* Products */ = {
- isa = PBXGroup;
- children = (
- 7FED0C0F1E7C4A94001A7CCA /* libBVLinearGradient.a */,
- );
- name = Products;
- sourceTree = "";
- };
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -612,6 +654,7 @@
31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */,
B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */,
7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */,
+ F9678BD770064C20ACFA154A /* imageCropPicker.xcodeproj */,
);
name = Libraries;
sourceTree = "";
@@ -628,6 +671,8 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
+ 37DA4BA81E6F55D3002B058E /* QBImagePicker.framework */,
+ 37DA4BA51E6F55D3002B058E /* RSKImageCropper.framework */,
13B07FAE1A68108700A75B9A /* Mattermost */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* MattermostTests */,
@@ -676,6 +721,7 @@
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
+ 37DA4BA41E6F55AD002B058E /* Embed Frameworks */,
);
buildRules = (
);
@@ -723,9 +769,13 @@
projectDirPath = "";
projectReferences = (
{
- ProductGroup = 7FED0C0B1E7C4A94001A7CCA /* Products */;
+ ProductGroup = 37D8FEBE1E80B5230091F3BD /* Products */;
ProjectRef = 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */;
},
+ {
+ ProductGroup = 37DA4B761E6F5419002B058E /* Products */;
+ ProjectRef = F9678BD770064C20ACFA154A /* imageCropPicker.xcodeproj */;
+ },
{
ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */;
ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */;
@@ -779,7 +829,7 @@
ProjectRef = 31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */;
},
{
- ProductGroup = 7F6877861E7835AD0094B63F /* Products */;
+ ProductGroup = 37DD11071E79EBE1004111BA /* Products */;
ProjectRef = B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */;
},
{
@@ -864,6 +914,27 @@
remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
+ 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libBVLinearGradient.a;
+ remoteRef = 37D8FEC11E80B5230091F3BD /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 37DA4B7A1E6F5419002B058E /* libimageCropPicker.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libimageCropPicker.a;
+ remoteRef = 37DA4B791E6F5419002B058E /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRNDeviceInfo.a;
+ remoteRef = 37DD11271E79EBE1004111BA /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -997,13 +1068,6 @@
remoteRef = 7F63D2821E6C957C001FAE12 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
- 7F6877A61E7835AD0094B63F /* libRNDeviceInfo.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = libRNDeviceInfo.a;
- remoteRef = 7F6877A51E7835AD0094B63F /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
7F6877B01E7835E50094B63F /* libToolTipMenu.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1039,13 +1103,6 @@
remoteRef = 7FDF290B1E1F4B4E00DBBE56 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
- 7FED0C0F1E7C4A94001A7CCA /* libBVLinearGradient.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = libBVLinearGradient.a;
- remoteRef = 7FED0C0E1E7C4A94001A7CCA /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1156,7 +1213,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1175,7 +1231,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist
index 79152053b..c78c3386a 100644
--- a/ios/Mattermost/Info.plist
+++ b/ios/Mattermost/Info.plist
@@ -2,6 +2,10 @@
+ NSCameraUsageDescription
+
+ NSPhotoLibraryUsageDescription
+
CFBundleDevelopmentRegion
en
CFBundleExecutable
@@ -36,9 +40,9 @@
- NSAllowsArbitraryLoads
-
+ NSAllowsArbitraryLoads
+
NSLocationWhenInUseUsageDescription
UIAppFonts
diff --git a/package.json b/package.json
index 1c8143665..ed7cec21d 100644
--- a/package.json
+++ b/package.json
@@ -19,11 +19,12 @@
"react-native-cookies": "2.0.0",
"react-native-device-info": "0.10.1",
"react-native-drawer": "2.3.0",
+ "react-native-image-crop-picker": "0.12.8",
"react-native-keyboard-aware-scroll-view": "0.2.7",
"react-native-keyboard-spacer": "0.3.1",
+ "react-native-linear-gradient": "2.0.0",
"react-native-message-bar": "1.6.0",
"react-native-push-notification": "2.2.1",
- "react-native-linear-gradient": "2.0.0",
"react-native-search-bar": "enahum/react-native-search-bar.git",
"react-native-svg": "4.5.0",
"react-native-tooltip": "5.0.0",
diff --git a/test/app/actions/thread.test.js b/test/app/actions/thread.test.js
index f2ba20beb..fa3d4b5d4 100644
--- a/test/app/actions/thread.test.js
+++ b/test/app/actions/thread.test.js
@@ -13,26 +13,36 @@ describe('Actions.Views.Thread', () => {
await ThreadActions.handleCommentDraftChanged('1234', 'draft1')(store.dispatch, store.getState);
assert.deepEqual(store.getState().views.thread, {
- draft: {
- 1234: 'draft1'
+ drafts: {
+ 1234: {
+ draft: 'draft1'
+ }
}
});
await ThreadActions.handleCommentDraftChanged('1235', 'draft2')(store.dispatch, store.getState);
assert.deepEqual(store.getState().views.thread, {
- draft: {
- 1234: 'draft1',
- 1235: 'draft2'
+ drafts: {
+ 1234: {
+ draft: 'draft1'
+ },
+ 1235: {
+ draft: 'draft2'
+ }
}
});
await ThreadActions.handleCommentDraftChanged('1235', 'draft3')(store.dispatch, store.getState);
assert.deepEqual(store.getState().views.thread, {
- draft: {
- 1234: 'draft1',
- 1235: 'draft3'
+ drafts: {
+ 1234: {
+ draft: 'draft1'
+ },
+ 1235: {
+ draft: 'draft3'
+ }
}
});
});