-
-
-
-
+ }, [dispatch, intl, history]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={} description={} />
+ = 1} icon='user-plus' iconComponent={PersonAddIcon} label={} description={} />
+ = 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={} description={ }} />} />
+ } description={} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
-
- 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={} description={} />
- = 7} icon='user-plus' iconComponent={PersonAddIcon} label={} description={} />
- = 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={} description={ }} />} />
- } description={} />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
-}
-
-export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));
+export default Onboarding;
diff --git a/app/javascript/mastodon/features/onboarding/profile.jsx b/app/javascript/mastodon/features/onboarding/profile.jsx
new file mode 100644
index 0000000000..19ba0bcb95
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/profile.jsx
@@ -0,0 +1,162 @@
+import { useState, useMemo, useCallback, createRef } from 'react';
+
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { useHistory } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
+
+
+import { ReactComponent as AddPhotoAlternateIcon } from '@material-symbols/svg-600/outlined/add_photo_alternate.svg';
+import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
+import Toggle from 'react-toggle';
+
+import { updateAccount } from 'mastodon/actions/accounts';
+import { Button } from 'mastodon/components/button';
+import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { Icon } from 'mastodon/components/icon';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { me } from 'mastodon/initial_state';
+import { useAppSelector } from 'mastodon/store';
+import { unescapeHTML } from 'mastodon/utils/html';
+
+const messages = defineMessages({
+ uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' },
+ uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' },
+});
+
+export const Profile = () => {
+ const account = useAppSelector(state => state.getIn(['accounts', me]));
+ const [displayName, setDisplayName] = useState(account.get('display_name'));
+ const [note, setNote] = useState(unescapeHTML(account.get('note')));
+ const [avatar, setAvatar] = useState(null);
+ const [header, setHeader] = useState(null);
+ const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
+ const [indexable, setIndexable] = useState(account.get('indexable'));
+ const [isSaving, setIsSaving] = useState(false);
+ const [errors, setErrors] = useState();
+ const avatarFileRef = createRef();
+ const headerFileRef = createRef();
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const history = useHistory();
+
+ const handleDisplayNameChange = useCallback(e => {
+ setDisplayName(e.target.value);
+ }, [setDisplayName]);
+
+ const handleNoteChange = useCallback(e => {
+ setNote(e.target.value);
+ }, [setNote]);
+
+ const handleDiscoverableChange = useCallback(e => {
+ setDiscoverable(e.target.checked);
+ }, [setDiscoverable]);
+
+ const handleIndexableChange = useCallback(e => {
+ setIndexable(e.target.checked);
+ }, [setIndexable]);
+
+ const handleAvatarChange = useCallback(e => {
+ setAvatar(e.target?.files?.[0]);
+ }, [setAvatar]);
+
+ const handleHeaderChange = useCallback(e => {
+ setHeader(e.target?.files?.[0]);
+ }, [setHeader]);
+
+ const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : account.get('avatar'), [avatar, account]);
+ const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : account.get('header'), [header, account]);
+
+ const handleSubmit = useCallback(() => {
+ setIsSaving(true);
+
+ dispatch(updateAccount({
+ displayName,
+ note,
+ avatar,
+ header,
+ discoverable,
+ indexable,
+ })).then(() => history.push('/start/follows')).catch(err => {
+ setIsSaving(false);
+ setErrors(err.response.data.details);
+ });
+ }, [dispatch, displayName, note, avatar, header, discoverable, indexable, history]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx
index 3349244223..adc0f9cba3 100644
--- a/app/javascript/mastodon/features/onboarding/share.jsx
+++ b/app/javascript/mastodon/features/onboarding/share.jsx
@@ -1,31 +1,25 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
import SwipeableViews from 'react-swipeable-views';
-import Column from 'mastodon/components/column';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { Icon } from 'mastodon/components/icon';
import { me, domain } from 'mastodon/initial_state';
+import { useAppSelector } from 'mastodon/store';
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 PureComponent {
static propTypes = {
@@ -141,59 +135,47 @@ class TipCarousel extends PureComponent {
}
-class Share extends PureComponent {
+export const Share = () => {
+ const account = useAppSelector(state => state.getIn(['accounts', me]));
+ const intl = useIntl();
+ const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
- static propTypes = {
- onBack: PropTypes.func,
- account: ImmutablePropTypes.record,
- intl: PropTypes.object,
- };
+ return (
+ <>
+
- render () {
- const { onBack, account, intl } = this.props;
+
+
- const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
+
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
+
+
+
+
+
-}
+
+
+
+
+
+
+
-export default connect(mapStateToProps)(injectIntl(Share));
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 02c69cbbaf..d3fee272f0 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -210,7 +210,7 @@ class SwitchingColumnsArea extends PureComponent {
-
+
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 9cbaf93054..0414460375 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -390,7 +390,7 @@
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
- "loading_indicator.label": "Loading...",
+ "loading_indicator.label": "Loading…",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.duration": "Duration",
@@ -479,6 +479,17 @@
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
"onboarding.follows.title": "Personalize your home feed",
+ "onboarding.profile.discoverable": "Feature profile and posts in discovery algorithms",
+ "onboarding.profile.display_name": "Display name",
+ "onboarding.profile.display_name_hint": "Your full name or your fun name…",
+ "onboarding.profile.indexable": "Include public posts in search results",
+ "onboarding.profile.lead": "You can always complete this later in the settings, where even more customization options are available.",
+ "onboarding.profile.note": "Bio",
+ "onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
+ "onboarding.profile.save_and_continue": "Save and continue",
+ "onboarding.profile.title": "Profile setup",
+ "onboarding.profile.upload_avatar": "Upload profile picture",
+ "onboarding.profile.upload_header": "Upload profile header",
"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:",
diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts
index 00066e2840..a04ebe6291 100644
--- a/app/javascript/mastodon/models/account.ts
+++ b/app/javascript/mastodon/models/account.ts
@@ -67,6 +67,7 @@ export const accountDefaultValues: AccountShape = {
bot: false,
created_at: '',
discoverable: false,
+ indexable: false,
display_name: '',
display_name_html: '',
emojis: List
(),
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c8cfe46a8c..9f87352f54 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2552,7 +2552,7 @@ $ui-header-height: 55px;
.column-title {
text-align: center;
- padding-bottom: 40px;
+ padding-bottom: 32px;
h3 {
font-size: 24px;
@@ -2743,58 +2743,6 @@ $ui-header-height: 55px;
}
}
-.onboarding__progress-indicator {
- display: flex;
- align-items: center;
- margin-bottom: 30px;
- position: sticky;
- background: $ui-base-color;
-
- @media screen and (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;
@@ -2871,6 +2819,28 @@ $ui-header-height: 55px;
}
}
+.onboarding__profile {
+ position: relative;
+ margin-bottom: 40px + 20px;
+
+ .app-form__avatar-input {
+ border: 2px solid $ui-base-color;
+ position: absolute;
+ inset-inline-start: -2px;
+ bottom: -40px;
+ z-index: 2;
+ }
+
+ .app-form__header-input {
+ margin: 0 -20px;
+ border-radius: 0;
+
+ img {
+ border-radius: 0;
+ }
+ }
+}
+
.compose-form__highlightable {
display: flex;
flex-direction: column;
@@ -3145,6 +3115,7 @@ $ui-header-height: 55px;
cursor: pointer;
background-color: transparent;
border: 0;
+ border-radius: 10px;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: rgba($base-overlay-background, 0);
@@ -3169,81 +3140,41 @@ $ui-header-height: 55px;
}
.react-toggle-track {
- width: 50px;
- height: 24px;
+ width: 32px;
+ height: 20px;
padding: 0;
- border-radius: 30px;
- background-color: $ui-base-color;
- transition: background-color 0.2s ease;
+ border-radius: 10px;
+ background-color: #626982;
}
-.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled)
- .react-toggle-track {
- background-color: darken($ui-base-color, 10%);
+.react-toggle--focus {
+ outline: $ui-button-focus-outline;
}
.react-toggle--checked .react-toggle-track {
- background-color: darken($ui-highlight-color, 2%);
-}
-
-.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled)
- .react-toggle-track {
background-color: $ui-highlight-color;
}
-.react-toggle-track-check {
- position: absolute;
- width: 14px;
- height: 10px;
- top: 0;
- bottom: 0;
- margin-top: auto;
- margin-bottom: auto;
- line-height: 0;
- inset-inline-start: 8px;
- opacity: 0;
- transition: opacity 0.25s ease;
-}
-
-.react-toggle--checked .react-toggle-track-check {
- opacity: 1;
- transition: opacity 0.25s ease;
-}
-
+.react-toggle-track-check,
.react-toggle-track-x {
- position: absolute;
- width: 10px;
- height: 10px;
- top: 0;
- bottom: 0;
- margin-top: auto;
- margin-bottom: auto;
- line-height: 0;
- inset-inline-end: 10px;
- opacity: 1;
- transition: opacity 0.25s ease;
-}
-
-.react-toggle--checked .react-toggle-track-x {
- opacity: 0;
+ display: none;
}
.react-toggle-thumb {
position: absolute;
- top: 1px;
- inset-inline-start: 1px;
- width: 22px;
- height: 22px;
- border: 1px solid $ui-base-color;
+ top: 2px;
+ inset-inline-start: 2px;
+ width: 16px;
+ height: 16px;
border-radius: 50%;
- background-color: darken($simple-background-color, 2%);
+ background-color: $primary-text-color;
box-sizing: border-box;
transition: all 0.25s ease;
transition-property: border-color, left;
}
.react-toggle--checked .react-toggle-thumb {
- inset-inline-start: 27px;
+ inset-inline-start: 32px - 16px - 2px;
border-color: $ui-highlight-color;
}
@@ -4066,6 +3997,17 @@ a.status-card {
justify-content: center;
}
+.button .loading-indicator {
+ position: static;
+ transform: none;
+
+ .circular-progress {
+ color: $primary-text-color;
+ width: 22px;
+ height: 22px;
+ }
+}
+
.circular-progress {
color: lighten($ui-base-color, 26%);
animation: 1.4s linear 0s infinite normal none running simple-rotate;
@@ -5799,12 +5741,14 @@ a.status-card {
&__toggle {
display: flex;
align-items: center;
- margin-bottom: 10px;
+ margin-bottom: 16px;
+ gap: 8px;
& > span {
- font-size: 17px;
+ display: block;
+ font-size: 14px;
font-weight: 500;
- margin-inline-start: 10px;
+ line-height: 20px;
}
}
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 0f8eecee01..e72a01936c 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -36,7 +36,7 @@ code {
}
.input {
- margin-bottom: 15px;
+ margin-bottom: 16px;
overflow: hidden;
&.hidden {
@@ -266,12 +266,13 @@ code {
font-size: 14px;
color: $primary-text-color;
display: block;
- font-weight: 500;
- padding-top: 5px;
+ font-weight: 600;
+ line-height: 20px;
}
.hint {
- margin-bottom: 15px;
+ line-height: 16px;
+ margin-bottom: 12px;
}
ul {
@@ -427,7 +428,8 @@ code {
input[type='datetime-local'],
textarea {
box-sizing: border-box;
- font-size: 16px;
+ font-size: 14px;
+ line-height: 20px;
color: $primary-text-color;
display: block;
width: 100%;
@@ -435,9 +437,9 @@ code {
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%);
- border: 1px solid darken($ui-base-color, 14%);
- border-radius: 4px;
- padding: 10px;
+ border: 1px solid darken($ui-base-color, 10%);
+ border-radius: 8px;
+ padding: 10px 16px;
&::placeholder {
color: lighten($darker-text-color, 4%);
@@ -451,14 +453,13 @@ code {
border-color: $valid-value-color;
}
- &:hover {
- border-color: darken($ui-base-color, 20%);
- }
-
&:active,
&:focus {
border-color: $highlight-text-color;
- background: darken($ui-base-color, 8%);
+ }
+
+ @media screen and (width <= 600px) {
+ font-size: 16px;
}
}
@@ -524,12 +525,11 @@ code {
border-radius: 4px;
background: $ui-button-background-color;
color: $ui-button-color;
- font-size: 18px;
- line-height: inherit;
+ font-size: 15px;
+ line-height: 22px;
height: auto;
- padding: 10px;
+ padding: 7px 18px;
text-decoration: none;
- text-transform: uppercase;
text-align: center;
box-sizing: border-box;
cursor: pointer;
@@ -1220,3 +1220,74 @@ code {
background: $highlight-text-color;
}
}
+
+.app-form {
+ & > * {
+ margin-bottom: 16px;
+ }
+
+ &__avatar-input,
+ &__header-input {
+ display: block;
+ border-radius: 8px;
+ background: var(--dropdown-background-color);
+ position: relative;
+ cursor: pointer;
+
+ img {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 8px;
+ z-index: 0;
+ }
+
+ .icon {
+ position: absolute;
+ inset-inline-start: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ color: $darker-text-color;
+ z-index: 3;
+ }
+
+ &.selected .icon {
+ color: $primary-text-color;
+ transform: none;
+ inset-inline-start: auto;
+ inset-inline-end: 8px;
+ top: auto;
+ bottom: 8px;
+ }
+
+ &.invalid img {
+ outline: 1px solid $error-value-color;
+ outline-offset: -1px;
+ }
+
+ &.invalid::before {
+ display: block;
+ content: '';
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ background: rgba($error-value-color, 0.25);
+ z-index: 2;
+ border-radius: 8px;
+ }
+
+ &:hover {
+ background-color: var(--dropdown-border-color);
+ }
+ }
+
+ &__avatar-input {
+ width: 80px;
+ height: 80px;
+ }
+
+ &__header-input {
+ aspect-ratio: 580/193;
+ }
+}
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 8c6520b303..5d1292a6bd 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -6,7 +6,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
# Please update `app/javascript/mastodon/api_types/accounts.ts` when making changes to the attributes
- attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
+ attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections
@@ -112,6 +112,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
object.suspended? ? false : object.discoverable
end
+ def indexable
+ object.suspended? ? false : object.indexable
+ end
+
def moved_to_account
object.suspended? ? nil : AccountDecorator.new(object.moved_to_account)
end
diff --git a/config/routes.rb b/config/routes.rb
index 82431f6ec3..150b26cf1e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,7 +31,7 @@ Rails.application.routes.draw do
/favourites
/bookmarks
/pinned
- /start
+ /start/(*any)
/directory
/explore/(*any)
/search