-
+
diff --git a/app/javascript/mastodon/features/follow_recommendations/components/account.jsx b/app/javascript/mastodon/features/follow_recommendations/components/account.jsx
deleted file mode 100644
index 9cb26fe64..000000000
--- a/app/javascript/mastodon/features/follow_recommendations/components/account.jsx
+++ /dev/null
@@ -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 =
;
- } else {
- button =
;
- }
-
- return (
-
-
-
-
-
-
-
-
{getFirstSentence(account.get('note_plain'))}
-
-
-
- {button}
-
-
-
- );
- }
-
-}
-
-export default connect(makeMapStateToProps)(injectIntl(Account));
diff --git a/app/javascript/mastodon/features/follow_recommendations/index.jsx b/app/javascript/mastodon/features/follow_recommendations/index.jsx
deleted file mode 100644
index 7ba34b51f..000000000
--- a/app/javascript/mastodon/features/follow_recommendations/index.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
- {!isLoading && (
-
-
- {suggestions.size > 0 ? suggestions.map(suggestion => (
-
- )) : (
-
-
-
- )}
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
- );
- }
-
-}
-
-export default connect(mapStateToProps)(FollowRecommendations);
diff --git a/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx b/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx
new file mode 100644
index 000000000..40e166f6d
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const ArrowSmallRight = () => (
+
+);
+
+export default ArrowSmallRight;
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx b/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
new file mode 100644
index 000000000..97134c0c9
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
@@ -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 }) => (
+
+ {(new Array(steps)).fill().map((_, i) => (
+
+ {i > 0 && i })} />}
+
+
i })}>
+ {completed > i && }
+
+
+ ))}
+
+);
+
+ProgressIndicator.propTypes = {
+ steps: PropTypes.number.isRequired,
+ completed: PropTypes.number,
+};
+
+export default ProgressIndicator;
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/onboarding/components/step.jsx b/app/javascript/mastodon/features/onboarding/components/step.jsx
new file mode 100644
index 000000000..6f376e5d5
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/components/step.jsx
@@ -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 = (
+ <>
+
+
+
+
+
+
{label}
+
{description}
+
+
+ {completed && (
+
+
+
+ )}
+ >
+ );
+
+ if (href) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+Step.propTypes = {
+ label: PropTypes.node,
+ description: PropTypes.node,
+ icon: PropTypes.string,
+ completed: PropTypes.bool,
+ href: PropTypes.string,
+ onClick: PropTypes.func,
+};
+
+export default Step;
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx
new file mode 100644
index 000000000..c42daf2ff
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/follows.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {isLoading ? (new Array(8)).fill().map((_, i) =>
) : suggestions.map(suggestion => (
+
+ ))}
+
+
+
{text} }} />
+
+
+
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(Follows);
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx
new file mode 100644
index 000000000..5980ba0d0
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/index.jsx
@@ -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 ;
+ case 'share':
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ 0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} />
+ = 7} icon='user-plus' label={} description={} />
+ = 1} icon='pencil-square-o' label={} description={} />
+ } description={} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(Onboarding);
diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx
new file mode 100644
index 000000000..897b2e74d
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/share.jsx
@@ -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 (
+
+
+
+
+
+ );
+ }
+
+}
+
+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 (
+
+
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Share));
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 0e73f4c09..2178687a9 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -51,12 +51,12 @@ import {
Lists,
Directory,
Explore,
- FollowRecommendations,
+ Onboarding,
About,
PrivacyPolicy,
} from './util/async-components';
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
-import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
+import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import Header from './components/header';
// Dummy import, to make sure that ends up in the application bundle.
@@ -192,7 +192,7 @@ class SwitchingColumnsArea extends React.PureComponent {
-
+
@@ -389,7 +389,6 @@ class UI extends React.PureComponent {
// On first launch, redirect to the follow recommendations page
if (signedIn && this.props.firstLaunch) {
this.context.router.history.replace('/start');
- this.props.dispatch(closeOnboarding());
}
if (signedIn) {
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index e6382fa10..c1774512a 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -150,8 +150,8 @@ export function Directory () {
return import(/* webpackChunkName: "features/directory" */'../../directory');
}
-export function FollowRecommendations () {
- return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
+export function Onboarding () {
+ return import(/* webpackChunkName: "features/onboarding" */'../../onboarding');
}
export function CompareHistoryModal () {
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 6d6683808..b45b6e6f0 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -2264,40 +2264,6 @@
],
"path": "app/javascript/mastodon/features/filters/select_filter.json"
},
- {
- "descriptors": [
- {
- "defaultMessage": "Follow",
- "id": "account.follow"
- },
- {
- "defaultMessage": "Unfollow",
- "id": "account.unfollow"
- }
- ],
- "path": "app/javascript/mastodon/features/follow_recommendations/components/account.json"
- },
- {
- "descriptors": [
- {
- "defaultMessage": "Follow people you'd like to see posts from! Here are some suggestions.",
- "id": "follow_recommendations.heading"
- },
- {
- "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!",
- "id": "follow_recommendations.lead"
- },
- {
- "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.",
- "id": "empty_column.follow_recommendations"
- },
- {
- "defaultMessage": "Done",
- "id": "follow_recommendations.done"
- }
- ],
- "path": "app/javascript/mastodon/features/follow_recommendations/index.json"
- },
{
"descriptors": [
{
@@ -3218,6 +3184,125 @@
],
"path": "app/javascript/mastodon/features/notifications/index.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Popular on Mastodon",
+ "id": "onboarding.follows.title"
+ },
+ {
+ "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!",
+ "id": "onboarding.follows.lead"
+ },
+ {
+ "defaultMessage": "Did you know? 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!",
+ "id": "onboarding.tips.accounts_from_other_servers"
+ },
+ {
+ "defaultMessage": "Take me back",
+ "id": "onboarding.actions.back"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/onboarding/follows.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "You've made it!",
+ "id": "onboarding.start.title"
+ },
+ {
+ "defaultMessage": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
+ "id": "onboarding.start.lead"
+ },
+ {
+ "defaultMessage": "Customize your profile",
+ "id": "onboarding.steps.setup_profile.title"
+ },
+ {
+ "defaultMessage": "Others are more likely to interact with you with a filled out profile.",
+ "id": "onboarding.steps.setup_profile.body"
+ },
+ {
+ "defaultMessage": "Follow {count, plural, one {one person} other {# people}}",
+ "id": "onboarding.steps.follow_people.title"
+ },
+ {
+ "defaultMessage": "You curate your own feed. Lets fill it with interesting people.",
+ "id": "onboarding.steps.follow_people.body"
+ },
+ {
+ "defaultMessage": "Make your first post",
+ "id": "onboarding.steps.publish_status.title"
+ },
+ {
+ "defaultMessage": "Say hello to the world.",
+ "id": "onboarding.steps.publish_status.body"
+ },
+ {
+ "defaultMessage": "Share your profile",
+ "id": "onboarding.steps.share_profile.title"
+ },
+ {
+ "defaultMessage": "Let your friends know how to find you on Mastodon!",
+ "id": "onboarding.steps.share_profile.body"
+ },
+ {
+ "defaultMessage": "Want to skip right ahead?",
+ "id": "onboarding.start.skip"
+ },
+ {
+ "defaultMessage": "See what's trending",
+ "id": "onboarding.actions.go_to_explore"
+ },
+ {
+ "defaultMessage": "Don't show this screen again",
+ "id": "onboarding.actions.close"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/onboarding/index.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "I'm {username} on Mastodon! Come follow me at {url}",
+ "id": "onboarding.share.message"
+ },
+ {
+ "defaultMessage": "Copied",
+ "id": "copypaste.copied"
+ },
+ {
+ "defaultMessage": "Copy to clipboard",
+ "id": "copypaste.copy_to_clipboard"
+ },
+ {
+ "defaultMessage": "Share your profile",
+ "id": "onboarding.share.title"
+ },
+ {
+ "defaultMessage": "Let people know how they can find you on Mastodon!",
+ "id": "onboarding.share.lead"
+ },
+ {
+ "defaultMessage": "Possible next steps:",
+ "id": "onboarding.share.next_steps"
+ },
+ {
+ "defaultMessage": "Go to your home feed",
+ "id": "onboarding.actions.go_to_home"
+ },
+ {
+ "defaultMessage": "See what's trending",
+ "id": "onboarding.actions.go_to_explore"
+ },
+ {
+ "defaultMessage": "Take me back",
+ "id": "onboarding.action.back"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/onboarding/share.json"
+ },
{
"descriptors": [
{
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 31fa3ca3a..55b34f3c8 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -182,6 +182,7 @@
"conversation.with": "With {names}",
"copypaste.copied": "Copied",
"copypaste.copy": "Copy",
+ "copypaste.copy_to_clipboard": "Copy to clipboard",
"directory.federated": "From known fediverse",
"directory.local": "From {domain} only",
"directory.new_arrivals": "New arrivals",
@@ -222,7 +223,6 @@
"empty_column.explore_statuses": "Nothing is trending right now. Check back later!",
"empty_column.favourited_statuses": "You don't have any favourite posts yet. When you favourite one, it will show up here.",
"empty_column.favourites": "No one has favourited this post yet. When someone does, they will show up here.",
- "empty_column.follow_recommendations": "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.",
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
@@ -261,9 +261,6 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post",
- "follow_recommendations.done": "Done",
- "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
- "follow_recommendations.lead": "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!",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
@@ -443,6 +440,29 @@
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
+ "onboarding.action.back": "Take me back",
+ "onboarding.actions.back": "Take me back",
+ "onboarding.actions.close": "Don't show this screen again",
+ "onboarding.actions.go_to_explore": "See what's trending",
+ "onboarding.actions.go_to_home": "Go to your home feed",
+ "onboarding.follows.lead": "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!",
+ "onboarding.follows.title": "Popular on Mastodon",
+ "onboarding.share.lead": "Let people know how they can find you on Mastodon!",
+ "onboarding.share.message": "I'm {username} on Mastodon! Come follow me at {url}",
+ "onboarding.share.next_steps": "Possible next steps:",
+ "onboarding.share.title": "Share your profile",
+ "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
+ "onboarding.start.skip": "Want to skip right ahead?",
+ "onboarding.start.title": "You've made it!",
+ "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
+ "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
+ "onboarding.steps.publish_status.body": "Say hello to the world.",
+ "onboarding.steps.publish_status.title": "Make your first post",
+ "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
+ "onboarding.steps.setup_profile.title": "Customize your profile",
+ "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
+ "onboarding.steps.share_profile.title": "Share your profile",
+ "onboarding.tips.accounts_from_other_servers": "Did you know? 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!",
"password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
"password_confirmation.mismatching": "Password confirmation does not match",
"picture_in_picture.restore": "Put it back",
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index 4e1256d1b..640b2a889 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -4,6 +4,7 @@ import {
} from '../actions/accounts';
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
+import { me } from 'mastodon/initial_state';
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
followers_count: account.followers_count,
@@ -19,6 +20,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) {
@@ -29,9 +38,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;
}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 842b7af51..11549cb39 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -46,6 +46,7 @@ import {
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
COMPOSE_CHANGE_MEDIA_FOCUS,
COMPOSE_SET_STATUS,
+ COMPOSE_FOCUS,
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
@@ -526,6 +527,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;
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index a496e73b8..bd7c61e40 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1466,16 +1466,6 @@ body > [data-popper-placement] {
}
}
-.follow-recommendations-account {
- .icon-button {
- color: $ui-primary-color;
-
- &.active {
- color: $valid-value-color;
- }
- }
-}
-
.account__wrapper {
display: flex;
gap: 10px;
@@ -1869,6 +1859,11 @@ a.account__display-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+
+ &__account {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
}
.display-name__html {
@@ -2744,13 +2739,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;
@@ -2765,45 +2754,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);
}
}
@@ -7497,6 +7715,8 @@ noscript {
align-items: center;
color: $valid-value-color;
gap: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
a {
color: inherit;