Custom Discriminator in EditUsernameModalBody
This commit is contained in:
parent
fa3937e084
commit
38914a45cb
26 changed files with 615 additions and 165 deletions
|
@ -5535,6 +5535,18 @@
|
|||
"messageformat": "Usernames must have at most {max, number} characters.",
|
||||
"description": "Shown if user has attempted to enter a username with too many characters - currently min is 25"
|
||||
},
|
||||
"icu:ProfileEditor--username--check-discriminator-min": {
|
||||
"messageformat": "Invalid username, enter a minimum of 2 digits.",
|
||||
"description": "Shown if user has attempted to enter a username with too few digits in discriminator - currently min is 2"
|
||||
},
|
||||
"icu:ProfileEditor--username--check-discriminator-all-zero": {
|
||||
"messageformat": "This number can’t be 00. Enter a digit between 1-9",
|
||||
"description": "Shown if user has attempted to enter a username with 00 as discriminator"
|
||||
},
|
||||
"icu:ProfileEditor--username--check-discriminator-leading-zero": {
|
||||
"messageformat": "This number can’t start with 00. Enter a digit between 1-9",
|
||||
"description": "Shown if user has attempted to enter a username with leading 0 in discriminator"
|
||||
},
|
||||
"icu:ProfileEditor--username--unavailable": {
|
||||
"messageformat": "This username is not available",
|
||||
"description": "Shown if the username is not available for registration"
|
||||
|
@ -5776,11 +5788,11 @@
|
|||
"description": "Title for the phone number sharing setting row"
|
||||
},
|
||||
"icu:Preferences__pnp__sharing--description--everyone": {
|
||||
"messageformat": "Your phone number will be visible to people and groups you message. People who have your number in their phone contacts will also see it on Signal.",
|
||||
"messageformat": "Your phone number will be visible to people and groups you message.",
|
||||
"description": "Description for the phone number sharing setting row when the value is Everyone"
|
||||
},
|
||||
"icu:Preferences__pnp__sharing--description--nobody": {
|
||||
"messageformat": "Nobody will see your phone number on Signal.",
|
||||
"messageformat": "Nobody will see your phone number on Signal, even when you're messaging them.",
|
||||
"description": "Description for the phone number sharing setting row when the value is Nobody"
|
||||
},
|
||||
"icu:Preferences__pnp--page-title": {
|
||||
|
@ -5800,11 +5812,11 @@
|
|||
"description": "Title for the phone number discoverability setting row"
|
||||
},
|
||||
"icu:Preferences__pnp__discoverability--description--everyone": {
|
||||
"messageformat": "Anyone who has your phone number in their contacts will see you as a contact on Signal. Others will be able to reach you with your phone number when they start a new chat or group.",
|
||||
"messageformat": "Anyone who has your phone number will see you're on Signal and can start chats with you.",
|
||||
"description": "Description for the phone number discoverability setting row wth the value is everyone"
|
||||
},
|
||||
"icu:Preferences__pnp__discoverability--description--nobody": {
|
||||
"messageformat": "Nobody on Signal will be able to reach you with your phone number.",
|
||||
"messageformat": "Nobody will be able to see you're on Signal unless you message them or have an existing chat with them.",
|
||||
"description": "Description for the phone number discoverability setting row wth the value is nobody"
|
||||
},
|
||||
"icu:Preferences__pnp__discoverability__everyone": {
|
||||
|
@ -7019,6 +7031,10 @@
|
|||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||
},
|
||||
"icu:WhoCanFindMeReadOnlyToast": {
|
||||
"messageformat": "To change this setting, set “Who can see my number” to “Nobody”.",
|
||||
"description": "A toast displayed when user clicks disabled option in settings window"
|
||||
},
|
||||
"icu:WhatsNew__modal-title": {
|
||||
"messageformat": "What's New",
|
||||
"description": "Title for the whats new modal"
|
||||
|
|
|
@ -23,6 +23,6 @@
|
|||
"buildCreation": 0,
|
||||
"buildExpiration": 0,
|
||||
"certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIIF2zCCA8OgAwIBAgIUAMHz4g60cIDBpPr1gyZ/JDaaPpcwDQYJKoZIhvcNAQEL\nBQAwdTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxHjAcBgNVBAoTFVNpZ25hbCBNZXNzZW5nZXIsIExMQzEZ\nMBcGA1UEAxMQU2lnbmFsIE1lc3NlbmdlcjAeFw0yMjAxMjYwMDQ1NTFaFw0zMjAx\nMjQwMDQ1NTBaMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw\nFAYDVQQHEw1Nb3VudGFpbiBWaWV3MR4wHAYDVQQKExVTaWduYWwgTWVzc2VuZ2Vy\nLCBMTEMxGTAXBgNVBAMTEFNpZ25hbCBNZXNzZW5nZXIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDEecifxMHHlDhxbERVdErOhGsLO08PUdNkATjZ1kT5\n1uPf5JPiRbus9F4J/GgBQ4ANSAjIDZuFY0WOvG/i0qvxthpW70ocp8IjkiWTNiA8\n1zQNQdCiWbGDU4B1sLi2o4JgJMweSkQFiyDynqWgHpw+KmvytCzRWnvrrptIfE4G\nPxNOsAtXFbVH++8JO42IaKRVlbfpe/lUHbjiYmIpQroZPGPY4Oql8KM3o39ObPnT\no1WoM4moyOOZpU3lV1awftvWBx1sbTBL02sQWfHRxgNVF+Pj0fdDMMFdFJobArrL\nVfK2Ua+dYN4pV5XIxzVarSRW73CXqQ+2qloPW/ynpa3gRtYeGWV4jl7eD0PmeHpK\nOY78idP4H1jfAv0TAVeKpuB5ZFZ2szcySxrQa8d7FIf0kNJe9gIRjbQ+XrvnN+ZZ\nvj6d+8uBJq8LfQaFhlVfI0/aIdggScapR7w8oLpvdflUWqcTLeXVNLVrg15cEDwd\nlV8PVscT/KT0bfNzKI80qBq8LyRmauAqP0CDjayYGb2UAabnhefgmRY6aBE5mXxd\nbyAEzzCS3vDxjeTD8v8nbDq+SD6lJi0i7jgwEfNDhe9XK50baK15Udc8Cr/ZlhGM\njNmWqBd0jIpaZm1rzWA0k4VwXtDwpBXSz8oBFshiXs3FD6jHY2IhOR3ppbyd4qRU\npwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUtfNLxuXWS9DlgGuMUMNnW7yx83EwHwYDVR0jBBgwFoAUtfNLxuXWS9Dl\ngGuMUMNnW7yx83EwDQYJKoZIhvcNAQELBQADggIBABUeiryS0qjykBN75aoHO9bV\nPrrX+DSJIB9V2YzkFVyh/io65QJMG8naWVGOSpVRwUwhZVKh3JVp/miPgzTGAo7z\nhrDIoXc+ih7orAMb19qol/2Ha8OZLa75LojJNRbZoCR5C+gM8C+spMLjFf9k3JVx\ndajhtRUcR0zYhwsBS7qZ5Me0d6gRXD0ZiSbadMMxSw6KfKk3ePmPb9gX+MRTS63c\n8mLzVYB/3fe/bkpq4RUwzUHvoZf+SUD7NzSQRQQMfvAHlxk11TVNxScYPtxXDyiy\n3Cssl9gWrrWqQ/omuHipoH62J7h8KAYbr6oEIq+Czuenc3eCIBGBBfvCpuFOgckA\nXXE4MlBasEU0MO66GrTCgMt9bAmSw3TrRP12+ZUFxYNtqWluRU8JWQ4FCCPcz9pg\nMRBOgn4lTxDZG+I47OKNuSRjFEP94cdgxd3H/5BK7WHUz1tAGQ4BgepSXgmjzifF\nT5FVTDTl3ZnWUVBXiHYtbOBgLiSIkbqGMCLtrBtFIeQ7RRTb3L+IE9R0UB0cJB3A\nXbf1lVkOcmrdu2h8A32aCwtr5S1fBF1unlG7imPmqJfpOMWa8yIF/KWVm29JAPq8\nLrsybb0z5gg8w7ZblEuB9zOW9M3l60DXuJO6l7g+deV6P96rv2unHS8UlvWiVWDy\n9qfgAJizyy3kqM4lOwBH\n-----END CERTIFICATE-----\n",
|
||||
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj",
|
||||
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=",
|
||||
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"sfuUrl": "https://sfu.voip.signal.org/",
|
||||
"challengeUrl": "https://signalcaptchas.org/challenge/generate.html",
|
||||
"registrationChallengeUrl": "https://signalcaptchas.org/registration/generate.html",
|
||||
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P",
|
||||
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=",
|
||||
"serverTrustRoot": "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF",
|
||||
"updatesEnabled": true
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
"@react-aria/utils": "3.16.0",
|
||||
"@react-spring/web": "9.5.5",
|
||||
"@signalapp/better-sqlite3": "8.6.0",
|
||||
"@signalapp/libsignal-client": "0.36.0",
|
||||
"@signalapp/libsignal-client": "0.39.1",
|
||||
"@signalapp/ringrtc": "2.36.0",
|
||||
"@signalapp/windows-dummy-keystroke": "1.0.0",
|
||||
"@types/fabric": "4.5.3",
|
||||
|
@ -199,7 +199,7 @@
|
|||
"@electron/notarize": "2.1.0",
|
||||
"@formatjs/intl": "2.6.7",
|
||||
"@mixer/parallel-prettier": "2.0.3",
|
||||
"@signalapp/mock-server": "4.4.1",
|
||||
"@signalapp/mock-server": "4.5.0",
|
||||
"@storybook/addon-a11y": "7.4.5",
|
||||
"@storybook/addon-actions": "7.4.5",
|
||||
"@storybook/addon-controls": "7.4.5",
|
||||
|
|
41
stylesheets/components/AutoSizeInput.scss
Normal file
41
stylesheets/components/AutoSizeInput.scss
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.AutoSizeInput {
|
||||
&__input {
|
||||
@include font-body-1;
|
||||
|
||||
background: inherit;
|
||||
border: none;
|
||||
resize: none;
|
||||
padding: 0;
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
|
||||
&:placeholder {
|
||||
color: $color-gray-45;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&--sizer {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
|
||||
z-index: $z-index-negative;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,8 +63,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__discriminator {
|
||||
user-select: none;
|
||||
&__discriminator__input {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
&__error {
|
||||
|
|
|
@ -297,6 +297,10 @@
|
|||
&:last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&--readonly {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
@import './components/AnnouncementsOnlyGroupBanner.scss';
|
||||
@import './components/App.scss';
|
||||
@import './components/AudioCapture.scss';
|
||||
@import './components/AutoSizeInput.scss';
|
||||
@import './components/Avatar.scss';
|
||||
@import './components/AvatarEditor.scss';
|
||||
@import './components/AvatarModalButtons.scss';
|
||||
|
|
33
ts/components/AutoSizeInput.stories.tsx
Normal file
33
ts/components/AutoSizeInput.stories.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './AutoSizeInput';
|
||||
import { AutoSizeInput } from './AutoSizeInput';
|
||||
|
||||
export default {
|
||||
title: 'Components/AutoSizeInput',
|
||||
argTypes: {},
|
||||
args: {},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
disabled: Boolean(overrideProps.disabled),
|
||||
disableSpellcheck: overrideProps.disableSpellcheck,
|
||||
onChange: action('onChange'),
|
||||
placeholder: overrideProps.placeholder ?? 'Enter some text here',
|
||||
value: overrideProps.value ?? '',
|
||||
});
|
||||
|
||||
function Controller(props: PropsType): JSX.Element {
|
||||
const { value: initialValue } = props;
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
return <AutoSizeInput {...props} onChange={setValue} value={value} />;
|
||||
}
|
||||
|
||||
export function Simple(): JSX.Element {
|
||||
return <Controller {...createProps()} />;
|
||||
}
|
99
ts/components/AutoSizeInput.tsx
Normal file
99
ts/components/AutoSizeInput.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
disableSpellcheck?: boolean;
|
||||
disabled?: boolean;
|
||||
moduleClassName?: string;
|
||||
onChange: (newValue: string) => void;
|
||||
onEnter?: () => void;
|
||||
placeholder: string;
|
||||
value?: string;
|
||||
maxLength?: number;
|
||||
}>;
|
||||
|
||||
export function AutoSizeInput({
|
||||
disableSpellcheck,
|
||||
disabled,
|
||||
moduleClassName,
|
||||
onChange,
|
||||
onEnter,
|
||||
placeholder,
|
||||
value = '',
|
||||
maxLength,
|
||||
}: PropsType): JSX.Element {
|
||||
const [root, setRoot] = useState<HTMLElement | null>(null);
|
||||
const hiddenRef = useRef<HTMLSpanElement | null>(null);
|
||||
|
||||
const [width, setWidth] = useState<undefined | number>(undefined);
|
||||
const getClassName = getClassNamesFor('AutoSizeInput', moduleClassName);
|
||||
|
||||
const handleChange = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
event => {
|
||||
if (onEnter && event.key === 'Enter') {
|
||||
onEnter();
|
||||
}
|
||||
},
|
||||
[onEnter]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const elem = document.createElement('div');
|
||||
document.body.appendChild(elem);
|
||||
|
||||
setRoot(elem);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(elem);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setWidth(hiddenRef.current?.clientWidth || undefined);
|
||||
}, [value, root]);
|
||||
|
||||
return (
|
||||
<div className={getClassName('__container')}>
|
||||
<input
|
||||
type="text"
|
||||
className={getClassName('__input')}
|
||||
dir="auto"
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
spellCheck={!disableSpellcheck}
|
||||
style={{ width }}
|
||||
/>
|
||||
|
||||
{root &&
|
||||
createPortal(
|
||||
<span
|
||||
ref={hiddenRef}
|
||||
className={classNames(
|
||||
getClassName('__input'),
|
||||
getClassName('__input--sizer')
|
||||
)}
|
||||
>
|
||||
{value || placeholder}
|
||||
</span>,
|
||||
root
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -66,6 +66,7 @@ export default {
|
|||
i18n,
|
||||
onClose: action('onClose'),
|
||||
setUsernameReservationError: action('setUsernameReservationError'),
|
||||
clearUsernameReservation: action('clearUsernameReservation'),
|
||||
reserveUsername: action('reserveUsername'),
|
||||
confirmUsername: action('confirmUsername'),
|
||||
},
|
||||
|
|
|
@ -7,12 +7,14 @@ import classNames from 'classnames';
|
|||
import type { LocalizerType } from '../types/Util';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { getNickname, getDiscriminator } from '../types/Username';
|
||||
import { getNickname, getDiscriminator, isCaseChange } from '../types/Username';
|
||||
import {
|
||||
UsernameReservationState,
|
||||
UsernameReservationError,
|
||||
} from '../state/ducks/usernameEnums';
|
||||
import type { ReserveUsernameOptionsType } from '../state/ducks/username';
|
||||
|
||||
import { AutoSizeInput } from './AutoSizeInput';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { Input } from './Input';
|
||||
import { Spinner } from './Spinner';
|
||||
|
@ -33,7 +35,8 @@ export type ActionPropsDataType = Readonly<{
|
|||
setUsernameReservationError(
|
||||
error: UsernameReservationError | undefined
|
||||
): void;
|
||||
reserveUsername(nickname: string | undefined): void;
|
||||
clearUsernameReservation(): void;
|
||||
reserveUsername(optiona: ReserveUsernameOptionsType): void;
|
||||
confirmUsername(): void;
|
||||
}>;
|
||||
|
||||
|
@ -45,6 +48,14 @@ export type PropsType = PropsDataType &
|
|||
ActionPropsDataType &
|
||||
ExternalPropsDataType;
|
||||
|
||||
enum UpdateState {
|
||||
Original = 'Original',
|
||||
Nickname = 'Nickname',
|
||||
Discriminator = 'Discriminator',
|
||||
}
|
||||
|
||||
const DISCRIMINATOR_MAX_LENGTH = 19;
|
||||
|
||||
export function EditUsernameModalBody({
|
||||
i18n,
|
||||
currentUsername,
|
||||
|
@ -54,6 +65,7 @@ export function EditUsernameModalBody({
|
|||
maxNickname,
|
||||
reservation,
|
||||
setUsernameReservationError,
|
||||
clearUsernameReservation,
|
||||
error,
|
||||
state,
|
||||
onClose,
|
||||
|
@ -66,37 +78,76 @@ export function EditUsernameModalBody({
|
|||
return getNickname(currentUsername);
|
||||
}, [currentUsername]);
|
||||
|
||||
const isReserving = state === UsernameReservationState.Reserving;
|
||||
const isConfirming = state === UsernameReservationState.Confirming;
|
||||
const canSave = !isReserving && !isConfirming && reservation !== undefined;
|
||||
const currentDiscriminator =
|
||||
currentUsername === undefined
|
||||
? undefined
|
||||
: getDiscriminator(currentUsername);
|
||||
|
||||
const [hasEverChanged, setHasEverChanged] = useState(false);
|
||||
const [updateState, setUpdateState] = useState(UpdateState.Original);
|
||||
const [nickname, setNickname] = useState(currentNickname);
|
||||
const [isLearnMoreVisible, setIsLearnMoreVisible] = useState(false);
|
||||
const [isConfirmingSave, setIsConfirmingSave] = useState(false);
|
||||
|
||||
const [customDiscriminator, setCustomDiscriminator] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const discriminator = useMemo(() => {
|
||||
// Always give preference to user-selected custom discriminator.
|
||||
if (
|
||||
customDiscriminator !== undefined ||
|
||||
updateState === UpdateState.Discriminator
|
||||
) {
|
||||
return customDiscriminator;
|
||||
}
|
||||
|
||||
if (reservation !== undefined) {
|
||||
// New discriminator from reservation
|
||||
return getDiscriminator(reservation.username);
|
||||
}
|
||||
|
||||
return currentDiscriminator;
|
||||
}, [reservation, updateState, currentDiscriminator, customDiscriminator]);
|
||||
|
||||
// Disallow non-numeric discriminator
|
||||
const updateCustomDiscriminator = useCallback((newValue: string): void => {
|
||||
const digits = newValue.replace(/[^\d]+/g, '');
|
||||
setUpdateState(UpdateState.Discriminator);
|
||||
setCustomDiscriminator(digits);
|
||||
}, []);
|
||||
|
||||
// When we change nickname with previously erased discriminator - reset the
|
||||
// discriminator state.
|
||||
useEffect(() => {
|
||||
if (customDiscriminator !== '' || !reservation) {
|
||||
return;
|
||||
}
|
||||
setCustomDiscriminator(undefined);
|
||||
}, [customDiscriminator, reservation]);
|
||||
|
||||
// Clear reservation if user erases the nickname
|
||||
useEffect(() => {
|
||||
if (updateState === UpdateState.Nickname && !nickname) {
|
||||
clearUsernameReservation();
|
||||
}
|
||||
}, [clearUsernameReservation, nickname, updateState]);
|
||||
|
||||
const isReserving = state === UsernameReservationState.Reserving;
|
||||
const isConfirming = state === UsernameReservationState.Confirming;
|
||||
const canSave =
|
||||
!isReserving &&
|
||||
!isConfirming &&
|
||||
(reservation !== undefined || customDiscriminator);
|
||||
const isDiscriminatorVisible =
|
||||
Boolean(nickname || customDiscriminator) &&
|
||||
(discriminator || updateState === UpdateState.Discriminator);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === UsernameReservationState.Closed) {
|
||||
onClose();
|
||||
}
|
||||
}, [state, onClose]);
|
||||
|
||||
const discriminator = useMemo(() => {
|
||||
if (reservation !== undefined) {
|
||||
// New discriminator
|
||||
return getDiscriminator(reservation.username);
|
||||
}
|
||||
|
||||
// User never changed the nickname - return discriminator from the current
|
||||
// username.
|
||||
if (!hasEverChanged && currentUsername) {
|
||||
return getDiscriminator(currentUsername);
|
||||
}
|
||||
|
||||
// No reservation, different nickname - no discriminator
|
||||
return undefined;
|
||||
}, [reservation, hasEverChanged, currentUsername]);
|
||||
|
||||
const errorString = useMemo(() => {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
|
@ -120,6 +171,17 @@ export function EditUsernameModalBody({
|
|||
if (error === UsernameReservationError.UsernameNotAvailable) {
|
||||
return i18n('icu:ProfileEditor--username--unavailable');
|
||||
}
|
||||
if (error === UsernameReservationError.NotEnoughDiscriminator) {
|
||||
return i18n('icu:ProfileEditor--username--check-discriminator-min');
|
||||
}
|
||||
if (error === UsernameReservationError.AllZeroDiscriminator) {
|
||||
return i18n('icu:ProfileEditor--username--check-discriminator-all-zero');
|
||||
}
|
||||
if (error === UsernameReservationError.LeadingZeroDiscriminator) {
|
||||
return i18n(
|
||||
'icu:ProfileEditor--username--check-discriminator-leading-zero'
|
||||
);
|
||||
}
|
||||
// Displayed through confirmation modal below
|
||||
if (
|
||||
error === UsernameReservationError.General ||
|
||||
|
@ -132,25 +194,45 @@ export function EditUsernameModalBody({
|
|||
|
||||
useEffect(() => {
|
||||
// Initial effect run
|
||||
if (!hasEverChanged) {
|
||||
if (updateState === UpdateState.Original) {
|
||||
return;
|
||||
}
|
||||
|
||||
reserveUsername(nickname);
|
||||
}, [hasEverChanged, nickname, reserveUsername]);
|
||||
// Sanity-check, we should never get here.
|
||||
if (!nickname) {
|
||||
return;
|
||||
}
|
||||
|
||||
// User just erased discriminator
|
||||
if (updateState === UpdateState.Discriminator && !customDiscriminator) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConfirming) {
|
||||
return;
|
||||
}
|
||||
|
||||
reserveUsername({ nickname, customDiscriminator });
|
||||
}, [
|
||||
updateState,
|
||||
nickname,
|
||||
reserveUsername,
|
||||
isConfirming,
|
||||
customDiscriminator,
|
||||
]);
|
||||
|
||||
const onChange = useCallback((newNickname: string) => {
|
||||
setHasEverChanged(true);
|
||||
setUpdateState(UpdateState.Nickname);
|
||||
setNickname(newNickname);
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
if (!currentUsername) {
|
||||
if (!currentUsername || (reservation && isCaseChange(reservation))) {
|
||||
confirmUsername();
|
||||
} else {
|
||||
setIsConfirmingSave(true);
|
||||
}
|
||||
}, [confirmUsername, currentUsername]);
|
||||
}, [confirmUsername, currentUsername, reservation]);
|
||||
|
||||
const onCancelSave = useCallback(() => {
|
||||
setIsConfirmingSave(false);
|
||||
|
@ -172,7 +254,7 @@ export function EditUsernameModalBody({
|
|||
|
||||
let title = i18n('icu:ProfileEditor--username--title');
|
||||
if (nickname && discriminator) {
|
||||
title = `${nickname}${discriminator}`;
|
||||
title = `${nickname}.${discriminator}`;
|
||||
}
|
||||
|
||||
const learnMoreTitle = (
|
||||
|
@ -201,14 +283,20 @@ export function EditUsernameModalBody({
|
|||
value={nickname}
|
||||
>
|
||||
{isReserving && <Spinner size="16px" svgSize="small" />}
|
||||
{discriminator && (
|
||||
{isDiscriminatorVisible ? (
|
||||
<>
|
||||
<div className="EditUsernameModalBody__divider" />
|
||||
<div className="EditUsernameModalBody__discriminator">
|
||||
{discriminator}
|
||||
</div>
|
||||
<AutoSizeInput
|
||||
moduleClassName="EditUsernameModalBody__discriminator"
|
||||
disableSpellcheck
|
||||
disabled={isConfirming}
|
||||
value={discriminator}
|
||||
onChange={updateCustomDiscriminator}
|
||||
placeholder="00"
|
||||
maxLength={DISCRIMINATOR_MAX_LENGTH}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</Input>
|
||||
|
||||
{errorString && (
|
||||
|
@ -289,7 +377,7 @@ export function EditUsernameModalBody({
|
|||
i18n={i18n}
|
||||
onClose={() => {
|
||||
if (nickname) {
|
||||
reserveUsername(nickname);
|
||||
reserveUsername({ nickname, customDiscriminator });
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -22,6 +22,8 @@ import type {
|
|||
ZoomFactorType,
|
||||
} from '../types/Storage.d';
|
||||
import type { ThemeSettingType } from '../types/StorageUIKeys';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type {
|
||||
ConversationColorType,
|
||||
|
@ -47,7 +49,9 @@ import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
|||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||
import { Select } from './Select';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ToastManager } from './ToastManager';
|
||||
import { getCustomColorStyle } from '../util/getCustomColorStyle';
|
||||
import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled';
|
||||
import {
|
||||
DEFAULT_DURATIONS_IN_SECONDS,
|
||||
DEFAULT_DURATIONS_SET,
|
||||
|
@ -357,6 +361,7 @@ export function Preferences({
|
|||
string | null
|
||||
>(localeOverride);
|
||||
const [languageSearchInput, setLanguageSearchInput] = useState('');
|
||||
const [toast, setToast] = useState<AnyToast | undefined>();
|
||||
|
||||
function closeLanguageDialog() {
|
||||
setLanguageDialog(null);
|
||||
|
@ -1464,16 +1469,16 @@ export function Preferences({
|
|||
text: i18n('icu:Preferences__pnp__discoverability__everyone'),
|
||||
value: PhoneNumberDiscoverability.Discoverable,
|
||||
},
|
||||
...(whoCanSeeMe === PhoneNumberSharingMode.Nobody
|
||||
? [
|
||||
{
|
||||
text: i18n(
|
||||
'icu:Preferences__pnp__discoverability__nobody'
|
||||
),
|
||||
value: PhoneNumberDiscoverability.NotDiscoverable,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: i18n('icu:Preferences__pnp__discoverability__nobody'),
|
||||
value: PhoneNumberDiscoverability.NotDiscoverable,
|
||||
readOnly: whoCanSeeMe === PhoneNumberSharingMode.Everybody,
|
||||
onClick:
|
||||
whoCanSeeMe === PhoneNumberSharingMode.Everybody
|
||||
? () =>
|
||||
setToast({ toastType: ToastType.WhoCanFindMeReadOnly })
|
||||
: noop,
|
||||
},
|
||||
]}
|
||||
value={whoCanFindMe}
|
||||
/>
|
||||
|
@ -1572,6 +1577,14 @@ export function Preferences({
|
|||
{settings}
|
||||
</div>
|
||||
</div>
|
||||
<ToastManager
|
||||
OS="unused"
|
||||
hideToast={() => setToast(undefined)}
|
||||
i18n={i18n}
|
||||
onUndoArchive={shouldNeverBeCalled}
|
||||
openFileInFolder={shouldNeverBeCalled}
|
||||
toast={toast}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1638,6 +1651,8 @@ function Control({
|
|||
type SettingsRadioOptionType<Enum> = Readonly<{
|
||||
text: string;
|
||||
value: Enum;
|
||||
readOnly?: boolean;
|
||||
onClick?: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsRadio<Enum>({
|
||||
|
@ -1655,11 +1670,13 @@ function SettingsRadio<Enum>({
|
|||
|
||||
return (
|
||||
<div className="Preferences__padding">
|
||||
{options.map(({ text, value: optionValue }, i) => {
|
||||
{options.map(({ text, value: optionValue, readOnly, onClick }, i) => {
|
||||
const htmlId = htmlIds[i];
|
||||
return (
|
||||
<label
|
||||
className="Preferences__settings-radio__label"
|
||||
className={classNames('Preferences__settings-radio__label', {
|
||||
'Preferences__settings-radio__label--readonly': readOnly,
|
||||
})}
|
||||
key={htmlId}
|
||||
htmlFor={htmlId}
|
||||
>
|
||||
|
@ -1668,7 +1685,8 @@ function SettingsRadio<Enum>({
|
|||
variant={CircleCheckboxVariant.Small}
|
||||
id={htmlId}
|
||||
checked={value === optionValue}
|
||||
onChange={() => onChange(optionValue)}
|
||||
onClick={onClick}
|
||||
onChange={readOnly ? noop : () => onChange(optionValue)}
|
||||
/>
|
||||
{text}
|
||||
</label>
|
||||
|
|
|
@ -100,6 +100,7 @@ function renderEditUsernameModalBody(props: {
|
|||
state={UsernameReservationState.Open}
|
||||
error={undefined}
|
||||
setUsernameReservationError={action('setUsernameReservationError')}
|
||||
clearUsernameReservation={action('clearUsernameReservation')}
|
||||
reserveUsername={action('reserveUsername')}
|
||||
confirmUsername={action('confirmUsername')}
|
||||
{...props}
|
||||
|
|
|
@ -130,6 +130,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
group: 'Hike Group 🏔',
|
||||
},
|
||||
};
|
||||
case ToastType.WhoCanFindMeReadOnly:
|
||||
return { toastType: ToastType.WhoCanFindMeReadOnly };
|
||||
default:
|
||||
throw missingCaseError(toastType);
|
||||
}
|
||||
|
|
|
@ -392,5 +392,11 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.WhoCanFindMeReadOnly) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:WhoCanFindMeReadOnlyToast')}</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
throw missingCaseError(toastType);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,13 @@ import { sleep } from '../util/sleep';
|
|||
import { getMinNickname, getMaxNickname } from '../util/Username';
|
||||
import { bytesToUuid, uuidToBytes } from '../util/uuidToBytes';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
import { ReserveUsernameError, ConfirmUsernameResult } from '../types/Username';
|
||||
import {
|
||||
ReserveUsernameError,
|
||||
ConfirmUsernameResult,
|
||||
getNickname,
|
||||
getDiscriminator,
|
||||
isCaseChange,
|
||||
} from '../types/Username';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
|
@ -35,6 +41,7 @@ export type WriteUsernameOptionsType = Readonly<
|
|||
|
||||
export type ReserveUsernameOptionsType = Readonly<{
|
||||
nickname: string;
|
||||
customDiscriminator: string | undefined;
|
||||
previousUsername: string | undefined;
|
||||
abortSignal?: AbortSignal;
|
||||
}>;
|
||||
|
@ -60,7 +67,8 @@ export async function reserveUsername(
|
|||
throw new Error('server interface is not available!');
|
||||
}
|
||||
|
||||
const { nickname, previousUsername, abortSignal } = options;
|
||||
const { nickname, customDiscriminator, previousUsername, abortSignal } =
|
||||
options;
|
||||
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
|
||||
|
@ -69,11 +77,39 @@ export async function reserveUsername(
|
|||
}
|
||||
|
||||
try {
|
||||
const candidates = usernames.generateCandidates(
|
||||
nickname,
|
||||
getMinNickname(),
|
||||
getMaxNickname()
|
||||
);
|
||||
if (previousUsername !== undefined && !customDiscriminator) {
|
||||
const previousNickname = getNickname(previousUsername);
|
||||
|
||||
// Case change
|
||||
if (
|
||||
previousNickname !== undefined &&
|
||||
nickname.toLowerCase() === previousNickname.toLowerCase()
|
||||
) {
|
||||
const previousDiscriminator = getDiscriminator(previousUsername);
|
||||
const newUsername = `${nickname}.${previousDiscriminator}`;
|
||||
const hash = usernames.hash(newUsername);
|
||||
return {
|
||||
ok: true,
|
||||
reservation: { previousUsername, username: newUsername, hash },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = customDiscriminator
|
||||
? [
|
||||
usernames.fromParts(
|
||||
nickname,
|
||||
customDiscriminator,
|
||||
getMinNickname(),
|
||||
getMaxNickname()
|
||||
).username,
|
||||
]
|
||||
: usernames.generateCandidates(
|
||||
nickname,
|
||||
getMinNickname(),
|
||||
getMaxNickname()
|
||||
);
|
||||
|
||||
const hashes = candidates.map(username => usernames.hash(username));
|
||||
|
||||
const { usernameHash } = await server.reserveUsername({
|
||||
|
@ -111,7 +147,7 @@ export async function reserveUsername(
|
|||
}
|
||||
if (error instanceof LibSignalErrorBase) {
|
||||
if (
|
||||
error.code === ErrorCode.CannotBeEmpty ||
|
||||
error.code === ErrorCode.NicknameCannotBeEmpty ||
|
||||
error.code === ErrorCode.NicknameTooShort
|
||||
) {
|
||||
return {
|
||||
|
@ -137,6 +173,32 @@ export async function reserveUsername(
|
|||
error: ReserveUsernameError.CheckCharacters,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === ErrorCode.DiscriminatorCannotBeZero) {
|
||||
return {
|
||||
ok: false,
|
||||
error: ReserveUsernameError.AllZeroDiscriminator,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === ErrorCode.DiscriminatorCannotHaveLeadingZeros) {
|
||||
return {
|
||||
ok: false,
|
||||
error: ReserveUsernameError.LeadingZeroDiscriminator,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
error.code === ErrorCode.DiscriminatorCannotBeEmpty ||
|
||||
error.code === ErrorCode.DiscriminatorCannotBeSingleDigit ||
|
||||
// This is handled on UI level
|
||||
error.code === ErrorCode.DiscriminatorTooLarge
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: ReserveUsernameError.NotEnoughDiscriminator,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
@ -171,32 +233,54 @@ export async function confirmUsername(
|
|||
throw new Error('server interface is not available!');
|
||||
}
|
||||
|
||||
const { previousUsername, username, hash } = reservation;
|
||||
const { previousUsername, username } = reservation;
|
||||
const previousLink = window.storage.get('usernameLink');
|
||||
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
|
||||
if (me.get('username') !== previousUsername) {
|
||||
throw new Error('Username has changed on another device');
|
||||
}
|
||||
const proof = usernames.generateProof(username);
|
||||
|
||||
const { hash } = reservation;
|
||||
strictAssert(usernames.hash(username).equals(hash), 'username hash mismatch');
|
||||
|
||||
try {
|
||||
const { entropy, encryptedUsername } =
|
||||
usernames.createUsernameLink(username);
|
||||
|
||||
await window.storage.remove('usernameLink');
|
||||
await window.storage.remove('usernameCorrupted');
|
||||
await window.storage.remove('usernameLinkCorrupted');
|
||||
|
||||
const { usernameLinkHandle: serverIdString } = await server.confirmUsername(
|
||||
{
|
||||
let serverIdString: string;
|
||||
let entropy: Buffer;
|
||||
if (previousLink && isCaseChange(reservation)) {
|
||||
log.info('confirmUsername: updating link only');
|
||||
|
||||
const updatedLink = usernames.createUsernameLink(
|
||||
username,
|
||||
Buffer.from(previousLink.entropy)
|
||||
);
|
||||
({ entropy } = updatedLink);
|
||||
|
||||
({ usernameLinkHandle: serverIdString } =
|
||||
await server.replaceUsernameLink({
|
||||
encryptedUsername: updatedLink.encryptedUsername,
|
||||
keepLinkHandle: true,
|
||||
}));
|
||||
} else {
|
||||
log.info('confirmUsername: confirming and replacing link');
|
||||
|
||||
const newLink = usernames.createUsernameLink(username);
|
||||
({ entropy } = newLink);
|
||||
|
||||
const proof = usernames.generateProof(username);
|
||||
|
||||
({ usernameLinkHandle: serverIdString } = await server.confirmUsername({
|
||||
hash,
|
||||
proof,
|
||||
encryptedUsername,
|
||||
encryptedUsername: newLink.encryptedUsername,
|
||||
abortSignal,
|
||||
}
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
await window.storage.put('usernameLink', {
|
||||
entropy,
|
||||
|
@ -263,7 +347,10 @@ export async function resetLink(username: string): Promise<void> {
|
|||
await window.storage.remove('usernameLinkCorrupted');
|
||||
|
||||
const { usernameLinkHandle: serverIdString } =
|
||||
await server.replaceUsernameLink({ encryptedUsername });
|
||||
await server.replaceUsernameLink({
|
||||
encryptedUsername,
|
||||
keepLinkHandle: false,
|
||||
});
|
||||
|
||||
await window.storage.put('usernameLink', {
|
||||
entropy,
|
||||
|
|
|
@ -53,6 +53,7 @@ const SET_USERNAME_EDIT_STATE = 'username/SET_USERNAME_EDIT_STATE';
|
|||
const OPEN_USERNAME_RESERVATION_MODAL = 'username/OPEN_RESERVATION_MODAL';
|
||||
const CLOSE_USERNAME_RESERVATION_MODAL = 'username/CLOSE_RESERVATION_MODAL';
|
||||
const SET_USERNAME_RESERVATION_ERROR = 'username/SET_RESERVATION_ERROR';
|
||||
const CLEAR_USERNAME_RESERVATION = 'username/CLEAR_RESERVATION';
|
||||
const RESERVE_USERNAME = 'username/RESERVE_USERNAME';
|
||||
const CONFIRM_USERNAME = 'username/CONFIRM_USERNAME';
|
||||
const DELETE_USERNAME = 'username/DELETE_USERNAME';
|
||||
|
@ -80,6 +81,10 @@ type SetUsernameReservationErrorActionType = ReadonlyDeep<{
|
|||
};
|
||||
}>;
|
||||
|
||||
type ClearUsernameReservation = ReadonlyDeep<{
|
||||
type: typeof CLEAR_USERNAME_RESERVATION;
|
||||
}>;
|
||||
|
||||
type ReserveUsernameActionType = ReadonlyDeep<
|
||||
PromiseAction<
|
||||
typeof RESERVE_USERNAME,
|
||||
|
@ -102,6 +107,7 @@ export type UsernameActionType = ReadonlyDeep<
|
|||
| OpenUsernameReservationModalActionType
|
||||
| CloseUsernameReservationModalActionType
|
||||
| SetUsernameReservationErrorActionType
|
||||
| ClearUsernameReservation
|
||||
| ReserveUsernameActionType
|
||||
| ConfirmUsernameActionType
|
||||
| DeleteUsernameActionType
|
||||
|
@ -113,6 +119,7 @@ export const actions = {
|
|||
openUsernameReservationModal,
|
||||
closeUsernameReservationModal,
|
||||
setUsernameReservationError,
|
||||
clearUsernameReservation,
|
||||
reserveUsername,
|
||||
confirmUsername,
|
||||
deleteUsername,
|
||||
|
@ -152,20 +159,27 @@ export function setUsernameReservationError(
|
|||
};
|
||||
}
|
||||
|
||||
export function clearUsernameReservation(): ClearUsernameReservation {
|
||||
return {
|
||||
type: CLEAR_USERNAME_RESERVATION,
|
||||
};
|
||||
}
|
||||
|
||||
const INPUT_DELAY_MS = 500;
|
||||
|
||||
export type ReserveUsernameOptionsType = ReadonlyDeep<{
|
||||
nickname: string;
|
||||
customDiscriminator?: string;
|
||||
doReserveUsername?: typeof usernameServices.reserveUsername;
|
||||
delay?: number;
|
||||
}>;
|
||||
|
||||
export function reserveUsername(
|
||||
nickname: string,
|
||||
{
|
||||
doReserveUsername = usernameServices.reserveUsername,
|
||||
delay = INPUT_DELAY_MS,
|
||||
}: ReserveUsernameOptionsType = {}
|
||||
): ThunkAction<
|
||||
export function reserveUsername({
|
||||
nickname,
|
||||
customDiscriminator,
|
||||
doReserveUsername = usernameServices.reserveUsername,
|
||||
delay = INPUT_DELAY_MS,
|
||||
}: ReserveUsernameOptionsType): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
|
@ -192,6 +206,7 @@ export function reserveUsername(
|
|||
return doReserveUsername({
|
||||
previousUsername: username,
|
||||
nickname,
|
||||
customDiscriminator,
|
||||
abortSignal,
|
||||
});
|
||||
};
|
||||
|
@ -387,6 +402,17 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === CLEAR_USERNAME_RESERVATION) {
|
||||
return {
|
||||
...state,
|
||||
usernameReservation: {
|
||||
...usernameReservation,
|
||||
error: undefined,
|
||||
reservation: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'username/RESERVE_USERNAME_PENDING') {
|
||||
usernameReservation.abortController?.abort();
|
||||
|
||||
|
@ -394,6 +420,8 @@ export function reducer(
|
|||
return {
|
||||
...state,
|
||||
usernameReservation: {
|
||||
...usernameReservation,
|
||||
error: undefined,
|
||||
state: UsernameReservationState.Reserving,
|
||||
abortController: meta.abortController,
|
||||
},
|
||||
|
@ -433,6 +461,12 @@ export function reducer(
|
|||
stateError = UsernameReservationError.CheckStartingCharacter;
|
||||
} else if (error === ReserveUsernameError.CheckCharacters) {
|
||||
stateError = UsernameReservationError.CheckCharacters;
|
||||
} else if (error === ReserveUsernameError.NotEnoughDiscriminator) {
|
||||
stateError = UsernameReservationError.NotEnoughDiscriminator;
|
||||
} else if (error === ReserveUsernameError.AllZeroDiscriminator) {
|
||||
stateError = UsernameReservationError.AllZeroDiscriminator;
|
||||
} else if (error === ReserveUsernameError.LeadingZeroDiscriminator) {
|
||||
stateError = UsernameReservationError.LeadingZeroDiscriminator;
|
||||
} else {
|
||||
throw missingCaseError(error);
|
||||
}
|
||||
|
|
|
@ -40,4 +40,7 @@ export enum UsernameReservationError {
|
|||
UsernameNotAvailable = 'UsernameNotAvailable',
|
||||
General = 'General',
|
||||
ConflictOrGone = 'ConflictOrGone',
|
||||
NotEnoughDiscriminator = 'NotEnoughDiscriminator',
|
||||
AllZeroDiscriminator = 'AllZeroDiscriminator',
|
||||
LeadingZeroDiscriminator = 'LeadingZeroDiscriminator',
|
||||
}
|
||||
|
|
|
@ -116,7 +116,8 @@ describe('electron/state/ducks/username', () => {
|
|||
const doReserveUsername = sinon.stub().resolves(DEFAULT_RESERVATION);
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
actions.reserveUsername('test', {
|
||||
actions.reserveUsername({
|
||||
nickname: 'test',
|
||||
doReserveUsername,
|
||||
delay: 1000,
|
||||
})(dispatch, () => emptyState, null);
|
||||
|
|
|
@ -165,14 +165,14 @@ describe('pnp/username', function (this: Mocha.Suite) {
|
|||
|
||||
debug('waiting for generated discriminator');
|
||||
const discriminator = profileEditor.locator(
|
||||
'.EditUsernameModalBody__discriminator:not(:empty)'
|
||||
'.EditUsernameModalBody__discriminator__input[value]'
|
||||
);
|
||||
await discriminator.waitFor();
|
||||
|
||||
const discriminatorValue = await discriminator.innerText();
|
||||
assert.match(discriminatorValue, /^\.\d+$/);
|
||||
const discriminatorValue = await discriminator.inputValue();
|
||||
assert.match(discriminatorValue, /^\d+$/);
|
||||
|
||||
const username = `${NICKNAME}${discriminatorValue}`;
|
||||
const username = `${NICKNAME}.${discriminatorValue}`;
|
||||
|
||||
debug('saving username');
|
||||
let state = await phone.expectStorageState('consistency check');
|
||||
|
|
|
@ -828,6 +828,7 @@ export type ReserveUsernameOptionsType = Readonly<{
|
|||
|
||||
export type ReplaceUsernameLinkOptionsType = Readonly<{
|
||||
encryptedUsername: Uint8Array;
|
||||
keepLinkHandle: boolean;
|
||||
}>;
|
||||
|
||||
export type ConfirmUsernameOptionsType = Readonly<{
|
||||
|
@ -2036,6 +2037,7 @@ export function initialize({
|
|||
|
||||
async function replaceUsernameLink({
|
||||
encryptedUsername,
|
||||
keepLinkHandle,
|
||||
}: ReplaceUsernameLinkOptionsType): Promise<ReplaceUsernameLinkResultType> {
|
||||
return replaceUsernameLinkResultZod.parse(
|
||||
await _ajax({
|
||||
|
@ -2046,6 +2048,7 @@ export function initialize({
|
|||
usernameLinkEncryptedValue: toWebSafeBase64(
|
||||
Bytes.toBase64(encryptedUsername)
|
||||
),
|
||||
keepLinkHandle,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -48,6 +48,7 @@ export enum ToastType {
|
|||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||
UnsupportedOS = 'UnsupportedOS',
|
||||
UserAddedToGroup = 'UserAddedToGroup',
|
||||
WhoCanFindMeReadOnly = 'WhoCanFindMeReadOnly',
|
||||
}
|
||||
|
||||
export type AnyToast =
|
||||
|
@ -108,4 +109,5 @@ export type AnyToast =
|
|||
| {
|
||||
toastType: ToastType.UserAddedToGroup;
|
||||
parameters: { contact: string; group: string };
|
||||
};
|
||||
}
|
||||
| { toastType: ToastType.WhoCanFindMeReadOnly };
|
||||
|
|
|
@ -16,6 +16,9 @@ export enum ReserveUsernameError {
|
|||
TooManyCharacters = 'TooManyCharacters',
|
||||
CheckStartingCharacter = 'CheckStartingCharacter',
|
||||
CheckCharacters = 'CheckCharacters',
|
||||
NotEnoughDiscriminator = 'NotEnoughDiscriminator',
|
||||
AllZeroDiscriminator = 'AllZeroDiscriminator',
|
||||
LeadingZeroDiscriminator = 'LeadingZeroDiscriminator',
|
||||
}
|
||||
|
||||
export enum ConfirmUsernameResult {
|
||||
|
@ -41,11 +44,18 @@ export function getNickname(username: string): string | undefined {
|
|||
return match[1];
|
||||
}
|
||||
|
||||
export function getDiscriminator(username: string): string {
|
||||
const match = username.match(/(\..*)$/);
|
||||
export function getDiscriminator(username: string): string | undefined {
|
||||
const match = username.match(/\.([0-9]*)$/);
|
||||
if (!match) {
|
||||
return '';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function isCaseChange({
|
||||
previousUsername,
|
||||
username,
|
||||
}: UsernameReservationType): boolean {
|
||||
return previousUsername?.toLowerCase() === username.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -2765,6 +2765,14 @@
|
|||
"updated": "2021-12-10T23:24:03.829Z",
|
||||
"reasonDetail": "Doesn't touch the DOM."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AutoSizeInput.tsx",
|
||||
"line": " const hiddenRef = useRef<HTMLSpanElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-11T16:58:57.146Z",
|
||||
"reasonDetail": "Needs access to a hidden span element to get its width"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AvatarTextEditor.tsx",
|
||||
|
@ -2801,6 +2809,46 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-07-30T16:57:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const timeouts = useRef<Map<string, NodeJS.Timeout>>(new Map());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For hiding call reaction bursts after timeouts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const burstsShown = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "In wrapping function, track bursts so we can hide on unmount."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const shownBursts = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Keep track of shown reaction bursts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurstEmoji.tsx",
|
||||
"line": " const containerRef = React.useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For determining position of container for animations."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const burstsShown = useRef<Map<string, number>>(new Map());",
|
||||
"reasonCategory": "sageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent bursts shown for burst behavior like throttling."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
|
@ -2846,6 +2894,14 @@
|
|||
"updated": "2023-12-21T11:13:56.623Z",
|
||||
"reasonDetail": "Calling reactions bursts"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const reactionsShown = useRef<",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent reactions shown for reactions burst"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.tsx",
|
||||
|
@ -3449,6 +3505,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-08-10T00:23:35.320Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/CallingNotification.tsx",
|
||||
"line": " const menuTriggerRef = React.useRef<ContextMenuTriggerType | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-12-08T20:28:57.595Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||
|
@ -3534,13 +3597,6 @@
|
|||
"updated": "2021-01-20T21:30:08.430Z",
|
||||
"reasonDetail": "Doesn't touch the DOM."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/CallingNotification.tsx",
|
||||
"line": " const menuTriggerRef = React.useRef<ContextMenuTriggerType | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-12-08T20:28:57.595Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/TimelineMessage.tsx",
|
||||
|
@ -3816,53 +3872,5 @@
|
|||
"line": " message.innerHTML = window.i18n('icu:optimizingApplication');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-17T21:02:59.414Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const timeouts = useRef<Map<string, NodeJS.Timeout>>(new Map());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For hiding call reaction bursts after timeouts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const shownBursts = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Keep track of shown reaction bursts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const burstsShown = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "In wrapping function, track bursts so we can hide on unmount."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurstEmoji.tsx",
|
||||
"line": " const containerRef = React.useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For determining position of container for animations."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const reactionsShown = useRef<",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent reactions shown for reactions burst"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const burstsShown = useRef<Map<string, number>>(new Map());",
|
||||
"reasonCategory": "sageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent bursts shown for burst behavior like throttling."
|
||||
}
|
||||
]
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -3945,29 +3945,21 @@
|
|||
bindings "^1.5.0"
|
||||
tar "^6.1.0"
|
||||
|
||||
"@signalapp/libsignal-client@0.36.0":
|
||||
version "0.36.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.36.0.tgz#9deb971b9ae3894e24f4413ada66f09f2b325ac4"
|
||||
integrity sha512-EXwPDks7DSwtt4aGtiAwcU8UYGEeZfMWpCzayVbSkQ8h/QLn3oxYeiWV5maVNJh40kdv05rdIGgamZEBC7yBTA==
|
||||
"@signalapp/libsignal-client@0.39.1", "@signalapp/libsignal-client@^0.39.1":
|
||||
version "0.39.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.39.1.tgz#15b41f15c516ae3eecf8a098a9c9c7aac00444d7"
|
||||
integrity sha512-Drna/0rQTa/jB475KssoBA86Da/DLdJYDznkbiFG2YD/OeWEKoDpi64bp+BIpnc2o16GnVhGLFzNvMfVkI41eQ==
|
||||
dependencies:
|
||||
node-gyp-build "^4.2.3"
|
||||
type-fest "^3.5.0"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/libsignal-client@^0.30.2":
|
||||
version "0.30.2"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.30.2.tgz#0a5162271c6c03eaa900036aa9dc34a0e2e5e6ae"
|
||||
integrity sha512-1ND5nyRUDHDLCPuX09SXAXMn96sk8oSsI/wUO2E+FzBhEHZPNARzGx1pBnRK7Y6bGDg1arUnsp60nCFAZyB+iw==
|
||||
"@signalapp/mock-server@4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.5.0.tgz#532afb96a916dea17e39d5112bbc58b0135ccbe2"
|
||||
integrity sha512-kcZHfipopBV6UtbEwI7O96zE0au+BLf8qDHAzCefFG28Wy7c4nsia3Me0tKBrguvgo3ySq70iuqTGrqsyzR/5g==
|
||||
dependencies:
|
||||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/mock-server@4.4.1":
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.4.1.tgz#8e7dd69ae69bdf60f4f1f8609066ffc9163f16a6"
|
||||
integrity sha512-aexMLAjQiEhcxLr+XhU7Rsrohgn3xMU1SdIbM1UPUIrpGQqLFfA+X+E0huQqBED6k3KgArCQ6zmy4uJ65acHVQ==
|
||||
dependencies:
|
||||
"@signalapp/libsignal-client" "^0.30.2"
|
||||
"@signalapp/libsignal-client" "^0.39.1"
|
||||
debug "^4.3.2"
|
||||
long "^4.0.0"
|
||||
micro "^9.3.4"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue