mirror of
https://github.com/danog/telegram-tt.git
synced 2024-11-26 12:24:46 +01:00
Re-write browser history; Introduce playwright tests (another attempt) (#1809)
This commit is contained in:
parent
0c5b3e560e
commit
697b709d89
@ -14,3 +14,4 @@ src/lib/music-metadata-browser
|
||||
webpack.config.js
|
||||
jest.config.js
|
||||
src/lib/secret-sauce/
|
||||
playwright.config.ts
|
||||
|
@ -5,6 +5,7 @@ module.exports = {
|
||||
'<rootDir>/tests/staticFileMock.js',
|
||||
},
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/tests/playwright/',
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/legacy_notes_and_workbook/',
|
||||
'<rootDir>/client/src/stylesheets/',
|
||||
|
2276
package-lock.json
generated
2276
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,9 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env APP_ENV=development webpack serve --env mode=dev --env isDevServer --mode development --config ./webpack.config.js",
|
||||
"dev:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 webpack serve --env mode=dev --env isDevServer --mode development --config ./webpack.config.js --port 1235",
|
||||
"build": "webpack --mode production",
|
||||
"build:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 webpack --env mode=dev --mode development --config ./webpack.config.js",
|
||||
"build:staging": "rm -rf dist/ && APP_ENV=staging APP_VERSION=$(npm run print_version --silent) npm run build && ./deploy/copy_to_dist.sh",
|
||||
"build:production": "npm i && rm -rf dist/ && APP_VERSION=$(npm run inc_version --silent) APP_ENV=production npm run build && ./deploy/copy_to_dist.sh",
|
||||
"deploy:production": "npm run build:production && git add -A && git commit -a -m '[Build]' --no-verify && git push",
|
||||
@ -17,6 +19,8 @@
|
||||
"gramjs:tl": "node ./src/lib/gramjs/tl/generateModules.js",
|
||||
"gramjs:lint:fix": "eslint ./src/lib/gramjs --fix",
|
||||
"test": "cross-env APP_ENV=test jest --verbose --forceExit",
|
||||
"test:playwright": "playwright test",
|
||||
"test:record": "playwright codegen localhost:1235",
|
||||
"prepare": "husky install",
|
||||
"statoscope:validate": "statoscope validate --input public/build-stats.json",
|
||||
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json"
|
||||
@ -47,6 +51,7 @@
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@statoscope/cli": "^5.20.1",
|
||||
"@statoscope/webpack-plugin": "^5.20.1",
|
||||
"@playwright/test": "^1.18.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@types/croppie": "^2.6.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
@ -92,6 +97,7 @@
|
||||
"replace-in-file": "^6.3.2",
|
||||
"sass": "^1.50.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"serve": "^13.0.2",
|
||||
"style-loader": "^3.3.1",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.0",
|
||||
|
38
playwright.config.ts
Normal file
38
playwright.config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { PlaywrightTestConfig, devices } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: 'tests/playwright',
|
||||
timeout: process.env.CI ? 60 * 5 * 1000 : 30 * 1000,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
webServer: {
|
||||
command: 'npm run build:mocked && serve -l 1235 dist',
|
||||
port: 1235,
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:1235/',
|
||||
video: 'retain-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'ios',
|
||||
use: { ...devices['iPhone X'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
export default config;
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
TelegramClient, sessions, Api as GramJs, connection,
|
||||
sessions, Api as GramJs, connection,
|
||||
} from '../../../lib/gramjs';
|
||||
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
|
||||
|
||||
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
|
||||
import { TwoFaParams } from '../../../lib/gramjs/client/2fa';
|
||||
|
||||
|
@ -42,10 +42,11 @@ const Auth: FC<StateProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
useHistoryBack(
|
||||
(!isMobile && authState === 'authorizationStateWaitPhoneNumber')
|
||||
|| (isMobile && authState === 'authorizationStateWaitQrCode'), handleChangeAuthorizationMethod,
|
||||
);
|
||||
useHistoryBack({
|
||||
isActive: (!isMobile && authState === 'authorizationStateWaitPhoneNumber')
|
||||
|| (isMobile && authState === 'authorizationStateWaitQrCode'),
|
||||
onBack: handleChangeAuthorizationMethod,
|
||||
});
|
||||
|
||||
// Prevent refresh when rotating device
|
||||
useEffect(() => {
|
||||
|
@ -45,7 +45,10 @@ const AuthCode: FC<StateProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
useHistoryBack(true, returnToAuthPhoneNumber);
|
||||
useHistoryBack({
|
||||
isActive: true,
|
||||
onBack: returnToAuthPhoneNumber,
|
||||
});
|
||||
|
||||
const onCodeChange = useCallback((e: FormEvent<HTMLInputElement>) => {
|
||||
if (authError) {
|
||||
|
@ -15,10 +15,13 @@ export type OwnProps = {
|
||||
onContentChange: (content: LeftColumnContent) => void;
|
||||
};
|
||||
|
||||
const ArchivedChats: FC<OwnProps> = ({ isActive, onReset, onContentChange }) => {
|
||||
const ArchivedChats: FC<OwnProps> = ({ isActive, onReset }) => {
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onContentChange, LeftColumnContent.Archived);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="ArchivedChats">
|
||||
|
@ -134,7 +134,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}) : undefined), [activeChatFolder, setActiveChatFolder]);
|
||||
|
||||
useHistoryBack(activeChatFolder !== 0, () => setActiveChatFolder(0, { forceOnHeavyAnimation: true }));
|
||||
useHistoryBack({
|
||||
isActive: activeChatFolder !== 0,
|
||||
onBack: () => setActiveChatFolder(0, { forceOnHeavyAnimation: true }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
@ -58,7 +58,10 @@ const ContactList: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
useHistoryBack(isActive, onReset);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const handleClick = useCallback((id: string) => {
|
||||
openChat({ id, shouldReplaceHistory: true });
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
DEBUG,
|
||||
FEEDBACK_URL,
|
||||
IS_BETA,
|
||||
IS_TEST,
|
||||
} from '../../../config';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -26,7 +27,6 @@ import { clearWebsync } from '../../../util/websync';
|
||||
import { selectCurrentMessageList, selectTheme } from '../../../global/selectors';
|
||||
import { isChatArchived } from '../../../global/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { disableHistoryBack } from '../../../hooks/useHistoryBack';
|
||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
@ -129,7 +129,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, !areChatsLoaded,
|
||||
);
|
||||
|
||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME;
|
||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
||||
|
||||
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }) => (
|
||||
@ -191,7 +191,6 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
const handleSwitchToWebK = useCallback(() => {
|
||||
setPermanentWebVersion('K');
|
||||
clearWebsync();
|
||||
disableHistoryBack();
|
||||
}, []);
|
||||
|
||||
const handleOpenTipsChat = useCallback(() => {
|
||||
@ -302,7 +301,6 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
<MenuItem
|
||||
icon="char-W"
|
||||
href={LEGACY_VERSION_URL}
|
||||
onClick={disableHistoryBack}
|
||||
>
|
||||
Switch to Old Version
|
||||
</MenuItem>
|
||||
|
@ -64,7 +64,10 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const handleFilterChange = useCallback((query: string) => {
|
||||
setGlobalSearchQuery({ query });
|
||||
|
@ -46,7 +46,10 @@ const NewChatStep2: FC<OwnProps & StateProps > = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [about, setAbout] = useState('');
|
||||
|
@ -76,7 +76,10 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
|
||||
setGlobalSearchDate({ date: value.getTime() / 1000 });
|
||||
}, [setGlobalSearchDate]);
|
||||
|
||||
useHistoryBack(isActive, onReset, undefined, undefined, true);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -169,7 +169,6 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.EditProfile:
|
||||
return (
|
||||
<SettingsEditProfile
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive && isScreenActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
@ -188,15 +187,15 @@ const Settings: FC<OwnProps> = ({
|
||||
);
|
||||
case SettingsScreens.QuickReaction:
|
||||
return (
|
||||
<SettingsQuickReaction onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||
<SettingsQuickReaction isActive={isScreenActive} onReset={handleReset} />
|
||||
);
|
||||
case SettingsScreens.Notifications:
|
||||
return (
|
||||
<SettingsNotifications onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||
<SettingsNotifications isActive={isScreenActive} onReset={handleReset} />
|
||||
);
|
||||
case SettingsScreens.DataStorage:
|
||||
return (
|
||||
<SettingsDataStorage onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||
<SettingsDataStorage isActive={isScreenActive} onReset={handleReset} />
|
||||
);
|
||||
case SettingsScreens.Privacy:
|
||||
return (
|
||||
@ -208,7 +207,7 @@ const Settings: FC<OwnProps> = ({
|
||||
);
|
||||
case SettingsScreens.Language:
|
||||
return (
|
||||
<SettingsLanguage onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
|
||||
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
||||
);
|
||||
case SettingsScreens.GeneralChatBackground:
|
||||
return (
|
||||
@ -221,7 +220,6 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.GeneralChatBackgroundColor:
|
||||
return (
|
||||
<SettingsGeneralBackgroundColor
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
@ -229,7 +227,6 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.ActiveSessions:
|
||||
return (
|
||||
<SettingsActiveSessions
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
@ -237,7 +234,6 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyBlockedUsers:
|
||||
return (
|
||||
<SettingsPrivacyBlockedUsers
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
|
@ -5,7 +5,6 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ApiSession } from '../../../api/types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import { formatPastTimeShort } from '../../../util/dateFormat';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
@ -22,7 +21,6 @@ import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -34,7 +32,6 @@ type StateProps = {
|
||||
|
||||
const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
byHash,
|
||||
orderedHashes,
|
||||
@ -119,7 +116,10 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
||||
}, [byHash, orderedHashes]);
|
||||
const hasOtherSessions = Boolean(otherSessionHashes.length);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.ActiveSessions);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
function renderCurrentSession(session: ApiSession) {
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { SettingsScreens, ISettings } from '../../../types';
|
||||
import { ISettings } from '../../../types';
|
||||
|
||||
import { AUTODOWNLOAD_FILESIZE_MB_LIMITS } from '../../../config';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
@ -13,7 +13,6 @@ import RangeSlider from '../../ui/RangeSlider';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -37,7 +36,6 @@ type StateProps = Pick<ISettings, (
|
||||
|
||||
const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
canAutoLoadPhotoFromContacts,
|
||||
canAutoLoadPhotoInPrivateChats,
|
||||
@ -59,7 +57,10 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const renderFileSizeCallback = useCallback((value: number) => {
|
||||
return lang('AutodownloadSizeLimitUpTo', lang('FileSize.MB', String(AUTODOWNLOAD_FILESIZE_MB_LIMITS[value]), 'i'));
|
||||
|
@ -5,7 +5,7 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
import { ProfileEditProgress, SettingsScreens } from '../../../types';
|
||||
import { ProfileEditProgress } from '../../../types';
|
||||
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
@ -23,7 +23,6 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
type OwnProps = {
|
||||
isActive: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -46,7 +45,6 @@ const ERROR_BIO_TOO_LONG = 'Bio can\' be longer than 70 characters';
|
||||
|
||||
const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
currentAvatarHash,
|
||||
currentFirstName,
|
||||
@ -87,7 +85,10 @@ const SettingsEditProfile: FC<OwnProps & StateProps> = ({
|
||||
return Boolean(photo) || isProfileFieldsTouched || isUsernameAvailable === true;
|
||||
}, [photo, isProfileFieldsTouched, isUsernameError, isUsernameAvailable]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.EditProfile);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
// Due to the parent Transition, this component never gets unmounted,
|
||||
// that's why we use throttled API call on every update.
|
||||
|
@ -167,7 +167,10 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
||||
return stickerSetsById?.[id]?.installedDate ? stickerSetsById[id] : false;
|
||||
}).filter<ApiStickerSet>(Boolean as any);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content custom-scroll">
|
||||
|
@ -108,7 +108,10 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackground);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const isUploading = loadedWallpapers?.[0] && loadedWallpapers[0].slug === UPLOADING_WALLPAPER_SLUG;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { SettingsScreens, ThemeKey } from '../../../types';
|
||||
import { ThemeKey } from '../../../types';
|
||||
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import {
|
||||
@ -22,7 +22,6 @@ import './SettingsGeneralBackgroundColor.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -52,7 +51,6 @@ const PREDEFINED_COLORS = [
|
||||
|
||||
const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
theme,
|
||||
backgroundColor,
|
||||
@ -205,7 +203,10 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
|
||||
isDragging && 'is-dragging',
|
||||
);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackgroundColor);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className}>
|
||||
|
@ -3,7 +3,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ISettings, LangCode, SettingsScreens } from '../../../types';
|
||||
import { ISettings, LangCode } from '../../../types';
|
||||
import { ApiLanguage } from '../../../api/types';
|
||||
|
||||
import { setLanguage } from '../../../util/langProvider';
|
||||
@ -15,7 +15,6 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -23,7 +22,6 @@ type StateProps = Pick<ISettings, 'languages' | 'language'>;
|
||||
|
||||
const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
languages,
|
||||
language,
|
||||
@ -56,7 +54,10 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
return languages ? buildOptions(languages) : undefined;
|
||||
}, [languages]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Language);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content settings-item settings-language custom-scroll settings-item--first">
|
||||
|
@ -43,7 +43,10 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [lastSyncTime, profileId, loadProfilePhotos]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
|
@ -5,8 +5,6 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import { playNotifySound } from '../../../util/notifications';
|
||||
@ -16,7 +14,6 @@ import RangeSlider from '../../ui/RangeSlider';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -35,7 +32,6 @@ type StateProps = {
|
||||
|
||||
const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
hasPrivateChatsNotifications,
|
||||
hasPrivateChatsMessagePreview,
|
||||
@ -136,7 +132,10 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Notifications);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content custom-scroll">
|
||||
|
@ -59,7 +59,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Privacy);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
function getVisibilityValue(visibility?: PrivacyVisibility) {
|
||||
switch (visibility) {
|
||||
|
@ -4,7 +4,6 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { ApiChat, ApiCountryCode, ApiUser } from '../../../api/types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import { CHAT_HEIGHT_PX } from '../../../config';
|
||||
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
|
||||
@ -25,7 +24,6 @@ import BlockUserModal from './BlockUserModal';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -38,7 +36,6 @@ type StateProps = {
|
||||
|
||||
const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
chatsByIds,
|
||||
usersByIds,
|
||||
@ -53,7 +50,10 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
|
||||
unblockContact({ contactId });
|
||||
}, [unblockContact]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.PrivacyBlockedUsers);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
function renderContact(contactId: string, i: number, viewportOffset: number) {
|
||||
const isPrivate = isUserId(contactId);
|
||||
|
@ -84,7 +84,10 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [lang, screen]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, screen);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const descriptionText = useMemo(() => {
|
||||
switch (screen) {
|
||||
|
@ -92,7 +92,10 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
onScreenSelect(SettingsScreens.Privacy);
|
||||
}, [isAllowList, newSelectedContactIds, onScreenSelect, screen, setPrivacySettings]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, screen);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="NewChat-inner step-1">
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { SettingsScreens } from '../../../types';
|
||||
import { ApiAvailableReaction } from '../../../api/types';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -11,7 +10,6 @@ import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -23,12 +21,15 @@ type StateProps = {
|
||||
const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onReset,
|
||||
onScreenSelect,
|
||||
availableReactions,
|
||||
selectedReaction,
|
||||
}) => {
|
||||
const { setDefaultReaction } = getActions();
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General);
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const options = availableReactions?.filter((l) => !l.isInactive).map((l) => {
|
||||
return {
|
||||
|
@ -91,7 +91,6 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
<SettingsFoldersMain
|
||||
onCreateFolder={handleCreateFolder}
|
||||
onEditFolder={handleEditFolder}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.FoldersCreateFolder,
|
||||
SettingsScreens.FoldersEditFolder,
|
||||
@ -111,7 +110,6 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
onAddIncludedChats={handleAddIncludedChats}
|
||||
onAddExcludedChats={handleAddExcludedChats}
|
||||
onReset={handleReset}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.FoldersIncludedChats,
|
||||
SettingsScreens.FoldersExcludedChats,
|
||||
@ -127,7 +125,6 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onReset={handleReset}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
@ -139,7 +136,6 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onReset={handleReset}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
|
@ -3,8 +3,6 @@ import React, {
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getGlobal } from '../../../../global';
|
||||
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { unique } from '../../../../util/iteratees';
|
||||
|
||||
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../../config';
|
||||
@ -26,7 +24,6 @@ type OwnProps = {
|
||||
state: FoldersState;
|
||||
dispatch: FolderEditDispatch;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -35,7 +32,6 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
||||
state,
|
||||
dispatch,
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
}) => {
|
||||
const { chatFilter } = state;
|
||||
@ -103,12 +99,10 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
||||
}
|
||||
}, [mode, selectedChatIds, dispatch]);
|
||||
|
||||
useHistoryBack(
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onReset,
|
||||
onScreenSelect,
|
||||
mode === 'included' ? SettingsScreens.FoldersIncludedChats : SettingsScreens.FoldersExcludedChats,
|
||||
);
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
if (!displayedIds) {
|
||||
return <Loading />;
|
||||
|
@ -3,8 +3,6 @@ import React, {
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
||||
import { isUserId } from '../../../../global/helpers';
|
||||
@ -34,7 +32,6 @@ type OwnProps = {
|
||||
onAddIncludedChats: () => void;
|
||||
onAddExcludedChats: () => void;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
@ -57,7 +54,6 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
onAddIncludedChats,
|
||||
onAddExcludedChats,
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
onBack,
|
||||
loadedActiveChatIds,
|
||||
@ -120,9 +116,10 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onBack, onScreenSelect, state.mode === 'edit'
|
||||
? SettingsScreens.FoldersEditFolder
|
||||
: SettingsScreens.FoldersCreateFolder);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack,
|
||||
});
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { currentTarget } = event;
|
||||
|
@ -4,7 +4,6 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import { ApiChatFolder } from '../../../../api/types';
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { throttle } from '../../../../util/schedulers';
|
||||
@ -23,7 +22,6 @@ type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onCreateFolder: () => void;
|
||||
onEditFolder: (folder: ApiChatFolder) => void;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -41,7 +39,6 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onCreateFolder,
|
||||
onEditFolder,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
orderedFolderIds,
|
||||
foldersById,
|
||||
@ -88,7 +85,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Folders);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const chatsCountByFolderId = useFolderManagerForChatsCount();
|
||||
const userFolders = useMemo(() => {
|
||||
|
@ -161,7 +161,6 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<SettingsTwoFaStart
|
||||
onStart={handleStartWizard}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaNewPassword,
|
||||
SettingsScreens.TwoFaNewPasswordConfirm,
|
||||
@ -177,11 +176,9 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaNewPassword:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
placeholder={lang('PleaseEnterPassword')}
|
||||
submitLabel={lang('Continue')}
|
||||
onSubmit={handleNewPassword}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaNewPasswordConfirm,
|
||||
SettingsScreens.TwoFaNewPasswordHint,
|
||||
@ -196,12 +193,10 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaNewPasswordConfirm:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
expectedPassword={state.password}
|
||||
placeholder={lang('PleaseReEnterPassword')}
|
||||
submitLabel={lang('Continue')}
|
||||
onSubmit={handleNewPasswordConfirm}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaNewPasswordHint,
|
||||
SettingsScreens.TwoFaNewPasswordEmail,
|
||||
@ -218,8 +213,6 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
icon="hint"
|
||||
placeholder={lang('PasswordHintPlaceholder')}
|
||||
onSubmit={handleNewPasswordHint}
|
||||
screen={currentScreen}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaNewPasswordEmail,
|
||||
SettingsScreens.TwoFaNewPasswordEmailCode,
|
||||
@ -240,8 +233,6 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
placeholder={lang('RecoveryEmailTitle')}
|
||||
shouldConfirm
|
||||
onSubmit={handleNewPasswordEmail}
|
||||
screen={currentScreen}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaNewPasswordEmailCode,
|
||||
SettingsScreens.TwoFaCongratulations,
|
||||
@ -257,8 +248,6 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
error={error}
|
||||
clearError={clearTwoFaError}
|
||||
onSubmit={handleEmailCode}
|
||||
screen={currentScreen}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
|
||||
onReset={onReset}
|
||||
/>
|
||||
@ -295,13 +284,11 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaChangePasswordCurrent:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
clearError={clearTwoFaError}
|
||||
hint={hint}
|
||||
onSubmit={handleChangePasswordCurrent}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaChangePasswordNew,
|
||||
SettingsScreens.TwoFaChangePasswordConfirm,
|
||||
@ -315,10 +302,8 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaChangePasswordNew:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
placeholder={lang('PleaseEnterNewFirstPassword')}
|
||||
onSubmit={handleChangePasswordNew}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaChangePasswordConfirm,
|
||||
SettingsScreens.TwoFaChangePasswordHint,
|
||||
@ -331,11 +316,9 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaChangePasswordConfirm:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
expectedPassword={state.password}
|
||||
placeholder={lang('PleaseReEnterPassword')}
|
||||
onSubmit={handleChangePasswordConfirm}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaChangePasswordHint,
|
||||
SettingsScreens.TwoFaCongratulations,
|
||||
@ -353,10 +336,8 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
icon="hint"
|
||||
placeholder={lang('PasswordHintPlaceholder')}
|
||||
onSubmit={handleChangePasswordHint}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
|
||||
onReset={onReset}
|
||||
screen={currentScreen}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -368,23 +349,19 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
clearError={clearTwoFaError}
|
||||
hint={hint}
|
||||
onSubmit={handleTurnOff}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive}
|
||||
onReset={onReset}
|
||||
screen={currentScreen}
|
||||
/>
|
||||
);
|
||||
|
||||
case SettingsScreens.TwoFaRecoveryEmailCurrentPassword:
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
clearError={clearTwoFaError}
|
||||
hint={hint}
|
||||
onSubmit={handleRecoveryEmailCurrentPassword}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaRecoveryEmail,
|
||||
SettingsScreens.TwoFaRecoveryEmailCode,
|
||||
@ -397,12 +374,10 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaRecoveryEmail:
|
||||
return (
|
||||
<SettingsTwoFaSkippableForm
|
||||
screen={currentScreen}
|
||||
icon="email"
|
||||
type="email"
|
||||
placeholder={lang('RecoveryEmailTitle')}
|
||||
onSubmit={handleRecoveryEmail}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.TwoFaRecoveryEmailCode,
|
||||
SettingsScreens.TwoFaCongratulations,
|
||||
@ -414,12 +389,10 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
|
||||
case SettingsScreens.TwoFaRecoveryEmailCode:
|
||||
return (
|
||||
<SettingsTwoFaEmailCode
|
||||
screen={currentScreen}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
clearError={clearTwoFaError}
|
||||
onSubmit={handleEmailCode}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
|
||||
onReset={onReset}
|
||||
/>
|
||||
|
@ -30,7 +30,10 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
||||
onScreenSelect(SettingsScreens.Privacy);
|
||||
}, [onScreenSelect]);
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaCongratulations);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
|
@ -4,7 +4,6 @@ import React, {
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import { ApiSticker } from '../../../../api/types';
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment';
|
||||
import { selectAnimatedEmoji } from '../../../../global/selectors';
|
||||
@ -21,9 +20,7 @@ type OwnProps = {
|
||||
clearError: NoneToVoidFunction;
|
||||
onSubmit: (hint: string) => void;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
screen: SettingsScreens;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -41,9 +38,7 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
|
||||
clearError,
|
||||
onSubmit,
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
screen,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -60,7 +55,10 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, screen);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (error && clearError) {
|
||||
|
@ -27,7 +27,10 @@ const SettingsTwoFaEnabled: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaEnabled);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
|
@ -2,8 +2,6 @@ import React, {
|
||||
FC, memo, useCallback, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
|
||||
@ -11,7 +9,6 @@ import PasswordMonkey from '../../../common/PasswordMonkey';
|
||||
import PasswordForm from '../../../common/PasswordForm';
|
||||
|
||||
type OwnProps = {
|
||||
screen: SettingsScreens;
|
||||
error?: string;
|
||||
isLoading?: boolean;
|
||||
expectedPassword?: string;
|
||||
@ -21,16 +18,13 @@ type OwnProps = {
|
||||
clearError?: NoneToVoidFunction;
|
||||
onSubmit: (password: string) => void;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
const EQUAL_PASSWORD_ERROR = 'Passwords Should Be Equal';
|
||||
|
||||
const SettingsTwoFaPassword: FC<OwnProps> = ({
|
||||
screen,
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
error,
|
||||
isLoading,
|
||||
@ -61,7 +55,10 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, screen);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
|
@ -4,7 +4,6 @@ import React, {
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import { ApiSticker } from '../../../../api/types';
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment';
|
||||
import { selectAnimatedEmoji } from '../../../../global/selectors';
|
||||
@ -28,9 +27,7 @@ type OwnProps = {
|
||||
clearError?: NoneToVoidFunction;
|
||||
onSubmit: (value?: string) => void;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
screen: SettingsScreens;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -49,9 +46,7 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
|
||||
clearError,
|
||||
onSubmit,
|
||||
isActive,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
screen,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -96,7 +91,10 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, screen);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
|
@ -2,7 +2,6 @@ import React, { FC, memo } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import { ApiSticker } from '../../../../api/types';
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import { selectAnimatedEmoji } from '../../../../global/selectors';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
@ -14,7 +13,6 @@ import AnimatedEmoji from '../../../common/AnimatedEmoji';
|
||||
type OwnProps = {
|
||||
onStart: NoneToVoidFunction;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -23,11 +21,14 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const SettingsTwoFaStart: FC<OwnProps & StateProps> = ({
|
||||
isActive, onScreenSelect, onReset, animatedEmoji, onStart,
|
||||
isActive, onReset, animatedEmoji, onStart,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaDisabled);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
|
@ -54,6 +54,7 @@ import HistoryCalendar from './HistoryCalendar.async';
|
||||
import GroupCall from '../calls/group/GroupCall.async';
|
||||
import ActiveCallHeader from '../calls/ActiveCallHeader.async';
|
||||
import PhoneCall from '../calls/phone/PhoneCall.async';
|
||||
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
|
||||
import NewContactModal from './NewContactModal.async';
|
||||
import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
||||
import WebAppModal from './WebAppModal.async';
|
||||
@ -387,6 +388,7 @@ const Main: FC<StateProps> = ({
|
||||
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
||||
<BotTrustModal bot={botTrustRequest?.bot} type={botTrustRequest?.type} />
|
||||
<BotAttachModal bot={botAttachRequest?.bot} />
|
||||
<MessageListHistoryHandler />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -397,12 +397,9 @@ const MediaViewer: FC<StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isOpen, closeMediaViewer, openMediaViewer, {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
origin,
|
||||
avatarOwnerId: avatarOwner && avatarOwner.id,
|
||||
useHistoryBack({
|
||||
isActive: isOpen,
|
||||
onBack: closeMediaViewer,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
49
src/components/middle/MessageListHistoryHandler.tsx
Normal file
49
src/components/middle/MessageListHistoryHandler.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { FC, memo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { createMessageHash } from '../../util/routing';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import { MessageList as GlobalMessageList } from '../../global/types';
|
||||
|
||||
type StateProps = {
|
||||
messageLists?: GlobalMessageList[];
|
||||
};
|
||||
|
||||
// Actual `MessageList` components are unmounted when deep in the history,
|
||||
// so we need a separate component just for handling history
|
||||
const MessageListHistoryHandler: FC<StateProps> = ({ messageLists }) => {
|
||||
const { openChat } = getActions();
|
||||
|
||||
const closeChat = () => {
|
||||
openChat({ id: undefined }, { forceSyncOnIOs: true });
|
||||
};
|
||||
|
||||
const MessageHistoryRecord: FC<GlobalMessageList> = ({ chatId, type, threadId }) => {
|
||||
useHistoryBack({
|
||||
isActive: true,
|
||||
hash: createMessageHash(chatId, type, threadId),
|
||||
onBack: closeChat,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messageLists?.map((messageList, i) => (
|
||||
<MessageHistoryRecord
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${messageList.chatId}_${messageList.threadId}_${messageList.type}_${i}`}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...messageList}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
messageLists: global.messages.messageLists,
|
||||
};
|
||||
},
|
||||
)(MessageListHistoryHandler));
|
@ -6,7 +6,6 @@ import { getActions, withGlobal } from '../../global';
|
||||
import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types';
|
||||
import {
|
||||
MessageListType,
|
||||
MessageList as GlobalMessageList,
|
||||
ActiveEmojiInteraction,
|
||||
} from '../../global/types';
|
||||
import { ThemeKey } from '../../types';
|
||||
@ -48,7 +47,6 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { createMessageHash } from '../../util/routing';
|
||||
import useCustomBackground from '../../hooks/useCustomBackground';
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
||||
@ -104,7 +102,6 @@ type StateProps = {
|
||||
animationLevel?: number;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
currentTransitionKey: number;
|
||||
messageLists?: GlobalMessageList[];
|
||||
isChannel?: boolean;
|
||||
areChatSettingsLoaded?: boolean;
|
||||
canSubscribe?: boolean;
|
||||
@ -126,7 +123,6 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
messageListType,
|
||||
isPrivate,
|
||||
isPinnedMessageList,
|
||||
messageLists,
|
||||
canPost,
|
||||
currentUserBannedRights,
|
||||
defaultBannedRights,
|
||||
@ -158,6 +154,7 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
openPreviousChat,
|
||||
unpinAllMessages,
|
||||
loadUser,
|
||||
loadChatSettings,
|
||||
@ -293,8 +290,8 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
const handleUnpinAllMessages = useCallback(() => {
|
||||
unpinAllMessages({ chatId });
|
||||
closeUnpinModal();
|
||||
openChat({ id: chatId });
|
||||
}, [unpinAllMessages, openChat, closeUnpinModal, chatId]);
|
||||
openPreviousChat();
|
||||
}, [unpinAllMessages, chatId, closeUnpinModal, openPreviousChat]);
|
||||
|
||||
const handleTabletFocus = useCallback(() => {
|
||||
openChat({ id: chatId });
|
||||
@ -347,21 +344,15 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
renderingCanPost && isNotchShown && !isSelectModeActive && 'with-notch',
|
||||
);
|
||||
|
||||
const closeChat = () => {
|
||||
openChat({ id: undefined }, { forceSyncOnIOs: true });
|
||||
};
|
||||
useHistoryBack({
|
||||
isActive: isSelectModeActive,
|
||||
onBack: exitMessageSelectMode,
|
||||
});
|
||||
|
||||
useHistoryBack(
|
||||
renderingChatId && renderingThreadId,
|
||||
closeChat,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
messageLists?.map(createMessageHash) || [],
|
||||
);
|
||||
|
||||
useHistoryBack(isMobileSearchActive, closeLocalTextSearch);
|
||||
useHistoryBack(isSelectModeActive, exitMessageSelectMode);
|
||||
useHistoryBack({
|
||||
isActive: isMobileSearchActive,
|
||||
onBack: closeLocalTextSearch,
|
||||
});
|
||||
|
||||
const isMessagingDisabled = Boolean(
|
||||
!isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
||||
@ -573,7 +564,7 @@ export default memo(withGlobal(
|
||||
isSeenByModalOpen: Boolean(global.seenByModal),
|
||||
isReactorListModalOpen: Boolean(global.reactorModal),
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1),
|
||||
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
||||
activeEmojiInteractions,
|
||||
lastSyncTime,
|
||||
};
|
||||
@ -620,7 +611,6 @@ export default memo(withGlobal(
|
||||
),
|
||||
pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0,
|
||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||
messageLists,
|
||||
isChannel,
|
||||
canSubscribe,
|
||||
canStartBot,
|
||||
|
@ -73,7 +73,10 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [connectionState, isActive, loadContactList]);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
return members ? members.map((member) => member.userId) : [];
|
||||
|
@ -91,7 +91,10 @@ const GifSearch: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
function renderContent() {
|
||||
if (query === undefined) {
|
||||
|
@ -33,7 +33,10 @@ const PollResults: FC<OwnProps & StateProps> = ({
|
||||
lastSyncTime,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
if (!message || !chat) {
|
||||
return <Loading />;
|
||||
|
@ -223,11 +223,13 @@ const RightColumn: FC<StateProps> = ({
|
||||
}
|
||||
}, [chatId]);
|
||||
|
||||
useHistoryBack(isChatSelected && (
|
||||
contentKey === RightColumnContent.ChatInfo
|
||||
|| contentKey === RightColumnContent.Management
|
||||
|| contentKey === RightColumnContent.AddingMembers
|
||||
), () => close(false), toggleChatInfo);
|
||||
useHistoryBack({
|
||||
isActive: isChatSelected && (
|
||||
contentKey === RightColumnContent.ChatInfo
|
||||
|| contentKey === RightColumnContent.Management
|
||||
|| contentKey === RightColumnContent.AddingMembers),
|
||||
onBack: () => close(false),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
function renderContent(isActive: boolean) {
|
||||
|
@ -66,7 +66,10 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(searchTextMessagesLocal, foundIds);
|
||||
|
||||
|
@ -57,7 +57,10 @@ const StickerSearch: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
function renderContent() {
|
||||
if (query === undefined) {
|
||||
|
@ -84,7 +84,10 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
|
@ -40,7 +40,10 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const handleRecentActionsClick = useCallback(() => {
|
||||
onScreenSelect(ManagementScreens.GroupRecentActions);
|
||||
|
@ -65,7 +65,10 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
|| (privacyType === 'private' && isPublic),
|
||||
);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (privacyType && !privateLink) {
|
||||
|
@ -42,7 +42,10 @@ const ManageChatRemovedUsers: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
const [isRemoveUserModalOpen, openRemoveUserModal, closeRemoveUserModal] = useFlag();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const removedMembers = useMemo(() => {
|
||||
if (!chat || !chat.fullInfo || !chat.fullInfo.kickedMembers) {
|
||||
|
@ -63,7 +63,10 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
const linkedChatId = linkedChat?.id;
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadGroupsForDiscussion();
|
||||
|
@ -96,7 +96,10 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
const isPublicGroup = chat.username || hasLinkedChannel;
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && canInvite) {
|
||||
|
@ -63,7 +63,10 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
|
||||
const [customTitle, setCustomTitle] = useState('');
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const selectedChatMember = useMemo(() => {
|
||||
const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedUserId);
|
||||
|
@ -139,7 +139,10 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, '.ListItem-button', true);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
function renderSearchField() {
|
||||
return (
|
||||
|
@ -69,7 +69,10 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const handleRemovedUsersClick = useCallback(() => {
|
||||
onScreenSelect(ManagementScreens.GroupRemovedUsers);
|
||||
|
@ -25,7 +25,10 @@ type StateProps = {
|
||||
const ManageGroupRecentActions: FC<OwnProps & StateProps> = ({ chat, onClose, isActive }) => {
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const adminMembers = useMemo(() => {
|
||||
if (!chat || !chat.fullInfo || !chat.fullInfo.adminMembers) {
|
||||
|
@ -48,7 +48,10 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const selectedChatMember = useMemo(() => {
|
||||
if (!chat || !chat.fullInfo || !chat.fullInfo.members) {
|
||||
|
@ -41,7 +41,10 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
serverTimeOffset,
|
||||
}) => {
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
if (!members || !usersById) {
|
||||
|
@ -61,7 +61,10 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
|
||||
const [selectedUsageOption, setSelectedUsageOption] = useState('0');
|
||||
const [isSubmitBlocked, setIsSubmitBlocked] = useState(false);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useOnChange(([oldEditingInvite]) => {
|
||||
if (oldEditingInvite === editingInvite) return;
|
||||
|
@ -71,7 +71,10 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}, [invite, lang, showNotification]);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const renderImporters = () => {
|
||||
if (!importers?.length && requesters?.length) return undefined;
|
||||
|
@ -91,7 +91,10 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [animationData]);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const hasDetailedCountdown = useMemo(() => {
|
||||
if (!exportedInvites) return undefined;
|
||||
|
@ -54,7 +54,10 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [animationData]);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat?.joinRequests && !isUserId(chatId)) {
|
||||
|
@ -40,7 +40,10 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localEnabledReactions, setLocalEnabledReactions] = useState(enabledReactions || []);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const handleSaveReactions = useCallback(() => {
|
||||
if (!chat) return;
|
||||
|
@ -58,7 +58,10 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
const currentFirstName = user ? (user.firstName || '') : '';
|
||||
const currentLastName = user ? (user.lastName || '') : '';
|
||||
|
@ -34,7 +34,7 @@ type OwnProps = {
|
||||
noCompact?: boolean;
|
||||
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onClose?: () => void;
|
||||
onClose: () => void;
|
||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
children: React.ReactNode;
|
||||
@ -84,11 +84,15 @@ const Menu: FC<OwnProps> = ({
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => (isOpen && onClose ? captureEscKeyListener(onClose) : undefined),
|
||||
() => (isOpen ? captureEscKeyListener(onClose) : undefined),
|
||||
[isOpen, onClose],
|
||||
);
|
||||
|
||||
useHistoryBack(isOpen, onClose, undefined, undefined, autoClose);
|
||||
useHistoryBack({
|
||||
isActive: isOpen,
|
||||
onBack: onClose,
|
||||
shouldBeReplaced: true,
|
||||
});
|
||||
|
||||
useEffectWithPrevDeps(([prevIsOpen]) => {
|
||||
if (isOpen || (!isOpen && prevIsOpen === true)) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { FC, useCallback } from '../../lib/teact/teact';
|
||||
|
||||
import { IS_TEST } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { IS_COMPACT_MENU } from '../../util/environment';
|
||||
@ -91,7 +92,7 @@ const MenuItem: FC<OwnProps> = (props) => {
|
||||
download={download}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel}
|
||||
target={href.startsWith(window.location.origin) ? '_self' : '_blank'}
|
||||
target={href.startsWith(window.location.origin) || IS_TEST ? '_self' : '_blank'}
|
||||
rel="noopener noreferrer"
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={onClick}
|
||||
|
@ -67,17 +67,10 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
: undefined), [isOpen, onClose, onEnter]);
|
||||
useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]);
|
||||
|
||||
const { forceClose } = useHistoryBack(isOpen, onClose);
|
||||
|
||||
// For modals that are closed by unmounting without changing `isOpen` to `false`
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isOpen) {
|
||||
forceClose();
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
useHistoryBack({
|
||||
isActive: isOpen,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffectWithPrevDeps(([prevIsOpen]) => {
|
||||
document.body.classList.toggle('has-open-dialog', isOpen);
|
||||
|
@ -4,6 +4,7 @@ export const APP_VERSION = process.env.APP_VERSION!;
|
||||
export const DEBUG = process.env.APP_ENV !== 'production';
|
||||
export const DEBUG_MORE = false;
|
||||
|
||||
export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1';
|
||||
export const IS_TEST = process.env.APP_ENV === 'test';
|
||||
export const IS_PERF = process.env.APP_ENV === 'perf';
|
||||
export const IS_BETA = process.env.APP_ENV === 'staging';
|
||||
|
@ -3,10 +3,13 @@ import { addActionHandler } from './index';
|
||||
import { INITIAL_STATE } from './initialState';
|
||||
import { initCache, loadCache } from './cache';
|
||||
import { cloneDeep } from '../util/iteratees';
|
||||
import { IS_MOCKED_CLIENT } from '../config';
|
||||
|
||||
initCache();
|
||||
|
||||
addActionHandler('init', () => {
|
||||
const initial = cloneDeep(INITIAL_STATE);
|
||||
return loadCache(initial) || initial;
|
||||
const state = loadCache(initial) || initial;
|
||||
if (IS_MOCKED_CLIENT) state.authState = 'authorizationStateReady';
|
||||
return state;
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import { FocusDirection } from '../../types';
|
||||
|
||||
import {
|
||||
IS_MOCKED_CLIENT,
|
||||
IS_TEST, MESSAGE_LIST_SLICE, MESSAGE_LIST_VIEWPORT_LIMIT, TMP_CHAT_ID,
|
||||
} from '../../config';
|
||||
import {
|
||||
@ -41,7 +42,7 @@ export function updateCurrentMessageList(
|
||||
): GlobalState {
|
||||
const { messageLists } = global.messages;
|
||||
let newMessageLists: MessageList[] = messageLists;
|
||||
if (shouldReplaceHistory || IS_TEST) {
|
||||
if (shouldReplaceHistory || (IS_TEST && !IS_MOCKED_CLIENT)) {
|
||||
newMessageLists = chatId ? [{ chatId, threadId, type }] : [];
|
||||
} else if (chatId) {
|
||||
const last = messageLists[messageLists.length - 1];
|
||||
|
@ -1,54 +1,78 @@
|
||||
import { useCallback, useEffect, useRef } from '../lib/teact/teact';
|
||||
|
||||
import useOnChange from './useOnChange';
|
||||
import { useEffect, useRef } from '../lib/teact/teact';
|
||||
import { IS_TEST } from '../config';
|
||||
import { fastRaf } from '../util/schedulers';
|
||||
import { IS_IOS } from '../util/environment';
|
||||
import usePrevious from './usePrevious';
|
||||
import { getActions } from '../global';
|
||||
import { areSortedArraysEqual } from '../util/iteratees';
|
||||
|
||||
type HistoryState = {
|
||||
currentIndex: number;
|
||||
nextStateIndexToReplace: number;
|
||||
isHistoryAltered: boolean;
|
||||
isDisabled: boolean;
|
||||
isEdge: boolean;
|
||||
currentIndexes: number[];
|
||||
};
|
||||
import { getActions } from '../lib/teact/teactn';
|
||||
|
||||
export const LOCATION_HASH = window.location.hash;
|
||||
const PATH_BASE = `${window.location.pathname}${window.location.search}`;
|
||||
// Carefully selected by swiping and observing visual changes
|
||||
// TODO: may be different on other devices such as iPad, maybe take dpi into account?
|
||||
const SAFARI_EDGE_BACK_GESTURE_LIMIT = 300;
|
||||
const SAFARI_EDGE_BACK_GESTURE_DURATION = 350;
|
||||
export const LOCATION_HASH = window.location.hash;
|
||||
const PATH_BASE = `${window.location.pathname}${window.location.search}`;
|
||||
|
||||
const historyState: HistoryState = {
|
||||
currentIndex: 0,
|
||||
nextStateIndexToReplace: -1,
|
||||
isHistoryAltered: false,
|
||||
isDisabled: false,
|
||||
isEdge: false,
|
||||
currentIndexes: [],
|
||||
type HistoryRecord = {
|
||||
index: number;
|
||||
// Should this record be replaced by the next record (for example Menu)
|
||||
shouldBeReplaced?: boolean;
|
||||
// Mark this record as replaced by the next record. Only used to check if needed to perform effectBack
|
||||
markReplaced?: VoidFunction;
|
||||
onBack?: VoidFunction;
|
||||
// Set if the element is closed in the UI, but not in the real history
|
||||
isClosed?: boolean;
|
||||
};
|
||||
|
||||
export const disableHistoryBack = () => {
|
||||
historyState.isDisabled = true;
|
||||
type HistoryOperationGo = {
|
||||
type: 'go';
|
||||
delta: number;
|
||||
};
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
type HistoryOperationState = {
|
||||
type: 'pushState' | 'replaceState';
|
||||
data: any;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
type HistoryOperation = HistoryOperationGo | HistoryOperationState;
|
||||
|
||||
// Needed to dismiss any 'trashed' history records from the previous page reloads.
|
||||
const historyUniqueSessionId = Number(new Date());
|
||||
// Reflects real history state, but also contains information on which records should be replaced by the next record and
|
||||
// which records are deferred to close on the next operation
|
||||
let historyState: HistoryRecord[];
|
||||
// Reflects current real history index
|
||||
let historyCursor: number;
|
||||
// If we alter real history programmatically, the popstate event will be fired, which we don't need
|
||||
let isAlteringHistory = false;
|
||||
// Unfortunately Safari doesn't really like when there's 2+ consequent history operations in one frame, so we need
|
||||
// to delay them to the next raf
|
||||
let deferredHistoryOperations: HistoryOperation[] = [];
|
||||
let isSafariGestureAnimation = false;
|
||||
|
||||
// Do not remove: used for history unit tests
|
||||
if (IS_TEST) {
|
||||
(window as any).TEST_getHistoryState = () => historyState;
|
||||
(window as any).TEST_getHistoryCursor = () => historyCursor;
|
||||
}
|
||||
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
const x = event.touches[0].pageX;
|
||||
|
||||
if (x <= SAFARI_EDGE_BACK_GESTURE_LIMIT || x >= window.innerWidth - SAFARI_EDGE_BACK_GESTURE_LIMIT) {
|
||||
historyState.isEdge = true;
|
||||
isSafariGestureAnimation = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (historyState.isEdge) {
|
||||
setTimeout(() => {
|
||||
historyState.isEdge = false;
|
||||
}, SAFARI_EDGE_BACK_GESTURE_DURATION);
|
||||
function handleTouchEnd() {
|
||||
if (!isSafariGestureAnimation) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
isSafariGestureAnimation = false;
|
||||
}, SAFARI_EDGE_BACK_GESTURE_DURATION);
|
||||
}
|
||||
|
||||
if (IS_IOS) {
|
||||
window.addEventListener('touchstart', handleTouchStart);
|
||||
@ -56,197 +80,209 @@ if (IS_IOS) {
|
||||
window.addEventListener('popstate', handleTouchEnd);
|
||||
}
|
||||
|
||||
window.history.replaceState({ index: historyState.currentIndex }, '', PATH_BASE);
|
||||
function applyDeferredHistoryOperations() {
|
||||
const goOperations = deferredHistoryOperations.filter((op) => op.type === 'go') as HistoryOperationGo[];
|
||||
const stateOperations = deferredHistoryOperations.filter((op) => op.type !== 'go') as HistoryOperationState[];
|
||||
const goCount = goOperations.reduce((acc, op) => acc + op.delta, 0);
|
||||
if (goCount) {
|
||||
window.history.go(goCount);
|
||||
}
|
||||
|
||||
export default function useHistoryBack(
|
||||
isActive: boolean | undefined,
|
||||
onBack: ((noDisableAnimation: boolean) => void) | undefined,
|
||||
onForward?: (state: any) => void,
|
||||
currentState?: any,
|
||||
shouldReplaceNext = false,
|
||||
hashes?: string[],
|
||||
) {
|
||||
const indexRef = useRef(-1);
|
||||
const isForward = useRef(false);
|
||||
const prevIsActive = usePrevious(isActive);
|
||||
const isClosed = useRef(true);
|
||||
const indexHashRef = useRef<{ index: number; hash: string }[]>([]);
|
||||
const prevHashes = usePrevious(hashes);
|
||||
const isHashChangedFromEvent = useRef<boolean>(false);
|
||||
stateOperations.forEach((op) => window.history[op.type](op.data, '', op.hash));
|
||||
|
||||
const handleChange = useCallback((isForceClose = false) => {
|
||||
if (!hashes) {
|
||||
if (isActive && !isForceClose) {
|
||||
isClosed.current = false;
|
||||
deferredHistoryOperations = [];
|
||||
}
|
||||
|
||||
if (isForward.current) {
|
||||
isForward.current = false;
|
||||
historyState.currentIndexes.push(indexRef.current);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
const index = ++historyState.currentIndex;
|
||||
function deferHistoryOperation(historyOperation: HistoryOperation) {
|
||||
if (!deferredHistoryOperations.length) fastRaf(applyDeferredHistoryOperations);
|
||||
deferredHistoryOperations.push(historyOperation);
|
||||
}
|
||||
|
||||
historyState.currentIndexes.push(index);
|
||||
// Resets history to the `root` state
|
||||
function resetHistory() {
|
||||
historyCursor = 0;
|
||||
historyState = [{
|
||||
index: 0,
|
||||
onBack: () => window.history.back(),
|
||||
}];
|
||||
|
||||
window.history[(
|
||||
(
|
||||
historyState.currentIndexes.includes(historyState.nextStateIndexToReplace - 1)
|
||||
&& window.history.state.index !== 0
|
||||
&& historyState.nextStateIndexToReplace === index
|
||||
&& !shouldReplaceNext
|
||||
)
|
||||
? 'replaceState'
|
||||
: 'pushState'
|
||||
)]({
|
||||
index,
|
||||
state: currentState,
|
||||
}, '');
|
||||
window.history.replaceState({ index: 0, historyUniqueSessionId }, PATH_BASE);
|
||||
}
|
||||
|
||||
indexRef.current = index;
|
||||
resetHistory();
|
||||
|
||||
if (shouldReplaceNext) {
|
||||
historyState.nextStateIndexToReplace = historyState.currentIndex + 1;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
function cleanupClosed(alreadyClosedCount = 1) {
|
||||
let countClosed = alreadyClosedCount;
|
||||
for (let i = historyCursor - 1; i > 0; i--) {
|
||||
if (!historyState[i].isClosed) break;
|
||||
countClosed++;
|
||||
}
|
||||
if (countClosed) {
|
||||
isAlteringHistory = true;
|
||||
deferHistoryOperation({
|
||||
type: 'go',
|
||||
delta: -countClosed,
|
||||
});
|
||||
}
|
||||
return countClosed;
|
||||
}
|
||||
|
||||
if ((isForceClose || !isActive) && !isClosed.current) {
|
||||
if ((indexRef.current === historyState.currentIndex || !shouldReplaceNext)) {
|
||||
historyState.isHistoryAltered = true;
|
||||
window.history.back();
|
||||
|
||||
setTimeout(() => {
|
||||
historyState.nextStateIndexToReplace = -1;
|
||||
}, 400);
|
||||
}
|
||||
historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(indexRef.current), 1);
|
||||
|
||||
isClosed.current = true;
|
||||
}
|
||||
} else {
|
||||
const prev = prevHashes || [];
|
||||
if (prev.length < hashes.length) {
|
||||
setTimeout(() => {
|
||||
const index = ++historyState.currentIndex;
|
||||
historyState.currentIndexes.push(index);
|
||||
|
||||
window.history.pushState({
|
||||
index,
|
||||
state: currentState,
|
||||
}, '', `#${hashes[hashes.length - 1]}`);
|
||||
|
||||
indexHashRef.current.push({
|
||||
index,
|
||||
hash: hashes[hashes.length - 1],
|
||||
});
|
||||
}, 0);
|
||||
} else {
|
||||
const delta = prev.length - hashes.length;
|
||||
if (isHashChangedFromEvent.current) {
|
||||
isHashChangedFromEvent.current = false;
|
||||
} else {
|
||||
if (hashes.length !== indexHashRef.current.length) {
|
||||
if (delta > 0) {
|
||||
const last = indexHashRef.current[indexHashRef.current.length - delta - 1];
|
||||
let realDelta = delta;
|
||||
if (last) {
|
||||
const indexLast = historyState.currentIndexes.findIndex(
|
||||
(l) => l === last.index,
|
||||
);
|
||||
realDelta = historyState.currentIndexes.length - indexLast - 1;
|
||||
}
|
||||
historyState.isHistoryAltered = true;
|
||||
window.history.go(-realDelta);
|
||||
const removed = indexHashRef.current.splice(indexHashRef.current.length - delta - 1, delta);
|
||||
removed.forEach(({ index }) => {
|
||||
historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(index), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hashes.length > 0) {
|
||||
setTimeout(() => {
|
||||
const index = ++historyState.currentIndex;
|
||||
historyState.currentIndexes[historyState.currentIndexes.length - 1] = index;
|
||||
|
||||
window.history.replaceState({
|
||||
index,
|
||||
state: currentState,
|
||||
}, '', `${PATH_BASE}#${hashes[hashes.length - 1]}`);
|
||||
|
||||
indexHashRef.current[indexHashRef.current.length - 1] = {
|
||||
index,
|
||||
hash: hashes[hashes.length - 1],
|
||||
};
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
function cleanupTrashedState() {
|
||||
// Navigation to previous page reload, state of which was trashed by reload
|
||||
for (let i = historyState.length - 1; i > 0; i--) {
|
||||
if (historyState[i].isClosed) {
|
||||
continue;
|
||||
}
|
||||
}, [currentState, hashes, isActive, prevHashes, shouldReplaceNext]);
|
||||
if (isSafariGestureAnimation) {
|
||||
getActions().disableHistoryAnimations();
|
||||
}
|
||||
historyState[i].onBack?.();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = (event: PopStateEvent) => {
|
||||
if (historyState.isHistoryAltered) {
|
||||
setTimeout(() => {
|
||||
historyState.isHistoryAltered = false;
|
||||
}, 0);
|
||||
return;
|
||||
resetHistory();
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', ({ state }: PopStateEvent) => {
|
||||
if (isAlteringHistory) {
|
||||
isAlteringHistory = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
cleanupTrashedState();
|
||||
|
||||
if (!window.location.hash) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { index, historyUniqueSessionId: previousUniqueSessionId } = state;
|
||||
if (previousUniqueSessionId !== historyUniqueSessionId) {
|
||||
cleanupTrashedState();
|
||||
return;
|
||||
}
|
||||
|
||||
// New real history state matches the old virtual one. Not possible in theory, but in practice we have Safari
|
||||
if (index === historyCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < historyCursor) {
|
||||
// Navigating back
|
||||
let alreadyClosedCount = 0;
|
||||
for (let i = historyCursor; i > index - alreadyClosedCount; i--) {
|
||||
if (historyState[i].isClosed) {
|
||||
alreadyClosedCount++;
|
||||
continue;
|
||||
}
|
||||
const { index: i } = event.state;
|
||||
const index = i || 0;
|
||||
try {
|
||||
const currIndex = hashes ? indexHashRef.current[indexHashRef.current.length - 1].index : indexRef.current;
|
||||
|
||||
const prev = historyState.currentIndexes[historyState.currentIndexes.indexOf(currIndex) - 1];
|
||||
|
||||
if (historyState.isDisabled) return;
|
||||
|
||||
if ((!isClosed.current && (index === 0 || index === prev)) || (hashes && (index === 0 || index === prev))) {
|
||||
if (hashes) {
|
||||
isHashChangedFromEvent.current = true;
|
||||
indexHashRef.current.pop();
|
||||
}
|
||||
|
||||
historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(currIndex), 1);
|
||||
|
||||
if (onBack) {
|
||||
if (historyState.isEdge) {
|
||||
getActions()
|
||||
.disableHistoryAnimations();
|
||||
}
|
||||
onBack(!historyState.isEdge);
|
||||
isClosed.current = true;
|
||||
}
|
||||
} else if (index === currIndex && isClosed.current && onForward && !hashes) {
|
||||
isForward.current = true;
|
||||
if (historyState.isEdge) {
|
||||
getActions()
|
||||
.disableHistoryAnimations();
|
||||
}
|
||||
onForward(event.state.state);
|
||||
}
|
||||
} catch (e) {
|
||||
// Forward navigation for hashed is not supported
|
||||
if (isSafariGestureAnimation) {
|
||||
getActions().disableHistoryAnimations();
|
||||
}
|
||||
historyState[i].onBack?.();
|
||||
}
|
||||
|
||||
const countClosed = cleanupClosed(alreadyClosedCount);
|
||||
historyCursor += index - historyCursor - countClosed;
|
||||
|
||||
// Can happen when we have deferred a real back for some element (for example Menu), closed via UI,
|
||||
// pressed back button and caused a pushState.
|
||||
if (historyCursor < 0) {
|
||||
historyCursor = 0;
|
||||
}
|
||||
} else if (index > historyCursor) {
|
||||
// Forward navigation is not yet supported
|
||||
isAlteringHistory = true;
|
||||
deferHistoryOperation({
|
||||
type: 'go',
|
||||
delta: -(index - historyCursor),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default function useHistoryBack({
|
||||
isActive,
|
||||
shouldBeReplaced,
|
||||
hash,
|
||||
onBack,
|
||||
}: {
|
||||
isActive?: boolean;
|
||||
shouldBeReplaced?: boolean;
|
||||
hash?: string;
|
||||
title?: string;
|
||||
onBack: VoidFunction;
|
||||
}) {
|
||||
// Active index of the record
|
||||
const indexRef = useRef<number>();
|
||||
const wasReplaced = useRef(false);
|
||||
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
const pushState = (forceReplace = false) => {
|
||||
// Check if the old state should be replaced
|
||||
const shouldReplace = forceReplace || historyState[historyCursor].shouldBeReplaced;
|
||||
indexRef.current = shouldReplace ? historyCursor : ++historyCursor;
|
||||
|
||||
historyCursor = indexRef.current;
|
||||
|
||||
// Mark the previous record as replaced so effectBack doesn't perform back operation on the new record
|
||||
const previousRecord = historyState[indexRef.current];
|
||||
if (previousRecord && !previousRecord.isClosed) {
|
||||
previousRecord.markReplaced?.();
|
||||
}
|
||||
|
||||
historyState[indexRef.current] = {
|
||||
index: indexRef.current,
|
||||
onBack,
|
||||
shouldBeReplaced,
|
||||
markReplaced: () => {
|
||||
wasReplaced.current = true;
|
||||
},
|
||||
};
|
||||
|
||||
const hasChanged = hashes
|
||||
? (!prevHashes || !areSortedArraysEqual(prevHashes, hashes))
|
||||
: prevIsActive !== isActive;
|
||||
|
||||
if (!historyState.isDisabled && hasChanged) {
|
||||
handleChange();
|
||||
// Delete forward navigation in the virtual history. Not really needed, just looks better when debugging `logState`
|
||||
for (let i = indexRef.current + 1; i < historyState.length; i++) {
|
||||
delete historyState[i];
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [
|
||||
currentState, handleChange, hashes, isActive, onBack, onForward, prevHashes, prevIsActive, shouldReplaceNext,
|
||||
]);
|
||||
|
||||
return {
|
||||
forceClose: () => handleChange(true),
|
||||
deferHistoryOperation({
|
||||
type: shouldReplace ? 'replaceState' : 'pushState',
|
||||
data: {
|
||||
index: indexRef.current,
|
||||
historyUniqueSessionId,
|
||||
},
|
||||
hash: hash ? `#${hash}` : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const processBack = () => {
|
||||
// Only process back on open records
|
||||
if (indexRef.current && historyState[indexRef.current] && !wasReplaced.current) {
|
||||
historyState[indexRef.current].isClosed = true;
|
||||
wasReplaced.current = true;
|
||||
if (indexRef.current === historyCursor && !shouldBeReplaced) {
|
||||
historyCursor -= cleanupClosed();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process back navigation when element is unmounted
|
||||
useEffect(() => {
|
||||
isFirstRender.current = false;
|
||||
return () => {
|
||||
if (!isActive || wasReplaced.current) return;
|
||||
processBack();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useOnChange(() => {
|
||||
if (isFirstRender.current && !isActive) return;
|
||||
|
||||
if (isActive) {
|
||||
pushState();
|
||||
} else {
|
||||
processBack();
|
||||
}
|
||||
}, [isActive]);
|
||||
}
|
||||
|
331
src/lib/gramjs/client/MockClient.ts
Normal file
331
src/lib/gramjs/client/MockClient.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { UpdateConnectionState } from '../network';
|
||||
import Request from '../tl/api';
|
||||
import { default as GramJs } from "../tl/api";
|
||||
|
||||
type Peer = {
|
||||
peer: GramJs.Chat | GramJs.Channel | GramJs.User;
|
||||
TEST_messages: GramJs.Message[];
|
||||
TEST_sendMessage: (data: CreateMessageParams) => GramJs.Message | undefined;
|
||||
};
|
||||
|
||||
type CreateMessageParams = {
|
||||
fromId?: any;
|
||||
repliesChannelId?: any;
|
||||
replyingTo?: GramJs.MessageReplyHeader;
|
||||
};
|
||||
|
||||
class TelegramClient {
|
||||
addEventHandler(callback: any, event: any) {
|
||||
callback(event.build(new UpdateConnectionState(UpdateConnectionState.connected)));
|
||||
}
|
||||
|
||||
private lastId = 0;
|
||||
|
||||
private peers: Peer[] = [];
|
||||
|
||||
private dialogs: GramJs.Dialog[] = [];
|
||||
|
||||
start() {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const user = this.createUser({
|
||||
firstName: 'Test',
|
||||
lastName: 'Account',
|
||||
});
|
||||
user.TEST_sendMessage({});
|
||||
|
||||
const chat = this.createChat({});
|
||||
chat.TEST_sendMessage({});
|
||||
|
||||
const channel = this.createChannel({
|
||||
title: 'Test Channel',
|
||||
username: 'testchannel',
|
||||
});
|
||||
|
||||
const discussion = this.createChannel({
|
||||
title: 'Test Discussion',
|
||||
username: 'testdiscuss',
|
||||
isMegagroup: true,
|
||||
});
|
||||
|
||||
const message = channel.TEST_sendMessage({
|
||||
repliesChannelId: discussion.peer.id,
|
||||
});
|
||||
|
||||
const { id } = discussion.TEST_sendMessage({})!;
|
||||
|
||||
discussion.TEST_sendMessage({
|
||||
fromId: new GramJs.PeerUser({
|
||||
userId: user.peer.id,
|
||||
}),
|
||||
replyingTo: new GramJs.MessageReplyHeader({
|
||||
replyToMsgId: id,
|
||||
replyToPeerId: new GramJs.PeerChannel({
|
||||
channelId: channel.peer.id,
|
||||
}),
|
||||
replyToTopId: message!.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
createDialog(peer: GramJs.TypePeer) {
|
||||
return new GramJs.Dialog({
|
||||
peer,
|
||||
topMessage: 0,
|
||||
readInboxMaxId: 0,
|
||||
readOutboxMaxId: 0,
|
||||
unreadCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
unreadReactionsCount: 0,
|
||||
notifySettings: new GramJs.PeerNotifySettings({}),
|
||||
});
|
||||
}
|
||||
|
||||
createMessage(peer: GramJs.TypePeer) {
|
||||
return ({
|
||||
fromId,
|
||||
repliesChannelId,
|
||||
replyingTo,
|
||||
}: CreateMessageParams) => {
|
||||
const pi = this.getPeerIndex(peer);
|
||||
const p = this.getPeer(peer);
|
||||
if (!p || pi === undefined) return;
|
||||
|
||||
const message = new GramJs.Message({
|
||||
id: p.TEST_messages.length + 1,
|
||||
fromId,
|
||||
peerId: peer,
|
||||
date: Number(new Date()) / 1000 + pi * 60,
|
||||
message: 'lol @channel',
|
||||
entities: [new GramJs.MessageEntityMention({
|
||||
offset: 4,
|
||||
length: 8,
|
||||
})],
|
||||
replyTo: replyingTo,
|
||||
replies: new GramJs.MessageReplies({
|
||||
comments: true,
|
||||
replies: 0,
|
||||
repliesPts: 0,
|
||||
channelId: repliesChannelId ? BigInt(repliesChannelId) : undefined,
|
||||
}),
|
||||
});
|
||||
this.peers[pi].TEST_messages.push(message);
|
||||
return message;
|
||||
};
|
||||
}
|
||||
|
||||
createChat({}) {
|
||||
const chat = new GramJs.Chat({
|
||||
id: BigInt(this.lastId++),
|
||||
title: 'Some chat',
|
||||
photo: new GramJs.ChatPhotoEmpty(),
|
||||
participantsCount: 1,
|
||||
date: 1000,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
const peerChat = new GramJs.PeerChat({
|
||||
chatId: chat.id,
|
||||
});
|
||||
|
||||
this.dialogs.push(this.createDialog(peerChat));
|
||||
|
||||
const testChat: Peer = { peer: chat, TEST_messages: [], TEST_sendMessage: this.createMessage(peerChat) };
|
||||
|
||||
this.peers.push(testChat);
|
||||
|
||||
return testChat;
|
||||
}
|
||||
|
||||
createChannel({ title, username, isMegagroup }: {
|
||||
title: string;
|
||||
username: string;
|
||||
isMegagroup?: boolean;
|
||||
}) {
|
||||
const channel = new GramJs.Channel({
|
||||
username,
|
||||
id: BigInt(this.lastId++),
|
||||
megagroup: isMegagroup ? true : undefined,
|
||||
title,
|
||||
photo: new GramJs.ChatPhotoEmpty(),
|
||||
participantsCount: 1,
|
||||
date: 1000,
|
||||
creator: true,
|
||||
});
|
||||
|
||||
const peerChannel = new GramJs.PeerChannel({
|
||||
channelId: channel.id,
|
||||
});
|
||||
|
||||
this.dialogs.push(this.createDialog(peerChannel));
|
||||
|
||||
const testChat: Peer = { peer: channel, TEST_messages: [], TEST_sendMessage: this.createMessage(peerChannel) };
|
||||
|
||||
this.peers.push(testChat);
|
||||
|
||||
return testChat;
|
||||
}
|
||||
|
||||
createUser({
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}): Peer {
|
||||
const user = new GramJs.User({
|
||||
// self: true,
|
||||
verified: true,
|
||||
id: BigInt(this.lastId++),
|
||||
// accessHash?: long;
|
||||
firstName,
|
||||
lastName,
|
||||
username: 'man',
|
||||
// phone?: string;
|
||||
// photo?: Api.TypeUserProfilePhoto;
|
||||
// status?: Api.TypeUserStatus;
|
||||
// botInfoVersion?: int;
|
||||
// restrictionReason?: Api.//TypeRestrictionReason[];
|
||||
// botInlinePlaceholder?: string;
|
||||
// langCode?: string;
|
||||
});
|
||||
|
||||
const peerUser = new GramJs.PeerUser({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
this.dialogs.push(this.createDialog(peerUser));
|
||||
|
||||
const testChat: Peer = { peer: user, TEST_messages: [], TEST_sendMessage: this.createMessage(peerUser) };
|
||||
|
||||
this.peers.push(testChat);
|
||||
|
||||
return testChat;
|
||||
}
|
||||
|
||||
async invoke(request: Request) {
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
if (request instanceof GramJs.messages.GetDiscussionMessage) {
|
||||
return new GramJs.messages.DiscussionMessage({
|
||||
messages: [
|
||||
this.peers[3].TEST_messages[0],
|
||||
],
|
||||
maxId: 2,
|
||||
unreadCount: 1,
|
||||
chats: [],
|
||||
users: [],
|
||||
});
|
||||
}
|
||||
if (request instanceof GramJs.messages.GetHistory) {
|
||||
const peer = this.getPeer(request.peer);
|
||||
if (!peer) return;
|
||||
|
||||
return new GramJs.messages.Messages({
|
||||
messages: peer.TEST_messages,
|
||||
chats: [],
|
||||
users: [],
|
||||
});
|
||||
}
|
||||
if (request instanceof GramJs.messages.GetReplies) {
|
||||
const peer = this.peers[3];
|
||||
if (!peer) return;
|
||||
|
||||
return new GramJs.messages.ChannelMessages({
|
||||
messages: peer.TEST_messages,
|
||||
pts: 0,
|
||||
count: peer.TEST_messages.length,
|
||||
chats: [],
|
||||
users: [],
|
||||
});
|
||||
}
|
||||
if (request instanceof GramJs.messages.GetDialogFilters) {
|
||||
return [new GramJs.DialogFilter({
|
||||
contacts: true,
|
||||
nonContacts: true,
|
||||
groups: true,
|
||||
broadcasts: true,
|
||||
bots: true,
|
||||
// excludeMuted?: true;
|
||||
// excludeRead?: true;
|
||||
// excludeArchived?: true;
|
||||
id: 1,
|
||||
title: 'Dialog Filter',
|
||||
// emoticon?: string;
|
||||
pinnedPeers: [],
|
||||
includePeers: [],
|
||||
excludePeers: [],
|
||||
})];
|
||||
}
|
||||
if (request instanceof GramJs.messages.GetPinnedDialogs) {
|
||||
return new GramJs.messages.PeerDialogs({
|
||||
dialogs: [],
|
||||
chats: [],
|
||||
messages: [],
|
||||
users: [],
|
||||
state: new GramJs.updates.State({
|
||||
pts: 0,
|
||||
qts: 0,
|
||||
date: 0,
|
||||
seq: 0,
|
||||
unreadCount: 0,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (request instanceof GramJs.messages.GetDialogs) {
|
||||
if (request.folderId || !(request.offsetPeer instanceof GramJs.InputPeerEmpty)) {
|
||||
return new GramJs.messages.Dialogs({
|
||||
dialogs: [],
|
||||
users: [],
|
||||
chats: [],
|
||||
messages: [],
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.messages.Dialogs({
|
||||
dialogs: this.dialogs,
|
||||
messages: this.getAllMessages(),
|
||||
chats: this.getChats(),
|
||||
users: this.getUsers(),
|
||||
});
|
||||
}
|
||||
// console.log(request.className, request);
|
||||
}
|
||||
|
||||
private getPeerIndex(peer: GramJs.TypeInputPeer) {
|
||||
const id = 'channelId' in peer ? peer.channelId : (
|
||||
'userId' in peer ? peer.userId : (
|
||||
'chatId' in peer ? peer.chatId : undefined
|
||||
)
|
||||
);
|
||||
|
||||
if (!id) return undefined;
|
||||
|
||||
return this.peers.findIndex((l) => l.peer.id.toString() === id.toString());
|
||||
}
|
||||
|
||||
private getPeer(peer: GramJs.TypeInputPeer) {
|
||||
const index = this.getPeerIndex(peer);
|
||||
if (index === undefined) return undefined;
|
||||
|
||||
return this.peers[index];
|
||||
}
|
||||
|
||||
private getAllMessages() {
|
||||
return this.peers.reduce((acc: GramJs.Message[], el) => {
|
||||
acc.push(...el.TEST_messages);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private getChats() {
|
||||
return this.peers.filter((l) => !(l.peer instanceof GramJs.User)).map((l) => l.peer);
|
||||
}
|
||||
|
||||
private getUsers() {
|
||||
return this.peers.filter((l) => l.peer instanceof GramJs.User).map((l) => l.peer);
|
||||
}
|
||||
}
|
||||
|
||||
export default TelegramClient;
|
@ -1,17 +1,12 @@
|
||||
import { MessageList, MessageListType } from '../global/types';
|
||||
import { MessageListType } from '../global/types';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
|
||||
import { LOCATION_HASH } from '../hooks/useHistoryBack';
|
||||
|
||||
export function createMessageHash(messageList: MessageList) {
|
||||
const typeOrThreadId = messageList.type !== 'thread' ? (
|
||||
`_${messageList.type}`
|
||||
) : messageList.threadId !== -1 ? (
|
||||
`_${messageList.threadId}`
|
||||
) : '';
|
||||
|
||||
return `${messageList.chatId}${typeOrThreadId}`;
|
||||
}
|
||||
export const createMessageHash = (chatId: string, type: string, threadId: number): string => (
|
||||
chatId.toString()
|
||||
+ (type !== 'thread' ? `_${type}`
|
||||
: (threadId !== -1 ? `_${threadId}` : ''))
|
||||
);
|
||||
|
||||
export function parseLocationHash() {
|
||||
if (!LOCATION_HASH) return undefined;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DEBUG, DEBUG_MORE } from '../config';
|
||||
import { DEBUG, DEBUG_MORE, IS_TEST } from '../config';
|
||||
import { getActions } from '../global';
|
||||
import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||
import { notifyClientReady, playNotifySoundDebounced } from './notifications';
|
||||
@ -77,7 +77,7 @@ if (IS_SERVICE_WORKER_SUPPORTED) {
|
||||
console.error('[SW] ServiceWorker not available');
|
||||
}
|
||||
|
||||
if (!IS_IOS && !IS_ANDROID) {
|
||||
if (!IS_IOS && !IS_ANDROID && !IS_TEST) {
|
||||
getActions().showDialog({ data: { message: 'SERVICE_WORKER_DISABLED', hasErrorKey: true } });
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { APP_VERSION, DEBUG } from '../config';
|
||||
import { APP_VERSION, DEBUG, IS_MOCKED_CLIENT } from '../config';
|
||||
import { getGlobal } from '../global';
|
||||
import { hasStoredSession } from './sessions';
|
||||
|
||||
@ -25,6 +25,7 @@ const saveSync = (authed: boolean) => {
|
||||
let lastTimeout: NodeJS.Timeout | undefined;
|
||||
|
||||
export const forceWebsync = (authed: boolean) => {
|
||||
if (IS_MOCKED_CLIENT) return undefined;
|
||||
const currentTs = getTs();
|
||||
|
||||
const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
|
||||
|
@ -5,6 +5,8 @@ const {
|
||||
DefinePlugin,
|
||||
EnvironmentPlugin,
|
||||
ProvidePlugin,
|
||||
|
||||
NormalModuleReplacementPlugin,
|
||||
} = require('webpack');
|
||||
const HtmlWebackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
@ -118,6 +120,10 @@ module.exports = (env = {}, argv = {}) => {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...(process.env.APP_MOCKED_CLIENT === '1' ? [new NormalModuleReplacementPlugin(
|
||||
/src\/lib\/gramjs\/client\/TelegramClient\.js/,
|
||||
'./MockClient.ts'
|
||||
)] : []),
|
||||
new HtmlWebackPlugin({
|
||||
appName: process.env.APP_ENV === 'production' ? 'Telegram Web' : 'Telegram Web Beta',
|
||||
appleIcon: process.env.APP_ENV === 'production' ? 'apple-touch-icon' : './apple-touch-icon-dev',
|
||||
@ -130,6 +136,7 @@ module.exports = (env = {}, argv = {}) => {
|
||||
}),
|
||||
new EnvironmentPlugin({
|
||||
APP_ENV: 'production',
|
||||
APP_MOCKED_CLIENT: '',
|
||||
APP_NAME: null,
|
||||
APP_VERSION: appVersion,
|
||||
TELEGRAM_T_API_ID: undefined,
|
||||
|
Loading…
Reference in New Issue
Block a user