Skip to content

Commit

Permalink
Add profile setup to onboarding in web UI (mastodon#27829)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron authored Nov 15, 2023
1 parent d807412 commit d67bd44
Show file tree
Hide file tree
Showing 18 changed files with 519 additions and 411 deletions.
2 changes: 2 additions & 0 deletions app/controllers/api/v1/accounts/credentials_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ def update
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422
end

private
Expand Down
15 changes: 15 additions & 0 deletions app/javascript/mastodon/actions/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,18 @@ export function unpinAccountFail(error) {
error,
};
}

export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch, getState) => {
const data = new FormData();

data.append('display_name', displayName);
data.append('note', note);
if (avatar) data.append('avatar', avatar);
if (header) data.append('header', header);
data.append('discoverable', discoverable);
data.append('indexable', indexable);

return api(getState).patch('/api/v1/accounts/update_credentials', data).then(response => {
dispatch(importFetchedAccount(response.data));
});
};
1 change: 1 addition & 0 deletions app/javascript/mastodon/api_types/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ApiAccountJSON {
bot: boolean;
created_at: string;
discoverable: boolean;
indexable: boolean;
display_name: string;
emojis: ApiCustomEmojiJSON[];
fields: ApiAccountFieldJSON[];
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/components/admin/Retention.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class Retention extends PureComponent {
let content;

if (loading) {
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading' />;
} else {
content = (
<table className='retention__table'>
Expand Down
26 changes: 21 additions & 5 deletions app/javascript/mastodon/components/loading_indicator.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { useIntl, defineMessages } from 'react-intl';

import { CircularProgress } from './circular_progress';

export const LoadingIndicator: React.FC = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
const messages = defineMessages({
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
});

export const LoadingIndicator: React.FC = () => {
const intl = useIntl();

return (
<div
className='loading-indicator'
role='progressbar'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loading)}
>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
};

This file was deleted.

15 changes: 11 additions & 4 deletions app/javascript/mastodon/features/onboarding/components/step.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import PropTypes from 'prop-types';

import { Link } from 'react-router-dom';

import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';

import { Icon } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';

const Step = ({ label, description, icon, iconComponent, completed, onClick, href }) => {
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
const content = (
<>
<div className='onboarding__steps__item__icon'>
Expand All @@ -29,6 +31,12 @@ const Step = ({ label, description, icon, iconComponent, completed, onClick, hre
{content}
</a>
);
} else if (to) {
return (
<Link to={to} className='onboarding__steps__item'>
{content}
</Link>
);
}

return (
Expand All @@ -45,7 +53,6 @@ Step.propTypes = {
iconComponent: PropTypes.func,
completed: PropTypes.bool,
href: PropTypes.string,
to: PropTypes.string,
onClick: PropTypes.func,
};

export default Step;
99 changes: 41 additions & 58 deletions app/javascript/mastodon/features/onboarding/follows.jsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,62 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useEffect } from 'react';

import { FormattedMessage } from 'react-intl';

import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';

import { useDispatch } from 'react-redux';


import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { EmptyAccount } from 'mastodon/components/empty_account';
import Account from 'mastodon/containers/account_container';
import { useAppSelector } from 'mastodon/store';

const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
});

class Follows extends PureComponent {

static propTypes = {
onBack: PropTypes.func,
dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
};
export const Follows = () => {
const dispatch = useDispatch();
const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading']));
const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items']));

componentDidMount () {
const { dispatch } = this.props;
useEffect(() => {
dispatch(fetchSuggestions(true));
}

componentWillUnmount () {
const { dispatch } = this.props;
dispatch(markAsPartial('home'));
}

render () {
const { onBack, isLoading, suggestions } = this.props;
return () => {
dispatch(markAsPartial('home'));
};
}, [dispatch]);

let loadedContent;
let loadedContent;

if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}

return (
<Column>
<ColumnBackButton onClick={onBack} />

<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>
if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}

<div className='follow-recommendations'>
{loadedContent}
</div>
return (
<>
<ColumnBackButton />

<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>

<div className='onboarding__footer'>
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
</div>
<div className='follow-recommendations'>
{loadedContent}
</div>
</Column>
);
}

}
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>

export default connect(mapStateToProps)(Follows);
<div className='onboarding__footer'>
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></Link>
</div>
</div>
</>
);
};
Loading

0 comments on commit d67bd44

Please sign in to comment.