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