[Glitch] Add new onboarding flow to web UI
Port 0461f83320
to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
pull/2493/head
parent
335cfab32f
commit
4537b4b961
|
@ -84,6 +84,7 @@ export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTIO
|
|||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||
|
||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
|
@ -144,6 +145,14 @@ export function resetCompose() {
|
|||
};
|
||||
}
|
||||
|
||||
export const focusCompose = routerHistory => dispatch => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(routerHistory);
|
||||
};
|
||||
|
||||
export function mentionCompose(account, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
import { openModal } from './modal';
|
||||
import { changeSetting, saveSettings } from './settings';
|
||||
|
||||
export function showOnboardingOnce() {
|
||||
return (dispatch, getState) => {
|
||||
const alreadySeen = getState().getIn(['settings', 'onboarded']);
|
||||
export const INTRODUCTION_VERSION = 20181216044202;
|
||||
|
||||
if (!alreadySeen) {
|
||||
dispatch(openModal({
|
||||
modalType: 'ONBOARDING',
|
||||
}));
|
||||
dispatch(changeSetting(['onboarded'], true));
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
};
|
||||
}
|
||||
export const closeOnboarding = () => dispatch => {
|
||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const Check = () => (
|
||||
<svg width='14' height='11' viewBox='0 0 14 11'>
|
||||
<path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
|
||||
<path fillRule='evenodd' d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z' clipRule='evenodd' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
|
@ -13,13 +13,16 @@ export class ColumnBackButton extends PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { history } = this.props;
|
||||
const { onClick, history } = this.props;
|
||||
|
||||
if (history.location?.state?.fromMastodon) {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (history.location?.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
|
|
|
@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { length } from 'stringz';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { maxChars } from 'flavours/glitch/initial_state';
|
||||
import { isMobile } from 'flavours/glitch/is_mobile';
|
||||
|
@ -84,6 +85,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
showSearch: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
highlighted: false,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
@ -209,6 +214,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
this._updateFocusAndSelection({ });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
this._updateFocusAndSelection(prevProps);
|
||||
}
|
||||
|
@ -257,6 +266,8 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
textarea.focus();
|
||||
if (!singleColumn) textarea.scrollIntoView();
|
||||
this.setState({ highlighted: true });
|
||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
|
@ -302,6 +313,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
spoilersAlwaysOn,
|
||||
isEditing,
|
||||
} = this.props;
|
||||
const { highlighted } = this.state;
|
||||
|
||||
const countText = this.getFulltextForCharacterCounting();
|
||||
|
||||
|
@ -332,29 +344,31 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={isSubmitting}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
suggestions={suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onSuggestionsFetchRequested={onFetchSuggestions}
|
||||
onSuggestionsClearRequested={onClearSuggestions}
|
||||
onSuggestionSelected={this.handleSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
<TextareaIcons advancedOptions={advancedOptions} />
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
</AutosuggestTextarea>
|
||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={isSubmitting}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
suggestions={suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onSuggestionsFetchRequested={onFetchSuggestions}
|
||||
onSuggestionsClearRequested={onClearSuggestions}
|
||||
onSuggestionSelected={this.handleSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
<TextareaIcons advancedOptions={advancedOptions} />
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
</AutosuggestTextarea>
|
||||
</div>
|
||||
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<OptionsContainer
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
|
||||
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'>
|
||||
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} 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>
|
||||
</Permalink>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,119 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
|
||||
import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
|
||||
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
|
||||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||
|
||||
import Account from './components/account';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
});
|
||||
|
||||
class FollowRecommendations extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
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 { history, dispatch } = this.props;
|
||||
|
||||
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());
|
||||
}
|
||||
}));
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(FollowRecommendations));
|
|
@ -0,0 +1,7 @@
|
|||
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 'flavours/glitch/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,49 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import Check from 'flavours/glitch/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 'flavours/glitch/components/column';
|
||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
|
||||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Account from 'flavours/glitch/containers/account_container';
|
||||
import EmptyAccount from 'flavours/glitch/components/account';
|
||||
import { FormattedMessage, FormattedHTMLMessage } from 'react-intl';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import { me } from 'flavours/glitch/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,149 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { focusCompose } from 'flavours/glitch/actions/compose';
|
||||
import { closeOnboarding } from 'flavours/glitch/actions/onboarding';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
|
||||
|
||||
import ArrowSmallRight from './components/arrow_small_right';
|
||||
import Step from './components/step';
|
||||
import Follows from './follows';
|
||||
import Share from './share';
|
||||
|
||||
|
||||
|
||||
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 'flavours/glitch/components/column';
|
||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { me, domain } from 'flavours/glitch/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 'flavours/glitch/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));
|
|
@ -18,6 +18,7 @@ import PermaLink from 'flavours/glitch/components/permalink';
|
|||
import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
|
||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
|
||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
|
@ -62,7 +63,7 @@ import {
|
|||
GettingStartedMisc,
|
||||
Directory,
|
||||
Explore,
|
||||
FollowRecommendations,
|
||||
Onboarding,
|
||||
About,
|
||||
PrivacyPolicy,
|
||||
} from './util/async-components';
|
||||
|
@ -86,7 +87,7 @@ const mapStateToProps = state => ({
|
|||
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
||||
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
||||
moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
|
||||
firstLaunch: false, // TODO: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
username: state.getIn(['accounts', me, 'username']),
|
||||
});
|
||||
|
||||
|
@ -216,7 +217,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
||||
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||
|
@ -417,7 +418,6 @@ class UI extends Component {
|
|||
// On first launch, redirect to the follow recommendations page
|
||||
if (signedIn && this.props.firstLaunch) {
|
||||
this.props.history.replace('/start');
|
||||
// TODO: this.props.dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
if (signedIn) {
|
||||
|
|
|
@ -170,8 +170,8 @@ export function Directory () {
|
|||
return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory');
|
||||
}
|
||||
|
||||
export function FollowRecommendations () {
|
||||
return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations');
|
||||
export function Onboarding () {
|
||||
return import(/* webpackChunkName: "features/glitch/async/onboarding" */'flavours/glitch/features/onboarding');
|
||||
}
|
||||
|
||||
export function CompareHistoryModal () {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
|
||||
import {
|
||||
ACCOUNT_FOLLOW_SUCCESS,
|
||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
|
@ -20,6 +22,14 @@ const normalizeAccounts = (state, accounts) => {
|
|||
return state;
|
||||
};
|
||||
|
||||
const incrementFollowers = (state, accountId) =>
|
||||
state.updateIn([accountId, 'followers_count'], num => num + 1)
|
||||
.updateIn([me, 'following_count'], num => num + 1);
|
||||
|
||||
const decrementFollowers = (state, accountId) =>
|
||||
state.updateIn([accountId, 'followers_count'], num => Math.max(0, num - 1))
|
||||
.updateIn([me, 'following_count'], num => Math.max(0, num - 1));
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function accountsCounters(state = initialState, action) {
|
||||
|
@ -30,9 +40,9 @@ export default function accountsCounters(state = initialState, action) {
|
|||
return normalizeAccounts(state, action.accounts);
|
||||
case ACCOUNT_FOLLOW_SUCCESS:
|
||||
return action.alreadyFollowing ? state :
|
||||
state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
|
||||
incrementFollowers(state, action.relationship.id);
|
||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||
return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
|
||||
return decrementFollowers(state, action.relationship.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import {
|
|||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||
COMPOSE_SET_STATUS,
|
||||
COMPOSE_FOCUS,
|
||||
} from '../actions/compose';
|
||||
import { REDRAFT } from '../actions/statuses';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
|
@ -651,6 +652,8 @@ export default function compose(state = initialState, action) {
|
|||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||
case COMPOSE_LANGUAGE_CHANGE:
|
||||
return state.set('language', action.language);
|
||||
case COMPOSE_FOCUS:
|
||||
return state.set('focusDate', new Date());
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -45,16 +45,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.follow-recommendations-account {
|
||||
.icon-button {
|
||||
color: $ui-primary-color;
|
||||
|
||||
&.active {
|
||||
color: $valid-value-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
|
|
@ -914,13 +914,7 @@ $ui-header-height: 55px;
|
|||
|
||||
.column-title {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
padding-bottom: 40px;
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
|
@ -935,45 +929,274 @@ $ui-header-height: 55px;
|
|||
font-weight: 400;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.follow-recommendations-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 200px;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
|
||||
&__background {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
height: 220px;
|
||||
width: auto;
|
||||
@media screen and (min-width: 600px) {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.column-list {
|
||||
margin: 0 20px;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
background: darken($ui-base-color, 2%);
|
||||
border-radius: 4px;
|
||||
.onboarding__footer {
|
||||
margin-top: 30px;
|
||||
color: $dark-text-color;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
&__empty-message {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color: $darker-text-color;
|
||||
.link-button {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: $highlight-text-color;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
font-size: 17px;
|
||||
height: 56px;
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding__illustration {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 10px;
|
||||
max-height: 200px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.onboarding__lead {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color: $darker-text-color;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding__links {
|
||||
margin-bottom: 30px;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding__steps {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&__item {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
margin-bottom: 2px;
|
||||
text-decoration: none;
|
||||
text-align: start;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex: 0 0 auto;
|
||||
background: $ui-base-color;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: $dark-text-color;
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__progress {
|
||||
flex: 0 0 auto;
|
||||
background: $valid-value-color;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
color: $primary-text-color;
|
||||
|
||||
svg {
|
||||
height: 14px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
flex: 1 1 auto;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
h6 {
|
||||
color: $primary-text-color;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $darker-text-color;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding__progress-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
position: sticky;
|
||||
background: $ui-base-color;
|
||||
|
||||
@media screen and (min-width: 600) {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
&__line {
|
||||
height: 4px;
|
||||
flex: 1 1 auto;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__step {
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 50%;
|
||||
color: $primary-text-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $valid-value-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__step.active,
|
||||
&__line.active {
|
||||
background: $valid-value-color;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
$valid-value-color,
|
||||
lighten($valid-value-color, 8%),
|
||||
$valid-value-color
|
||||
);
|
||||
background-size: 200px 100%;
|
||||
animation: skeleton 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.follow-recommendations {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.account:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-paste-text {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 16px;
|
||||
color: $primary-text-color;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
transition: border-color 300ms linear;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:focus,
|
||||
&.focused {
|
||||
transition: none;
|
||||
outline: 0;
|
||||
border-color: $highlight-text-color;
|
||||
}
|
||||
|
||||
&.copied {
|
||||
border-color: $valid-value-color;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compose-form__highlightable {
|
||||
border-radius: 4px;
|
||||
transition: box-shadow 300ms linear;
|
||||
|
||||
&.active {
|
||||
transition: none;
|
||||
box-shadow: 0 0 0 2px rgba(lighten($highlight-text-color, 8%), 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -406,6 +406,11 @@ body > [data-popper-placement] {
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&__account {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
|
|
Loading…
Reference in New Issue