import React from 'react'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import snackbar from 'soapbox/actions/snackbar'; import Column from '../ui/components/column'; import { SimpleForm, FieldsGroup, TextInput, Checkbox, FileChooser, SimpleTextarea, } from 'soapbox/features/forms'; import ProfilePreview from './components/profile_preview'; import { Map as ImmutableMap, List as ImmutableList, } from 'immutable'; import { patchMe } from 'soapbox/actions/me'; import { updateNotificationSettings } from 'soapbox/actions/accounts'; import { unescape } from 'lodash'; import { isVerified } from 'soapbox/utils/accounts'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { getFeatures } from 'soapbox/utils/features'; const hidesNetwork = account => { const pleroma = account.get('pleroma'); if (!pleroma) return false; const { hide_followers, hide_follows, hide_followers_count, hide_follows_count } = pleroma.toJS(); return hide_followers && hide_follows && hide_followers_count && hide_follows_count; }; const messages = defineMessages({ heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' }, metaFieldLabel: { id: 'edit_profile.fields.meta_fields.label_placeholder', defaultMessage: 'Label' }, metaFieldContent: { id: 'edit_profile.fields.meta_fields.content_placeholder', defaultMessage: 'Content' }, verified: { id: 'edit_profile.fields.verified_display_name', defaultMessage: 'Verified users may not update their display name' }, }); const mapStateToProps = state => { const me = state.get('me'); const soapbox = getSoapboxConfig(state); const meta = state.getIn(['meta', 'pleroma']); const account = state.getIn(['accounts', me]).set('pleroma', meta); return { account, maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4), verifiedCanEditName: soapbox.get('verifiedCanEditName'), supportsEmailList: getFeatures(state.get('instance')).emailList, }; }; // Forces fields to be maxFields size, filling empty values const normalizeFields = (fields, maxFields) => ( ImmutableList(fields).setSize(maxFields).map(field => field ? field : ImmutableMap({ name: '', value: '' }), ) ); // HTML unescape for special chars, eg const unescapeParams = (map, params) => ( params.reduce((map, param) => ( map.set(param, unescape(map.get(param))) ), map) ); export default @connect(mapStateToProps) @injectIntl class EditProfile extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, account: ImmutablePropTypes.map, maxFields: PropTypes.number, verifiedCanEditName: PropTypes.bool, }; state = { isLoading: false, } constructor(props) { super(props); const { account } = this.props; const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']); const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']); const initialState = account.withMutations(map => { map.merge(map.get('source')); map.delete('source'); map.set('fields', normalizeFields(map.get('fields'), props.maxFields)); map.set('stranger_notifications', strangerNotifications); map.set('accepts_email_list', acceptsEmailList); map.set('hide_network', hidesNetwork(account)); unescapeParams(map, ['display_name', 'bio']); }); this.state = initialState.toObject(); } makePreviewAccount = () => { const { account } = this.props; return account.merge(ImmutableMap({ header: this.state.header, avatar: this.state.avatar, display_name: this.state.display_name, })); } getFieldParams = () => { let params = ImmutableMap(); this.state.fields.forEach((f, i) => params = params .set(`fields_attributes[${i}][name]`, f.get('name')) .set(`fields_attributes[${i}][value]`, f.get('value')), ); return params; } getParams = () => { const { state } = this; return Object.assign({ discoverable: state.discoverable, bot: state.bot, display_name: state.display_name, note: state.note, avatar: state.avatar_file, header: state.header_file, locked: state.locked, accepts_email_list: state.accepts_email_list, hide_followers: state.hide_network, hide_follows: state.hide_network, hide_followers_count: state.hide_network, hide_follows_count: state.hide_network, }, this.getFieldParams().toJS()); } getFormdata = () => { const data = this.getParams(); let formData = new FormData(); for (let key in data) { // Compact the submission. This should probably be done better. const shouldAppend = Boolean(data[key] !== undefined || key.startsWith('fields_attributes')); if (shouldAppend) formData.append(key, data[key] || ''); } return formData; } handleSubmit = (event) => { const { dispatch } = this.props; const credentials = dispatch(patchMe(this.getFormdata())); const notifications = dispatch(updateNotificationSettings({ block_from_strangers: this.state.stranger_notifications || false, })); this.setState({ isLoading: true }); Promise.all([credentials, notifications]).then(() => { this.setState({ isLoading: false }); dispatch(snackbar.success('Profile saved!')); }).catch((error) => { this.setState({ isLoading: false }); }); event.preventDefault(); } handleCheckboxChange = e => { this.setState({ [e.target.name]: e.target.checked }); } handleTextChange = e => { this.setState({ [e.target.name]: e.target.value }); } handleFieldChange = (i, key) => { return (e) => { this.setState({ fields: this.state.fields.setIn([i, key], e.target.value), }); }; } handleFileChange = e => { const { name } = e.target; const [file] = e.target.files || []; const url = file ? URL.createObjectURL(file) : this.state[name]; this.setState({ [name]: url, [`${name}_file`]: file, }); } render() { const { intl, maxFields, account, verifiedCanEditName, supportsEmailList } = this.props; const verified = isVerified(account); const canEditName = verifiedCanEditName || !verified; return ( } name='display_name' value={this.state.display_name} onChange={this.handleTextChange} disabled={!canEditName} hint={!canEditName && intl.formatMessage(messages.verified)} /> } name='note' autoComplete='off' value={this.state.note} wrap='hard' onChange={this.handleTextChange} rows={3} /> } name='header' hint={} onChange={this.handleFileChange} /> } name='avatar' hint={} onChange={this.handleFileChange} /> } hint={} name='locked' checked={this.state.locked} onChange={this.handleCheckboxChange} /> } hint={} name='hide_network' checked={this.state.hide_network} onChange={this.handleCheckboxChange} /> } hint={} name='bot' checked={this.state.bot} onChange={this.handleCheckboxChange} /> } hint={} name='stranger_notifications' checked={this.state.stranger_notifications} onChange={this.handleCheckboxChange} /> {supportsEmailList && } hint={} name='accepts_email_list' checked={this.state.accepts_email_list} onChange={this.handleCheckboxChange} />} { this.state.fields.map((field, i) => ( )) } ); } }