diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0e5e2d929..69006a042 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/config/default.json b/config/default.json index 59fb06ce2..bd90d5488 100644 --- a/config/default.json +++ b/config/default.json @@ -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" } diff --git a/config/production.json b/config/production.json index 5f931c6e2..226f8dbdf 100644 --- a/config/production.json +++ b/config/production.json @@ -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 } diff --git a/package.json b/package.json index c2497f4cc..aa6f2be77 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/stylesheets/components/AutoSizeInput.scss b/stylesheets/components/AutoSizeInput.scss new file mode 100644 index 000000000..3806b313d --- /dev/null +++ b/stylesheets/components/AutoSizeInput.scss @@ -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; + } + } +} diff --git a/stylesheets/components/EditUsernameModalBody.scss b/stylesheets/components/EditUsernameModalBody.scss index 08c8e0c42..7345416a6 100644 --- a/stylesheets/components/EditUsernameModalBody.scss +++ b/stylesheets/components/EditUsernameModalBody.scss @@ -63,8 +63,8 @@ } } - &__discriminator { - user-select: none; + &__discriminator__input { + text-align: end; } &__error { diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss index ffef5d8af..8d5495052 100644 --- a/stylesheets/components/Preferences.scss +++ b/stylesheets/components/Preferences.scss @@ -297,6 +297,10 @@ &:last-child { margin-bottom: 8px; } + + &--readonly { + opacity: 0.4; + } } } diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 8c1c586e6..59e17ea4f 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/components/AutoSizeInput.stories.tsx b/ts/components/AutoSizeInput.stories.tsx new file mode 100644 index 000000000..94e51a041 --- /dev/null +++ b/ts/components/AutoSizeInput.stories.tsx @@ -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; + +const createProps = (overrideProps: Partial = {}): 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 ; +} + +export function Simple(): JSX.Element { + return ; +} diff --git a/ts/components/AutoSizeInput.tsx b/ts/components/AutoSizeInput.tsx new file mode 100644 index 000000000..d2cd0722c --- /dev/null +++ b/ts/components/AutoSizeInput.tsx @@ -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(null); + const hiddenRef = useRef(null); + + const [width, setWidth] = useState(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 ( +
+ + + {root && + createPortal( + + {value || placeholder} + , + root + )} +
+ ); +} diff --git a/ts/components/EditUsernameModalBody.stories.tsx b/ts/components/EditUsernameModalBody.stories.tsx index 92a9db488..28f024882 100644 --- a/ts/components/EditUsernameModalBody.stories.tsx +++ b/ts/components/EditUsernameModalBody.stories.tsx @@ -66,6 +66,7 @@ export default { i18n, onClose: action('onClose'), setUsernameReservationError: action('setUsernameReservationError'), + clearUsernameReservation: action('clearUsernameReservation'), reserveUsername: action('reserveUsername'), confirmUsername: action('confirmUsername'), }, diff --git a/ts/components/EditUsernameModalBody.tsx b/ts/components/EditUsernameModalBody.tsx index 8f4d86805..efdb658af 100644 --- a/ts/components/EditUsernameModalBody.tsx +++ b/ts/components/EditUsernameModalBody.tsx @@ -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 && } - {discriminator && ( + {isDiscriminatorVisible ? ( <>
-
- {discriminator} -
+ - )} + ) : null} {errorString && ( @@ -289,7 +377,7 @@ export function EditUsernameModalBody({ i18n={i18n} onClose={() => { if (nickname) { - reserveUsername(nickname); + reserveUsername({ nickname, customDiscriminator }); } }} > diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index 5d20285ef..d62a4e561 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -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(); 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}
+ setToast(undefined)} + i18n={i18n} + onUndoArchive={shouldNeverBeCalled} + openFileInFolder={shouldNeverBeCalled} + toast={toast} + /> ); } @@ -1638,6 +1651,8 @@ function Control({ type SettingsRadioOptionType = Readonly<{ text: string; value: Enum; + readOnly?: boolean; + onClick?: () => void; }>; function SettingsRadio({ @@ -1655,11 +1670,13 @@ function SettingsRadio({ return (
- {options.map(({ text, value: optionValue }, i) => { + {options.map(({ text, value: optionValue, readOnly, onClick }, i) => { const htmlId = htmlIds[i]; return ( diff --git a/ts/components/ProfileEditor.stories.tsx b/ts/components/ProfileEditor.stories.tsx index 7a8e5f3ef..64da82607 100644 --- a/ts/components/ProfileEditor.stories.tsx +++ b/ts/components/ProfileEditor.stories.tsx @@ -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} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 4efddb6e7..107abe6a2 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -130,6 +130,8 @@ function getToast(toastType: ToastType): AnyToast { group: 'Hike Group 🏔', }, }; + case ToastType.WhoCanFindMeReadOnly: + return { toastType: ToastType.WhoCanFindMeReadOnly }; default: throw missingCaseError(toastType); } diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index fc8b85024..02cf24ba9 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -392,5 +392,11 @@ export function ToastManager({ ); } + if (toastType === ToastType.WhoCanFindMeReadOnly) { + return ( + {i18n('icu:WhoCanFindMeReadOnlyToast')} + ); + } + throw missingCaseError(toastType); } diff --git a/ts/services/username.ts b/ts/services/username.ts index add87e083..f071d0b9c 100644 --- a/ts/services/username.ts +++ b/ts/services/username.ts @@ -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 { await window.storage.remove('usernameLinkCorrupted'); const { usernameLinkHandle: serverIdString } = - await server.replaceUsernameLink({ encryptedUsername }); + await server.replaceUsernameLink({ + encryptedUsername, + keepLinkHandle: false, + }); await window.storage.put('usernameLink', { entropy, diff --git a/ts/state/ducks/username.ts b/ts/state/ducks/username.ts index 5d4124be1..23b692775 100644 --- a/ts/state/ducks/username.ts +++ b/ts/state/ducks/username.ts @@ -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); } diff --git a/ts/state/ducks/usernameEnums.ts b/ts/state/ducks/usernameEnums.ts index aa4167c4c..b8b3610a5 100644 --- a/ts/state/ducks/usernameEnums.ts +++ b/ts/state/ducks/usernameEnums.ts @@ -40,4 +40,7 @@ export enum UsernameReservationError { UsernameNotAvailable = 'UsernameNotAvailable', General = 'General', ConflictOrGone = 'ConflictOrGone', + NotEnoughDiscriminator = 'NotEnoughDiscriminator', + AllZeroDiscriminator = 'AllZeroDiscriminator', + LeadingZeroDiscriminator = 'LeadingZeroDiscriminator', } diff --git a/ts/test-electron/state/ducks/username_test.ts b/ts/test-electron/state/ducks/username_test.ts index 885d490d7..44304800f 100644 --- a/ts/test-electron/state/ducks/username_test.ts +++ b/ts/test-electron/state/ducks/username_test.ts @@ -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); diff --git a/ts/test-mock/pnp/username_test.ts b/ts/test-mock/pnp/username_test.ts index b67f3e42b..4c0eebc07 100644 --- a/ts/test-mock/pnp/username_test.ts +++ b/ts/test-mock/pnp/username_test.ts @@ -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'); diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index cc8c260e0..5bea6fd1e 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -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 { return replaceUsernameLinkResultZod.parse( await _ajax({ @@ -2046,6 +2048,7 @@ export function initialize({ usernameLinkEncryptedValue: toWebSafeBase64( Bytes.toBase64(encryptedUsername) ), + keepLinkHandle, }, }) ); diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index a34ef9926..5b507acb1 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -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 }; diff --git a/ts/types/Username.ts b/ts/types/Username.ts index 265603a56..74b7d4e13 100644 --- a/ts/types/Username.ts +++ b/ts/types/Username.ts @@ -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(); +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 524ec2f42..3599ea1f9 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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(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>(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>(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>(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(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>(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(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(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>(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>(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>(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(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>(new Map());", - "reasonCategory": "sageTrusted", - "updated": "2024-01-06T00:59:20.678Z", - "reasonDetail": "Recent bursts shown for burst behavior like throttling." } ] diff --git a/yarn.lock b/yarn.lock index 0bc778db2..a648bf5a5 100644 --- a/yarn.lock +++ b/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"