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' + } } }); });