Username onboarding

This commit is contained in:
Fedor Indutny 2023-02-13 10:51:41 -08:00 committed by GitHub
parent 5626cea9c3
commit f9aaf30a32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 309 additions and 3 deletions

View file

@ -71,6 +71,7 @@ export default {
},
replaceAvatar: { action: true },
saveAvatarToDisk: { action: true },
markCompletedUsernameOnboarding: { action: true },
openUsernameReservationModal: { action: true },
setUsernameEditState: { action: true },
deleteUsername: { action: true },

View file

@ -34,6 +34,7 @@ import { assertDev } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenu } from './ContextMenu';
import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody';
import {
ConversationDetailsIcon,
IconType,
@ -48,6 +49,7 @@ export enum EditState {
ProfileName = 'ProfileName',
Bio = 'Bio',
Username = 'Username',
UsernameOnboarding = 'UsernameOnboarding',
}
type PropsExternalType = {
@ -67,11 +69,13 @@ export type PropsDataType = {
conversationId: string;
familyName?: string;
firstName: string;
hasCompletedUsernameOnboarding: boolean;
i18n: LocalizerType;
isUsernameFlagEnabled: boolean;
userAvatarData: ReadonlyArray<AvatarDataType>;
username?: string;
usernameEditState: UsernameEditState;
markCompletedUsernameOnboarding: () => void;
} & Pick<EmojiButtonProps, 'recentEmojis' | 'skinTone'>;
type PropsActionType = {
@ -124,8 +128,10 @@ export function ProfileEditor({
deleteUsername,
familyName,
firstName,
hasCompletedUsernameOnboarding,
i18n,
isUsernameFlagEnabled,
markCompletedUsernameOnboarding,
onEditStateChanged,
onProfileChanged,
onSetSkinTone,
@ -481,6 +487,16 @@ export function ProfileEditor({
content = renderEditUsernameModalBody({
onClose: () => setEditState(EditState.None),
});
} else if (editState === EditState.UsernameOnboarding) {
content = (
<UsernameOnboardingModalBody
i18n={i18n}
onNext={() => {
markCompletedUsernameOnboarding();
setEditState(EditState.Username);
}}
/>
);
} else if (editState === EditState.None) {
let maybeUsernameRow: JSX.Element | undefined;
if (isUsernameFlagEnabled) {
@ -560,7 +576,11 @@ export function ProfileEditor({
info={username && generateUsernameLink(username, { short: true })}
onClick={() => {
openUsernameReservationModal();
setEditState(EditState.Username);
if (username || hasCompletedUsernameOnboarding) {
setEditState(EditState.Username);
} else {
setEditState(EditState.UsernameOnboarding);
}
}}
actions={actions}
/>

View file

@ -32,11 +32,12 @@ export function ProfileEditorModal({
toggleProfileEditorHasError,
...restProps
}: PropsType): JSX.Element {
const MODAL_TITLES_BY_EDIT_STATE: Record<EditState, string> = {
const MODAL_TITLES_BY_EDIT_STATE: Record<EditState, string | undefined> = {
[EditState.BetterAvatar]: i18n('ProfileEditorModal--avatar'),
[EditState.Bio]: i18n('ProfileEditorModal--about'),
[EditState.None]: i18n('ProfileEditorModal--profile'),
[EditState.ProfileName]: i18n('ProfileEditorModal--name'),
[EditState.UsernameOnboarding]: undefined,
[EditState.Username]: i18n('ProfileEditorModal--username'),
};

View file

@ -0,0 +1,37 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { Meta, Story } from '@storybook/react';
import enMessages from '../../_locales/en/messages.json';
import { setupI18n } from '../util/setupI18n';
import type { PropsType } from './UsernameOnboardingModalBody';
import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody';
const i18n = setupI18n('en', enMessages);
export default {
component: UsernameOnboardingModalBody,
title: 'Components/UsernameOnboardingModalBody',
argTypes: {
i18n: {
defaultValue: i18n,
},
onNext: { action: true },
},
} as Meta;
type ArgsType = PropsType;
// eslint-disable-next-line react/function-component-definition
const Template: Story<ArgsType> = args => {
return <UsernameOnboardingModalBody {...args} />;
};
export const Normal = Template.bind({});
Normal.args = {};
Normal.story = {
name: 'normal',
};

View file

@ -0,0 +1,68 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Button } from './Button';
export type PropsType = Readonly<{
i18n: LocalizerType;
onNext: () => void;
}>;
const CLASS = 'UsernameOnboardingModalBody';
const SUPPORT_URL = 'https://support.signal.org/hc/articles/5389476324250';
export function UsernameOnboardingModalBody({
i18n,
onNext,
}: PropsType): JSX.Element {
return (
<div className={CLASS}>
<div className={`${CLASS}__large-at`} />
<div className={`${CLASS}__title`}>{i18n(`icu:${CLASS}__title`)}</div>
<div className={`${CLASS}__row`}>
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--number`} />
<div className={`${CLASS}__row__body`}>
{i18n(`icu:${CLASS}__row__number`)}
</div>
</div>
<div className={`${CLASS}__row`}>
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--link`} />
<div className={`${CLASS}__row__body`}>
{i18n(`icu:${CLASS}__row__link`)}
</div>
</div>
<div className={`${CLASS}__row`}>
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--lock`} />
<div className={`${CLASS}__row__body`}>
{i18n(`icu:${CLASS}__row__lock`)}
</div>
</div>
<div className={`${CLASS}__row ${CLASS}__row--center`}>
<a
className={`${CLASS}__learn-more`}
href={SUPPORT_URL}
rel="noreferrer"
target="_blank"
>
{i18n(`icu:${CLASS}__learn-more`)}
</a>
</div>
<Button className={`${CLASS}__submit`} onClick={onNext}>
{i18n(`icu:${CLASS}__continue`)}
</Button>
</div>
);
}