mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-19 06:08:57 +01:00
A few updates for the standalone registration flow
This commit is contained in:
@@ -391,11 +391,13 @@ $loading-height: 16px;
|
||||
text-align: center;
|
||||
|
||||
font-size: 10pt;
|
||||
.number {
|
||||
.number,
|
||||
input.form-control {
|
||||
font-size: 12pt;
|
||||
border: 2px solid variables.$color-ultramarine;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.phone-input .number {
|
||||
@@ -406,7 +408,8 @@ $loading-height: 16px;
|
||||
@media (min-height: 750px) and (min-width: 700px) {
|
||||
font-size: 14pt;
|
||||
|
||||
.number {
|
||||
.number,
|
||||
input.form-control {
|
||||
font-size: 16pt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,41 +4,24 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories.preload.ts';
|
||||
import type { AppStateType } from '../state/ducks/app.preload.ts';
|
||||
import type { VerificationTransport } from '../types/VerificationTransport.std.ts';
|
||||
import { ThemeType } from '../types/Util.std.ts';
|
||||
import { AppViewType } from '../types/app.std.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration.dom.tsx';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility.dom.ts';
|
||||
import { TitlebarDragArea } from './TitlebarDragArea.dom.tsx';
|
||||
import { ThemeType } from '../types/Util.std.ts';
|
||||
|
||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories.preload.ts';
|
||||
import type { AppStateType } from '../state/ducks/app.preload.ts';
|
||||
|
||||
type PropsType = {
|
||||
state: AppStateType;
|
||||
openInbox: () => void;
|
||||
getCaptchaToken: () => Promise<string>;
|
||||
registerSingleDevice: (
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
) => Promise<void>;
|
||||
uploadProfile: (opts: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) => Promise<void>;
|
||||
renderCallManager: () => React.JSX.Element;
|
||||
renderGlobalModalContainer: () => React.JSX.Element;
|
||||
hasSelectedStoryData: boolean;
|
||||
readyForUpdates: () => void;
|
||||
renderStandaloneRegistration: () => React.JSX.Element;
|
||||
renderStoryViewer: (closeView: () => unknown) => React.JSX.Element;
|
||||
renderInstallScreen: () => React.JSX.Element;
|
||||
renderLightbox: () => React.JSX.Element | null;
|
||||
requestVerification: (
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
) => Promise<{ sessionId: string }>;
|
||||
theme: ThemeType;
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
@@ -51,23 +34,18 @@ type PropsType = {
|
||||
|
||||
export function App({
|
||||
state,
|
||||
getCaptchaToken,
|
||||
hasSelectedStoryData,
|
||||
isFullScreen,
|
||||
isMaximized,
|
||||
openInbox,
|
||||
osClassName,
|
||||
readyForUpdates,
|
||||
registerSingleDevice,
|
||||
renderCallManager,
|
||||
renderGlobalModalContainer,
|
||||
renderInbox,
|
||||
renderInstallScreen,
|
||||
renderLightbox,
|
||||
renderStandaloneRegistration,
|
||||
renderStoryViewer,
|
||||
requestVerification,
|
||||
theme,
|
||||
uploadProfile,
|
||||
viewStory,
|
||||
}: PropsType): React.JSX.Element {
|
||||
let contents;
|
||||
@@ -75,20 +53,7 @@ export function App({
|
||||
if (state.appView === AppViewType.Installer) {
|
||||
contents = renderInstallScreen();
|
||||
} else if (state.appView === AppViewType.Standalone) {
|
||||
const onComplete = () => {
|
||||
window.IPC.removeSetupMenuItems();
|
||||
openInbox();
|
||||
};
|
||||
contents = (
|
||||
<StandaloneRegistration
|
||||
onComplete={onComplete}
|
||||
getCaptchaToken={getCaptchaToken}
|
||||
readyForUpdates={readyForUpdates}
|
||||
requestVerification={requestVerification}
|
||||
registerSingleDevice={registerSingleDevice}
|
||||
uploadProfile={uploadProfile}
|
||||
/>
|
||||
);
|
||||
contents = renderStandaloneRegistration();
|
||||
} else if (state.appView === AppViewType.Inbox) {
|
||||
contents = renderInbox();
|
||||
} else if (state.appView === AppViewType.Blank) {
|
||||
|
||||
@@ -4,31 +4,45 @@
|
||||
import React from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import messages from '../../_locales/en/messages.json';
|
||||
import { setupI18n } from '../util/setupI18n.dom.tsx';
|
||||
|
||||
import { StandaloneRegistration } from './StandaloneRegistration.dom.tsx';
|
||||
import type { PropsType } from './StandaloneRegistration.dom.tsx';
|
||||
import { SECOND } from '../util/durations/index.std.ts';
|
||||
import { sleep } from '../util/sleep.std.ts';
|
||||
|
||||
const i18n = setupI18n('en', messages);
|
||||
|
||||
export default {
|
||||
title: 'Components/StandaloneRegistration',
|
||||
args: {
|
||||
getCaptchaToken: fn(async () => {
|
||||
i18n,
|
||||
getCaptchaToken: fn(async (...params) => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('getCaptchaToken', params);
|
||||
await sleep(SECOND);
|
||||
return 'captcha-token';
|
||||
}) as () => Promise<string>,
|
||||
requestVerification: fn(async () => {
|
||||
requestVerification: fn(async (...params) => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('requestVerification', params);
|
||||
await sleep(SECOND);
|
||||
return { sessionId: 'fake-session-id' };
|
||||
}) as () => Promise<{ sessionId: string }>,
|
||||
registerSingleDevice: fn(async () => {
|
||||
registerSingleDevice: fn(async (...params) => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('registerSingleDevice', params);
|
||||
await sleep(SECOND);
|
||||
}) as () => Promise<void>,
|
||||
uploadProfile: fn(async () => {
|
||||
uploadInitialProfile: fn(async (...params) => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('uploadInitialProfile', params);
|
||||
await sleep(SECOND);
|
||||
}) as () => Promise<void>,
|
||||
onComplete: fn() as () => void,
|
||||
readyForUpdates: fn() as () => void,
|
||||
onComplete: action('onComplete'),
|
||||
readyForUpdates: action('readyForUpdates'),
|
||||
},
|
||||
} satisfies Meta<PropsType & { daysAgo?: number }>;
|
||||
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { Iti } from 'intl-tel-input';
|
||||
import intlTelInput from 'intl-tel-input';
|
||||
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { Iti } from 'intl-tel-input';
|
||||
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import { parseNumber } from '../util/libphonenumberUtil.std.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import { VerificationTransport } from '../types/VerificationTransport.std.ts';
|
||||
import { normalizeProfileName } from '../util/normalizeProfileName.std.ts';
|
||||
import { TitlebarDragArea } from './TitlebarDragArea.dom.tsx';
|
||||
import { AvatarPreview } from './AvatarPreview.dom.tsx';
|
||||
import { AvatarColors } from '../types/Colors.std.ts';
|
||||
import { AvatarEditor } from './AvatarEditor.dom.tsx';
|
||||
|
||||
import type { LocalizerType } from '../types/I18N.std.ts';
|
||||
import type {
|
||||
AvatarDataType,
|
||||
DeleteAvatarFromDiskActionType,
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar.std.ts';
|
||||
|
||||
function PhoneInput({
|
||||
initialValue,
|
||||
@@ -41,7 +53,7 @@ function PhoneInput({
|
||||
|
||||
pluginRef.current?.destroy();
|
||||
|
||||
const plugin = intlTelInput(elem);
|
||||
const plugin = intlTelInput(elem, { formatAsYouType: true });
|
||||
pluginRef.current = plugin;
|
||||
},
|
||||
[initialValue]
|
||||
@@ -68,24 +80,20 @@ function PhoneInput({
|
||||
[setIsValid, onNumberChange, onValidation]
|
||||
);
|
||||
|
||||
// We don't always get change events when expected because of int-tel-input
|
||||
const onChange = useCallback(
|
||||
(_: ChangeEvent<HTMLInputElement>) => {
|
||||
if (elemRef.current) {
|
||||
validateNumber(elemRef.current.value);
|
||||
}
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
validateNumber(event.target.value);
|
||||
},
|
||||
[validateNumber]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// Pacify TypeScript and handle events bubbling up
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
validateNumber(event.target.value);
|
||||
}
|
||||
},
|
||||
[validateNumber]
|
||||
);
|
||||
// We validate in more scenarios to make things more responsive
|
||||
const validate = useCallback(() => {
|
||||
if (elemRef.current) {
|
||||
validateNumber(elemRef.current.value);
|
||||
}
|
||||
}, [validateNumber]);
|
||||
|
||||
return (
|
||||
<div className="phone-input">
|
||||
@@ -96,7 +104,8 @@ function PhoneInput({
|
||||
type="tel"
|
||||
ref={onRef}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={validate}
|
||||
onKeyUp={validate}
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
</div>
|
||||
@@ -342,18 +351,36 @@ function VerificationCodeStage({
|
||||
}
|
||||
|
||||
function ProfileNameStage({
|
||||
uploadProfile,
|
||||
conversationId,
|
||||
deleteAvatarFromDisk,
|
||||
i18n,
|
||||
onNext,
|
||||
replaceAvatar,
|
||||
saveAvatarToDisk,
|
||||
uploadInitialProfile,
|
||||
userAvatarData,
|
||||
}: {
|
||||
uploadProfile: (opts: {
|
||||
conversationId?: string;
|
||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
i18n: LocalizerType;
|
||||
onNext: () => void;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
uploadInitialProfile: (opts: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarData: Uint8Array<ArrayBuffer>;
|
||||
}) => Promise<void>;
|
||||
onNext: () => void;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
}): React.JSX.Element {
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [isEditingAvatar, setIsEditingAvatar] = useState(false);
|
||||
|
||||
const [avatarData, setAvatarData] = useState<
|
||||
Uint8Array<ArrayBuffer> | undefined
|
||||
>(undefined);
|
||||
|
||||
const onChangeFirstName = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => setFirstName(event.target.value),
|
||||
@@ -367,24 +394,78 @@ function ProfileNameStage({
|
||||
|
||||
const onNextClick = useCallback(
|
||||
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!avatarData) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await uploadProfile({ firstName, lastName });
|
||||
await uploadInitialProfile({
|
||||
firstName,
|
||||
lastName,
|
||||
avatarData,
|
||||
});
|
||||
onNext();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
},
|
||||
[onNext, firstName, lastName, uploadProfile]
|
||||
[onNext, firstName, lastName, avatarData, uploadInitialProfile]
|
||||
);
|
||||
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
|
||||
if (isEditingAvatar) {
|
||||
return (
|
||||
<div className="step-body">
|
||||
<div className="banner-image module-splash-screen__logo module-img--128" />
|
||||
<div className="header">Set up profile avatar</div>
|
||||
<AvatarEditor
|
||||
avatarColor={AvatarColors[0]}
|
||||
avatarUrl={undefined}
|
||||
avatarValue={avatarData}
|
||||
conversationId={conversationId}
|
||||
conversationTitle={fullName}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
i18n={i18n}
|
||||
onCancel={() => {
|
||||
setIsEditingAvatar(false);
|
||||
}}
|
||||
onSave={(avatar: Uint8Array<ArrayBuffer> | undefined) => {
|
||||
setAvatarData(avatar);
|
||||
setIsEditingAvatar(false);
|
||||
}}
|
||||
userAvatarData={userAvatarData}
|
||||
replaceAvatar={replaceAvatar}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="step-body">
|
||||
<div className="banner-image module-splash-screen__logo module-img--128" />
|
||||
<div className="header">Select Profile Name</div>
|
||||
|
||||
<div className="header">Set up profile</div>
|
||||
<AvatarPreview
|
||||
avatarColor={AvatarColors[0]}
|
||||
avatarUrl={undefined}
|
||||
avatarValue={avatarData}
|
||||
conversationTitle={fullName}
|
||||
i18n={i18n}
|
||||
onAvatarLoaded={avatar => {
|
||||
setAvatarData(avatar);
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsEditingAvatar(true);
|
||||
}}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={`form-control ${firstName ? 'valid' : 'invalid'}`}
|
||||
type="text"
|
||||
@@ -396,6 +477,7 @@ function ProfileNameStage({
|
||||
value={firstName}
|
||||
onChange={onChangeFirstName}
|
||||
/>
|
||||
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
@@ -407,15 +489,13 @@ function ProfileNameStage({
|
||||
value={lastName}
|
||||
onChange={onChangeLastName}
|
||||
/>
|
||||
|
||||
{/* TODO(indutny): highlight error */}
|
||||
<div>{error}</div>
|
||||
<div className="StandaloneRegistration__error">{error}</div>
|
||||
</div>
|
||||
<div className="nav">
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
disabled={Boolean(normalizeProfileName(firstName))}
|
||||
disabled={!normalizeProfileName(firstName) || !avatarData}
|
||||
onClick={onNextClick}
|
||||
>
|
||||
Finish
|
||||
@@ -426,8 +506,12 @@ function ProfileNameStage({
|
||||
}
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
onComplete: () => void;
|
||||
conversationId?: string;
|
||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
i18n: LocalizerType;
|
||||
getCaptchaToken: () => Promise<string>;
|
||||
onComplete: () => void;
|
||||
readyForUpdates: () => void;
|
||||
requestVerification: (
|
||||
number: string,
|
||||
captcha: string,
|
||||
@@ -438,20 +522,29 @@ export type PropsType = Readonly<{
|
||||
code: string,
|
||||
sessionId: string
|
||||
) => Promise<void>;
|
||||
uploadProfile: (opts: {
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
uploadInitialProfile: (opts: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarData: Uint8Array<ArrayBuffer>;
|
||||
}) => Promise<void>;
|
||||
readyForUpdates: () => void;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
}>;
|
||||
|
||||
export function StandaloneRegistration({
|
||||
onComplete,
|
||||
conversationId,
|
||||
deleteAvatarFromDisk,
|
||||
getCaptchaToken,
|
||||
requestVerification,
|
||||
registerSingleDevice,
|
||||
uploadProfile,
|
||||
i18n,
|
||||
onComplete,
|
||||
readyForUpdates,
|
||||
registerSingleDevice,
|
||||
replaceAvatar,
|
||||
requestVerification,
|
||||
saveAvatarToDisk,
|
||||
uploadInitialProfile,
|
||||
userAvatarData,
|
||||
}: PropsType): React.JSX.Element {
|
||||
useEffect(() => {
|
||||
readyForUpdates();
|
||||
@@ -515,8 +608,14 @@ export function StandaloneRegistration({
|
||||
body = (
|
||||
<ProfileNameStage
|
||||
{...stageData}
|
||||
uploadProfile={uploadProfile}
|
||||
conversationId={conversationId}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
i18n={i18n}
|
||||
onNext={onComplete}
|
||||
replaceAvatar={replaceAvatar}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
uploadInitialProfile={uploadInitialProfile}
|
||||
userAvatarData={userAvatarData}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { requestVerification as doRequestVerification } from '../../textsecure/WebAPI.preload.ts';
|
||||
import { accountManager } from '../../textsecure/AccountManager.preload.ts';
|
||||
import type { VerificationTransport } from '../../types/VerificationTransport.std.ts';
|
||||
import { DataWriter } from '../../sql/Client.preload.ts';
|
||||
import { App } from '../../components/App.dom.tsx';
|
||||
import OS from '../../util/os/osMain.node.ts';
|
||||
import { getConversation } from '../../util/getConversation.preload.ts';
|
||||
import { getChallengeURL } from '../../challenge.dom.ts';
|
||||
import { writeProfile } from '../../services/writeProfile.preload.ts';
|
||||
import { challengeHandler } from '../../services/challengeHandler.preload.ts';
|
||||
import { SmartCallManager } from './CallManager.preload.tsx';
|
||||
import { SmartGlobalModalContainer } from './GlobalModalContainer.preload.tsx';
|
||||
import { SmartLightbox } from './Lightbox.preload.tsx';
|
||||
@@ -22,7 +14,6 @@ import {
|
||||
getTheme,
|
||||
} from '../selectors/user.std.ts';
|
||||
import { hasSelectedStoryData as getHasSelectedStoryData } from '../selectors/stories.preload.ts';
|
||||
import { useAppActions } from '../ducks/app.preload.ts';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import { useStoriesActions } from '../ducks/stories.preload.ts';
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary.dom.tsx';
|
||||
@@ -31,6 +22,7 @@ import { SmartInbox } from './Inbox.preload.tsx';
|
||||
import { SmartInstallScreen } from './InstallScreen.preload.tsx';
|
||||
import { getApp } from '../selectors/app.std.ts';
|
||||
import { SmartFunProvider } from './FunProvider.preload.tsx';
|
||||
import { SmartStandaloneRegistration } from './StandaloneRegistration.preload.tsx';
|
||||
|
||||
function renderInbox(): React.JSX.Element {
|
||||
return <SmartInbox />;
|
||||
@@ -56,6 +48,14 @@ function renderLightbox(): React.JSX.Element {
|
||||
return <SmartLightbox />;
|
||||
}
|
||||
|
||||
function renderStandaloneRegistration(): React.JSX.Element {
|
||||
return (
|
||||
<ErrorBoundary name="App/renderStandaloneRegistration">
|
||||
<SmartStandaloneRegistration />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStoryViewer(closeView: () => unknown): React.JSX.Element {
|
||||
return (
|
||||
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
|
||||
@@ -64,51 +64,6 @@ function renderStoryViewer(closeView: () => unknown): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
async function getCaptchaToken(): Promise<string> {
|
||||
const url = getChallengeURL('registration');
|
||||
document.location.href = url;
|
||||
return challengeHandler.requestCaptcha({
|
||||
reason: 'standalone registration',
|
||||
});
|
||||
}
|
||||
|
||||
function requestVerification(
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> {
|
||||
return doRequestVerification(number, captcha, transport);
|
||||
}
|
||||
|
||||
function registerSingleDevice(
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> {
|
||||
return accountManager.registerSingleDevice(number, code, sessionId);
|
||||
}
|
||||
|
||||
function readyForUpdates(): void {
|
||||
window.IPC.readyForUpdates();
|
||||
}
|
||||
|
||||
async function uploadProfile({
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}): Promise<void> {
|
||||
const us = window.ConversationController.getOurConversationOrThrow();
|
||||
us.set({ profileName: firstName, profileFamilyName: lastName });
|
||||
us.captureChange('standaloneProfile');
|
||||
await DataWriter.updateConversation(us.attributes);
|
||||
|
||||
await writeProfile(getConversation(us), {
|
||||
keepAvatar: true,
|
||||
});
|
||||
}
|
||||
|
||||
export const SmartApp = memo(function SmartApp() {
|
||||
const state = useSelector(getApp);
|
||||
const isMaximized = useSelector(getIsMainWindowMaximized);
|
||||
@@ -116,7 +71,6 @@ export const SmartApp = memo(function SmartApp() {
|
||||
const hasSelectedStoryData = useSelector(getHasSelectedStoryData);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const { openInbox } = useAppActions();
|
||||
const { scrollToMessage } = useConversationsActions();
|
||||
const { viewStory } = useStoriesActions();
|
||||
|
||||
@@ -128,21 +82,16 @@ export const SmartApp = memo(function SmartApp() {
|
||||
state={state}
|
||||
isMaximized={isMaximized}
|
||||
isFullScreen={isFullScreen}
|
||||
getCaptchaToken={getCaptchaToken}
|
||||
osClassName={osClassName}
|
||||
renderCallManager={renderCallManager}
|
||||
renderGlobalModalContainer={renderGlobalModalContainer}
|
||||
renderInstallScreen={renderInstallScreen}
|
||||
renderLightbox={renderLightbox}
|
||||
renderStandaloneRegistration={renderStandaloneRegistration}
|
||||
hasSelectedStoryData={hasSelectedStoryData}
|
||||
readyForUpdates={readyForUpdates}
|
||||
renderStoryViewer={renderStoryViewer}
|
||||
renderInbox={renderInbox}
|
||||
requestVerification={requestVerification}
|
||||
registerSingleDevice={registerSingleDevice}
|
||||
uploadProfile={uploadProfile}
|
||||
theme={theme}
|
||||
openInbox={openInbox}
|
||||
scrollToMessage={scrollToMessage}
|
||||
viewStory={viewStory}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { DataWriter } from '../../sql/Client.preload.ts';
|
||||
import { getConversation } from '../../util/getConversation.preload.ts';
|
||||
import { writeProfile } from '../../services/writeProfile.preload.ts';
|
||||
import { useAppActions } from '../ducks/app.preload.ts';
|
||||
import { requestVerification as doRequestVerification } from '../../textsecure/WebAPI.preload.ts';
|
||||
import { accountManager } from '../../textsecure/AccountManager.preload.ts';
|
||||
import { getChallengeURL } from '../../challenge.dom.ts';
|
||||
import { challengeHandler } from '../../services/challengeHandler.preload.ts';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getIntl, getUserConversationId } from '../selectors/user.std.ts';
|
||||
import { StandaloneRegistration } from '../../components/StandaloneRegistration.dom.tsx';
|
||||
|
||||
import type { VerificationTransport } from '../../types/VerificationTransport.std.ts';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import { getMe } from '../selectors/conversations.dom.ts';
|
||||
|
||||
export const SmartStandaloneRegistration = memo(
|
||||
function SmartStandaloneRegistration() {
|
||||
const { openInbox } = useAppActions();
|
||||
const { deleteAvatarFromDisk, replaceAvatar, saveAvatarToDisk } =
|
||||
useConversationsActions();
|
||||
|
||||
const i18n = useSelector(getIntl);
|
||||
const conversationId = useSelector(getUserConversationId);
|
||||
const me = useSelector(getMe);
|
||||
const userAvatarData = me?.avatars ?? [];
|
||||
|
||||
const onComplete = () => {
|
||||
window.IPC.removeSetupMenuItems();
|
||||
openInbox();
|
||||
};
|
||||
|
||||
return (
|
||||
<StandaloneRegistration
|
||||
conversationId={conversationId}
|
||||
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
||||
getCaptchaToken={getCaptchaToken}
|
||||
i18n={i18n}
|
||||
onComplete={onComplete}
|
||||
readyForUpdates={readyForUpdates}
|
||||
registerSingleDevice={registerSingleDevice}
|
||||
replaceAvatar={replaceAvatar}
|
||||
requestVerification={requestVerification}
|
||||
saveAvatarToDisk={saveAvatarToDisk}
|
||||
uploadInitialProfile={uploadInitialProfile}
|
||||
userAvatarData={userAvatarData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
async function getCaptchaToken(): Promise<string> {
|
||||
const url = getChallengeURL('registration');
|
||||
document.location.href = url;
|
||||
return challengeHandler.requestCaptcha({
|
||||
reason: 'standalone registration',
|
||||
});
|
||||
}
|
||||
|
||||
function readyForUpdates(): void {
|
||||
window.IPC.readyForUpdates();
|
||||
}
|
||||
|
||||
function registerSingleDevice(
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> {
|
||||
return accountManager.registerSingleDevice(number, code, sessionId);
|
||||
}
|
||||
|
||||
function requestVerification(
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> {
|
||||
return doRequestVerification(number, captcha, transport);
|
||||
}
|
||||
|
||||
async function uploadInitialProfile({
|
||||
firstName,
|
||||
lastName,
|
||||
avatarData,
|
||||
}: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarData: Uint8Array<ArrayBuffer>;
|
||||
}): Promise<void> {
|
||||
const us = window.ConversationController.getOurConversationOrThrow();
|
||||
us.set({ profileName: firstName, profileFamilyName: lastName });
|
||||
us.captureChange('standaloneProfile');
|
||||
await DataWriter.updateConversation(us.attributes);
|
||||
|
||||
await writeProfile(getConversation(us), {
|
||||
keepAvatar: false,
|
||||
avatarUpdate: {
|
||||
oldAvatar: undefined,
|
||||
newAvatar: avatarData,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user