Add new onboarding flow to web UI (#24619)
parent
4a0aa54a87
commit
30666618ea
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
class VerifiedBadge extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
link: PropTypes.string.isRequired,
|
||||
verifiedAt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { link } = this.props;
|
||||
|
||||
return (
|
||||
<span className='verified-badge'>
|
||||
<Icon id='check' className='verified-badge__mark' />
|
||||
<span dangerouslySetInnerHTML={{ __html: link }} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VerifiedBadge;
|
@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import DisplayName from 'mastodon/components/display_name';
|
||||
import { Link } from 'react-router-dom';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const getFirstSentence = str => {
|
||||
const arr = str.split(/(([.?!]+\s)|[.。?!\n•])/);
|
||||
|
||||
return arr[0];
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
const { account, dispatch } = this.props;
|
||||
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account follow-recommendations-account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link className='account__display-name account__display-name--with-note' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
|
||||
<DisplayName account={account} />
|
||||
|
||||
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
|
||||
</Link>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
@ -1,117 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||
import { changeSetting, saveSettings } from 'mastodon/actions/settings';
|
||||
import { requestBrowserPermission } from 'mastodon/actions/notifications';
|
||||
import { markAsPartial } from 'mastodon/actions/timelines';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import Account from './components/account';
|
||||
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
});
|
||||
|
||||
class FollowRecommendations extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, suggestions } = this.props;
|
||||
|
||||
// Don't re-fetch if we're e.g. navigating backwards to this page,
|
||||
// since we don't want followed accounts to disappear from the list
|
||||
|
||||
if (suggestions.size === 0) {
|
||||
dispatch(fetchSuggestions(true));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
// Force the home timeline to be reloaded when the user navigates
|
||||
// to it; if the user is new, it would've been empty before
|
||||
|
||||
dispatch(markAsPartial('home'));
|
||||
}
|
||||
|
||||
handleDone = () => {
|
||||
const { dispatch } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
dispatch(requestBrowserPermission((permission) => {
|
||||
if (permission === 'granted') {
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
|
||||
dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
}));
|
||||
|
||||
router.history.push('/home');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { suggestions, isLoading } = this.props;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable follow-recommendations-container'>
|
||||
<div className='column-title'>
|
||||
<svg viewBox='0 0 79 79' className='logo'>
|
||||
<use xlinkHref='#logo-symbol-icon' />
|
||||
</svg>
|
||||
|
||||
<h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
|
||||
<p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
|
||||
</div>
|
||||
|
||||
{!isLoading && (
|
||||
<React.Fragment>
|
||||
<div className='column-list'>
|
||||
{suggestions.size > 0 ? suggestions.map(suggestion => (
|
||||
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
)) : (
|
||||
<div className='column-list__empty-message'>
|
||||
<FormattedMessage id='empty_column.follow_recommendations' defaultMessage='Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='column-actions'>
|
||||
<img src={imageGreeting} alt='' className='column-actions__background' />
|
||||
<Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FollowRecommendations);
|
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const ArrowSmallRight = () => (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
|
||||
<path fillRule='evenodd' d='M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z' clipRule='evenodd' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ArrowSmallRight;
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Check from 'mastodon/components/check';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const ProgressIndicator = ({ steps, completed }) => (
|
||||
<div className='onboarding__progress-indicator'>
|
||||
{(new Array(steps)).fill().map((_, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <div className={classNames('onboarding__progress-indicator__line', { active: completed > i })} />}
|
||||
|
||||
<div className={classNames('onboarding__progress-indicator__step', { active: completed > i })}>
|
||||
{completed > i && <Check />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
ProgressIndicator.propTypes = {
|
||||
steps: PropTypes.number.isRequired,
|
||||
completed: PropTypes.number,
|
||||
};
|
||||
|
||||
export default ProgressIndicator;
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import Check from 'mastodon/components/check';
|
||||
|
||||
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||
const content = (
|
||||
<>
|
||||
<div className='onboarding__steps__item__icon'>
|
||||
<Icon id={icon} />
|
||||
</div>
|
||||
|
||||
<div className='onboarding__steps__item__description'>
|
||||
<h6>{label}</h6>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
{completed && (
|
||||
<div className='onboarding__steps__item__progress'>
|
||||
<Check />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} onClick={onClick} target='_blank' rel='noopener' className='onboarding__steps__item'>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={onClick} className='onboarding__steps__item'>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
Step.propTypes = {
|
||||
label: PropTypes.node,
|
||||
description: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
completed: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Step;
|
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||
import { markAsPartial } from 'mastodon/actions/timelines';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Account from 'mastodon/containers/account_container';
|
||||
import EmptyAccount from 'mastodon/components/account';
|
||||
import { FormattedMessage, FormattedHTMLMessage } from 'react-intl';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import ProgressIndicator from './components/progress_indicator';
|
||||
|
||||
const mapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
return state => ({
|
||||
account: getAccount(state, me),
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
});
|
||||
};
|
||||
|
||||
class Follows extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
account: ImmutablePropTypes.map,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchSuggestions(true));
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(markAsPartial('home'));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onBack, isLoading, suggestions, account } = this.props;
|
||||
|
||||
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>
|
||||
|
||||
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
|
||||
|
||||
<div className='follow-recommendations'>
|
||||
{isLoading ? (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />) : suggestions.map(suggestion => (
|
||||
<Account id={suggestion.get('account')} key={suggestion.get('account')} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className='onboarding__lead'><FormattedHTMLMessage 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: text => <strong>{text}</strong> }} /></p>
|
||||
|
||||
<div className='onboarding__footer'>
|
||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Follows);
|
@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { focusCompose } from 'mastodon/actions/compose';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import { closeOnboarding } from 'mastodon/actions/onboarding';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
import Follows from './follows';
|
||||
import Share from './share';
|
||||
import Step from './components/step';
|
||||
import ArrowSmallRight from './components/arrow_small_right';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const mapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
return state => ({
|
||||
account: getAccount(state, me),
|
||||
});
|
||||
};
|
||||
|
||||
class Onboarding extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
state = {
|
||||
step: null,
|
||||
profileClicked: false,
|
||||
shareClicked: false,
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
dispatch(closeOnboarding());
|
||||
router.history.push('/home');
|
||||
};
|
||||
|
||||
handleProfileClick = () => {
|
||||
this.setState({ profileClicked: true });
|
||||
};
|
||||
|
||||
handleFollowClick = () => {
|
||||
this.setState({ step: 'follows' });
|
||||
};
|
||||
|
||||
handleComposeClick = () => {
|
||||
const { dispatch } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
dispatch(focusCompose(router.history));
|
||||
};
|
||||
|
||||
handleShareClick = () => {
|
||||
this.setState({ step: 'share', shareClicked: true });
|
||||
};
|
||||
|
||||
handleBackClick = () => {
|
||||
this.setState({ step: null });
|
||||
};
|
||||
|
||||
handleWindowFocus = debounce(() => {
|
||||
const { dispatch, account } = this.props;
|
||||
dispatch(fetchAccount(account.get('id')));
|
||||
}, 1000, { trailing: true });
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('focus', this.handleWindowFocus, false);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', this.handleWindowFocus);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
const { step, shareClicked } = this.state;
|
||||
|
||||
switch(step) {
|
||||
case 'follows':
|
||||
return <Follows onBack={this.handleBackClick} />;
|
||||
case 'share':
|
||||
return <Share onBack={this.handleBackClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
<img src={illustration} alt='' className='onboarding__illustration' />
|
||||
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
|
||||
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
|
||||
</div>
|
||||
|
||||
<div className='onboarding__steps'>
|
||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage='You curate your own feed. Lets fill it with interesting people.' />} />
|
||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
||||
</div>
|
||||
|
||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage='Want to skip right ahead?' /></p>
|
||||
|
||||
<div className='onboarding__links'>
|
||||
<Link to='/explore' className='onboarding__link'>
|
||||
<ArrowSmallRight />
|
||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='onboarding__footer'>
|
||||
<button className='link-button' onClick={this.handleClose}><FormattedMessage id='onboarding.actions.close' defaultMessage="Don't show this screen again" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Onboarding);
|
@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { me, domain } from 'mastodon/initial_state';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import ArrowSmallRight from './components/arrow_small_right';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const messages = defineMessages({
|
||||
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on Mastodon! Come follow me at {url}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
account: state.getIn(['accounts', me]),
|
||||
});
|
||||
|
||||
class CopyPasteText extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
copied: false,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.input = c;
|
||||
};
|
||||
|
||||
handleInputClick = () => {
|
||||
this.setState({ copied: false });
|
||||
this.input.focus();
|
||||
this.input.select();
|
||||
this.input.setSelectionRange(0, this.props.value.length);
|
||||
};
|
||||
|
||||
handleButtonClick = e => {
|
||||
e.stopPropagation();
|
||||
|
||||
const { value } = this.props;
|
||||
navigator.clipboard.writeText(value);
|
||||
this.input.blur();
|
||||
this.setState({ copied: true });
|
||||
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
this.setState({ focused: false });
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props;
|
||||
const { copied, focused } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
|
||||
<textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
|
||||
|
||||
<button className='button' onClick={this.handleButtonClick}>
|
||||
<Icon id='copy' /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Share extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { onBack, account, intl } = this.props;
|
||||
|
||||
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton onClick={onBack} />
|
||||
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
|
||||
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
|
||||
</div>
|
||||
|
||||
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
||||
|
||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
|
||||
|
||||
<div className='onboarding__links'>
|
||||
<Link to='/home' className='onboarding__link'>
|
||||
<ArrowSmallRight />
|
||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Go to your home feed' />
|
||||
</Link>
|
||||
|
||||
<Link to='/explore' className='onboarding__link'>
|
||||
<ArrowSmallRight />
|
||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='onboarding__footer'>
|
||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></button>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Share));
|
Loading…
Reference in new issue