Move Profile Editor into the new Settings Tab
This commit is contained in:
parent
829b84a54e
commit
799a0dcc54
51 changed files with 1480 additions and 960 deletions
|
@ -6556,6 +6556,10 @@
|
|||
"messageformat": "Your profile could not be updated. Please try again.",
|
||||
"description": "Error message when something goes wrong updating your profile."
|
||||
},
|
||||
"icu:ProfileEditorModal--sharing": {
|
||||
"messageformat": "Sharing",
|
||||
"description": "Title for username QR code and link screen"
|
||||
},
|
||||
"icu:AnnouncementsOnlyGroupBanner--modal": {
|
||||
"messageformat": "Message an admin",
|
||||
"description": "Modal title for the list of admins in a group"
|
||||
|
|
|
@ -98,6 +98,115 @@ $secondary-text-color: light-dark(
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__profile-chip {
|
||||
@include mixins.button-reset;
|
||||
& {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
width: calc(100% - 11px);
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 1px;
|
||||
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
padding-inline-start: 10px;
|
||||
padding-inline-end: 10px;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
@include mixins.light-theme {
|
||||
background: variables.$color-gray-15;
|
||||
}
|
||||
@include mixins.dark-theme {
|
||||
background: variables.$color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include mixins.keyboard-mode {
|
||||
background: variables.$color-gray-05;
|
||||
}
|
||||
@include mixins.dark-keyboard-mode {
|
||||
background: variables.$color-gray-75;
|
||||
}
|
||||
}
|
||||
&:hover:not(&--selected) {
|
||||
@include mixins.mouse-mode {
|
||||
background: variables.$color-gray-05;
|
||||
}
|
||||
@include mixins.dark-mouse-mode {
|
||||
background: variables.$color-gray-75;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
&__text-container {
|
||||
flex-grow: 1;
|
||||
// Aligning the top of capital letters one pixel below the top of the avatar
|
||||
margin-top: -4px;
|
||||
margin-bottom: -5px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include mixins.font-body-1-bold;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&__number {
|
||||
@include mixins.font-body-small;
|
||||
margin-top: 2px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&__username {
|
||||
@include mixins.font-body-small;
|
||||
margin-top: 2px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__qr-icon-container {
|
||||
margin-inline-start: 2px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 50%;
|
||||
@include mixins.light-theme {
|
||||
background-color: variables.$color-gray-15;
|
||||
}
|
||||
@include mixins.dark-theme {
|
||||
background-color: variables.$color-gray-65;
|
||||
}
|
||||
}
|
||||
&__qr-icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
@include mixins.color-svg-themed(
|
||||
'../images/icons/v3/qr_code/qr_code.svg',
|
||||
variables.$color-black,
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include mixins.button-reset;
|
||||
& {
|
||||
|
@ -105,11 +214,11 @@ $secondary-text-color: light-dark(
|
|||
align-items: center;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: calc(100% - 20px);
|
||||
width: calc(100% - 11px);
|
||||
padding-block: 14px;
|
||||
padding-inline: 0;
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-end: 1px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
@ -227,6 +336,7 @@ $secondary-text-color: light-dark(
|
|||
text-align: center;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
border-bottom: 1px solid variables.$color-gray-15;
|
||||
@include mixins.light-theme {
|
||||
|
@ -378,11 +488,11 @@ $secondary-text-color: light-dark(
|
|||
|
||||
& {
|
||||
display: inline-block;
|
||||
inset-inline-start: 12px;
|
||||
height: 20px;
|
||||
margin-inline-start: 12px;
|
||||
min-width: 20px;
|
||||
vertical-align: text-bottom;
|
||||
width: 20px;
|
||||
vertical-align: text-bottom;
|
||||
@include mixins.position-absolute-center-y;
|
||||
}
|
||||
|
||||
@include mixins.light-theme {
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
@use '../variables';
|
||||
|
||||
.ProfileEditor {
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: 24px;
|
||||
|
||||
&__icon {
|
||||
&--container {
|
||||
align-items: center;
|
||||
|
@ -337,6 +340,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__button-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-block: 1em 16px;
|
||||
gap: 8px;
|
||||
|
||||
.module-Button:not(:first-child) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProfileEditor__Title {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
.EditUsernameModalBody {
|
||||
.UsernameEditor {
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -143,6 +143,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__button-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-block: 1em 16px;
|
||||
gap: 8px;
|
||||
|
||||
.module-Button:not(:first-child) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__input__container.Input__container {
|
||||
/**
|
||||
* Discriminator should always be to the right of the nickname.
|
|
@ -4,12 +4,12 @@
|
|||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
.UsernameLinkModalBody {
|
||||
.UsernameLinkEditor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
max-width: 295px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
|
||||
&__container {
|
||||
|
@ -47,7 +47,7 @@
|
|||
width: 148px;
|
||||
height: 148px;
|
||||
|
||||
.UsernameLinkModalBody__card--shadow & {
|
||||
.UsernameLinkEditor__card--shadow & {
|
||||
outline: 2px solid variables.$color-gray-05;
|
||||
}
|
||||
|
||||
|
@ -199,7 +199,6 @@
|
|||
padding-inline: 16px;
|
||||
border-radius: 12px;
|
||||
margin-block-start: 20px;
|
||||
max-width: 296px;
|
||||
width: 100%;
|
||||
@include mixins.light-theme() {
|
||||
border: 2px solid variables.$color-gray-05;
|
||||
|
@ -297,6 +296,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__button-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-block: 1em 16px;
|
||||
gap: 8px;
|
||||
|
||||
.module-Button:not(:first-child) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__done {
|
||||
width: 100%;
|
||||
margin-block-end: 8px;
|
|
@ -99,7 +99,6 @@
|
|||
@use 'components/EditConversationAttributesModal.scss';
|
||||
@use 'components/EditHistoryMessagesModal.scss';
|
||||
@use 'components/EditNicknameAndNoteModal.scss';
|
||||
@use 'components/EditUsernameModalBody.scss';
|
||||
@use 'components/ForwardMessageModal.scss';
|
||||
@use 'components/fun/Fun.scss';
|
||||
@use 'components/GradientDial.scss';
|
||||
|
@ -192,7 +191,8 @@
|
|||
@use 'components/ToastManager.scss';
|
||||
@use 'components/Waveform.scss';
|
||||
@use 'components/WaveformScrubber.scss';
|
||||
@use 'components/UsernameLinkModalBody.scss';
|
||||
@use 'components/UsernameEditor.scss';
|
||||
@use 'components/UsernameLinkEditor.scss';
|
||||
@use 'components/UsernameMegaphone.scss';
|
||||
@use 'components/UsernameOnboardingModal.scss';
|
||||
@use 'components/WhatsNew.scss';
|
||||
|
|
|
@ -216,6 +216,8 @@ import { sendSyncRequests } from './textsecure/syncRequests';
|
|||
import { handleServerAlerts } from './util/handleServerAlerts';
|
||||
import { isLocalBackupsEnabled } from './util/isLocalBackupsEnabled';
|
||||
import { NavTab } from './state/ducks/nav';
|
||||
import { Page } from './components/Preferences';
|
||||
import { EditState } from './components/ProfileEditor';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -1348,38 +1350,14 @@ export async function startApp(): Promise<void> {
|
|||
window.reduxActions.app.openStandalone();
|
||||
});
|
||||
|
||||
let openingSettingsTab = false;
|
||||
window.Whisper.events.on('openSettingsTab', async () => {
|
||||
const logId = 'openSettingsTab';
|
||||
try {
|
||||
if (openingSettingsTab) {
|
||||
log.info(
|
||||
`${logId}: Already attempting to open settings tab, returning early`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
openingSettingsTab = true;
|
||||
|
||||
const newTab = NavTab.Settings;
|
||||
const needToCancel =
|
||||
await window.Signal.Services.beforeNavigate.shouldCancelNavigation({
|
||||
context: logId,
|
||||
newTab,
|
||||
});
|
||||
|
||||
if (needToCancel) {
|
||||
log.info(`${logId}: Cancelling navigation to the settings tab`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.reduxActions.nav.changeNavTab(newTab);
|
||||
} finally {
|
||||
if (!openingSettingsTab) {
|
||||
log.warn(`${logId}: openingSettingsTab was already false in finally!`);
|
||||
}
|
||||
openingSettingsTab = false;
|
||||
}
|
||||
window.reduxActions.nav.changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.None,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
window.Whisper.events.on('stageLocalBackupForImport', () => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type {
|
||||
|
@ -21,6 +22,7 @@ import { avatarDataToBytes } from '../util/avatarDataToBytes';
|
|||
import { createAvatarData } from '../util/createAvatarData';
|
||||
import { isSameAvatarData } from '../util/isSameAvatarData';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
|
||||
export type PropsType = {
|
||||
avatarColor?: AvatarColorType;
|
||||
|
@ -83,6 +85,22 @@ export function AvatarEditor({
|
|||
[localAvatarData]
|
||||
);
|
||||
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'AvatarEditor',
|
||||
tryClose,
|
||||
});
|
||||
|
||||
const hasChanges =
|
||||
!isEqual(initialAvatar, avatarPreview) ||
|
||||
Boolean(pendingClear && avatarUrl);
|
||||
const onTryClose = useCallback(() => {
|
||||
const onDiscard = () => undefined;
|
||||
confirmDiscardIf(hasChanges, onDiscard);
|
||||
}, [confirmDiscardIf, hasChanges]);
|
||||
tryClose.current = onTryClose;
|
||||
|
||||
const selectedAvatar = getSelectedAvatar(provisionalSelectedAvatar);
|
||||
|
||||
// Caching the Uint8Array produced into avatarData as buffer because
|
||||
|
@ -151,9 +169,6 @@ export function AvatarEditor({
|
|||
setInitialAvatar(avatarBuffer);
|
||||
}, []);
|
||||
|
||||
const hasChanges =
|
||||
initialAvatar !== avatarPreview || Boolean(pendingClear && avatarUrl);
|
||||
|
||||
let content: JSX.Element | undefined;
|
||||
|
||||
if (editMode === EditMode.Main) {
|
||||
|
@ -166,6 +181,7 @@ export function AvatarEditor({
|
|||
avatarValue={avatarPreview}
|
||||
conversationTitle={conversationTitle}
|
||||
i18n={i18n}
|
||||
isEditable
|
||||
isGroup={isGroup}
|
||||
onAvatarLoaded={handleAvatarLoaded}
|
||||
onClear={() => {
|
||||
|
@ -233,12 +249,23 @@ export function AvatarEditor({
|
|||
<AvatarModalButtons
|
||||
hasChanges={hasChanges}
|
||||
i18n={i18n}
|
||||
onCancel={onCancel}
|
||||
onCancel={() => {
|
||||
setAvatarPreview(initialAvatar);
|
||||
setPendingClear(false);
|
||||
|
||||
// Delay navigation until new avatar data resolves and we are no longer dirty
|
||||
setTimeout(() => onCancel(), 500);
|
||||
}}
|
||||
onSave={() => {
|
||||
if (selectedAvatar) {
|
||||
replaceAvatar(selectedAvatar, selectedAvatar, conversationId);
|
||||
}
|
||||
onSave(avatarPreview);
|
||||
|
||||
setInitialAvatar(avatarPreview);
|
||||
setPendingClear(false);
|
||||
|
||||
// Delay navigation until new avatar data resolves and we are no longer dirty
|
||||
setTimeout(() => onSave(avatarPreview), 500);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -297,5 +324,10 @@ export function AvatarEditor({
|
|||
throw missingCaseError(editMode);
|
||||
}
|
||||
|
||||
return <div className="AvatarEditor">{content}</div>;
|
||||
return (
|
||||
<div className="AvatarEditor">
|
||||
{confirmDiscardModal}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
onAvatarLoaded: action('onAvatarLoaded'),
|
||||
onClear: action('onClear'),
|
||||
onClick: action('onClick'),
|
||||
showUploadButton: Boolean(overrideProps.showUploadButton),
|
||||
style: overrideProps.style,
|
||||
});
|
||||
|
||||
|
@ -67,6 +68,7 @@ export function NoStateGroupUploadMe(): JSX.Element {
|
|||
avatarColor: AvatarColors[1],
|
||||
isEditable: true,
|
||||
isGroup: true,
|
||||
showUploadButton: true,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ export type PropsType = {
|
|||
onAvatarLoaded?: (avatarBuffer: Uint8Array) => unknown;
|
||||
onClear?: () => unknown;
|
||||
onClick?: () => unknown;
|
||||
showUploadButton?: boolean;
|
||||
style?: CSSProperties;
|
||||
} & Pick<ConversationType, 'avatarPlaceholderGradient' | 'hasAvatar'>;
|
||||
|
||||
|
@ -50,6 +51,7 @@ export function AvatarPreview({
|
|||
onAvatarLoaded,
|
||||
onClear,
|
||||
onClick,
|
||||
showUploadButton,
|
||||
style = {},
|
||||
}: PropsType): JSX.Element {
|
||||
const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>();
|
||||
|
@ -184,7 +186,7 @@ export function AvatarPreview({
|
|||
style={componentStyle}
|
||||
>
|
||||
{content}
|
||||
{isEditable && <div className="AvatarPreview__upload" />}
|
||||
{showUploadButton && <div className="AvatarPreview__upload" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -232,7 +234,7 @@ export function AvatarPreview({
|
|||
type="button"
|
||||
/>
|
||||
)}
|
||||
{isEditable && <div className="AvatarPreview__upload" />}
|
||||
{showUploadButton && <div className="AvatarPreview__upload" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -104,9 +104,6 @@ export type PropsType = {
|
|||
// NotePreviewModal
|
||||
notePreviewModalProps: { conversationId: string } | null;
|
||||
renderNotePreviewModal: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
// SafetyNumberModal
|
||||
safetyNumberModalContactId: string | undefined;
|
||||
renderSafetyNumber: () => JSX.Element;
|
||||
|
@ -208,9 +205,6 @@ export function GlobalModalContainer({
|
|||
// NotePreviewModal
|
||||
notePreviewModalProps,
|
||||
renderNotePreviewModal,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
|
@ -333,10 +327,6 @@ export function GlobalModalContainer({
|
|||
return renderNotePreviewModal();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
||||
if (isProfileNameWarningModalVisible) {
|
||||
return renderProfileNameWarningModal();
|
||||
}
|
||||
|
|
|
@ -152,6 +152,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
totalBytes: 0,
|
||||
downloadedBytes: 0,
|
||||
},
|
||||
changeLocation: action('changeLocation'),
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
clearSearchQuery: action('clearSearchQuery'),
|
||||
|
@ -320,7 +321,6 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
'toggleConversationInChooseMembers'
|
||||
),
|
||||
toggleNavTabsCollapse: action('toggleNavTabsCollapse'),
|
||||
toggleProfileEditor: action('toggleProfileEditor'),
|
||||
updateFilterByUnread: action('updateFilterByUnread'),
|
||||
updateSearchTerm: action('updateSearchTerm'),
|
||||
|
||||
|
|
|
@ -54,11 +54,14 @@ import {
|
|||
NavSidebarSearchHeader,
|
||||
} from './NavSidebar';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
|
||||
import type { ServerAlertsType } from '../util/handleServerAlerts';
|
||||
import { getServerAlertDialog } from './ServerAlerts';
|
||||
import { NavTab } from '../state/ducks/nav';
|
||||
import type { Location } from '../state/ducks/nav';
|
||||
import { Page } from './Preferences';
|
||||
import { EditState } from './ProfileEditor';
|
||||
|
||||
export type PropsType = {
|
||||
backupMediaDownloadProgress: {
|
||||
|
@ -122,6 +125,7 @@ export type PropsType = {
|
|||
|
||||
// Action Creators
|
||||
blockConversation: (conversationId: string) => void;
|
||||
changeLocation: (location: Location) => void;
|
||||
clearConversationSearch: () => void;
|
||||
clearGroupCreationError: () => void;
|
||||
clearSearchQuery: () => void;
|
||||
|
@ -163,7 +167,6 @@ export type PropsType = {
|
|||
toggleComposeEditingAvatar: () => unknown;
|
||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||
toggleProfileEditor: (initialEditState?: ProfileEditorEditState) => void;
|
||||
updateSearchTerm: (query: string) => void;
|
||||
updateFilterByUnread: (filterByUnread: boolean) => void;
|
||||
|
||||
|
@ -195,6 +198,7 @@ export function LeftPane({
|
|||
blockConversation,
|
||||
cancelBackupMediaDownload,
|
||||
challengeStatus,
|
||||
changeLocation,
|
||||
clearConversationSearch,
|
||||
clearGroupCreationError,
|
||||
clearSearchQuery,
|
||||
|
@ -244,7 +248,6 @@ export function LeftPane({
|
|||
selectedConversationId,
|
||||
targetedMessageId,
|
||||
toggleNavTabsCollapse,
|
||||
toggleProfileEditor,
|
||||
setChallengeStatus,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupExpireTimer,
|
||||
|
@ -667,7 +670,13 @@ export function LeftPane({
|
|||
actionText={i18n('icu:LeftPane--corrupted-username--action-text')}
|
||||
onClick={() => {
|
||||
openUsernameReservationModal();
|
||||
toggleProfileEditor(ProfileEditorEditState.Username);
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.Username,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:LeftPane--corrupted-username--text')}
|
||||
|
@ -677,7 +686,15 @@ export function LeftPane({
|
|||
maybeBanner = (
|
||||
<LeftPaneBanner
|
||||
actionText={i18n('icu:LeftPane--corrupted-username-link--action-text')}
|
||||
onClick={() => toggleProfileEditor(ProfileEditorEditState.UsernameLink)}
|
||||
onClick={() => {
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.UsernameLink,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:LeftPane--corrupted-username-link--text')}
|
||||
</LeftPaneBanner>
|
||||
|
|
|
@ -66,6 +66,12 @@ export const ModalHost = React.memo(function ModalHostInner({
|
|||
}
|
||||
return handleOutsideClick(
|
||||
node => {
|
||||
// In strange event propagation situations we can get the actual document.body
|
||||
// node here. We don't want to handle those events.
|
||||
if (node === document.body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore clicks that originate in the calling/pip
|
||||
// when we're not handling a component in the calling/pip
|
||||
if (
|
||||
|
|
|
@ -10,9 +10,12 @@ import type { LocalizerType, ThemeType } from '../types/Util';
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import { NavTab } from '../state/ducks/nav';
|
||||
import type { Location } from '../state/ducks/nav';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import { Page } from './Preferences';
|
||||
import { EditState } from './ProfileEditor';
|
||||
|
||||
type NavTabsItemBadgesProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
|
@ -193,11 +196,11 @@ export type NavTabsProps = Readonly<{
|
|||
hasFailedStorySends: boolean;
|
||||
hasPendingUpdate: boolean;
|
||||
i18n: LocalizerType;
|
||||
isInternalUser: boolean;
|
||||
me: ConversationType;
|
||||
navTabsCollapsed: boolean;
|
||||
onNavTabSelected: (tab: NavTab) => void;
|
||||
onChangeLocation: (location: Location) => void;
|
||||
onToggleNavTabsCollapse: (collapsed: boolean) => void;
|
||||
onToggleProfileEditor: () => void;
|
||||
renderCallsTab: () => ReactNode;
|
||||
renderChatsTab: () => ReactNode;
|
||||
renderStoriesTab: () => ReactNode;
|
||||
|
@ -215,11 +218,11 @@ export function NavTabs({
|
|||
hasFailedStorySends,
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
isInternalUser,
|
||||
me,
|
||||
navTabsCollapsed,
|
||||
onNavTabSelected,
|
||||
onChangeLocation,
|
||||
onToggleNavTabsCollapse,
|
||||
onToggleProfileEditor,
|
||||
renderCallsTab,
|
||||
renderChatsTab,
|
||||
renderStoriesTab,
|
||||
|
@ -232,7 +235,18 @@ export function NavTabs({
|
|||
unreadStoriesCount,
|
||||
}: NavTabsProps): JSX.Element {
|
||||
function handleSelectionChange(key: Key) {
|
||||
onNavTabSelected(key as NavTab);
|
||||
const tab = key as NavTab;
|
||||
if (tab === NavTab.Settings) {
|
||||
onChangeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.None,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
onChangeLocation({ tab });
|
||||
}
|
||||
}
|
||||
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
|
@ -309,44 +323,48 @@ export function NavTabs({
|
|||
hasPendingUpdate={hasPendingUpdate}
|
||||
/>
|
||||
</TabList>
|
||||
<div className="NavTabs__Misc">
|
||||
<button
|
||||
type="button"
|
||||
className="NavTabs__Item NavTabs__Item--Profile"
|
||||
onClick={() => {
|
||||
onToggleProfileEditor();
|
||||
}}
|
||||
aria-label={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
>
|
||||
<Tooltip
|
||||
content={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
theme={Theme.Dark}
|
||||
direction={isRTL ? TooltipPlacement.Left : TooltipPlacement.Right}
|
||||
delay={600}
|
||||
{isInternalUser && (
|
||||
<div className="NavTabs__Misc">
|
||||
<button
|
||||
type="button"
|
||||
className="NavTabs__Item NavTabs__Item--Profile"
|
||||
onClick={() => {
|
||||
handleSelectionChange(NavTab.Settings);
|
||||
}}
|
||||
aria-label={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span className="NavTabs__ItemContent">
|
||||
<Avatar
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
title={me.title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
/>
|
||||
<Tooltip
|
||||
content={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
theme={Theme.Dark}
|
||||
direction={
|
||||
isRTL ? TooltipPlacement.Left : TooltipPlacement.Right
|
||||
}
|
||||
delay={600}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span className="NavTabs__ItemContent">
|
||||
<Avatar
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
title={me.title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
<TabPanel id={NavTab.Chats} className="NavTabs__TabPanel">
|
||||
{renderChatsTab}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Page, Preferences } from './Preferences';
|
||||
|
@ -13,6 +13,13 @@ import { EmojiSkinTone } from './fun/data/emojis';
|
|||
import { DAY, DurationInSeconds, WEEK } from '../util/durations';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { EditState, ProfileEditor } from './ProfileEditor';
|
||||
import {
|
||||
UsernameEditState,
|
||||
UsernameLinkState,
|
||||
} from '../state/ducks/usernameEnums';
|
||||
|
||||
import type { PropsType } from './Preferences';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
@ -20,6 +27,12 @@ import type { MessageAttributesType } from '../model-types';
|
|||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const me = {
|
||||
...getDefaultConversation(),
|
||||
phoneNumber: '(215) 555-2345',
|
||||
username: 'someone.243',
|
||||
};
|
||||
|
||||
const availableMicrophones = [
|
||||
{
|
||||
name: 'DefAuLt (Headphones)',
|
||||
|
@ -89,6 +102,55 @@ function renderUpdateDialog(
|
|||
/>
|
||||
);
|
||||
}
|
||||
function RenderProfileEditor(): JSX.Element {
|
||||
const contentsRef = useRef<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<ProfileEditor
|
||||
aboutEmoji={undefined}
|
||||
aboutText={undefined}
|
||||
color={undefined}
|
||||
contentsRef={contentsRef}
|
||||
conversationId="something"
|
||||
deleteAvatarFromDisk={action('deleteAvatarFromDisk')}
|
||||
deleteUsername={action('deleteUsername')}
|
||||
familyName={me.familyName}
|
||||
firstName={me.firstName ?? ''}
|
||||
hasCompletedUsernameLinkOnboarding={false}
|
||||
i18n={i18n}
|
||||
editState={EditState.None}
|
||||
markCompletedUsernameLinkOnboarding={action(
|
||||
'markCompletedUsernameLinkOnboarding'
|
||||
)}
|
||||
onProfileChanged={action('onProfileChanged')}
|
||||
onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')}
|
||||
openUsernameReservationModal={action('openUsernameReservationModal')}
|
||||
profileAvatarUrl={undefined}
|
||||
recentEmojis={[]}
|
||||
renderUsernameEditor={() => <div />}
|
||||
replaceAvatar={action('replaceAvatar')}
|
||||
resetUsernameLink={action('resetUsernameLink')}
|
||||
saveAttachment={action('saveAttachment')}
|
||||
saveAvatarToDisk={action('saveAvatarToDisk')}
|
||||
setEditState={action('setEditState')}
|
||||
setUsernameEditState={action('setUsernameEditState')}
|
||||
setUsernameLinkColor={action('setUsernameLinkColor')}
|
||||
showToast={action('showToast')}
|
||||
emojiSkinToneDefault={null}
|
||||
userAvatarData={[]}
|
||||
username={undefined}
|
||||
usernameCorrupted={false}
|
||||
usernameEditState={UsernameEditState.Editing}
|
||||
usernameLink={undefined}
|
||||
usernameLinkColor={undefined}
|
||||
usernameLinkCorrupted={false}
|
||||
usernameLinkState={UsernameLinkState.Ready}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderToastManager(): JSX.Element {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Components/Preferences',
|
||||
|
@ -124,6 +186,7 @@ export default {
|
|||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
backupFeatureEnabled: false,
|
||||
badge: undefined,
|
||||
blockedCount: 0,
|
||||
customColors: {},
|
||||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||
|
@ -170,6 +233,7 @@ export default {
|
|||
isUpdateDownloaded: false,
|
||||
lastSyncTime: Date.now(),
|
||||
localeOverride: null,
|
||||
me,
|
||||
navTabsCollapsed: false,
|
||||
notificationContent: 'name',
|
||||
otherTabsUnreadStats: {
|
||||
|
@ -177,6 +241,7 @@ export default {
|
|||
unreadMentionsCount: 0,
|
||||
markedUnread: false,
|
||||
},
|
||||
page: Page.Profile,
|
||||
preferredSystemLocales: ['en'],
|
||||
resolvedLocale: 'en',
|
||||
selectedCamera:
|
||||
|
@ -185,11 +250,14 @@ export default {
|
|||
selectedSpeaker: availableSpeakers[1],
|
||||
sentMediaQualitySetting: 'standard',
|
||||
themeSetting: 'system',
|
||||
theme: ThemeType.light,
|
||||
universalExpireTimer: DurationInSeconds.HOUR,
|
||||
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
|
||||
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
|
||||
zoomFactor: 1,
|
||||
|
||||
renderProfileEditor: RenderProfileEditor,
|
||||
renderToastManager,
|
||||
renderUpdateDialog,
|
||||
getConversationsWithCustomColor: () => [],
|
||||
|
||||
|
@ -263,6 +331,8 @@ export default {
|
|||
setGlobalDefaultConversationColor: action(
|
||||
'setGlobalDefaultConversationColor'
|
||||
),
|
||||
setPage: action('setPage'),
|
||||
showToast: action('showToast'),
|
||||
validateBackup: async () => {
|
||||
return {
|
||||
result: validateBackupResult,
|
||||
|
@ -272,40 +342,82 @@ export default {
|
|||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => <Preferences {...args} />;
|
||||
const Template: StoryFn<PropsType> = args => {
|
||||
const [page, setPage] = useState(args.page);
|
||||
return <Preferences {...args} page={page} setPage={setPage} />;
|
||||
};
|
||||
|
||||
export const _Preferences = Template.bind({});
|
||||
|
||||
export const General = Template.bind({});
|
||||
General.args = {
|
||||
page: Page.General,
|
||||
};
|
||||
export const Appearance = Template.bind({});
|
||||
Appearance.args = {
|
||||
page: Page.Appearance,
|
||||
};
|
||||
export const Chats = Template.bind({});
|
||||
Chats.args = {
|
||||
page: Page.Chats,
|
||||
};
|
||||
export const Calls = Template.bind({});
|
||||
Calls.args = {
|
||||
page: Page.Calls,
|
||||
};
|
||||
export const Notifications = Template.bind({});
|
||||
Notifications.args = {
|
||||
page: Page.Notifications,
|
||||
};
|
||||
export const Privacy = Template.bind({});
|
||||
Privacy.args = {
|
||||
page: Page.Privacy,
|
||||
};
|
||||
export const DataUsage = Template.bind({});
|
||||
DataUsage.args = {
|
||||
page: Page.DataUsage,
|
||||
};
|
||||
export const Internal = Template.bind({});
|
||||
Internal.args = {
|
||||
page: Page.Internal,
|
||||
isInternalUser: true,
|
||||
};
|
||||
|
||||
export const Blocked1 = Template.bind({});
|
||||
Blocked1.args = {
|
||||
blockedCount: 1,
|
||||
page: Page.Privacy,
|
||||
};
|
||||
|
||||
export const BlockedMany = Template.bind({});
|
||||
BlockedMany.args = {
|
||||
blockedCount: 55,
|
||||
page: Page.Privacy,
|
||||
};
|
||||
|
||||
export const CustomUniversalExpireTimer = Template.bind({});
|
||||
CustomUniversalExpireTimer.args = {
|
||||
universalExpireTimer: DurationInSeconds.fromSeconds(9000),
|
||||
page: Page.Privacy,
|
||||
};
|
||||
|
||||
export const PNPSharingDisabled = Template.bind({});
|
||||
PNPSharingDisabled.args = {
|
||||
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
|
||||
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
|
||||
page: Page.PNP,
|
||||
};
|
||||
|
||||
export const PNPDiscoverabilityDisabled = Template.bind({});
|
||||
PNPDiscoverabilityDisabled.args = {
|
||||
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
|
||||
whoCanFindMe: PhoneNumberDiscoverability.NotDiscoverable,
|
||||
page: Page.PNP,
|
||||
};
|
||||
|
||||
export const BackupsPaidActive = Template.bind({});
|
||||
BackupsPaidActive.args = {
|
||||
initialPage: Page.Backups,
|
||||
page: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
mediaSize: 539_249_410_039,
|
||||
|
@ -324,7 +436,7 @@ BackupsPaidActive.args = {
|
|||
|
||||
export const BackupsPaidCancelled = Template.bind({});
|
||||
BackupsPaidCancelled.args = {
|
||||
initialPage: Page.Backups,
|
||||
page: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
mediaSize: 539_249_410_039,
|
||||
|
@ -343,7 +455,7 @@ BackupsPaidCancelled.args = {
|
|||
|
||||
export const BackupsFree = Template.bind({});
|
||||
BackupsFree.args = {
|
||||
initialPage: Page.Backups,
|
||||
page: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'free',
|
||||
|
@ -353,13 +465,12 @@ BackupsFree.args = {
|
|||
|
||||
export const BackupsOff = Template.bind({});
|
||||
BackupsOff.args = {
|
||||
initialPage: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
};
|
||||
|
||||
export const BackupsSubscriptionNotFound = Template.bind({});
|
||||
BackupsSubscriptionNotFound.args = {
|
||||
initialPage: Page.Backups,
|
||||
page: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'not-found',
|
||||
|
@ -373,19 +484,13 @@ BackupsSubscriptionNotFound.args = {
|
|||
|
||||
export const BackupsSubscriptionExpired = Template.bind({});
|
||||
BackupsSubscriptionExpired.args = {
|
||||
initialPage: Page.Backups,
|
||||
page: Page.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'expired',
|
||||
},
|
||||
};
|
||||
|
||||
export const Internal = Template.bind({});
|
||||
Internal.args = {
|
||||
initialPage: Page.Internal,
|
||||
isInternalUser: true,
|
||||
};
|
||||
|
||||
export const UpdateAvailable = Template.bind({});
|
||||
UpdateAvailable.args = {
|
||||
hasPendingUpdate: true,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
||||
import React, {
|
||||
useCallback,
|
||||
|
@ -12,6 +13,45 @@ import React, {
|
|||
import { isNumber, noop, partition } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
||||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||
import { Select } from './Select';
|
||||
import { Spinner } from './Spinner';
|
||||
import { getCustomColorStyle } from '../util/getCustomColorStyle';
|
||||
import {
|
||||
DEFAULT_DURATIONS_IN_SECONDS,
|
||||
DEFAULT_DURATIONS_SET,
|
||||
format as formatExpirationTimer,
|
||||
} from '../util/expirationTimer';
|
||||
import { DurationInSeconds } from '../util/durations';
|
||||
import { focusableSelector } from '../util/focusableSelectors';
|
||||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { removeDiacritics } from '../util/removeDiacritics';
|
||||
import { assertDev } from '../util/assert';
|
||||
import { I18n } from './I18n';
|
||||
import { FunSkinTonesList } from './fun/FunSkinTones';
|
||||
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
||||
import {
|
||||
SettingsControl as Control,
|
||||
SettingsRadio,
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil';
|
||||
import { PreferencesBackups } from './PreferencesBackups';
|
||||
import { PreferencesInternal } from './PreferencesInternal';
|
||||
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
|
||||
import { NavTabsToggle } from './NavTabs';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
|
||||
import type { MediaDeviceSettings } from '../types/Calling';
|
||||
import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
|
||||
import type {
|
||||
|
@ -34,47 +74,12 @@ import type {
|
|||
SentMediaQualityType,
|
||||
ThemeType,
|
||||
} from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||
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,
|
||||
format as formatExpirationTimer,
|
||||
} from '../util/expirationTimer';
|
||||
import { DurationInSeconds } from '../util/durations';
|
||||
import { focusableSelector } from '../util/focusableSelectors';
|
||||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { removeDiacritics } from '../util/removeDiacritics';
|
||||
import { assertDev } from '../util/assert';
|
||||
import { I18n } from './I18n';
|
||||
import { FunSkinTonesList } from './fun/FunSkinTones';
|
||||
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
||||
import type {
|
||||
BackupsSubscriptionType,
|
||||
BackupStatusType,
|
||||
} from '../types/backups';
|
||||
import {
|
||||
SettingsControl as Control,
|
||||
SettingsRadio,
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil';
|
||||
import { PreferencesBackups } from './PreferencesBackups';
|
||||
import { PreferencesInternal } from './PreferencesInternal';
|
||||
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
|
||||
import { NavTabsToggle } from './NavTabs';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import type { MessageCountBySchemaVersionType } from '../sql/Interface';
|
||||
import type { MessageAttributesType } from '../model-types';
|
||||
|
||||
|
@ -116,7 +121,7 @@ export type PropsDataType = {
|
|||
hasStoriesDisabled: boolean;
|
||||
hasTextFormatting: boolean;
|
||||
hasTypingIndicators: boolean;
|
||||
initialPage?: Page;
|
||||
page: Page;
|
||||
lastSyncTime?: number;
|
||||
notificationContent: NotificationSettingType;
|
||||
phoneNumber: string | undefined;
|
||||
|
@ -143,6 +148,9 @@ export type PropsDataType = {
|
|||
isUpdateDownloaded: boolean;
|
||||
navTabsCollapsed: boolean;
|
||||
otherTabsUnreadStats: UnreadStats;
|
||||
me: ConversationType;
|
||||
badge: BadgeType | undefined;
|
||||
theme: ThemeType;
|
||||
|
||||
// Limited support features
|
||||
isAutoDownloadUpdatesSupported: boolean;
|
||||
|
@ -164,6 +172,12 @@ export type PropsDataType = {
|
|||
|
||||
type PropsFunctionType = {
|
||||
// Render props
|
||||
renderProfileEditor: (options: {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
}) => JSX.Element;
|
||||
renderToastManager: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderUpdateDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
|
@ -193,6 +207,8 @@ type PropsFunctionType = {
|
|||
value: CustomColorType;
|
||||
}
|
||||
) => unknown;
|
||||
setPage: (page: Page) => unknown;
|
||||
showToast: (toast: AnyToast) => unknown;
|
||||
validateBackup: () => Promise<BackupValidationResultType>;
|
||||
|
||||
// Change handlers
|
||||
|
@ -245,6 +261,7 @@ export type PropsPreloadType = Omit<PropsType, 'i18n'>;
|
|||
|
||||
export enum Page {
|
||||
// Accessible through left nav
|
||||
Profile = 'Profile',
|
||||
General = 'General',
|
||||
Appearance = 'Appearance',
|
||||
Chats = 'Chats',
|
||||
|
@ -297,6 +314,7 @@ export function Preferences({
|
|||
availableSpeakers,
|
||||
backupFeatureEnabled,
|
||||
backupSubscriptionStatus,
|
||||
badge,
|
||||
blockedCount,
|
||||
cloudBackupStatus,
|
||||
customColors,
|
||||
|
@ -336,7 +354,6 @@ export function Preferences({
|
|||
hasTextFormatting,
|
||||
hasTypingIndicators,
|
||||
i18n,
|
||||
initialPage = Page.General,
|
||||
initialSpellCheckSetting,
|
||||
isAutoDownloadUpdatesSupported,
|
||||
isAutoLaunchSupported,
|
||||
|
@ -351,6 +368,7 @@ export function Preferences({
|
|||
isUpdateDownloaded,
|
||||
lastSyncTime,
|
||||
makeSyncRequest,
|
||||
me,
|
||||
navTabsCollapsed,
|
||||
notificationContent,
|
||||
onAudioNotificationsChange,
|
||||
|
@ -390,12 +408,15 @@ export function Preferences({
|
|||
onWhoCanFindMeChange,
|
||||
onZoomFactorChange,
|
||||
otherTabsUnreadStats,
|
||||
page,
|
||||
phoneNumber = '',
|
||||
preferredSystemLocales,
|
||||
refreshCloudBackupStatus,
|
||||
refreshBackupSubscriptionStatus,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
renderProfileEditor,
|
||||
renderToastManager,
|
||||
renderUpdateDialog,
|
||||
resetAllChatColors,
|
||||
resetDefaultChatColor,
|
||||
|
@ -405,7 +426,10 @@ export function Preferences({
|
|||
selectedSpeaker,
|
||||
sentMediaQualitySetting,
|
||||
setGlobalDefaultConversationColor,
|
||||
setPage,
|
||||
showToast,
|
||||
localeOverride,
|
||||
theme,
|
||||
themeSetting,
|
||||
universalExpireTimer = DurationInSeconds.ZERO,
|
||||
validateBackup,
|
||||
|
@ -422,7 +446,6 @@ export function Preferences({
|
|||
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
|
||||
const [confirmContentProtection, setConfirmContentProtection] =
|
||||
useState(false);
|
||||
const [page, setPage] = useState<Page>(initialPage);
|
||||
const [showSyncFailed, setShowSyncFailed] = useState(false);
|
||||
const [nowSyncing, setNowSyncing] = useState(false);
|
||||
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
|
||||
|
@ -434,7 +457,6 @@ export function Preferences({
|
|||
string | null | undefined
|
||||
>(localeOverride);
|
||||
const [languageSearchInput, setLanguageSearchInput] = useState('');
|
||||
const [toast, setToast] = useState<AnyToast | undefined>();
|
||||
const [confirmPnpNotDiscoverable, setConfirmPnpNoDiscoverable] =
|
||||
useState(false);
|
||||
|
||||
|
@ -616,12 +638,14 @@ export function Preferences({
|
|||
});
|
||||
}, [localeSearchOptions, languageSearchInput]);
|
||||
|
||||
let pageTitle: string | undefined;
|
||||
let pageBackButton: JSX.Element | undefined;
|
||||
let pageContents: JSX.Element | undefined;
|
||||
if (page === Page.General) {
|
||||
pageTitle = i18n('icu:Preferences__button--general');
|
||||
pageContents = (
|
||||
let content: JSX.Element | undefined;
|
||||
|
||||
if (page === Page.Profile) {
|
||||
content = renderProfileEditor({
|
||||
contentsRef: settingsPaneRef,
|
||||
});
|
||||
} else if (page === Page.General) {
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow>
|
||||
<Control
|
||||
|
@ -719,6 +743,13 @@ export function Preferences({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--general')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Appearance) {
|
||||
let zoomFactors = DEFAULT_ZOOM_FACTORS;
|
||||
|
||||
|
@ -742,8 +773,7 @@ export function Preferences({
|
|||
: i18n('icu:Preferences__Language__SystemLanguage');
|
||||
}
|
||||
|
||||
pageTitle = i18n('icu:Preferences__button--appearance');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<SettingsRow>
|
||||
<Control
|
||||
icon="Preferences__LanguageIcon"
|
||||
|
@ -933,6 +963,13 @@ export function Preferences({
|
|||
/>
|
||||
</SettingsRow>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--appearance')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Chats) {
|
||||
let spellCheckDirtyText: string | undefined;
|
||||
if (
|
||||
|
@ -946,8 +983,7 @@ export function Preferences({
|
|||
|
||||
const lastSyncDate = new Date(lastSyncTime || 0);
|
||||
|
||||
pageTitle = i18n('icu:Preferences__button--chats');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow title={i18n('icu:Preferences__button--chats')}>
|
||||
<Checkbox
|
||||
|
@ -1058,9 +1094,15 @@ export function Preferences({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--chats')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Calls) {
|
||||
pageTitle = i18n('icu:Preferences__button--calls');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow title={i18n('icu:calling')}>
|
||||
<Checkbox
|
||||
|
@ -1201,9 +1243,15 @@ export function Preferences({
|
|||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--calls')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Notifications) {
|
||||
pageTitle = i18n('icu:Preferences__button--notifications');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow>
|
||||
<Checkbox
|
||||
|
@ -1283,12 +1331,17 @@ export function Preferences({
|
|||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--notifications')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Privacy) {
|
||||
const isCustomDisappearingMessageValue =
|
||||
!DEFAULT_DURATIONS_SET.has(universalExpireTimer);
|
||||
|
||||
pageTitle = i18n('icu:Preferences__button--privacy');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow>
|
||||
<Control
|
||||
|
@ -1527,9 +1580,15 @@ export function Preferences({
|
|||
) : null}
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--privacy')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.DataUsage) {
|
||||
pageTitle = i18n('icu:Preferences__button--data-usage');
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow title={i18n('icu:Preferences__media-auto-download')}>
|
||||
<Checkbox
|
||||
|
@ -1628,9 +1687,15 @@ export function Preferences({
|
|||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--data-usage')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.ChatColor) {
|
||||
pageTitle = i18n('icu:ChatColorPicker__menu-title');
|
||||
pageBackButton = (
|
||||
const backButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
|
@ -1638,7 +1703,7 @@ export function Preferences({
|
|||
type="button"
|
||||
/>
|
||||
);
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<ChatColorPicker
|
||||
customColors={customColors}
|
||||
getConversationsWithCustomColor={getConversationsWithCustomColor}
|
||||
|
@ -1657,6 +1722,14 @@ export function Preferences({
|
|||
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
|
||||
/>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:ChatColorPicker__menu-title')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.PNP) {
|
||||
let sharingDescription: string;
|
||||
|
||||
|
@ -1674,8 +1747,7 @@ export function Preferences({
|
|||
);
|
||||
}
|
||||
|
||||
pageTitle = i18n('icu:Preferences__pnp--page-title');
|
||||
pageBackButton = (
|
||||
const backButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
|
@ -1683,7 +1755,7 @@ export function Preferences({
|
|||
type="button"
|
||||
/>
|
||||
);
|
||||
pageContents = (
|
||||
const pageContents = (
|
||||
<>
|
||||
<SettingsRow
|
||||
title={i18n('icu:Preferences__pnp__sharing--title')}
|
||||
|
@ -1734,7 +1806,7 @@ export function Preferences({
|
|||
onClick:
|
||||
whoCanSeeMe === PhoneNumberSharingMode.Everybody
|
||||
? () =>
|
||||
setToast({ toastType: ToastType.WhoCanFindMeReadOnly })
|
||||
showToast({ toastType: ToastType.WhoCanFindMeReadOnly })
|
||||
: noop,
|
||||
},
|
||||
]}
|
||||
|
@ -1791,25 +1863,43 @@ export function Preferences({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__pnp--page-title')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Backups) {
|
||||
pageTitle = i18n('icu:Preferences__button--backups');
|
||||
pageContents = (
|
||||
<PreferencesBackups
|
||||
i18n={i18n}
|
||||
cloudBackupStatus={cloudBackupStatus}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
locale={resolvedLocale}
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={
|
||||
<PreferencesBackups
|
||||
i18n={i18n}
|
||||
cloudBackupStatus={cloudBackupStatus}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
locale={resolvedLocale}
|
||||
/>
|
||||
}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--backups')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Internal) {
|
||||
pageTitle = i18n('icu:Preferences__button--internal');
|
||||
pageContents = (
|
||||
<PreferencesInternal
|
||||
i18n={i18n}
|
||||
exportLocalBackup={exportLocalBackup}
|
||||
validateBackup={validateBackup}
|
||||
getMessageCountBySchemaVersion={getMessageCountBySchemaVersion}
|
||||
getMessageSampleForSchemaVersion={getMessageSampleForSchemaVersion}
|
||||
content = (
|
||||
<PreferencesContent
|
||||
contents={
|
||||
<PreferencesInternal
|
||||
i18n={i18n}
|
||||
exportLocalBackup={exportLocalBackup}
|
||||
validateBackup={validateBackup}
|
||||
getMessageCountBySchemaVersion={getMessageCountBySchemaVersion}
|
||||
getMessageSampleForSchemaVersion={getMessageSampleForSchemaVersion}
|
||||
/>
|
||||
}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:Preferences__button--internal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1844,6 +1934,49 @@ export function Preferences({
|
|||
</div>
|
||||
) : null}
|
||||
<div className="Preferences__scroll-area">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
'Preferences__profile-chip': true,
|
||||
'Preferences__profile-chip--selected': page === Page.Profile,
|
||||
})}
|
||||
onClick={() => setPage(Page.Profile)}
|
||||
>
|
||||
<div className="Preferences__profile-chip__avatar">
|
||||
<Avatar
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
title={me.title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
/>
|
||||
</div>
|
||||
<div className="Preferences__profile-chip__text-container">
|
||||
<div className="Preferences__profile-chip__name">
|
||||
{me.title}
|
||||
</div>
|
||||
<div className="Preferences__profile-chip__number">
|
||||
{me.phoneNumber}
|
||||
</div>
|
||||
{me.username && (
|
||||
<div className="Preferences__profile-chip__username">
|
||||
{me.username}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="Preferences__profile-chip__qr-icon-container">
|
||||
<div className="Preferences__profile-chip__qr-icon" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
|
@ -1951,32 +2084,11 @@ export function Preferences({
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="Preferences__content">
|
||||
<div className="Preferences__title">
|
||||
{pageBackButton}
|
||||
<div className="Preferences__title--header">{pageTitle}</div>
|
||||
</div>
|
||||
<div className="Preferences__page">
|
||||
<div className="Preferences__settings-pane-spacer" />
|
||||
<div className="Preferences__settings-pane" ref={settingsPaneRef}>
|
||||
{pageContents}
|
||||
</div>
|
||||
<div className="Preferences__settings-pane-spacer" />
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
<ToastManager
|
||||
OS="unused"
|
||||
hideToast={() => setToast(undefined)}
|
||||
i18n={i18n}
|
||||
onShowDebugLog={shouldNeverBeCalled}
|
||||
onUndoArchive={shouldNeverBeCalled}
|
||||
openFileInFolder={shouldNeverBeCalled}
|
||||
showAttachmentNotAvailableModal={shouldNeverBeCalled}
|
||||
toast={toast}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
isInFullScreenCall={false}
|
||||
/>
|
||||
{renderToastManager({
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
})}
|
||||
</FunEmojiLocalizationProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1989,3 +2101,31 @@ function localizeDefault(i18n: LocalizerType, deviceLabel: string): string {
|
|||
)
|
||||
: deviceLabel;
|
||||
}
|
||||
|
||||
export function PreferencesContent({
|
||||
backButton,
|
||||
contents,
|
||||
contentsRef,
|
||||
title,
|
||||
}: {
|
||||
backButton?: JSX.Element | undefined;
|
||||
contents: JSX.Element | undefined;
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
title: string | undefined;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="Preferences__content">
|
||||
<div className="Preferences__title">
|
||||
{backButton}
|
||||
<div className="Preferences__title--header">{title}</div>
|
||||
</div>
|
||||
<div className="Preferences__page">
|
||||
<div className="Preferences__settings-pane-spacer" />
|
||||
<div className="Preferences__settings-pane" ref={contentsRef}>
|
||||
{contents}
|
||||
</div>
|
||||
<div className="Preferences__settings-pane-spacer" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import casual from 'casual';
|
|||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import type { PropsType } from './ProfileEditor';
|
||||
import { ProfileEditor } from './ProfileEditor';
|
||||
import { EditUsernameModalBody } from './EditUsernameModalBody';
|
||||
import { EditState, ProfileEditor } from './ProfileEditor';
|
||||
import { UsernameEditor } from './UsernameEditor';
|
||||
import {
|
||||
UsernameEditState,
|
||||
UsernameLinkState,
|
||||
|
@ -51,6 +51,7 @@ export default {
|
|||
conversationId: generateUuid(),
|
||||
color: getRandomColor(),
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
editState: EditState.None,
|
||||
familyName: casual.last_name,
|
||||
firstName: casual.first_name,
|
||||
i18n,
|
||||
|
@ -65,7 +66,6 @@ export default {
|
|||
userAvatarData: [],
|
||||
username: undefined,
|
||||
|
||||
onEditStateChanged: action('onEditStateChanged'),
|
||||
onProfileChanged: action('onProfileChanged'),
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
|
@ -83,12 +83,9 @@ export default {
|
|||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
function renderEditUsernameModalBody(props: {
|
||||
isRootModal: boolean;
|
||||
onClose: () => void;
|
||||
}): JSX.Element {
|
||||
function renderUsernameEditor(props: { onClose: () => void }): JSX.Element {
|
||||
return (
|
||||
<EditUsernameModalBody
|
||||
<UsernameEditor
|
||||
i18n={i18n}
|
||||
minNickname={3}
|
||||
maxNickname={20}
|
||||
|
@ -111,13 +108,16 @@ const Template: StoryFn<PropsType> = args => {
|
|||
const [emojiSkinToneDefault, setEmojiSkinToneDefault] = useState(
|
||||
EmojiSkinTone.None
|
||||
);
|
||||
const [editState, setEditState] = useState(args.editState);
|
||||
|
||||
return (
|
||||
<ProfileEditor
|
||||
{...args}
|
||||
editState={editState}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
renderEditUsernameModalBody={renderEditUsernameModalBody}
|
||||
renderUsernameEditor={renderUsernameEditor}
|
||||
setEditState={setEditState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,40 +10,23 @@ import React, {
|
|||
} from 'react';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type {
|
||||
AvatarDataType,
|
||||
AvatarUpdateOptionsType,
|
||||
DeleteAvatarFromDiskActionType,
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar';
|
||||
import { AvatarEditor } from './AvatarEditor';
|
||||
import { AvatarPreview } from './AvatarPreview';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ConfirmDiscardDialog } from './ConfirmDiscardDialog';
|
||||
import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
|
||||
import { EmojiButton, EmojiButtonVariant } from './emoji/EmojiButton';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import { Input } from './Input';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
||||
import type {
|
||||
ProfileDataType,
|
||||
SaveAttachmentActionCreatorType,
|
||||
} from '../state/ducks/conversations';
|
||||
import { UsernameEditState } from '../state/ducks/usernameEnums';
|
||||
import type { UsernameLinkState } from '../state/ducks/usernameEnums';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import type { ShowToastAction } from '../state/ducks/toast';
|
||||
import { getEmojiData, unifiedToEmoji } from './emoji/lib';
|
||||
import { assertDev, strictAssert } from '../util/assert';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { UsernameLinkModalBody } from './UsernameLinkModalBody';
|
||||
import { UsernameLinkEditor } from './UsernameLinkEditor';
|
||||
import {
|
||||
ConversationDetailsIcon,
|
||||
IconType,
|
||||
|
@ -54,7 +37,6 @@ import { Tooltip, TooltipPlacement } from './Tooltip';
|
|||
import { offsetDistanceModifier } from '../util/popperUtil';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji';
|
||||
import type { EmojiVariantKey } from './fun/data/emojis';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiParentKeyByEnglishShortName,
|
||||
|
@ -66,9 +48,30 @@ import {
|
|||
} from './fun/data/emojis';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis';
|
||||
import { isFunPickerEnabled } from './fun/isFunPickerEnabled';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer';
|
||||
import { PreferencesContent } from './Preferences';
|
||||
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type {
|
||||
AvatarDataType,
|
||||
AvatarUpdateOptionsType,
|
||||
DeleteAvatarFromDiskActionType,
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar';
|
||||
import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type {
|
||||
ProfileDataType,
|
||||
SaveAttachmentActionCreatorType,
|
||||
} from '../state/ducks/conversations';
|
||||
import type { UsernameLinkState } from '../state/ducks/usernameEnums';
|
||||
import type { ShowToastAction } from '../state/ducks/toast';
|
||||
import type { EmojiVariantKey } from './fun/data/emojis';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
|
||||
export enum EditState {
|
||||
None = 'None',
|
||||
|
@ -80,36 +83,33 @@ export enum EditState {
|
|||
}
|
||||
|
||||
type PropsExternalType = {
|
||||
onEditStateChanged: (editState: EditState) => unknown;
|
||||
onProfileChanged: (
|
||||
profileData: ProfileDataType,
|
||||
avatarUpdateOptions: AvatarUpdateOptionsType
|
||||
) => unknown;
|
||||
renderEditUsernameModalBody: (props: {
|
||||
isRootModal: boolean;
|
||||
onClose: () => void;
|
||||
}) => JSX.Element;
|
||||
renderUsernameEditor: (props: { onClose: () => void }) => JSX.Element;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
aboutEmoji?: string;
|
||||
aboutText?: string;
|
||||
profileAvatarUrl?: string;
|
||||
color?: AvatarColorType;
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
conversationId: string;
|
||||
familyName?: string;
|
||||
firstName: string;
|
||||
hasCompletedUsernameLinkOnboarding: boolean;
|
||||
i18n: LocalizerType;
|
||||
editState: EditState;
|
||||
profileAvatarUrl?: string;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
username?: string;
|
||||
initialEditState?: EditState;
|
||||
usernameCorrupted: boolean;
|
||||
usernameEditState: UsernameEditState;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
usernameLinkColor?: number;
|
||||
usernameLink?: string;
|
||||
usernameLinkColor?: number;
|
||||
usernameLinkCorrupted: boolean;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
} & Pick<EmojiButtonProps, 'recentEmojis' | 'emojiSkinToneDefault'>;
|
||||
|
||||
type PropsActionType = {
|
||||
|
@ -121,9 +121,9 @@ type PropsActionType = {
|
|||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
setUsernameEditState: (editState: UsernameEditState) => void;
|
||||
setUsernameLinkColor: (color: number) => void;
|
||||
toggleProfileEditor: () => void;
|
||||
resetUsernameLink: () => void;
|
||||
deleteUsername: () => void;
|
||||
setEditState: (editState: EditState) => void;
|
||||
showToast: ShowToastAction;
|
||||
openUsernameReservationModal: () => void;
|
||||
};
|
||||
|
@ -178,26 +178,26 @@ export function ProfileEditor({
|
|||
aboutText,
|
||||
color,
|
||||
conversationId,
|
||||
contentsRef,
|
||||
deleteAvatarFromDisk,
|
||||
deleteUsername,
|
||||
familyName,
|
||||
firstName,
|
||||
hasCompletedUsernameLinkOnboarding,
|
||||
i18n,
|
||||
initialEditState = EditState.None,
|
||||
editState,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
onEditStateChanged,
|
||||
onProfileChanged,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
openUsernameReservationModal,
|
||||
profileAvatarUrl,
|
||||
recentEmojis,
|
||||
renderEditUsernameModalBody,
|
||||
renderUsernameEditor,
|
||||
replaceAvatar,
|
||||
resetUsernameLink,
|
||||
toggleProfileEditor,
|
||||
saveAttachment,
|
||||
saveAvatarToDisk,
|
||||
setEditState,
|
||||
setUsernameEditState,
|
||||
setUsernameLinkColor,
|
||||
showToast,
|
||||
|
@ -212,10 +212,21 @@ export function ProfileEditor({
|
|||
usernameLinkCorrupted,
|
||||
}: PropsType): JSX.Element {
|
||||
const focusInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [editState, setEditState] = useState<EditState>(initialEditState);
|
||||
const [confirmDiscardAction, setConfirmDiscardAction] = useState<
|
||||
(() => unknown) | undefined
|
||||
>(undefined);
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'ProfileEditor',
|
||||
tryClose,
|
||||
});
|
||||
|
||||
const TITLES_BY_EDIT_STATE: Record<EditState, string | undefined> = {
|
||||
[EditState.BetterAvatar]: i18n('icu:ProfileEditorModal--avatar'),
|
||||
[EditState.Bio]: i18n('icu:ProfileEditorModal--about'),
|
||||
[EditState.None]: i18n('icu:ProfileEditorModal--profile'),
|
||||
[EditState.ProfileName]: i18n('icu:ProfileEditorModal--name'),
|
||||
[EditState.Username]: i18n('icu:ProfileEditorModal--username'),
|
||||
[EditState.UsernameLink]: i18n('icu:ProfileEditorModal--sharing'),
|
||||
};
|
||||
|
||||
// This is here to avoid component re-render jitters in the time it takes
|
||||
// redux to come back with the correct state
|
||||
|
@ -265,8 +276,7 @@ export function ProfileEditor({
|
|||
// To make AvatarEditor re-render less often
|
||||
const handleBack = useCallback(() => {
|
||||
setEditState(EditState.None);
|
||||
onEditStateChanged(EditState.None);
|
||||
}, [setEditState, onEditStateChanged]);
|
||||
}, [setEditState]);
|
||||
|
||||
const handleEmojiPickerOpenChange = useCallback((open: boolean) => {
|
||||
setEmojiPickerOpen(open);
|
||||
|
@ -306,7 +316,6 @@ export function ProfileEditor({
|
|||
setStartingAvatarUrl(undefined);
|
||||
|
||||
setAvatarBuffer(avatar);
|
||||
setEditState(EditState.None);
|
||||
onProfileChanged(
|
||||
{
|
||||
...stagedProfile,
|
||||
|
@ -321,8 +330,9 @@ export function ProfileEditor({
|
|||
}
|
||||
);
|
||||
setOldAvatarBuffer(avatar);
|
||||
handleBack();
|
||||
},
|
||||
[onProfileChanged, stagedProfile, oldAvatarBuffer]
|
||||
[handleBack, oldAvatarBuffer, onProfileChanged, stagedProfile]
|
||||
);
|
||||
|
||||
const getFullNameText = () => {
|
||||
|
@ -339,17 +349,6 @@ export function ProfileEditor({
|
|||
focusNode.setSelectionRange(focusNode.value.length, focusNode.value.length);
|
||||
}, [editState]);
|
||||
|
||||
useEffect(() => {
|
||||
onEditStateChanged(editState);
|
||||
}, [editState, onEditStateChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
// If we opened at a nested sub-modal - close when leaving it.
|
||||
if (editState === EditState.None && initialEditState !== EditState.None) {
|
||||
toggleProfileEditor();
|
||||
}
|
||||
}, [initialEditState, editState, toggleProfileEditor]);
|
||||
|
||||
// To make AvatarEditor re-render less often
|
||||
const handleAvatarLoaded = useCallback(
|
||||
(avatar: Uint8Array) => {
|
||||
|
@ -359,6 +358,25 @@ export function ProfileEditor({
|
|||
[setAvatarBuffer, setOldAvatarBuffer]
|
||||
);
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
const hasNameChanges =
|
||||
stagedProfile.familyName !== fullName.familyName ||
|
||||
stagedProfile.firstName !== fullName.firstName;
|
||||
const hasAboutChanges =
|
||||
stagedProfile.aboutText !== fullBio.aboutText ||
|
||||
stagedProfile.aboutEmoji !== fullBio.aboutEmoji;
|
||||
const onDiscard = () => {
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
...fullName,
|
||||
...fullBio,
|
||||
}));
|
||||
};
|
||||
|
||||
confirmDiscardIf(hasNameChanges || hasAboutChanges, onDiscard);
|
||||
}, [confirmDiscardIf, stagedProfile, fullName, fullBio, setStagedProfile]);
|
||||
tryClose.current = onTryClose;
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
if (editState === EditState.BetterAvatar) {
|
||||
|
@ -414,29 +432,8 @@ export function ProfileEditor({
|
|||
placeholder={i18n('icu:ProfileEditor--last-name')}
|
||||
value={stagedProfile.familyName}
|
||||
/>
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const handleCancel = () => {
|
||||
handleBack();
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
familyName,
|
||||
firstName,
|
||||
}));
|
||||
};
|
||||
|
||||
const hasChanges =
|
||||
stagedProfile.familyName !== fullName.familyName ||
|
||||
stagedProfile.firstName !== fullName.firstName;
|
||||
if (hasChanges) {
|
||||
setConfirmDiscardAction(() => handleCancel);
|
||||
} else {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
<div className="ProfileEditor__button-footer">
|
||||
<Button onClick={handleBack} variant={ButtonVariant.Secondary}>
|
||||
{i18n('icu:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -451,12 +448,14 @@ export function ProfileEditor({
|
|||
});
|
||||
|
||||
onProfileChanged(stagedProfile, { keepAvatar: true });
|
||||
handleBack();
|
||||
|
||||
// Delay navigation until setFullName resolves and we are no longer dirty
|
||||
setTimeout(() => handleBack(), 500);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:save')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (editState === EditState.Bio) {
|
||||
|
@ -565,28 +564,8 @@ export function ProfileEditor({
|
|||
);
|
||||
})}
|
||||
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const handleCancel = () => {
|
||||
handleBack();
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
...fullBio,
|
||||
}));
|
||||
};
|
||||
|
||||
const hasChanges =
|
||||
stagedProfile.aboutText !== fullBio.aboutText ||
|
||||
stagedProfile.aboutEmoji !== fullBio.aboutEmoji;
|
||||
if (hasChanges) {
|
||||
setConfirmDiscardAction(() => handleCancel);
|
||||
} else {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
<div className="ProfileEditor__button-footer">
|
||||
<Button onClick={handleBack} variant={ButtonVariant.Secondary}>
|
||||
{i18n('icu:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -598,22 +577,23 @@ export function ProfileEditor({
|
|||
});
|
||||
|
||||
onProfileChanged(stagedProfile, { keepAvatar: true });
|
||||
handleBack();
|
||||
|
||||
// Delay navigation until setFullBio resolves and we are no longer dirty
|
||||
setTimeout(() => handleBack(), 500);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:save')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (editState === EditState.Username) {
|
||||
content = renderEditUsernameModalBody({
|
||||
isRootModal: initialEditState === editState,
|
||||
onClose: () => setEditState(EditState.None),
|
||||
content = renderUsernameEditor({
|
||||
onClose: handleBack,
|
||||
});
|
||||
} else if (editState === EditState.UsernameLink) {
|
||||
content = (
|
||||
<UsernameLinkModalBody
|
||||
<UsernameLinkEditor
|
||||
i18n={i18n}
|
||||
link={usernameLink}
|
||||
username={username ?? ''}
|
||||
|
@ -838,6 +818,16 @@ export function ProfileEditor({
|
|||
throw missingCaseError(editState);
|
||||
}
|
||||
|
||||
const backButton =
|
||||
editState !== EditState.None ? (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={handleBack}
|
||||
type="button"
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{usernameEditState === UsernameEditState.ConfirmingDelete && (
|
||||
|
@ -859,18 +849,12 @@ export function ProfileEditor({
|
|||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{confirmDiscardAction && (
|
||||
<ConfirmDiscardDialog
|
||||
i18n={i18n}
|
||||
onDiscard={confirmDiscardAction}
|
||||
onClose={() => setConfirmDiscardAction(undefined)}
|
||||
/>
|
||||
)}
|
||||
{confirmDiscardModal}
|
||||
|
||||
{isResettingUsernameLink && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="UsernameLinkModal__error"
|
||||
dialogName="ProfileEditor__resettingUsername"
|
||||
onClose={() => setIsResettingUsernameLink(false)}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
|
@ -910,7 +894,12 @@ export function ProfileEditor({
|
|||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
<div className="ProfileEditor">{content}</div>
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={<div className="ProfileEditor">{content}</div>}
|
||||
contentsRef={contentsRef}
|
||||
title={TITLES_BY_EDIT_STATE[editState]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from './Modal';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import type { PropsType as ProfileEditorPropsType } from './ProfileEditor';
|
||||
import { ProfileEditor, EditState } from './ProfileEditor';
|
||||
import type { ProfileDataType } from '../state/ducks/conversations';
|
||||
import type { AvatarUpdateOptionsType } from '../types/Avatar';
|
||||
|
||||
export type PropsDataType = {
|
||||
hasError: boolean;
|
||||
} & Pick<ProfileEditorPropsType, 'renderEditUsernameModalBody'>;
|
||||
|
||||
type PropsType = {
|
||||
myProfileChanged: (
|
||||
profileData: ProfileDataType,
|
||||
avatarUpdateOptions: AvatarUpdateOptionsType
|
||||
) => unknown;
|
||||
toggleProfileEditor: () => unknown;
|
||||
toggleProfileEditorHasError: () => unknown;
|
||||
} & PropsDataType &
|
||||
Omit<ProfileEditorPropsType, 'onEditStateChanged' | 'onProfileChanged'>;
|
||||
|
||||
export function ProfileEditorModal({
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
color,
|
||||
conversationId,
|
||||
deleteAvatarFromDisk,
|
||||
deleteUsername,
|
||||
familyName,
|
||||
firstName,
|
||||
hasCompletedUsernameLinkOnboarding,
|
||||
hasError,
|
||||
i18n,
|
||||
initialEditState,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
myProfileChanged,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
openUsernameReservationModal,
|
||||
profileAvatarUrl,
|
||||
recentEmojis,
|
||||
renderEditUsernameModalBody,
|
||||
replaceAvatar,
|
||||
resetUsernameLink,
|
||||
saveAttachment,
|
||||
saveAvatarToDisk,
|
||||
setUsernameEditState,
|
||||
setUsernameLinkColor,
|
||||
showToast,
|
||||
emojiSkinToneDefault,
|
||||
toggleProfileEditor,
|
||||
toggleProfileEditorHasError,
|
||||
userAvatarData,
|
||||
username,
|
||||
usernameCorrupted,
|
||||
usernameEditState,
|
||||
usernameLink,
|
||||
usernameLinkColor,
|
||||
usernameLinkCorrupted,
|
||||
usernameLinkState,
|
||||
}: PropsType): JSX.Element {
|
||||
const MODAL_TITLES_BY_EDIT_STATE: Record<EditState, string | undefined> = {
|
||||
[EditState.BetterAvatar]: i18n('icu:ProfileEditorModal--avatar'),
|
||||
[EditState.Bio]: i18n('icu:ProfileEditorModal--about'),
|
||||
[EditState.None]: i18n('icu:ProfileEditorModal--profile'),
|
||||
[EditState.ProfileName]: i18n('icu:ProfileEditorModal--name'),
|
||||
[EditState.Username]: i18n('icu:ProfileEditorModal--username'),
|
||||
[EditState.UsernameLink]: undefined,
|
||||
};
|
||||
|
||||
const [modalTitle, setModalTitle] = useState(
|
||||
MODAL_TITLES_BY_EDIT_STATE[EditState.None]
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
dialogName="ProfileEditorModal.error"
|
||||
cancelText={i18n('icu:Confirmation--confirm')}
|
||||
i18n={i18n}
|
||||
onClose={toggleProfileEditorHasError}
|
||||
>
|
||||
{i18n('icu:ProfileEditorModal--error')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="ProfileEditorModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={toggleProfileEditor}
|
||||
title={modalTitle}
|
||||
>
|
||||
<ProfileEditor
|
||||
aboutEmoji={aboutEmoji}
|
||||
aboutText={aboutText}
|
||||
color={color}
|
||||
conversationId={conversationId}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
deleteUsername={deleteUsername}
|
||||
familyName={familyName}
|
||||
firstName={firstName}
|
||||
hasCompletedUsernameLinkOnboarding={hasCompletedUsernameLinkOnboarding}
|
||||
i18n={i18n}
|
||||
initialEditState={initialEditState}
|
||||
markCompletedUsernameLinkOnboarding={
|
||||
markCompletedUsernameLinkOnboarding
|
||||
}
|
||||
onEditStateChanged={editState => {
|
||||
setModalTitle(MODAL_TITLES_BY_EDIT_STATE[editState]);
|
||||
}}
|
||||
onProfileChanged={myProfileChanged}
|
||||
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
|
||||
openUsernameReservationModal={openUsernameReservationModal}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
recentEmojis={recentEmojis}
|
||||
renderEditUsernameModalBody={renderEditUsernameModalBody}
|
||||
replaceAvatar={replaceAvatar}
|
||||
resetUsernameLink={resetUsernameLink}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
setUsernameEditState={setUsernameEditState}
|
||||
setUsernameLinkColor={setUsernameLinkColor}
|
||||
showToast={showToast}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
toggleProfileEditor={toggleProfileEditor}
|
||||
userAvatarData={userAvatarData}
|
||||
username={username}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
usernameEditState={usernameEditState}
|
||||
usernameLink={usernameLink}
|
||||
usernameLinkColor={usernameLinkColor}
|
||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||
usernameLinkState={usernameLinkState}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -151,7 +151,7 @@ export function TextStoryCreator({
|
|||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'SendStoryModal',
|
||||
name: 'TextStoryCreator',
|
||||
tryClose,
|
||||
});
|
||||
const onTryClose = useCallback(() => {
|
||||
|
|
|
@ -7,8 +7,8 @@ import type { Meta, StoryFn } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
|
||||
import type { PropsType } from './EditUsernameModalBody';
|
||||
import { EditUsernameModalBody } from './EditUsernameModalBody';
|
||||
import type { PropsType } from './UsernameEditor';
|
||||
import { UsernameEditor } from './UsernameEditor';
|
||||
import {
|
||||
UsernameReservationState as State,
|
||||
UsernameReservationError,
|
||||
|
@ -23,8 +23,8 @@ const DEFAULT_RESERVATION: UsernameReservationType = {
|
|||
};
|
||||
|
||||
export default {
|
||||
component: EditUsernameModalBody,
|
||||
title: 'Components/EditUsernameModalBody',
|
||||
component: UsernameEditor,
|
||||
title: 'Components/UsernameEditor',
|
||||
argTypes: {
|
||||
usernameCorrupted: {
|
||||
type: { name: 'boolean' },
|
||||
|
@ -54,7 +54,6 @@ export default {
|
|||
},
|
||||
},
|
||||
args: {
|
||||
isRootModal: false,
|
||||
usernameCorrupted: false,
|
||||
currentUsername: undefined,
|
||||
state: State.Open,
|
||||
|
@ -86,7 +85,7 @@ const Template: StoryFn<ArgsType> = args => {
|
|||
hash: new Uint8Array(),
|
||||
};
|
||||
}
|
||||
return <EditUsernameModalBody {...args} reservation={reservation} />;
|
||||
return <UsernameEditor {...args} reservation={reservation} />;
|
||||
};
|
||||
|
||||
export const WithoutUsername = Template.bind({});
|
|
@ -1,8 +1,15 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
|
@ -22,6 +29,7 @@ import { Input } from './Input';
|
|||
import { Spinner } from './Spinner';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
|
||||
export type PropsDataType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
|
@ -47,7 +55,6 @@ export type ActionPropsDataType = Readonly<{
|
|||
|
||||
export type ExternalPropsDataType = Readonly<{
|
||||
onClose(): void;
|
||||
isRootModal: boolean;
|
||||
}>;
|
||||
|
||||
export type PropsType = PropsDataType &
|
||||
|
@ -62,7 +69,7 @@ enum UpdateState {
|
|||
|
||||
const DISCRIMINATOR_MAX_LENGTH = 9;
|
||||
|
||||
export function EditUsernameModalBody({
|
||||
export function UsernameEditor({
|
||||
i18n,
|
||||
currentUsername,
|
||||
usernameCorrupted,
|
||||
|
@ -77,7 +84,6 @@ export function EditUsernameModalBody({
|
|||
error,
|
||||
state,
|
||||
recoveredUsername,
|
||||
isRootModal,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
const currentNickname = useMemo(() => {
|
||||
|
@ -155,16 +161,12 @@ export function EditUsernameModalBody({
|
|||
|
||||
useEffect(() => {
|
||||
if (state === UsernameReservationState.Closed) {
|
||||
onClose();
|
||||
setTimeout(() => onClose(), 500);
|
||||
}
|
||||
}, [state, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
state === UsernameReservationState.Closed &&
|
||||
recoveredUsername &&
|
||||
isRootModal
|
||||
) {
|
||||
if (state === UsernameReservationState.Closed && recoveredUsername) {
|
||||
showToast({
|
||||
toastType: ToastType.UsernameRecovered,
|
||||
parameters: {
|
||||
|
@ -172,7 +174,7 @@ export function EditUsernameModalBody({
|
|||
},
|
||||
});
|
||||
}
|
||||
}, [state, recoveredUsername, showToast, isRootModal]);
|
||||
}, [state, recoveredUsername, showToast]);
|
||||
|
||||
const errorString = useMemo(() => {
|
||||
if (!error) {
|
||||
|
@ -284,6 +286,31 @@ export function EditUsernameModalBody({
|
|||
setIsLearnMoreVisible(true);
|
||||
}, []);
|
||||
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'UsernameEditor',
|
||||
tryClose,
|
||||
});
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
const onDiscard = noop;
|
||||
confirmDiscardIf(
|
||||
Boolean(
|
||||
currentNickname !== nickname ||
|
||||
(customDiscriminator && customDiscriminator !== currentDiscriminator)
|
||||
),
|
||||
onDiscard
|
||||
);
|
||||
}, [
|
||||
confirmDiscardIf,
|
||||
currentDiscriminator,
|
||||
currentNickname,
|
||||
customDiscriminator,
|
||||
nickname,
|
||||
]);
|
||||
tryClose.current = onTryClose;
|
||||
|
||||
let title = i18n('icu:ProfileEditor--username--title');
|
||||
if (nickname && discriminator) {
|
||||
title = `${nickname}.${discriminator}`;
|
||||
|
@ -291,21 +318,20 @@ export function EditUsernameModalBody({
|
|||
|
||||
const learnMoreTitle = (
|
||||
<>
|
||||
<i className="EditUsernameModalBody__learn-more__hashtag" />
|
||||
<i className="UsernameEditor__learn-more__hashtag" />
|
||||
{i18n('icu:EditUsernameModalBody__learn-more__title')}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="EditUsernameModalBody__header">
|
||||
<div className="EditUsernameModalBody__header__large-at" />
|
||||
<div className="UsernameEditor__header">
|
||||
<div className="UsernameEditor__header__large-at" />
|
||||
|
||||
<div className="EditUsernameModalBody__header__preview">{title}</div>
|
||||
<div className="UsernameEditor__header__preview">{title}</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
moduleClassName="EditUsernameModalBody__input"
|
||||
moduleClassName="UsernameEditor__input"
|
||||
i18n={i18n}
|
||||
disableSpellcheck
|
||||
disabled={isConfirming}
|
||||
|
@ -317,9 +343,9 @@ export function EditUsernameModalBody({
|
|||
{isReserving && <Spinner size="16px" svgSize="small" />}
|
||||
{isDiscriminatorVisible ? (
|
||||
<>
|
||||
<div className="EditUsernameModalBody__divider" />
|
||||
<div className="UsernameEditor__divider" />
|
||||
<AutoSizeInput
|
||||
moduleClassName="EditUsernameModalBody__discriminator"
|
||||
moduleClassName="UsernameEditor__discriminator"
|
||||
disableSpellcheck
|
||||
disabled={isConfirming}
|
||||
value={discriminator}
|
||||
|
@ -330,28 +356,26 @@ export function EditUsernameModalBody({
|
|||
</>
|
||||
) : null}
|
||||
</Input>
|
||||
|
||||
{errorString && (
|
||||
<div className="EditUsernameModalBody__error">{errorString}</div>
|
||||
<div className="UsernameEditor__error">{errorString}</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'EditUsernameModalBody__info',
|
||||
!errorString ? 'EditUsernameModalBody__info--no-error' : undefined
|
||||
'UsernameEditor__info',
|
||||
!errorString ? 'UsernameEditor__info--no-error' : undefined
|
||||
)}
|
||||
>
|
||||
{i18n('icu:EditUsernameModalBody__username-helper')}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="EditUsernameModalBody__learn-more-button"
|
||||
className="UsernameEditor__learn-more-button"
|
||||
onClick={onLearnMore}
|
||||
>
|
||||
{i18n('icu:EditUsernameModalBody__learn-more')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal.ButtonFooter>
|
||||
<div className="UsernameEditor__button-footer">
|
||||
<Button
|
||||
disabled={isConfirming}
|
||||
onClick={onCancel}
|
||||
|
@ -366,32 +390,33 @@ export function EditUsernameModalBody({
|
|||
i18n('icu:save')
|
||||
)}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
|
||||
{confirmDiscardModal}
|
||||
|
||||
{isLearnMoreVisible && (
|
||||
<Modal
|
||||
modalName="EditUsernamModalBody.LearnMore"
|
||||
moduleClassName="EditUsernameModalBody__learn-more"
|
||||
modalName="UsernameEditor.LearnMore"
|
||||
moduleClassName="UsernameEditor__learn-more"
|
||||
i18n={i18n}
|
||||
onClose={() => setIsLearnMoreVisible(false)}
|
||||
title={learnMoreTitle}
|
||||
>
|
||||
{i18n('icu:EditUsernameModalBody__learn-more__body')}
|
||||
|
||||
<Modal.ButtonFooter>
|
||||
<div className="UsernameEditor__button-footer">
|
||||
<Button
|
||||
onClick={() => setIsLearnMoreVisible(false)}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('icu:ok')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{error === UsernameReservationError.General && (
|
||||
<ConfirmationDialog
|
||||
dialogName="EditUsernameModalBody.generalError"
|
||||
dialogName="UsernameEditor.generalError"
|
||||
cancelText={i18n('icu:ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
|
@ -400,10 +425,9 @@ export function EditUsernameModalBody({
|
|||
{i18n('icu:ProfileEditor--username--general-error')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{error === UsernameReservationError.ConflictOrGone && (
|
||||
<ConfirmationDialog
|
||||
dialogName="EditUsernameModalBody.conflictOrGone"
|
||||
dialogName="UsernameEditor.conflictOrGone"
|
||||
cancelText={i18n('icu:ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
|
@ -418,10 +442,9 @@ export function EditUsernameModalBody({
|
|||
})}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isConfirmingSave && (
|
||||
<ConfirmationDialog
|
||||
dialogName="EditUsernameModalBody.confirmChange"
|
||||
dialogName="UsernameEditor.confirmChange"
|
||||
cancelText={i18n('icu:cancel')}
|
||||
actions={[
|
||||
{
|
||||
|
@ -438,10 +461,9 @@ export function EditUsernameModalBody({
|
|||
{i18n('icu:EditUsernameModalBody__change-confirmation')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isConfirmingReset && (
|
||||
<ConfirmationDialog
|
||||
dialogName="EditUsernameModalBody.confirmReset"
|
||||
dialogName="UsernameEditor.confirmReset"
|
||||
cancelText={i18n('icu:cancel')}
|
||||
actions={[
|
||||
{
|
|
@ -8,12 +8,12 @@ import { action } from '@storybook/addon-actions';
|
|||
import { UsernameLinkState } from '../state/ducks/usernameEnums';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import type { PropsType } from './UsernameLinkModalBody';
|
||||
import type { PropsType } from './UsernameLinkEditor';
|
||||
import {
|
||||
UsernameLinkModalBody,
|
||||
UsernameLinkEditor,
|
||||
PRINT_WIDTH,
|
||||
PRINT_HEIGHT,
|
||||
} from './UsernameLinkModalBody';
|
||||
} from './UsernameLinkEditor';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
const ColorEnum = Proto.AccountRecord.UsernameLink.Color;
|
||||
|
@ -21,8 +21,8 @@ const ColorEnum = Proto.AccountRecord.UsernameLink.Color;
|
|||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
component: UsernameLinkModalBody,
|
||||
title: 'Components/UsernameLinkModalBody',
|
||||
component: UsernameLinkEditor,
|
||||
title: 'Components/UsernameLinkEditor',
|
||||
argTypes: {
|
||||
link: {
|
||||
control: { type: 'text' },
|
||||
|
@ -92,7 +92,7 @@ const Template: StoryFn<PropsType> = args => {
|
|||
return (
|
||||
<>
|
||||
<Modal modalName="story" i18n={i18n} hasXButton>
|
||||
<UsernameLinkModalBody {...args} saveAttachment={saveAttachment} />
|
||||
<UsernameLinkEditor {...args} saveAttachment={saveAttachment} />
|
||||
</Modal>
|
||||
{attachment && (
|
||||
<img
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import classnames from 'classnames';
|
||||
import { changeDpiBlob } from 'changedpi';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import type { SaveAttachmentActionCreatorType } from '../state/ducks/conversations';
|
||||
|
@ -18,10 +19,10 @@ import { drop } from '../util/drop';
|
|||
import { splitText } from '../util/splitText';
|
||||
import { loadImage } from '../util/loadImage';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { Spinner } from './Spinner';
|
||||
import { BrandedQRCode } from './BrandedQRCode';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
|
@ -63,7 +64,7 @@ export const COLOR_MAP: ReadonlyMap<number, ColorMapEntryType> = new Map([
|
|||
[ColorEnum.PURPLE, { fg: '#7651c5', bg: '#a183d4', tint: '#f5f3fb' }],
|
||||
]);
|
||||
|
||||
const CLASS = 'UsernameLinkModalBody';
|
||||
const CLASS = 'UsernameLinkEditor';
|
||||
|
||||
export const PRINT_WIDTH = 424;
|
||||
export const PRINT_HEIGHT = 576;
|
||||
|
@ -396,25 +397,25 @@ function UsernameLinkColors({
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<Modal.ButtonFooter>
|
||||
<div className="UsernameLinkEditor__button-footer">
|
||||
<Button variant={ButtonVariant.Secondary} onClick={onCancel}>
|
||||
{i18n('icu:cancel')}
|
||||
</Button>
|
||||
<Button variant={ButtonVariant.Primary} onClick={onSave}>
|
||||
{i18n('icu:save')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
enum ResetModalVisibility {
|
||||
enum RecoveryModalVisibility {
|
||||
NotMounted = 'NotMounted',
|
||||
Closed = 'Closed',
|
||||
Open = 'Open',
|
||||
}
|
||||
|
||||
export function UsernameLinkModalBody({
|
||||
export function UsernameLinkEditor({
|
||||
i18n,
|
||||
link,
|
||||
username,
|
||||
|
@ -432,8 +433,8 @@ export function UsernameLinkModalBody({
|
|||
const [pngData, setPngData] = useState<Uint8Array | undefined>();
|
||||
const [showColors, setShowColors] = useState(false);
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
const [resetModalVisibility, setResetModalVisibility] = useState(
|
||||
ResetModalVisibility.NotMounted
|
||||
const [recoveryModalVisibility, setRecoveryModalVisibility] = useState(
|
||||
RecoveryModalVisibility.NotMounted
|
||||
);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [colorId, setColorId] = useState(initialColorId);
|
||||
|
@ -538,11 +539,6 @@ export function UsernameLinkModalBody({
|
|||
setShowColors(false);
|
||||
}, [setUsernameLinkColor, colorId]);
|
||||
|
||||
const onUsernameLinkColorCancel = useCallback(() => {
|
||||
setShowColors(false);
|
||||
setColorId(initialColorId);
|
||||
}, [initialColorId]);
|
||||
|
||||
// Reset sub modal
|
||||
|
||||
const onClickReset = useCallback(() => {
|
||||
|
@ -581,24 +577,51 @@ export function UsernameLinkModalBody({
|
|||
setShowError(true);
|
||||
}, [usernameLinkState]);
|
||||
|
||||
const onResetModalClose = useCallback(() => {
|
||||
setResetModalVisibility(ResetModalVisibility.Closed);
|
||||
const onRecoveryModalClose = useCallback(() => {
|
||||
setRecoveryModalVisibility(RecoveryModalVisibility.Closed);
|
||||
}, []);
|
||||
|
||||
const isReady = usernameLinkState === UsernameLinkState.Ready;
|
||||
const isResettingLink = usernameLinkCorrupted || !isReady;
|
||||
|
||||
useEffect(() => {
|
||||
setResetModalVisibility(x => {
|
||||
setRecoveryModalVisibility(x => {
|
||||
// Initial mount shouldn't show the modal
|
||||
if (x === ResetModalVisibility.NotMounted || isResettingLink) {
|
||||
return ResetModalVisibility.Closed;
|
||||
if (x === RecoveryModalVisibility.NotMounted || isResettingLink) {
|
||||
return RecoveryModalVisibility.Closed;
|
||||
}
|
||||
|
||||
return ResetModalVisibility.Open;
|
||||
return RecoveryModalVisibility.Open;
|
||||
});
|
||||
}, [isResettingLink]);
|
||||
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'UsernameLinkEditor',
|
||||
tryClose,
|
||||
});
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
const onDiscard = noop;
|
||||
confirmDiscardIf(showColors && colorId !== initialColorId, onDiscard);
|
||||
}, [colorId, confirmDiscardIf, initialColorId, showColors]);
|
||||
tryClose.current = onTryClose;
|
||||
const onUsernameLinkColorCancel = useCallback(() => {
|
||||
const onDiscard = () => {
|
||||
setShowColors(false);
|
||||
setColorId(initialColorId);
|
||||
};
|
||||
confirmDiscardIf(showColors && colorId !== initialColorId, onDiscard);
|
||||
}, [
|
||||
colorId,
|
||||
confirmDiscardIf,
|
||||
initialColorId,
|
||||
setColorId,
|
||||
setShowColors,
|
||||
showColors,
|
||||
]);
|
||||
|
||||
const info = (
|
||||
<>
|
||||
<div className={classnames(`${CLASS}__actions`)}>
|
||||
|
@ -652,14 +675,6 @@ export function UsernameLinkModalBody({
|
|||
>
|
||||
{i18n('icu:UsernameLinkModalBody__reset')}
|
||||
</button>
|
||||
|
||||
<Button
|
||||
className={classnames(`${CLASS}__done`)}
|
||||
variant={ButtonVariant.Primary}
|
||||
onClick={onBack}
|
||||
>
|
||||
{i18n('icu:UsernameLinkModalBody__done')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -751,11 +766,11 @@ export function UsernameLinkModalBody({
|
|||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{resetModalVisibility === ResetModalVisibility.Open && (
|
||||
{recoveryModalVisibility === RecoveryModalVisibility.Open && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="UsernameLinkModal__error"
|
||||
onClose={onResetModalClose}
|
||||
onClose={onRecoveryModalClose}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:ok')}
|
||||
>
|
||||
|
@ -774,6 +789,8 @@ export function UsernameLinkModalBody({
|
|||
) : (
|
||||
info
|
||||
)}
|
||||
|
||||
{confirmDiscardModal}
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -195,6 +195,7 @@ export function EditConversationAttributesModal({
|
|||
onClick={() => {
|
||||
setEditingAvatar(true);
|
||||
}}
|
||||
showUploadButton
|
||||
style={{
|
||||
height: 96,
|
||||
width: 96,
|
||||
|
|
|
@ -177,6 +177,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
isEditable
|
||||
isGroup
|
||||
onClick={toggleComposeEditingAvatar}
|
||||
showUploadButton
|
||||
style={{
|
||||
height: 96,
|
||||
margin: 0,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import * as log from '../logging/log';
|
||||
|
||||
import type { NavTab } from '../state/ducks/nav';
|
||||
import type { Location } from '../state/ducks/nav';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { sleep } from '../util/sleep';
|
||||
|
||||
|
@ -14,9 +14,10 @@ export enum BeforeNavigateResponse {
|
|||
CancelNavigation = 'CancelNavigation',
|
||||
TimedOut = 'TimedOut',
|
||||
}
|
||||
export type BeforeNavigateCallback = (
|
||||
newTab: NavTab
|
||||
) => Promise<BeforeNavigateResponse>;
|
||||
export type BeforeNavigateCallback = (options: {
|
||||
existingLocation?: Location;
|
||||
newLocation: Location;
|
||||
}) => Promise<BeforeNavigateResponse>;
|
||||
export type BeforeNavigateEntry = {
|
||||
name: string;
|
||||
callback: BeforeNavigateCallback;
|
||||
|
@ -63,10 +64,12 @@ export class BeforeNavigateService {
|
|||
|
||||
async shouldCancelNavigation({
|
||||
context,
|
||||
newTab,
|
||||
existingLocation,
|
||||
newLocation,
|
||||
}: {
|
||||
context: string;
|
||||
newTab: NavTab;
|
||||
existingLocation: Location;
|
||||
newLocation: Location;
|
||||
}): Promise<boolean> {
|
||||
const logId = `shouldCancelNavigation/${context}`;
|
||||
const entries = Array.from(this.#beforeNavigateCallbacks);
|
||||
|
@ -75,8 +78,8 @@ export class BeforeNavigateService {
|
|||
const entry = entries[i];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await Promise.race([
|
||||
entry.callback(newTab),
|
||||
timeOutAfter(5 * SECOND),
|
||||
entry.callback({ existingLocation, newLocation }),
|
||||
timeOutAfter(30 * SECOND),
|
||||
]);
|
||||
if (response === BeforeNavigateResponse.Noop) {
|
||||
continue;
|
||||
|
|
|
@ -96,7 +96,11 @@ export async function writeProfile(
|
|||
} = {};
|
||||
if (profileData.sameAvatar) {
|
||||
log.info('writeProfile: not updating avatar');
|
||||
} else if (avatarRequestHeaders && encryptedAvatarData && newAvatar) {
|
||||
} else if (
|
||||
typeof avatarRequestHeaders === 'object' &&
|
||||
encryptedAvatarData &&
|
||||
newAvatar
|
||||
) {
|
||||
log.info('writeProfile: uploading new avatar');
|
||||
const avatarUrl = await server.uploadAvatar(
|
||||
avatarRequestHeaders,
|
||||
|
|
|
@ -36,13 +36,8 @@ import { instance as libphonenumberInstance } from '../../util/libphonenumberIns
|
|||
import type {
|
||||
ShowSendAnywayDialogActionType,
|
||||
ShowErrorModalActionType,
|
||||
ToggleProfileEditorErrorActionType,
|
||||
} from './globalModals';
|
||||
import {
|
||||
SHOW_SEND_ANYWAY_DIALOG,
|
||||
SHOW_ERROR_MODAL,
|
||||
TOGGLE_PROFILE_EDITOR_ERROR,
|
||||
} from './globalModals';
|
||||
import { SHOW_SEND_ANYWAY_DIALOG, SHOW_ERROR_MODAL } from './globalModals';
|
||||
import {
|
||||
MODIFY_LIST,
|
||||
DELETE_LIST,
|
||||
|
@ -183,8 +178,13 @@ import {
|
|||
isWithinMaxEdits,
|
||||
MESSAGE_MAX_EDIT_COUNT,
|
||||
} from '../../util/canEditMessage';
|
||||
import type { ChangeNavTabActionType } from './nav';
|
||||
import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav';
|
||||
import type { ChangeLocationAction } from './nav';
|
||||
import {
|
||||
CHANGE_LOCATION,
|
||||
NavTab,
|
||||
changeLocation,
|
||||
actions as navActions,
|
||||
} from './nav';
|
||||
import { sortByMessageOrder } from '../../types/ForwardDraft';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
import {
|
||||
|
@ -220,6 +220,8 @@ import { markFailed } from '../../test-node/util/messageFailures';
|
|||
import { cleanupMessages } from '../../util/cleanup';
|
||||
import { MessageModel } from '../../models/messages';
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
import { Page } from '../../components/Preferences';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -586,6 +588,7 @@ export type ConversationsStateType = ReadonlyDeep<{
|
|||
pendingRequestedAvatarDownload: Record<string, boolean>;
|
||||
|
||||
preloadData?: ConversationPreloadDataType;
|
||||
hasProfileUpdateError?: boolean;
|
||||
}>;
|
||||
|
||||
// Helpers
|
||||
|
@ -649,6 +652,8 @@ export const CONVERSATION_UNLOADED = 'CONVERSATION_UNLOADED';
|
|||
export const SHOW_SPOILER = 'conversations/SHOW_SPOILER';
|
||||
export const SET_PENDING_REQUESTED_AVATAR_DOWNLOAD =
|
||||
'conversations/SET_PENDING_REQUESTED_AVATAR_DOWNLOAD';
|
||||
export const SET_PROFILE_UPDATE_ERROR =
|
||||
'conversations/SET_PROFILE_UPDATE_ERROR';
|
||||
|
||||
export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{
|
||||
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
|
||||
|
@ -846,6 +851,12 @@ export type SetPendingRequestedAvatarDownloadActionType = ReadonlyDeep<{
|
|||
value: boolean;
|
||||
};
|
||||
}>;
|
||||
export type SetProfileUpdateErrorActionType = ReadonlyDeep<{
|
||||
type: typeof SET_PROFILE_UPDATE_ERROR;
|
||||
payload: {
|
||||
newErrorState: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type MessagesAddedActionType = ReadonlyDeep<{
|
||||
type: 'MESSAGES_ADDED';
|
||||
|
@ -1082,6 +1093,7 @@ export type ConversationActionType =
|
|||
| ReviewConversationNameCollisionActionType
|
||||
| ScrollToMessageActionType
|
||||
| SetPendingRequestedAvatarDownloadActionType
|
||||
| SetProfileUpdateErrorActionType
|
||||
| TargetedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
| SetComposeGroupExpireTimerActionType
|
||||
|
@ -1219,6 +1231,7 @@ export const actions = {
|
|||
setMuteExpiration,
|
||||
setPinned,
|
||||
setPreJoinConversation,
|
||||
setProfileUpdateError,
|
||||
setVoiceNotePlaybackRate,
|
||||
showArchivedConversations,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
|
@ -2214,12 +2227,7 @@ function saveAvatarToDisk(
|
|||
function myProfileChanged(
|
||||
profileData: ProfileDataType,
|
||||
avatarUpdateOptions: AvatarUpdateOptionsType
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
NoopActionType | ToggleProfileEditorErrorActionType
|
||||
> {
|
||||
): ThunkAction<void, RootStateType, unknown, SetProfileUpdateErrorActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const conversation = getMe(getState());
|
||||
|
||||
|
@ -2235,13 +2243,32 @@ function myProfileChanged(
|
|||
// writeProfile above updates the backbone model which in turn updates
|
||||
// redux through it's on:change event listener. Once we lose Backbone
|
||||
// we'll need to manually sync these new changes.
|
||||
|
||||
// We just want to clear whatever error was there before:
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
type: SET_PROFILE_UPDATE_ERROR,
|
||||
payload: {
|
||||
newErrorState: false,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
log.error('myProfileChanged', Errors.toLogFormat(err));
|
||||
dispatch({ type: TOGGLE_PROFILE_EDITOR_ERROR });
|
||||
|
||||
// Make sure the user sees an error dialog
|
||||
dispatch({
|
||||
type: SET_PROFILE_UPDATE_ERROR,
|
||||
payload: {
|
||||
newErrorState: true,
|
||||
},
|
||||
});
|
||||
// And take them to the profile editor to resolve it
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.None,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -3067,7 +3094,7 @@ export function markOpenConversationRead(
|
|||
const state = getState();
|
||||
const { nav } = state;
|
||||
|
||||
if (nav.selectedNavTab !== NavTab.Chats) {
|
||||
if (nav.selectedLocation.tab !== NavTab.Chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3305,6 +3332,16 @@ function setIsFetchingUUID(
|
|||
},
|
||||
};
|
||||
}
|
||||
function setProfileUpdateError(
|
||||
newErrorState: boolean
|
||||
): SetProfileUpdateErrorActionType {
|
||||
return {
|
||||
type: SET_PROFILE_UPDATE_ERROR,
|
||||
payload: {
|
||||
newErrorState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type PushPanelForConversationActionType = ReadonlyDeep<
|
||||
(panel: PanelRequestType) => unknown
|
||||
|
@ -4676,13 +4713,13 @@ function showConversation({
|
|||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
TargetedConversationChangedActionType | ChangeNavTabActionType
|
||||
TargetedConversationChangedActionType | ChangeLocationAction
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const { conversations, nav } = getState();
|
||||
|
||||
if (nav.selectedNavTab !== NavTab.Chats) {
|
||||
dispatch(navActions.changeNavTab(NavTab.Chats));
|
||||
if (nav.selectedLocation.tab !== NavTab.Chats) {
|
||||
dispatch(navActions.changeLocation({ tab: NavTab.Chats }));
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
conversation?.setMarkedUnread(false);
|
||||
}
|
||||
|
@ -5469,7 +5506,7 @@ export function reducer(
|
|||
action: Readonly<
|
||||
| ConversationActionType
|
||||
| StoryDistributionListsActionType
|
||||
| ChangeNavTabActionType
|
||||
| ChangeLocationAction
|
||||
>
|
||||
): ConversationsStateType {
|
||||
if (action.type === CLEAR_CONVERSATIONS_PENDING_VERIFICATION) {
|
||||
|
@ -5656,6 +5693,15 @@ export function reducer(
|
|||
preJoinConversation: data,
|
||||
};
|
||||
}
|
||||
if (action.type === SET_PROFILE_UPDATE_ERROR) {
|
||||
const { payload } = action;
|
||||
const { newErrorState } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
hasProfileUpdateError: newErrorState,
|
||||
};
|
||||
}
|
||||
if (action.type === 'CONVERSATIONS_UPDATED') {
|
||||
const { payload } = action;
|
||||
const { data: conversations } = payload;
|
||||
|
@ -7299,8 +7345,8 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (
|
||||
action.type === CHANGE_NAV_TAB &&
|
||||
action.payload.selectedNavTab === NavTab.Chats
|
||||
action.type === CHANGE_LOCATION &&
|
||||
action.payload.selectedLocation.tab === NavTab.Chats
|
||||
) {
|
||||
const { messagesByConversation, selectedConversationId } = state;
|
||||
if (selectedConversationId == null) {
|
||||
|
|
|
@ -17,7 +17,6 @@ import type {
|
|||
import type { MessagePropsType } from '../selectors/message';
|
||||
import type { RecipientsByConversation } from './stories';
|
||||
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
|
||||
import type { EditState as ProfileEditorEditState } from '../../components/ProfileEditor';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import * as SingleServePromise from '../../services/singleServePromise';
|
||||
import * as Stickers from '../../types/Stickers';
|
||||
|
@ -125,7 +124,6 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
forwardMessagesProps?: ForwardMessagesPropsType;
|
||||
gv2MigrationProps?: MigrateToGV2PropsType;
|
||||
hasConfirmationModal: boolean;
|
||||
isProfileEditorVisible: boolean;
|
||||
isProfileNameWarningModalVisible: boolean;
|
||||
profileNameWarningModalConversationType?: string;
|
||||
isShortcutGuideModalVisible: boolean;
|
||||
|
@ -143,8 +141,6 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
requestor: 'call' | 'voiceNote';
|
||||
abortController: AbortController;
|
||||
};
|
||||
profileEditorHasError: boolean;
|
||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
||||
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
|
||||
safetyNumberModalContactId?: string;
|
||||
stickerPackPreviewId?: string;
|
||||
|
@ -181,9 +177,6 @@ const TOGGLE_DRAFT_GIF_MESSAGE_SEND_MODAL =
|
|||
const TOGGLE_FORWARD_MESSAGES_MODAL =
|
||||
'globalModals/TOGGLE_FORWARD_MESSAGES_MODAL';
|
||||
const TOGGLE_NOTE_PREVIEW_MODAL = 'globalModals/TOGGLE_NOTE_PREVIEW_MODAL';
|
||||
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
|
||||
const TOGGLE_PROFILE_NAME_WARNING_MODAL =
|
||||
'globalModals/TOGGLE_PROFILE_NAME_WARNING_MODAL';
|
||||
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
|
||||
|
@ -324,17 +317,6 @@ type ToggleNotePreviewModalActionType = ReadonlyDeep<{
|
|||
payload: NotePreviewModalPropsType | null;
|
||||
}>;
|
||||
|
||||
type ToggleProfileEditorActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||
payload: {
|
||||
initialEditState?: ProfileEditorEditState;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ToggleProfileEditorErrorActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_EDITOR_ERROR;
|
||||
}>;
|
||||
|
||||
export type ToggleProfileNameWarningModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_NAME_WARNING_MODAL;
|
||||
payload?: {
|
||||
|
@ -545,8 +527,6 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ToggleForwardMessagesModalActionType
|
||||
| ToggleMessageRequestActionsConfirmationActionType
|
||||
| ToggleNotePreviewModalActionType
|
||||
| ToggleProfileEditorActionType
|
||||
| ToggleProfileEditorErrorActionType
|
||||
| ToggleProfileNameWarningModalActionType
|
||||
| ToggleSafetyNumberModalActionType
|
||||
| ToggleSignalConnectionsModalActionType
|
||||
|
@ -602,8 +582,6 @@ export const actions = {
|
|||
toggleForwardMessagesModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
toggleNotePreviewModal,
|
||||
toggleProfileEditor,
|
||||
toggleProfileEditorHasError,
|
||||
toggleProfileNameWarningModal,
|
||||
toggleSafetyNumberModal,
|
||||
toggleSignalConnectionsModal,
|
||||
|
@ -949,16 +927,6 @@ function toggleNotePreviewModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleProfileEditor(
|
||||
initialEditState?: ProfileEditorEditState
|
||||
): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR, payload: { initialEditState } };
|
||||
}
|
||||
|
||||
function toggleProfileEditorHasError(): ToggleProfileEditorErrorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR_ERROR };
|
||||
}
|
||||
|
||||
function toggleProfileNameWarningModal(
|
||||
conversationType?: string
|
||||
): ToggleProfileNameWarningModalActionType {
|
||||
|
@ -1335,7 +1303,6 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
criticalIdlePrimaryDeviceModal: false,
|
||||
draftGifMessageSendModalProps: null,
|
||||
editNicknameAndNoteModalProps: null,
|
||||
isProfileEditorVisible: false,
|
||||
isProfileNameWarningModalVisible: false,
|
||||
profileNameWarningModalConversationType: undefined,
|
||||
isShortcutGuideModalVisible: false,
|
||||
|
@ -1344,8 +1311,6 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
isWhatsNewVisible: false,
|
||||
lowDiskSpaceBackupImportModal: null,
|
||||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
messageRequestActionsConfirmationProps: null,
|
||||
tapToViewNotAvailableModalProps: undefined,
|
||||
notePreviewModalProps: null,
|
||||
|
@ -1377,20 +1342,6 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PROFILE_EDITOR) {
|
||||
return {
|
||||
...state,
|
||||
isProfileEditorVisible: !state.isProfileEditorVisible,
|
||||
profileEditorInitialEditState: action.payload.initialEditState,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PROFILE_EDITOR_ERROR) {
|
||||
return {
|
||||
...state,
|
||||
profileEditorHasError: !state.profileEditorHasError,
|
||||
};
|
||||
}
|
||||
if (action.type === TOGGLE_PROFILE_NAME_WARNING_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -2,8 +2,15 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import * as log from '../../logging/log';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { Page } from '../../components/Preferences';
|
||||
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { EditState } from '../../components/ProfileEditor';
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -13,35 +20,77 @@ export enum NavTab {
|
|||
Stories = 'Stories',
|
||||
Settings = 'Settings',
|
||||
}
|
||||
export type Location = ReadonlyDeep<
|
||||
| {
|
||||
tab: NavTab.Settings;
|
||||
details:
|
||||
| {
|
||||
page: Page.Profile;
|
||||
state: EditState;
|
||||
}
|
||||
| { page: Exclude<Page, Page.Profile> };
|
||||
}
|
||||
| { tab: Exclude<NavTab, NavTab.Settings> }
|
||||
>;
|
||||
|
||||
function printLocation(location: Location): string {
|
||||
if (location.tab === NavTab.Settings) {
|
||||
if (location.details.page === Page.Profile) {
|
||||
return `${location.tab}/${location.details.page}/${location.details.state}`;
|
||||
}
|
||||
return `${location.tab}/${location.details.page}`;
|
||||
}
|
||||
|
||||
return `${location.tab}`;
|
||||
}
|
||||
|
||||
// State
|
||||
|
||||
export type NavStateType = ReadonlyDeep<{
|
||||
selectedNavTab: NavTab;
|
||||
selectedLocation: Location;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
||||
export const CHANGE_NAV_TAB = 'nav/CHANGE_NAV_TAB';
|
||||
export const CHANGE_LOCATION = 'nav/CHANGE_LOCATION';
|
||||
|
||||
export type ChangeNavTabActionType = ReadonlyDeep<{
|
||||
type: typeof CHANGE_NAV_TAB;
|
||||
payload: { selectedNavTab: NavTab };
|
||||
export type ChangeLocationAction = ReadonlyDeep<{
|
||||
type: typeof CHANGE_LOCATION;
|
||||
payload: { selectedLocation: Location };
|
||||
}>;
|
||||
|
||||
export type NavActionType = ReadonlyDeep<ChangeNavTabActionType>;
|
||||
export type NavActionType = ReadonlyDeep<ChangeLocationAction>;
|
||||
|
||||
// Action Creators
|
||||
|
||||
function changeNavTab(selectedNavTab: NavTab): NavActionType {
|
||||
return {
|
||||
type: CHANGE_NAV_TAB,
|
||||
payload: { selectedNavTab },
|
||||
export function changeLocation(
|
||||
newLocation: Location
|
||||
): ThunkAction<void, RootStateType, unknown, NavActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const existingLocation = getState().nav.selectedLocation;
|
||||
const logId = `changeLocation/${printLocation(newLocation)}`;
|
||||
|
||||
const needToCancel =
|
||||
await window.Signal.Services.beforeNavigate.shouldCancelNavigation({
|
||||
context: logId,
|
||||
existingLocation,
|
||||
newLocation,
|
||||
});
|
||||
|
||||
if (needToCancel) {
|
||||
log.info(`${logId}: Cancelling navigation`);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHANGE_LOCATION,
|
||||
payload: { selectedLocation: newLocation },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
changeNavTab,
|
||||
changeLocation,
|
||||
};
|
||||
|
||||
export const useNavActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
|
@ -51,7 +100,9 @@ export const useNavActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
|||
|
||||
export function getEmptyState(): NavStateType {
|
||||
return {
|
||||
selectedNavTab: NavTab.Chats,
|
||||
selectedLocation: {
|
||||
tab: NavTab.Chats,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -59,10 +110,10 @@ export function reducer(
|
|||
state: Readonly<NavStateType> = getEmptyState(),
|
||||
action: Readonly<NavActionType>
|
||||
): NavStateType {
|
||||
if (action.type === CHANGE_NAV_TAB) {
|
||||
if (action.type === CHANGE_LOCATION) {
|
||||
return {
|
||||
...state,
|
||||
selectedNavTab: action.payload.selectedNavTab,
|
||||
selectedLocation: action.payload.selectedLocation,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -43,10 +43,10 @@ export type UsernameStateType = ReadonlyDeep<{
|
|||
// ProfileEditor
|
||||
editState: UsernameEditState;
|
||||
|
||||
// UsernameLinkModalBody
|
||||
// UsernameLinkEditor
|
||||
linkState: UsernameLinkState;
|
||||
|
||||
// EditUsernameModalBody
|
||||
// UsernameEditor
|
||||
usernameReservation: UsernameReservationStateType;
|
||||
}>;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export enum UsernameEditState {
|
|||
}
|
||||
|
||||
//
|
||||
// UsernameLinkModalBody
|
||||
// UsernameLinkEditor
|
||||
//
|
||||
|
||||
export enum UsernameLinkState {
|
||||
|
@ -22,7 +22,7 @@ export enum UsernameLinkState {
|
|||
}
|
||||
|
||||
//
|
||||
// EditUsernameModalBody
|
||||
// UsernameEditor
|
||||
//
|
||||
|
||||
export enum UsernameReservationState {
|
||||
|
|
|
@ -1340,6 +1340,11 @@ export const getPreloadedConversationId = createSelector(
|
|||
({ preloadData }): string | undefined => preloadData?.conversationId
|
||||
);
|
||||
|
||||
export const getProfileUpdateError = createSelector(
|
||||
getConversations,
|
||||
({ hasProfileUpdateError }): boolean => Boolean(hasProfileUpdateError)
|
||||
);
|
||||
|
||||
export const getPendingAvatarDownloadSelector = createSelector(
|
||||
getConversations,
|
||||
(conversations: ConversationsStateType) => {
|
||||
|
|
|
@ -83,16 +83,6 @@ export const getForwardMessagesProps = createSelector(
|
|||
({ forwardMessagesProps }) => forwardMessagesProps
|
||||
);
|
||||
|
||||
export const getProfileEditorHasError = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ profileEditorHasError }) => profileEditorHasError
|
||||
);
|
||||
|
||||
export const getProfileEditorInitialEditState = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ profileEditorInitialEditState }) => profileEditorInitialEditState
|
||||
);
|
||||
|
||||
export const getEditNicknameAndNoteModalProps = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ editNicknameAndNoteModalProps }) => editNicknameAndNoteModalProps
|
||||
|
|
|
@ -14,7 +14,11 @@ function getNav(state: StateType): NavStateType {
|
|||
}
|
||||
|
||||
export const getSelectedNavTab = createSelector(getNav, nav => {
|
||||
return nav.selectedNavTab;
|
||||
return nav.selectedLocation.tab;
|
||||
});
|
||||
|
||||
export const getSelectedLocation = createSelector(getNav, nav => {
|
||||
return nav.selectedLocation;
|
||||
});
|
||||
|
||||
export const getOtherTabsUnreadStats = createSelector(
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { EditUsernameModalBody } from '../../components/EditUsernameModalBody';
|
||||
import { getMinNickname, getMaxNickname } from '../../util/Username';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getUsernameReservationState,
|
||||
getUsernameReservationObject,
|
||||
getUsernameReservationError,
|
||||
getRecoveredUsername,
|
||||
} from '../selectors/username';
|
||||
import { getUsernameCorrupted } from '../selectors/items';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
|
||||
export type SmartEditUsernameModalBodyProps = Readonly<{
|
||||
isRootModal: boolean;
|
||||
onClose(): void;
|
||||
}>;
|
||||
|
||||
export const SmartEditUsernameModalBody = memo(
|
||||
function SmartEditUsernameModalBody({
|
||||
isRootModal,
|
||||
onClose,
|
||||
}: SmartEditUsernameModalBodyProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { username } = useSelector(getMe);
|
||||
const usernameCorrupted = useSelector(getUsernameCorrupted);
|
||||
const currentUsername = usernameCorrupted ? undefined : username;
|
||||
const minNickname = getMinNickname();
|
||||
const maxNickname = getMaxNickname();
|
||||
const state = useSelector(getUsernameReservationState);
|
||||
const recoveredUsername = useSelector(getRecoveredUsername);
|
||||
const reservation = useSelector(getUsernameReservationObject);
|
||||
const error = useSelector(getUsernameReservationError);
|
||||
const {
|
||||
setUsernameReservationError,
|
||||
clearUsernameReservation,
|
||||
reserveUsername,
|
||||
confirmUsername,
|
||||
} = useUsernameActions();
|
||||
const { showToast } = useToastActions();
|
||||
return (
|
||||
<EditUsernameModalBody
|
||||
i18n={i18n}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
currentUsername={currentUsername}
|
||||
minNickname={minNickname}
|
||||
maxNickname={maxNickname}
|
||||
state={state}
|
||||
recoveredUsername={recoveredUsername}
|
||||
reservation={reservation}
|
||||
error={error}
|
||||
setUsernameReservationError={setUsernameReservationError}
|
||||
clearUsernameReservation={clearUsernameReservation}
|
||||
reserveUsername={reserveUsername}
|
||||
confirmUsername={confirmUsername}
|
||||
showToast={showToast}
|
||||
isRootModal={isRootModal}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -11,7 +11,6 @@ import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
|
|||
import { SmartContactModal } from './ContactModal';
|
||||
import { SmartEditHistoryMessagesModal } from './EditHistoryMessagesModal';
|
||||
import { SmartForwardMessagesModal } from './ForwardMessagesModal';
|
||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||
import { SmartUsernameOnboardingModal } from './UsernameOnboardingModal';
|
||||
import { SmartSafetyNumberModal } from './SafetyNumberModal';
|
||||
import { SmartSendAnywayDialog } from './SendAnywayDialog';
|
||||
|
@ -58,10 +57,6 @@ function renderEditNicknameAndNoteModal(): JSX.Element {
|
|||
return <SmartEditNicknameAndNoteModal />;
|
||||
}
|
||||
|
||||
function renderProfileEditor(): JSX.Element {
|
||||
return <SmartProfileEditorModal />;
|
||||
}
|
||||
|
||||
function renderProfileNameWarningModal(): JSX.Element {
|
||||
return <SmartProfileNameWarningModal />;
|
||||
}
|
||||
|
@ -143,7 +138,6 @@ export const SmartGlobalModalContainer = memo(
|
|||
mediaPermissionsModalProps,
|
||||
messageRequestActionsConfirmationProps,
|
||||
notePreviewModalProps,
|
||||
isProfileEditorVisible,
|
||||
isProfileNameWarningModalVisible,
|
||||
profileNameWarningModalConversationType,
|
||||
isShortcutGuideModalVisible,
|
||||
|
@ -254,7 +248,6 @@ export const SmartGlobalModalContainer = memo(
|
|||
hideTapToViewNotAvailableModal={hideTapToViewNotAvailableModal}
|
||||
i18n={i18n}
|
||||
isAboutContactModalVisible={aboutContactModalContactId != null}
|
||||
isProfileEditorVisible={isProfileEditorVisible}
|
||||
isProfileNameWarningModalVisible={isProfileNameWarningModalVisible}
|
||||
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
|
||||
isSignalConnectionsVisible={isSignalConnectionsVisible}
|
||||
|
@ -280,7 +273,6 @@ export const SmartGlobalModalContainer = memo(
|
|||
renderMessageRequestActionsConfirmation
|
||||
}
|
||||
renderNotePreviewModal={renderNotePreviewModal}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderProfileNameWarningModal={renderProfileNameWarningModal}
|
||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||
renderSafetyNumber={renderSafetyNumber}
|
||||
|
|
|
@ -106,6 +106,7 @@ import {
|
|||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
} from '../../util/backupMediaDownload';
|
||||
import { useNavActions } from '../ducks/nav';
|
||||
|
||||
function renderMessageSearchResult(id: string): JSX.Element {
|
||||
return <SmartMessageSearchResult id={id} />;
|
||||
|
@ -347,8 +348,8 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||
const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } =
|
||||
useItemsActions();
|
||||
const { setChallengeStatus } = useNetworkActions();
|
||||
const { showUserNotFoundModal, toggleProfileEditor } =
|
||||
useGlobalModalActions();
|
||||
const { showUserNotFoundModal } = useGlobalModalActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
let hasExpiredDialog = false;
|
||||
let unsupportedOSDialogType: 'error' | 'warning' | undefined;
|
||||
|
@ -377,6 +378,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||
blockConversation={blockConversation}
|
||||
cancelBackupMediaDownload={cancelBackupMediaDownload}
|
||||
challengeStatus={challengeStatus}
|
||||
changeLocation={changeLocation}
|
||||
clearConversationSearch={clearConversationSearch}
|
||||
clearGroupCreationError={clearGroupCreationError}
|
||||
clearSearchQuery={clearSearchQuery}
|
||||
|
@ -448,7 +450,6 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||
toggleComposeEditingAvatar={toggleComposeEditingAvatar}
|
||||
toggleConversationInChooseMembers={toggleConversationInChooseMembers}
|
||||
toggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||
toggleProfileEditor={toggleProfileEditor}
|
||||
unsupportedOSDialogType={unsupportedOSDialogType}
|
||||
updateSearchTerm={updateSearchTerm}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
|
|
|
@ -15,10 +15,12 @@ import {
|
|||
getHasAnyFailedStorySends,
|
||||
getStoriesNotificationCount,
|
||||
} from '../selectors/stories';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { getStoriesEnabled } from '../selectors/items';
|
||||
import {
|
||||
getStoriesEnabled,
|
||||
isInternalUser as isInternalUserSelector,
|
||||
} from '../selectors/items';
|
||||
import { getSelectedNavTab } from '../selectors/nav';
|
||||
import type { NavTab } from '../ducks/nav';
|
||||
import type { Location } from '../ducks/nav';
|
||||
import { useNavActions } from '../ducks/nav';
|
||||
import { getHasPendingUpdate } from '../selectors/updates';
|
||||
import { getCallHistoryUnreadCount } from '../selectors/callHistory';
|
||||
|
@ -42,7 +44,7 @@ export const SmartNavTabs = memo(function SmartNavTabs({
|
|||
}: SmartNavTabsProps): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const selectedNavTab = useSelector(getSelectedNavTab);
|
||||
const { changeNavTab } = useNavActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
const me = useSelector(getMe);
|
||||
const badge = useSelector(getPreferredBadgeSelector)(me.badges);
|
||||
const theme = useSelector(getTheme);
|
||||
|
@ -52,18 +54,17 @@ export const SmartNavTabs = memo(function SmartNavTabs({
|
|||
const unreadCallsCount = useSelector(getCallHistoryUnreadCount);
|
||||
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
||||
const hasPendingUpdate = useSelector(getHasPendingUpdate);
|
||||
const isInternalUser = useSelector(isInternalUserSelector);
|
||||
|
||||
const { toggleProfileEditor } = useGlobalModalActions();
|
||||
|
||||
const onNavTabSelected = useCallback(
|
||||
(tab: NavTab) => {
|
||||
const onChangeLocation = useCallback(
|
||||
(location: Location) => {
|
||||
// For some reason react-aria will call this more often than the tab
|
||||
// actually changing.
|
||||
if (tab !== selectedNavTab) {
|
||||
changeNavTab(tab);
|
||||
if (location.tab !== selectedNavTab) {
|
||||
changeLocation(location);
|
||||
}
|
||||
},
|
||||
[changeNavTab, selectedNavTab]
|
||||
[changeLocation, selectedNavTab]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -72,11 +73,11 @@ export const SmartNavTabs = memo(function SmartNavTabs({
|
|||
hasFailedStorySends={hasFailedStorySends}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
i18n={i18n}
|
||||
isInternalUser={isInternalUser}
|
||||
me={me}
|
||||
navTabsCollapsed={navTabsCollapsed}
|
||||
onNavTabSelected={onNavTabSelected}
|
||||
onChangeLocation={onChangeLocation}
|
||||
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
|
||||
onToggleProfileEditor={toggleProfileEditor}
|
||||
renderCallsTab={renderCallsTab}
|
||||
renderChatsTab={renderChatsTab}
|
||||
renderStoriesTab={renderStoriesTab}
|
||||
|
|
|
@ -3,11 +3,16 @@
|
|||
|
||||
import React, { StrictMode, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { getConversationsWithCustomColorSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationsWithCustomColorSelector,
|
||||
getMe,
|
||||
} from '../selectors/conversations';
|
||||
import {
|
||||
getCustomColors,
|
||||
getItems,
|
||||
|
@ -17,7 +22,12 @@ import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../../textsecure/Storage';
|
|||
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
|
||||
import { isBackupFeatureEnabledForRedux } from '../../util/isBackupEnabled';
|
||||
import { format } from '../../types/PhoneNumber';
|
||||
import { getIntl, getUserDeviceId, getUserNumber } from '../selectors/user';
|
||||
import {
|
||||
getIntl,
|
||||
getTheme,
|
||||
getUserDeviceId,
|
||||
getUserNumber,
|
||||
} from '../selectors/user';
|
||||
import { EmojiSkinTone } from '../../components/fun/data/emojis';
|
||||
import { renderClearingDataView } from '../../shims/renderClearingDataView';
|
||||
import OS from '../../util/os/osPreload';
|
||||
|
@ -41,22 +51,27 @@ import { getConversation } from '../../util/getConversation';
|
|||
import { waitForEvent } from '../../shims/events';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { sendSyncRequests } from '../../textsecure/syncRequests';
|
||||
|
||||
import { SmartUpdateDialog } from './UpdateDialog';
|
||||
import { Preferences } from '../../components/Preferences';
|
||||
|
||||
import type { StorageAccessType, ZoomFactorType } from '../../types/Storage';
|
||||
import type { ThemeType } from '../../util/preload';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { Page, Preferences } from '../../components/Preferences';
|
||||
import { useUpdatesActions } from '../ducks/updates';
|
||||
import {
|
||||
getHasPendingUpdate,
|
||||
isUpdateDownloaded as getIsUpdateDownloaded,
|
||||
} from '../selectors/updates';
|
||||
import { getHasAnyFailedStorySends } from '../selectors/stories';
|
||||
import { getOtherTabsUnreadStats } from '../selectors/nav';
|
||||
import { getOtherTabsUnreadStats, getSelectedLocation } from '../selectors/nav';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { SmartProfileEditor } from './ProfileEditor';
|
||||
import { NavTab, useNavActions } from '../ducks/nav';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { DataReader } from '../../sql/Client';
|
||||
|
||||
import type { StorageAccessType, ZoomFactorType } from '../../types/Storage';
|
||||
import type { ThemeType } from '../../util/preload';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
const DEFAULT_NOTIFICATION_SETTING = 'message';
|
||||
|
||||
function renderUpdateDialog(
|
||||
|
@ -65,6 +80,18 @@ function renderUpdateDialog(
|
|||
return <SmartUpdateDialog {...props} disableDismiss />;
|
||||
}
|
||||
|
||||
function renderProfileEditor(options: {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
}): JSX.Element {
|
||||
return <SmartProfileEditor contentsRef={options.contentsRef} />;
|
||||
}
|
||||
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
function getSystemTraySettingValues(
|
||||
systemTraySetting: SystemTraySetting | undefined
|
||||
): {
|
||||
|
@ -92,7 +119,7 @@ function getSystemTraySettingValues(
|
|||
};
|
||||
}
|
||||
|
||||
export function SmartPreferences(): JSX.Element {
|
||||
export function SmartPreferences(): JSX.Element | null {
|
||||
const {
|
||||
addCustomColor,
|
||||
editCustomColor,
|
||||
|
@ -106,9 +133,12 @@ export function SmartPreferences(): JSX.Element {
|
|||
const { removeCustomColorOnConversations, resetAllChatColors } =
|
||||
useConversationsActions();
|
||||
const { startUpdate } = useUpdatesActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
const { showToast } = useToastActions();
|
||||
|
||||
// Selectors
|
||||
|
||||
const currentLocation = useSelector(getSelectedLocation);
|
||||
const customColors = useSelector(getCustomColors) ?? {};
|
||||
const getConversationsWithCustomColor = useSelector(
|
||||
getConversationsWithCustomColorSelector
|
||||
|
@ -120,6 +150,9 @@ export function SmartPreferences(): JSX.Element {
|
|||
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
|
||||
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
||||
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
|
||||
const me = useSelector(getMe);
|
||||
const badge = useSelector(getPreferredBadgeSelector)(me.badges);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
// The weird ones
|
||||
|
||||
|
@ -583,6 +616,31 @@ export function SmartPreferences(): JSX.Element {
|
|||
}
|
||||
);
|
||||
|
||||
if (currentLocation.tab !== NavTab.Settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { page } = currentLocation.details;
|
||||
const setPage = (newPage: Page, editState?: EditState) => {
|
||||
if (newPage === Page.Profile) {
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: newPage,
|
||||
state: editState || EditState.None,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: newPage,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StrictMode>
|
||||
<Preferences
|
||||
|
@ -594,6 +652,7 @@ export function SmartPreferences(): JSX.Element {
|
|||
availableSpeakers={availableSpeakers}
|
||||
backupFeatureEnabled={backupFeatureEnabled}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
badge={badge}
|
||||
blockedCount={blockedCount}
|
||||
cloudBackupStatus={cloudBackupStatus}
|
||||
customColors={customColors}
|
||||
|
@ -655,6 +714,7 @@ export function SmartPreferences(): JSX.Element {
|
|||
lastSyncTime={lastSyncTime}
|
||||
localeOverride={localeOverride}
|
||||
makeSyncRequest={makeSyncRequest}
|
||||
me={me}
|
||||
navTabsCollapsed={navTabsCollapsed}
|
||||
notificationContent={notificationContent}
|
||||
onAudioNotificationsChange={onAudioNotificationsChange}
|
||||
|
@ -697,11 +757,14 @@ export function SmartPreferences(): JSX.Element {
|
|||
onWhoCanSeeMeChange={onWhoCanSeeMeChange}
|
||||
onZoomFactorChange={onZoomFactorChange}
|
||||
otherTabsUnreadStats={otherTabsUnreadStats}
|
||||
page={page}
|
||||
preferredSystemLocales={preferredSystemLocales}
|
||||
refreshCloudBackupStatus={refreshCloudBackupStatus}
|
||||
refreshBackupSubscriptionStatus={refreshBackupSubscriptionStatus}
|
||||
removeCustomColorOnConversations={removeCustomColorOnConversations}
|
||||
removeCustomColor={removeCustomColor}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderToastManager={renderToastManager}
|
||||
renderUpdateDialog={renderUpdateDialog}
|
||||
resetAllChatColors={resetAllChatColors}
|
||||
resetDefaultChatColor={resetDefaultChatColor}
|
||||
|
@ -711,6 +774,9 @@ export function SmartPreferences(): JSX.Element {
|
|||
selectedSpeaker={selectedSpeaker}
|
||||
sentMediaQualitySetting={sentMediaQualitySetting}
|
||||
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
|
||||
setPage={setPage}
|
||||
showToast={showToast}
|
||||
theme={theme}
|
||||
themeSetting={themeSetting}
|
||||
universalExpireTimer={universalExpireTimer}
|
||||
validateBackup={validateBackup}
|
||||
|
|
167
ts/state/smart/ProfileEditor.tsx
Normal file
167
ts/state/smart/ProfileEditor.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { ProfileEditor } from '../../components/ProfileEditor';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
import { getMe, getProfileUpdateError } from '../selectors/conversations';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import {
|
||||
getEmojiSkinToneDefault,
|
||||
getHasCompletedUsernameLinkOnboarding,
|
||||
getUsernameCorrupted,
|
||||
getUsernameLink,
|
||||
getUsernameLinkColor,
|
||||
getUsernameLinkCorrupted,
|
||||
} from '../selectors/items';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getUsernameEditState,
|
||||
getUsernameLinkState,
|
||||
} from '../selectors/username';
|
||||
import { SmartUsernameEditor } from './UsernameEditor';
|
||||
import { getSelectedLocation } from '../selectors/nav';
|
||||
import { NavTab, useNavActions } from '../ducks/nav';
|
||||
import { Page } from '../../components/Preferences';
|
||||
|
||||
import type { EditState } from '../../components/ProfileEditor';
|
||||
import type { SmartUsernameEditorProps } from './UsernameEditor';
|
||||
import { ConfirmationDialog } from '../../components/ConfirmationDialog';
|
||||
|
||||
function renderUsernameEditor(props: SmartUsernameEditorProps): JSX.Element {
|
||||
return <SmartUsernameEditor {...props} />;
|
||||
}
|
||||
|
||||
export const SmartProfileEditor = memo(function SmartProfileEditor(props: {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
}) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const {
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
avatars: userAvatarData = [],
|
||||
color,
|
||||
familyName,
|
||||
firstName,
|
||||
id: conversationId,
|
||||
profileAvatarUrl,
|
||||
username,
|
||||
} = useSelector(getMe);
|
||||
const selectedLocation = useSelector(getSelectedLocation);
|
||||
const hasCompletedUsernameLinkOnboarding = useSelector(
|
||||
getHasCompletedUsernameLinkOnboarding
|
||||
);
|
||||
const hasError = useSelector(getProfileUpdateError);
|
||||
const recentEmojis = useSelector(selectRecentEmojis);
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const usernameCorrupted = useSelector(getUsernameCorrupted);
|
||||
const usernameEditState = useSelector(getUsernameEditState);
|
||||
const usernameLink = useSelector(getUsernameLink);
|
||||
const usernameLinkColor = useSelector(getUsernameLinkColor);
|
||||
const usernameLinkCorrupted = useSelector(getUsernameLinkCorrupted);
|
||||
const usernameLinkState = useSelector(getUsernameLinkState);
|
||||
|
||||
const {
|
||||
deleteAvatarFromDisk,
|
||||
myProfileChanged,
|
||||
replaceAvatar,
|
||||
saveAttachment,
|
||||
saveAvatarToDisk,
|
||||
setProfileUpdateError,
|
||||
} = useConversationsActions();
|
||||
const {
|
||||
resetUsernameLink,
|
||||
setUsernameLinkColor,
|
||||
setUsernameEditState,
|
||||
openUsernameReservationModal,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
deleteUsername,
|
||||
} = useUsernameActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
let errorDialog: JSX.Element | undefined;
|
||||
if (hasError) {
|
||||
errorDialog = (
|
||||
<ConfirmationDialog
|
||||
dialogName="ProfileEditorModal.error"
|
||||
cancelText={i18n('icu:Confirmation--confirm')}
|
||||
i18n={i18n}
|
||||
onClose={() => setProfileUpdateError(false)}
|
||||
>
|
||||
{i18n('icu:ProfileEditorModal--error')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLocation.tab !== NavTab.Settings ||
|
||||
selectedLocation.details.page !== Page.Profile
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const editState = selectedLocation.details.state;
|
||||
const setEditState = (newState: EditState) => {
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: newState,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{errorDialog}
|
||||
<ProfileEditor
|
||||
aboutEmoji={aboutEmoji}
|
||||
aboutText={aboutText}
|
||||
color={color}
|
||||
contentsRef={props.contentsRef}
|
||||
conversationId={conversationId}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
deleteUsername={deleteUsername}
|
||||
familyName={familyName}
|
||||
firstName={firstName ?? ''}
|
||||
hasCompletedUsernameLinkOnboarding={hasCompletedUsernameLinkOnboarding}
|
||||
i18n={i18n}
|
||||
editState={editState}
|
||||
markCompletedUsernameLinkOnboarding={
|
||||
markCompletedUsernameLinkOnboarding
|
||||
}
|
||||
onProfileChanged={myProfileChanged}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
openUsernameReservationModal={openUsernameReservationModal}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
recentEmojis={recentEmojis}
|
||||
renderUsernameEditor={renderUsernameEditor}
|
||||
replaceAvatar={replaceAvatar}
|
||||
resetUsernameLink={resetUsernameLink}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
setEditState={setEditState}
|
||||
setUsernameEditState={setUsernameEditState}
|
||||
setUsernameLinkColor={setUsernameLinkColor}
|
||||
showToast={showToast}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
userAvatarData={userAvatarData}
|
||||
username={username}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
usernameEditState={usernameEditState}
|
||||
usernameLink={usernameLink}
|
||||
usernameLinkColor={usernameLinkColor}
|
||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||
usernameLinkState={usernameLinkState}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -1,127 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ProfileEditorModal } from '../../components/ProfileEditorModal';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import {
|
||||
getProfileEditorHasError,
|
||||
getProfileEditorInitialEditState,
|
||||
} from '../selectors/globalModals';
|
||||
import {
|
||||
getEmojiSkinToneDefault,
|
||||
getHasCompletedUsernameLinkOnboarding,
|
||||
getUsernameCorrupted,
|
||||
getUsernameLink,
|
||||
getUsernameLinkColor,
|
||||
getUsernameLinkCorrupted,
|
||||
} from '../selectors/items';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getUsernameEditState,
|
||||
getUsernameLinkState,
|
||||
} from '../selectors/username';
|
||||
import type { SmartEditUsernameModalBodyProps } from './EditUsernameModalBody';
|
||||
import { SmartEditUsernameModalBody } from './EditUsernameModalBody';
|
||||
|
||||
function renderEditUsernameModalBody(
|
||||
props: SmartEditUsernameModalBodyProps
|
||||
): JSX.Element {
|
||||
return <SmartEditUsernameModalBody {...props} />;
|
||||
}
|
||||
|
||||
export const SmartProfileEditorModal = memo(function SmartProfileEditorModal() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const {
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
avatars: userAvatarData = [],
|
||||
color,
|
||||
familyName,
|
||||
firstName,
|
||||
id: conversationId,
|
||||
profileAvatarUrl,
|
||||
username,
|
||||
} = useSelector(getMe);
|
||||
const hasCompletedUsernameLinkOnboarding = useSelector(
|
||||
getHasCompletedUsernameLinkOnboarding
|
||||
);
|
||||
const hasError = useSelector(getProfileEditorHasError);
|
||||
const initialEditState = useSelector(getProfileEditorInitialEditState);
|
||||
const recentEmojis = useSelector(selectRecentEmojis);
|
||||
const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault);
|
||||
const usernameCorrupted = useSelector(getUsernameCorrupted);
|
||||
const usernameEditState = useSelector(getUsernameEditState);
|
||||
const usernameLink = useSelector(getUsernameLink);
|
||||
const usernameLinkColor = useSelector(getUsernameLinkColor);
|
||||
const usernameLinkCorrupted = useSelector(getUsernameLinkCorrupted);
|
||||
const usernameLinkState = useSelector(getUsernameLinkState);
|
||||
|
||||
const {
|
||||
replaceAvatar,
|
||||
saveAvatarToDisk,
|
||||
saveAttachment,
|
||||
deleteAvatarFromDisk,
|
||||
myProfileChanged,
|
||||
} = useConversationsActions();
|
||||
const {
|
||||
resetUsernameLink,
|
||||
setUsernameLinkColor,
|
||||
setUsernameEditState,
|
||||
openUsernameReservationModal,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
deleteUsername,
|
||||
} = useUsernameActions();
|
||||
const { toggleProfileEditor, toggleProfileEditorHasError } =
|
||||
useGlobalModalActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { setEmojiSkinToneDefault } = useItemsActions();
|
||||
|
||||
return (
|
||||
<ProfileEditorModal
|
||||
aboutEmoji={aboutEmoji}
|
||||
aboutText={aboutText}
|
||||
color={color}
|
||||
conversationId={conversationId}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
deleteUsername={deleteUsername}
|
||||
familyName={familyName}
|
||||
firstName={firstName ?? ''}
|
||||
hasCompletedUsernameLinkOnboarding={hasCompletedUsernameLinkOnboarding}
|
||||
hasError={hasError}
|
||||
i18n={i18n}
|
||||
initialEditState={initialEditState}
|
||||
markCompletedUsernameLinkOnboarding={markCompletedUsernameLinkOnboarding}
|
||||
myProfileChanged={myProfileChanged}
|
||||
onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault}
|
||||
openUsernameReservationModal={openUsernameReservationModal}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
recentEmojis={recentEmojis}
|
||||
renderEditUsernameModalBody={renderEditUsernameModalBody}
|
||||
replaceAvatar={replaceAvatar}
|
||||
resetUsernameLink={resetUsernameLink}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
setUsernameEditState={setUsernameEditState}
|
||||
setUsernameLinkColor={setUsernameLinkColor}
|
||||
showToast={showToast}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
toggleProfileEditor={toggleProfileEditor}
|
||||
toggleProfileEditorHasError={toggleProfileEditorHasError}
|
||||
userAvatarData={userAvatarData}
|
||||
username={username}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
usernameEditState={usernameEditState}
|
||||
usernameLink={usernameLink}
|
||||
usernameLinkColor={usernameLinkColor}
|
||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||
usernameLinkState={usernameLinkState}
|
||||
/>
|
||||
);
|
||||
});
|
62
ts/state/smart/UsernameEditor.tsx
Normal file
62
ts/state/smart/UsernameEditor.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { UsernameEditor } from '../../components/UsernameEditor';
|
||||
import { getMinNickname, getMaxNickname } from '../../util/Username';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getUsernameReservationState,
|
||||
getUsernameReservationObject,
|
||||
getUsernameReservationError,
|
||||
getRecoveredUsername,
|
||||
} from '../selectors/username';
|
||||
import { getUsernameCorrupted } from '../selectors/items';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
|
||||
export type SmartUsernameEditorProps = Readonly<{
|
||||
onClose(): void;
|
||||
}>;
|
||||
|
||||
export const SmartUsernameEditor = memo(function SmartUsernameEditor({
|
||||
onClose,
|
||||
}: SmartUsernameEditorProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { username } = useSelector(getMe);
|
||||
const usernameCorrupted = useSelector(getUsernameCorrupted);
|
||||
const currentUsername = usernameCorrupted ? undefined : username;
|
||||
const minNickname = getMinNickname();
|
||||
const maxNickname = getMaxNickname();
|
||||
const state = useSelector(getUsernameReservationState);
|
||||
const recoveredUsername = useSelector(getRecoveredUsername);
|
||||
const reservation = useSelector(getUsernameReservationObject);
|
||||
const error = useSelector(getUsernameReservationError);
|
||||
const {
|
||||
setUsernameReservationError,
|
||||
clearUsernameReservation,
|
||||
reserveUsername,
|
||||
confirmUsername,
|
||||
} = useUsernameActions();
|
||||
const { showToast } = useToastActions();
|
||||
return (
|
||||
<UsernameEditor
|
||||
i18n={i18n}
|
||||
usernameCorrupted={usernameCorrupted}
|
||||
currentUsername={currentUsername}
|
||||
minNickname={minNickname}
|
||||
maxNickname={maxNickname}
|
||||
state={state}
|
||||
recoveredUsername={recoveredUsername}
|
||||
reservation={reservation}
|
||||
error={error}
|
||||
setUsernameReservationError={setUsernameReservationError}
|
||||
clearUsernameReservation={clearUsernameReservation}
|
||||
reserveUsername={reserveUsername}
|
||||
confirmUsername={confirmUsername}
|
||||
showToast={showToast}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -5,27 +5,35 @@ import React, { memo, useCallback } from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { UsernameOnboardingModal } from '../../components/UsernameOnboardingModal';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
import { NavTab, useNavActions } from '../ducks/nav';
|
||||
import { Page } from '../../components/Preferences';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
|
||||
export const SmartUsernameOnboardingModal = memo(
|
||||
function SmartUsernameOnboardingModal(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { toggleProfileEditor, toggleUsernameOnboarding } =
|
||||
useGlobalModalActions();
|
||||
const { toggleUsernameOnboarding } = useGlobalModalActions();
|
||||
const { openUsernameReservationModal } = useUsernameActions();
|
||||
const { changeLocation } = useNavActions();
|
||||
|
||||
const onNext = useCallback(async () => {
|
||||
await window.storage.put('hasCompletedUsernameOnboarding', true);
|
||||
openUsernameReservationModal();
|
||||
toggleProfileEditor(EditState.Username);
|
||||
changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: Page.Profile,
|
||||
state: EditState.Username,
|
||||
},
|
||||
});
|
||||
toggleUsernameOnboarding();
|
||||
}, [
|
||||
toggleProfileEditor,
|
||||
toggleUsernameOnboarding,
|
||||
changeLocation,
|
||||
openUsernameReservationModal,
|
||||
toggleUsernameOnboarding,
|
||||
]);
|
||||
|
||||
const onSkip = useCallback(async () => {
|
||||
|
|
|
@ -10,21 +10,6 @@ import {
|
|||
} from '../../../state/ducks/globalModals';
|
||||
|
||||
describe('both/state/ducks/globalModals', () => {
|
||||
describe('toggleProfileEditor', () => {
|
||||
const { toggleProfileEditor } = actions;
|
||||
|
||||
it('toggles isProfileEditorVisible', () => {
|
||||
const state = getEmptyState();
|
||||
const nextState = reducer(state, toggleProfileEditor());
|
||||
|
||||
assert.isTrue(nextState.isProfileEditorVisible);
|
||||
|
||||
const nextNextState = reducer(nextState, toggleProfileEditor());
|
||||
|
||||
assert.isFalse(nextNextState.isProfileEditorVisible);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showWhatsNewModal/hideWhatsNewModal', () => {
|
||||
const { showWhatsNewModal, hideWhatsNewModal } = actions;
|
||||
|
||||
|
|
|
@ -185,8 +185,8 @@ describe('pnp/username', function (this: Mocha.Suite) {
|
|||
|
||||
const window = await app.getWindow();
|
||||
|
||||
debug('opening avatar context menu');
|
||||
await window.getByRole('button', { name: 'Profile' }).click();
|
||||
debug('opening settings tab context menu');
|
||||
await window.locator('[data-key="Settings"]').click();
|
||||
|
||||
debug('opening username editor');
|
||||
const profileEditor = window.locator('.ProfileEditor');
|
||||
|
@ -198,7 +198,7 @@ describe('pnp/username', function (this: Mocha.Suite) {
|
|||
|
||||
debug('waiting for generated discriminator');
|
||||
const discriminator = profileEditor.locator(
|
||||
'.EditUsernameModalBody__discriminator__input[value]'
|
||||
'.UsernameEditor__discriminator__input[value]'
|
||||
);
|
||||
await discriminator.waitFor();
|
||||
|
||||
|
|
|
@ -856,17 +856,18 @@ export type GroupLogResponseType = {
|
|||
}
|
||||
);
|
||||
|
||||
export type ProfileRequestDataType = {
|
||||
about: string | null;
|
||||
aboutEmoji: string | null;
|
||||
avatar: boolean;
|
||||
sameAvatar: boolean;
|
||||
commitment: string;
|
||||
name: string;
|
||||
paymentAddress: string | null;
|
||||
phoneNumberSharing: string | null;
|
||||
version: string;
|
||||
};
|
||||
const uploadProfileZod = z.object({
|
||||
about: z.string().nullish(),
|
||||
aboutEmoji: z.string().nullish(),
|
||||
avatar: z.boolean(),
|
||||
sameAvatar: z.boolean(),
|
||||
commitment: z.string(),
|
||||
name: z.string(),
|
||||
paymentAddress: z.string().nullish(),
|
||||
phoneNumberSharing: z.string().nullish(),
|
||||
version: z.string(),
|
||||
});
|
||||
export type ProfileRequestDataType = z.infer<typeof uploadProfileZod>;
|
||||
|
||||
const uploadAvatarHeadersZod = z.object({
|
||||
acl: z.string(),
|
||||
|
@ -878,6 +879,14 @@ const uploadAvatarHeadersZod = z.object({
|
|||
signature: z.string(),
|
||||
});
|
||||
export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>;
|
||||
const uploadAvatarOrOther = z.union([
|
||||
uploadAvatarHeadersZod,
|
||||
z.string(),
|
||||
z.undefined(),
|
||||
]);
|
||||
export type UploadAvatarHeadersOrOtherType = z.infer<
|
||||
typeof uploadAvatarOrOther
|
||||
>;
|
||||
|
||||
const remoteConfigResponseZod = z.object({
|
||||
config: z
|
||||
|
@ -1544,7 +1553,7 @@ export type WebAPIType = {
|
|||
) => Promise<void>;
|
||||
putProfile: (
|
||||
jsonData: ProfileRequestDataType
|
||||
) => Promise<UploadAvatarHeadersType | undefined>;
|
||||
) => Promise<UploadAvatarHeadersOrOtherType>;
|
||||
putStickers: (
|
||||
encryptedManifest: Uint8Array,
|
||||
encryptedStickers: ReadonlyArray<Uint8Array>,
|
||||
|
@ -2644,13 +2653,13 @@ export function initialize({
|
|||
|
||||
async function putProfile(
|
||||
jsonData: ProfileRequestDataType
|
||||
): Promise<UploadAvatarHeadersType | undefined> {
|
||||
): Promise<UploadAvatarHeadersOrOtherType> {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
httpType: 'PUT',
|
||||
responseType: 'json',
|
||||
jsonData,
|
||||
zodSchema: uploadAvatarHeadersZod,
|
||||
zodSchema: uploadAvatarOrOther,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
getTitle,
|
||||
getTitleNoDefault,
|
||||
canHaveUsername,
|
||||
renderNumber,
|
||||
} from './getTitle';
|
||||
import { hasDraft } from './hasDraft';
|
||||
import { isAciString } from './isAciString';
|
||||
|
@ -135,6 +136,8 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
|
||||
const { customColor, customColorId } = getCustomColorData(attributes);
|
||||
|
||||
const isItMe = isMe(attributes);
|
||||
|
||||
// TODO: DESKTOP-720
|
||||
return {
|
||||
id: attributes.id,
|
||||
|
@ -193,7 +196,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
isBlocked: isBlocked(attributes),
|
||||
reportingToken: attributes.reportingToken,
|
||||
removalStage: attributes.removalStage,
|
||||
isMe: isMe(attributes),
|
||||
isMe: isItMe,
|
||||
isGroupV1AndDisabled: isGroupV1(attributes),
|
||||
isPinned: attributes.isPinned,
|
||||
isUntrusted: model.isUntrusted(),
|
||||
|
@ -228,7 +231,10 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
systemGivenName: attributes.systemGivenName,
|
||||
systemFamilyName: attributes.systemFamilyName,
|
||||
systemNickname: attributes.systemNickname,
|
||||
phoneNumber: getNumber(attributes),
|
||||
phoneNumber:
|
||||
isItMe && attributes.e164
|
||||
? renderNumber(attributes.e164)
|
||||
: getNumber(attributes),
|
||||
profileName: getProfileName(attributes),
|
||||
profileSharing: attributes.profileSharing,
|
||||
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
|
||||
|
|
|
@ -771,6 +771,14 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-03-26T17:14:14.370Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AvatarEditor.tsx",
|
||||
"line": " const tryClose = useRef<() => void | undefined>();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-05-24T03:40:20.019Z",
|
||||
"reasonDetail": "Holding on to a close function"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/AvatarTextEditor.tsx",
|
||||
|
@ -1414,6 +1422,14 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-07-30T16:57:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/ProfileEditor.tsx",
|
||||
"line": " const tryClose = useRef<() => void | undefined>();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-05-24T03:23:25.769Z",
|
||||
"reasonDetail": "Holding on to a close function"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/QrCode.tsx",
|
||||
|
@ -1572,6 +1588,22 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-08-10T00:23:35.320Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/UsernameEditor.tsx",
|
||||
"line": " const tryClose = useRef<() => void | undefined>();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-05-28T00:57:39.376Z",
|
||||
"reasonDetail": "Holding on to a close function"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/UsernameLinkEditor.tsx",
|
||||
"line": " const tryClose = useRef<() => void | undefined>();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-05-28T00:57:39.376Z",
|
||||
"reasonDetail": "Holding on to a close function"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/AttachmentStatusIcon.tsx",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue