remove onboarding wizard
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -21,7 +21,6 @@ import { createAccount } from 'pl-fe/actions/accounts';
|
||||
import { createApp } from 'pl-fe/actions/apps';
|
||||
import { fetchMeSuccess, fetchMeFail } from 'pl-fe/actions/me';
|
||||
import { obtainOAuthToken, revokeOAuthToken } from 'pl-fe/actions/oauth';
|
||||
import { startOnboarding } from 'pl-fe/actions/onboarding';
|
||||
import * as BuildConfig from 'pl-fe/build-config';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
import { selectAccount } from 'pl-fe/selectors';
|
||||
@ -306,7 +305,6 @@ const register = (params: CreateAccountParams) =>
|
||||
if ('identifier' in response) {
|
||||
toast.info(response.message);
|
||||
} else {
|
||||
dispatch(startOnboarding());
|
||||
return dispatch(authLoggedIn(response, app));
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
import { mockStore, mockWindowProperty, rootState } from 'pl-fe/jest/test-helpers';
|
||||
|
||||
import { checkOnboardingStatus, startOnboarding, endOnboarding } from './onboarding';
|
||||
|
||||
describe('checkOnboarding()', () => {
|
||||
let mockGetItem: any;
|
||||
|
||||
mockWindowProperty('localStorage', {
|
||||
getItem: (key: string) => mockGetItem(key),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetItem = vi.fn().mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('does nothing if localStorage item is not set', async() => {
|
||||
mockGetItem = vi.fn().mockReturnValue(null);
|
||||
|
||||
const state = { ...rootState };
|
||||
state.onboarding.needsOnboarding = false;
|
||||
const store = mockStore(state);
|
||||
|
||||
await store.dispatch(checkOnboardingStatus());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does nothing if localStorage item is invalid', async() => {
|
||||
mockGetItem = vi.fn().mockReturnValue('invalid');
|
||||
|
||||
const state = { ...rootState };
|
||||
state.onboarding.needsOnboarding = false;
|
||||
const store = mockStore(state);
|
||||
|
||||
await store.dispatch(checkOnboardingStatus());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('dispatches the correct action', async() => {
|
||||
mockGetItem = vi.fn().mockReturnValue('1');
|
||||
|
||||
const state = { ...rootState };
|
||||
state.onboarding.needsOnboarding = false;
|
||||
const store = mockStore(state);
|
||||
|
||||
await store.dispatch(checkOnboardingStatus());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startOnboarding()', () => {
|
||||
let mockSetItem: any;
|
||||
|
||||
mockWindowProperty('localStorage', {
|
||||
setItem: (key: string, value: string) => mockSetItem(key, value),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetItem = vi.fn();
|
||||
});
|
||||
|
||||
it('dispatches the correct action', async() => {
|
||||
const state = { ...rootState };
|
||||
state.onboarding.needsOnboarding = false;
|
||||
const store = mockStore(state);
|
||||
|
||||
await store.dispatch(startOnboarding());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||
expect(mockSetItem.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endOnboarding()', () => {
|
||||
let mockRemoveItem: any;
|
||||
|
||||
mockWindowProperty('localStorage', {
|
||||
removeItem: (key: string) => mockRemoveItem(key),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockRemoveItem = vi.fn();
|
||||
});
|
||||
|
||||
it('dispatches the correct action', async() => {
|
||||
const state = { ...rootState };
|
||||
state.onboarding.needsOnboarding = false;
|
||||
const store = mockStore(state);
|
||||
|
||||
await store.dispatch(endOnboarding());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([{ type: 'ONBOARDING_END' }]);
|
||||
expect(mockRemoveItem.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -1,41 +0,0 @@
|
||||
const ONBOARDING_START = 'ONBOARDING_START';
|
||||
const ONBOARDING_END = 'ONBOARDING_END';
|
||||
|
||||
const ONBOARDING_LOCAL_STORAGE_KEY = 'plfe:onboarding';
|
||||
|
||||
type OnboardingStartAction = {
|
||||
type: typeof ONBOARDING_START;
|
||||
}
|
||||
|
||||
type OnboardingEndAction = {
|
||||
type: typeof ONBOARDING_END;
|
||||
}
|
||||
|
||||
type OnboardingActions = OnboardingStartAction | OnboardingEndAction
|
||||
|
||||
const checkOnboardingStatus = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||
const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1';
|
||||
|
||||
if (needsOnboarding) {
|
||||
dispatch({ type: ONBOARDING_START });
|
||||
}
|
||||
};
|
||||
|
||||
const startOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||
localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1');
|
||||
dispatch({ type: ONBOARDING_START });
|
||||
};
|
||||
|
||||
const endOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||
localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY);
|
||||
dispatch({ type: ONBOARDING_END });
|
||||
};
|
||||
|
||||
export {
|
||||
type OnboardingActions,
|
||||
ONBOARDING_END,
|
||||
ONBOARDING_START,
|
||||
checkOnboardingStatus,
|
||||
endOnboarding,
|
||||
startOnboarding,
|
||||
};
|
||||
@ -1,122 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import { endOnboarding } from 'pl-fe/actions/onboarding';
|
||||
import LandingGradient from 'pl-fe/components/landing-gradient';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
|
||||
import AvatarSelectionStep from './steps/avatar-selection-step';
|
||||
import BioStep from './steps/bio-step';
|
||||
import CompletedStep from './steps/completed-step';
|
||||
import CoverPhotoSelectionStep from './steps/cover-photo-selection-step';
|
||||
import DisplayNameStep from './steps/display-name-step';
|
||||
import FediverseStep from './steps/fediverse-step';
|
||||
import SuggestedAccountsStep from './steps/suggested-accounts-step';
|
||||
|
||||
const OnboardingWizard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const { theme } = useSettings();
|
||||
|
||||
const [currentStep, setCurrentStep] = React.useState<number>(0);
|
||||
|
||||
const handleSwipe = (nextStep: number) => {
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const handlePreviousStep = () => {
|
||||
setCurrentStep((prevStep) => Math.max(0, prevStep - 1));
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
setCurrentStep((prevStep) => Math.min(prevStep + 1, steps.length - 1));
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
const steps = [
|
||||
<AvatarSelectionStep onNext={handleNextStep} />,
|
||||
<DisplayNameStep onNext={handleNextStep} />,
|
||||
<BioStep onNext={handleNextStep} />,
|
||||
<CoverPhotoSelectionStep onNext={handleNextStep} />,
|
||||
<SuggestedAccountsStep onNext={handleNextStep} />,
|
||||
];
|
||||
|
||||
if (features.federating){
|
||||
steps.push(<FediverseStep onNext={handleNextStep} />);
|
||||
}
|
||||
|
||||
steps.push(<CompletedStep onComplete={handleComplete} />);
|
||||
|
||||
const handleKeyUp = ({ key }: KeyboardEvent): void => {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
handlePreviousStep();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
handleNextStep();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDotClick = (nextStep: number) => {
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-testid='onboarding-wizard'>
|
||||
{(theme?.backgroundGradient ?? true) && <LandingGradient />}
|
||||
|
||||
<main className='flex h-screen flex-col overflow-x-hidden'>
|
||||
<div className='flex h-full flex-col items-center justify-center'>
|
||||
<ReactSwipeableViews animateHeight index={currentStep} onChangeIndex={handleSwipe}>
|
||||
{steps.map((step, i) => (
|
||||
<div key={i} className='w-full max-w-[100vw] py-6 sm:mx-auto sm:max-w-lg md:max-w-2xl'>
|
||||
<div
|
||||
className={clsx({
|
||||
'transition-opacity ease-linear': true,
|
||||
'opacity-0 duration-500': currentStep !== i,
|
||||
'opacity-100 duration-75': currentStep === i,
|
||||
})}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ReactSwipeableViews>
|
||||
|
||||
<HStack space={3} alignItems='center' justifyContent='center' className='relative'>
|
||||
{steps.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
tabIndex={0}
|
||||
onClick={() => handleDotClick(i)}
|
||||
className={clsx({
|
||||
'w-5 h-5 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
||||
'bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-700/75 hover:bg-gray-400': i !== currentStep,
|
||||
'bg-primary-600': i === currentStep,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { OnboardingWizard as default };
|
||||
@ -1,122 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'pl-fe/actions/me';
|
||||
import { BigCard } from 'pl-fe/components/big-card';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { isDefaultAvatar } from 'pl-fe/utils/accounts';
|
||||
import resizeImage from 'pl-fe/utils/resize-image';
|
||||
|
||||
import type { PlfeResponse } from 'pl-fe/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>();
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [isDisabled, setDisabled] = React.useState<boolean>(true);
|
||||
const isDefault = account ? isDefaultAvatar(account.avatar) : false;
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const maxPixels = 400 * 400;
|
||||
const rawFile = event.target.files?.item(0);
|
||||
|
||||
if (!rawFile) return;
|
||||
|
||||
resizeImage(rawFile, maxPixels).then((file) => {
|
||||
const url = file ? URL.createObjectURL(file) : account?.avatar as string;
|
||||
|
||||
setSelectedFile(url);
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ avatar: rawFile }));
|
||||
|
||||
return Promise.all([credentials]).then(() => {
|
||||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: { response: PlfeResponse }) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error(error.response.json.error.replace('Validation failed: ', ''));
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.avatar.title' defaultMessage='Choose a profile picture' />}
|
||||
subtitle={<FormattedMessage id='onboarding.avatar.subtitle' defaultMessage='Just have fun with it.' />}
|
||||
>
|
||||
<Stack space={10}>
|
||||
<div className='relative mx-auto rounded-lg bg-gray-200'>
|
||||
{account && (
|
||||
<Avatar src={selectedFile || account.avatar} alt={account.avatar_description} size={175} isCat={account.is_cat} />
|
||||
)}
|
||||
|
||||
{isSubmitting && (
|
||||
<div className='absolute inset-0 flex items-center justify-center rounded-lg bg-white/80 dark:bg-primary-900/80'>
|
||||
<Spinner withText={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={openFilePicker}
|
||||
type='button'
|
||||
className={clsx({
|
||||
'absolute bottom-3 right-2 p-1 bg-primary-600 rounded-lg ring-2 ring-white dark:ring-primary-900 hover:bg-primary-700': true,
|
||||
'opacity-50 pointer-events-none': isSubmitting,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Icon src={require('@phosphor-icons/core/regular/plus.svg')} className='size-5 text-white' />
|
||||
</button>
|
||||
|
||||
<input type='file' className='hidden' ref={fileInput} onChange={handleFileChange} />
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button block theme='primary' type='button' onClick={onNext} disabled={isDefault && isDisabled || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isDisabled && (
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { AvatarSelectionStep as default };
|
||||
@ -1,97 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'pl-fe/actions/me';
|
||||
import { BigCard } from 'pl-fe/components/big-card';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Textarea from 'pl-fe/components/ui/textarea';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { PlfeResponse } from 'pl-fe/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const BioStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { account } = useOwnAccount();
|
||||
const [value, setValue] = React.useState<string>(account?.__meta.source?.note ?? '');
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [errors, setErrors] = React.useState<string[]>([]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ note: value }));
|
||||
|
||||
Promise.all([credentials])
|
||||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: { response: PlfeResponse }) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([error.response.json.error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.note.title' defaultMessage='Write a short bio' />}
|
||||
subtitle={<FormattedMessage id='onboarding.note.subtitle' defaultMessage='You can always edit this later.' />}
|
||||
>
|
||||
<Stack space={5}>
|
||||
<div>
|
||||
<FormGroup
|
||||
hintText={<FormattedMessage id='onboarding.bio.hint' defaultMessage='Max 500 characters' />}
|
||||
labelText={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Textarea
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={intl.formatMessage(messages.bioPlaceholder)}
|
||||
value={value}
|
||||
maxLength={500}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { BioStep as default };
|
||||
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Card, { CardBody } from 'pl-fe/components/ui/card';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
|
||||
const CompletedStep = ({ onComplete }: { onComplete: () => void }) => (
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<Icon strokeWidth={1} src={require('@tabler/icons/outline/confetti.svg')} className='mx-auto size-16 text-primary-600 dark:text-primary-400' />
|
||||
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.finished.title' defaultMessage='Onboarding complete' />
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='onboarding.finished.message'
|
||||
defaultMessage='We are very excited to welcome you to our community! Tap the button below to get started.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className='mx-auto pt-10 sm:w-2/3 md:w-1/2'>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
onClick={onComplete}
|
||||
>
|
||||
<FormattedMessage id='onboarding.view_feed' defaultMessage='View feed' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export { CompletedStep as default };
|
||||
@ -1,152 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'pl-fe/actions/me';
|
||||
import { BigCard } from 'pl-fe/components/big-card';
|
||||
import StillImage from 'pl-fe/components/still-image';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||
import resizeImage from 'pl-fe/utils/resize-image';
|
||||
|
||||
import type { PlfeResponse } from 'pl-fe/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>();
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [isDisabled, setDisabled] = React.useState<boolean>(true);
|
||||
const isDefault = account ? isDefaultHeader(account.header) : false;
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const maxPixels = 1920 * 1080;
|
||||
const rawFile = event.target.files?.item(0);
|
||||
|
||||
if (!rawFile) return;
|
||||
|
||||
resizeImage(rawFile, maxPixels).then((file) => {
|
||||
const url = file ? URL.createObjectURL(file) : account?.header as string;
|
||||
|
||||
setSelectedFile(url);
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ header: file }));
|
||||
|
||||
Promise.all([credentials]).then(() => {
|
||||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: { response: PlfeResponse }) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error(error.response.json.error.replace('Validation failed: ', ''));
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.header.title' defaultMessage='Pick a cover image' />}
|
||||
subtitle={<FormattedMessage id='onboarding.header.subtitle' defaultMessage='This will be shown at the top of your profile.' />}
|
||||
>
|
||||
<Stack space={10}>
|
||||
<div className='rounded-lg border border-solid border-gray-200 dark:border-gray-800'>
|
||||
<div
|
||||
role='button'
|
||||
className='relative flex h-24 items-center justify-center rounded-t-md bg-gray-200 dark:bg-gray-800'
|
||||
>
|
||||
{selectedFile || account?.header && (
|
||||
<StillImage
|
||||
src={selectedFile || account.header}
|
||||
alt={account.header_description || intl.formatMessage(messages.header)}
|
||||
className='absolute inset-0 rounded-t-md object-cover'
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSubmitting && (
|
||||
<div
|
||||
className='absolute inset-0 flex items-center justify-center rounded-t-md bg-white/80 dark:bg-primary-900/80'
|
||||
>
|
||||
<Spinner withText={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={openFilePicker}
|
||||
type='button'
|
||||
className={clsx({
|
||||
'absolute -top-3 -right-3 p-1 bg-primary-600 rounded-full ring-2 ring-white dark:ring-primary-900 hover:bg-primary-700': true,
|
||||
'opacity-50 pointer-events-none': isSubmitting,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Icon src={require('@phosphor-icons/core/regular/plus.svg')} className='size-5 text-white' />
|
||||
</button>
|
||||
|
||||
<input type='file' className='hidden' ref={fileInput} onChange={handleFileChange} />
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col px-4 pb-4'>
|
||||
{account && (
|
||||
<Avatar
|
||||
src={account.avatar}
|
||||
alt={account.avatar_description}
|
||||
isCat={account.is_cat}
|
||||
size={64}
|
||||
className='-mt-8 mb-2 ring-2 ring-white dark:ring-primary-800'
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text weight='bold' size='sm'>{account?.display_name}</Text>
|
||||
<Text theme='muted' size='sm'>@{account?.username}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button block theme='primary' type='button' onClick={onNext} disabled={isDefault && isDisabled || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isDisabled && (
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { CoverPhotoSelectionStep as default };
|
||||
@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'pl-fe/actions/me';
|
||||
import { BigCard } from 'pl-fe/components/big-card';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { PlfeResponse } from 'pl-fe/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
usernamePlaceholder: { id: 'onboarding.display_name.placeholder', defaultMessage: 'Eg. John Smith' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { account } = useOwnAccount();
|
||||
const [value, setValue] = React.useState<string>(account?.display_name || '');
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [errors, setErrors] = React.useState<string[]>([]);
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
const isValid = trimmedValue.length > 0;
|
||||
const isDisabled = !isValid || value.length > 30;
|
||||
|
||||
const hintText = React.useMemo(() => {
|
||||
const charsLeft = 30 - value.length;
|
||||
const suffix = charsLeft === 1 ? 'character remaining' : 'characters remaining';
|
||||
|
||||
return `${charsLeft} ${suffix}`;
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ display_name: value }));
|
||||
|
||||
Promise.all([credentials])
|
||||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: { response: PlfeResponse }) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([error.response.json.error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.display_name.title' defaultMessage='Choose a display name' />}
|
||||
subtitle={<FormattedMessage id='onboarding.display_name.subtitle' defaultMessage='You can always edit this later.' />}
|
||||
>
|
||||
<Stack space={5}>
|
||||
<FormGroup
|
||||
hintText={hintText}
|
||||
labelText={<FormattedMessage id='onboarding.display_name.label' defaultMessage='Display name' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Input
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={intl.formatMessage(messages.usernamePlaceholder)}
|
||||
type='text'
|
||||
value={value}
|
||||
maxLength={30}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isDisabled || isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { DisplayNameStep as default };
|
||||
@ -1,93 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'pl-fe/components/account';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Card, { CardBody } from 'pl-fe/components/ui/card';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
|
||||
const FediverseStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { account } = useOwnAccount();
|
||||
const instance = useInstance();
|
||||
|
||||
return (
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<Icon strokeWidth={1} src={require('@phosphor-icons/core/regular/planet.svg')} className='mx-auto size-16 text-primary-600 dark:text-primary-400' />
|
||||
|
||||
<Text size='2xl' weight='bold'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.title'
|
||||
defaultMessage='{siteTitle} is just one part of the Fediverse'
|
||||
values={{
|
||||
siteTitle: instance.title,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Stack space={4}>
|
||||
<div className='border-b border-solid border-gray-200 pb-2 dark:border-gray-800 sm:pb-5'>
|
||||
<Stack space={4}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.message'
|
||||
defaultMessage='The Fediverse is a social network made up of thousands of diverse and independently-run social media sites (aka "servers"). You can follow users — and like, repost, and reply to posts — from most other Fediverse servers, because they can communicate with {siteTitle}.'
|
||||
values={{
|
||||
siteTitle: instance.title,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.trailer'
|
||||
defaultMessage='Because it is distributed and anyone can run their own server, the Fediverse is resilient and open. If you choose to join another server or set up your own, you can interact with the same people and continue on the same social graph.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{account && (
|
||||
<div className='rounded-lg bg-primary-50 p-4 text-center dark:bg-gray-800'>
|
||||
<Account account={account} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.its_you'
|
||||
defaultMessage='This is you! Other people can follow you from other servers by using your full @-handle.'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.other_instances'
|
||||
defaultMessage='When browsing your timeline, pay attention to the full username after the second @ symbol to know which server a post is from.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<div className='mx-auto pt-10 sm:w-2/3 md:w-1/2'>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
onClick={onNext}
|
||||
>
|
||||
<FormattedMessage id='onboarding.fediverse.next' defaultMessage='Next' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export { FediverseStep as default };
|
||||
@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { BigCard } from 'pl-fe/components/big-card';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import { useOnboardingSuggestions } from 'pl-fe/queries/suggestions';
|
||||
|
||||
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { data, isFetching } = useOnboardingSuggestions();
|
||||
|
||||
const renderSuggestions = () => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col sm:pb-10 sm:pt-4'>
|
||||
<ScrollableList
|
||||
scrollKey='suggestedAccounts'
|
||||
isLoading={isFetching}
|
||||
style={{ height: 320 }}
|
||||
useWindowScroll={false}
|
||||
>
|
||||
{data.map((suggestion) => (
|
||||
<div key={suggestion.account.id} className='py-2'>
|
||||
<AccountContainer
|
||||
id={suggestion.account.id}
|
||||
showAccountHoverCard={false}
|
||||
withLinkToProfile={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEmpty = () => (
|
||||
<div className='my-2 rounded-lg bg-primary-50 p-8 text-center dark:bg-gray-800'>
|
||||
<Text>
|
||||
<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.' />
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBody = () => {
|
||||
if (!data || data.length === 0) {
|
||||
return renderEmpty();
|
||||
} else {
|
||||
return renderSuggestions();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.suggestions.title' defaultMessage='Suggested accounts' />}
|
||||
subtitle={<FormattedMessage id='onboarding.suggestions.subtitle' defaultMessage='Here are a few of the most popular accounts you might like.' />}
|
||||
>
|
||||
{renderBody()}
|
||||
|
||||
<Stack>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
onClick={onNext}
|
||||
>
|
||||
<FormattedMessage id='onboarding.done' defaultMessage='Done' />
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { SuggestedAccountsStep as default };
|
||||
@ -134,7 +134,6 @@ export const EventHeader = lazy(() => import('pl-fe/features/event/components/ev
|
||||
export const LightningAddress = lazy(() => import('pl-fe/features/crypto-donate/components/lightning-address'));
|
||||
export const MfaForm = lazy(() => import('pl-fe/features/security/mfa-form'));
|
||||
export const ModalRoot = lazy(() => import('pl-fe/features/ui/components/modal-root'));
|
||||
export const OnboardingWizard = lazy(() => import('pl-fe/features/onboarding/onboarding-wizard'));
|
||||
export const AccountHoverCard = lazy(() => import('pl-fe/components/account-hover-card'));
|
||||
export const StatusHoverCard = lazy(() => import('pl-fe/components/status-hover-card'));
|
||||
export const Video = lazy(() => import('pl-fe/features/video'));
|
||||
|
||||
@ -8,10 +8,8 @@ import { ScrollContext } from 'react-router-scroll-4';
|
||||
import * as BuildConfig from 'pl-fe/build-config';
|
||||
import LoadingScreen from 'pl-fe/components/loading-screen';
|
||||
import SiteErrorBoundary from 'pl-fe/components/site-error-boundary';
|
||||
import { ModalRoot, OnboardingWizard } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { ModalRoot } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { useCachedLocationHandler } from 'pl-fe/utils/redirect';
|
||||
|
||||
@ -24,11 +22,8 @@ const PlFeMount = () => {
|
||||
useCachedLocationHandler();
|
||||
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const { account } = useOwnAccount();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && needsOnboarding;
|
||||
const { redirectRootNoLogin, gdpr } = plFeConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
@ -59,10 +54,7 @@ const PlFeMount = () => {
|
||||
|
||||
<Route>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
{showOnboarding
|
||||
? <OnboardingWizard />
|
||||
: <UI />
|
||||
}
|
||||
<UI />
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
|
||||
@ -6,7 +6,6 @@ import { Provider } from 'react-redux';
|
||||
import { StatProvider } from 'pl-fe/contexts/stat-context';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
|
||||
import { checkOnboardingStatus } from '../actions/onboarding';
|
||||
import { preload } from '../actions/preload';
|
||||
import { store } from '../store';
|
||||
|
||||
@ -17,9 +16,6 @@ import PlFeMount from './pl-fe-mount';
|
||||
// Preload happens synchronously
|
||||
store.dispatch(preload() as any);
|
||||
|
||||
// This happens synchronously
|
||||
store.dispatch(checkOnboardingStatus() as any);
|
||||
|
||||
/** The root React node of the application. */
|
||||
const PlFe: React.FC = () => (
|
||||
<>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
"account.follows": "Following",
|
||||
"account.follows.empty": "This user doesn't follow anyone yet.",
|
||||
"account.follows_you": "Follows you",
|
||||
"account.header.alt": "Profile header",
|
||||
"account.header.alt": "Header",
|
||||
"account.header.description": "Header description",
|
||||
"account.hide_reblogs": "Hide reposts from @{name}",
|
||||
"account.instance_favicon": "Visit {domain} timeline",
|
||||
@ -816,7 +816,6 @@
|
||||
"empty_column.favourited_statuses": "You don't have any liked posts yet. When you like one, it will show up here.",
|
||||
"empty_column.favourites": "No one has liked this post yet. When someone does, they will show up here.",
|
||||
"empty_column.filters": "You haven't created any muted words yet.",
|
||||
"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 haven't followed any hashtag yet.",
|
||||
"empty_column.group": "There are no posts in this group yet.",
|
||||
@ -1300,34 +1299,6 @@
|
||||
"notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}",
|
||||
"oauth_consumer.tooltip": "Sign in with {provider}",
|
||||
"oauth_consumers.title": "Other ways to sign in",
|
||||
"onboarding.avatar.subtitle": "Just have fun with it.",
|
||||
"onboarding.avatar.title": "Choose a profile picture",
|
||||
"onboarding.bio.hint": "Max 500 characters",
|
||||
"onboarding.bio.placeholder": "Tell the world a little about yourself…",
|
||||
"onboarding.display_name.label": "Display name",
|
||||
"onboarding.display_name.placeholder": "Eg. John Smith",
|
||||
"onboarding.display_name.subtitle": "You can always edit this later.",
|
||||
"onboarding.display_name.title": "Choose a display name",
|
||||
"onboarding.done": "Done",
|
||||
"onboarding.error": "An unexpected error occurred. Please try again or skip this step.",
|
||||
"onboarding.fediverse.its_you": "This is you! Other people can follow you from other servers by using your full @-handle.",
|
||||
"onboarding.fediverse.message": "The Fediverse is a social network made up of thousands of diverse and independently-run social media sites (aka \"servers\"). You can follow users — and like, repost, and reply to posts — from most other Fediverse servers, because they can communicate with {siteTitle}.",
|
||||
"onboarding.fediverse.next": "Next",
|
||||
"onboarding.fediverse.other_instances": "When browsing your timeline, pay attention to the full username after the second @ symbol to know which server a post is from.",
|
||||
"onboarding.fediverse.title": "{siteTitle} is just one part of the Fediverse",
|
||||
"onboarding.fediverse.trailer": "Because it is distributed and anyone can run their own server, the Fediverse is resilient and open. If you choose to join another server or set up your own, you can interact with the same people and continue on the same social graph.",
|
||||
"onboarding.finished.message": "We are very excited to welcome you to our community! Tap the button below to get started.",
|
||||
"onboarding.finished.title": "Onboarding complete",
|
||||
"onboarding.header.subtitle": "This will be shown at the top of your profile.",
|
||||
"onboarding.header.title": "Pick a cover image",
|
||||
"onboarding.next": "Next",
|
||||
"onboarding.note.subtitle": "You can always edit this later.",
|
||||
"onboarding.note.title": "Write a short bio",
|
||||
"onboarding.saving": "Saving…",
|
||||
"onboarding.skip": "Skip for now",
|
||||
"onboarding.suggestions.subtitle": "Here are a few of the most popular accounts you might like.",
|
||||
"onboarding.suggestions.title": "Suggested accounts",
|
||||
"onboarding.view_feed": "View feed",
|
||||
"password_reset.confirmation": "Check your email for confirmation.",
|
||||
"password_reset.fields.email_placeholder": "E-mail address",
|
||||
"password_reset.fields.username_placeholder": "Email or username",
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import { __stub } from 'pl-fe/api';
|
||||
import { renderHook, waitFor } from 'pl-fe/jest/test-helpers';
|
||||
|
||||
import { useOnboardingSuggestions } from './suggestions';
|
||||
|
||||
describe('useOnboardingSuggestions', () => {
|
||||
describe('with a successful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions')
|
||||
.reply(200, [
|
||||
{ source: 'staff', account: { id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg' } },
|
||||
{ source: 'staff', account: { id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg' } },
|
||||
], {
|
||||
link: '<https://example.com/api/v2/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useOnboardingSuggestions());
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.data?.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useOnboardingSuggestions());
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -51,33 +51,4 @@ const useDismissSuggestion = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const useOnboardingSuggestions = () => {
|
||||
const client = useClient();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getSuggestions = async () => {
|
||||
const response = await client.myAccount.getSuggestions();
|
||||
|
||||
const accounts = response.map(({ account }) => account);
|
||||
const accountIds = accounts.map((account) => account.id);
|
||||
dispatch(importEntities({ accounts }));
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const result = useQuery({
|
||||
queryKey: ['suggestions', 'v2'],
|
||||
queryFn: () => getSuggestions(),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
...result,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion };
|
||||
export { useSuggestions, useDismissSuggestion };
|
||||
|
||||
@ -15,7 +15,6 @@ import instance from './instance';
|
||||
import me from './me';
|
||||
import meta from './meta';
|
||||
import notifications from './notifications';
|
||||
import onboarding from './onboarding';
|
||||
import pending_statuses from './pending-statuses';
|
||||
import plfe from './pl-fe';
|
||||
import polls from './polls';
|
||||
@ -37,7 +36,6 @@ const reducers = {
|
||||
me,
|
||||
meta,
|
||||
notifications,
|
||||
onboarding,
|
||||
pending_statuses,
|
||||
plfe,
|
||||
polls,
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { ONBOARDING_START, ONBOARDING_END } from 'pl-fe/actions/onboarding';
|
||||
|
||||
import reducer from './onboarding';
|
||||
|
||||
describe('onboarding reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual({
|
||||
needsOnboarding: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('ONBOARDING_START', () => {
|
||||
it('sets "needsOnboarding" to "true"', () => {
|
||||
const initialState = { needsOnboarding: false };
|
||||
const action = { type: ONBOARDING_START } as any;
|
||||
expect(reducer(initialState, action).needsOnboarding).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ONBOARDING_END', () => {
|
||||
it('sets "needsOnboarding" to "false"', () => {
|
||||
const initialState = { needsOnboarding: true };
|
||||
const action = { type: ONBOARDING_END } as any;
|
||||
expect(reducer(initialState, action).needsOnboarding).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,24 +0,0 @@
|
||||
import { ONBOARDING_START, ONBOARDING_END } from 'pl-fe/actions/onboarding';
|
||||
|
||||
import type { OnboardingActions } from 'pl-fe/actions/onboarding';
|
||||
|
||||
type OnboardingState = {
|
||||
needsOnboarding: boolean;
|
||||
}
|
||||
|
||||
const initialState: OnboardingState = {
|
||||
needsOnboarding: false,
|
||||
};
|
||||
|
||||
const onboarding = (state: OnboardingState = initialState, action: OnboardingActions): OnboardingState => {
|
||||
switch (action.type) {
|
||||
case ONBOARDING_START:
|
||||
return { ...state, needsOnboarding: true };
|
||||
case ONBOARDING_END:
|
||||
return { ...state, needsOnboarding: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { onboarding as default };
|
||||
Reference in New Issue
Block a user