PLT-5518 RN: Allow user to upload + send image file attachments (#373)
* PLT-5518 RN: Allow user to upload + send image file attachments * Review feedback
This commit is contained in:
parent
a35183a677
commit
18cc7f1e7f
35 changed files with 829 additions and 175 deletions
32
NOTICE.txt
32
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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.mattermost;
|
|||
import com.facebook.react.ReactActivity;
|
||||
import com.psykar.cookiemanager.CookieManagerPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
import com.reactnative.ivpusic.imagepicker.PickerPackage;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.facebook.react.ReactPackage;
|
|||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
import com.reactnative.ivpusic.imagepicker.PickerPackage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
|
@ -38,7 +39,8 @@ public class MainApplication extends Application implements ReactApplication {
|
|||
new ReactNativePushNotificationPackage(),
|
||||
new VectorIconsPackage(),
|
||||
new RNSvgPackage(),
|
||||
new LinearGradientPackage()
|
||||
new LinearGradientPackage(),
|
||||
new PickerPackage()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,5 +13,7 @@ project(':react-native-vector-icons').projectDir = new File(rootProject.projectD
|
|||
include ':app'
|
||||
include ':react-native-linear-gradient'
|
||||
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||
include ':react-native-image-crop-picker'
|
||||
project(':react-native-image-crop-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-crop-picker/android')
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@ export function loadStorage() {
|
|||
if (otherStorage.postDrafts) {
|
||||
Object.keys(otherStorage.postDrafts).forEach((d) => {
|
||||
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});
|
||||
};
|
||||
|
|
|
|||
60
app/actions/views/file_upload.js
Normal file
60
app/actions/views/file_upload.js
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const style = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 43,
|
||||
right: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth}]}>
|
||||
<Image
|
||||
style={{height: imageHeight, width: imageWidth}}
|
||||
source={source}
|
||||
defaultSource={genericIcon}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
defaultSource={genericIcon}
|
||||
onError={this.handleError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
156
app/components/file_upload_preview/file_upload_preview.js
Normal file
156
app/components/file_upload_preview/file_upload_preview.js
Normal file
|
|
@ -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 (
|
||||
<View
|
||||
key={file.id}
|
||||
style={style.preview}
|
||||
>
|
||||
<FileAttachmentPreview file={file}/>
|
||||
<TouchableOpacity
|
||||
style={style.removeButton}
|
||||
onPress={() => this.props.actions.handleRemoveFile(file.id, this.props.channelId, this.props.rootId)}
|
||||
>
|
||||
<Font
|
||||
name='times'
|
||||
color='#fff'
|
||||
size={15}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 = (
|
||||
<View style={style.loader}>
|
||||
<Text style={style.loaderText}>{'Loading...'}</Text>
|
||||
<TouchableOpacity onPress={this.handleCancelUpload}>
|
||||
<Text style={style.cancelText}>{'Cancel'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
style={style.scrollView}
|
||||
contentContainerStyle={style.scrollViewContent}
|
||||
>
|
||||
{this.buildFilePreviews()}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={style.container}>
|
||||
{component}
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
});
|
||||
30
app/components/file_upload_preview/index.js
Normal file
30
app/components/file_upload_preview/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<View style={{padding: 7}}>
|
||||
<View>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
opacity: 0.7,
|
||||
fontSize: 11,
|
||||
marginBottom: 5,
|
||||
color: theme.centerChannelColor
|
||||
}}
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</View>
|
||||
<FileUploadPreview
|
||||
channelId={this.props.channelId}
|
||||
files={this.props.files}
|
||||
rootId={this.props.rootId}
|
||||
uploadFileRequestId={this.state.uploadFileRequestId}
|
||||
/>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
onChangeText={this.props.onChangeText}
|
||||
rootId={this.props.rootId}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'flex-end',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
style={style.inputWrapper}
|
||||
>
|
||||
{/*<TouchableHighlight
|
||||
style={{
|
||||
height: 36,
|
||||
padding: 9,
|
||||
width: 36
|
||||
}}
|
||||
<TouchableHighlight
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
style={style.buttonContainer}
|
||||
>
|
||||
<PaperClipIcon
|
||||
width={18}
|
||||
height={18}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
<View style={{width: 7}}/>*/}
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref='input'
|
||||
value={this.props.value}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handleSelectionChange}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onSubmitEditing={this.sendMessage}
|
||||
multiline={true}
|
||||
underlineColorAndroid='transparent'
|
||||
style={{
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderWidth: 1,
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
height: Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT),
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
paddingTop: 6
|
||||
}}
|
||||
/>
|
||||
<View style={{width: 7}}/>
|
||||
<TouchableHighlight onPress={this.sendMessage}>
|
||||
<Icon
|
||||
name='paper-plane'
|
||||
size={18}
|
||||
style={{
|
||||
color: theme.linkColor,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingVertical: 8
|
||||
},
|
||||
android: {
|
||||
paddingVertical: 7
|
||||
}
|
||||
}),
|
||||
paddingHorizontal: 9
|
||||
}}
|
||||
size={30}
|
||||
color={changeOpacity('#fff', 0.9)}
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref='input'
|
||||
value={this.props.value}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handleSelectionChange}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
onSubmitEditing={this.sendMessage}
|
||||
multiline={true}
|
||||
underlineColorAndroid='transparent'
|
||||
style={[style.input, {height: Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT)}]}
|
||||
/>
|
||||
<TouchableHighlight
|
||||
onPress={this.sendMessage}
|
||||
style={style.buttonContainer}
|
||||
>
|
||||
<Icon
|
||||
name='ios-arrow-round-up'
|
||||
size={34}
|
||||
color={theme.linkColor}
|
||||
style={{marginTop: 2}}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -293,6 +293,9 @@ const state = {
|
|||
},
|
||||
selectServer: {
|
||||
serverUrl: Config.DefaultServerUrl
|
||||
},
|
||||
thread: {
|
||||
drafts: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ export default class Channel extends React.PureComponent {
|
|||
teamId = currentTeam.id;
|
||||
}
|
||||
|
||||
const channelDraft = this.props.drafts[this.props.currentChannel.id];
|
||||
|
||||
return (
|
||||
<KeyboardLayout
|
||||
behavior='padding'
|
||||
|
|
@ -146,7 +148,8 @@ export default class Channel extends React.PureComponent {
|
|||
<ChannelPostList channel={currentChannel}/>
|
||||
<PostTextbox
|
||||
ref={this.attachPostTextbox}
|
||||
value={this.props.drafts[this.props.currentChannel.id]}
|
||||
files={channelDraft.files}
|
||||
value={channelDraft.draft}
|
||||
teamId={teamId}
|
||||
channelId={currentChannel.id}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ export default class OptionsModal extends PureComponent {
|
|||
items: PropTypes.array.isRequired,
|
||||
onCancelPress: PropTypes.func,
|
||||
requestClose: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
|
||||
title: PropTypes.string
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object
|
||||
])
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -71,7 +74,7 @@ export default class OptionsModal extends PureComponent {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={actions.goBack}>
|
||||
<TouchableWithoutFeedback onPress={actions.closeModal}>
|
||||
<View style={style.wrapper}>
|
||||
<AnimatedView style={{height: deviceHeight, left: 0, top: this.state.top, width: deviceWidth}}>
|
||||
<OptionsModalList
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from 'react-native';
|
||||
import Font from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class OptionsModalList extends PureComponent {
|
||||
static propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
|
|
@ -20,13 +22,25 @@ export default class OptionsModalList extends PureComponent {
|
|||
const {items, onCancelPress} = this.props;
|
||||
|
||||
const options = items.map((item, index) => {
|
||||
let textComponent;
|
||||
if (item.text.hasOwnProperty('id')) {
|
||||
textComponent = (
|
||||
<FormattedText
|
||||
style={[style.optionText, item.textStyle]}
|
||||
{...item.text}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
textComponent = <Text style={[style.optionText, item.textStyle]}>{item.text}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
onPress={item.action}
|
||||
style={[style.option, style.optionBorder]}
|
||||
>
|
||||
<Text style={[style.optionText, item.textStyle]}>{item.text}</Text>
|
||||
{textComponent}
|
||||
{item.icon &&
|
||||
<Font
|
||||
name={item.icon}
|
||||
|
|
|
|||
|
|
@ -10,24 +10,41 @@ import {
|
|||
} from 'react-native';
|
||||
import Font from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class OptionsModalList extends PureComponent {
|
||||
static propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
onCancelPress: PropTypes.func,
|
||||
title: PropTypes.string
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object
|
||||
])
|
||||
}
|
||||
|
||||
renderOptions = () => {
|
||||
const {items} = this.props;
|
||||
|
||||
const options = items.map((item, index) => {
|
||||
let textComponent;
|
||||
if (item.text.hasOwnProperty('id')) {
|
||||
textComponent = (
|
||||
<FormattedText
|
||||
style={[style.optionText, item.textStyle, (!item.icon && {textAlign: 'center'})]}
|
||||
{...item.text}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
textComponent = <Text style={[style.optionText, item.textStyle, (!item.icon && {textAlign: 'center'})]}>{item.text}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
onPress={item.action}
|
||||
style={[style.option, (index < items.length - 1 && style.optionBorder)]}
|
||||
>
|
||||
<Text style={[style.optionText, item.textStyle, (!item.icon && {textAlign: 'center'})]}>{item.text}</Text>
|
||||
{textComponent}
|
||||
{item.icon &&
|
||||
<Font
|
||||
name={item.icon}
|
||||
|
|
@ -39,14 +56,29 @@ export default class OptionsModalList extends PureComponent {
|
|||
);
|
||||
});
|
||||
|
||||
const title = (
|
||||
<View
|
||||
key={items.length}
|
||||
style={[style.option, style.optionBorder]}
|
||||
>
|
||||
<Text style={style.optionTitleText}>{this.props.title}</Text>
|
||||
</View>
|
||||
);
|
||||
let title;
|
||||
let titleComponent;
|
||||
if (this.props.title) {
|
||||
if (this.props.title.hasOwnProperty('id')) {
|
||||
titleComponent = (
|
||||
<FormattedText
|
||||
style={style.optionTitleText}
|
||||
{...this.props.title}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
titleComponent = <Text style={style.optionTitleText}>{this.props.title}</Text>;
|
||||
}
|
||||
|
||||
title = (
|
||||
<View
|
||||
key={items.length}
|
||||
style={[style.option, style.optionBorder]}
|
||||
>
|
||||
{titleComponent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<PostTextbox
|
||||
rootId={this.props.rootId}
|
||||
value={this.props.draft}
|
||||
files={this.props.files}
|
||||
teamId={this.props.teamId}
|
||||
channelId={this.props.channelId}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,15 @@ function makeMapStateToProps() {
|
|||
teamId = getCurrentTeamId(state);
|
||||
}
|
||||
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
teamId,
|
||||
channelId: ownProps.channelId,
|
||||
rootId: ownProps.rootId,
|
||||
draft: state.views.thread.draft[ownProps.rootId] || '',
|
||||
draft: threadDraft.draft,
|
||||
files: threadDraft.files,
|
||||
posts,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
|
|
|
|||
22
app/utils/file.js
Normal file
22
app/utils/file.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export function generateId() {
|
||||
// Implementation taken from http://stackoverflow.com/a/2117523
|
||||
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||
|
||||
id = id.replace(/[xy]/g, (c) => {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
37A98FBBA38D48E786B32BAD /* libimageCropPicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libimageCropPicker.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
|
||||
|
|
@ -343,6 +373,7 @@
|
|||
EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
F81F6DC42D394831B4549928 /* libRNSearchBar.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSearchBar.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
37D8FEBE1E80B5230091F3BD /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
37DA4B761E6F5419002B058E /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37DA4B7A1E6F5419002B058E /* libimageCropPicker.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
37DD11071E79EBE1004111BA /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5E91572E1DD0AC6500FF2AA8 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -541,14 +599,6 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F6877861E7835AD0094B63F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7F6877A61E7835AD0094B63F /* libRNDeviceInfo.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F6877AB1E7835E50094B63F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -582,14 +632,6 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7FED0C0B1E7C4A94001A7CCA /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7FED0C0F1E7C4A94001A7CCA /* libBVLinearGradient.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -612,6 +654,7 @@
|
|||
31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */,
|
||||
B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */,
|
||||
7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */,
|
||||
F9678BD770064C20ACFA154A /* imageCropPicker.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string></string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
@ -36,9 +40,9 @@
|
|||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UIAppFonts</key>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue