Username recovery improvements
This commit is contained in:
parent
a70ae1060d
commit
533a1b32d4
27 changed files with 423 additions and 99 deletions
|
@ -1637,8 +1637,12 @@
|
||||||
"messageformat": "My Computer",
|
"messageformat": "My Computer",
|
||||||
"description": "The placeholder for the 'choose device name' input"
|
"description": "The placeholder for the 'choose device name' input"
|
||||||
},
|
},
|
||||||
|
"icu:Preferences--phone-number": {
|
||||||
|
"messageformat": "Phone Number",
|
||||||
|
"description": "The label in settings panel shown for the phone number associated with user's account"
|
||||||
|
},
|
||||||
"icu:Preferences--device-name": {
|
"icu:Preferences--device-name": {
|
||||||
"messageformat": "Device name",
|
"messageformat": "Device Name",
|
||||||
"description": "The label in settings panel shown for the user-provided name for this desktop instance"
|
"description": "The label in settings panel shown for the user-provided name for this desktop instance"
|
||||||
},
|
},
|
||||||
"icu:chooseDeviceName": {
|
"icu:chooseDeviceName": {
|
||||||
|
@ -5512,15 +5516,19 @@
|
||||||
"description": "Default text for username field"
|
"description": "Default text for username field"
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor--username--corrupted--body": {
|
"icu:ProfileEditor--username--corrupted--body": {
|
||||||
"messageformat": "Something went wrong with your username, it’s no longer assigned to your account.",
|
"messageformat": "Something went wrong with your username, it’s no longer assigned to your account. You can try and set it again or choose a new one.",
|
||||||
"description": "Text of confirmation modal when the username gets corrupted"
|
"description": "Text of confirmation modal when the username gets corrupted"
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor--username--corrupted--delete-button": {
|
"icu:ProfileEditor--username--corrupted--delete-button": {
|
||||||
"messageformat": "Delete username",
|
"messageformat": "Delete username",
|
||||||
"description": "Button text for deletion of the username in case of corruption"
|
"description": "(Deleted 02/01/2024) Button text for deletion of the username in case of corruption"
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor--username--corrupted--create-button": {
|
"icu:ProfileEditor--username--corrupted--create-button": {
|
||||||
"messageformat": "Create username",
|
"messageformat": "Create username",
|
||||||
|
"description": "(Deleted 02/01/2024) Button text for creation of a new username in case of corruption"
|
||||||
|
},
|
||||||
|
"icu:ProfileEditor--username--corrupted--fix-button": {
|
||||||
|
"messageformat": "Fix now",
|
||||||
"description": "Button text for creation of a new username in case of corruption"
|
"description": "Button text for creation of a new username in case of corruption"
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor__username-link": {
|
"icu:ProfileEditor__username-link": {
|
||||||
|
@ -5651,9 +5659,25 @@
|
||||||
"messageformat": "Would you like to discard these changes?",
|
"messageformat": "Would you like to discard these changes?",
|
||||||
"description": "ConfirmationDialog text for discarding changes"
|
"description": "ConfirmationDialog text for discarding changes"
|
||||||
},
|
},
|
||||||
|
"icu:ProfileEditor--edit-photo": {
|
||||||
|
"messageformat": "Edit photo",
|
||||||
|
"description": "Text of a button on profile editor that leads to the avatar editor"
|
||||||
|
},
|
||||||
"icu:ProfileEditor--info--link": {
|
"icu:ProfileEditor--info--link": {
|
||||||
"messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>Learn More</learnMoreLink>",
|
"messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>Learn More</learnMoreLink>",
|
||||||
"description": "Information shown at the bottom of the profile editor section"
|
"description": "(Deleted 02/01/2024) Information shown at the bottom of the profile editor section"
|
||||||
|
},
|
||||||
|
"icu:ProfileEditor--info--general": {
|
||||||
|
"messageformat": "Your profile and changes to it will be visible to people you message, contacts and groups.",
|
||||||
|
"description": "Information shown in profile editor below profile name and about fields"
|
||||||
|
},
|
||||||
|
"icu:ProfileEditor--info--pnp": {
|
||||||
|
"messageformat": "Your username, QR code & link aren’t visible on your profile. Only share them with people you trust.",
|
||||||
|
"description": "Information shown in profile editor below pnp settings when username is set"
|
||||||
|
},
|
||||||
|
"icu:ProfileEditor--info--pnp--no-username": {
|
||||||
|
"messageformat": "People can now message you using your optional username so you don’t have to give out your phone number.",
|
||||||
|
"description": "Information shown in profile editor below pnp settings when no username is set"
|
||||||
},
|
},
|
||||||
"icu:Bio--speak-freely": {
|
"icu:Bio--speak-freely": {
|
||||||
"messageformat": "Speak Freely",
|
"messageformat": "Speak Freely",
|
||||||
|
@ -6840,7 +6864,7 @@
|
||||||
"description": "Placeholder for the username field"
|
"description": "Placeholder for the username field"
|
||||||
},
|
},
|
||||||
"icu:EditUsernameModalBody__username-helper": {
|
"icu:EditUsernameModalBody__username-helper": {
|
||||||
"messageformat": "Usernames let others message you without needing your phone number. They are paired with a set of digits to help keep your address private.",
|
"messageformat": "Usernames are always paired with a set of numbers.",
|
||||||
"description": "Shown on the edit username screen"
|
"description": "Shown on the edit username screen"
|
||||||
},
|
},
|
||||||
"icu:EditUsernameModalBody__learn-more": {
|
"icu:EditUsernameModalBody__learn-more": {
|
||||||
|
@ -6863,6 +6887,14 @@
|
||||||
"messageformat": "Continue",
|
"messageformat": "Continue",
|
||||||
"description": "Text of the primary button on username change confirmation modal"
|
"description": "Text of the primary button on username change confirmation modal"
|
||||||
},
|
},
|
||||||
|
"icu:EditUsernameModalBody__recover-confirmation": {
|
||||||
|
"messageformat": "Recovering your username will reset your existing QR code and link. Are you sure?",
|
||||||
|
"description": "Body of the confirmation dialog displayed when user is about to recover their username"
|
||||||
|
},
|
||||||
|
"icu:EditUsernameModalBody__username-recovered__text": {
|
||||||
|
"messageformat": "Your QR code and link have been reset and your username is {username}",
|
||||||
|
"description": "Text of toast displayed upon successful recovery of username"
|
||||||
|
},
|
||||||
"icu:UsernameLinkModalBody__hint": {
|
"icu:UsernameLinkModalBody__hint": {
|
||||||
"messageformat": "Scan this QR code with your phone to chat with me on Signal.",
|
"messageformat": "Scan this QR code with your phone to chat with me on Signal.",
|
||||||
"descrption": "Text of the hint displayed below generated QR code on the printable image."
|
"descrption": "Text of the hint displayed below generated QR code on the printable image."
|
||||||
|
@ -6896,7 +6928,7 @@
|
||||||
"description": "ARIA label of button for selecting username link color"
|
"description": "ARIA label of button for selecting username link color"
|
||||||
},
|
},
|
||||||
"icu:UsernameLinkModalBody__reset__confirm": {
|
"icu:UsernameLinkModalBody__reset__confirm": {
|
||||||
"messageformat": "If you reset your QR code, your existing QR code and link will no longer work.",
|
"messageformat": "If you reset your QR code and link, your existing QR code and link will no longer work.",
|
||||||
"description": "Text of confirmation modal when resetting the username link"
|
"description": "Text of confirmation modal when resetting the username link"
|
||||||
},
|
},
|
||||||
"icu:UsernameLinkModalBody__resetting-link": {
|
"icu:UsernameLinkModalBody__resetting-link": {
|
||||||
|
@ -6904,9 +6936,17 @@
|
||||||
"description": "Text shown when resetting the username link"
|
"description": "Text shown when resetting the username link"
|
||||||
},
|
},
|
||||||
"icu:UsernameLinkModalBody__error__text": {
|
"icu:UsernameLinkModalBody__error__text": {
|
||||||
"messageformat": "QR code and link not set. Check your network connection and try again.",
|
"messageformat": "Something went wrong with your QR code and link, it’s no longer valid. Try resetting it to create a new QR code and link.",
|
||||||
"description": "Text of the confirmation dialog shown on username link error"
|
"description": "Text of the confirmation dialog shown on username link error"
|
||||||
},
|
},
|
||||||
|
"icu:UsernameLinkModalBody__error__fix-now": {
|
||||||
|
"messageformat": "Fix now",
|
||||||
|
"description": "Text of the button in a confirmation dialog shown on username link error"
|
||||||
|
},
|
||||||
|
"icu:UsernameLinkModalBody__recovered__text": {
|
||||||
|
"messageformat": "Your QR code and link have been reset and a new QR code and link has been created.",
|
||||||
|
"description": "Text of the confirmation dialog shown on successful username link recovery"
|
||||||
|
},
|
||||||
"icu:UsernameOnboardingModalBody__title": {
|
"icu:UsernameOnboardingModalBody__title": {
|
||||||
"messageformat": "New ways to connect",
|
"messageformat": "New ways to connect",
|
||||||
"description": "Title of username onboarding modal"
|
"description": "Title of username onboarding modal"
|
||||||
|
|
|
@ -8,16 +8,16 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 20px;
|
||||||
height: 32px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
-webkit-mask-size: 100%;
|
-webkit-mask-size: 100%;
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
|
@ -96,7 +96,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__row {
|
&__row {
|
||||||
padding-inline: 0;
|
padding-inline: 8px;
|
||||||
|
padding-block: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
|
@ -112,10 +113,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-block: 24px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
margin-block: 16px;
|
margin-block: 12px;
|
||||||
margin-inline: 0;
|
margin-inline: 8px;
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
|
@ -134,7 +139,6 @@
|
||||||
&__button {
|
&__button {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 4px;
|
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
|
@ -181,9 +185,8 @@
|
||||||
&__error-icon {
|
&__error-icon {
|
||||||
-webkit-mask-size: 100%;
|
-webkit-mask-size: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
margin: 4px;
|
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
|
@ -210,8 +213,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
width: 24px;
|
width: 20px;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
margin-block-start: 4px;
|
margin-block-start: 4px;
|
||||||
margin-inline: 4px 12px;
|
margin-inline: 4px 12px;
|
||||||
|
|
||||||
|
@ -256,14 +259,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__reset-username-modal__ModalHost__width-container {
|
|
||||||
max-width: 438px;
|
|
||||||
|
|
||||||
.module-Modal__button-footer {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProfileEditor__Title {
|
.ProfileEditor__Title {
|
||||||
|
@ -297,3 +292,18 @@
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProfileEditor__EditPhotoContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-block-end: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProfileEditor__EditPhoto {
|
||||||
|
@include font-subtitle;
|
||||||
|
|
||||||
|
padding-block: 5px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,9 @@ export default {
|
||||||
component: EditUsernameModalBody,
|
component: EditUsernameModalBody,
|
||||||
title: 'Components/EditUsernameModalBody',
|
title: 'Components/EditUsernameModalBody',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
usernameCorrupted: {
|
||||||
|
type: { name: 'boolean' },
|
||||||
|
},
|
||||||
currentUsername: {
|
currentUsername: {
|
||||||
type: { name: 'string', required: false },
|
type: { name: 'string', required: false },
|
||||||
},
|
},
|
||||||
|
@ -57,6 +60,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
isRootModal: false,
|
||||||
|
usernameCorrupted: false,
|
||||||
currentUsername: undefined,
|
currentUsername: undefined,
|
||||||
state: State.Open,
|
state: State.Open,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { UsernameReservationType } from '../types/Username';
|
import type { UsernameReservationType } from '../types/Username';
|
||||||
|
import { ToastType } from '../types/Toast';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { getNickname, getDiscriminator, isCaseChange } from '../types/Username';
|
import { getNickname, getDiscriminator, isCaseChange } from '../types/Username';
|
||||||
import {
|
import {
|
||||||
|
@ -13,6 +14,7 @@ import {
|
||||||
UsernameReservationError,
|
UsernameReservationError,
|
||||||
} from '../state/ducks/usernameEnums';
|
} from '../state/ducks/usernameEnums';
|
||||||
import type { ReserveUsernameOptionsType } from '../state/ducks/username';
|
import type { ReserveUsernameOptionsType } from '../state/ducks/username';
|
||||||
|
import type { ShowToastAction } from '../state/ducks/toast';
|
||||||
|
|
||||||
import { AutoSizeInput } from './AutoSizeInput';
|
import { AutoSizeInput } from './AutoSizeInput';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
@ -24,9 +26,11 @@ import { Button, ButtonVariant } from './Button';
|
||||||
export type PropsDataType = Readonly<{
|
export type PropsDataType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
currentUsername?: string;
|
currentUsername?: string;
|
||||||
|
usernameCorrupted: boolean;
|
||||||
reservation?: UsernameReservationType;
|
reservation?: UsernameReservationType;
|
||||||
error?: UsernameReservationError;
|
error?: UsernameReservationError;
|
||||||
state: UsernameReservationState;
|
state: UsernameReservationState;
|
||||||
|
recoveredUsername: string | undefined;
|
||||||
minNickname: number;
|
minNickname: number;
|
||||||
maxNickname: number;
|
maxNickname: number;
|
||||||
}>;
|
}>;
|
||||||
|
@ -38,10 +42,12 @@ export type ActionPropsDataType = Readonly<{
|
||||||
clearUsernameReservation(): void;
|
clearUsernameReservation(): void;
|
||||||
reserveUsername(optiona: ReserveUsernameOptionsType): void;
|
reserveUsername(optiona: ReserveUsernameOptionsType): void;
|
||||||
confirmUsername(): void;
|
confirmUsername(): void;
|
||||||
|
showToast: ShowToastAction;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExternalPropsDataType = Readonly<{
|
export type ExternalPropsDataType = Readonly<{
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
isRootModal: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type PropsType = PropsDataType &
|
export type PropsType = PropsDataType &
|
||||||
|
@ -59,8 +65,10 @@ const DISCRIMINATOR_MAX_LENGTH = 19;
|
||||||
export function EditUsernameModalBody({
|
export function EditUsernameModalBody({
|
||||||
i18n,
|
i18n,
|
||||||
currentUsername,
|
currentUsername,
|
||||||
|
usernameCorrupted,
|
||||||
reserveUsername,
|
reserveUsername,
|
||||||
confirmUsername,
|
confirmUsername,
|
||||||
|
showToast,
|
||||||
minNickname,
|
minNickname,
|
||||||
maxNickname,
|
maxNickname,
|
||||||
reservation,
|
reservation,
|
||||||
|
@ -68,6 +76,8 @@ export function EditUsernameModalBody({
|
||||||
clearUsernameReservation,
|
clearUsernameReservation,
|
||||||
error,
|
error,
|
||||||
state,
|
state,
|
||||||
|
recoveredUsername,
|
||||||
|
isRootModal,
|
||||||
onClose,
|
onClose,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const currentNickname = useMemo(() => {
|
const currentNickname = useMemo(() => {
|
||||||
|
@ -87,6 +97,7 @@ export function EditUsernameModalBody({
|
||||||
const [nickname, setNickname] = useState(currentNickname);
|
const [nickname, setNickname] = useState(currentNickname);
|
||||||
const [isLearnMoreVisible, setIsLearnMoreVisible] = useState(false);
|
const [isLearnMoreVisible, setIsLearnMoreVisible] = useState(false);
|
||||||
const [isConfirmingSave, setIsConfirmingSave] = useState(false);
|
const [isConfirmingSave, setIsConfirmingSave] = useState(false);
|
||||||
|
const [isConfirmingReset, setIsConfirmingReset] = useState(false);
|
||||||
|
|
||||||
const [customDiscriminator, setCustomDiscriminator] = useState<
|
const [customDiscriminator, setCustomDiscriminator] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
|
@ -148,6 +159,21 @@ export function EditUsernameModalBody({
|
||||||
}
|
}
|
||||||
}, [state, onClose]);
|
}, [state, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
state === UsernameReservationState.Closed &&
|
||||||
|
recoveredUsername &&
|
||||||
|
isRootModal
|
||||||
|
) {
|
||||||
|
showToast({
|
||||||
|
toastType: ToastType.UsernameRecovered,
|
||||||
|
parameters: {
|
||||||
|
username: recoveredUsername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [state, recoveredUsername, showToast, isRootModal]);
|
||||||
|
|
||||||
const errorString = useMemo(() => {
|
const errorString = useMemo(() => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -227,14 +253,17 @@ export function EditUsernameModalBody({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSave = useCallback(() => {
|
const onSave = useCallback(() => {
|
||||||
if (!currentUsername || (reservation && isCaseChange(reservation))) {
|
if (usernameCorrupted) {
|
||||||
|
setIsConfirmingReset(true);
|
||||||
|
} else if (!currentUsername || (reservation && isCaseChange(reservation))) {
|
||||||
confirmUsername();
|
confirmUsername();
|
||||||
} else {
|
} else {
|
||||||
setIsConfirmingSave(true);
|
setIsConfirmingSave(true);
|
||||||
}
|
}
|
||||||
}, [confirmUsername, currentUsername, reservation]);
|
}, [confirmUsername, currentUsername, reservation, usernameCorrupted]);
|
||||||
|
|
||||||
const onCancelSave = useCallback(() => {
|
const onCancelSave = useCallback(() => {
|
||||||
|
setIsConfirmingReset(false);
|
||||||
setIsConfirmingSave(false);
|
setIsConfirmingSave(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -406,6 +435,26 @@ export function EditUsernameModalBody({
|
||||||
{i18n('icu:EditUsernameModalBody__change-confirmation')}
|
{i18n('icu:EditUsernameModalBody__change-confirmation')}
|
||||||
</ConfirmationDialog>
|
</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,
|
selectedConversationId: undefined,
|
||||||
targetedMessageId: undefined,
|
targetedMessageId: undefined,
|
||||||
|
openUsernameReservationModal: action('openUsernameReservationModal'),
|
||||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||||
searchInConversation: action('searchInConversation'),
|
searchInConversation: action('searchInConversation'),
|
||||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
NavSidebarSearchHeader,
|
NavSidebarSearchHeader,
|
||||||
} from './NavSidebar';
|
} from './NavSidebar';
|
||||||
import { ContextMenu } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
|
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
||||||
import type { UnreadStats } from '../util/countUnreadStats';
|
import type { UnreadStats } from '../util/countUnreadStats';
|
||||||
|
|
||||||
export enum LeftPaneMode {
|
export enum LeftPaneMode {
|
||||||
|
@ -119,6 +120,7 @@ export type PropsType = {
|
||||||
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
createGroup: () => void;
|
createGroup: () => void;
|
||||||
navTabsCollapsed: boolean;
|
navTabsCollapsed: boolean;
|
||||||
|
openUsernameReservationModal: () => void;
|
||||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||||
removeConversation: (conversationId: string) => void;
|
removeConversation: (conversationId: string) => void;
|
||||||
|
@ -138,7 +140,7 @@ export type PropsType = {
|
||||||
toggleComposeEditingAvatar: () => unknown;
|
toggleComposeEditingAvatar: () => unknown;
|
||||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||||
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||||
toggleProfileEditor: () => void;
|
toggleProfileEditor: (initialEditState?: ProfileEditorEditState) => void;
|
||||||
updateSearchTerm: (_: string) => void;
|
updateSearchTerm: (_: string) => void;
|
||||||
|
|
||||||
// Render Props
|
// Render Props
|
||||||
|
@ -193,6 +195,7 @@ export function LeftPane({
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
|
|
||||||
|
openUsernameReservationModal,
|
||||||
preferredWidthFromStorage,
|
preferredWidthFromStorage,
|
||||||
removeConversation,
|
removeConversation,
|
||||||
renderCaptchaDialog,
|
renderCaptchaDialog,
|
||||||
|
@ -560,7 +563,10 @@ export function LeftPane({
|
||||||
maybeBanner = (
|
maybeBanner = (
|
||||||
<LeftPaneBanner
|
<LeftPaneBanner
|
||||||
actionText={i18n('icu:LeftPane--corrupted-username--action-text')}
|
actionText={i18n('icu:LeftPane--corrupted-username--action-text')}
|
||||||
onClick={toggleProfileEditor}
|
onClick={() => {
|
||||||
|
openUsernameReservationModal();
|
||||||
|
toggleProfileEditor(ProfileEditorEditState.Username);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{i18n('icu:LeftPane--corrupted-username--text')}
|
{i18n('icu:LeftPane--corrupted-username--text')}
|
||||||
</LeftPaneBanner>
|
</LeftPaneBanner>
|
||||||
|
@ -569,7 +575,7 @@ export function LeftPane({
|
||||||
maybeBanner = (
|
maybeBanner = (
|
||||||
<LeftPaneBanner
|
<LeftPaneBanner
|
||||||
actionText={i18n('icu:LeftPane--corrupted-username-link--action-text')}
|
actionText={i18n('icu:LeftPane--corrupted-username-link--action-text')}
|
||||||
onClick={toggleProfileEditor}
|
onClick={() => toggleProfileEditor(ProfileEditorEditState.UsernameLink)}
|
||||||
>
|
>
|
||||||
{i18n('icu:LeftPane--corrupted-username-link--text')}
|
{i18n('icu:LeftPane--corrupted-username-link--text')}
|
||||||
</LeftPaneBanner>
|
</LeftPaneBanner>
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default {
|
||||||
customColors: {},
|
customColors: {},
|
||||||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||||
deviceName: 'Work Windows ME',
|
deviceName: 'Work Windows ME',
|
||||||
|
phoneNumber: '+1 555 123-4567',
|
||||||
hasAudioNotifications: true,
|
hasAudioNotifications: true,
|
||||||
hasAutoConvertEmoji: true,
|
hasAutoConvertEmoji: true,
|
||||||
hasAutoDownloadUpdate: true,
|
hasAutoDownloadUpdate: true,
|
||||||
|
|
|
@ -101,6 +101,7 @@ export type PropsDataType = {
|
||||||
hasTypingIndicators: boolean;
|
hasTypingIndicators: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
notificationContent: NotificationSettingType;
|
notificationContent: NotificationSettingType;
|
||||||
|
phoneNumber: string | undefined;
|
||||||
selectedCamera?: string;
|
selectedCamera?: string;
|
||||||
selectedMicrophone?: AudioDevice;
|
selectedMicrophone?: AudioDevice;
|
||||||
selectedSpeaker?: AudioDevice;
|
selectedSpeaker?: AudioDevice;
|
||||||
|
@ -325,6 +326,7 @@ export function Preferences({
|
||||||
onWhoCanSeeMeChange,
|
onWhoCanSeeMeChange,
|
||||||
onWhoCanFindMeChange,
|
onWhoCanFindMeChange,
|
||||||
onZoomFactorChange,
|
onZoomFactorChange,
|
||||||
|
phoneNumber = '',
|
||||||
preferredSystemLocales,
|
preferredSystemLocales,
|
||||||
removeCustomColor,
|
removeCustomColor,
|
||||||
removeCustomColorOnConversations,
|
removeCustomColorOnConversations,
|
||||||
|
@ -531,6 +533,10 @@ export function Preferences({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SettingsRow>
|
<SettingsRow>
|
||||||
|
<Control
|
||||||
|
left={i18n('icu:Preferences--phone-number')}
|
||||||
|
right={phoneNumber}
|
||||||
|
/>
|
||||||
<Control
|
<Control
|
||||||
left={i18n('icu:Preferences--device-name')}
|
left={i18n('icu:Preferences--device-name')}
|
||||||
right={deviceName}
|
right={deviceName}
|
||||||
|
|
|
@ -47,6 +47,9 @@ export default {
|
||||||
usernameLinkCorrupted: {
|
usernameLinkCorrupted: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
usernameLinkRecovered: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
aboutEmoji: '',
|
aboutEmoji: '',
|
||||||
|
@ -78,6 +81,7 @@ export default {
|
||||||
showToast: action('showToast'),
|
showToast: action('showToast'),
|
||||||
replaceAvatar: action('replaceAvatar'),
|
replaceAvatar: action('replaceAvatar'),
|
||||||
resetUsernameLink: action('resetUsernameLink'),
|
resetUsernameLink: action('resetUsernameLink'),
|
||||||
|
clearUsernameLinkRecovered: action('clearUsernameLinkRecovered'),
|
||||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||||
markCompletedUsernameLinkOnboarding: action(
|
markCompletedUsernameLinkOnboarding: action(
|
||||||
'markCompletedUsernameLinkOnboarding'
|
'markCompletedUsernameLinkOnboarding'
|
||||||
|
@ -89,6 +93,7 @@ export default {
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
function renderEditUsernameModalBody(props: {
|
function renderEditUsernameModalBody(props: {
|
||||||
|
isRootModal: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@ -98,10 +103,13 @@ function renderEditUsernameModalBody(props: {
|
||||||
maxNickname={20}
|
maxNickname={20}
|
||||||
state={UsernameReservationState.Open}
|
state={UsernameReservationState.Open}
|
||||||
error={undefined}
|
error={undefined}
|
||||||
|
recoveredUsername={undefined}
|
||||||
|
usernameCorrupted={false}
|
||||||
setUsernameReservationError={action('setUsernameReservationError')}
|
setUsernameReservationError={action('setUsernameReservationError')}
|
||||||
clearUsernameReservation={action('clearUsernameReservation')}
|
clearUsernameReservation={action('clearUsernameReservation')}
|
||||||
reserveUsername={action('reserveUsername')}
|
reserveUsername={action('reserveUsername')}
|
||||||
confirmUsername={action('confirmUsername')}
|
confirmUsername={action('confirmUsername')}
|
||||||
|
showToast={action('showToast')}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -164,3 +172,10 @@ ConfirmingDelete.args = {
|
||||||
username: 'signaluser.123',
|
username: 'signaluser.123',
|
||||||
usernameEditState: UsernameEditState.ConfirmingDelete,
|
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 { EmojiButton, EmojiButtonVariant } from './emoji/EmojiButton';
|
||||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
import { Intl } from './Intl';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
||||||
|
@ -62,7 +61,10 @@ type PropsExternalType = {
|
||||||
profileData: ProfileDataType,
|
profileData: ProfileDataType,
|
||||||
avatar: AvatarUpdateType
|
avatar: AvatarUpdateType
|
||||||
) => unknown;
|
) => unknown;
|
||||||
renderEditUsernameModalBody: (props: { onClose: () => void }) => JSX.Element;
|
renderEditUsernameModalBody: (props: {
|
||||||
|
isRootModal: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
|
@ -76,12 +78,12 @@ export type PropsDataType = {
|
||||||
hasCompletedUsernameLinkOnboarding: boolean;
|
hasCompletedUsernameLinkOnboarding: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isUsernameFlagEnabled: boolean;
|
isUsernameFlagEnabled: boolean;
|
||||||
phoneNumber?: string;
|
|
||||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||||
username?: string;
|
username?: string;
|
||||||
initialEditState?: EditState;
|
initialEditState?: EditState;
|
||||||
usernameCorrupted: boolean;
|
usernameCorrupted: boolean;
|
||||||
usernameEditState: UsernameEditState;
|
usernameEditState: UsernameEditState;
|
||||||
|
usernameLinkRecovered: boolean;
|
||||||
usernameLinkState: UsernameLinkState;
|
usernameLinkState: UsernameLinkState;
|
||||||
usernameLinkColor?: number;
|
usernameLinkColor?: number;
|
||||||
usernameLink?: string;
|
usernameLink?: string;
|
||||||
|
@ -97,7 +99,9 @@ type PropsActionType = {
|
||||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
setUsernameEditState: (editState: UsernameEditState) => void;
|
setUsernameEditState: (editState: UsernameEditState) => void;
|
||||||
setUsernameLinkColor: (color: number) => void;
|
setUsernameLinkColor: (color: number) => void;
|
||||||
|
toggleProfileEditor: () => void;
|
||||||
resetUsernameLink: () => void;
|
resetUsernameLink: () => void;
|
||||||
|
clearUsernameLinkRecovered: () => void;
|
||||||
deleteUsername: () => void;
|
deleteUsername: () => void;
|
||||||
showToast: ShowToastAction;
|
showToast: ShowToastAction;
|
||||||
openUsernameReservationModal: () => void;
|
openUsernameReservationModal: () => void;
|
||||||
|
@ -138,6 +142,7 @@ function getDefaultBios(i18n: LocalizerType): Array<DefaultBio> {
|
||||||
export function ProfileEditor({
|
export function ProfileEditor({
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
aboutText,
|
aboutText,
|
||||||
|
clearUsernameLinkRecovered,
|
||||||
color,
|
color,
|
||||||
conversationId,
|
conversationId,
|
||||||
deleteAvatarFromDisk,
|
deleteAvatarFromDisk,
|
||||||
|
@ -153,12 +158,12 @@ export function ProfileEditor({
|
||||||
onProfileChanged,
|
onProfileChanged,
|
||||||
onSetSkinTone,
|
onSetSkinTone,
|
||||||
openUsernameReservationModal,
|
openUsernameReservationModal,
|
||||||
phoneNumber,
|
|
||||||
profileAvatarPath,
|
profileAvatarPath,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
renderEditUsernameModalBody,
|
renderEditUsernameModalBody,
|
||||||
replaceAvatar,
|
replaceAvatar,
|
||||||
resetUsernameLink,
|
resetUsernameLink,
|
||||||
|
toggleProfileEditor,
|
||||||
saveAttachment,
|
saveAttachment,
|
||||||
saveAvatarToDisk,
|
saveAvatarToDisk,
|
||||||
setUsernameEditState,
|
setUsernameEditState,
|
||||||
|
@ -169,6 +174,7 @@ export function ProfileEditor({
|
||||||
username,
|
username,
|
||||||
usernameCorrupted,
|
usernameCorrupted,
|
||||||
usernameEditState,
|
usernameEditState,
|
||||||
|
usernameLinkRecovered,
|
||||||
usernameLinkState,
|
usernameLinkState,
|
||||||
usernameLinkColor,
|
usernameLinkColor,
|
||||||
usernameLink,
|
usernameLink,
|
||||||
|
@ -209,6 +215,7 @@ export function ProfileEditor({
|
||||||
firstName,
|
firstName,
|
||||||
});
|
});
|
||||||
const [isResettingUsername, setIsResettingUsername] = useState(false);
|
const [isResettingUsername, setIsResettingUsername] = useState(false);
|
||||||
|
const [isResettingUsernameLink, setIsResettingUsernameLink] = useState(false);
|
||||||
|
|
||||||
// Reset username edit state when leaving
|
// Reset username edit state when leaving
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -276,6 +283,13 @@ export function ProfileEditor({
|
||||||
onEditStateChanged(editState);
|
onEditStateChanged(editState);
|
||||||
}, [editState, onEditStateChanged]);
|
}, [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
|
// To make AvatarEditor re-render less often
|
||||||
const handleAvatarLoaded = useCallback(
|
const handleAvatarLoaded = useCallback(
|
||||||
avatar => {
|
avatar => {
|
||||||
|
@ -512,6 +526,7 @@ export function ProfileEditor({
|
||||||
);
|
);
|
||||||
} else if (editState === EditState.Username) {
|
} else if (editState === EditState.Username) {
|
||||||
content = renderEditUsernameModalBody({
|
content = renderEditUsernameModalBody({
|
||||||
|
isRootModal: initialEditState === editState,
|
||||||
onClose: () => setEditState(EditState.None),
|
onClose: () => setEditState(EditState.None),
|
||||||
});
|
});
|
||||||
} else if (editState === EditState.UsernameLink) {
|
} else if (editState === EditState.UsernameLink) {
|
||||||
|
@ -522,9 +537,11 @@ export function ProfileEditor({
|
||||||
username={username ?? ''}
|
username={username ?? ''}
|
||||||
colorId={usernameLinkColor}
|
colorId={usernameLinkColor}
|
||||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||||
|
usernameLinkRecovered={usernameLinkRecovered}
|
||||||
usernameLinkState={usernameLinkState}
|
usernameLinkState={usernameLinkState}
|
||||||
setUsernameLinkColor={setUsernameLinkColor}
|
setUsernameLinkColor={setUsernameLinkColor}
|
||||||
resetUsernameLink={resetUsernameLink}
|
resetUsernameLink={resetUsernameLink}
|
||||||
|
clearUsernameLinkRecovered={clearUsernameLinkRecovered}
|
||||||
saveAttachment={saveAttachment}
|
saveAttachment={saveAttachment}
|
||||||
showToast={showToast}
|
showToast={showToast}
|
||||||
onBack={() => setEditState(EditState.None)}
|
onBack={() => setEditState(EditState.None)}
|
||||||
|
@ -614,6 +631,11 @@ export function ProfileEditor({
|
||||||
}
|
}
|
||||||
label={i18n('icu:ProfileEditor__username-link')}
|
label={i18n('icu:ProfileEditor__username-link')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (usernameLinkCorrupted) {
|
||||||
|
setIsResettingUsernameLink(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setEditState(EditState.UsernameLink);
|
setEditState(EditState.UsernameLink);
|
||||||
}}
|
}}
|
||||||
alwaysShowActions
|
alwaysShowActions
|
||||||
|
@ -656,6 +678,7 @@ export function ProfileEditor({
|
||||||
|
|
||||||
maybeUsernameRows = (
|
maybeUsernameRows = (
|
||||||
<>
|
<>
|
||||||
|
<hr className="ProfileEditor__divider" />
|
||||||
<PanelRow
|
<PanelRow
|
||||||
className="ProfileEditor__row"
|
className="ProfileEditor__row"
|
||||||
icon={
|
icon={
|
||||||
|
@ -678,6 +701,11 @@ export function ProfileEditor({
|
||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
{maybeUsernameLinkRow}
|
{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}
|
avatarValue={avatarBuffer}
|
||||||
conversationTitle={getFullNameText()}
|
conversationTitle={getFullNameText()}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isEditable
|
|
||||||
onAvatarLoaded={handleAvatarLoaded}
|
onAvatarLoaded={handleAvatarLoaded}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditState(EditState.BetterAvatar);
|
setEditState(EditState.BetterAvatar);
|
||||||
|
@ -700,11 +727,17 @@ export function ProfileEditor({
|
||||||
width: 80,
|
width: 80,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h1 className="ProfileEditor__Title">{getFullNameText()}</h1>
|
<div className="ProfileEditor__EditPhotoContainer">
|
||||||
{phoneNumber != null && (
|
<Button
|
||||||
<p className="ProfileEditor__PhoneNumber">{phoneNumber}</p>
|
onClick={() => {
|
||||||
)}
|
setEditState(EditState.BetterAvatar);
|
||||||
<hr className="ProfileEditor__divider" />
|
}}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
className="ProfileEditor__EditPhoto"
|
||||||
|
>
|
||||||
|
{i18n('icu:ProfileEditor--edit-photo')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<PanelRow
|
<PanelRow
|
||||||
className="ProfileEditor__row"
|
className="ProfileEditor__row"
|
||||||
icon={
|
icon={
|
||||||
|
@ -715,7 +748,6 @@ export function ProfileEditor({
|
||||||
setEditState(EditState.ProfileName);
|
setEditState(EditState.ProfileName);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{maybeUsernameRows}
|
|
||||||
<PanelRow
|
<PanelRow
|
||||||
className="ProfileEditor__row"
|
className="ProfileEditor__row"
|
||||||
icon={
|
icon={
|
||||||
|
@ -736,26 +768,10 @@ export function ProfileEditor({
|
||||||
setEditState(EditState.Bio);
|
setEditState(EditState.Bio);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<hr className="ProfileEditor__divider" />
|
|
||||||
<div className="ProfileEditor__info">
|
<div className="ProfileEditor__info">
|
||||||
<Intl
|
{i18n('icu:ProfileEditor--info--general')}
|
||||||
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>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{maybeUsernameRows}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} 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 && (
|
{isResettingUsername && (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
dialogName="ProfileEditor.confirmResetUsername"
|
dialogName="ProfileEditor.confirmResetUsername"
|
||||||
|
@ -799,15 +837,7 @@ export function ProfileEditor({
|
||||||
onClose={() => setIsResettingUsername(false)}
|
onClose={() => setIsResettingUsername(false)}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
text: i18n(
|
text: i18n('icu:ProfileEditor--username--corrupted--fix-button'),
|
||||||
'icu:ProfileEditor--username--corrupted--delete-button'
|
|
||||||
),
|
|
||||||
action: () => deleteUsername(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n(
|
|
||||||
'icu:ProfileEditor--username--corrupted--create-button'
|
|
||||||
),
|
|
||||||
style: 'affirmative',
|
style: 'affirmative',
|
||||||
action: () => {
|
action: () => {
|
||||||
openUsernameReservationModal();
|
openUsernameReservationModal();
|
||||||
|
|
|
@ -74,6 +74,7 @@ export function ProfileEditorModal({
|
||||||
}}
|
}}
|
||||||
onProfileChanged={myProfileChanged}
|
onProfileChanged={myProfileChanged}
|
||||||
onSetSkinTone={onSetSkinTone}
|
onSetSkinTone={onSetSkinTone}
|
||||||
|
toggleProfileEditor={toggleProfileEditor}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -150,6 +150,13 @@ function getToast(toastType: ToastType): AnyToast {
|
||||||
return { toastType: ToastType.UnsupportedMultiAttachment };
|
return { toastType: ToastType.UnsupportedMultiAttachment };
|
||||||
case ToastType.UnsupportedOS:
|
case ToastType.UnsupportedOS:
|
||||||
return { toastType: ToastType.UnsupportedOS };
|
return { toastType: ToastType.UnsupportedOS };
|
||||||
|
case ToastType.UsernameRecovered:
|
||||||
|
return {
|
||||||
|
toastType: ToastType.UsernameRecovered,
|
||||||
|
parameters: {
|
||||||
|
username: 'maya.45',
|
||||||
|
},
|
||||||
|
};
|
||||||
case ToastType.UserAddedToGroup:
|
case ToastType.UserAddedToGroup:
|
||||||
return {
|
return {
|
||||||
toastType: ToastType.UserAddedToGroup,
|
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) {
|
if (toastType === ToastType.UserAddedToGroup) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
|
|
|
@ -35,6 +35,9 @@ export default {
|
||||||
usernameLinkCorrupted: {
|
usernameLinkCorrupted: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
usernameLinkRecovered: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
usernameLinkState: {
|
usernameLinkState: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
options: [
|
options: [
|
||||||
|
@ -66,6 +69,7 @@ export default {
|
||||||
showToast: action('showToast'),
|
showToast: action('showToast'),
|
||||||
resetUsernameLink: action('resetUsernameLink'),
|
resetUsernameLink: action('resetUsernameLink'),
|
||||||
setUsernameLinkColor: action('setUsernameLinkColor'),
|
setUsernameLinkColor: action('setUsernameLinkColor'),
|
||||||
|
clearUsernameLinkRecovered: action('clearUsernameLinkRecovered'),
|
||||||
onBack: action('onBack'),
|
onBack: action('onBack'),
|
||||||
},
|
},
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
|
|
@ -29,9 +29,11 @@ export type PropsType = Readonly<{
|
||||||
colorId?: number;
|
colorId?: number;
|
||||||
usernameLinkCorrupted: boolean;
|
usernameLinkCorrupted: boolean;
|
||||||
usernameLinkState: UsernameLinkState;
|
usernameLinkState: UsernameLinkState;
|
||||||
|
usernameLinkRecovered: boolean;
|
||||||
|
|
||||||
setUsernameLinkColor: (colorId: number) => void;
|
setUsernameLinkColor: (colorId: number) => void;
|
||||||
resetUsernameLink: () => void;
|
resetUsernameLink: () => void;
|
||||||
|
clearUsernameLinkRecovered: () => void;
|
||||||
saveAttachment: SaveAttachmentActionCreatorType;
|
saveAttachment: SaveAttachmentActionCreatorType;
|
||||||
showToast: ShowToastAction;
|
showToast: ShowToastAction;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
@ -532,10 +534,12 @@ export function UsernameLinkModalBody({
|
||||||
username,
|
username,
|
||||||
usernameLinkCorrupted,
|
usernameLinkCorrupted,
|
||||||
usernameLinkState,
|
usernameLinkState,
|
||||||
|
usernameLinkRecovered,
|
||||||
colorId: initialColorId = ColorEnum.UNKNOWN,
|
colorId: initialColorId = ColorEnum.UNKNOWN,
|
||||||
|
|
||||||
setUsernameLinkColor,
|
setUsernameLinkColor,
|
||||||
resetUsernameLink,
|
resetUsernameLink,
|
||||||
|
clearUsernameLinkRecovered,
|
||||||
saveAttachment,
|
saveAttachment,
|
||||||
showToast,
|
showToast,
|
||||||
|
|
||||||
|
@ -544,6 +548,7 @@ export function UsernameLinkModalBody({
|
||||||
const [pngData, setPngData] = useState<Uint8Array | undefined>();
|
const [pngData, setPngData] = useState<Uint8Array | undefined>();
|
||||||
const [showColors, setShowColors] = useState(false);
|
const [showColors, setShowColors] = useState(false);
|
||||||
const [confirmReset, setConfirmReset] = useState(false);
|
const [confirmReset, setConfirmReset] = useState(false);
|
||||||
|
const [isRecovered, setIsRecovered] = useState(false);
|
||||||
const [showError, setShowError] = useState(false);
|
const [showError, setShowError] = useState(false);
|
||||||
const [colorId, setColorId] = useState(initialColorId);
|
const [colorId, setColorId] = useState(initialColorId);
|
||||||
|
|
||||||
|
@ -662,10 +667,17 @@ export function UsernameLinkModalBody({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onConfirmReset = useCallback(() => {
|
const onConfirmReset = useCallback(() => {
|
||||||
|
setShowError(false);
|
||||||
setConfirmReset(false);
|
setConfirmReset(false);
|
||||||
resetUsernameLink();
|
resetUsernameLink();
|
||||||
}, [resetUsernameLink]);
|
}, [resetUsernameLink]);
|
||||||
|
|
||||||
|
const onCloseError = useCallback(() => {
|
||||||
|
if (showError) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}, [showError, onBack]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!usernameLinkCorrupted) {
|
if (!usernameLinkCorrupted) {
|
||||||
return;
|
return;
|
||||||
|
@ -682,12 +694,21 @@ export function UsernameLinkModalBody({
|
||||||
setShowError(true);
|
setShowError(true);
|
||||||
}, [usernameLinkState]);
|
}, [usernameLinkState]);
|
||||||
|
|
||||||
const onClearError = useCallback(() => {
|
useEffect(() => {
|
||||||
setShowError(false);
|
if (usernameLinkRecovered) {
|
||||||
|
setIsRecovered(true);
|
||||||
|
|
||||||
|
// Only show the modal once
|
||||||
|
clearUsernameLinkRecovered();
|
||||||
|
}
|
||||||
|
}, [usernameLinkRecovered, clearUsernameLinkRecovered]);
|
||||||
|
|
||||||
|
const onClearIsRecovered = useCallback(() => {
|
||||||
|
setIsRecovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isResettingLink =
|
const isReady = usernameLinkState === UsernameLinkState.Ready;
|
||||||
usernameLinkCorrupted || usernameLinkState !== UsernameLinkState.Ready;
|
const isResettingLink = usernameLinkCorrupted || !isReady;
|
||||||
|
|
||||||
const info = (
|
const info = (
|
||||||
<>
|
<>
|
||||||
|
@ -754,7 +775,7 @@ export function UsernameLinkModalBody({
|
||||||
);
|
);
|
||||||
|
|
||||||
let linkImage: JSX.Element | undefined;
|
let linkImage: JSX.Element | undefined;
|
||||||
if (usernameLinkState === UsernameLinkState.Ready && link) {
|
if (isReady && link) {
|
||||||
linkImage = (
|
linkImage = (
|
||||||
<svg
|
<svg
|
||||||
className={`${CLASS}__card__qr__blotches`}
|
className={`${CLASS}__card__qr__blotches`}
|
||||||
|
@ -820,11 +841,30 @@ export function UsernameLinkModalBody({
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
dialogName="UsernameLinkModal__error"
|
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}
|
cancelButtonVariant={ButtonVariant.Secondary}
|
||||||
cancelText={i18n('icu:ok')}
|
cancelText={i18n('icu:ok')}
|
||||||
>
|
>
|
||||||
{i18n('icu:UsernameLinkModalBody__error__text')}
|
{i18n('icu:UsernameLinkModalBody__recovered__text')}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ export class SettingsChannel extends EventEmitter {
|
||||||
|
|
||||||
public install(): void {
|
public install(): void {
|
||||||
this.installSetting('deviceName', { setter: false });
|
this.installSetting('deviceName', { setter: false });
|
||||||
|
this.installSetting('phoneNumber', { setter: false });
|
||||||
|
|
||||||
// ChatColorPicker redux hookups
|
// ChatColorPicker redux hookups
|
||||||
this.installCallback('getCustomColors');
|
this.installCallback('getCustomColors');
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { UsernameReservationType } from '../types/Username';
|
||||||
import {
|
import {
|
||||||
ReserveUsernameError,
|
ReserveUsernameError,
|
||||||
ConfirmUsernameResult,
|
ConfirmUsernameResult,
|
||||||
|
ResetUsernameLinkResult,
|
||||||
getNickname,
|
getNickname,
|
||||||
getDiscriminator,
|
getDiscriminator,
|
||||||
isCaseChange,
|
isCaseChange,
|
||||||
|
@ -245,10 +246,10 @@ export async function confirmUsername(
|
||||||
const { hash } = reservation;
|
const { hash } = reservation;
|
||||||
strictAssert(usernames.hash(username).equals(hash), 'username hash mismatch');
|
strictAssert(usernames.hash(username).equals(hash), 'username hash mismatch');
|
||||||
|
|
||||||
|
const wasCorrupted = window.storage.get('usernameCorrupted');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.storage.remove('usernameLink');
|
await window.storage.remove('usernameLink');
|
||||||
await window.storage.remove('usernameCorrupted');
|
|
||||||
await window.storage.remove('usernameLinkCorrupted');
|
|
||||||
|
|
||||||
let serverIdString: string;
|
let serverIdString: string;
|
||||||
let entropy: Buffer;
|
let entropy: Buffer;
|
||||||
|
@ -288,6 +289,8 @@ export async function confirmUsername(
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateUsernameAndSyncProfile(username);
|
await updateUsernameAndSyncProfile(username);
|
||||||
|
await window.storage.remove('usernameCorrupted');
|
||||||
|
await window.storage.remove('usernameLinkCorrupted');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HTTPError) {
|
if (error instanceof HTTPError) {
|
||||||
if (error.code === 413 || error.code === 429) {
|
if (error.code === 413 || error.code === 429) {
|
||||||
|
@ -305,7 +308,9 @@ export async function confirmUsername(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfirmUsernameResult.Ok;
|
return wasCorrupted
|
||||||
|
? ConfirmUsernameResult.OkRecovered
|
||||||
|
: ConfirmUsernameResult.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUsername(
|
export async function deleteUsername(
|
||||||
|
@ -324,12 +329,14 @@ export async function deleteUsername(
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.storage.remove('usernameLink');
|
await window.storage.remove('usernameLink');
|
||||||
await window.storage.remove('usernameCorrupted');
|
|
||||||
await server.deleteUsername(abortSignal);
|
await server.deleteUsername(abortSignal);
|
||||||
|
await window.storage.remove('usernameCorrupted');
|
||||||
await updateUsernameAndSyncProfile(undefined);
|
await updateUsernameAndSyncProfile(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetLink(username: string): Promise<void> {
|
export async function resetLink(
|
||||||
|
username: string
|
||||||
|
): Promise<ResetUsernameLinkResult> {
|
||||||
const { server } = window.textsecure;
|
const { server } = window.textsecure;
|
||||||
if (!server) {
|
if (!server) {
|
||||||
throw new Error('server interface is not available!');
|
throw new Error('server interface is not available!');
|
||||||
|
@ -343,8 +350,9 @@ export async function resetLink(username: string): Promise<void> {
|
||||||
|
|
||||||
const { entropy, encryptedUsername } = usernames.createUsernameLink(username);
|
const { entropy, encryptedUsername } = usernames.createUsernameLink(username);
|
||||||
|
|
||||||
|
const wasCorrupted = window.storage.get('usernameLinkCorrupted');
|
||||||
|
|
||||||
await window.storage.remove('usernameLink');
|
await window.storage.remove('usernameLink');
|
||||||
await window.storage.remove('usernameLinkCorrupted');
|
|
||||||
|
|
||||||
const { usernameLinkHandle: serverIdString } =
|
const { usernameLinkHandle: serverIdString } =
|
||||||
await server.replaceUsernameLink({
|
await server.replaceUsernameLink({
|
||||||
|
@ -356,9 +364,14 @@ export async function resetLink(username: string): Promise<void> {
|
||||||
entropy,
|
entropy,
|
||||||
serverId: uuidToBytes(serverIdString),
|
serverId: uuidToBytes(serverIdString),
|
||||||
});
|
});
|
||||||
|
await window.storage.remove('usernameLinkCorrupted');
|
||||||
|
|
||||||
me.captureChange('usernameLink');
|
me.captureChange('usernameLink');
|
||||||
storageServiceUploadJob();
|
storageServiceUploadJob();
|
||||||
|
|
||||||
|
return wasCorrupted
|
||||||
|
? ResetUsernameLinkResult.OkRecovered
|
||||||
|
: ResetUsernameLinkResult.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
const USERNAME_LINK_ENTROPY_SIZE = 32;
|
const USERNAME_LINK_ENTROPY_SIZE = 32;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type { UsernameReservationType } from '../../types/Username';
|
||||||
import {
|
import {
|
||||||
ReserveUsernameError,
|
ReserveUsernameError,
|
||||||
ConfirmUsernameResult,
|
ConfirmUsernameResult,
|
||||||
|
ResetUsernameLinkResult,
|
||||||
} from '../../types/Username';
|
} from '../../types/Username';
|
||||||
import * as usernameServices from '../../services/username';
|
import * as usernameServices from '../../services/username';
|
||||||
import { storageServiceUploadJob } from '../../services/storage';
|
import { storageServiceUploadJob } from '../../services/storage';
|
||||||
|
@ -33,6 +34,7 @@ import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
export type UsernameReservationStateType = ReadonlyDeep<{
|
export type UsernameReservationStateType = ReadonlyDeep<{
|
||||||
state: UsernameReservationState;
|
state: UsernameReservationState;
|
||||||
|
recoveredUsername?: string;
|
||||||
reservation?: UsernameReservationType;
|
reservation?: UsernameReservationType;
|
||||||
error?: UsernameReservationError;
|
error?: UsernameReservationError;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
|
@ -44,6 +46,7 @@ export type UsernameStateType = ReadonlyDeep<{
|
||||||
|
|
||||||
// UsernameLinkModalBody
|
// UsernameLinkModalBody
|
||||||
linkState: UsernameLinkState;
|
linkState: UsernameLinkState;
|
||||||
|
linkRecovered: boolean;
|
||||||
|
|
||||||
// EditUsernameModalBody
|
// EditUsernameModalBody
|
||||||
usernameReservation: UsernameReservationStateType;
|
usernameReservation: UsernameReservationStateType;
|
||||||
|
@ -60,6 +63,7 @@ const RESERVE_USERNAME = 'username/RESERVE_USERNAME';
|
||||||
const CONFIRM_USERNAME = 'username/CONFIRM_USERNAME';
|
const CONFIRM_USERNAME = 'username/CONFIRM_USERNAME';
|
||||||
const DELETE_USERNAME = 'username/DELETE_USERNAME';
|
const DELETE_USERNAME = 'username/DELETE_USERNAME';
|
||||||
const RESET_USERNAME_LINK = 'username/RESET_USERNAME_LINK';
|
const RESET_USERNAME_LINK = 'username/RESET_USERNAME_LINK';
|
||||||
|
const CLEAR_USERNAME_LINK_RECOVERED = 'username/CLEAR_USERNAME_LINK_RECOVERED';
|
||||||
|
|
||||||
type SetUsernameEditStateActionType = ReadonlyDeep<{
|
type SetUsernameEditStateActionType = ReadonlyDeep<{
|
||||||
type: typeof SET_USERNAME_EDIT_STATE;
|
type: typeof SET_USERNAME_EDIT_STATE;
|
||||||
|
@ -83,7 +87,7 @@ type SetUsernameReservationErrorActionType = ReadonlyDeep<{
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ClearUsernameReservation = ReadonlyDeep<{
|
type ClearUsernameReservationActionType = ReadonlyDeep<{
|
||||||
type: typeof CLEAR_USERNAME_RESERVATION;
|
type: typeof CLEAR_USERNAME_RESERVATION;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -101,19 +105,23 @@ type DeleteUsernameActionType = ReadonlyDeep<
|
||||||
PromiseAction<typeof DELETE_USERNAME, void>
|
PromiseAction<typeof DELETE_USERNAME, void>
|
||||||
>;
|
>;
|
||||||
type ResetUsernameLinkActionType = ReadonlyDeep<
|
type ResetUsernameLinkActionType = ReadonlyDeep<
|
||||||
PromiseAction<typeof RESET_USERNAME_LINK, void>
|
PromiseAction<typeof RESET_USERNAME_LINK, ResetUsernameLinkResult>
|
||||||
>;
|
>;
|
||||||
|
type ClearUsernameLinkRecoveredActionType = ReadonlyDeep<{
|
||||||
|
type: typeof CLEAR_USERNAME_LINK_RECOVERED;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type UsernameActionType = ReadonlyDeep<
|
export type UsernameActionType = ReadonlyDeep<
|
||||||
| SetUsernameEditStateActionType
|
| SetUsernameEditStateActionType
|
||||||
| OpenUsernameReservationModalActionType
|
| OpenUsernameReservationModalActionType
|
||||||
| CloseUsernameReservationModalActionType
|
| CloseUsernameReservationModalActionType
|
||||||
| SetUsernameReservationErrorActionType
|
| SetUsernameReservationErrorActionType
|
||||||
| ClearUsernameReservation
|
| ClearUsernameReservationActionType
|
||||||
| ReserveUsernameActionType
|
| ReserveUsernameActionType
|
||||||
| ConfirmUsernameActionType
|
| ConfirmUsernameActionType
|
||||||
| DeleteUsernameActionType
|
| DeleteUsernameActionType
|
||||||
| ResetUsernameLinkActionType
|
| ResetUsernameLinkActionType
|
||||||
|
| ClearUsernameLinkRecoveredActionType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -127,6 +135,7 @@ export const actions = {
|
||||||
deleteUsername,
|
deleteUsername,
|
||||||
markCompletedUsernameOnboarding,
|
markCompletedUsernameOnboarding,
|
||||||
resetUsernameLink,
|
resetUsernameLink,
|
||||||
|
clearUsernameLinkRecovered,
|
||||||
setUsernameLinkColor,
|
setUsernameLinkColor,
|
||||||
markCompletedUsernameLinkOnboarding,
|
markCompletedUsernameLinkOnboarding,
|
||||||
};
|
};
|
||||||
|
@ -165,7 +174,7 @@ export function setUsernameReservationError(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearUsernameReservation(): ClearUsernameReservation {
|
export function clearUsernameReservation(): ClearUsernameReservationActionType {
|
||||||
return {
|
return {
|
||||||
type: CLEAR_USERNAME_RESERVATION,
|
type: CLEAR_USERNAME_RESERVATION,
|
||||||
};
|
};
|
||||||
|
@ -352,12 +361,19 @@ function setUsernameLinkColor(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearUsernameLinkRecovered(): ClearUsernameLinkRecoveredActionType {
|
||||||
|
return {
|
||||||
|
type: CLEAR_USERNAME_LINK_RECOVERED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
|
|
||||||
export function getEmptyState(): UsernameStateType {
|
export function getEmptyState(): UsernameStateType {
|
||||||
return {
|
return {
|
||||||
editState: UsernameEditState.Editing,
|
editState: UsernameEditState.Editing,
|
||||||
linkState: UsernameLinkState.Ready,
|
linkState: UsernameLinkState.Ready,
|
||||||
|
linkRecovered: false,
|
||||||
usernameReservation: {
|
usernameReservation: {
|
||||||
state: UsernameReservationState.Closed,
|
state: UsernameReservationState.Closed,
|
||||||
},
|
},
|
||||||
|
@ -370,6 +386,17 @@ export function reducer(
|
||||||
): UsernameStateType {
|
): UsernameStateType {
|
||||||
const { usernameReservation } = state;
|
const { usernameReservation } = state;
|
||||||
|
|
||||||
|
if (action.type === OPEN_USERNAME_RESERVATION_MODAL) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editState: UsernameEditState.Editing,
|
||||||
|
linkState: UsernameLinkState.Ready,
|
||||||
|
usernameReservation: {
|
||||||
|
state: UsernameReservationState.Open,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === SET_USERNAME_EDIT_STATE) {
|
if (action.type === SET_USERNAME_EDIT_STATE) {
|
||||||
const { editState } = action.payload;
|
const { editState } = action.payload;
|
||||||
return {
|
return {
|
||||||
|
@ -378,15 +405,6 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === OPEN_USERNAME_RESERVATION_MODAL) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
usernameReservation: {
|
|
||||||
state: UsernameReservationState.Open,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === CLOSE_USERNAME_RESERVATION_MODAL) {
|
if (action.type === CLOSE_USERNAME_RESERVATION_MODAL) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -546,6 +564,20 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (payload === ConfirmUsernameResult.OkRecovered) {
|
||||||
|
const { reservation } = state.usernameReservation;
|
||||||
|
assertDev(
|
||||||
|
reservation !== undefined,
|
||||||
|
'Must be reserving before resolving confirmation'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
usernameReservation: {
|
||||||
|
state: UsernameReservationState.Closed,
|
||||||
|
recoveredUsername: reservation.username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
if (payload === ConfirmUsernameResult.ConflictOrGone) {
|
if (payload === ConfirmUsernameResult.ConflictOrGone) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -597,13 +629,16 @@ export function reducer(
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
linkState: UsernameLinkState.Updating,
|
linkState: UsernameLinkState.Updating,
|
||||||
|
linkRecovered: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'username/RESET_USERNAME_LINK_FULFILLED') {
|
if (action.type === 'username/RESET_USERNAME_LINK_FULFILLED') {
|
||||||
|
const { payload } = action;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
linkState: UsernameLinkState.Ready,
|
linkState: UsernameLinkState.Ready,
|
||||||
|
linkRecovered: payload === ResetUsernameLinkResult.OkRecovered,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,6 +646,14 @@ export function reducer(
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
linkState: UsernameLinkState.Error,
|
linkState: UsernameLinkState.Error,
|
||||||
|
linkRecovered: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'username/CLEAR_USERNAME_LINK_RECOVERED') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
linkRecovered: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,11 @@ export const getUsernameLinkState = createSelector(
|
||||||
(state: UsernameStateType): UsernameLinkState => state.linkState
|
(state: UsernameStateType): UsernameLinkState => state.linkState
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getUsernameLinkRecovered = createSelector(
|
||||||
|
getUsernameState,
|
||||||
|
(state: UsernameStateType): boolean => state.linkRecovered
|
||||||
|
);
|
||||||
|
|
||||||
export const getUsernameReservation = createSelector(
|
export const getUsernameReservation = createSelector(
|
||||||
getUsernameState,
|
getUsernameState,
|
||||||
(state: UsernameStateType): UsernameReservationStateType =>
|
(state: UsernameStateType): UsernameReservationStateType =>
|
||||||
|
@ -54,3 +59,9 @@ export const getUsernameReservationError = createSelector(
|
||||||
reservation: UsernameReservationStateType
|
reservation: UsernameReservationStateType
|
||||||
): UsernameReservationError | undefined => reservation.error
|
): UsernameReservationError | undefined => reservation.error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getRecoveredUsername = createSelector(
|
||||||
|
getUsernameReservation,
|
||||||
|
(reservation: UsernameReservationStateType): string | undefined =>
|
||||||
|
reservation.recoveredUsername
|
||||||
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
getUsernameReservationState,
|
getUsernameReservationState,
|
||||||
getUsernameReservationObject,
|
getUsernameReservationObject,
|
||||||
getUsernameReservationError,
|
getUsernameReservationError,
|
||||||
|
getRecoveredUsername,
|
||||||
} from '../selectors/username';
|
} from '../selectors/username';
|
||||||
import { getUsernameCorrupted } from '../selectors/items';
|
import { getUsernameCorrupted } from '../selectors/items';
|
||||||
import { getMe } from '../selectors/conversations';
|
import { getMe } from '../selectors/conversations';
|
||||||
|
@ -25,10 +26,12 @@ function mapStateToProps(state: StateType): PropsDataType {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
i18n,
|
i18n,
|
||||||
|
usernameCorrupted,
|
||||||
currentUsername: usernameCorrupted ? undefined : username,
|
currentUsername: usernameCorrupted ? undefined : username,
|
||||||
minNickname: getMinNickname(),
|
minNickname: getMinNickname(),
|
||||||
maxNickname: getMaxNickname(),
|
maxNickname: getMaxNickname(),
|
||||||
state: getUsernameReservationState(state),
|
state: getUsernameReservationState(state),
|
||||||
|
recoveredUsername: getRecoveredUsername(state),
|
||||||
reservation: getUsernameReservationObject(state),
|
reservation: getUsernameReservationObject(state),
|
||||||
error: getUsernameReservationError(state),
|
error: getUsernameReservationError(state),
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,9 +25,11 @@ import { selectRecentEmojis } from '../selectors/emojis';
|
||||||
import {
|
import {
|
||||||
getUsernameEditState,
|
getUsernameEditState,
|
||||||
getUsernameLinkState,
|
getUsernameLinkState,
|
||||||
|
getUsernameLinkRecovered,
|
||||||
} from '../selectors/username';
|
} from '../selectors/username';
|
||||||
|
|
||||||
function renderEditUsernameModalBody(props: {
|
function renderEditUsernameModalBody(props: {
|
||||||
|
isRootModal: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return <SmartEditUsernameModalBody {...props} />;
|
return <SmartEditUsernameModalBody {...props} />;
|
||||||
|
@ -46,7 +48,6 @@ function mapStateToProps(
|
||||||
firstName,
|
firstName,
|
||||||
familyName,
|
familyName,
|
||||||
id: conversationId,
|
id: conversationId,
|
||||||
phoneNumber,
|
|
||||||
username,
|
username,
|
||||||
} = getMe(state);
|
} = getMe(state);
|
||||||
const recentEmojis = selectRecentEmojis(state);
|
const recentEmojis = selectRecentEmojis(state);
|
||||||
|
@ -56,6 +57,7 @@ function mapStateToProps(
|
||||||
getHasCompletedUsernameLinkOnboarding(state);
|
getHasCompletedUsernameLinkOnboarding(state);
|
||||||
const usernameEditState = getUsernameEditState(state);
|
const usernameEditState = getUsernameEditState(state);
|
||||||
const usernameLinkState = getUsernameLinkState(state);
|
const usernameLinkState = getUsernameLinkState(state);
|
||||||
|
const usernameLinkRecovered = getUsernameLinkRecovered(state);
|
||||||
const usernameLinkColor = getUsernameLinkColor(state);
|
const usernameLinkColor = getUsernameLinkColor(state);
|
||||||
const usernameLink = getUsernameLink(state);
|
const usernameLink = getUsernameLink(state);
|
||||||
const usernameCorrupted = getUsernameCorrupted(state);
|
const usernameCorrupted = getUsernameCorrupted(state);
|
||||||
|
@ -76,7 +78,6 @@ function mapStateToProps(
|
||||||
isUsernameFlagEnabled,
|
isUsernameFlagEnabled,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
skinTone,
|
skinTone,
|
||||||
phoneNumber,
|
|
||||||
userAvatarData,
|
userAvatarData,
|
||||||
username,
|
username,
|
||||||
usernameCorrupted,
|
usernameCorrupted,
|
||||||
|
@ -84,6 +85,7 @@ function mapStateToProps(
|
||||||
usernameLinkState,
|
usernameLinkState,
|
||||||
usernameLinkColor,
|
usernameLinkColor,
|
||||||
usernameLinkCorrupted,
|
usernameLinkCorrupted,
|
||||||
|
usernameLinkRecovered,
|
||||||
usernameLink,
|
usernameLink,
|
||||||
|
|
||||||
renderEditUsernameModalBody,
|
renderEditUsernameModalBody,
|
||||||
|
|
|
@ -58,6 +58,7 @@ export enum ToastType {
|
||||||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||||
UnsupportedOS = 'UnsupportedOS',
|
UnsupportedOS = 'UnsupportedOS',
|
||||||
UserAddedToGroup = 'UserAddedToGroup',
|
UserAddedToGroup = 'UserAddedToGroup',
|
||||||
|
UsernameRecovered = 'UsernameRecovered',
|
||||||
VoiceNoteLimit = 'VoiceNoteLimit',
|
VoiceNoteLimit = 'VoiceNoteLimit',
|
||||||
VoiceNoteMustBeTheOnlyAttachment = 'VoiceNoteMustBeTheOnlyAttachment',
|
VoiceNoteMustBeTheOnlyAttachment = 'VoiceNoteMustBeTheOnlyAttachment',
|
||||||
WhoCanFindMeReadOnly = 'WhoCanFindMeReadOnly',
|
WhoCanFindMeReadOnly = 'WhoCanFindMeReadOnly',
|
||||||
|
@ -138,6 +139,10 @@ export type AnyToast =
|
||||||
toastType: ToastType.UserAddedToGroup;
|
toastType: ToastType.UserAddedToGroup;
|
||||||
parameters: { contact: string; group: string };
|
parameters: { contact: string; group: string };
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
toastType: ToastType.UsernameRecovered;
|
||||||
|
parameters: { username: string };
|
||||||
|
}
|
||||||
| { toastType: ToastType.VoiceNoteLimit }
|
| { toastType: ToastType.VoiceNoteLimit }
|
||||||
| { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment }
|
| { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment }
|
||||||
| { toastType: ToastType.WhoCanFindMeReadOnly };
|
| { toastType: ToastType.WhoCanFindMeReadOnly };
|
||||||
|
|
|
@ -23,9 +23,15 @@ export enum ReserveUsernameError {
|
||||||
|
|
||||||
export enum ConfirmUsernameResult {
|
export enum ConfirmUsernameResult {
|
||||||
Ok = 'Ok',
|
Ok = 'Ok',
|
||||||
|
OkRecovered = 'OkRecovered',
|
||||||
ConflictOrGone = 'ConflictOrGone',
|
ConflictOrGone = 'ConflictOrGone',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ResetUsernameLinkResult {
|
||||||
|
Ok = 'Ok',
|
||||||
|
OkRecovered = 'OkRecovered',
|
||||||
|
}
|
||||||
|
|
||||||
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||||
try {
|
try {
|
||||||
window.SignalContext.usernames.hash(searchTerm);
|
window.SignalContext.usernames.hash(searchTerm);
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { StoryViewModeType, StoryViewTargetType } from '../types/Stories';
|
||||||
import { isValidE164 } from './isValidE164';
|
import { isValidE164 } from './isValidE164';
|
||||||
import { fromWebSafeBase64 } from './webSafeBase64';
|
import { fromWebSafeBase64 } from './webSafeBase64';
|
||||||
import { getConversation } from './getConversation';
|
import { getConversation } from './getConversation';
|
||||||
|
import { instance, PhoneNumberFormat } from './libphonenumberInstance';
|
||||||
|
|
||||||
type SentMediaQualityType = 'standard' | 'high';
|
type SentMediaQualityType = 'standard' | 'high';
|
||||||
type ThemeType = 'light' | 'dark' | 'system';
|
type ThemeType = 'light' | 'dark' | 'system';
|
||||||
|
@ -90,6 +91,7 @@ export type IPCEventsValuesType = {
|
||||||
readReceiptSetting: boolean;
|
readReceiptSetting: boolean;
|
||||||
typingIndicatorSetting: boolean;
|
typingIndicatorSetting: boolean;
|
||||||
deviceName: string | undefined;
|
deviceName: string | undefined;
|
||||||
|
phoneNumber: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IPCEventsCallbacksType = {
|
export type IPCEventsCallbacksType = {
|
||||||
|
@ -158,6 +160,7 @@ type ValuesWithSetters = Omit<
|
||||||
| 'readReceiptSetting'
|
| 'readReceiptSetting'
|
||||||
| 'typingIndicatorSetting'
|
| 'typingIndicatorSetting'
|
||||||
| 'deviceName'
|
| 'deviceName'
|
||||||
|
| 'phoneNumber'
|
||||||
|
|
||||||
// Optional
|
// Optional
|
||||||
| 'mediaPermissions'
|
| 'mediaPermissions'
|
||||||
|
@ -222,6 +225,11 @@ export function createIPCEvents(
|
||||||
},
|
},
|
||||||
|
|
||||||
getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
|
getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
|
||||||
|
getPhoneNumber: () => {
|
||||||
|
const e164 = window.textsecure.storage.user.getNumber();
|
||||||
|
const parsedNumber = instance.parse(e164);
|
||||||
|
return instance.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
|
||||||
|
},
|
||||||
|
|
||||||
getZoomFactor: () => {
|
getZoomFactor: () => {
|
||||||
return ipcRenderer.invoke('getZoomFactor');
|
return ipcRenderer.invoke('getZoomFactor');
|
||||||
|
|
|
@ -44,6 +44,7 @@ installSetting('callRingtoneNotification');
|
||||||
installSetting('callSystemNotification');
|
installSetting('callSystemNotification');
|
||||||
installSetting('countMutedConversations');
|
installSetting('countMutedConversations');
|
||||||
installSetting('deviceName');
|
installSetting('deviceName');
|
||||||
|
installSetting('phoneNumber');
|
||||||
installSetting('hasStoriesDisabled');
|
installSetting('hasStoriesDisabled');
|
||||||
installSetting('hideMenuBar');
|
installSetting('hideMenuBar');
|
||||||
installSetting('incomingCallNotification');
|
installSetting('incomingCallNotification');
|
||||||
|
|
|
@ -28,6 +28,7 @@ SettingsWindowProps.onRender(
|
||||||
customColors,
|
customColors,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
deviceName,
|
deviceName,
|
||||||
|
phoneNumber,
|
||||||
doDeleteAllData,
|
doDeleteAllData,
|
||||||
doneRendering,
|
doneRendering,
|
||||||
editCustomColor,
|
editCustomColor,
|
||||||
|
@ -129,6 +130,7 @@ SettingsWindowProps.onRender(
|
||||||
customColors={customColors}
|
customColors={customColors}
|
||||||
defaultConversationColor={defaultConversationColor}
|
defaultConversationColor={defaultConversationColor}
|
||||||
deviceName={deviceName}
|
deviceName={deviceName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
doDeleteAllData={doDeleteAllData}
|
doDeleteAllData={doDeleteAllData}
|
||||||
doneRendering={doneRendering}
|
doneRendering={doneRendering}
|
||||||
editCustomColor={editCustomColor}
|
editCustomColor={editCustomColor}
|
||||||
|
|
|
@ -31,6 +31,7 @@ const settingCallRingtoneNotification = createSetting(
|
||||||
const settingCallSystemNotification = createSetting('callSystemNotification');
|
const settingCallSystemNotification = createSetting('callSystemNotification');
|
||||||
const settingCountMutedConversations = createSetting('countMutedConversations');
|
const settingCountMutedConversations = createSetting('countMutedConversations');
|
||||||
const settingDeviceName = createSetting('deviceName', { setter: false });
|
const settingDeviceName = createSetting('deviceName', { setter: false });
|
||||||
|
const settingPhoneNumber = createSetting('phoneNumber', { setter: false });
|
||||||
const settingHideMenuBar = createSetting('hideMenuBar');
|
const settingHideMenuBar = createSetting('hideMenuBar');
|
||||||
const settingIncomingCallNotification = createSetting(
|
const settingIncomingCallNotification = createSetting(
|
||||||
'incomingCallNotification'
|
'incomingCallNotification'
|
||||||
|
@ -163,6 +164,7 @@ async function renderPreferences() {
|
||||||
isPhoneNumberSharingSupported,
|
isPhoneNumberSharingSupported,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
notificationContent,
|
notificationContent,
|
||||||
|
phoneNumber,
|
||||||
selectedCamera,
|
selectedCamera,
|
||||||
selectedMicrophone,
|
selectedMicrophone,
|
||||||
selectedSpeaker,
|
selectedSpeaker,
|
||||||
|
@ -205,6 +207,7 @@ async function renderPreferences() {
|
||||||
isPhoneNumberSharingSupported: ipcPNP(),
|
isPhoneNumberSharingSupported: ipcPNP(),
|
||||||
lastSyncTime: settingLastSyncTime.getValue(),
|
lastSyncTime: settingLastSyncTime.getValue(),
|
||||||
notificationContent: settingNotificationSetting.getValue(),
|
notificationContent: settingNotificationSetting.getValue(),
|
||||||
|
phoneNumber: settingPhoneNumber.getValue(),
|
||||||
selectedCamera: settingVideoInput.getValue(),
|
selectedCamera: settingVideoInput.getValue(),
|
||||||
selectedMicrophone: settingAudioInput.getValue(),
|
selectedMicrophone: settingAudioInput.getValue(),
|
||||||
selectedSpeaker: settingAudioOutput.getValue(),
|
selectedSpeaker: settingAudioOutput.getValue(),
|
||||||
|
@ -275,6 +278,7 @@ async function renderPreferences() {
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
localeOverride,
|
localeOverride,
|
||||||
notificationContent,
|
notificationContent,
|
||||||
|
phoneNumber,
|
||||||
preferredSystemLocales,
|
preferredSystemLocales,
|
||||||
resolvedLocale,
|
resolvedLocale,
|
||||||
selectedCamera,
|
selectedCamera,
|
||||||
|
|
Loading…
Reference in a new issue