diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index e04001e4c..0d75025c2 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -46,11 +46,18 @@ export function fetchMe() { }; } -export function patchMe(params) { +export function patchMe(params, formData = false) { return (dispatch, getState) => { dispatch(patchMeRequest()); + + const options = formData ? { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } : {}; + return api(getState) - .patch('/api/v1/accounts/update_credentials', params) + .patch('/api/v1/accounts/update_credentials', params, options) .then(response => { dispatch(patchMeSuccess(response.data)); }).catch(error => { diff --git a/app/soapbox/components/ui/form-group/form-group.tsx b/app/soapbox/components/ui/form-group/form-group.tsx index bd8a078a1..c0b587526 100644 --- a/app/soapbox/components/ui/form-group/form-group.tsx +++ b/app/soapbox/components/ui/form-group/form-group.tsx @@ -2,8 +2,8 @@ import React, { useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; interface IFormGroup { - hintText?: string | React.ReactNode, - labelText: string, + hintText?: React.ReactNode, + labelText: React.ReactNode, errors?: string[] } diff --git a/app/soapbox/components/ui/tabs/tabs.tsx b/app/soapbox/components/ui/tabs/tabs.tsx index a0dfe7abb..dbcbcbe33 100644 --- a/app/soapbox/components/ui/tabs/tabs.tsx +++ b/app/soapbox/components/ui/tabs/tabs.tsx @@ -92,7 +92,7 @@ const AnimatedTab: React.FC = ({ index, ...props }) => { }; type Item = { - text: string, + text: React.ReactNode, title?: string, href?: string, to?: string, diff --git a/app/soapbox/components/ui/textarea/textarea.tsx b/app/soapbox/components/ui/textarea/textarea.tsx index 81a8488b8..b0b72573a 100644 --- a/app/soapbox/components/ui/textarea/textarea.tsx +++ b/app/soapbox/components/ui/textarea/textarea.tsx @@ -8,6 +8,7 @@ interface ITextarea extends Pick ({ - displayFqn: displayFqn(state), -}); - -const ProfilePreview = ({ account, displayFqn }) => ( -
- -
- -
-
-
- -
-
- {account.get('username')} - - - {account.get('display_name')} - {account.get('verified') && } - - - @{getAcct(account, displayFqn)} -
-
- -
-); - -ProfilePreview.propTypes = { - account: ImmutablePropTypes.record, - displayFqn: PropTypes.bool, -}; - -export default connect(mapStateToProps)(ProfilePreview); diff --git a/app/soapbox/features/edit_profile/components/profile_preview.tsx b/app/soapbox/features/edit_profile/components/profile_preview.tsx new file mode 100644 index 000000000..e750bb273 --- /dev/null +++ b/app/soapbox/features/edit_profile/components/profile_preview.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import StillImage from 'soapbox/components/still_image'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import { useSoapboxConfig } from 'soapbox/hooks'; + +import type { Account } from 'soapbox/types/entities'; + +interface IProfilePreview { + account: Account, +} + +/** Displays a preview of the user's account, including avatar, banner, etc. */ +const ProfilePreview: React.FC = ({ account }) => { + const { displayFqn } = useSoapboxConfig(); + + return ( +
+ +
+ +
+
+
+ +
+
+ {account.username} + + + {account.display_name} + {account.verified && } + + + @{displayFqn ? account.fqn : account.acct} +
+
+ +
+ ); +}; + +export default ProfilePreview; diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js deleted file mode 100644 index fe0c66882..000000000 --- a/app/soapbox/features/edit_profile/index.js +++ /dev/null @@ -1,436 +0,0 @@ -import { - Map as ImmutableMap, - List as ImmutableList, -} from 'immutable'; -import { unescape } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -// import { updateNotificationSettings } from 'soapbox/actions/accounts'; -import { patchMe } from 'soapbox/actions/me'; -import snackbar from 'soapbox/actions/snackbar'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -// import Icon from 'soapbox/components/icon'; -import { - Checkbox, -} from 'soapbox/features/forms'; -import { makeGetAccount } from 'soapbox/selectors'; -import { getFeatures } from 'soapbox/utils/features'; -import resizeImage from 'soapbox/utils/resize_image'; - -import { Button, Column, Form, FormActions, FormGroup, Input, Textarea } from '../../components/ui'; - -import ProfilePreview from './components/profile_preview'; - -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' }, - header: { id: 'edit_profile.header', 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' }, - success: { id: 'edit_profile.success', defaultMessage: 'Profile saved!' }, - error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' }, - bioPlaceholder: { id: 'edit_profile.fields.bio_placeholder', defaultMessage: 'Tell us about yourself.' }, - displayNamePlaceholder: { id: 'edit_profile.fields.display_name_placeholder', defaultMessage: 'Name' }, - websitePlaceholder: { id: 'edit_profile.fields.website_placeholder', defaultMessage: 'Display a Link' }, - locationPlaceholder: { id: 'edit_profile.fields.location_placeholder', defaultMessage: 'Location' }, - cancel: { id: 'common.cancel', defaultMessage: 'Cancel' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = state => { - const me = state.get('me'); - const account = getAccount(state, me); - const soapbox = getSoapboxConfig(state); - const features = getFeatures(state.instance); - - return { - account, - features, - maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4), - verifiedCanEditName: soapbox.get('verifiedCanEditName'), - }; - }; - - return mapStateToProps; -}; - -// Forces fields to be maxFields size, filling empty values -const normalizeFields = (fields, maxFields) => ( - ImmutableList(fields).setSize(Math.max(fields.size, 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(makeMapStateToProps) -@injectIntl -class EditProfile extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - account: ImmutablePropTypes.record, - maxFields: PropTypes.number, - verifiedCanEditName: PropTypes.bool, - }; - - state = { - isLoading: false, - } - - constructor(props) { - super(props); - const { account, maxFields } = this.props; - - const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']); - const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']); - const discoverable = account.getIn(['source', 'pleroma', 'discoverable']); - - const initialState = ImmutableMap(account).withMutations(map => { - map.merge(map.get('source')); - map.delete('source'); - map.set('fields', normalizeFields(map.get('fields'), Math.min(maxFields, 4))); - map.set('stranger_notifications', strangerNotifications); - map.set('accepts_email_list', acceptsEmailList); - map.set('hide_network', hidesNetwork(account)); - map.set('discoverable', discoverable); - 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 || account.get('username'), - website: this.state.website || account.get('website'), - location: this.state.location || account.get('location'), - })); - } - - 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, - website: state.website, - location: state.location, - birthday: state.birthday, - 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(); - const formData = new FormData(); - for (const key in data) { - const hasValue = data[key] !== null && data[key] !== undefined; - // Compact the submission. This should probably be done better. - const shouldAppend = Boolean(hasValue || key.startsWith('fields_attributes')); - if (shouldAppend) formData.append(key, hasValue ? data[key] : ''); - } - return formData; - } - - handleSubmit = (event) => { - const { dispatch, intl } = this.props; - - const credentials = dispatch(patchMe(this.getFormdata())); - /* Bad API url, was causing errors in the promise call below blocking the success message after making edits. */ - /* 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(intl.formatMessage(messages.success))); - }).catch((error) => { - this.setState({ isLoading: false }); - dispatch(snackbar.error(intl.formatMessage(messages.error))); - }); - - 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 = maxPixels => { - return e => { - const { name } = e.target; - const [f] = e.target.files || []; - - resizeImage(f, maxPixels).then(file => { - const url = file ? URL.createObjectURL(file) : this.state[name]; - - this.setState({ - [name]: url, - [`${name}_file`]: file, - }); - }).catch(console.error); - }; - } - - handleAddField = () => { - this.setState({ - fields: this.state.fields.push(ImmutableMap({ name: '', value: '' })), - }); - } - - handleDeleteField = i => { - return () => { - this.setState({ - fields: normalizeFields(this.state.fields.delete(i), Math.min(this.props.maxFields, 4)), - }); - }; - } - - render() { - const { intl, account, verifiedCanEditName, features /* maxFields */ } = this.props; - const verified = account.get('verified'); - const canEditName = verifiedCanEditName || !verified; - - return ( - -
- } - hintText={!canEditName && intl.formatMessage(messages.verified)} - > - - - - {features.birthdays && ( - } - > - - - )} - - {features.accountLocation && ( - } - > - - - )} - - {features.accountWebsite && ( - } - > - - - )} - - } - > -