diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx index 0597b228b..3e7898371 100644 --- a/src/components/auth/Auth.tsx +++ b/src/components/auth/Auth.tsx @@ -6,6 +6,7 @@ import { GlobalActions, GlobalState } from '../../global/types'; import '../../modules/actions/initial'; import { pick } from '../../util/iteratees'; import { PLATFORM_ENV } from '../../util/environment'; +import useHistoryBack from '../../hooks/useHistoryBack'; import UiLoader from '../common/UiLoader'; import AuthPhoneNumber from './AuthPhoneNumber'; @@ -17,14 +18,31 @@ import AuthQrCode from './AuthQrCode'; import './Auth.scss'; type StateProps = Pick; -type DispatchProps = Pick; +type DispatchProps = Pick; -const Auth: FC = ({ authState, reset, initApi }) => { +const Auth: FC = ({ + authState, reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode, +}) => { useEffect(() => { reset(); initApi(); }, [reset, initApi]); + const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android'; + + const handleChangeAuthorizationMethod = () => { + if (!isMobile) { + goToAuthQrCode(); + } else { + returnToAuthPhoneNumber(); + } + }; + + useHistoryBack( + (!isMobile && authState === 'authorizationStateWaitPhoneNumber') + || (isMobile && authState === 'authorizationStateWaitQrCode'), handleChangeAuthorizationMethod, + ); + switch (authState) { case 'authorizationStateWaitCode': return ; @@ -37,7 +55,7 @@ const Auth: FC = ({ authState, reset, initApi }) => case 'authorizationStateWaitQrCode': return ; default: - return PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android' + return isMobile ? : ; } @@ -45,5 +63,5 @@ const Auth: FC = ({ authState, reset, initApi }) => export default memo(withGlobal( (global): StateProps => pick(global, ['authState']), - (global, actions): DispatchProps => pick(actions, ['reset', 'initApi']), + (global, actions): DispatchProps => pick(actions, ['reset', 'initApi', 'returnToAuthPhoneNumber', 'goToAuthQrCode']), )(Auth)); diff --git a/src/components/auth/AuthCode.tsx b/src/components/auth/AuthCode.tsx index 77a298d8c..069293204 100644 --- a/src/components/auth/AuthCode.tsx +++ b/src/components/auth/AuthCode.tsx @@ -7,19 +7,27 @@ import { GlobalState, GlobalActions } from '../../global/types'; import { IS_TOUCH_ENV } from '../../util/environment'; import { pick } from '../../util/iteratees'; +import useHistoryBack from '../../hooks/useHistoryBack'; import InputText from '../ui/InputText'; import Loading from '../ui/Loading'; import TrackingMonkey from '../common/TrackingMonkey'; -import useHistoryBack from '../../hooks/useHistoryBack'; type StateProps = Pick; -type DispatchProps = Pick; +type DispatchProps = Pick; const CODE_LENGTH = 5; const AuthCode: FC = ({ - authPhoneNumber, authIsCodeViaApp, authIsLoading, authError, setAuthCode, returnToAuthPhoneNumber, clearAuthError, + authPhoneNumber, + authIsCodeViaApp, + authIsLoading, + authError, + setAuthCode, + returnToAuthPhoneNumber, + clearAuthError, }) => { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -34,7 +42,7 @@ const AuthCode: FC = ({ } }, []); - useHistoryBack(returnToAuthPhoneNumber); + useHistoryBack(true, returnToAuthPhoneNumber); const onCodeChange = useCallback((e: FormEvent) => { if (authError) { @@ -119,5 +127,9 @@ const AuthCode: FC = ({ export default memo(withGlobal( (global): StateProps => pick(global, ['authPhoneNumber', 'authIsCodeViaApp', 'authIsLoading', 'authError']), - (setGlobal, actions): DispatchProps => pick(actions, ['setAuthCode', 'returnToAuthPhoneNumber', 'clearAuthError']), + (setGlobal, actions): DispatchProps => pick(actions, [ + 'setAuthCode', + 'returnToAuthPhoneNumber', + 'clearAuthError', + ]), )(AuthCode)); diff --git a/src/components/auth/AuthPhoneNumber.tsx b/src/components/auth/AuthPhoneNumber.tsx index a3252188c..a67e8b449 100644 --- a/src/components/auth/AuthPhoneNumber.tsx +++ b/src/components/auth/AuthPhoneNumber.tsx @@ -8,7 +8,9 @@ import React, { FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState, } from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; -import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/environment'; +import { + IS_SAFARI, IS_TOUCH_ENV, +} from '../../util/environment'; import { preloadImage } from '../../util/files'; import preloadFonts from '../../util/fonts'; import { pick } from '../../util/iteratees'; diff --git a/src/components/auth/AuthQrCode.tsx b/src/components/auth/AuthQrCode.tsx index 59c5f52fb..e3de0c422 100644 --- a/src/components/auth/AuthQrCode.tsx +++ b/src/components/auth/AuthQrCode.tsx @@ -9,7 +9,6 @@ import { pick } from '../../util/iteratees'; import Loading from '../ui/Loading'; import Button from '../ui/Button'; -import useHistoryBack from '../../hooks/useHistoryBack'; type StateProps = Pick; type DispatchProps = Pick; @@ -17,7 +16,10 @@ type DispatchProps = Pick; const DATA_PREFIX = 'tg://login?token='; const AuthCode: FC = ({ - connectionState, authState, authQrCode, returnToAuthPhoneNumber, + connectionState, + authState, + authQrCode, + returnToAuthPhoneNumber, }) => { // eslint-disable-next-line no-null/no-null const qrCodeRef = useRef(null); @@ -41,8 +43,6 @@ const AuthCode: FC = ({ }, container); }, [connectionState, authQrCode]); - useHistoryBack(returnToAuthPhoneNumber); - const isAuthReady = authState === 'authorizationStateWaitQrCode'; return ( diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index 6a15c50dd..037150eb2 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -28,7 +28,7 @@ type OwnProps = { children: any; }; -type StateProps = Pick & { +type StateProps = Pick & { hasCustomBackground?: boolean; hasCustomBackgroundColor: boolean; isRightColumnShown?: boolean; @@ -82,6 +82,7 @@ const UiLoader: FC = ({ hasCustomBackground, hasCustomBackgroundColor, isRightColumnShown, + shouldSkipHistoryAnimations, setIsUiReady, }) => { const [isReady, markReady] = useFlag(); @@ -126,7 +127,7 @@ const UiLoader: FC = ({ return (
{children} - {shouldRenderMask && ( + {shouldRenderMask && !shouldSkipHistoryAnimations && (
{page === 'main' ? ( <> @@ -156,6 +157,7 @@ export default withGlobal( const { background, backgroundColor } = global.settings.themes[theme] || {}; return { + shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, uiReadyState: global.uiReadyState, hasCustomBackground: Boolean(background), hasCustomBackgroundColor: Boolean(backgroundColor), diff --git a/src/components/left/ArchivedChats.tsx b/src/components/left/ArchivedChats.tsx index dec5d208c..9f2e0cba8 100644 --- a/src/components/left/ArchivedChats.tsx +++ b/src/components/left/ArchivedChats.tsx @@ -1,20 +1,25 @@ import React, { FC, memo } from '../../lib/teact/teact'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Button from '../ui/Button'; import ChatList from './main/ChatList'; +import { LeftColumnContent } from '../../types'; import './ArchivedChats.scss'; export type OwnProps = { isActive: boolean; onReset: () => void; + onContentChange: (content: LeftColumnContent) => void; }; -const ArchivedChats: FC = ({ isActive, onReset }) => { +const ArchivedChats: FC = ({ isActive, onReset, onContentChange }) => { const lang = useLang(); + useHistoryBack(isActive, onReset, onContentChange, LeftColumnContent.Archived); + return (
diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index f61ebe0bf..fae950c6e 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -22,6 +22,7 @@ type StateProps = { searchQuery?: string; searchDate?: number; activeChatFolder: number; + shouldSkipHistoryAnimations?: boolean; }; type DispatchProps = Pick = ({ searchQuery, searchDate, activeChatFolder, + shouldSkipHistoryAnimations, setGlobalSearchQuery, setGlobalSearchChatId, resetChatCreation, @@ -80,14 +82,20 @@ const LeftColumn: FC = ({ } const handleReset = useCallback((forceReturnToChatList?: boolean) => { - if ( - content === LeftColumnContent.NewGroupStep2 + if (content === LeftColumnContent.NewGroupStep2 && !forceReturnToChatList ) { setContent(LeftColumnContent.NewGroupStep1); return; } + if (content === LeftColumnContent.NewChannelStep2 + && !forceReturnToChatList + ) { + setContent(LeftColumnContent.NewChannelStep1); + return; + } + if (content === LeftColumnContent.NewGroupStep1) { const pickerSearchInput = document.getElementById('new-group-picker-search'); if (pickerSearchInput) { @@ -205,8 +213,8 @@ const LeftColumn: FC = ({ setLastResetTime(Date.now()); }, RESET_TRANSITION_DELAY_MS); }, [ - content, activeChatFolder, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId, resetChatCreation, - settingsScreen, + content, activeChatFolder, settingsScreen, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId, + resetChatCreation, ]); const handleSearchQuery = useCallback((query: string) => { @@ -220,7 +228,7 @@ const LeftColumn: FC = ({ if (query !== searchQuery) { setGlobalSearchQuery({ query }); } - }, [content, setGlobalSearchQuery, searchQuery]); + }, [content, searchQuery, setGlobalSearchQuery]); useEffect( () => (content !== LeftColumnContent.ChatList || activeChatFolder === 0 @@ -237,10 +245,15 @@ const LeftColumn: FC = ({ } }, [clearTwoFaError, loadPasswordInfo, settingsScreen]); + const handleSettingsScreenSelect = (screen: SettingsScreens) => { + setContent(LeftColumnContent.Settings); + setSettingsScreen(screen); + }; + return ( = ({ ); case ContentType.Settings: return ( ); case ContentType.NewChannel: return ( = ({ return ( = ({ onContentChange={setContent} onSearchQuery={handleSearchQuery} onReset={handleReset} + shouldSkipTransition={shouldSkipHistoryAnimations} /> ); } @@ -310,8 +329,11 @@ export default memo(withGlobal( chatFolders: { activeChatFolder, }, + shouldSkipHistoryAnimations, } = global; - return { searchQuery: query, searchDate: date, activeChatFolder }; + return { + searchQuery: query, searchDate: date, activeChatFolder, shouldSkipHistoryAnimations, + }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'setGlobalSearchQuery', 'setGlobalSearchChatId', 'resetChatCreation', 'setGlobalSearchDate', diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 58c2eda06..469d23205 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -16,6 +16,7 @@ import useShowTransition from '../../../hooks/useShowTransition'; import buildClassName from '../../../util/buildClassName'; import useThrottledMemo from '../../../hooks/useThrottledMemo'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import Transition from '../../ui/Transition'; @@ -144,6 +145,8 @@ const ChatFolders: FC = ({ } }) : undefined), [activeChatFolder, setActiveChatFolder]); + useHistoryBack(activeChatFolder !== 0, () => setActiveChatFolder(0)); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.metaKey && e.code.startsWith('Digit') && folderTabs) { diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index 9bcf1e570..53453cfa3 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -12,6 +12,7 @@ import searchWords from '../../../util/searchWords'; import { pick } from '../../../util/iteratees'; import { getUserFullName, sortUserIds } from '../../../modules/helpers'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import InfiniteScroll from '../../ui/InfiniteScroll'; @@ -20,6 +21,8 @@ import Loading from '../../ui/Loading'; export type OwnProps = { filter: string; + isActive: boolean; + onReset: () => void; }; type StateProps = { @@ -33,6 +36,7 @@ type DispatchProps = Pick; const runThrottled = throttle((cb) => cb(), 60000, true); const ContactList: FC = ({ + isActive, onReset, filter, usersById, contactIds, loadContactList, openChat, serverTimeOffset, }) => { // Due to the parent Transition, this component never gets unmounted, @@ -43,6 +47,8 @@ const ContactList: FC = ({ }); }); + useHistoryBack(isActive, onReset); + const handleClick = useCallback( (id: number) => { openChat({ id }); diff --git a/src/components/left/main/LeftMain.tsx b/src/components/left/main/LeftMain.tsx index 45de22650..76022a825 100644 --- a/src/components/left/main/LeftMain.tsx +++ b/src/components/left/main/LeftMain.tsx @@ -31,6 +31,7 @@ type OwnProps = { searchQuery?: string; searchDate?: number; contactsFilter: string; + shouldSkipTransition?: boolean; onSearchQuery: (query: string) => void; onContentChange: (content: LeftColumnContent) => void; onReset: () => void; @@ -49,6 +50,7 @@ const LeftMain: FC = ({ searchQuery, searchDate, contactsFilter, + shouldSkipTransition, onSearchQuery, onContentChange, onReset, @@ -140,12 +142,13 @@ const LeftMain: FC = ({ onSelectContacts={handleSelectContacts} onSelectArchived={handleSelectArchived} onReset={onReset} + shouldSkipTransition={shouldSkipTransition} /> {() => } = ({ /> ); case LeftColumnContent.Contacts: - return ; + return ; default: return undefined; } diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index b4366aec1..b1d628e71 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -40,6 +40,14 @@ transform: rotate(-45deg) scaleX(0.75) translate(0.375rem, 0.1875rem); } } + + &.no-animation { + transition: none; + + &::before, &::after { + transition: none; + } + } } .archived-badge { diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index a45da551e..3eeff7586 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -31,6 +31,7 @@ import './LeftMainHeader.scss'; type OwnProps = { content: LeftColumnContent; contactsFilter: string; + shouldSkipTransition?: boolean; onSearchQuery: (query: string) => void; onSelectSettings: () => void; onSelectContacts: () => void; @@ -71,6 +72,7 @@ const LeftMainHeader: FC = ({ onReset, searchQuery, isLoading, + shouldSkipTransition, currentUserId, globalSearchChatId, searchDate, @@ -118,10 +120,15 @@ const LeftMainHeader: FC = ({ onClick={hasMenu ? onTrigger : () => onReset()} ariaLabel={hasMenu ? lang('AccDescrOpenMenu2') : 'Return to chat list'} > -
+
); - }, [hasMenu, lang, onReset]); + }, [hasMenu, lang, onReset, shouldSkipTransition]); const handleSearchFocus = useCallback(() => { if (!searchQuery) { diff --git a/src/components/left/newChat/NewChat.tsx b/src/components/left/newChat/NewChat.tsx index b2384f117..f1b8e88e5 100644 --- a/src/components/left/newChat/NewChat.tsx +++ b/src/components/left/newChat/NewChat.tsx @@ -13,6 +13,7 @@ import NewChatStep2 from './NewChatStep2'; import './NewChat.scss'; export type OwnProps = { + isActive: boolean; isChannel?: boolean; content: LeftColumnContent; onContentChange: (content: LeftColumnContent) => void; @@ -22,6 +23,7 @@ export type OwnProps = { const RENDER_COUNT = Object.keys(LeftColumnContent).length / 2; const NewChat: FC = ({ + isActive, isChannel = false, content, onContentChange, @@ -40,13 +42,14 @@ const NewChat: FC = ({ renderCount={RENDER_COUNT} activeKey={content} > - {() => { + {(isStepActive) => { switch (content) { case LeftColumnContent.NewChannelStep1: case LeftColumnContent.NewGroupStep1: return ( = ({ return ( diff --git a/src/components/left/newChat/NewChatStep1.tsx b/src/components/left/newChat/NewChatStep1.tsx index 5dd1a0379..50999dcc1 100644 --- a/src/components/left/newChat/NewChatStep1.tsx +++ b/src/components/left/newChat/NewChatStep1.tsx @@ -11,6 +11,7 @@ import { throttle } from '../../../util/schedulers'; import searchWords from '../../../util/searchWords'; import { getUserFullName, sortChatIds } from '../../../modules/helpers'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import Picker from '../../common/Picker'; import FloatingActionButton from '../../ui/FloatingActionButton'; @@ -18,6 +19,7 @@ import Button from '../../ui/Button'; export type OwnProps = { isChannel?: boolean; + isActive: boolean; selectedMemberIds: number[]; onSelectedMemberIdsChange: (ids: number[]) => void; onNextStep: () => void; @@ -41,6 +43,7 @@ const runThrottled = throttle((cb) => cb(), 60000, true); const NewChatStep1: FC = ({ isChannel, + isActive, selectedMemberIds, onSelectedMemberIdsChange, onNextStep, @@ -64,6 +67,10 @@ const NewChatStep1: FC = ({ }); }); + const lang = useLang(); + + useHistoryBack(isActive, onReset); + const handleFilterChange = useCallback((query: string) => { setGlobalSearchQuery({ query }); }, [setGlobalSearchQuery]); @@ -108,8 +115,6 @@ const NewChatStep1: FC = ({ } }, [selectedMemberIds.length, isChannel, setGlobalSearchQuery, onNextStep]); - const lang = useLang(); - return (
diff --git a/src/components/left/newChat/NewChatStep2.tsx b/src/components/left/newChat/NewChatStep2.tsx index 3fa1b4875..71db28680 100644 --- a/src/components/left/newChat/NewChatStep2.tsx +++ b/src/components/left/newChat/NewChatStep2.tsx @@ -8,6 +8,7 @@ import { ChatCreationProgress } from '../../../types'; import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import InputText from '../../ui/InputText'; import FloatingActionButton from '../../ui/FloatingActionButton'; @@ -19,6 +20,7 @@ import PrivateChatInfo from '../../common/PrivateChatInfo'; export type OwnProps = { isChannel?: boolean; + isActive: boolean; memberIds: number[]; onReset: (forceReturnToChatList?: boolean) => void; }; @@ -35,6 +37,7 @@ const MAX_USERS_FOR_LEGACY_CHAT = 199; // Accounting for current user const NewChatStep2: FC = ({ isChannel, + isActive, memberIds, onReset, creationProgress, @@ -44,6 +47,8 @@ const NewChatStep2: FC = ({ }) => { const lang = useLang(); + useHistoryBack(isActive, onReset); + const [title, setTitle] = useState(''); const [about, setAbout] = useState(''); const [photo, setPhoto] = useState(); diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index 15ccb8efb..00ca71cd1 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -10,6 +10,7 @@ import { pick } from '../../../util/iteratees'; import { parseDateString } from '../../../util/dateFormat'; import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import TabList from '../../ui/TabList'; import Transition from '../../ui/Transition'; @@ -76,6 +77,8 @@ const LeftSearch: FC = ({ setGlobalSearchDate({ date: value.getTime() / 1000 }); }, [setGlobalSearchDate]); + useHistoryBack(isActive, onReset, undefined, undefined, true); + // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); const handleKeyDown = useKeyboardListNavigation(containerRef, isActive, undefined, '.ListItem-button', true); diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index 992c84788..23c9ccf46 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -27,6 +27,7 @@ } .settings-content { + background: var(--color-background); height: calc(100% - var(--header-height)); overflow-y: auto; diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index dc546e394..03d1ca248 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -28,16 +28,77 @@ import './Settings.scss'; const TRANSITION_RENDER_COUNT = Object.keys(SettingsScreens).length / 2; const TRANSITION_DURATION = 200; +const TWO_FA_SCREENS = [ + SettingsScreens.TwoFaDisabled, + SettingsScreens.TwoFaNewPassword, + SettingsScreens.TwoFaNewPasswordConfirm, + SettingsScreens.TwoFaNewPasswordHint, + SettingsScreens.TwoFaNewPasswordEmail, + SettingsScreens.TwoFaNewPasswordEmailCode, + SettingsScreens.TwoFaCongratulations, + SettingsScreens.TwoFaEnabled, + SettingsScreens.TwoFaChangePasswordCurrent, + SettingsScreens.TwoFaChangePasswordNew, + SettingsScreens.TwoFaChangePasswordConfirm, + SettingsScreens.TwoFaChangePasswordHint, + SettingsScreens.TwoFaTurnOff, + SettingsScreens.TwoFaRecoveryEmailCurrentPassword, + SettingsScreens.TwoFaRecoveryEmail, + SettingsScreens.TwoFaRecoveryEmailCode, +]; + +const FOLDERS_SCREENS = [ + SettingsScreens.Folders, + SettingsScreens.FoldersCreateFolder, + SettingsScreens.FoldersEditFolder, + SettingsScreens.FoldersIncludedChats, + SettingsScreens.FoldersExcludedChats, +]; + +const PRIVACY_SCREENS = [ + SettingsScreens.PrivacyBlockedUsers, + SettingsScreens.PrivacyActiveSessions, +]; + +const PRIVACY_PHONE_NUMBER_SCREENS = [ + SettingsScreens.PrivacyPhoneNumberAllowedContacts, + SettingsScreens.PrivacyPhoneNumberDeniedContacts, +]; + +const PRIVACY_LAST_SEEN_PHONE_SCREENS = [ + SettingsScreens.PrivacyLastSeenAllowedContacts, + SettingsScreens.PrivacyLastSeenDeniedContacts, +]; + +const PRIVACY_PROFILE_PHOTO_SCREENS = [ + SettingsScreens.PrivacyProfilePhotoAllowedContacts, + SettingsScreens.PrivacyProfilePhotoDeniedContacts, +]; + +const PRIVACY_FORWARDING_SCREENS = [ + SettingsScreens.PrivacyForwardingAllowedContacts, + SettingsScreens.PrivacyForwardingDeniedContacts, +]; + +const PRIVACY_GROUP_CHATS_SCREENS = [ + SettingsScreens.PrivacyGroupChatsAllowedContacts, + SettingsScreens.PrivacyGroupChatsDeniedContacts, +]; + export type OwnProps = { + isActive: boolean; currentScreen: SettingsScreens; onScreenSelect: (screen: SettingsScreens) => void; + shouldSkipTransition?: boolean; onReset: () => void; }; const Settings: FC = ({ + isActive, currentScreen, onScreenSelect, onReset, + shouldSkipTransition, }) => { const [foldersState, foldersDispatch] = useFoldersReducer(); const [twoFaState, twoFaDispatch] = useTwoFaReducer(); @@ -75,47 +136,93 @@ const Settings: FC = ({ handleReset(); }, [foldersDispatch, handleReset]); - function renderCurrentSectionContent() { + function renderCurrentSectionContent(isScreenActive: boolean, screen: SettingsScreens) { + const privacyAllowScreens: Record = { + [SettingsScreens.PrivacyPhoneNumber]: PRIVACY_PHONE_NUMBER_SCREENS.includes(screen), + [SettingsScreens.PrivacyLastSeen]: PRIVACY_LAST_SEEN_PHONE_SCREENS.includes(screen), + [SettingsScreens.PrivacyProfilePhoto]: PRIVACY_PROFILE_PHOTO_SCREENS.includes(screen), + [SettingsScreens.PrivacyForwarding]: PRIVACY_FORWARDING_SCREENS.includes(screen), + [SettingsScreens.PrivacyGroupChats]: PRIVACY_GROUP_CHATS_SCREENS.includes(screen), + }; + + const isTwoFaScreen = TWO_FA_SCREENS.includes(screen); + const isFoldersScreen = FOLDERS_SCREENS.includes(screen); + const isPrivacyScreen = PRIVACY_SCREENS.includes(screen) + || isTwoFaScreen + || Object.keys(privacyAllowScreens).includes(screen.toString()) + || Object.values(privacyAllowScreens).find((key) => key === true); + switch (currentScreen) { case SettingsScreens.Main: return ( - + ); case SettingsScreens.EditProfile: return ( - + ); case SettingsScreens.General: return ( - + ); case SettingsScreens.Notifications: return ( - + ); case SettingsScreens.Privacy: return ( - + ); case SettingsScreens.Language: return ( - + ); case SettingsScreens.GeneralChatBackground: return ( - + ); case SettingsScreens.GeneralChatBackgroundColor: return ( - + ); case SettingsScreens.PrivacyActiveSessions: return ( - + ); case SettingsScreens.PrivacyBlockedUsers: return ( - + ); case SettingsScreens.PrivacyPhoneNumber: case SettingsScreens.PrivacyLastSeen: @@ -123,7 +230,12 @@ const Settings: FC = ({ case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyGroupChats: return ( - + ); case SettingsScreens.PrivacyPhoneNumberAllowedContacts: @@ -136,6 +248,8 @@ const Settings: FC = ({ isAllowList screen={currentScreen} onScreenSelect={onScreenSelect} + isActive={isScreenActive || privacyAllowScreens[currentScreen]} + onReset={handleReset} /> ); @@ -148,6 +262,8 @@ const Settings: FC = ({ ); @@ -159,8 +275,10 @@ const Settings: FC = ({ return ( @@ -187,7 +305,10 @@ const Settings: FC = ({ currentScreen={currentScreen} state={twoFaState} dispatch={twoFaDispatch} + shownScreen={screen} + isActive={isScreenActive} onScreenSelect={onScreenSelect} + onReset={handleReset} /> ); @@ -196,7 +317,7 @@ const Settings: FC = ({ } } - function renderCurrentSection() { + function renderCurrentSection(isScreenActive: boolean, isFrom: boolean, currentKey: SettingsScreens) { return ( <> = ({ onSaveFilter={handleSaveFilter} editedFolderId={foldersState.folderId} /> - {renderCurrentSectionContent()} + {renderCurrentSectionContent(isScreenActive, currentKey)} ); } @@ -213,7 +334,7 @@ const Settings: FC = ({ return ( diff --git a/src/components/left/settings/SettingsEditProfile.tsx b/src/components/left/settings/SettingsEditProfile.tsx index a407b4b92..d253aec6c 100644 --- a/src/components/left/settings/SettingsEditProfile.tsx +++ b/src/components/left/settings/SettingsEditProfile.tsx @@ -6,7 +6,7 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { ApiMediaFormat } from '../../../api/types'; import { GlobalActions } from '../../../global/types'; -import { ProfileEditProgress } from '../../../types'; +import { ProfileEditProgress, SettingsScreens } from '../../../types'; import { throttle } from '../../../util/schedulers'; import { pick } from '../../../util/iteratees'; @@ -21,6 +21,13 @@ import Spinner from '../../ui/Spinner'; import InputText from '../../ui/InputText'; import renderText from '../../common/helpers/renderText'; import UsernameInput from '../../common/UsernameInput'; +import useHistoryBack from '../../../hooks/useHistoryBack'; + +type OwnProps = { + isActive: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; type StateProps = { currentAvatarHash?: string; @@ -43,7 +50,10 @@ const MAX_BIO_LENGTH = 70; const ERROR_FIRST_NAME_MISSING = 'Please provide your first name'; const ERROR_BIO_TOO_LONG = 'Bio can\' be longer than 70 characters'; -const SettingsEditProfile: FC = ({ +const SettingsEditProfile: FC = ({ + isActive, + onScreenSelect, + onReset, currentAvatarHash, currentFirstName, currentLastName, @@ -55,6 +65,8 @@ const SettingsEditProfile: FC = ({ updateProfile, checkUsername, }) => { + const lang = useLang(); + const [isUsernameTouched, setIsUsernameTouched] = useState(false); const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false); const [error, setError] = useState(); @@ -78,6 +90,8 @@ const SettingsEditProfile: FC = ({ return Boolean(photo) || isProfileFieldsTouched || isUsernameAvailable === true; }, [photo, isProfileFieldsTouched, isUsernameError, isUsernameAvailable]); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.EditProfile); + // Due to the parent Transition, this component never gets unmounted, // that's why we use throttled API call on every update. useEffect(() => { @@ -165,8 +179,6 @@ const SettingsEditProfile: FC = ({ updateProfile, ]); - const lang = useLang(); - return (
@@ -242,7 +254,7 @@ const SettingsEditProfile: FC = ({ ); }; -export default memo(withGlobal( +export default memo(withGlobal( (global): StateProps => { const { currentUserId } = global; const { progress, isUsernameAvailable } = global.profileEdit || {}; diff --git a/src/components/left/settings/SettingsGeneral.tsx b/src/components/left/settings/SettingsGeneral.tsx index 56f5dd750..5f2c7ac84 100644 --- a/src/components/left/settings/SettingsGeneral.tsx +++ b/src/components/left/settings/SettingsGeneral.tsx @@ -12,6 +12,7 @@ import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; import useFlag from '../../../hooks/useFlag'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import RangeSlider from '../../ui/RangeSlider'; @@ -21,7 +22,9 @@ import SettingsStickerSet from './SettingsStickerSet'; import StickerSetModal from '../../common/StickerSetModal.async'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = Pick = ({ + isActive, onScreenSelect, + onReset, stickerSetIds, stickerSetsById, messageTextSize, @@ -120,6 +125,8 @@ const SettingsGeneral: FC = ({ return stickerSetsById && stickerSetsById[id] && stickerSetsById[id].installedDate ? stickerSetsById[id] : false; }).filter(Boolean as any); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General); + return (
diff --git a/src/components/left/settings/SettingsGeneralBackground.tsx b/src/components/left/settings/SettingsGeneralBackground.tsx index 82cee76c7..c2bde98a6 100644 --- a/src/components/left/settings/SettingsGeneralBackground.tsx +++ b/src/components/left/settings/SettingsGeneralBackground.tsx @@ -14,6 +14,7 @@ import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; import { getAverageColor, getPatternColor, rgb2hex } from '../../../util/colors'; import { selectTheme } from '../../../modules/selectors'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import Checkbox from '../../ui/Checkbox'; @@ -23,7 +24,9 @@ import WallpaperTile from './WallpaperTile'; import './SettingsGeneralBackground.scss'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -42,7 +45,9 @@ const SUPPORTED_TYPES = 'image/jpeg'; const runThrottled = throttle((cb) => cb(), 60000, true); const SettingsGeneralBackground: FC = ({ + isActive, onScreenSelect, + onReset, background, isBlurred, loadedWallpapers, @@ -106,6 +111,8 @@ const SettingsGeneralBackground: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackground); + const isUploading = loadedWallpapers && loadedWallpapers[0] && loadedWallpapers[0].slug === UPLOADING_WALLPAPER_SLUG; return ( diff --git a/src/components/left/settings/SettingsGeneralBackgroundColor.tsx b/src/components/left/settings/SettingsGeneralBackgroundColor.tsx index 2ca90e3f7..f900283b5 100644 --- a/src/components/left/settings/SettingsGeneralBackgroundColor.tsx +++ b/src/components/left/settings/SettingsGeneralBackgroundColor.tsx @@ -15,13 +15,16 @@ import { captureEvents, RealTouchEvent } from '../../../util/captureEvents'; import { selectTheme } from '../../../modules/selectors'; import useFlag from '../../../hooks/useFlag'; import buildClassName from '../../../util/buildClassName'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import InputText from '../../ui/InputText'; import './SettingsGeneralBackgroundColor.scss'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -51,6 +54,9 @@ const PREDEFINED_COLORS = [ ]; const SettingsGeneralBackground: FC = ({ + isActive, + onScreenSelect, + onReset, theme, backgroundColor, setThemeSettings, @@ -195,6 +201,8 @@ const SettingsGeneralBackground: FC = ({ isDragging && 'is-dragging', ); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackgroundColor); + return (
diff --git a/src/components/left/settings/SettingsLanguage.tsx b/src/components/left/settings/SettingsLanguage.tsx index 73d7b974c..5a39ac66b 100644 --- a/src/components/left/settings/SettingsLanguage.tsx +++ b/src/components/left/settings/SettingsLanguage.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { ISettings } from '../../../types'; +import { ISettings, SettingsScreens } from '../../../types'; import { ApiLanguage } from '../../../api/types'; import { setLanguage } from '../../../util/langProvider'; @@ -13,12 +13,22 @@ import { pick } from '../../../util/iteratees'; import RadioGroup from '../../ui/RadioGroup'; import Loading from '../../ui/Loading'; import useFlag from '../../../hooks/useFlag'; +import useHistoryBack from '../../../hooks/useHistoryBack'; + +type OwnProps = { + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; type StateProps = Pick; type DispatchProps = Pick; -const SettingsLanguage: FC = ({ +const SettingsLanguage: FC = ({ + isActive, + onScreenSelect, + onReset, languages, language, loadLanguages, @@ -47,6 +57,8 @@ const SettingsLanguage: FC = ({ return languages ? buildOptions(languages) : undefined; }, [languages]); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Language); + return (
{options ? ( @@ -77,7 +89,7 @@ function buildOptions(languages: ApiLanguage[]) { }); } -export default memo(withGlobal( +export default memo(withGlobal( (global): StateProps => { return { languages: global.settings.byKey.languages, diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 263c93584..5740e7430 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -8,12 +8,15 @@ import { selectUser } from '../../../modules/selectors'; import { getUserFullName } from '../../../modules/helpers'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import Avatar from '../../common/Avatar'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -21,11 +24,15 @@ type StateProps = { }; const SettingsMain: FC = ({ + isActive, onScreenSelect, + onReset, currentUser, }) => { const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main); + return (
diff --git a/src/components/left/settings/SettingsNotifications.tsx b/src/components/left/settings/SettingsNotifications.tsx index 051efcba4..245cad043 100644 --- a/src/components/left/settings/SettingsNotifications.tsx +++ b/src/components/left/settings/SettingsNotifications.tsx @@ -5,12 +5,20 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; +import { SettingsScreens } from '../../../types'; import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import Checkbox from '../../ui/Checkbox'; +type OwnProps = { + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; + type StateProps = { hasPrivateChatsNotifications: boolean; hasPrivateChatsMessagePreview: boolean; @@ -25,7 +33,10 @@ type DispatchProps = Pick; -const SettingsNotifications: FC = ({ +const SettingsNotifications: FC = ({ + isActive, + onScreenSelect, + onReset, hasPrivateChatsNotifications, hasPrivateChatsMessagePreview, hasGroupNotifications, @@ -73,6 +84,8 @@ const SettingsNotifications: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Notifications); + return (
@@ -145,7 +158,7 @@ const SettingsNotifications: FC = ({ ); }; -export default memo(withGlobal((global): StateProps => { +export default memo(withGlobal((global): StateProps => { return { hasPrivateChatsNotifications: Boolean(global.settings.byKey.hasPrivateChatsNotifications), hasPrivateChatsMessagePreview: Boolean(global.settings.byKey.hasPrivateChatsMessagePreview), diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 6ee50dcb3..d253c00c8 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -6,12 +6,15 @@ import { PrivacyVisibility, SettingsScreens } from '../../../types'; import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import Checkbox from '../../ui/Checkbox'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -32,7 +35,9 @@ type DispatchProps = Pick; const SettingsPrivacy: FC = ({ + isActive, onScreenSelect, + onReset, hasPassword, blockedCount, sessionsCount, @@ -58,6 +63,8 @@ const SettingsPrivacy: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Privacy); + function getVisibilityValue(visibility?: PrivacyVisibility) { switch (visibility) { case 'everybody': diff --git a/src/components/left/settings/SettingsPrivacyActiveSessions.tsx b/src/components/left/settings/SettingsPrivacyActiveSessions.tsx index cb7205bae..802be3135 100644 --- a/src/components/left/settings/SettingsPrivacyActiveSessions.tsx +++ b/src/components/left/settings/SettingsPrivacyActiveSessions.tsx @@ -5,15 +5,23 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; import { ApiSession } from '../../../api/types'; +import { SettingsScreens } from '../../../types'; import { pick } from '../../../util/iteratees'; import { formatPastTimeShort } from '../../../util/dateFormat'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import ConfirmDialog from '../../ui/ConfirmDialog'; +type OwnProps = { + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; + type StateProps = { activeSessions: ApiSession[]; }; @@ -22,7 +30,10 @@ type DispatchProps = Pick; -const SettingsPrivacyActiveSessions: FC = ({ +const SettingsPrivacyActiveSessions: FC = ({ + isActive, + onScreenSelect, + onReset, activeSessions, loadAuthorizations, terminateAuthorization, @@ -52,6 +63,8 @@ const SettingsPrivacyActiveSessions: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.PrivacyActiveSessions); + function renderCurrentSession(session: ApiSession) { return (
@@ -140,7 +153,7 @@ function getDeviceEnvironment(session: ApiSession) { return `${session.deviceModel}${session.deviceModel ? ', ' : ''} ${session.platform} ${session.systemVersion}`; } -export default memo(withGlobal( +export default memo(withGlobal( (global): StateProps => { return { activeSessions: global.activeSessions, diff --git a/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx b/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx index 841e33aee..ec57695b1 100644 --- a/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx +++ b/src/components/left/settings/SettingsPrivacyBlockedUsers.tsx @@ -5,6 +5,7 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; import { ApiChat, ApiUser } from '../../../api/types'; +import { SettingsScreens } from '../../../types'; import { CHAT_HEIGHT_PX } from '../../../config'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; @@ -15,12 +16,19 @@ import { import renderText from '../../common/helpers/renderText'; import buildClassName from '../../../util/buildClassName'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import FloatingActionButton from '../../ui/FloatingActionButton'; import Avatar from '../../common/Avatar'; import Loading from '../../ui/Loading'; +type OwnProps = { + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; +}; + type StateProps = { chatsByIds: Record; usersByIds: Record; @@ -29,7 +37,10 @@ type StateProps = { type DispatchProps = Pick; -const SettingsPrivacyBlockedUsers: FC = ({ +const SettingsPrivacyBlockedUsers: FC = ({ + isActive, + onScreenSelect, + onReset, chatsByIds, usersByIds, blockedIds, @@ -41,6 +52,8 @@ const SettingsPrivacyBlockedUsers: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.PrivacyBlockedUsers); + function renderContact(contactId: number, i: number, viewportOffset: number) { const isPrivate = isChatPrivate(contactId); const user = isPrivate ? usersByIds[contactId] : undefined; @@ -118,7 +131,7 @@ const SettingsPrivacyBlockedUsers: FC = ({ }; -export default memo(withGlobal( +export default memo(withGlobal( (global): StateProps => { const { chats: { diff --git a/src/components/left/settings/SettingsPrivacyVisibility.tsx b/src/components/left/settings/SettingsPrivacyVisibility.tsx index e5b16a636..b8369b14f 100644 --- a/src/components/left/settings/SettingsPrivacyVisibility.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibility.tsx @@ -9,6 +9,7 @@ import { ApiPrivacySettings, SettingsScreens } from '../../../types'; import useLang from '../../../hooks/useLang'; import { pick } from '../../../util/iteratees'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import RadioGroup from '../../ui/RadioGroup'; @@ -16,7 +17,9 @@ import { getPrivacyKey } from './helper/privacy'; type OwnProps = { screen: SettingsScreens; + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = Partial & { @@ -28,7 +31,9 @@ type DispatchProps = Pick; const SettingsPrivacyVisibility: FC = ({ screen, + isActive, onScreenSelect, + onReset, visibility, allowUserIds, allowChatIds, @@ -81,6 +86,8 @@ const SettingsPrivacyVisibility: FC = ({ } }, [lang, screen]); + useHistoryBack(isActive, onReset, onScreenSelect, screen); + const descriptionText = useMemo(() => { switch (screen) { case SettingsScreens.PrivacyLastSeen: diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index d1b3f0bc4..884aae77e 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -14,6 +14,7 @@ import { getPrivacyKey } from './helper/privacy'; import { getChatTitle, isChatGroup, isChatPrivate, prepareChatList, } from '../../../modules/helpers'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import Picker from '../../common/Picker'; import FloatingActionButton from '../../ui/FloatingActionButton'; @@ -21,7 +22,9 @@ import FloatingActionButton from '../../ui/FloatingActionButton'; export type OwnProps = { isAllowList?: boolean; screen: SettingsScreens; + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -47,7 +50,9 @@ const SettingsPrivacyVisibilityExceptionList: FC { const lang = useLang(); @@ -122,6 +127,9 @@ const SettingsPrivacyVisibilityExceptionList: FC void; onReset: () => void; }; const SettingsFolders: FC = ({ currentScreen, + shownScreen, state, dispatch, + isActive, onScreenSelect, onReset, }) => { @@ -82,6 +86,14 @@ const SettingsFolders: FC = ({ ); case SettingsScreens.FoldersCreateFolder: @@ -93,6 +105,12 @@ const SettingsFolders: FC = ({ onAddIncludedChats={handleAddIncludedChats} onAddExcludedChats={handleAddExcludedChats} onReset={handleReset} + onScreenSelect={onScreenSelect} + isActive={isActive || [ + SettingsScreens.FoldersIncludedChats, + SettingsScreens.FoldersExcludedChats, + ].includes(shownScreen)} + onBack={onReset} /> ); case SettingsScreens.FoldersIncludedChats: @@ -101,6 +119,9 @@ const SettingsFolders: FC = ({ mode="included" state={state} dispatch={dispatch} + onReset={handleReset} + onScreenSelect={onScreenSelect} + isActive={isActive} /> ); case SettingsScreens.FoldersExcludedChats: @@ -109,6 +130,9 @@ const SettingsFolders: FC = ({ mode="excluded" state={state} dispatch={dispatch} + onReset={handleReset} + onScreenSelect={onScreenSelect} + isActive={isActive} /> ); diff --git a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx index 9801f07b7..3c00687de 100644 --- a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx +++ b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx @@ -5,6 +5,7 @@ import { withGlobal } from '../../../../lib/teact/teactn'; import { GlobalActions } from '../../../../global/types'; import { ApiChat } from '../../../../api/types'; +import { SettingsScreens } from '../../../../types'; import useLang from '../../../../hooks/useLang'; import { pick } from '../../../../util/iteratees'; @@ -15,6 +16,7 @@ import { FolderEditDispatch, selectChatFilters, } from '../../../../hooks/reducers/useFoldersReducer'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import SettingsFoldersChatsPicker from './SettingsFoldersChatsPicker'; @@ -24,6 +26,9 @@ type OwnProps = { mode: 'included' | 'excluded'; state: FoldersState; dispatch: FolderEditDispatch; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -37,6 +42,9 @@ type StateProps = { type DispatchProps = Pick; const SettingsFoldersChatFilters: FC = ({ + isActive, + onScreenSelect, + onReset, mode, state, dispatch, @@ -132,6 +140,9 @@ const SettingsFoldersChatFilters: FC = ({ } }, [mode, selectedChatIds, dispatch]); + useHistoryBack(isActive, onReset, onScreenSelect, + mode === 'included' ? SettingsScreens.FoldersIncludedChats : SettingsScreens.FoldersExcludedChats); + if (!displayedIds) { return ; } diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index 435bbc7a3..db8796052 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -1,22 +1,24 @@ import React, { - FC, memo, useCallback, useState, useEffect, useMemo, + FC, memo, useCallback, useEffect, useMemo, useState, } from '../../../../lib/teact/teact'; import { withGlobal } from '../../../../lib/teact/teactn'; import { GlobalActions } from '../../../../global/types'; +import { SettingsScreens } from '../../../../types'; import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; -import { pick, findIntersectionWithSet } from '../../../../util/iteratees'; +import { findIntersectionWithSet, pick } from '../../../../util/iteratees'; import { isChatPrivate } from '../../../../modules/helpers'; import getAnimationData from '../../../common/helpers/animatedAssets'; import { - FoldersState, - FolderEditDispatch, - INCLUDED_CHAT_TYPES, EXCLUDED_CHAT_TYPES, + FolderEditDispatch, + FoldersState, + INCLUDED_CHAT_TYPES, selectChatFilters, } from '../../../../hooks/reducers/useFoldersReducer'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import ListItem from '../../../ui/ListItem'; import AnimatedSticker from '../../../common/AnimatedSticker'; @@ -32,7 +34,10 @@ type OwnProps = { dispatch: FolderEditDispatch; onAddIncludedChats: () => void; onAddExcludedChats: () => void; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; onReset: () => void; + onBack: () => void; }; type StateProps = { @@ -54,7 +59,10 @@ const SettingsFoldersEdit: FC = ({ dispatch, onAddIncludedChats, onAddExcludedChats, + isActive, + onScreenSelect, onReset, + onBack, loadedActiveChatIds, loadedArchivedChatIds, editChatFolder, @@ -128,6 +136,10 @@ const SettingsFoldersEdit: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onBack, onScreenSelect, state.mode === 'edit' + ? SettingsScreens.FoldersEditFolder + : SettingsScreens.FoldersCreateFolder); + function handleChange(event: React.ChangeEvent) { const { currentTarget } = event; dispatch({ type: 'setTitle', payload: currentTarget.value.trim() }); diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 6bb3bcc59..42ffa9db8 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -5,7 +5,7 @@ import { withGlobal } from '../../../../lib/teact/teactn'; import { GlobalActions } from '../../../../global/types'; import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types'; -import { NotifyException, NotifySettings } from '../../../../types'; +import { NotifyException, NotifySettings, SettingsScreens } from '../../../../types'; import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; import { pick } from '../../../../util/iteratees'; @@ -14,6 +14,7 @@ import { throttle } from '../../../../util/schedulers'; import getAnimationData from '../../../common/helpers/animatedAssets'; import { getFolderDescriptionText } from '../../../../modules/helpers'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import ListItem from '../../../ui/ListItem'; import Button from '../../../ui/Button'; @@ -23,6 +24,9 @@ import AnimatedSticker from '../../../common/AnimatedSticker'; type OwnProps = { onCreateFolder: () => void; onEditFolder: (folder: ApiChatFolder) => void; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { @@ -44,6 +48,9 @@ const MAX_ALLOWED_FOLDERS = 10; const SettingsFoldersMain: FC = ({ onCreateFolder, onEditFolder, + isActive, + onScreenSelect, + onReset, chatsById, usersById, orderedFolderIds, @@ -90,6 +97,8 @@ const SettingsFoldersMain: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Folders); + const userFolders = useMemo(() => { if (!orderedFolderIds) { return undefined; diff --git a/src/components/left/settings/twoFa/SettingsTwoFa.tsx b/src/components/left/settings/twoFa/SettingsTwoFa.tsx index b94f3c0ff..b872e89c1 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFa.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFa.tsx @@ -20,8 +20,11 @@ import SettingsTwoFaEmailCode from './SettingsTwoFaEmailCode'; export type OwnProps = { state: TwoFaState; currentScreen: SettingsScreens; + shownScreen: SettingsScreens; dispatch: TwoFaDispatch; + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = GlobalState['twoFaSettings']; @@ -33,13 +36,16 @@ type DispatchProps = Pick = ({ currentScreen, + shownScreen, state, hint, isLoading, error, waitingEmailCodeLength, dispatch, + isActive, onScreenSelect, + onReset, updatePassword, checkPassword, clearTwoFaError, @@ -158,25 +164,54 @@ const SettingsTwoFa: FC = ({ return ( ); case SettingsScreens.TwoFaNewPassword: return ( ); case SettingsScreens.TwoFaNewPasswordConfirm: return ( ); @@ -186,6 +221,14 @@ const SettingsTwoFa: FC = ({ icon="hint" placeholder={lang('PasswordHintPlaceholder')} onSubmit={handleNewPasswordHint} + screen={currentScreen} + onScreenSelect={onScreenSelect} + isActive={isActive || [ + SettingsScreens.TwoFaNewPasswordEmail, + SettingsScreens.TwoFaNewPasswordEmailCode, + SettingsScreens.TwoFaCongratulations, + ].includes(shownScreen)} + onReset={onReset} /> ); @@ -200,6 +243,13 @@ const SettingsTwoFa: FC = ({ placeholder={lang('RecoveryEmailTitle')} shouldConfirm onSubmit={handleNewPasswordEmail} + screen={currentScreen} + onScreenSelect={onScreenSelect} + isActive={isActive || [ + SettingsScreens.TwoFaNewPasswordEmailCode, + SettingsScreens.TwoFaCongratulations, + ].includes(shownScreen)} + onReset={onReset} /> ); @@ -210,6 +260,10 @@ const SettingsTwoFa: FC = ({ error={error} clearError={clearTwoFaError} onSubmit={handleEmailCode} + screen={currentScreen} + onScreenSelect={onScreenSelect} + isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations} + onReset={onReset} /> ); @@ -217,6 +271,8 @@ const SettingsTwoFa: FC = ({ return ( ); @@ -224,34 +280,70 @@ const SettingsTwoFa: FC = ({ return ( ); case SettingsScreens.TwoFaChangePasswordCurrent: return ( ); case SettingsScreens.TwoFaChangePasswordNew: return ( ); case SettingsScreens.TwoFaChangePasswordConfirm: return ( ); @@ -264,6 +356,10 @@ const SettingsTwoFa: FC = ({ icon="hint" placeholder={lang('PasswordHintPlaceholder')} onSubmit={handleChangePasswordHint} + onScreenSelect={onScreenSelect} + isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations} + onReset={onReset} + screen={currentScreen} /> ); @@ -275,37 +371,60 @@ const SettingsTwoFa: FC = ({ clearError={clearTwoFaError} hint={hint} onSubmit={handleTurnOff} + onScreenSelect={onScreenSelect} + isActive={isActive} + onReset={onReset} + screen={currentScreen} /> ); case SettingsScreens.TwoFaRecoveryEmailCurrentPassword: return ( ); case SettingsScreens.TwoFaRecoveryEmail: return ( ); case SettingsScreens.TwoFaRecoveryEmailCode: return ( ); diff --git a/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx b/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx index fc03c8fad..6e61344e4 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx @@ -6,25 +6,32 @@ import { SettingsScreens } from '../../../../types'; import { selectAnimatedEmoji } from '../../../../modules/selectors'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import Button from '../../../ui/Button'; import AnimatedEmoji from '../../../common/AnimatedEmoji'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { animatedEmoji: ApiSticker; }; -const SettingsTwoFaCongratulations: FC = ({ animatedEmoji, onScreenSelect }) => { +const SettingsTwoFaCongratulations: FC = ({ + isActive, onReset, animatedEmoji, onScreenSelect, +}) => { const lang = useLang(); const handleClick = () => { onScreenSelect(SettingsScreens.Privacy); }; + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaCongratulations); + return (
diff --git a/src/components/left/settings/twoFa/SettingsTwoFaEmailCode.tsx b/src/components/left/settings/twoFa/SettingsTwoFaEmailCode.tsx index 1858a621e..a1832759b 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaEmailCode.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaEmailCode.tsx @@ -4,10 +4,12 @@ import React, { import { withGlobal } from '../../../../lib/teact/teactn'; import { ApiSticker } from '../../../../api/types'; +import { SettingsScreens } from '../../../../types'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment'; import { selectAnimatedEmoji } from '../../../../modules/selectors'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import AnimatedEmoji from '../../../common/AnimatedEmoji'; import InputText from '../../../ui/InputText'; @@ -18,6 +20,10 @@ type OwnProps = { error?: string; clearError: NoneToVoidFunction; onSubmit: (hint: string) => void; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; + screen: SettingsScreens; }; type StateProps = { @@ -34,6 +40,10 @@ const SettingsTwoFaEmailCode: FC = ({ error, clearError, onSubmit, + isActive, + onScreenSelect, + onReset, + screen, }) => { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -50,6 +60,8 @@ const SettingsTwoFaEmailCode: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, screen); + const handleInputChange = (e: React.ChangeEvent) => { if (error && clearError) { clearError(); diff --git a/src/components/left/settings/twoFa/SettingsTwoFaEnabled.tsx b/src/components/left/settings/twoFa/SettingsTwoFaEnabled.tsx index e176ade45..44d401d79 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaEnabled.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaEnabled.tsx @@ -6,22 +6,29 @@ import { SettingsScreens } from '../../../../types'; import { selectAnimatedEmoji } from '../../../../modules/selectors'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import ListItem from '../../../ui/ListItem'; import AnimatedEmoji from '../../../common/AnimatedEmoji'; import renderText from '../../../common/helpers/renderText'; type OwnProps = { + isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { animatedEmoji: ApiSticker; }; -const SettingsTwoFaEnabled: FC = ({ animatedEmoji, onScreenSelect }) => { +const SettingsTwoFaEnabled: FC = ({ + isActive, onReset, animatedEmoji, onScreenSelect, +}) => { const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaEnabled); + return (
diff --git a/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx b/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx index 398edac73..8a1cf06d6 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx @@ -2,12 +2,16 @@ import React, { FC, memo, useCallback, useState, } from '../../../../lib/teact/teact'; +import { SettingsScreens } from '../../../../types'; + import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import PasswordMonkey from '../../../common/PasswordMonkey'; import PasswordForm from '../../../common/PasswordForm'; type OwnProps = { + screen: SettingsScreens; error?: string; isLoading?: boolean; expectedPassword?: string; @@ -16,11 +20,18 @@ type OwnProps = { submitLabel?: string; 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 = ({ + screen, + isActive, + onScreenSelect, + onReset, error, isLoading, expectedPassword, @@ -50,6 +61,8 @@ const SettingsTwoFaPassword: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, screen); + return (
diff --git a/src/components/left/settings/twoFa/SettingsTwoFaSkippableForm.tsx b/src/components/left/settings/twoFa/SettingsTwoFaSkippableForm.tsx index b77ada225..a877770de 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaSkippableForm.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaSkippableForm.tsx @@ -4,11 +4,13 @@ import React, { import { withGlobal } from '../../../../lib/teact/teactn'; import { ApiSticker } from '../../../../api/types'; +import { SettingsScreens } from '../../../../types'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../../util/environment'; import { selectAnimatedEmoji } from '../../../../modules/selectors'; import useFlag from '../../../../hooks/useFlag'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import Button from '../../../ui/Button'; import Modal from '../../../ui/Modal'; @@ -25,6 +27,10 @@ type OwnProps = { shouldConfirm?: boolean; clearError?: NoneToVoidFunction; onSubmit: (value?: string) => void; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; + screen: SettingsScreens; }; type StateProps = { @@ -42,6 +48,10 @@ const SettingsTwoFaSkippableForm: FC = ({ shouldConfirm, clearError, onSubmit, + isActive, + onScreenSelect, + onReset, + screen, }) => { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -86,6 +96,8 @@ const SettingsTwoFaSkippableForm: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, screen); + return (
diff --git a/src/components/left/settings/twoFa/SettingsTwoFaStart.tsx b/src/components/left/settings/twoFa/SettingsTwoFaStart.tsx index f2a8112d6..ce2b4acde 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaStart.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaStart.tsx @@ -2,24 +2,33 @@ import React, { FC, memo } from '../../../../lib/teact/teact'; import { withGlobal } from '../../../../lib/teact/teactn'; import { ApiSticker } from '../../../../api/types'; +import { SettingsScreens } from '../../../../types'; import { selectAnimatedEmoji } from '../../../../modules/selectors'; import useLang from '../../../../hooks/useLang'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import Button from '../../../ui/Button'; import AnimatedEmoji from '../../../common/AnimatedEmoji'; type OwnProps = { onStart: NoneToVoidFunction; + isActive?: boolean; + onScreenSelect: (screen: SettingsScreens) => void; + onReset: () => void; }; type StateProps = { animatedEmoji: ApiSticker; }; -const SettingsTwoFaStart: FC = ({ animatedEmoji, onStart }) => { +const SettingsTwoFaStart: FC = ({ + isActive, onScreenSelect, onReset, animatedEmoji, onStart, +}) => { const lang = useLang(); + useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaDisabled); + return (
diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index f9800fdb1..e3e66212c 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -87,6 +87,14 @@ overflow: hidden; } } + + #Main.history-animation-disabled & { + transition: none; + + &:after { + transition: none; + } + } } @media (max-width: 600px) { @@ -104,6 +112,18 @@ @media (max-width: 600px) { height: calc(var(--vh, 1vh) * 100 + 1px); } + + #Main.history-animation-disabled & { + transition: none; + + .overlay-backdrop { + transition: none; + } + } +} + +#Main.history-animation-disabled .overlay-backdrop { + transition: none; } #MiddleColumn { @@ -155,6 +175,14 @@ transform: translate3d(-20vw, 0, 0); } } + + #Main.history-animation-disabled & { + transition: none; + + &:after { + transition: none; + } + } } .SymbolMenu { diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 3b98a1aa9..262ec6ebd 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -49,6 +49,7 @@ type StateProps = { audioMessage?: ApiMessage; safeLinkModalUrl?: string; isHistoryCalendarOpen: boolean; + shouldSkipHistoryAnimations?: boolean; }; type DispatchProps = Pick = ({ audioMessage, safeLinkModalUrl, isHistoryCalendarOpen, + shouldSkipHistoryAnimations, loadAnimatedEmojis, loadNotificationSettings, loadNotificationExceptions, @@ -98,15 +100,17 @@ const Main: FC = ({ const { transitionClassNames: middleColumnTransitionClassNames, - } = useShowTransition(!isLeftColumnShown, undefined, true); + } = useShowTransition(!isLeftColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations); const { transitionClassNames: rightColumnTransitionClassNames, - } = useShowTransition(isRightColumnShown, undefined, true); + } = useShowTransition(isRightColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations); + const className = buildClassName( middleColumnTransitionClassNames.replace(/([\w-]+)/g, 'middle-column-$1'), rightColumnTransitionClassNames.replace(/([\w-]+)/g, 'right-column-$1'), + shouldSkipHistoryAnimations && 'history-animation-disabled', ); useEffect(() => { @@ -230,6 +234,7 @@ export default memo(withGlobal( audioMessage, safeLinkModalUrl: global.safeLinkModalUrl, isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt), + shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 2e690ca05..92b806862 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -58,6 +58,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import { renderMessageText } from '../common/helpers/renderMessageText'; import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Spinner from '../ui/Spinner'; import ShowTransition from '../ui/ShowTransition'; @@ -443,6 +444,14 @@ const MediaViewer: FC = ({ const lang = useLang(); + useHistoryBack(isOpen, closeMediaViewer, openMediaViewer, { + chatId, + threadId, + messageId, + origin, + avatarOwnerId: avatarOwner && avatarOwner.id, + }); + function renderSlide(isActive: boolean) { if (isAvatar) { return ( diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index f03783dbd..7cfe0197b 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -35,6 +35,7 @@ import { selectIsRightColumnShown, selectPinnedIds, selectTheme, + selectThreadOriginChat, } from '../../modules/selectors'; import { getCanPostInChat, getMessageSendingRestrictionReason, isChatPrivate } from '../../modules/helpers'; import captureEscKeyListener from '../../util/captureEscKeyListener'; @@ -45,6 +46,7 @@ import useWindowSize from '../../hooks/useWindowSize'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Transition from '../ui/Transition'; import MiddleHeader from './MiddleHeader'; @@ -64,6 +66,7 @@ type StateProps = { messageListType?: MessageListType; isPrivate?: boolean; isPinnedMessageList?: boolean; + isScheduledMessageList?: boolean; canPost?: boolean; messageSendingRestrictionReason?: string; hasPinnedOrAudioMessage?: boolean; @@ -78,9 +81,12 @@ type StateProps = { isMobileSearchActive?: boolean; isSelectModeActive?: boolean; animationLevel?: number; + originChatId?: number; + shouldSkipHistoryAnimations?: boolean; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined; @@ -94,6 +100,7 @@ const MiddleColumn: FC = ({ messageListType, isPrivate, isPinnedMessageList, + isScheduledMessageList, canPost, messageSendingRestrictionReason, hasPinnedOrAudioMessage, @@ -108,9 +115,13 @@ const MiddleColumn: FC = ({ isMobileSearchActive, isSelectModeActive, animationLevel, + originChatId, + shouldSkipHistoryAnimations, openChat, unpinAllMessages, loadUser, + closeLocalTextSearch, + exitMessageSelectMode, }) => { const { width: windowWidth } = useWindowSize(); @@ -223,6 +234,31 @@ const MiddleColumn: FC = ({ renderingCanPost && isNotchShown && !isSelectModeActive && 'with-notch', ); + const closeChat = () => { + if (renderingThreadId !== MAIN_THREAD_ID) { + openChat({ id: originChatId, threadId: MAIN_THREAD_ID }, true); + } else if (isPinnedMessageList || isScheduledMessageList) { + openChat({ id: chatId, type: 'thread' }); + } else { + openChat({ id: undefined }, true); + } + }; + + useHistoryBack(renderingChatId && renderingThreadId, closeChat, openChat, { + id: chatId, + threadId: MAIN_THREAD_ID, + }); + + const isDiscussion = renderingChatId && renderingThreadId !== MAIN_THREAD_ID; + + useHistoryBack(isDiscussion || isPinnedMessageList || isScheduledMessageList, closeChat, openChat, { + id: chatId, + threadId: renderingThreadId, + }); + + useHistoryBack(isMobileSearchActive, closeLocalTextSearch); + useHistoryBack(isSelectModeActive, exitMessageSelectMode); + return (
= ({ messageListType={renderingMessageListType} /> @@ -371,15 +407,19 @@ export default memo(withGlobal( const canPost = chat && getCanPostInChat(chat, threadId); const isBotNotStarted = selectIsChatBotNotStarted(global, chatId); const isPinnedMessageList = messageListType === 'pinned'; + const isScheduledMessageList = messageListType === 'scheduled'; + const originChat = selectThreadOriginChat(global, chatId, threadId); return { ...state, chatId, threadId, messageListType, + originChatId: originChat ? originChat.id : chatId, isPrivate: isChatPrivate(chatId), canPost: !isPinnedMessageList && (!chat || canPost) && (!isBotNotStarted || IS_SINGLE_COLUMN_LAYOUT), isPinnedMessageList, + isScheduledMessageList, messageSendingRestrictionReason: chat && getMessageSendingRestrictionReason(chat), hasPinnedOrAudioMessage: ( threadId !== MAIN_THREAD_ID @@ -387,9 +427,10 @@ export default memo(withGlobal( || Boolean(audioChatId && audioMessageId) ), pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0, + shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ - 'openChat', 'unpinAllMessages', 'loadUser', + 'openChat', 'unpinAllMessages', 'loadUser', 'closeLocalTextSearch', 'exitMessageSelectMode', ]), )(MiddleColumn)); diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 04535aae3..c1c3485c6 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -94,6 +94,7 @@ type StateProps = { lastSyncTime?: number; notifySettings: NotifySettings; notifyExceptions?: Record; + shouldSkipHistoryAnimations?: boolean; }; type DispatchProps = Pick = ({ lastSyncTime, notifySettings, notifyExceptions, + shouldSkipHistoryAnimations, openChatWithInfo, pinMessage, focusMessage, @@ -384,7 +386,10 @@ const MiddleHeader: FC = ({ return (
- + {renderInfo} @@ -421,7 +426,7 @@ const MiddleHeader: FC = ({ export default memo(withGlobal( (global, { chatId, threadId, messageListType }): StateProps => { - const { isLeftColumnShown, lastSyncTime } = global; + const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global; const { byId: chatsById } = global.chats; const chat = selectChat(global, chatId); @@ -463,6 +468,7 @@ export default memo(withGlobal( lastSyncTime, notifySettings: selectNotifySettings(global), notifyExceptions: selectNotifyExceptions(global), + shouldSkipHistoryAnimations, }; const messagesById = selectChatMessages(global, chatId); diff --git a/src/components/right/GifSearch.tsx b/src/components/right/GifSearch.tsx index 562cf461d..8535e7b40 100644 --- a/src/components/right/GifSearch.tsx +++ b/src/components/right/GifSearch.tsx @@ -18,6 +18,7 @@ import { pick } from '../../util/iteratees'; import buildClassName from '../../util/buildClassName'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import InfiniteScroll from '../ui/InfiniteScroll'; import GifButton from '../common/GifButton'; @@ -25,6 +26,11 @@ import Loading from '../ui/Loading'; import './GifSearch.scss'; +type OwnProps = { + onClose: NoneToVoidFunction; + isActive: boolean; +}; + type StateProps = { query?: string; results?: ApiVideo[]; @@ -37,7 +43,9 @@ type DispatchProps = Pick = ({ +const GifSearch: FC = ({ + onClose, + isActive, query, results, chat, @@ -67,6 +75,8 @@ const GifSearch: FC = ({ const lang = useLang(); + useHistoryBack(isActive, onClose); + function renderContent() { if (query === undefined) { return undefined; diff --git a/src/components/right/PollResults.tsx b/src/components/right/PollResults.tsx index 44d62d208..fb015a562 100644 --- a/src/components/right/PollResults.tsx +++ b/src/components/right/PollResults.tsx @@ -6,24 +6,34 @@ import { selectChat, selectChatMessage } from '../../modules/selectors'; import { buildCollectionByKey } from '../../util/iteratees'; import { getMessagePoll } from '../../modules/helpers'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import PollAnswerResults from './PollAnswerResults'; import Loading from '../ui/Loading'; import './PollResults.scss'; +type OwnProps = { + onClose: NoneToVoidFunction; + isActive: boolean; +}; + type StateProps = { chat?: ApiChat; message?: ApiMessage; lastSyncTime?: number; }; -const PollResults: FC = ({ +const PollResults: FC = ({ + onClose, + isActive, chat, message, lastSyncTime, }) => { const lang = useLang(); + useHistoryBack(isActive, onClose); + if (!message || !chat) { return ; } diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index f64103635..daceb47db 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -17,6 +17,7 @@ import { import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import useWindowSize from '../../hooks/useWindowSize'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; +import useHistoryBack from '../../hooks/useHistoryBack'; import RightHeader from './RightHeader'; import Profile from './Profile'; @@ -35,6 +36,7 @@ type StateProps = { threadId?: number; currentProfileUserId?: number; isChatSelected: boolean; + shouldSkipHistoryAnimations?: boolean; }; type DispatchProps = Pick = ({ setStickerSearchQuery, setGifSearchQuery, closePollResults, + shouldSkipHistoryAnimations, }) => { const { width: windowWidth } = useWindowSize(); const [profileState, setProfileState] = useState(ProfileState.Profile); @@ -88,21 +91,21 @@ const RightColumn: FC = ({ const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1; - const close = useCallback(() => { + const close = useCallback((shouldScrollUp = true) => { switch (contentKey) { case RightColumnContent.ChatInfo: - if (isScrolledDown) { + if (isScrolledDown && shouldScrollUp) { setProfileState(ProfileState.Profile); break; } - toggleChatInfo(); + toggleChatInfo(undefined, true); break; case RightColumnContent.UserInfo: - if (isScrolledDown) { + if (isScrolledDown && shouldScrollUp) { setProfileState(ProfileState.Profile); break; } - openUserInfo({ id: undefined }); + openUserInfo({ id: undefined }, true); break; case RightColumnContent.Management: { switch (managementScreen) { @@ -139,9 +142,11 @@ const RightColumn: FC = ({ break; } case RightColumnContent.StickerSearch: - case RightColumnContent.GifSearch: { blurSearchInput(); setStickerSearchQuery({ query: undefined }); + break; + case RightColumnContent.GifSearch: { + blurSearchInput(); setGifSearchQuery({ query: undefined }); break; } @@ -187,8 +192,13 @@ const RightColumn: FC = ({ } }, [contentKey, chatId]); + + useHistoryBack(isChatSelected && (contentKey === RightColumnContent.ChatInfo + || contentKey === RightColumnContent.UserInfo || contentKey === RightColumnContent.Management), + () => close(false), toggleChatInfo); + // eslint-disable-next-line consistent-return - function renderContent() { + function renderContent(isActive: boolean) { if (renderingContentKey === -1) { return undefined; } @@ -206,7 +216,7 @@ const RightColumn: FC = ({ /> ); case RightColumnContent.Search: - return ; + return ; case RightColumnContent.Management: return ( = ({ selectedChatMemberId={selectedChatMemberId} onScreenSelect={setManagementScreen} onChatMemberSelect={handleSelectChatMember} + isActive={isOpen && isActive} + onClose={close} /> ); + case RightColumnContent.StickerSearch: - return ; + return ; case RightColumnContent.GifSearch: - return ; + return ; case RightColumnContent.PollResults: - return ; + return ; } } @@ -248,9 +261,10 @@ const RightColumn: FC = ({ profileState={profileState} managementScreen={managementScreen} onClose={close} + shouldSkipAnimation={shouldSkipTransition || shouldSkipHistoryAnimations} /> pick(actions, [ diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index e919f6979..4cfa3b1d6 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -36,6 +36,7 @@ type OwnProps = { isStickerSearch?: boolean; isGifSearch?: boolean; isPollResults?: boolean; + shouldSkipAnimation?: boolean; profileState?: ProfileState; managementScreen?: ManagementScreens; onClose: () => void; @@ -102,6 +103,7 @@ const RightHeader: FC = ({ searchTextMessagesLocal, toggleManagement, openHistoryCalendar, + shouldSkipAnimation, }) => { // eslint-disable-next-line no-null/no-null const backButtonRef = useRef(null); @@ -278,7 +280,7 @@ const RightHeader: FC = ({ const buttonClassName = buildClassName( 'animated-close-icon', - shouldSkipTransition && 'no-transition', + (shouldSkipTransition || shouldSkipAnimation) && 'no-transition', ); // Add class in the next AF to synchronize with animation with Transition components @@ -299,7 +301,7 @@ const RightHeader: FC = ({
{renderHeaderContent} diff --git a/src/components/right/RightSearch.tsx b/src/components/right/RightSearch.tsx index c86f4859e..7d176216f 100644 --- a/src/components/right/RightSearch.tsx +++ b/src/components/right/RightSearch.tsx @@ -23,6 +23,7 @@ import useLang from '../../hooks/useLang'; import { orderBy, pick } from '../../util/iteratees'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation'; +import useHistoryBack from '../../hooks/useHistoryBack'; import InfiniteScroll from '../ui/InfiniteScroll'; import ListItem from '../ui/ListItem'; @@ -34,6 +35,8 @@ import './RightSearch.scss'; export type OwnProps = { chatId: number; threadId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -56,6 +59,8 @@ interface Result { const RightSearch: FC = ({ chatId, threadId, + onClose, + isActive, chat, messagesById, query, @@ -125,6 +130,8 @@ const RightSearch: FC = ({ ); }; + useHistoryBack(isActive, onClose); + // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => { diff --git a/src/components/right/StickerSearch.tsx b/src/components/right/StickerSearch.tsx index f2d773cac..853c2cd2f 100644 --- a/src/components/right/StickerSearch.tsx +++ b/src/components/right/StickerSearch.tsx @@ -10,12 +10,18 @@ import { throttle } from '../../util/schedulers'; import { selectCurrentStickerSearch } from '../../modules/selectors'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Loading from '../ui/Loading'; import StickerSetResult from './StickerSetResult'; import './StickerSearch.scss'; +type OwnProps = { + onClose: NoneToVoidFunction; + isActive: boolean; +}; + type StateProps = { query?: string; featuredIds?: string[]; @@ -28,7 +34,9 @@ const INTERSECTION_THROTTLE = 200; const runThrottled = throttle((cb) => cb(), 60000, true); -const StickerSearch: FC = ({ +const StickerSearch: FC = ({ + onClose, + isActive, query, featuredIds, resultIds, @@ -53,6 +61,8 @@ const StickerSearch: FC = ({ }); }); + useHistoryBack(isActive, onClose); + function renderContent() { if (query === undefined) { return undefined; diff --git a/src/components/right/management/ManageChannel.tsx b/src/components/right/management/ManageChannel.tsx index 79d6b3597..69c30f8ef 100644 --- a/src/components/right/management/ManageChannel.tsx +++ b/src/components/right/management/ManageChannel.tsx @@ -22,12 +22,15 @@ import Spinner from '../../ui/Spinner'; import FloatingActionButton from '../../ui/FloatingActionButton'; import ConfirmDialog from '../../ui/ConfirmDialog'; import useFlag from '../../../hooks/useFlag'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import './Management.scss'; type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -56,6 +59,8 @@ const ManageChannel: FC = ({ leaveChannel, deleteChannel, openChat, + onClose, + isActive, }) => { const currentTitle = chat ? (chat.title || '') : ''; const currentAbout = chat && chat.fullInfo ? (chat.fullInfo.about || '') : ''; @@ -71,6 +76,8 @@ const ManageChannel: FC = ({ const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const lang = useLang(); + useHistoryBack(isActive, onClose); + useEffect(() => { if (progress === ManagementProgress.Complete) { setIsProfileFieldsTouched(false); diff --git a/src/components/right/management/ManageChatAdministrators.tsx b/src/components/right/management/ManageChatAdministrators.tsx index 02c63513c..a4b1f0b2a 100644 --- a/src/components/right/management/ManageChatAdministrators.tsx +++ b/src/components/right/management/ManageChatAdministrators.tsx @@ -9,6 +9,7 @@ import { getUserFullName, isChatChannel } from '../../../modules/helpers'; import { selectChat } from '../../../modules/selectors'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import PrivateChatInfo from '../../common/PrivateChatInfo'; @@ -17,6 +18,8 @@ type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; onChatMemberSelect: (memberId: number, isPromotedByCurrentUser?: boolean) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -33,9 +36,13 @@ const ManageChatAdministrators: FC = ({ usersById, onScreenSelect, onChatMemberSelect, + onClose, + isActive, }) => { const lang = useLang(); + useHistoryBack(isActive, onClose); + function handleRecentActionsClick() { onScreenSelect(ManagementScreens.GroupRecentActions); } diff --git a/src/components/right/management/ManageChatPrivacyType.tsx b/src/components/right/management/ManageChatPrivacyType.tsx index 2ef8ea653..5f760d1c1 100644 --- a/src/components/right/management/ManageChatPrivacyType.tsx +++ b/src/components/right/management/ManageChatPrivacyType.tsx @@ -12,6 +12,7 @@ import { pick } from '../../../util/iteratees'; import { isChatChannel } from '../../../modules/helpers'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import SafeLink from '../../common/SafeLink'; import ListItem from '../../ui/ListItem'; @@ -26,6 +27,8 @@ type PrivacyType = 'private' | 'public'; type OwnProps = { chatId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -41,6 +44,8 @@ type DispatchProps = Pick = ({ chat, + onClose, + isActive, isChannel, progress, isUsernameAvailable, @@ -60,6 +65,8 @@ const ManageChatPrivacyType: FC = ({ || (privacyType === 'private' && isPublic) ); + useHistoryBack(isActive, onClose); + useEffect(() => { if (privacyType && !privateLink) { updatePrivateLink(); diff --git a/src/components/right/management/ManageDiscussion.tsx b/src/components/right/management/ManageDiscussion.tsx index 110ba0379..997066b42 100644 --- a/src/components/right/management/ManageDiscussion.tsx +++ b/src/components/right/management/ManageDiscussion.tsx @@ -12,6 +12,7 @@ import { selectChat } from '../../../modules/selectors'; import { pick } from '../../../util/iteratees'; import getAnimationData from '../../common/helpers/animatedAssets'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import NothingFound from '../../common/NothingFound'; @@ -26,6 +27,8 @@ import { isChatChannel } from '../../../modules/helpers'; type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -40,6 +43,8 @@ type DispatchProps = Pick = ({ chat, + onClose, + isActive, chatId, chatsByIds, linkedChat, @@ -59,6 +64,8 @@ const ManageDiscussion: FC = ({ const lang = useLang(); const linkedChatId = linkedChat && linkedChat.id; + useHistoryBack(isActive, onClose); + useEffect(() => { loadGroupsForDiscussion(); }, [loadGroupsForDiscussion]); diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index 78ace1e3a..049f3b63c 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -16,6 +16,7 @@ import { selectChat } from '../../../modules/selectors'; import { formatInteger } from '../../../util/textFormat'; import { pick } from '../../../util/iteratees'; import renderText from '../../common/helpers/renderText'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import AvatarEditable from '../../ui/AvatarEditable'; import InputText from '../../ui/InputText'; @@ -30,6 +31,8 @@ import './Management.scss'; type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -68,6 +71,8 @@ const ManageGroup: FC = ({ deleteChannel, closeManagement, openChat, + onClose, + isActive, }) => { const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); const currentTitle = chat.title; @@ -82,6 +87,8 @@ const ManageGroup: FC = ({ const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const lang = useLang(); + useHistoryBack(isActive, onClose); + useEffect(() => { if (progress === ManagementProgress.Complete) { setIsProfileFieldsTouched(false); diff --git a/src/components/right/management/ManageGroupAdminRights.tsx b/src/components/right/management/ManageGroupAdminRights.tsx index 48acdb0fa..cb2bbfced 100644 --- a/src/components/right/management/ManageGroupAdminRights.tsx +++ b/src/components/right/management/ManageGroupAdminRights.tsx @@ -12,6 +12,7 @@ import { selectChat } from '../../../modules/selectors'; import { getUserFullName, isChatBasicGroup, isChatChannel } from '../../../modules/helpers'; import useLang from '../../../hooks/useLang'; import useFlag from '../../../hooks/useFlag'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import ListItem from '../../ui/ListItem'; @@ -26,6 +27,8 @@ type OwnProps = { selectedChatMemberId?: number; isPromotedByCurrentUser?: boolean; onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -49,6 +52,8 @@ const ManageGroupAdminRights: FC = ({ isChannel, isFormFullyDisabled, updateChatAdmin, + onClose, + isActive, }) => { const [permissions, setPermissions] = useState({}); const [isTouched, setIsTouched] = useState(false); @@ -57,6 +62,8 @@ const ManageGroupAdminRights: FC = ({ const [customTitle, setCustomTitle] = useState(''); const lang = useLang(); + useHistoryBack(isActive, onClose); + const selectedChatMember = useMemo(() => { if (!chat.fullInfo || !chat.fullInfo.adminMembers) { return undefined; diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index 630522914..3337f4135 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -8,6 +8,7 @@ import { GlobalActions } from '../../../global/types'; import { selectChat } from '../../../modules/selectors'; import { sortUserIds, isChatChannel } from '../../../modules/helpers'; import { pick } from '../../../util/iteratees'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import NothingFound from '../../common/NothingFound'; @@ -15,6 +16,8 @@ import ListItem from '../../ui/ListItem'; type OwnProps = { chatId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -31,6 +34,8 @@ const ManageGroupMembers: FC = ({ usersById, isChannel, openUserInfo, + onClose, + isActive, serverTimeOffset, }) => { const memberIds = useMemo(() => { @@ -45,6 +50,8 @@ const ManageGroupMembers: FC = ({ openUserInfo({ id }); }, [openUserInfo]); + useHistoryBack(isActive, onClose); + return (
diff --git a/src/components/right/management/ManageGroupPermissions.tsx b/src/components/right/management/ManageGroupPermissions.tsx index 0653c8ad9..c3563277f 100644 --- a/src/components/right/management/ManageGroupPermissions.tsx +++ b/src/components/right/management/ManageGroupPermissions.tsx @@ -10,6 +10,7 @@ import { GlobalActions } from '../../../global/types'; import useLang from '../../../hooks/useLang'; import { selectChat } from '../../../modules/selectors'; import { pick } from '../../../util/iteratees'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import Checkbox from '../../ui/Checkbox'; @@ -21,6 +22,8 @@ type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; onChatMemberSelect: (memberId: number, isPromotedByCurrentUser?: boolean) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -61,12 +64,16 @@ const ManageGroupPermissions: FC = ({ chat, currentUserId, updateChatDefaultBannedRights, + onClose, + isActive, }) => { const [permissions, setPermissions] = useState({}); const [havePermissionChanged, setHavePermissionChanged] = useState(false); const [isLoading, setIsLoading] = useState(false); const lang = useLang(); + useHistoryBack(isActive, onClose); + const handleRemovedUsersClick = useCallback(() => { onScreenSelect(ManagementScreens.GroupRemovedUsers); }, [onScreenSelect]); diff --git a/src/components/right/management/ManageGroupRecentActions.tsx b/src/components/right/management/ManageGroupRecentActions.tsx index c29ad6976..e97ef3dea 100644 --- a/src/components/right/management/ManageGroupRecentActions.tsx +++ b/src/components/right/management/ManageGroupRecentActions.tsx @@ -6,6 +6,7 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { ApiChat, ApiChatMember } from '../../../api/types'; import useLang from '../../../hooks/useLang'; import { selectChat } from '../../../modules/selectors'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import ListItem from '../../ui/ListItem'; import Checkbox from '../../ui/Checkbox'; @@ -13,15 +14,19 @@ import PrivateChatInfo from '../../common/PrivateChatInfo'; type OwnProps = { chatId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { chat?: ApiChat; }; -const ManageGroupRecentActions: FC = ({ chat }) => { +const ManageGroupRecentActions: FC = ({ chat, onClose, isActive }) => { const lang = useLang(); + useHistoryBack(isActive, onClose); + const adminMembers = useMemo(() => { if (!chat || !chat.fullInfo || !chat.fullInfo.adminMembers) { return []; diff --git a/src/components/right/management/ManageGroupRemovedUsers.tsx b/src/components/right/management/ManageGroupRemovedUsers.tsx index f65c15392..68ed0249d 100644 --- a/src/components/right/management/ManageGroupRemovedUsers.tsx +++ b/src/components/right/management/ManageGroupRemovedUsers.tsx @@ -10,12 +10,15 @@ import { selectChat } from '../../../modules/selectors'; import { getUserFullName } from '../../../modules/helpers'; import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import ListItem from '../../ui/ListItem'; type OwnProps = { chatId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -29,9 +32,13 @@ const ManageGroupRemovedUsers: FC = ({ chat, usersById, updateChatMemberBannedRights, + onClose, + isActive, }) => { const lang = useLang(); + useHistoryBack(isActive, onClose); + const removedMembers = useMemo(() => { if (!chat || !chat.fullInfo || !chat.fullInfo.kickedMembers) { return []; diff --git a/src/components/right/management/ManageGroupUserPermissions.tsx b/src/components/right/management/ManageGroupUserPermissions.tsx index 5f363b1ee..721434765 100644 --- a/src/components/right/management/ManageGroupUserPermissions.tsx +++ b/src/components/right/management/ManageGroupUserPermissions.tsx @@ -11,6 +11,7 @@ import { pick } from '../../../util/iteratees'; import { selectChat } from '../../../modules/selectors'; import useLang from '../../../hooks/useLang'; import useFlag from '../../../hooks/useFlag'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import ListItem from '../../ui/ListItem'; @@ -24,6 +25,8 @@ type OwnProps = { selectedChatMemberId?: number; isPromotedByCurrentUser?: boolean; onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -39,6 +42,8 @@ const ManageGroupUserPermissions: FC = ({ onScreenSelect, updateChatMemberBannedRights, isFormFullyDisabled, + onClose, + isActive, }) => { const [permissions, setPermissions] = useState({}); const [havePermissionChanged, setHavePermissionChanged] = useState(false); @@ -46,6 +51,8 @@ const ManageGroupUserPermissions: FC = ({ const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag(); const lang = useLang(); + useHistoryBack(isActive, onClose); + const selectedChatMember = useMemo(() => { if (!chat || !chat.fullInfo || !chat.fullInfo.members) { return undefined; diff --git a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx index 3e0c9bd32..064e6dba3 100644 --- a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx +++ b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx @@ -8,6 +8,7 @@ import { ManagementScreens } from '../../../types'; import { selectChat } from '../../../modules/selectors'; import { sortUserIds, isChatChannel } from '../../../modules/helpers'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import ListItem from '../../ui/ListItem'; @@ -17,6 +18,8 @@ type OwnProps = { chatId: number; onScreenSelect: (screen: ManagementScreens) => void; onChatMemberSelect: (memberId: number) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -32,8 +35,12 @@ const ManageGroupUserPermissionsCreate: FC = ({ isChannel, onScreenSelect, onChatMemberSelect, + onClose, + isActive, serverTimeOffset, }) => { + useHistoryBack(isActive, onClose); + const memberIds = useMemo(() => { if (!members || !usersById) { return undefined; diff --git a/src/components/right/management/ManageUser.tsx b/src/components/right/management/ManageUser.tsx index 8397d6c57..7958839dd 100644 --- a/src/components/right/management/ManageUser.tsx +++ b/src/components/right/management/ManageUser.tsx @@ -15,6 +15,7 @@ import { import { selectIsChatMuted } from '../../../modules/helpers'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import InputText from '../../ui/InputText'; import ListItem from '../../ui/ListItem'; @@ -28,6 +29,8 @@ import './Management.scss'; type OwnProps = { userId: number; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -54,12 +57,16 @@ const ManageUser: FC = ({ deleteHistory, closeManagement, openChat, + onClose, + isActive, }) => { const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false); const [error, setError] = useState(); const lang = useLang(); + useHistoryBack(isActive, onClose); + const currentFirstName = user ? (user.firstName || '') : ''; const currentLastName = user ? (user.lastName || '') : ''; diff --git a/src/components/right/management/Management.tsx b/src/components/right/management/Management.tsx index ad1531429..92a24b6d3 100644 --- a/src/components/right/management/Management.tsx +++ b/src/components/right/management/Management.tsx @@ -26,6 +26,8 @@ export type OwnProps = { isPromotedByCurrentUser?: boolean; onScreenSelect: (screen: ManagementScreens) => void; onChatMemberSelect: (memberId: number, isPromotedByCurrentUser?: boolean) => void; + onClose: NoneToVoidFunction; + isActive: boolean; }; type StateProps = { @@ -39,17 +41,59 @@ const Management: FC = ({ isPromotedByCurrentUser, onScreenSelect, onChatMemberSelect, + onClose, + isActive, managementType, }) => { switch (currentScreen) { case ManagementScreens.Initial: { switch (managementType) { case 'user': - return ; + return ( + + ); case 'group': - return ; + return ( + + ); case 'channel': - return ; + return ( + + ); } break; @@ -57,7 +101,11 @@ const Management: FC = ({ case ManagementScreens.ChatPrivacyType: return ( - + ); case ManagementScreens.Discussion: @@ -65,6 +113,8 @@ const Management: FC = ({ ); @@ -74,12 +124,22 @@ const Management: FC = ({ chatId={chatId} onScreenSelect={onScreenSelect} onChatMemberSelect={onChatMemberSelect} + isActive={isActive || [ + ManagementScreens.GroupRemovedUsers, + ManagementScreens.GroupUserPermissionsCreate, + ManagementScreens.GroupUserPermissions, + ].includes(currentScreen)} + onClose={onClose} /> ); case ManagementScreens.GroupRemovedUsers: return ( - + ); case ManagementScreens.GroupUserPermissionsCreate: @@ -88,6 +148,10 @@ const Management: FC = ({ chatId={chatId} onChatMemberSelect={onChatMemberSelect} onScreenSelect={onScreenSelect} + isActive={isActive || [ + ManagementScreens.GroupUserPermissions, + ].includes(currentScreen)} + onClose={onClose} /> ); @@ -98,6 +162,8 @@ const Management: FC = ({ selectedChatMemberId={selectedChatMemberId} isPromotedByCurrentUser={isPromotedByCurrentUser} onScreenSelect={onScreenSelect} + isActive={isActive} + onClose={onClose} /> ); @@ -107,6 +173,11 @@ const Management: FC = ({ chatId={chatId} onScreenSelect={onScreenSelect} onChatMemberSelect={onChatMemberSelect} + isActive={isActive || [ + ManagementScreens.ChatAdminRights, + ManagementScreens.GroupRecentActions, + ].includes(currentScreen)} + onClose={onClose} /> ); @@ -114,6 +185,8 @@ const Management: FC = ({ return ( ); @@ -124,13 +197,19 @@ const Management: FC = ({ selectedChatMemberId={selectedChatMemberId} isPromotedByCurrentUser={isPromotedByCurrentUser} onScreenSelect={onScreenSelect} + isActive={isActive} + onClose={onClose} /> ); case ManagementScreens.ChannelSubscribers: case ManagementScreens.GroupMembers: return ( - + ); } diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index 0ee008f14..2f685280b 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -10,6 +10,9 @@ type OwnProps = { positionX?: 'left' | 'right'; positionY?: 'top' | 'bottom'; footer?: string; + forceOpen?: boolean; + onOpen?: NoneToVoidFunction; + onClose?: NoneToVoidFunction; children: any; }; @@ -20,6 +23,9 @@ const DropdownMenu: FC = ({ positionX = 'left', positionY = 'top', footer, + forceOpen, + onOpen, + onClose, }) => { // eslint-disable-next-line no-null/no-null const menuRef = useRef(null); @@ -29,6 +35,9 @@ const DropdownMenu: FC = ({ const toggleIsOpen = () => { setIsOpen(!isOpen); + if (isOpen) { + if (onClose) onClose(); + } else if (onOpen) onOpen(); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -48,6 +57,7 @@ const DropdownMenu: FC = ({ const handleClose = () => { setIsOpen(false); + if (onClose) onClose(); }; return ( @@ -61,13 +71,14 @@ const DropdownMenu: FC = ({ {children} diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx index fe7ec65b3..49ae67cc8 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -8,6 +8,7 @@ import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import buildClassName from '../../util/buildClassName'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; +import useHistoryBack from '../../hooks/useHistoryBack'; import './Menu.scss'; @@ -20,6 +21,7 @@ type OwnProps = { positionX?: 'left' | 'right'; positionY?: 'top' | 'bottom'; autoClose?: boolean; + shouldSkipTransition?: boolean; footer?: string; noCloseOnBackdrop?: boolean; onKeyDown?: (e: React.KeyboardEvent) => void; @@ -48,6 +50,7 @@ const Menu: FC = ({ onClose, onMouseEnter, onMouseLeave, + shouldSkipTransition, }) => { // eslint-disable-next-line no-null/no-null let menuRef = useRef(null); @@ -56,9 +59,22 @@ const Menu: FC = ({ } const backdropContainerRef = containerRef || menuRef; - const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd); + const { + transitionClassNames, + } = useShowTransition( + isOpen, + onCloseAnimationEnd, + shouldSkipTransition, + undefined, + shouldSkipTransition, + ); - useEffect(() => (isOpen && onClose ? captureEscKeyListener(onClose) : undefined), [isOpen, onClose]); + useEffect( + () => (isOpen && onClose ? captureEscKeyListener(onClose) : undefined), + [isOpen, onClose], + ); + + useHistoryBack(isOpen, onClose, undefined, undefined, autoClose); useEffectWithPrevDeps(([prevIsOpen]) => { if (prevIsOpen !== undefined) { diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index aadf052f6..985025f75 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,4 +1,6 @@ -import React, { FC, useEffect, useRef } from '../../lib/teact/teact'; +import React, { + FC, useEffect, useRef, +} from '../../lib/teact/teact'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import trapFocus from '../../util/trapFocus'; @@ -7,6 +9,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import useShowTransition from '../../hooks/useShowTransition'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useLang from '../../hooks/useLang'; +import useHistoryBack from '../../hooks/useHistoryBack'; import Button from './Button'; import Portal from './Portal'; @@ -28,20 +31,29 @@ type OwnProps = { onEnter?: () => void; }; -const Modal: FC = (props) => { +type StateProps = { + shouldSkipHistoryAnimations?: boolean; +}; + +const Modal: FC = ({ + title, + className, + isOpen, + header, + hasCloseButton, + noBackdrop, + children, + onClose, + onCloseAnimationEnd, + onEnter, + shouldSkipHistoryAnimations, +}) => { const { - title, - className, - isOpen, - header, - hasCloseButton, - noBackdrop, - children, - onClose, - onCloseAnimationEnd, - onEnter, - } = props; - const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd); + shouldRender, + transitionClassNames, + } = useShowTransition( + isOpen, onCloseAnimationEnd, shouldSkipHistoryAnimations, undefined, shouldSkipHistoryAnimations, + ); // eslint-disable-next-line no-null/no-null const modalRef = useRef(null); @@ -50,6 +62,8 @@ const Modal: FC = (props) => { : undefined), [isOpen, onClose, onEnter]); useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]); + useHistoryBack(isOpen, onClose); + useEffectWithPrevDeps(([prevIsOpen]) => { document.body.classList.toggle('has-open-dialog', isOpen); diff --git a/src/components/ui/Transition.scss b/src/components/ui/Transition.scss index aa6075b5b..7bb2ade5c 100644 --- a/src/components/ui/Transition.scss +++ b/src/components/ui/Transition.scss @@ -19,6 +19,10 @@ } } + &.skip-slide-transition { + transition: none !important; + } + /* * scroll-slide */ diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index 5be8bb785..d2a6ed2c6 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -13,7 +13,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import './Transition.scss'; -type ChildrenFn = (isActive: boolean, isFrom: boolean) => any; +type ChildrenFn = (isActive: boolean, isFrom: boolean, currentKey: number) => any; type OwnProps = { ref?: RefObject; activeKey: number; @@ -239,7 +239,9 @@ const Transition: FC = ({ const render = renders[key]; return ( - typeof render === 'function' ?
{render(key === activeKey, key === prevActiveKey)}
: undefined + typeof render === 'function' + ?
{render(key === activeKey, key === prevActiveKey, activeKey)}
+ : undefined ); }); diff --git a/src/global/index.ts b/src/global/index.ts index 269986a0c..abdceccc1 100644 --- a/src/global/index.ts +++ b/src/global/index.ts @@ -3,16 +3,10 @@ import { addReducer } from '../lib/teact/teactn'; import { INITIAL_STATE } from './initial'; import { initCache, loadCache } from './cache'; import { cloneDeep } from '../util/iteratees'; -import { selectCurrentMessageList } from '../modules/selectors'; initCache(); addReducer('init', () => { const initial = cloneDeep(INITIAL_STATE); - const newGlobal = loadCache(initial) || initial; - - const currentMessageList = selectCurrentMessageList(newGlobal) || {}; - window.history.replaceState(currentMessageList, ''); - - return newGlobal; + return loadCache(initial) || initial; }); diff --git a/src/global/types.ts b/src/global/types.ts index 3d3a89821..e454086ce 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -65,6 +65,7 @@ export type GlobalState = { isLeftColumnShown: boolean; isPollModalOpen?: boolean; uiReadyState: 0 | 1 | 2; + shouldSkipHistoryAnimations?: boolean; connectionState?: ApiUpdateConnectionStateType; currentUserId?: number; lastSyncTime?: number; @@ -403,7 +404,8 @@ export type ActionTypes = ( 'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' | // ui 'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' | - 'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' | + 'toggleSafeLinkModal' | 'disableHistoryAnimations' | 'openHistoryCalendar' | 'closeHistoryCalendar' | + 'disableContextMenuHint' | // auth 'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' | 'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' | diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index f051e99f8..cf9f546eb 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -1,15 +1,137 @@ -// This is unsafe and can be not chained as `popstate` event is asynchronous +import { useEffect, useRef } from '../lib/teact/teact'; -export default function useHistoryBack(handler: NoneToVoidFunction) { - function handlePopState() { - handler(); +import { IS_IOS } from '../util/environment'; +import usePrevious from './usePrevious'; +import { getDispatch } from '../lib/teact/teactn'; + +// 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; + +let isEdge = false; + +const handleTouchStart = (event: TouchEvent) => { + const x = event.touches[0].pageX; + + if (x <= SAFARI_EDGE_BACK_GESTURE_LIMIT || x >= window.innerWidth - SAFARI_EDGE_BACK_GESTURE_LIMIT) { + isEdge = true; } +}; - window.addEventListener('popstate', handlePopState); - window.history.pushState({}, ''); +const handleTouchEnd = () => { + if (isEdge) { + setTimeout(() => { + isEdge = false; + }, SAFARI_EDGE_BACK_GESTURE_DURATION); + } +}; - return () => { - window.removeEventListener('popstate', handlePopState); - window.history.back(); - }; +if (IS_IOS) { + window.addEventListener('touchstart', handleTouchStart); + window.addEventListener('touchend', handleTouchEnd); + window.addEventListener('popstate', handleTouchEnd); +} + +let currentIndex = 0; +let nextStateIndexToReplace = -1; +let isHistoryAltered = false; +const currentIndexes: number[] = []; + +window.history.replaceState({ index: currentIndex }, ''); + +export default function useHistoryBack( + isActive: boolean | undefined, + onBack: ((noDisableAnimation: boolean) => void) | undefined, + onForward?: (state: any) => void, + currentState?: any, + shouldReplaceNext = false, +) { + const indexRef = useRef(-1); + const isForward = useRef(false); + const prevIsActive = usePrevious(isActive); + const isClosed = useRef(true); + + useEffect(() => { + const handlePopState = (event: PopStateEvent) => { + if (isHistoryAltered) { + setTimeout(() => { + isHistoryAltered = false; + }, 0); + return; + } + const { index: i } = event.state; + const index = i || 0; + + const prev = currentIndexes[currentIndexes.indexOf(indexRef.current) - 1]; + + if (!isClosed.current && (index === 0 || index === prev)) { + currentIndexes.splice(currentIndexes.indexOf(indexRef.current), 1); + + if (onBack) { + if (isEdge) { + getDispatch().disableHistoryAnimations(); + } + onBack(!isEdge); + isClosed.current = true; + } + } else if (index === indexRef.current && isClosed.current && onForward) { + isForward.current = true; + if (isEdge) { + getDispatch().disableHistoryAnimations(); + } + onForward(event.state.state); + } + }; + + if (prevIsActive !== isActive) { + if (isActive) { + isClosed.current = false; + + if (isForward.current) { + isForward.current = false; + currentIndexes.push(indexRef.current); + } else { + setTimeout(() => { + const index = ++currentIndex; + + currentIndexes.push(index); + + window.history[ + (currentIndexes.includes(nextStateIndexToReplace - 1) + && window.history.state.index !== 0 + && nextStateIndexToReplace === index + && !shouldReplaceNext) + ? 'replaceState' + : 'pushState' + ]({ + index, + state: currentState, + }, ''); + + indexRef.current = index; + + if (shouldReplaceNext) { + nextStateIndexToReplace = currentIndex + 1; + } + }, 0); + } + } else if (!isClosed.current) { + if (indexRef.current === currentIndex || !shouldReplaceNext) { + isHistoryAltered = true; + window.history.back(); + + setTimeout(() => { + nextStateIndexToReplace = -1; + }, 400); + } + currentIndexes.splice(currentIndexes.indexOf(indexRef.current), 1); + + isClosed.current = true; + } + } + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, [currentState, isActive, onBack, onForward, prevIsActive, shouldReplaceNext]); } diff --git a/src/hooks/useShowTransition.ts b/src/hooks/useShowTransition.ts index 2c46755d5..54c5ac0df 100644 --- a/src/hooks/useShowTransition.ts +++ b/src/hooks/useShowTransition.ts @@ -15,6 +15,7 @@ export default ( // СSS class should be added in a separate tick to turn on CSS transition. const [hasOpenClassName, setHasOpenClassName] = useState(isOpen && noOpenTransition); + if (isOpen) { setIsClosed(false); setHasOpenClassName(true); @@ -39,12 +40,14 @@ export default ( } } + // `noCloseTransition`, when set to true, should remove the open class immediately + const shouldHaveOpenClassName = hasOpenClassName && !(noCloseTransition && !isOpen); const isClosing = Boolean(closeTimeoutRef.current); const shouldRender = isOpen || isClosing; const transitionClassNames = buildClassName( className && 'opacity-transition', className, - hasOpenClassName && 'open', + shouldHaveOpenClassName && 'open', shouldRender && 'shown', isClosing && 'closing', ); diff --git a/src/lib/teact/dom-events.ts b/src/lib/teact/dom-events.ts index a508df895..2cde1a040 100644 --- a/src/lib/teact/dom-events.ts +++ b/src/lib/teact/dom-events.ts @@ -8,26 +8,28 @@ const delegationRegistry: Record> = {}; const delegatedEventsByElement = new Map>(); const documentEventCounters: Record = {}; -export function addEventListener(element: HTMLElement, propName: string, handler: Handler) { +export function addEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { const eventName = resolveEventName(propName, element); - if (canUseEventDelegation(eventName, element)) { + if (canUseEventDelegation(eventName, element, asCapture)) { addDelegatedListener(eventName, element, handler); } else { - element.addEventListener(eventName, handler); + element.addEventListener(eventName, handler, asCapture); } } -export function removeEventListener(element: HTMLElement, propName: string, handler: Handler) { +export function removeEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { const eventName = resolveEventName(propName, element); - if (canUseEventDelegation(eventName, element)) { + if (canUseEventDelegation(eventName, element, asCapture)) { removeDelegatedListener(eventName, element); } else { - element.removeEventListener(eventName, handler); + element.removeEventListener(eventName, handler, asCapture); } } function resolveEventName(propName: string, element: HTMLElement) { - const eventName = propName.replace(/^on/, '').toLowerCase(); + const eventName = propName + .replace(/^on/, '') + .replace(/Capture$/, '').toLowerCase(); if (eventName === 'change' && element.tagName !== 'SELECT') { // React behavior repeated here. @@ -51,9 +53,10 @@ function resolveEventName(propName: string, element: HTMLElement) { return eventName; } -function canUseEventDelegation(realEventName: string, element: HTMLElement) { +function canUseEventDelegation(realEventName: string, element: HTMLElement, asCapture: boolean) { return ( - !NON_BUBBLEABLE_EVENTS.has(realEventName) + !asCapture + && !NON_BUBBLEABLE_EVENTS.has(realEventName) && element.tagName !== 'VIDEO' && element.tagName !== 'IFRAME' ); diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 05f6fd24a..d63c6cde0 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -428,7 +428,7 @@ function addAttribute(element: HTMLElement, key: string, value: any) { } else if (key === 'style') { element.style.cssText = value; } else if (key.startsWith('on')) { - addEventListener(element, key, value); + addEventListener(element, key, value, key.endsWith('Capture')); } else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) { element.setAttribute(key, value); } else if (!FILTERED_ATTRIBUTES.has(key)) { @@ -444,7 +444,7 @@ function removeAttribute(element: HTMLElement, key: string, value: any) { } else if (key === 'style') { element.style.cssText = ''; } else if (key.startsWith('on')) { - removeEventListener(element, key, value); + removeEventListener(element, key, value, key.endsWith('Capture')); } else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) { element.removeAttribute(key); } else if (!FILTERED_ATTRIBUTES.has(key)) { diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index 9c32e7f1e..1dc78b6d9 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -46,10 +46,15 @@ function runCallbacks() { const runCallbacksThrottled = throttleWithRaf(runCallbacks); -export function setGlobal(newGlobal?: GlobalState) { +// `noThrottle = true` is used as a workaround for iOS gesture history navigation +export function setGlobal(newGlobal?: GlobalState, noThrottle = false) { if (typeof newGlobal === 'object' && newGlobal !== currentGlobal) { currentGlobal = newGlobal; - runCallbacksThrottled(); + if (!noThrottle) { + runCallbacksThrottled(); + } else { + runCallbacks(); + } } } @@ -61,12 +66,12 @@ export function getDispatch() { return actions; } -function onDispatch(name: string, payload?: ActionPayload) { +function onDispatch(name: string, payload?: ActionPayload, noThrottle?: boolean) { if (reducers[name]) { reducers[name].forEach((reducer) => { const newGlobal = reducer(currentGlobal, actions, payload); if (newGlobal) { - setGlobal(newGlobal); + setGlobal(newGlobal, noThrottle); } }); } @@ -139,8 +144,8 @@ export function addReducer(name: ActionTypes, reducer: Reducer) { if (!reducers[name]) { reducers[name] = []; - actions[name] = (payload?: ActionPayload) => { - onDispatch(name, payload); + actions[name] = (payload?: ActionPayload, noThrottle = false) => { + onDispatch(name, payload, noThrottle); }; } diff --git a/src/modules/actions/ui/chats.ts b/src/modules/actions/ui/chats.ts index d5831a626..b41950e00 100644 --- a/src/modules/actions/ui/chats.ts +++ b/src/modules/actions/ui/chats.ts @@ -1,25 +1,14 @@ -import { addReducer, getDispatch, setGlobal } from '../../../lib/teact/teactn'; +import { addReducer, setGlobal } from '../../../lib/teact/teactn'; import { exitMessageSelectMode, updateCurrentMessageList, } from '../../reducers'; import { selectCurrentMessageList } from '../../selectors'; - -window.addEventListener('popstate', (e) => { - if (!e.state) { - return; - } - - const { chatId: id, threadId, messageListType: type } = e.state; - - getDispatch().openChat({ - id, threadId, type, noPushState: true, - }); -}); +import { closeLocalTextSearch } from './localSearch'; addReducer('openChat', (global, actions, payload) => { const { - id, threadId = -1, type = 'thread', noPushState, + id, threadId = -1, type = 'thread', } = payload!; const currentMessageList = selectCurrentMessageList(global); @@ -31,6 +20,7 @@ addReducer('openChat', (global, actions, payload) => { || currentMessageList.type !== type )) { global = exitMessageSelectMode(global); + global = closeLocalTextSearch(global); global = { ...global, @@ -44,10 +34,6 @@ addReducer('openChat', (global, actions, payload) => { }; setGlobal(global); - - if (!noPushState) { - window.history.pushState({ chatId: id, threadId, messageListType: type }, ''); - } } return updateCurrentMessageList(global, id, threadId, type); diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index 0d52a5811..8683e822b 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -8,6 +8,8 @@ import { setLanguage } from '../../../util/langProvider'; import switchTheme from '../../../util/switchTheme'; import { selectTheme } from '../../selectors'; +const HISTORY_ANIMATION_DURATION = 450; + subscribeToSystemThemeChange(); addReducer('init', (global) => { @@ -68,6 +70,21 @@ addReducer('clearAuthError', (global) => { }; }); +addReducer('disableHistoryAnimations', () => { + setTimeout(() => { + setGlobal({ + ...getGlobal(), + shouldSkipHistoryAnimations: false, + }); + document.body.classList.remove('no-animate'); + }, HISTORY_ANIMATION_DURATION); + + setGlobal({ + ...getGlobal(), + shouldSkipHistoryAnimations: true, + }, true); +}); + function subscribeToSystemThemeChange() { function handleSystemThemeChange() { const currentThemeMatch = document.documentElement.className.match(/theme-(\w+)/); diff --git a/src/modules/actions/ui/localSearch.ts b/src/modules/actions/ui/localSearch.ts index 3d83e45a2..79b480c36 100644 --- a/src/modules/actions/ui/localSearch.ts +++ b/src/modules/actions/ui/localSearch.ts @@ -8,6 +8,7 @@ import { import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { selectCurrentMessageList } from '../../selectors'; import { buildChatThreadKey } from '../../helpers'; +import { GlobalState } from '../../../global/types'; addReducer('openLocalTextSearch', (global) => { const { chatId, threadId } = selectCurrentMessageList(global) || {}; @@ -18,16 +19,7 @@ addReducer('openLocalTextSearch', (global) => { return updateLocalTextSearch(global, chatId, threadId, true); }); -addReducer('closeLocalTextSearch', (global) => { - const { chatId, threadId } = selectCurrentMessageList(global) || {}; - if (!chatId || !threadId) { - return undefined; - } - - global = updateLocalTextSearch(global, chatId, threadId, false); - global = replaceLocalTextSearchResults(global, chatId, threadId, undefined); - return global; -}); +addReducer('closeLocalTextSearch', closeLocalTextSearch); addReducer('setLocalTextSearchQuery', (global, actions, payload) => { const { chatId, threadId } = selectCurrentMessageList(global) || {}; @@ -57,3 +49,14 @@ addReducer('setLocalMediaSearchType', (global, actions, payload) => { const { mediaType } = payload!; return updateLocalMediaSearchType(global, chatId, mediaType); }); + +export function closeLocalTextSearch(global: GlobalState): GlobalState { + const { chatId, threadId } = selectCurrentMessageList(global) || {}; + if (!chatId || !threadId) { + return global; + } + + global = updateLocalTextSearch(global, chatId, threadId, false); + global = replaceLocalTextSearchResults(global, chatId, threadId, undefined); + return global; +} diff --git a/src/styles/_common.scss b/src/styles/_common.scss index 5554e4ce6..659da08fc 100644 --- a/src/styles/_common.scss +++ b/src/styles/_common.scss @@ -65,6 +65,7 @@ // Used by ChatList and ContactList components .chat-list { + background: var(--color-background); height: 100%; overflow-y: auto; padding: .5rem .125rem .5rem .4375rem;