From 5cc962593e1aaa93f0d5f182740b697aee9fc1b8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 29 Apr 2022 12:58:57 -0500 Subject: [PATCH 01/16] ProfilePreview: convert to TSX --- .../components/profile_preview.js | 46 ------------------- .../components/profile_preview.tsx | 44 ++++++++++++++++++ 2 files changed, 44 insertions(+), 46 deletions(-) delete mode 100644 app/soapbox/features/edit_profile/components/profile_preview.js create mode 100644 app/soapbox/features/edit_profile/components/profile_preview.tsx diff --git a/app/soapbox/features/edit_profile/components/profile_preview.js b/app/soapbox/features/edit_profile/components/profile_preview.js deleted file mode 100644 index 7fe1e8e4c..000000000 --- a/app/soapbox/features/edit_profile/components/profile_preview.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import StillImage from 'soapbox/components/still_image'; -import VerificationBadge from 'soapbox/components/verification_badge'; -import { getAcct } from 'soapbox/utils/accounts'; -import { displayFqn } from 'soapbox/utils/state'; - -const mapStateToProps = state => ({ - 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; From e6a797d712657a40da22d03d12260ac43eb6076e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 29 Apr 2022 12:59:13 -0500 Subject: [PATCH 02/16] normalizeAccount(): normalize `discoverable` field --- app/soapbox/normalizers/account.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index cdb80dfd4..5db110021 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -27,6 +27,7 @@ export const AccountRecord = ImmutableRecord({ birthday: undefined as Date | undefined, bot: false, created_at: new Date(), + discoverable: false, display_name: '', emojis: ImmutableList(), favicon: '', @@ -255,6 +256,11 @@ const addStaffFields = (account: ImmutableMap) => { }); }; +const normalizeDiscoverable = (account: ImmutableMap) => { + const discoverable = Boolean(account.get('discoverable') || account.getIn(['source', 'pleroma', 'discoverable'])); + return account.set('discoverable', discoverable); +}; + export const normalizeAccount = (account: Record) => { return AccountRecord( ImmutableMap(fromJS(account)).withMutations(account => { @@ -269,6 +275,7 @@ export const normalizeAccount = (account: Record) => { normalizeLocation(account); normalizeFqn(account); normalizeFavicon(account); + normalizeDiscoverable(account); addDomain(account); addStaffFields(account); fixUsername(account); From 858740ad4717e0b0135551092cd9dc0094e2946b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 29 Apr 2022 14:12:52 -0500 Subject: [PATCH 03/16] EditProfile: convert to React.FC+TSX (mostly) --- .../components/ui/form-group/form-group.tsx | 4 +- .../components/ui/textarea/textarea.tsx | 1 + app/soapbox/features/edit_profile/index.js | 436 ------------------ app/soapbox/features/edit_profile/index.tsx | 421 +++++++++++++++++ app/soapbox/features/forms/index.tsx | 9 +- app/soapbox/normalizers/account.ts | 2 +- 6 files changed, 433 insertions(+), 440 deletions(-) delete mode 100644 app/soapbox/features/edit_profile/index.js create mode 100644 app/soapbox/features/edit_profile/index.tsx 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/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 { - 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 && ( - } - > - - - )} - - } - > -