diff --git a/app/actions/views/edit_profile.js b/app/actions/views/edit_profile.js
index fc5868cb1..3e91b7be5 100644
--- a/app/actions/views/edit_profile.js
+++ b/app/actions/views/edit_profile.js
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {updateMe} from 'mattermost-redux/actions/users';
+import {updateMe, setDefaultProfileImage} from 'mattermost-redux/actions/users';
import {ViewTypes} from 'app/constants';
@@ -25,7 +25,15 @@ export function setProfileImageUri(imageUri = '') {
};
}
+export function removeProfileImage(user) {
+ return async (dispatch) => {
+ const result = await dispatch(setDefaultProfileImage(user));
+ return result;
+ };
+}
+
export default {
updateUser,
setProfileImageUri,
+ removeProfileImage,
};
diff --git a/app/components/__snapshots__/profile_picture_button.test.js.snap b/app/components/__snapshots__/profile_picture_button.test.js.snap
new file mode 100644
index 000000000..8f915b597
--- /dev/null
+++ b/app/components/__snapshots__/profile_picture_button.test.js.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`profile_picture_button should match snapshot 1`] = `
+
+`;
diff --git a/app/components/attachment_button.js b/app/components/attachment_button.js
index eddf52718..bc1a1d33b 100644
--- a/app/components/attachment_button.js
+++ b/app/components/attachment_button.js
@@ -42,6 +42,7 @@ export default class AttachmentButton extends PureComponent {
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
+ extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
@@ -52,6 +53,7 @@ export default class AttachmentButton extends PureComponent {
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
+ extraOptions: null,
};
static contextTypes = {
@@ -351,6 +353,7 @@ export default class AttachmentButton extends PureComponent {
fileCount,
maxFileCount,
onShowFileMaxWarning,
+ extraOptions,
} = this.props;
if (fileCount === maxFileCount) {
@@ -416,6 +419,14 @@ export default class AttachmentButton extends PureComponent {
});
}
+ if (extraOptions) {
+ extraOptions.forEach((option) => {
+ if (option !== null) {
+ items.push(option);
+ }
+ });
+ }
+
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
@@ -479,4 +490,3 @@ const style = StyleSheet.create({
justifyContent: 'center',
},
});
-
diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js
index 6324e33ab..46ba37496 100644
--- a/app/components/profile_picture/profile_picture.js
+++ b/app/components/profile_picture/profile_picture.js
@@ -31,6 +31,7 @@ export default class ProfilePicture extends PureComponent {
edit: PropTypes.bool,
imageUri: PropTypes.string,
profileImageUri: PropTypes.string,
+ profileImageRemove: PropTypes.bool,
theme: PropTypes.object.isRequired,
actions: PropTypes.shape({
getStatusForId: PropTypes.func.isRequired,
@@ -47,6 +48,7 @@ export default class ProfilePicture extends PureComponent {
state = {
pictureUrl: null,
+ otherImageProps: {},
};
componentDidMount() {
@@ -97,15 +99,34 @@ export default class ProfilePicture extends PureComponent {
}
};
+ showDefaultImage = () => {
+ if (this.mounted) {
+ this.setState({otherImageProps: {defaultSource: placeholder}});
+ }
+ };
+
clearProfileImageUri = () => {
if (this.props.isCurrentUser && this.props.profileImageUri !== '') {
this.props.actions.setProfileImageUri('');
}
}
+ componentDidUpdate(prevProps, prevState) {
+ if (!this.props.edit) {
+ if (this.state.otherImageProps !== prevState.otherImageProps) {
+ this.showDefaultImage();
+ }
+ }
+
+ if (this.props.profileImageRemove !== prevProps.profileImageRemove) {
+ this.setImageURL(null);
+ this.showDefaultImage();
+ }
+ }
+
render() {
const {edit, showStatus, theme} = this.props;
- const {pictureUrl} = this.state;
+ const {pictureUrl, otherImageProps} = this.state;
const style = getStyleSheet(theme);
let statusIcon;
@@ -146,11 +167,6 @@ export default class ProfilePicture extends PureComponent {
};
}
- const otherImageProps = {};
- if (!this.props.edit) {
- otherImageProps.defaultSource = placeholder;
- }
-
return (
{
+ let action = null;
+ const {removeProfileImage} = this.props;
+ const {id, last_picture_update: lastPictureUpdate} = this.props.currentUser;
+
+ const profileImageUrl = Client4.getProfilePictureUrl(id, lastPictureUpdate);
+
+ if (removeProfileImage !== null) {
+ action = removeProfileImage;
+ }
+
+ // Check if image url includes query string for timestamp.
+ // If so, it means the image has been updated from the default
+ // i.e. '.../image?_=1544159746868'
+ if (profileImageUrl.includes('?')) {
+ return {
+ action,
+ text: {
+ id: t('mobile.edit_profile.remove_profile_photo'),
+ defaultMessage: 'Remove Photo',
+ },
+ textStyle: {
+ color: '#CC3239',
+ },
+ icon: 'trash',
+ iconStyle: {
+ color: '#CC3239',
+ },
+ };
+ }
+ return null;
+ }
+
+ render() {
+ const {children, ...props} = this.props;
+ const {extraOptions} = this.state;
+
+ // Avoid passing unneeded props
+ Reflect.deleteProperty(props, 'currentUser');
+
+ return (
+
+ {children}
+
+ );
+ }
+}
diff --git a/app/components/profile_picture_button.test.js b/app/components/profile_picture_button.test.js
new file mode 100644
index 000000000..a10138e75
--- /dev/null
+++ b/app/components/profile_picture_button.test.js
@@ -0,0 +1,66 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import Preferences from 'mattermost-redux/constants/preferences';
+
+import ProfilePictureButton from './profile_picture_button.js';
+
+import {Client4} from 'mattermost-redux/client';
+
+describe('profile_picture_button', () => {
+ const navigator = {
+ setOnNavigatorEvent: jest.fn(),
+ setButtons: jest.fn(),
+ dismissModal: jest.fn(),
+ push: jest.fn(),
+ };
+
+ const baseProps = {
+ theme: Preferences.THEMES.default,
+ navigator,
+ currentUser: {
+ first_name: 'Dwight',
+ last_name: 'Schrute',
+ username: 'ieatbeets',
+ email: 'dwight@schrutefarms.com',
+ nickname: 'Dragon',
+ position: 'position',
+ },
+ blurTextBox: jest.fn(),
+ maxFileSize: 20 * 1024 * 1024,
+ uploadFiles: jest.fn(),
+ };
+
+ test('should match snapshot', async () => {
+ const wrapper = shallow(
+ ,
+ );
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should NOT return option to remove when profile picture is default', () => {
+ Client4.getProfilePictureUrl = jest.fn(() => 'image.png');
+ const wrapper = shallow(
+ ,
+ );
+ const instance = wrapper.instance();
+
+ // test default image (WITHOUT query param)
+ instance.getRemoveProfileImageOption();
+ expect(wrapper.state('extraOptions')).toEqual([null]);
+ });
+
+ test('should return option to remove profile picture if customized', () => {
+ Client4.getProfilePictureUrl = jest.fn(() => 'image.png?query');
+ const wrapper = shallow(
+ ,
+ );
+ const instance = wrapper.instance();
+
+ // test custom image (WITH query param)
+ instance.getRemoveProfileImageOption();
+ expect(wrapper.state('extraOptions')).not.toEqual([null]);
+ });
+});
diff --git a/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap
new file mode 100644
index 000000000..bd83dd9f0
--- /dev/null
+++ b/app/screens/edit_profile/__snapshots__/edit_profile.test.js.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`edit_profile should match snapshot 1`] = `
+
+
+
+
+
+`;
diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js
index 5ca9a7f34..fa9e40bb3 100644
--- a/app/screens/edit_profile/edit_profile.js
+++ b/app/screens/edit_profile/edit_profile.js
@@ -21,8 +21,8 @@ import TextSetting from 'app/components/widgets/settings/text_setting';
import Loading from 'app/components/loading';
import ErrorText from 'app/components/error_text';
import StatusBar from 'app/components/status_bar/index';
+import ProfilePictureButton from 'app/components/profile_picture_button';
import ProfilePicture from 'app/components/profile_picture';
-import AttachmentButton from 'app/components/attachment_button';
import mattermostBucket from 'app/mattermost_bucket';
import LocalConfig from 'assets/config';
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
@@ -59,6 +59,7 @@ export default class EditProfile extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
setProfileImageUri: PropTypes.func.isRequired,
+ removeProfileImage: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
}).isRequired,
config: PropTypes.object.isRequired,
@@ -157,6 +158,7 @@ export default class EditProfile extends PureComponent {
const {
profileImage,
+ profileImageRemove,
firstName,
lastName,
username,
@@ -172,13 +174,17 @@ export default class EditProfile extends PureComponent {
position,
email,
};
- const {actions} = this.props;
+ const {actions, currentUser} = this.props;
if (profileImage) {
actions.setProfileImageUri(profileImage.uri);
this.uploadProfileImage().catch(this.handleUploadError);
}
+ if (profileImageRemove) {
+ actions.removeProfileImage(currentUser.id);
+ }
+
if (this.canUpdate()) {
const {error} = await actions.updateUser(user);
if (error) {
@@ -196,6 +202,14 @@ export default class EditProfile extends PureComponent {
this.emitCanUpdateAccount(true);
};
+ handleRemoveProfileImage = () => {
+ this.setState({profileImageRemove: true});
+ this.emitCanUpdateAccount(true);
+ this.props.navigator.dismissModal({
+ animationType: 'none',
+ });
+ }
+
uploadProfileImage = async () => {
const {profileImage} = this.state;
const {currentUser} = this.props;
@@ -452,7 +466,7 @@ export default class EditProfile extends PureComponent {
this.scrollView = ref;
};
- render() {
+ renderProfilePicture = () => {
const {
currentUser,
theme,
@@ -461,12 +475,51 @@ export default class EditProfile extends PureComponent {
const {
profileImage,
+ profileImageRemove,
+ } = this.state;
+
+ const style = getStyleSheet(theme);
+ const uri = profileImage ? profileImage.uri : null;
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ render() {
+ const {theme} = this.props;
+
+ const {
error,
updating,
} = this.state;
const style = getStyleSheet(theme);
- const uri = profileImage ? profileImage.uri : null;
if (updating) {
return (
@@ -501,29 +554,7 @@ export default class EditProfile extends PureComponent {
>
{displayError}
-
-
-
-
-
+ {this.renderProfilePicture()}
{this.renderFirstNameSettings()}
{this.renderLastNameSettings()}
diff --git a/app/screens/edit_profile/edit_profile.test.js b/app/screens/edit_profile/edit_profile.test.js
new file mode 100644
index 000000000..249fedf10
--- /dev/null
+++ b/app/screens/edit_profile/edit_profile.test.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import Preferences from 'mattermost-redux/constants/preferences';
+
+import EditProfile from './edit_profile.js';
+
+jest.mock('react-intl');
+jest.mock('app/utils/theme', () => {
+ const original = require.requireActual('app/utils/theme');
+ return {
+ ...original,
+ changeOpacity: jest.fn(),
+ };
+});
+
+describe('edit_profile', () => {
+ const navigator = {
+ setOnNavigatorEvent: jest.fn(),
+ setButtons: jest.fn(),
+ dismissModal: jest.fn(),
+ push: jest.fn(),
+ };
+
+ const actions = {
+ updateUser: jest.fn(),
+ setProfileImageUri: jest.fn(),
+ removeProfileImage: jest.fn(),
+ };
+
+ const baseProps = {
+ actions,
+ config: {
+ ShowEmailAddress: true,
+ },
+ theme: Preferences.THEMES.default,
+ navigator,
+ currentUser: {
+ first_name: 'Dwight',
+ last_name: 'Schrute',
+ username: 'ieatbeets',
+ email: 'dwight@schrutefarms.com',
+ nickname: 'Dragon',
+ position: 'position',
+ },
+ };
+
+ test('should match snapshot', async () => {
+ const wrapper = shallow(
+ ,
+ {context: {intl: {formatMessage: jest.fn()}}},
+ );
+ expect(wrapper.instance().renderProfilePicture()).toMatchSnapshot();
+ });
+
+ test('should match state on handleRemoveProfileImage', () => {
+ const newNavigator = {
+ dismissModal: jest.fn(),
+ setOnNavigatorEvent: jest.fn(),
+ setButtons: jest.fn(),
+ };
+ const wrapper = shallow(
+ ,
+ {context: {intl: {formatMessage: jest.fn()}}},
+ );
+ wrapper.setProps({profileImageRemove: false});
+
+ const instance = wrapper.instance();
+ instance.emitCanUpdateAccount = jest.fn();
+ instance.handleRemoveProfileImage();
+
+ expect(wrapper.state('profileImageRemove')).toEqual(true);
+ expect(instance.emitCanUpdateAccount).toHaveBeenCalledTimes(1);
+ expect(instance.emitCanUpdateAccount).toBeCalledWith(true);
+
+ expect(newNavigator.dismissModal).toHaveBeenCalledTimes(1);
+ expect(newNavigator.dismissModal).toBeCalledWith({animationType: 'none'});
+ });
+});
diff --git a/app/screens/edit_profile/index.js b/app/screens/edit_profile/index.js
index 6986c7d4c..50f3eff2c 100644
--- a/app/screens/edit_profile/index.js
+++ b/app/screens/edit_profile/index.js
@@ -7,7 +7,7 @@ import {bindActionCreators} from 'redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {setProfileImageUri, updateUser} from 'app/actions/views/edit_profile';
+import {setProfileImageUri, removeProfileImage, updateUser} from 'app/actions/views/edit_profile';
import EditProfile from './edit_profile';
@@ -22,6 +22,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setProfileImageUri,
+ removeProfileImage,
updateUser,
}, dispatch),
};
diff --git a/app/screens/options_modal/options_modal_list.android.js b/app/screens/options_modal/options_modal_list.android.js
index 5602d00ac..5c02d1c20 100644
--- a/app/screens/options_modal/options_modal_list.android.js
+++ b/app/screens/options_modal/options_modal_list.android.js
@@ -41,6 +41,11 @@ export default class OptionsModalList extends PureComponent {
const options = items.map((item, index) => {
let textComponent;
+ let optionIconStyle = style.optionIcon;
+ if (typeof item.iconStyle !== 'undefined') {
+ optionIconStyle = item.iconStyle;
+ }
+
if (item.text.hasOwnProperty('id')) {
textComponent = (
}
diff --git a/app/screens/options_modal/options_modal_list.ios.js b/app/screens/options_modal/options_modal_list.ios.js
index 50ce66556..30a0b3447 100644
--- a/app/screens/options_modal/options_modal_list.ios.js
+++ b/app/screens/options_modal/options_modal_list.ios.js
@@ -45,6 +45,11 @@ export default class OptionsModalList extends PureComponent {
const options = items.map((item, index) => {
let textComponent;
+ let optionIconStyle = style.optionIcon;
+ if (typeof item.iconStyle !== 'undefined') {
+ optionIconStyle = item.iconStyle;
+ }
+
if (item.text.hasOwnProperty('id')) {
textComponent = (
}