Re-write browser history; Introduce playwright tests (another attempt) (#1809)

This commit is contained in:
Alexander Zinchuk 2022-05-03 14:17:05 +01:00
parent 0c5b3e560e
commit 697b709d89
81 changed files with 3241 additions and 437 deletions

View File

@ -14,3 +14,4 @@ src/lib/music-metadata-browser
webpack.config.js
jest.config.js
src/lib/secret-sauce/
playwright.config.ts

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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;

View File

@ -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';

View File

@ -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(() => {

View File

@ -45,7 +45,10 @@ const AuthCode: FC<StateProps> = ({
}
}, []);
useHistoryBack(true, returnToAuthPhoneNumber);
useHistoryBack({
isActive: true,
onBack: returnToAuthPhoneNumber,
});
const onCodeChange = useCallback((e: FormEvent<HTMLInputElement>) => {
if (authError) {

View File

@ -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">

View File

@ -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) => {

View File

@ -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 });

View File

@ -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>

View File

@ -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 });

View File

@ -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('');

View File

@ -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);

View File

@ -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}
/>

View File

@ -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 (

View File

@ -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'));

View File

@ -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.

View File

@ -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">

View File

@ -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;

View File

@ -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}>

View File

@ -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">

View File

@ -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) {

View File

@ -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">

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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">

View File

@ -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 {

View File

@ -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}
/>
);

View File

@ -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 />;

View File

@ -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;

View File

@ -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(() => {

View File

@ -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}
/>

View File

@ -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">

View File

@ -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) {

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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>
);
};

View File

@ -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(() => {

View 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));

View File

@ -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,

View File

@ -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) : [];

View File

@ -91,7 +91,10 @@ const GifSearch: FC<OwnProps & StateProps> = ({
const lang = useLang();
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
function renderContent() {
if (query === undefined) {

View File

@ -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 />;

View File

@ -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) {

View File

@ -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);

View File

@ -57,7 +57,10 @@ const StickerSearch: FC<OwnProps & StateProps> = ({
});
});
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
function renderContent() {
if (query === undefined) {

View File

@ -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) {

View File

@ -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);

View File

@ -65,7 +65,10 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|| (privacyType === 'private' && isPublic),
);
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
useEffect(() => {
if (privacyType && !privateLink) {

View File

@ -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) {

View File

@ -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();

View File

@ -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) {

View File

@ -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);

View File

@ -139,7 +139,10 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
}
}, '.ListItem-button', true);
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
function renderSearchField() {
return (

View File

@ -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);

View File

@ -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) {

View File

@ -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) {

View File

@ -41,7 +41,10 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
isActive,
serverTimeOffset,
}) => {
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
const memberIds = useMemo(() => {
if (!members || !usersById) {

View File

@ -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;

View File

@ -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;

View File

@ -91,7 +91,10 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
}
}, [animationData]);
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
const hasDetailedCountdown = useMemo(() => {
if (!exportedInvites) return undefined;

View File

@ -54,7 +54,10 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
}
}, [animationData]);
useHistoryBack(isActive, onClose);
useHistoryBack({
isActive,
onBack: onClose,
});
useEffect(() => {
if (!chat?.joinRequests && !isUserId(chatId)) {

View File

@ -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;

View File

@ -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 || '') : '';

View File

@ -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)) {

View File

@ -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}

View File

@ -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);

View File

@ -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';

View File

@ -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;
});

View File

@ -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];

View File

@ -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]);
}

View 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;

View File

@ -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;

View File

@ -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 } });
}
}

View File

@ -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) || '{}');

View File

@ -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,