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\"?",