From a6f8f232a8f8a10767369fac099555d7df16f036 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 23 Apr 2025 18:57:46 +0200 Subject: [PATCH] Introduce Multi-Accounts --- public/compatTest.js | 4 +- src/api/gramjs/apiBuilders/appConfig.ts | 1 + src/api/gramjs/localDb.ts | 14 +- src/api/gramjs/methods/client.ts | 3 +- src/api/gramjs/worker/connector.ts | 25 ++-- src/api/types/misc.ts | 8 +- src/assets/localization/fallback.strings | 4 + src/bundles/calls.ts | 2 +- src/bundles/main.ts | 8 -- src/components/App.tsx | 37 ++--- src/components/auth/Auth.scss | 6 + src/components/auth/Auth.tsx | 2 +- src/components/auth/AuthCode.tsx | 2 +- src/components/auth/AuthPhoneNumber.tsx | 68 ++++++--- src/components/auth/AuthQrCode.tsx | 41 ++++-- src/components/auth/CountryCodeInput.tsx | 2 +- src/components/auth/helpers/backNavigation.ts | 13 ++ .../calls/group/GroupCallParticipantVideo.tsx | 2 +- src/components/calls/phone/PhoneCall.tsx | 6 +- src/components/common/AnimatedSticker.tsx | 2 +- src/components/common/Audio.tsx | 6 +- src/components/common/Avatar.tsx | 11 +- src/components/common/Composer.tsx | 13 +- src/components/common/CustomEmojiPicker.tsx | 2 +- src/components/common/Document.tsx | 4 +- src/components/common/File.tsx | 2 +- src/components/common/FullNameTitle.tsx | 11 +- src/components/common/GifButton.tsx | 2 +- src/components/common/PasswordForm.tsx | 2 +- src/components/common/ProfileInfo.tsx | 2 +- src/components/common/ProfilePhoto.tsx | 2 +- src/components/common/SliderDots.tsx | 2 +- src/components/common/StickerButton.tsx | 2 +- src/components/common/StickerView.tsx | 2 +- .../common/helpers/mediaDimensions.ts | 2 +- src/components/common/helpers/renderText.tsx | 2 +- .../common/hooks/useAnimatedEmoji.ts | 2 +- src/components/common/pickers/PickerItem.tsx | 2 +- .../common/profile/BusinessHours.tsx | 2 +- .../common/profile/UserBirthday.tsx | 2 +- .../common/reactions/CustomEmojiEffect.tsx | 2 +- .../common/reactions/PaidReactionEmoji.tsx | 2 +- src/components/left/LeftColumn.tsx | 6 +- src/components/left/main/AccountMenuItems.tsx | 117 ++++++++++++++++ src/components/left/main/Chat.tsx | 2 +- src/components/left/main/ChatFolders.tsx | 2 +- src/components/left/main/ChatList.tsx | 2 +- src/components/left/main/ForumPanel.tsx | 2 +- src/components/left/main/LeftMain.tsx | 2 +- src/components/left/main/LeftMainHeader.scss | 41 ++++-- src/components/left/main/LeftMainHeader.tsx | 15 +- .../left/main/LeftSideMenuItems.tsx | 36 ++++- src/components/left/main/StatusButton.tsx | 2 +- src/components/left/main/Topic.tsx | 2 +- .../left/main/hooks/useTopicContextActions.ts | 2 +- src/components/left/newChat/NewChat.tsx | 2 +- .../search/helpers/createMapStateToProps.ts | 9 +- src/components/left/settings/Settings.tsx | 2 +- .../left/settings/SettingsCustomEmoji.tsx | 4 +- .../left/settings/SettingsDataStorage.tsx | 4 +- .../left/settings/SettingsDoNotTranslate.tsx | 4 +- .../left/settings/SettingsExperimental.tsx | 32 +++-- .../left/settings/SettingsGeneral.tsx | 69 +++++---- .../settings/SettingsGeneralBackground.tsx | 4 +- .../SettingsGeneralBackgroundColor.tsx | 4 +- .../left/settings/SettingsLanguage.tsx | 17 +-- .../left/settings/SettingsPerformance.tsx | 8 +- .../left/settings/SettingsPrivacy.tsx | 8 +- .../left/settings/SettingsStickers.tsx | 4 +- .../settings/twoFa/SettingsTwoFaEmailCode.tsx | 2 +- .../twoFa/SettingsTwoFaSkippableForm.tsx | 2 +- src/components/main/DownloadManager.tsx | 2 +- src/components/main/Main.tsx | 10 +- src/components/main/NewContactModal.tsx | 2 +- .../main/premium/PremiumFeatureModal.tsx | 9 +- .../mediaViewer/MediaViewerContent.tsx | 3 +- .../mediaViewer/MediaViewerFooter.tsx | 2 +- .../mediaViewer/MediaViewerSlides.tsx | 2 +- src/components/mediaViewer/SeekLine.tsx | 2 +- src/components/mediaViewer/VideoPlayer.tsx | 2 +- .../mediaViewer/VideoPlayerControls.tsx | 2 +- .../mediaViewer/helpers/ghostAnimation.ts | 2 +- .../middle/EmojiInteractionAnimation.tsx | 2 +- src/components/middle/HeaderActions.tsx | 2 +- .../middle/MessageSelectToolbar.tsx | 9 +- src/components/middle/MiddleColumn.tsx | 9 +- .../middle/RequirementToContactMessage.tsx | 4 +- .../middle/composer/AttachBotIcon.tsx | 4 +- .../middle/composer/AttachBotItem.tsx | 4 +- src/components/middle/composer/AttachMenu.tsx | 6 +- .../middle/composer/AttachmentModal.tsx | 4 +- .../middle/composer/BotCommandMenu.tsx | 2 +- .../middle/composer/BotKeyboardMenu.tsx | 2 +- .../middle/composer/CustomSendMenu.tsx | 2 +- .../middle/composer/EmojiButton.tsx | 2 +- .../middle/composer/EmojiPicker.tsx | 2 +- src/components/middle/composer/GifPicker.tsx | 2 +- .../middle/composer/InlineBotTooltip.tsx | 2 +- .../middle/composer/MessageInput.tsx | 13 +- src/components/middle/composer/SendAsMenu.tsx | 2 +- .../middle/composer/StickerPicker.tsx | 2 +- .../middle/composer/StickerSetCover.tsx | 2 +- src/components/middle/composer/SymbolMenu.tsx | 2 +- .../middle/composer/WebPagePreview.tsx | 4 +- .../helpers/applyIosAutoCapitalizationFix.ts | 2 +- .../composer/hooks/useCustomEmojiTooltip.ts | 2 +- .../composer/hooks/useStickerTooltip.ts | 2 +- .../composer/hooks/useVoiceRecording.ts | 2 +- .../middle/helpers/preventMessageInputBlur.ts | 2 +- .../middle/hooks/useMessageObservers.ts | 2 +- src/components/middle/hooks/useScrollHooks.ts | 2 +- .../middle/message/ActionMessage.tsx | 6 +- src/components/middle/message/Album.tsx | 4 +- src/components/middle/message/BaseStory.tsx | 2 +- src/components/middle/message/Invoice.tsx | 4 +- src/components/middle/message/Location.tsx | 4 +- src/components/middle/message/Message.tsx | 10 +- .../middle/message/MessagePhoneCall.tsx | 2 +- src/components/middle/message/Photo.tsx | 4 +- .../middle/message/SponsoredMessage.tsx | 6 +- src/components/middle/message/Sticker.tsx | 2 +- src/components/middle/message/WebPage.tsx | 4 +- .../middle/message/helpers/copyOptions.ts | 2 +- .../message/helpers/getCustomAppendixBg.ts | 4 +- .../middle/message/helpers/messageActions.tsx | 2 +- .../middle/message/hooks/useOuterHandlers.ts | 2 +- src/components/middle/panes/AudioPlayer.tsx | 2 +- .../middle/panes/HeaderPinnedMessage.tsx | 2 +- src/components/middle/search/MiddleSearch.tsx | 2 +- src/components/modals/gift/GiftComposer.tsx | 5 +- src/components/modals/gift/GiftModal.tsx | 4 +- .../locationAccess/LocationAccessModal.tsx | 2 +- src/components/modals/map/MapModal.tsx | 2 +- .../preparedMessage/PreparedMessageModal.tsx | 4 +- src/components/modals/webApp/WebAppModal.tsx | 8 +- .../modals/webApp/WebAppModalTabContent.tsx | 2 +- src/components/right/GifSearch.tsx | 2 +- src/components/right/Profile.tsx | 11 +- src/components/story/Story.tsx | 2 +- src/components/story/StorySlides.tsx | 2 +- .../story/helpers/ghostAnimation.ts | 2 +- src/components/ui/Button.tsx | 2 +- src/components/ui/InfiniteScroll.tsx | 2 +- src/components/ui/ListItem.tsx | 2 +- src/components/ui/Menu.tsx | 2 +- src/components/ui/MenuItem.scss | 1 + src/components/ui/MenuItem.tsx | 10 +- src/components/ui/ResponsiveHoverButton.tsx | 2 +- src/components/ui/Tab.tsx | 2 +- src/components/ui/TabList.tsx | 2 +- src/config.ts | 17 ++- src/global/actions/api/bots.ts | 17 +-- src/global/actions/api/initial.ts | 49 +++++-- src/global/actions/api/messages.ts | 2 +- src/global/actions/api/settings.ts | 19 +-- src/global/actions/apiUpdaters/calls.async.ts | 2 +- src/global/actions/apiUpdaters/calls.ts | 2 +- src/global/actions/apiUpdaters/initial.ts | 15 +- src/global/actions/ui/bots.ts | 25 ++-- src/global/actions/ui/calls.ts | 2 +- src/global/actions/ui/chats.ts | 2 +- src/global/actions/ui/initial.ts | 31 +++-- src/global/actions/ui/messages.ts | 2 +- src/global/actions/ui/misc.ts | 5 +- src/global/actions/ui/passcode.ts | 2 +- src/global/actions/ui/settings.ts | 97 ++++++------- src/global/cache.ts | 118 +++++++++++----- src/global/helpers/messageMedia.ts | 7 +- src/global/helpers/misc.ts | 17 +++ src/global/init.ts | 13 +- src/global/initialState.ts | 63 +++++---- src/global/reducers/passcode.ts | 43 +++--- src/global/reducers/settings.ts | 43 ++++-- src/global/reducers/sharedState.ts | 13 ++ src/global/selectors/chats.ts | 2 +- src/global/selectors/messages.ts | 2 +- src/global/selectors/settings.ts | 8 +- src/global/selectors/sharedState.ts | 13 ++ src/global/selectors/ui.ts | 15 +- src/global/shared/sharedState.worker.ts | 80 +++++++++++ src/global/shared/sharedStateConnector.ts | 116 ++++++++++++++++ src/global/types/actions.ts | 6 +- src/global/types/globalState.ts | 17 +-- src/global/types/index.ts | 1 + src/global/types/sharedState.ts | 37 +++++ src/hooks/animations/useViewTransition.ts | 2 +- src/hooks/media/useUnsupportedMedia.ts | 2 +- src/hooks/scroll/useTopOverscroll.tsx | 2 +- src/hooks/useAppLayout.ts | 2 +- src/hooks/useCanvasBlur.ts | 2 +- src/hooks/useChatContextActions.ts | 2 +- src/hooks/useContextMenuHandlers.ts | 2 +- src/hooks/useElectronDrag.ts | 2 +- src/hooks/useFastClick.ts | 2 +- src/hooks/useFocusAfterAnimation.ts | 2 +- src/hooks/useHistoryBack.ts | 2 +- src/hooks/useMediaWithLoadProgress.ts | 2 +- src/hooks/useMouseInside.ts | 2 +- src/hooks/useMultiaccountInfo.ts | 94 +++++++++++++ src/hooks/usePictureInPicture.ts | 2 +- src/hooks/usePreventPinchZoomGesture.ts | 2 +- src/hooks/useStreaming.ts | 2 +- src/hooks/window/useFullscreen.ts | 2 +- src/index.tsx | 43 +++--- src/lib/gramjs/client/auth.ts | 6 +- src/lib/gramjs/sessions/CallbackSession.ts | 4 +- src/lib/gramjs/types.ts | 2 +- src/lib/rlottie/RLottie.ts | 6 +- src/lib/video-preview/VideoPreview.ts | 2 +- src/serviceWorker/progressive.ts | 25 +++- src/styles/index.scss | 12 ++ src/types/index.ts | 62 +++++++-- src/types/language.d.ts | 4 + src/util/PopupManager.ts | 2 +- src/util/animateScroll.ts | 2 +- src/util/audioPlayer.ts | 2 +- src/util/betterView.ts | 2 +- src/util/browser/globalEnvironment.ts | 6 + src/util/{ => browser}/multitab.ts | 59 +++----- src/util/{ => browser}/windowEnvironment.ts | 10 +- src/util/cacheApi.ts | 11 +- src/util/captureEvents.ts | 2 +- src/util/data/freeze.ts | 5 + src/util/deepDiff.ts | 7 + src/util/deepLinkParser.ts | 2 +- src/util/deepMerge.ts | 12 +- src/util/deeplink.ts | 2 +- src/util/emoji/customEmojiManager.ts | 2 +- src/util/establishMultitabRole.ts | 15 +- src/util/focusEditableElement.ts | 2 +- src/util/init.ts | 13 +- src/util/languageDetection.ts | 2 +- src/util/localization/index.ts | 22 ++- src/util/mediaLoader.ts | 36 +++-- src/util/multiaccount.ts | 131 ++++++++++++++++++ src/util/notifications.tsx | 2 +- src/util/oldLangProvider.ts | 4 +- src/util/parseHtmlAsFormattedText.ts | 2 +- src/util/resetScroll.ts | 2 +- src/util/routing.ts | 2 +- src/util/sessions.ts | 117 +++++++++++++--- src/util/setupServiceWorker.ts | 2 +- src/util/swipeController.ts | 2 +- src/util/switchTheme.ts | 4 +- src/util/updatePageTitle.ts | 2 +- src/util/updateWebmanifest.ts | 2 +- src/util/websync.ts | 2 +- src/util/windowSize.ts | 2 +- 248 files changed, 1841 insertions(+), 853 deletions(-) create mode 100644 src/components/auth/helpers/backNavigation.ts create mode 100644 src/components/left/main/AccountMenuItems.tsx create mode 100644 src/global/reducers/sharedState.ts create mode 100644 src/global/selectors/sharedState.ts create mode 100644 src/global/shared/sharedState.worker.ts create mode 100644 src/global/shared/sharedStateConnector.ts create mode 100644 src/global/types/sharedState.ts create mode 100644 src/hooks/useMultiaccountInfo.ts create mode 100644 src/util/browser/globalEnvironment.ts rename src/util/{ => browser}/multitab.ts (87%) rename src/util/{ => browser}/windowEnvironment.ts (94%) create mode 100644 src/util/data/freeze.ts create mode 100644 src/util/multiaccount.ts diff --git a/public/compatTest.js b/public/compatTest.js index 808c4160d..74a3bbaaf 100644 --- a/public/compatTest.js +++ b/public/compatTest.js @@ -11,9 +11,10 @@ function compatTest() { var hasNumberFormat = hasIntl && typeof Intl.NumberFormat !== 'undefined'; var hasWebLocks = typeof navigator.locks !== 'undefined'; var hasBigInt = typeof BigInt !== 'undefined'; + var hasBroadcastChannel = typeof BroadcastChannel !== 'undefined'; var isCompatible = hasPromise && hasWebSockets && hasWebCrypto && hasObjectFromEntries && hasResizeObserver - && hasCssSupports && hasDisplayNames && hasPluralRules && hasNumberFormat && hasWebLocks && hasBigInt; + && hasCssSupports && hasDisplayNames && hasPluralRules && hasNumberFormat && hasWebLocks && hasBigInt && hasBroadcastChannel; if (isCompatible || (window.localStorage && window.localStorage.getItem('tt-ignore-compat'))) { window.isCompatTestPassed = true; @@ -33,6 +34,7 @@ function compatTest() { console.warn('Intl.NumberFormat', hasNumberFormat); console.warn('WebLocks', hasWebLocks); console.warn('BigInt', hasBigInt); + console.warn('BroadcastChannel', hasBroadcastChannel); } // Hardcoded page because server forbids iframe embedding diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 11110debb..ce6d6ffd7 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -159,6 +159,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'), recommendedChannels: getLimit(appConfig, 'recommended_channels_limit', 'recommendedChannels'), savedDialogsPinned: getLimit(appConfig, 'saved_dialogs_pinned_limit', 'savedDialogsPinned'), + moreAccounts: DEFAULT_LIMITS.moreAccounts, }, hash, areStoriesHidden: appConfig.stories_all_hidden, diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index 871a506b2..3d659f541 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -1,13 +1,11 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../lib/gramjs'; -import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../config'; +import { DEBUG } from '../../config'; +import { DATA_BROADCAST_CHANNEL_NAME } from '../../util/multiaccount'; import { throttle } from '../../util/schedulers'; import { omitVirtualClassFields } from './apiBuilders/helpers'; -// eslint-disable-next-line no-restricted-globals -const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self; - export type StoryRepairInfo = { type: 'story'; peerId: string; @@ -36,7 +34,7 @@ export interface LocalDb { channelPtsById: Record; } -const channel = IS_MULTITAB_SUPPORTED ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) : undefined; +const channel = new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME); let batchedUpdates: { name: string; @@ -44,7 +42,7 @@ let batchedUpdates: { value: any; }[] = []; const throttledLocalDbUpdate = throttle(() => { - channel!.postMessage({ + channel.postMessage({ type: 'localDbUpdate', batchedUpdates, }); @@ -109,9 +107,7 @@ function createLocalDbInitial(initial?: LocalDb): LocalDb { return acc2; }, {} as Record); - acc[key] = IS_MULTITAB_SUPPORTED - ? createProxy(key, convertedValue) - : convertedValue; + acc[key] = createProxy(key, convertedValue); return acc; }, {} as LocalDb) as LocalDb; } diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 0610cae3d..892c06f39 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -76,7 +76,7 @@ export async function init(initialArgs: ApiInitialArgs) { const { userAgent, platform, sessionData, isWebmSupported, maxBufferSize, webAuthToken, dcId, mockScenario, shouldForceHttpTransport, shouldAllowHttpTransport, - shouldDebugExportedSenders, langCode, isTestServerRequested, + shouldDebugExportedSenders, langCode, isTestServerRequested, accountIds, } = initialArgs; const session = new sessions.CallbackSession(sessionData, onSessionUpdate); @@ -133,6 +133,7 @@ export async function init(initialArgs: ApiInitialArgs) { webAuthToken, webAuthTokenFailed: onWebAuthTokenFailed, mockScenario, + accountIds, }); } catch (err: any) { // eslint-disable-next-line no-console diff --git a/src/api/gramjs/worker/connector.ts b/src/api/gramjs/worker/connector.ts index 0fc0bab56..51b530e98 100644 --- a/src/api/gramjs/worker/connector.ts +++ b/src/api/gramjs/worker/connector.ts @@ -1,17 +1,17 @@ import type { Api } from '../../../lib/gramjs'; -import type { TypedBroadcastChannel } from '../../../util/multitab'; +import type { TypedBroadcastChannel } from '../../../util/browser/multitab'; import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; import type { LocalDb } from '../localDb'; import type { MethodArgs, MethodResponse, Methods } from '../methods/types'; import type { OriginPayload, ThenArg, WorkerMessageEvent } from './types'; -import { DATA_BROADCAST_CHANNEL_NAME, DEBUG, IGNORE_UNHANDLED_ERRORS } from '../../../config'; +import { DEBUG, IGNORE_UNHANDLED_ERRORS } from '../../../config'; import { logDebugMessage } from '../../../util/debugConsole'; import Deferred from '../../../util/Deferred'; import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole'; import generateUniqueId from '../../../util/generateUniqueId'; +import { ACCOUNT_SLOT, DATA_BROADCAST_CHANNEL_NAME } from '../../../util/multiaccount'; import { pause, throttleWithTickEnd } from '../../../util/schedulers'; -import { IS_MULTITAB_SUPPORTED } from '../../../util/windowEnvironment'; type RequestState = { messageId: string; @@ -48,9 +48,7 @@ subscribeToMasterChange((isMasterTabNew) => { isMasterTab = isMasterTabNew; }); -const channel = IS_MULTITAB_SUPPORTED - ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel - : undefined; +const channel = new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel; const postMessagesOnTickEnd = throttleWithTickEnd(() => { const payloads = pendingPayloads; @@ -64,8 +62,6 @@ function postMessageOnTickEnd(payload: OriginPayload) { } export function initApiOnMasterTab(initialArgs: ApiInitialArgs) { - if (!channel) return; - channel.postMessage({ type: 'initApi', token: getCurrentTabId(), @@ -93,7 +89,14 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) { console.log('>>> START LOAD WORKER'); } - worker = new Worker(new URL('./worker.ts', import.meta.url)); + const params = new URLSearchParams(); + if (ACCOUNT_SLOT) { + params.set('account', String(ACCOUNT_SLOT)); + } + + worker = new Worker(new URL('./worker.ts', import.meta.url), { + name: params.toString(), + }); subscribeToWorker(onUpdate); if (initialArgs.platform === 'iOS') { @@ -132,8 +135,6 @@ export function updateFullLocalDb(initial: LocalDb) { } export function callApiOnMasterTab(payload: any) { - if (!channel) return; - channel.postMessage({ type: 'callApi', token: getCurrentTabId(), @@ -254,8 +255,6 @@ export function cancelApiProgress(progressCallback: ApiOnProgress) { if (isMasterTab) { cancelApiProgressMaster(messageId); } else { - if (!channel) return; - channel.postMessage({ type: 'cancelApiProgress', token: getCurrentTabId(), diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 9652f73ae..4ff505494 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -23,6 +23,7 @@ export interface ApiInitialArgs { shouldDebugExportedSenders?: boolean; langCode: string; isTestServerRequested?: boolean; + accountIds?: string[]; } export interface ApiOnProgress { @@ -102,7 +103,7 @@ export interface ApiWebSession { export interface ApiSessionData { mainDcId: number; - keys: Record; + keys: Record; isTest?: true; } @@ -343,10 +344,11 @@ export type ApiLimitType = | 'chatlistInvites' | 'chatlistJoined' | 'recommendedChannels' - | 'savedDialogsPinned'; + | 'savedDialogsPinned' + | 'moreAccounts'; export type ApiLimitTypeWithModal = Exclude; export type ApiLimitTypeForPromo = Exclude>> FINISH LOAD MAIN BUNDLE'); } - -const { passcode: { isScreenLocked }, connectionState } = getGlobal(); -if (!connectionState && !isScreenLocked && !IS_MULTITAB_SUPPORTED) { - getActions().initApi(); -} diff --git a/src/components/App.tsx b/src/components/App.tsx index d10cef8bd..0d9b83bce 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,6 @@ import type { FC } from '../lib/teact/teact'; import React, { useEffect, useLayoutEffect } from '../lib/teact/teact'; -import { getActions, withGlobal } from '../global'; +import { withGlobal } from '../global'; import type { GlobalState } from '../global/types'; import type { ThemeKey } from '../types'; @@ -10,12 +10,12 @@ import { DARK_THEME_BG_COLOR, INACTIVE_MARKER, LIGHT_THEME_BG_COLOR, PAGE_TITLE, } from '../config'; import { selectTabState, selectTheme } from '../global/selectors'; -import { addActiveTabChangeListener } from '../util/activeTabMonitor'; +import { IS_INSTALL_PROMPT_SUPPORTED, PLATFORM_ENV } from '../util/browser/windowEnvironment'; import buildClassName from '../util/buildClassName'; import { setupBeforeInstallPrompt } from '../util/installPrompt'; -import { parseInitialLocationHash } from '../util/routing'; +import { ACCOUNT_SLOT, getAccountsInfo, getAccountSlotUrl } from '../util/multiaccount'; +import { getInitialLocationHash, parseInitialLocationHash } from '../util/routing'; import { hasStoredSession } from '../util/sessions'; -import { IS_INSTALL_PROMPT_SUPPORTED, IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from '../util/windowEnvironment'; import { updateSizes } from '../util/windowSize'; import useAppLayout from '../hooks/useAppLayout'; @@ -61,8 +61,6 @@ const App: FC = ({ isTestServer, theme, }) => { - const { disconnect } = getActions(); - const [isInactive, markInactive, unmarkInactive] = useFlag(false); const { isMobile } = useAppLayout(); const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android'; @@ -73,6 +71,22 @@ const App: FC = ({ } }, []); + useEffect(() => { + const hash = getInitialLocationHash(); + // If there is no stored session on first slot, navigate to any other slot with stored session + if (!hasStoredSession() && !ACCOUNT_SLOT && !hash) { + const accounts = getAccountsInfo(); + Object.keys(accounts).forEach((key) => { + const slot = Number(key); + const account = accounts[slot]; + if (account) { + const url = getAccountSlotUrl(slot); + window.location.href = `${url}#${hash || 'login'}`; + } + }); + } + }, []); + // Prevent drop on elements that do not accept it useEffect(() => { const body = document.body; @@ -161,17 +175,6 @@ const App: FC = ({ updateSizes(); }, []); - useEffect(() => { - if (IS_MULTITAB_SUPPORTED) return; - - addActiveTabChangeListener(() => { - disconnect(); - document.title = INACTIVE_PAGE_TITLE; - - markInactive(); - }); - }, [activeKey, disconnect, markInactive]); - useEffect(() => { if (isInactiveAuth) { document.title = INACTIVE_PAGE_TITLE; diff --git a/src/components/auth/Auth.scss b/src/components/auth/Auth.scss index 30db9437b..4dc074fb2 100644 --- a/src/components/auth/Auth.scss +++ b/src/components/auth/Auth.scss @@ -225,6 +225,12 @@ right: 0.5rem; } +.auth-close { + position: absolute; + top: 1rem; + left: 1rem; +} + @keyframes qr-show { 0% { opacity: 0; diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx index dea02d3a8..ec7592790 100644 --- a/src/components/auth/Auth.tsx +++ b/src/components/auth/Auth.tsx @@ -6,7 +6,7 @@ import { getActions, withGlobal } from '../../global'; import type { GlobalState } from '../../global/types'; -import { PLATFORM_ENV } from '../../util/windowEnvironment'; +import { PLATFORM_ENV } from '../../util/browser/windowEnvironment'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useElectronDrag from '../../hooks/useElectronDrag'; diff --git a/src/components/auth/AuthCode.tsx b/src/components/auth/AuthCode.tsx index e8fc8cd2a..1beb1b3fa 100644 --- a/src/components/auth/AuthCode.tsx +++ b/src/components/auth/AuthCode.tsx @@ -7,8 +7,8 @@ import { getActions, withGlobal } from '../../global'; import type { GlobalState } from '../../global/types'; +import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { pick } from '../../util/iteratees'; -import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; diff --git a/src/components/auth/AuthPhoneNumber.tsx b/src/components/auth/AuthPhoneNumber.tsx index 1bf9c3e47..afb126aa8 100644 --- a/src/components/auth/AuthPhoneNumber.tsx +++ b/src/components/auth/AuthPhoneNumber.tsx @@ -1,7 +1,7 @@ import type { ChangeEvent } from 'react'; import type { FC } from '../../lib/teact/teact'; import React, { - memo, useCallback, useEffect, useLayoutEffect, useRef, useState, + memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; @@ -9,18 +9,23 @@ import type { ApiCountryCode } from '../../api/types'; import type { GlobalState } from '../../global/types'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; +import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { preloadImage } from '../../util/files'; import preloadFonts from '../../util/fonts'; import { pick } from '../../util/iteratees'; +import { getAccountSlotUrl } from '../../util/multiaccount'; import { oldSetLanguage } from '../../util/oldLangProvider'; import { formatPhoneNumber, getCountryCodeByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber'; -import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/windowEnvironment'; +import { navigateBack } from './helpers/backNavigation'; import { getSuggestedLanguage } from './helpers/getSuggestedLanguage'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLangString from '../../hooks/useLangString'; +import useLastCallback from '../../hooks/useLastCallback'; +import useMultiaccountInfo from '../../hooks/useMultiaccountInfo'; +import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import Checkbox from '../ui/Checkbox'; import InputText from '../ui/InputText'; @@ -62,7 +67,7 @@ const AuthPhoneNumber: FC = ({ loadCountryList, clearAuthErrorKey, goToAuthQrCode, - setSettingOption, + setSharedSettingOption, } = getActions(); const lang = useLang(); @@ -78,6 +83,16 @@ const AuthPhoneNumber: FC = ({ const [lastSelection, setLastSelection] = useState<[number, number] | undefined>(); const [isLoading, markIsLoading, unmarkIsLoading] = useFlag(); + const accountsInfo = useMultiaccountInfo(); + const hasActiveAccount = Object.values(accountsInfo).length > 0; + const phoneNumberSlots = useMemo(() => ( + Object.entries(accountsInfo) + .reduce((acc, [key, { phone }]) => { + if (phone) acc[phone] = Number(key); + return acc; + }, {} as Record) + ), [accountsInfo]); + const fullNumber = country ? `+${country.countryCode} ${phoneNumber || ''}` : phoneNumber; const canSubmit = fullNumber && fullNumber.replace(/[^\d]+/g, '').length >= MIN_NUMBER_LENGTH; @@ -105,7 +120,7 @@ const AuthPhoneNumber: FC = ({ } }, [country, authNearestCountry, isTouched, phoneCodeList]); - const parseFullNumber = useCallback((newFullNumber: string) => { + const parseFullNumber = useLastCallback((newFullNumber: string) => { if (!newFullNumber.length) { setPhoneNumber(''); } @@ -123,17 +138,17 @@ const AuthPhoneNumber: FC = ({ setCountry(selectedCountry); } setPhoneNumber(formatPhoneNumber(newFullNumber, selectedCountry)); - }, [phoneCodeList, country]); + }); - const handleLangChange = useCallback(() => { + const handleLangChange = useLastCallback(() => { markIsLoading(); void oldSetLanguage(suggestedLanguage, () => { unmarkIsLoading(); - setSettingOption({ language: suggestedLanguage }); + setSharedSettingOption({ language: suggestedLanguage }); }); - }, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]); + }); useEffect(() => { if (phoneNumber === undefined && authPhoneNumber) { @@ -148,19 +163,23 @@ const AuthPhoneNumber: FC = ({ }, [lastSelection]); const isJustPastedRef = useRef(false); - const handlePaste = useCallback(() => { + const handlePaste = useLastCallback(() => { isJustPastedRef.current = true; requestMeasure(() => { isJustPastedRef.current = false; }); - }, []); + }); - const handleCountryChange = useCallback((value: ApiCountryCode) => { + const handleBackNavigation = useLastCallback(() => { + navigateBack(); + }); + + const handleCountryChange = useLastCallback((value: ApiCountryCode) => { setCountry(value); setPhoneNumber(''); - }, []); + }); - const handlePhoneNumberChange = useCallback((e: ChangeEvent) => { + const handlePhoneNumberChange = useLastCallback((e: ChangeEvent) => { if (authErrorKey) { clearAuthErrorKey(); } @@ -186,11 +205,11 @@ const AuthPhoneNumber: FC = ({ && value.length - fullNumber.length > 1 && !isJustPastedRef.current ); parseFullNumber(shouldFixSafariAutoComplete ? `${country!.countryCode} ${value}` : value); - }, [authErrorKey, country, fullNumber, parseFullNumber]); + }); - const handleKeepSessionChange = useCallback((e: ChangeEvent) => { + const handleKeepSessionChange = useLastCallback((e: ChangeEvent) => { setAuthRememberMe(e.target.checked); - }, [setAuthRememberMe]); + }); function handleSubmit(event: React.FormEvent) { event.preventDefault(); @@ -199,19 +218,30 @@ const AuthPhoneNumber: FC = ({ return; } + const adaptedPhoneNumber = fullNumber?.replace(/[^\d]/g, ''); + if (adaptedPhoneNumber && phoneNumberSlots[adaptedPhoneNumber]) { + window.location.replace(getAccountSlotUrl(phoneNumberSlots[adaptedPhoneNumber])); + return; + } + if (canSubmit) { setAuthPhoneNumber({ phoneNumber: fullNumber }); } } - const handleGoToAuthQrCode = useCallback(() => { + const handleGoToAuthQrCode = useLastCallback(() => { goToAuthQrCode(); - }, [goToAuthQrCode]); + }); const isAuthReady = authState === 'authorizationStateWaitPhoneNumber'; return (
+ {hasActiveAccount && ( + + )}