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 && ( + + )}