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
This commit is contained in:
parent
0052b46c54
commit
7cda6be8d7
13 changed files with 845 additions and 53 deletions
25
app/actions/views/edit_profile.js
Normal file
25
app/actions/views/edit_profile.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
@ -6,6 +6,7 @@ import React, {PureComponent} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalStyles} from 'app/styles';
|
||||
|
|
@ -14,11 +15,12 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
|||
class ErrorText extends PureComponent {
|
||||
static propTypes = {
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {error, theme} = this.props;
|
||||
const {error, textStyle, theme} = this.props;
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -32,13 +34,13 @@ class ErrorText extends PureComponent {
|
|||
id={intl.id}
|
||||
defaultMessage={intl.defaultMessage}
|
||||
values={intl.values}
|
||||
style={[GlobalStyles.errorLabel, style.errorLabel]}
|
||||
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={[GlobalStyles.errorLabel, style.errorLabel]}>
|
||||
<Text style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}>
|
||||
{error.message || error}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<UserStatus
|
||||
size={this.props.statusSize}
|
||||
status={this.props.status}
|
||||
/>
|
||||
);
|
||||
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 = (
|
||||
<FontAwesomeIcon
|
||||
name='camera'
|
||||
size={this.props.statusSize / 1.7}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.status && !edit) {
|
||||
statusIcon = (
|
||||
<UserStatus
|
||||
size={this.props.statusSize}
|
||||
status={this.props.status}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
|
||||
|
|
@ -68,8 +94,8 @@ export default class ProfilePicture extends PureComponent {
|
|||
source={{uri: pictureUrl}}
|
||||
defaultSource={placeholder}
|
||||
/>
|
||||
{this.props.showStatus &&
|
||||
<View style={[style.statusWrapper, {borderRadius: this.props.statusSize / 2}]}>
|
||||
{(showStatus || edit) &&
|
||||
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>
|
||||
{statusIcon}
|
||||
</View>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<UserInfo user={currentUser}/>
|
||||
<UserInfo
|
||||
onPress={this.goToEditProfile}
|
||||
user={currentUser}
|
||||
/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
labelComponent={this.renderUserStatusLabel(currentUser.id)}
|
||||
|
|
|
|||
528
app/screens/edit_profile/edit_profile.js
Normal file
528
app/screens/edit_profile/edit_profile.js
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
// 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 {intlShape} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import StatusBar from 'app/components/status_bar/index';
|
||||
import ProfilePicture from 'app/components/profile_picture/index';
|
||||
import AttachmentButton from 'app/components/attachment_button';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import EditProfileItem from './edit_profile_item';
|
||||
|
||||
const holders = {
|
||||
firstName: {
|
||||
id: 'user.settings.general.firstName',
|
||||
defaultMessage: 'First Name'
|
||||
},
|
||||
lastName: {
|
||||
id: 'user.settings.general.lastName',
|
||||
defaultMessage: 'Last Name'
|
||||
},
|
||||
username: {
|
||||
id: 'user.settings.general.username',
|
||||
defaultMessage: 'Username'
|
||||
},
|
||||
nickname: {
|
||||
id: 'user.settings.general.nickname',
|
||||
defaultMessage: 'Nickname'
|
||||
},
|
||||
position: {
|
||||
id: 'user.settings.general.position',
|
||||
defaultMessage: 'Position'
|
||||
},
|
||||
email: {
|
||||
id: 'user.settings.general.email',
|
||||
defaultMessage: 'Email'
|
||||
}
|
||||
};
|
||||
|
||||
export default class EditProfile extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
handleUploadProfileImage: PropTypes.func.isRequired,
|
||||
updateUser: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
rightButton = {
|
||||
id: 'update-profile',
|
||||
disabled: true,
|
||||
showAsAction: 'always'
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const {email, first_name: firstName, last_name: lastName, nickname, position, username} = props.currentUser;
|
||||
const buttons = {
|
||||
rightButtons: [this.rightButton]
|
||||
};
|
||||
|
||||
this.rightButton.title = context.intl.formatMessage({id: 'mobile.account.settings.save', defaultMessage: 'Save'});
|
||||
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
|
||||
props.navigator.setButtons(buttons);
|
||||
|
||||
this.state = {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
nickname,
|
||||
position,
|
||||
username
|
||||
};
|
||||
}
|
||||
|
||||
canUpdate = (updatedField) => {
|
||||
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 (
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='firstName'
|
||||
format={holders.firstName}
|
||||
helpText={formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
|
||||
})}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={firstName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View>
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='lastName'
|
||||
format={holders.lastName}
|
||||
helpText={formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
|
||||
})}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={lastName}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderUsernameSettings = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {currentUser, theme} = this.props;
|
||||
const {username} = this.state;
|
||||
const disabled = currentUser.auth_service !== '';
|
||||
|
||||
return (
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='username'
|
||||
format={holders.username}
|
||||
helpText={formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
|
||||
})}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={username}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View>
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='email'
|
||||
format={holders.email}
|
||||
helpText={helpText}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={email}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='nickname'
|
||||
format={holders.nickname}
|
||||
helpText={formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
|
||||
})}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={nickname}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<EditProfileItem
|
||||
disabled={disabled}
|
||||
field='position'
|
||||
format={holders.position}
|
||||
helpText={formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
|
||||
})}
|
||||
updateValue={this.updateField}
|
||||
theme={theme}
|
||||
value={position}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={[style.container, style.flex]}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<View style={style.errorContainer}>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText
|
||||
error={error}
|
||||
textStyle={style.errorText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.flex}>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
innerRef={this.scrollViewRef}
|
||||
style={style.container}
|
||||
>
|
||||
{displayError}
|
||||
<View style={[style.scrollView]}>
|
||||
<View style={style.top}>
|
||||
<AttachmentButton
|
||||
blurTextBox={emptyFunction}
|
||||
theme={theme}
|
||||
navigator={navigator}
|
||||
wrapper={true}
|
||||
uploadFiles={this.handleUploadProfileImage}
|
||||
>
|
||||
<ProfilePicture
|
||||
userId={currentUser.id}
|
||||
size={150}
|
||||
statusBorderWidth={6}
|
||||
statusSize={40}
|
||||
edit={true}
|
||||
imageUri={uri}
|
||||
/>
|
||||
</AttachmentButton>
|
||||
</View>
|
||||
{this.renderFirstNameSettings()}
|
||||
<View style={style.separator}/>
|
||||
{this.renderLastNameSettings()}
|
||||
<View style={style.separator}/>
|
||||
{this.renderUsernameSettings()}
|
||||
<View style={style.separator}/>
|
||||
{this.renderEmailSettings()}
|
||||
<View style={style.separator}/>
|
||||
{this.renderNicknameSettings()}
|
||||
<View style={style.separator}/>
|
||||
{this.renderPositionSettings()}
|
||||
<View style={style.footer}/>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
129
app/screens/edit_profile/edit_profile_item.js
Normal file
129
app/screens/edit_profile/edit_profile_item.js
Normal file
|
|
@ -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 (
|
||||
<View>
|
||||
<View style={style.titleContainer15}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id={format.id}
|
||||
defaultMessage={format.defaultMessage}
|
||||
/>
|
||||
{optional && (
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInput
|
||||
ref={this.channelNameRef}
|
||||
value={value}
|
||||
onChangeText={this.onChangeText}
|
||||
style={[style.input, disabled ? style.disabled : null]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
editable={!disabled}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
{disabled &&
|
||||
<Text style={style.helpText}>
|
||||
{helpText}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
30
app/screens/edit_profile/index.js
Normal file
30
app/screens/edit_profile/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\"?",
|
||||
|
|
|
|||
Loading…
Reference in a new issue