diff --git a/app/actions/views/edit_profile.js b/app/actions/views/edit_profile.js
new file mode 100644
index 000000000..717fe4422
--- /dev/null
+++ b/app/actions/views/edit_profile.js
@@ -0,0 +1,25 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
+import {buildFileUploadData} from 'app/utils/file';
+
+export function updateUser(user, success, error) {
+ return async (dispatch, getState) => {
+ const result = await updateMe(user)(dispatch, getState);
+ const {data, error: err} = result;
+ if (data && success) {
+ success(data);
+ } else if (err && error) {
+ error({id: err.server_error_id, ...err});
+ }
+ return result;
+ };
+}
+
+export function handleUploadProfileImage(image, userId) {
+ return async (dispatch, getState) => {
+ const imageData = buildFileUploadData(image);
+ return await uploadProfileImage(userId, imageData)(dispatch, getState);
+ };
+}
diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js
index e1784078d..e2181aff8 100644
--- a/app/actions/views/file_upload.js
+++ b/app/actions/views/file_upload.js
@@ -4,9 +4,9 @@
import FormData from 'form-data';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
-import {lookupMimeType, parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
+import {parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
-import {generateId} from 'app/utils/file';
+import {buildFileUploadData, generateId} from 'app/utils/file';
import {ViewTypes} from 'app/constants';
export function handleUploadFiles(files, rootId) {
@@ -16,41 +16,19 @@ export function handleUploadFiles(files, rootId) {
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const clientIds = [];
- const re = /heic/i;
files.forEach((file) => {
- let name = file.fileName || file.path || file.uri;
-
- if (name.includes('/')) {
- name = name.split('/').pop();
- }
-
- let mimeType = lookupMimeType(name);
- let extension = name.split('.').pop().replace('.', '');
- const uri = file.uri;
+ const fileData = buildFileUploadData(file);
const clientId = generateId();
- if (re.test(extension)) {
- extension = 'JPG';
- name = name.replace(re, 'jpg');
- mimeType = 'image/jpeg';
- }
-
clientIds.push({
clientId,
- localPath: uri,
- name,
- type: mimeType,
- extension
+ localPath: fileData.uri,
+ name: fileData.name,
+ type: fileData.mimeType,
+ extension: fileData.extension
});
- const fileData = {
- uri,
- name,
- type: mimeType,
- extension
- };
-
formData.append('files', fileData);
formData.append('channel_id', channelId);
formData.append('client_ids', clientId);
diff --git a/app/components/post_textbox/components/attachment_button.js b/app/components/attachment_button.js
similarity index 94%
rename from app/components/post_textbox/components/attachment_button.js
rename to app/components/attachment_button.js
index 3ee902d3f..2b8f8dbdd 100644
--- a/app/components/post_textbox/components/attachment_button.js
+++ b/app/components/attachment_button.js
@@ -14,13 +14,19 @@ import {changeOpacity} from 'app/utils/theme';
class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
+ children: PropTypes.node,
+ fileCount: PropTypes.number,
intl: intlShape.isRequired,
+ maxFileCount: PropTypes.number.isRequired,
navigator: PropTypes.object.isRequired,
+ onShowFileMaxWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
- fileCount: PropTypes.number,
- maxFileCount: PropTypes.number.isRequired,
- onShowFileMaxWarning: PropTypes.func.isRequired
+ wrapper: PropTypes.bool
+ };
+
+ static defaultProps = {
+ maxFileCount: 5
};
attachFileFromCamera = () => {
@@ -142,7 +148,7 @@ class AttachmentButton extends PureComponent {
action();
}
}, 100);
- }
+ };
showFileAttachmentOptions = () => {
const {fileCount, maxFileCount, onShowFileMaxWarning} = this.props;
@@ -200,7 +206,17 @@ class AttachmentButton extends PureComponent {
};
render() {
- const {theme} = this.props;
+ const {theme, wrapper, children} = this.props;
+
+ if (wrapper) {
+ return (
+
+ {children}
+
+ );
+ }
return (
);
}
return (
-
+
{error.message || error}
);
diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js
index e2807e0a3..736b0f588 100644
--- a/app/components/post_textbox/post_textbox.js
+++ b/app/components/post_textbox/post_textbox.js
@@ -7,12 +7,12 @@ import {Alert, BackHandler, Keyboard, Platform, TextInput, TouchableOpacity, Vie
import {intlShape} from 'react-intl';
import {RequestStatus} from 'mattermost-redux/constants';
+import AttachmentButton from 'app/components/attachment_button';
import Autocomplete from 'app/components/autocomplete';
import FileUploadPreview from 'app/components/file_upload_preview';
import PaperPlane from 'app/components/paper_plane';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
-import AttachmentButton from './components/attachment_button';
import Typing from './components/typing';
const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36;
diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js
index 74a40b12e..9a1b5c132 100644
--- a/app/components/profile_picture/profile_picture.js
+++ b/app/components/profile_picture/profile_picture.js
@@ -4,9 +4,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, Platform, View} from 'react-native';
+import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import UserStatus from 'app/components/user_status';
-import {makeStyleSheetFromTheme} from 'app/utils/theme';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import placeholder from 'assets/images/profile.jpg';
@@ -25,6 +26,8 @@ export default class ProfilePicture extends PureComponent {
user: PropTypes.object,
showStatus: PropTypes.bool,
status: PropTypes.string,
+ edit: PropTypes.bool,
+ imageUri: PropTypes.string,
theme: PropTypes.object.isRequired,
actions: PropTypes.shape({
getStatusForId: PropTypes.func.isRequired
@@ -35,7 +38,8 @@ export default class ProfilePicture extends PureComponent {
showStatus: true,
size: 128,
statusBorderWidth: 2,
- statusSize: 14
+ statusSize: 14,
+ edit: false
};
componentDidMount() {
@@ -45,7 +49,7 @@ export default class ProfilePicture extends PureComponent {
}
render() {
- const {theme} = this.props;
+ const {edit, imageUri, showStatus, theme} = this.props;
const style = getStyleSheet(theme);
let pictureUrl;
@@ -53,12 +57,34 @@ export default class ProfilePicture extends PureComponent {
pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
}
- const statusIcon = (
-
- );
+ if (edit && imageUri) {
+ pictureUrl = imageUri;
+ }
+
+ let statusIcon;
+ let statusStyle;
+ if (edit) {
+ const iconColor = changeOpacity(theme.centerChannelColor, 0.6);
+ statusStyle = {
+ width: this.props.statusSize,
+ height: this.props.statusSize,
+ backgroundColor: 'white'
+ };
+ statusIcon = (
+
+ );
+ } else if (this.props.status && !edit) {
+ statusIcon = (
+
+ );
+ }
return (
@@ -68,8 +94,8 @@ export default class ProfilePicture extends PureComponent {
source={{uri: pictureUrl}}
defaultSource={placeholder}
/>
- {this.props.showStatus &&
-
+ {(showStatus || edit) &&
+
{statusIcon}
}
diff --git a/app/components/settings_drawer/settings_drawer.js b/app/components/settings_drawer/settings_drawer.js
index 3344e9dcc..1aa1ff9b8 100644
--- a/app/components/settings_drawer/settings_drawer.js
+++ b/app/components/settings_drawer/settings_drawer.js
@@ -179,6 +179,35 @@ export default class SettingsDrawer extends PureComponent {
});
});
+ goToEditProfile = wrapWithPreventDoubleTap(() => {
+ const {currentUser, navigator, theme} = this.props;
+ const {formatMessage} = this.context.intl;
+
+ this.closeSettingsDrawer();
+ navigator.showModal({
+ screen: 'EditProfile',
+ title: formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
+ animationType: 'slide-up',
+ animated: true,
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg
+ },
+ navigatorButtons: {
+ leftButtons: [{
+ id: 'close-settings',
+ icon: this.closeButton
+ }]
+ },
+ passProps: {
+ currentUser
+ }
+ });
+ });
+
goToSettings = wrapWithPreventDoubleTap(() => {
const {intl} = this.context;
const {navigator, theme} = this.props;
@@ -245,7 +274,10 @@ export default class SettingsDrawer extends PureComponent {
alwaysBounceVertical={false}
contentContainerStyle={style.wrapper}
>
-
+
{
+ const {currentUser} = this.props;
+ const keys = Object.keys(this.state);
+ const newState = {...this.state, ...(updatedField || {})};
+ Reflect.deleteProperty(newState, 'error');
+ Reflect.deleteProperty(newState, 'updating');
+
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ let userKey;
+ switch (key) {
+ case 'firstName':
+ userKey = 'first_name';
+ break;
+ case 'lastName':
+ userKey = 'last_name';
+ break;
+ default:
+ userKey = key;
+ break;
+ }
+
+ if (currentUser[userKey] !== newState[key]) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ close = () => {
+ this.props.navigator.dismissModal({
+ animationType: 'slide-down'
+ });
+ };
+
+ emitCanUpdateAccount = (enabled) => {
+ const buttons = {
+ rightButtons: [{...this.rightButton, disabled: !enabled}]
+ };
+
+ this.props.navigator.setButtons(buttons);
+ };
+
+ handleRequestError = (error) => {
+ this.setState({error, updating: false});
+ this.emitCanUpdateAccount(true);
+ if (this.scrollView) {
+ this.scrollView.props.scrollToPosition(0, 0);
+ }
+ };
+
+ submitUser = wrapWithPreventDoubleTap(async () => {
+ this.emitCanUpdateAccount(false);
+ this.setState({error: null, updating: true});
+
+ const {
+ profileImage,
+ firstName,
+ lastName,
+ username,
+ nickname,
+ position,
+ email
+ } = this.state;
+ const user = {
+ first_name: firstName,
+ last_name: lastName,
+ username,
+ nickname,
+ position,
+ email
+ };
+
+ if (profileImage) {
+ const {error} = await this.props.actions.handleUploadProfileImage(profileImage, this.props.currentUser.id);
+ if (error) {
+ this.handleRequestError(error);
+ return;
+ }
+ }
+
+ if (this.canUpdate()) {
+ const {error} = await this.props.actions.updateUser(user);
+ if (error) {
+ this.handleRequestError(error);
+ return;
+ }
+ }
+
+ this.close();
+ });
+
+ handleUploadProfileImage = (images) => {
+ const image = images && images.length > 0 && images[0];
+ this.setState({profileImage: image});
+ this.emitCanUpdateAccount(true);
+ };
+
+ updateField = (field) => {
+ this.setState(field, () => {
+ this.emitCanUpdateAccount(this.canUpdate(field));
+ });
+ };
+
+ onNavigatorEvent = (event) => {
+ if (event.type === 'NavBarButtonPress') {
+ switch (event.id) {
+ case 'update-profile':
+ this.submitUser();
+ break;
+ case 'close-settings':
+ this.close();
+ break;
+ }
+ }
+ };
+
+ renderFirstNameSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {config, currentUser, theme} = this.props;
+ const {firstName} = this.state;
+
+ const {auth_service: service} = currentUser;
+ const disabled = (service === 'ldap' && config.LdapFristNameAttributeSet === 'true') ||
+ (service === 'saml' && config.SamlFirstNameAttributeSet === 'true');
+
+ return (
+
+ );
+ };
+
+ renderLastNameSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {config, currentUser, theme} = this.props;
+ const {lastName} = this.state;
+
+ const {auth_service: service} = currentUser;
+ const disabled = (service === 'ldap' && config.LdapLastNameAttributeSet === 'true') ||
+ (service === 'saml' && config.SamlLastNameAttributeSet === 'true');
+
+ return (
+
+
+
+ );
+ };
+
+ renderUsernameSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {currentUser, theme} = this.props;
+ const {username} = this.state;
+ const disabled = currentUser.auth_service !== '';
+
+ return (
+
+ );
+ };
+
+ renderEmailSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {config, currentUser, theme} = this.props;
+ const {email} = this.state;
+
+ let helpText;
+ let disabled = false;
+
+ if (config.SendEmailNotifications !== 'true') {
+ disabled = true;
+ helpText = formatMessage({
+ id: 'user.settings.general.emailHelp1',
+ defaultMessage: 'Email is used for sign-in, notifications, and password reset. Email requires verification if changed.'
+ });
+ } else if (currentUser.auth_service !== '') {
+ disabled = true;
+
+ switch (currentUser.auth_service) {
+ case 'gitlab':
+ helpText = formatMessage({
+ id: 'user.settings.general.emailGitlabCantUpdate',
+ defaultMessage: 'Login occurs through GitLab. Email cannot be updated. Email address used for notifications is {email}.'
+ }, {email});
+ break;
+ case 'google':
+ helpText = formatMessage({
+ id: 'user.settings.general.emailGoogleCantUpdate',
+ defaultMessage: 'Login occurs through Google Apps. Email cannot be updated. Email address used for notifications is {email}.'
+ }, {email});
+ break;
+ case 'office365':
+ helpText = formatMessage({
+ id: 'user.settings.general.emailOffice365CantUpdate',
+ defaultMessage: 'Login occurs through Office 365. Email cannot be updated. Email address used for notifications is {email}.'
+ }, {email});
+ break;
+ case 'ldap':
+ helpText = formatMessage({
+ id: 'user.settings.general.emailLdapCantUpdate',
+ defaultMessage: 'Login occurs through AD/LDAP. Email cannot be updated. Email address used for notifications is {email}.'
+ }, {email});
+ break;
+ case 'saml':
+ helpText = formatMessage({
+ id: 'user.settings.general.emailSamlCantUpdate',
+ defaultMessage: 'Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.'
+ }, {email});
+ break;
+ }
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderNicknameSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {config, currentUser, theme} = this.props;
+ const {nickname} = this.state;
+
+ const {auth_service: service} = currentUser;
+ const disabled = (service === 'ldap' && config.LdapNicknameAttributeSet === 'true') ||
+ (service === 'saml' && config.SamlNicknameAttributeSet === 'true');
+
+ return (
+
+ );
+ };
+
+ renderPositionSettings = () => {
+ const {formatMessage} = this.context.intl;
+ const {config, currentUser, theme} = this.props;
+ const {position} = this.state;
+
+ const {auth_service: service} = currentUser;
+ const disabled = (service === 'ldap' || service === 'saml') && config.PositionAttribute === 'true';
+
+ return (
+
+ );
+ };
+
+ scrollViewRef = (ref) => {
+ this.scrollView = ref;
+ };
+
+ render() {
+ const {
+ currentUser,
+ theme,
+ navigator
+ } = this.props;
+
+ const {
+ profileImage,
+ error,
+ updating
+ } = this.state;
+
+ const style = getStyleSheet(theme);
+ const uri = profileImage ? profileImage.uri : null;
+
+ if (updating) {
+ return (
+
+
+
+
+ );
+ }
+
+ let displayError;
+ if (error) {
+ displayError = (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {displayError}
+
+
+
+
+
+
+ {this.renderFirstNameSettings()}
+
+ {this.renderLastNameSettings()}
+
+ {this.renderUsernameSettings()}
+
+ {this.renderEmailSettings()}
+
+ {this.renderNicknameSettings()}
+
+ {this.renderPositionSettings()}
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ flex: {
+ flex: 1
+ },
+ container: {
+ backgroundColor: theme.centerChannelBg
+ },
+ scrollView: {
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
+ paddingTop: 10
+ },
+ top: {
+ padding: 25,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ errorContainer: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
+ width: '100%'
+ },
+ errorWrapper: {
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ errorText: {
+ fontSize: 14
+ },
+ separator: {
+ height: 15
+ },
+ footer: {
+ height: 40,
+ width: '100%'
+ }
+ };
+});
+
diff --git a/app/screens/edit_profile/edit_profile_item.js b/app/screens/edit_profile/edit_profile_item.js
new file mode 100644
index 000000000..a28405e5b
--- /dev/null
+++ b/app/screens/edit_profile/edit_profile_item.js
@@ -0,0 +1,129 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {
+ View,
+ Text,
+ TextInput
+} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class AccountSettingsItem extends PureComponent {
+ static propTypes = {
+ disabled: PropTypes.bool,
+ field: PropTypes.string.isRequired,
+ format: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string.isRequired
+ }),
+ helpText: PropTypes.string,
+ optional: PropTypes.bool,
+ theme: PropTypes.object.isRequired,
+ updateValue: PropTypes.func.isRequired,
+ value: PropTypes.string.isRequired
+ };
+
+ static defaultProps = {
+ optional: false,
+ disabled: false
+ };
+
+ onChangeText = (value) => {
+ const {field, updateValue} = this.props;
+ updateValue({[field]: value});
+ };
+
+ render() {
+ const {
+ theme,
+ format,
+ helpText,
+ optional,
+ disabled,
+ value
+ } = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+
+ {optional && (
+
+ )}
+
+
+
+ {disabled &&
+
+ {helpText}
+
+ }
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ inputContainer: {
+ borderTopWidth: 1,
+ borderBottomWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
+ borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
+ backgroundColor: theme.centerChannelBg
+ },
+ input: {
+ color: theme.centerChannelColor,
+ fontSize: 14,
+ height: 40,
+ paddingHorizontal: 15
+ },
+ disabled: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
+ },
+ title: {
+ fontSize: 14,
+ color: theme.centerChannelColor,
+ marginLeft: 15
+ },
+ titleContainer15: {
+ flexDirection: 'row',
+ marginTop: 15
+ },
+ optional: {
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ fontSize: 14,
+ marginLeft: 5
+ },
+ helpText: {
+ fontSize: 12,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ marginHorizontal: 15,
+ marginVertical: 10
+ }
+ };
+});
diff --git a/app/screens/edit_profile/index.js b/app/screens/edit_profile/index.js
new file mode 100644
index 000000000..7ec938af1
--- /dev/null
+++ b/app/screens/edit_profile/index.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+
+import {getConfig} from 'mattermost-redux/selectors/entities/general';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import {updateUser, handleUploadProfileImage} from 'app/actions/views/edit_profile';
+
+import EditProfile from './edit_profile';
+
+function mapStateToProps(state) {
+ return {
+ config: getConfig(state),
+ theme: getTheme(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ handleUploadProfileImage,
+ updateUser
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EditProfile);
diff --git a/app/screens/index.js b/app/screens/index.js
index fcda5bd0a..6946f9111 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -11,13 +11,14 @@ import AdvancedSettings from 'app/screens/settings/advanced_settings';
import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
-import EditChannel from 'app/screens/edit_channel';
import ChannelMembers from 'app/screens/channel_members';
import ChannelPeek from 'app/screens/channel_peek';
import ClientUpgrade from 'app/screens/client_upgrade';
import Code from 'app/screens/code';
import CreateChannel from 'app/screens/create_channel';
+import EditChannel from 'app/screens/edit_channel';
import EditPost from 'app/screens/edit_post';
+import EditProfile from 'app/screens/edit_profile';
import ImagePreview from 'app/screens/image_preview';
import LoadTeam from 'app/screens/load_team';
import Login from 'app/screens/login';
@@ -64,13 +65,14 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel, false), store, Provider);
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
- Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(EditChannel), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(ChannelPeek), store, Provider);
Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(ClientUpgrade), store, Provider);
Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider);
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);
+ Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(EditChannel), store, Provider);
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
+ Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(EditProfile), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
Navigation.registerComponent('LoadTeam', () => wrapWithContextProvider(LoadTeam, false), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider);
@@ -78,13 +80,13 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider);
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider);
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider);
- Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification), store, Provider);
Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(NotificationSettings), store, Provider);
Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(NotificationSettingsEmail), store, Provider);
Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(NotificationSettingsMentions), store, Provider);
Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(NotificationSettingsMentionsKeywords), store, Provider);
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(NotificationSettingsMobile), store, Provider);
+ Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
Navigation.registerComponent('Root', () => Root, store, Provider);
Navigation.registerComponent('Search', () => wrapWithContextProvider(Search), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
diff --git a/app/utils/file.js b/app/utils/file.js
index 086b69665..6fd6439cb 100644
--- a/app/utils/file.js
+++ b/app/utils/file.js
@@ -2,6 +2,7 @@
// See License.txt for license information.
import RNFetchBlob from 'react-native-fetch-blob';
+import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import {DeviceTypes} from 'app/constants/';
const {DOCUMENTS_PATH, VIDEOS_PATH} = DeviceTypes;
@@ -53,3 +54,24 @@ export async function deleteFileCache() {
await RNFetchBlob.fs.unlink(VIDEOS_PATH);
return true;
}
+
+export function buildFileUploadData(file) {
+ const re = /heic/i;
+ const uri = file.uri;
+ let name = file.fileName || file.path || file.uri;
+ let mimeType = lookupMimeType(name);
+ let extension = name.split('.').pop().replace('.', '');
+
+ if (re.test(extension)) {
+ extension = 'JPG';
+ name = name.replace(re, 'jpg');
+ mimeType = 'image/jpeg';
+ }
+
+ return {
+ uri,
+ name,
+ type: mimeType,
+ extension
+ };
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index c99efb83f..69fa2f9be 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2098,6 +2098,7 @@
"mobile.routes.channels": "Channels",
"mobile.routes.code": "{language} Code",
"mobile.routes.code.noLanguage": "Code",
+ "mobile.routes.edit_profile": "Edit Profile",
"mobile.routes.enterServerUrl": "Enter Server URL",
"mobile.routes.login": "Login",
"mobile.routes.loginOptions": "Login Chooser",
@@ -2136,6 +2137,7 @@
"mobile.video.save_error_title": "Save Video Error",
"mobile.video_playback.failed_description": "An error occurred while trying to play the video.\n",
"mobile.video_playback.failed_title": "Video playback failed",
+ "mobile.account.settings.save": "Save",
"modal.manaul_status.ask": "Do not ask me again",
"modal.manaul_status.button": "Yes, set my status to \"Online\"",
"modal.manaul_status.message": "Would you like to switch your status to \"Online\"?",