From 7cda6be8d73e67b03f203c5bb6585cd3afbac9e7 Mon Sep 17 00:00:00 2001 From: enahum Date: Mon, 8 Jan 2018 14:18:57 -0300 Subject: [PATCH] Edit profile (#1322) * Add set status feature Small code refactoring Add Do Not Disturb option * Add Account Settings screen Add username, nicname, and position fields Add email field get deviceHeight from redux Clean up onPress handler in Settings Refactor each account settings item to it's own component Clean up email field and updateUser action method Use mobile localization format in en.json Update profile image Clean up ProfilePicture for editing Add loading and error components in AccountSettings Refactor updateRequest state into a function Follow design specs Remove unnecessary this.state.emailUpdated Polish AccountSettings and secondary components Refactor AttachmentButton into it's own componentg Add Account Settings icon in General Settings Add TODO comments * Update user status * Edit Profile screen refactored * Fix edit profile item text color --- app/actions/views/edit_profile.js | 25 + app/actions/views/file_upload.js | 36 +- .../components => }/attachment_button.js | 26 +- app/components/error_text.js | 8 +- app/components/post_textbox/post_textbox.js | 2 +- .../profile_picture/profile_picture.js | 48 +- .../settings_drawer/settings_drawer.js | 34 +- app/screens/edit_profile/edit_profile.js | 528 ++++++++++++++++++ app/screens/edit_profile/edit_profile_item.js | 129 +++++ app/screens/edit_profile/index.js | 30 + app/screens/index.js | 8 +- app/utils/file.js | 22 + assets/base/i18n/en.json | 2 + 13 files changed, 845 insertions(+), 53 deletions(-) create mode 100644 app/actions/views/edit_profile.js rename app/components/{post_textbox/components => }/attachment_button.js (94%) create mode 100644 app/screens/edit_profile/edit_profile.js create mode 100644 app/screens/edit_profile/edit_profile_item.js create mode 100644 app/screens/edit_profile/index.js 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\"?",