-
-
-
- {loadedContent}
-
-
-
{chunks} }} />
-
-
-
-
+
+
-
- );
- }
-}
+
+ {loadedContent}
+
-export default connect(mapStateToProps)(Follows);
+
{chunks} }} />
+
+
+
+
+
+ >
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/onboarding/index.jsx b/app/javascript/flavours/glitch/features/onboarding/index.jsx
index 2729e760f2..dbf1ba1f61 100644
--- a/app/javascript/flavours/glitch/features/onboarding/index.jsx
+++ b/app/javascript/flavours/glitch/features/onboarding/index.jsx
@@ -1,153 +1,90 @@
-import PropTypes from 'prop-types';
-import React from 'react';
+import { useCallback } from 'react';
-import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
-import { Link, withRouter } from 'react-router-dom';
+import { Link, Switch, Route, useHistory } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
import { ReactComponent as AccountCircleIcon } from '@material-symbols/svg-600/outlined/account_circle.svg';
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 { ReactComponent as EditNoteIcon } from '@material-symbols/svg-600/outlined/edit_note.svg';
import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg';
-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 { Icon } from 'flavours/glitch/components/icon';
import Column from 'flavours/glitch/features/ui/components/column';
import { me } from 'flavours/glitch/initial_state';
-import { makeGetAccount } from 'flavours/glitch/selectors';
+import { useAppSelector } from 'flavours/glitch/store';
import { assetHost } from 'flavours/glitch/utils/config';
-import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
-import Step from './components/step';
-import Follows from './follows';
-import Share from './share';
+import { Step } from './components/step';
+import { Follows } from './follows';
+import { Profile } from './profile';
+import { Share } from './share';
const messages = defineMessages({
template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
});
-const mapStateToProps = () => {
- const getAccount = makeGetAccount();
+const Onboarding = () => {
+ const account = useAppSelector(state => state.getIn(['accounts', me]));
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const history = useHistory();
- return state => ({
- account: getAccount(state, me),
- });
+ const handleComposeClick = useCallback(() => {
+ dispatch(focusCompose(history, intl.formatMessage(messages.template)));
+ }, [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={} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
-class Onboarding extends ImmutablePureComponent {
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- account: ImmutablePropTypes.record,
- ...WithRouterPropTypes,
- };
-
- state = {
- step: null,
- profileClicked: false,
- shareClicked: false,
- };
-
- handleClose = () => {
- const { dispatch, history } = this.props;
-
- dispatch(closeOnboarding());
- history.push('/home');
- };
-
- handleProfileClick = () => {
- this.setState({ profileClicked: true });
- };
-
- handleFollowClick = () => {
- this.setState({ step: 'follows' });
- };
-
- handleComposeClick = () => {
- const { dispatch, intl, history } = this.props;
-
- dispatch(focusCompose(history, intl.formatMessage(messages.template)));
- };
-
- 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' 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/flavours/glitch/features/onboarding/profile.jsx b/app/javascript/flavours/glitch/features/onboarding/profile.jsx
new file mode 100644
index 0000000000..571e746783
--- /dev/null
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/accounts';
+import { Button } from 'flavours/glitch/components/button';
+import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
+import { Icon } from 'flavours/glitch/components/icon';
+import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
+import { me } from 'flavours/glitch/initial_state';
+import { useAppSelector } from 'flavours/glitch/store';
+import { unescapeHTML } from 'flavours/glitch/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/flavours/glitch/features/onboarding/share.jsx b/app/javascript/flavours/glitch/features/onboarding/share.jsx
index 7c35c9a492..b5732c0abc 100644
--- a/app/javascript/flavours/glitch/features/onboarding/share.jsx
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/components/column';
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
import { Icon } from 'flavours/glitch/components/icon';
import { me, domain } from 'flavours/glitch/initial_state';
+import { useAppSelector } from 'flavours/glitch/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/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index dc3a7ce37b..839e06eea2 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -219,7 +219,7 @@ class SwitchingColumnsArea extends PureComponent {
-
+
diff --git a/app/javascript/flavours/glitch/models/account.ts b/app/javascript/flavours/glitch/models/account.ts
index 9d1bc20d06..8500009e0f 100644
--- a/app/javascript/flavours/glitch/models/account.ts
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 5a2962e881..97e6ff959e 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -898,7 +898,7 @@ $ui-header-height: 55px;
.column-title {
text-align: center;
- padding-bottom: 40px;
+ padding-bottom: 32px;
h3 {
font-size: 24px;
@@ -1083,58 +1083,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;
@@ -1211,6 +1159,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;
diff --git a/app/javascript/flavours/glitch/styles/components/misc.scss b/app/javascript/flavours/glitch/styles/components/misc.scss
index 768ac66baa..d4a78c47b1 100644
--- a/app/javascript/flavours/glitch/styles/components/misc.scss
+++ b/app/javascript/flavours/glitch/styles/components/misc.scss
@@ -855,6 +855,7 @@ body > [data-popper-placement] {
cursor: pointer;
background-color: transparent;
border: 0;
+ border-radius: 10px;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: rgba($base-overlay-background, 0);
@@ -879,81 +880,41 @@ body > [data-popper-placement] {
}
.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;
}
@@ -1239,6 +1200,17 @@ body > [data-popper-placement] {
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;
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index dd696cb4d7..9d1930ff0b 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -697,12 +697,14 @@
&__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/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index b8fc4a653f..8e93e07ec3 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -36,7 +36,7 @@ code {
}
.input {
- margin-bottom: 15px;
+ margin-bottom: 16px;
overflow: hidden;
&.hidden {
@@ -267,12 +267,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 {
@@ -428,7 +429,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%;
@@ -436,9 +438,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%);
@@ -452,14 +454,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;
}
}
@@ -525,12 +526,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;
@@ -1222,3 +1222,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;
+ }
+}