mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 17:08:57 +01:00
Username recovery improvements
This commit is contained in:
@@ -28,6 +28,9 @@ export default {
|
||||
component: EditUsernameModalBody,
|
||||
title: 'Components/EditUsernameModalBody',
|
||||
argTypes: {
|
||||
usernameCorrupted: {
|
||||
type: { name: 'boolean' },
|
||||
},
|
||||
currentUsername: {
|
||||
type: { name: 'string', required: false },
|
||||
},
|
||||
@@ -57,6 +60,8 @@ export default {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
isRootModal: false,
|
||||
usernameCorrupted: false,
|
||||
currentUsername: undefined,
|
||||
state: State.Open,
|
||||
error: undefined,
|
||||
|
||||
@@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { getNickname, getDiscriminator, isCaseChange } from '../types/Username';
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
UsernameReservationError,
|
||||
} from '../state/ducks/usernameEnums';
|
||||
import type { ReserveUsernameOptionsType } from '../state/ducks/username';
|
||||
import type { ShowToastAction } from '../state/ducks/toast';
|
||||
|
||||
import { AutoSizeInput } from './AutoSizeInput';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
@@ -24,9 +26,11 @@ import { Button, ButtonVariant } from './Button';
|
||||
export type PropsDataType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
currentUsername?: string;
|
||||
usernameCorrupted: boolean;
|
||||
reservation?: UsernameReservationType;
|
||||
error?: UsernameReservationError;
|
||||
state: UsernameReservationState;
|
||||
recoveredUsername: string | undefined;
|
||||
minNickname: number;
|
||||
maxNickname: number;
|
||||
}>;
|
||||
@@ -38,10 +42,12 @@ export type ActionPropsDataType = Readonly<{
|
||||
clearUsernameReservation(): void;
|
||||
reserveUsername(optiona: ReserveUsernameOptionsType): void;
|
||||
confirmUsername(): void;
|
||||
showToast: ShowToastAction;
|
||||
}>;
|
||||
|
||||
export type ExternalPropsDataType = Readonly<{
|
||||
onClose(): void;
|
||||
isRootModal: boolean;
|
||||
}>;
|
||||
|
||||
export type PropsType = PropsDataType &
|
||||
@@ -59,8 +65,10 @@ const DISCRIMINATOR_MAX_LENGTH = 19;
|
||||
export function EditUsernameModalBody({
|
||||
i18n,
|
||||
currentUsername,
|
||||
usernameCorrupted,
|
||||
reserveUsername,
|
||||
confirmUsername,
|
||||
showToast,
|
||||
minNickname,
|
||||
maxNickname,
|
||||
reservation,
|
||||
@@ -68,6 +76,8 @@ export function EditUsernameModalBody({
|
||||
clearUsernameReservation,
|
||||
error,
|
||||
state,
|
||||
recoveredUsername,
|
||||
isRootModal,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
const currentNickname = useMemo(() => {
|
||||
@@ -87,6 +97,7 @@ export function EditUsernameModalBody({
|
||||
const [nickname, setNickname] = useState(currentNickname);
|
||||
const [isLearnMoreVisible, setIsLearnMoreVisible] = useState(false);
|
||||
const [isConfirmingSave, setIsConfirmingSave] = useState(false);
|
||||
const [isConfirmingReset, setIsConfirmingReset] = useState(false);
|
||||
|
||||
const [customDiscriminator, setCustomDiscriminator] = useState<
|
||||
string | undefined
|
||||
@@ -148,6 +159,21 @@ export function EditUsernameModalBody({
|
||||
}
|
||||
}, [state, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
state === UsernameReservationState.Closed &&
|
||||
recoveredUsername &&
|
||||
isRootModal
|
||||
) {
|
||||
showToast({
|
||||
toastType: ToastType.UsernameRecovered,
|
||||
parameters: {
|
||||
username: recoveredUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [state, recoveredUsername, showToast, isRootModal]);
|
||||
|
||||
const errorString = useMemo(() => {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
@@ -227,14 +253,17 @@ export function EditUsernameModalBody({
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
if (!currentUsername || (reservation && isCaseChange(reservation))) {
|
||||
if (usernameCorrupted) {
|
||||
setIsConfirmingReset(true);
|
||||
} else if (!currentUsername || (reservation && isCaseChange(reservation))) {
|
||||
confirmUsername();
|
||||
} else {
|
||||
setIsConfirmingSave(true);
|
||||
}
|
||||
}, [confirmUsername, currentUsername, reservation]);
|
||||
}, [confirmUsername, currentUsername, reservation, usernameCorrupted]);
|
||||
|
||||
const onCancelSave = useCallback(() => {
|
||||
setIsConfirmingReset(false);
|
||||
setIsConfirmingSave(false);
|
||||
}, []);
|
||||
|
||||
@@ -406,6 +435,26 @@ export function EditUsernameModalBody({
|
||||
{i18n('icu:EditUsernameModalBody__change-confirmation')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isConfirmingReset && (
|
||||
<ConfirmationDialog
|
||||
dialogName="EditUsernameModalBody.confirmReset"
|
||||
cancelText={i18n('icu:cancel')}
|
||||
actions={[
|
||||
{
|
||||
action: onConfirmUsername,
|
||||
style: 'negative',
|
||||
text: i18n(
|
||||
'icu:EditUsernameModalBody__change-confirmation__continue'
|
||||
),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={onCancelSave}
|
||||
>
|
||||
{i18n('icu:EditUsernameModalBody__recover-confirmation')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
),
|
||||
selectedConversationId: undefined,
|
||||
targetedMessageId: undefined,
|
||||
openUsernameReservationModal: action('openUsernameReservationModal'),
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
NavSidebarSearchHeader,
|
||||
} from './NavSidebar';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
|
||||
export enum LeftPaneMode {
|
||||
@@ -119,6 +120,7 @@ export type PropsType = {
|
||||
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
createGroup: () => void;
|
||||
navTabsCollapsed: boolean;
|
||||
openUsernameReservationModal: () => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
removeConversation: (conversationId: string) => void;
|
||||
@@ -138,7 +140,7 @@ export type PropsType = {
|
||||
toggleComposeEditingAvatar: () => unknown;
|
||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||
toggleProfileEditor: () => void;
|
||||
toggleProfileEditor: (initialEditState?: ProfileEditorEditState) => void;
|
||||
updateSearchTerm: (_: string) => void;
|
||||
|
||||
// Render Props
|
||||
@@ -193,6 +195,7 @@ export function LeftPane({
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
||||
openUsernameReservationModal,
|
||||
preferredWidthFromStorage,
|
||||
removeConversation,
|
||||
renderCaptchaDialog,
|
||||
@@ -560,7 +563,10 @@ export function LeftPane({
|
||||
maybeBanner = (
|
||||
<LeftPaneBanner
|
||||
actionText={i18n('icu:LeftPane--corrupted-username--action-text')}
|
||||
onClick={toggleProfileEditor}
|
||||
onClick={() => {
|
||||
openUsernameReservationModal();
|
||||
toggleProfileEditor(ProfileEditorEditState.Username);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:LeftPane--corrupted-username--text')}
|
||||
</LeftPaneBanner>
|
||||
@@ -569,7 +575,7 @@ export function LeftPane({
|
||||
maybeBanner = (
|
||||
<LeftPaneBanner
|
||||
actionText={i18n('icu:LeftPane--corrupted-username-link--action-text')}
|
||||
onClick={toggleProfileEditor}
|
||||
onClick={() => toggleProfileEditor(ProfileEditorEditState.UsernameLink)}
|
||||
>
|
||||
{i18n('icu:LeftPane--corrupted-username-link--text')}
|
||||
</LeftPaneBanner>
|
||||
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
customColors: {},
|
||||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||
deviceName: 'Work Windows ME',
|
||||
phoneNumber: '+1 555 123-4567',
|
||||
hasAudioNotifications: true,
|
||||
hasAutoConvertEmoji: true,
|
||||
hasAutoDownloadUpdate: true,
|
||||
|
||||
@@ -101,6 +101,7 @@ export type PropsDataType = {
|
||||
hasTypingIndicators: boolean;
|
||||
lastSyncTime?: number;
|
||||
notificationContent: NotificationSettingType;
|
||||
phoneNumber: string | undefined;
|
||||
selectedCamera?: string;
|
||||
selectedMicrophone?: AudioDevice;
|
||||
selectedSpeaker?: AudioDevice;
|
||||
@@ -325,6 +326,7 @@ export function Preferences({
|
||||
onWhoCanSeeMeChange,
|
||||
onWhoCanFindMeChange,
|
||||
onZoomFactorChange,
|
||||
phoneNumber = '',
|
||||
preferredSystemLocales,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
@@ -531,6 +533,10 @@ export function Preferences({
|
||||
</div>
|
||||
</div>
|
||||
<SettingsRow>
|
||||
<Control
|
||||
left={i18n('icu:Preferences--phone-number')}
|
||||
right={phoneNumber}
|
||||
/>
|
||||
<Control
|
||||
left={i18n('icu:Preferences--device-name')}
|
||||
right={deviceName}
|
||||
|
||||
@@ -47,6 +47,9 @@ export default {
|
||||
usernameLinkCorrupted: {
|
||||
control: 'boolean',
|
||||
},
|
||||
usernameLinkRecovered: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
aboutEmoji: '',
|
||||
@@ -78,6 +81,7 @@ export default {
|
||||
showToast: action('showToast'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
resetUsernameLink: action('resetUsernameLink'),
|
||||
clearUsernameLinkRecovered: action('clearUsernameLinkRecovered'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
markCompletedUsernameLinkOnboarding: action(
|
||||
'markCompletedUsernameLinkOnboarding'
|
||||
@@ -89,6 +93,7 @@ export default {
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
function renderEditUsernameModalBody(props: {
|
||||
isRootModal: boolean;
|
||||
onClose: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
@@ -98,10 +103,13 @@ function renderEditUsernameModalBody(props: {
|
||||
maxNickname={20}
|
||||
state={UsernameReservationState.Open}
|
||||
error={undefined}
|
||||
recoveredUsername={undefined}
|
||||
usernameCorrupted={false}
|
||||
setUsernameReservationError={action('setUsernameReservationError')}
|
||||
clearUsernameReservation={action('clearUsernameReservation')}
|
||||
reserveUsername={action('reserveUsername')}
|
||||
confirmUsername={action('confirmUsername')}
|
||||
showToast={action('showToast')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -164,3 +172,10 @@ ConfirmingDelete.args = {
|
||||
username: 'signaluser.123',
|
||||
usernameEditState: UsernameEditState.ConfirmingDelete,
|
||||
};
|
||||
|
||||
export const Corrupted = Template.bind({});
|
||||
Corrupted.args = {
|
||||
isUsernameFlagEnabled: true,
|
||||
username: 'signaluser.123',
|
||||
usernameCorrupted: true,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
|
||||
import { EmojiButton, EmojiButtonVariant } from './emoji/EmojiButton';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import { Input } from './Input';
|
||||
import { Intl } from './Intl';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
||||
@@ -62,7 +61,10 @@ type PropsExternalType = {
|
||||
profileData: ProfileDataType,
|
||||
avatar: AvatarUpdateType
|
||||
) => unknown;
|
||||
renderEditUsernameModalBody: (props: { onClose: () => void }) => JSX.Element;
|
||||
renderEditUsernameModalBody: (props: {
|
||||
isRootModal: boolean;
|
||||
onClose: () => void;
|
||||
}) => JSX.Element;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
@@ -76,12 +78,12 @@ export type PropsDataType = {
|
||||
hasCompletedUsernameLinkOnboarding: boolean;
|
||||
i18n: LocalizerType;
|
||||
isUsernameFlagEnabled: boolean;
|
||||
phoneNumber?: string;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
username?: string;
|
||||
initialEditState?: EditState;
|
||||
usernameCorrupted: boolean;
|
||||
usernameEditState: UsernameEditState;
|
||||
usernameLinkRecovered: boolean;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
usernameLinkColor?: number;
|
||||
usernameLink?: string;
|
||||
@@ -97,7 +99,9 @@ type PropsActionType = {
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
setUsernameEditState: (editState: UsernameEditState) => void;
|
||||
setUsernameLinkColor: (color: number) => void;
|
||||
toggleProfileEditor: () => void;
|
||||
resetUsernameLink: () => void;
|
||||
clearUsernameLinkRecovered: () => void;
|
||||
deleteUsername: () => void;
|
||||
showToast: ShowToastAction;
|
||||
openUsernameReservationModal: () => void;
|
||||
@@ -138,6 +142,7 @@ function getDefaultBios(i18n: LocalizerType): Array<DefaultBio> {
|
||||
export function ProfileEditor({
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
clearUsernameLinkRecovered,
|
||||
color,
|
||||
conversationId,
|
||||
deleteAvatarFromDisk,
|
||||
@@ -153,12 +158,12 @@ export function ProfileEditor({
|
||||
onProfileChanged,
|
||||
onSetSkinTone,
|
||||
openUsernameReservationModal,
|
||||
phoneNumber,
|
||||
profileAvatarPath,
|
||||
recentEmojis,
|
||||
renderEditUsernameModalBody,
|
||||
replaceAvatar,
|
||||
resetUsernameLink,
|
||||
toggleProfileEditor,
|
||||
saveAttachment,
|
||||
saveAvatarToDisk,
|
||||
setUsernameEditState,
|
||||
@@ -169,6 +174,7 @@ export function ProfileEditor({
|
||||
username,
|
||||
usernameCorrupted,
|
||||
usernameEditState,
|
||||
usernameLinkRecovered,
|
||||
usernameLinkState,
|
||||
usernameLinkColor,
|
||||
usernameLink,
|
||||
@@ -209,6 +215,7 @@ export function ProfileEditor({
|
||||
firstName,
|
||||
});
|
||||
const [isResettingUsername, setIsResettingUsername] = useState(false);
|
||||
const [isResettingUsernameLink, setIsResettingUsernameLink] = useState(false);
|
||||
|
||||
// Reset username edit state when leaving
|
||||
useEffect(() => {
|
||||
@@ -276,6 +283,13 @@ export function ProfileEditor({
|
||||
onEditStateChanged(editState);
|
||||
}, [editState, onEditStateChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
// If we opened at a nested sub-modal - close when leaving it.
|
||||
if (editState === EditState.None && initialEditState !== EditState.None) {
|
||||
toggleProfileEditor();
|
||||
}
|
||||
}, [initialEditState, editState, toggleProfileEditor]);
|
||||
|
||||
// To make AvatarEditor re-render less often
|
||||
const handleAvatarLoaded = useCallback(
|
||||
avatar => {
|
||||
@@ -512,6 +526,7 @@ export function ProfileEditor({
|
||||
);
|
||||
} else if (editState === EditState.Username) {
|
||||
content = renderEditUsernameModalBody({
|
||||
isRootModal: initialEditState === editState,
|
||||
onClose: () => setEditState(EditState.None),
|
||||
});
|
||||
} else if (editState === EditState.UsernameLink) {
|
||||
@@ -522,9 +537,11 @@ export function ProfileEditor({
|
||||
username={username ?? ''}
|
||||
colorId={usernameLinkColor}
|
||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||
usernameLinkRecovered={usernameLinkRecovered}
|
||||
usernameLinkState={usernameLinkState}
|
||||
setUsernameLinkColor={setUsernameLinkColor}
|
||||
resetUsernameLink={resetUsernameLink}
|
||||
clearUsernameLinkRecovered={clearUsernameLinkRecovered}
|
||||
saveAttachment={saveAttachment}
|
||||
showToast={showToast}
|
||||
onBack={() => setEditState(EditState.None)}
|
||||
@@ -614,6 +631,11 @@ export function ProfileEditor({
|
||||
}
|
||||
label={i18n('icu:ProfileEditor__username-link')}
|
||||
onClick={() => {
|
||||
if (usernameLinkCorrupted) {
|
||||
setIsResettingUsernameLink(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setEditState(EditState.UsernameLink);
|
||||
}}
|
||||
alwaysShowActions
|
||||
@@ -656,6 +678,7 @@ export function ProfileEditor({
|
||||
|
||||
maybeUsernameRows = (
|
||||
<>
|
||||
<hr className="ProfileEditor__divider" />
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
icon={
|
||||
@@ -678,6 +701,11 @@ export function ProfileEditor({
|
||||
actions={actions}
|
||||
/>
|
||||
{maybeUsernameLinkRow}
|
||||
<div className="ProfileEditor__info">
|
||||
{username
|
||||
? i18n('icu:ProfileEditor--info--pnp')
|
||||
: i18n('icu:ProfileEditor--info--pnp--no-username')}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -690,7 +718,6 @@ export function ProfileEditor({
|
||||
avatarValue={avatarBuffer}
|
||||
conversationTitle={getFullNameText()}
|
||||
i18n={i18n}
|
||||
isEditable
|
||||
onAvatarLoaded={handleAvatarLoaded}
|
||||
onClick={() => {
|
||||
setEditState(EditState.BetterAvatar);
|
||||
@@ -700,11 +727,17 @@ export function ProfileEditor({
|
||||
width: 80,
|
||||
}}
|
||||
/>
|
||||
<h1 className="ProfileEditor__Title">{getFullNameText()}</h1>
|
||||
{phoneNumber != null && (
|
||||
<p className="ProfileEditor__PhoneNumber">{phoneNumber}</p>
|
||||
)}
|
||||
<hr className="ProfileEditor__divider" />
|
||||
<div className="ProfileEditor__EditPhotoContainer">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditState(EditState.BetterAvatar);
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
className="ProfileEditor__EditPhoto"
|
||||
>
|
||||
{i18n('icu:ProfileEditor--edit-photo')}
|
||||
</Button>
|
||||
</div>
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
icon={
|
||||
@@ -715,7 +748,6 @@ export function ProfileEditor({
|
||||
setEditState(EditState.ProfileName);
|
||||
}}
|
||||
/>
|
||||
{maybeUsernameRows}
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
icon={
|
||||
@@ -736,26 +768,10 @@ export function ProfileEditor({
|
||||
setEditState(EditState.Bio);
|
||||
}}
|
||||
/>
|
||||
<hr className="ProfileEditor__divider" />
|
||||
<div className="ProfileEditor__info">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ProfileEditor--info--link"
|
||||
components={{
|
||||
// This is a render prop, not a component
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
learnMoreLink: parts => (
|
||||
<a
|
||||
href="https://support.signal.org/hc/en-us/articles/360007459591"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{parts}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{i18n('icu:ProfileEditor--info--general')}
|
||||
</div>
|
||||
{maybeUsernameRows}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
@@ -791,6 +807,28 @@ export function ProfileEditor({
|
||||
/>
|
||||
)}
|
||||
|
||||
{isResettingUsernameLink && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="UsernameLinkModal__error"
|
||||
onClose={() => setIsResettingUsernameLink(false)}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
setIsResettingUsernameLink(false);
|
||||
setEditState(EditState.UsernameLink);
|
||||
},
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:UsernameLinkModalBody__error__fix-now'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:UsernameLinkModalBody__error__text')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isResettingUsername && (
|
||||
<ConfirmationDialog
|
||||
dialogName="ProfileEditor.confirmResetUsername"
|
||||
@@ -799,15 +837,7 @@ export function ProfileEditor({
|
||||
onClose={() => setIsResettingUsername(false)}
|
||||
actions={[
|
||||
{
|
||||
text: i18n(
|
||||
'icu:ProfileEditor--username--corrupted--delete-button'
|
||||
),
|
||||
action: () => deleteUsername(),
|
||||
},
|
||||
{
|
||||
text: i18n(
|
||||
'icu:ProfileEditor--username--corrupted--create-button'
|
||||
),
|
||||
text: i18n('icu:ProfileEditor--username--corrupted--fix-button'),
|
||||
style: 'affirmative',
|
||||
action: () => {
|
||||
openUsernameReservationModal();
|
||||
|
||||
@@ -74,6 +74,7 @@ export function ProfileEditorModal({
|
||||
}}
|
||||
onProfileChanged={myProfileChanged}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
toggleProfileEditor={toggleProfileEditor}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -150,6 +150,13 @@ function getToast(toastType: ToastType): AnyToast {
|
||||
return { toastType: ToastType.UnsupportedMultiAttachment };
|
||||
case ToastType.UnsupportedOS:
|
||||
return { toastType: ToastType.UnsupportedOS };
|
||||
case ToastType.UsernameRecovered:
|
||||
return {
|
||||
toastType: ToastType.UsernameRecovered,
|
||||
parameters: {
|
||||
username: 'maya.45',
|
||||
},
|
||||
};
|
||||
case ToastType.UserAddedToGroup:
|
||||
return {
|
||||
toastType: ToastType.UserAddedToGroup,
|
||||
|
||||
@@ -476,6 +476,16 @@ export function renderToast({
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.UsernameRecovered) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:EditUsernameModalBody__username-recovered__text', {
|
||||
username: toast.parameters.username,
|
||||
})}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.UserAddedToGroup) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
||||
@@ -35,6 +35,9 @@ export default {
|
||||
usernameLinkCorrupted: {
|
||||
control: 'boolean',
|
||||
},
|
||||
usernameLinkRecovered: {
|
||||
control: 'boolean',
|
||||
},
|
||||
usernameLinkState: {
|
||||
control: { type: 'select' },
|
||||
options: [
|
||||
@@ -66,6 +69,7 @@ export default {
|
||||
showToast: action('showToast'),
|
||||
resetUsernameLink: action('resetUsernameLink'),
|
||||
setUsernameLinkColor: action('setUsernameLinkColor'),
|
||||
clearUsernameLinkRecovered: action('clearUsernameLinkRecovered'),
|
||||
onBack: action('onBack'),
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
@@ -29,9 +29,11 @@ export type PropsType = Readonly<{
|
||||
colorId?: number;
|
||||
usernameLinkCorrupted: boolean;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
usernameLinkRecovered: boolean;
|
||||
|
||||
setUsernameLinkColor: (colorId: number) => void;
|
||||
resetUsernameLink: () => void;
|
||||
clearUsernameLinkRecovered: () => void;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
showToast: ShowToastAction;
|
||||
onBack: () => void;
|
||||
@@ -532,10 +534,12 @@ export function UsernameLinkModalBody({
|
||||
username,
|
||||
usernameLinkCorrupted,
|
||||
usernameLinkState,
|
||||
usernameLinkRecovered,
|
||||
colorId: initialColorId = ColorEnum.UNKNOWN,
|
||||
|
||||
setUsernameLinkColor,
|
||||
resetUsernameLink,
|
||||
clearUsernameLinkRecovered,
|
||||
saveAttachment,
|
||||
showToast,
|
||||
|
||||
@@ -544,6 +548,7 @@ export function UsernameLinkModalBody({
|
||||
const [pngData, setPngData] = useState<Uint8Array | undefined>();
|
||||
const [showColors, setShowColors] = useState(false);
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
const [isRecovered, setIsRecovered] = useState(false);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [colorId, setColorId] = useState(initialColorId);
|
||||
|
||||
@@ -662,10 +667,17 @@ export function UsernameLinkModalBody({
|
||||
}, []);
|
||||
|
||||
const onConfirmReset = useCallback(() => {
|
||||
setShowError(false);
|
||||
setConfirmReset(false);
|
||||
resetUsernameLink();
|
||||
}, [resetUsernameLink]);
|
||||
|
||||
const onCloseError = useCallback(() => {
|
||||
if (showError) {
|
||||
onBack();
|
||||
}
|
||||
}, [showError, onBack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!usernameLinkCorrupted) {
|
||||
return;
|
||||
@@ -682,12 +694,21 @@ export function UsernameLinkModalBody({
|
||||
setShowError(true);
|
||||
}, [usernameLinkState]);
|
||||
|
||||
const onClearError = useCallback(() => {
|
||||
setShowError(false);
|
||||
useEffect(() => {
|
||||
if (usernameLinkRecovered) {
|
||||
setIsRecovered(true);
|
||||
|
||||
// Only show the modal once
|
||||
clearUsernameLinkRecovered();
|
||||
}
|
||||
}, [usernameLinkRecovered, clearUsernameLinkRecovered]);
|
||||
|
||||
const onClearIsRecovered = useCallback(() => {
|
||||
setIsRecovered(false);
|
||||
}, []);
|
||||
|
||||
const isResettingLink =
|
||||
usernameLinkCorrupted || usernameLinkState !== UsernameLinkState.Ready;
|
||||
const isReady = usernameLinkState === UsernameLinkState.Ready;
|
||||
const isResettingLink = usernameLinkCorrupted || !isReady;
|
||||
|
||||
const info = (
|
||||
<>
|
||||
@@ -754,7 +775,7 @@ export function UsernameLinkModalBody({
|
||||
);
|
||||
|
||||
let linkImage: JSX.Element | undefined;
|
||||
if (usernameLinkState === UsernameLinkState.Ready && link) {
|
||||
if (isReady && link) {
|
||||
linkImage = (
|
||||
<svg
|
||||
className={`${CLASS}__card__qr__blotches`}
|
||||
@@ -820,11 +841,30 @@ export function UsernameLinkModalBody({
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="UsernameLinkModal__error"
|
||||
onClose={onClearError}
|
||||
onClose={onCloseError}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
actions={[
|
||||
{
|
||||
action: onConfirmReset,
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:UsernameLinkModalBody__error__fix-now'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:UsernameLinkModalBody__error__text')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isRecovered && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="UsernameLinkModal__error"
|
||||
onClose={onClearIsRecovered}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:ok')}
|
||||
>
|
||||
{i18n('icu:UsernameLinkModalBody__error__text')}
|
||||
{i18n('icu:UsernameLinkModalBody__recovered__text')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user