diff --git a/src/components/auth/AuthCode.tsx b/src/components/auth/AuthCode.tsx index 77a298d8c..1df561de4 100644 --- a/src/components/auth/AuthCode.tsx +++ b/src/components/auth/AuthCode.tsx @@ -12,14 +12,24 @@ import InputText from '../ui/InputText'; import Loading from '../ui/Loading'; import TrackingMonkey from '../common/TrackingMonkey'; import useHistoryBack from '../../hooks/useHistoryBack'; +import { HistoryWrapper } from '../../util/history'; 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, + setShouldSkipUiLoaderTransition, }) => { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -34,7 +44,15 @@ const AuthCode: FC = ({ } }, []); - useHistoryBack(returnToAuthPhoneNumber); + const handleBackToPhoneNumber = () => { + returnToAuthPhoneNumber(); + HistoryWrapper.back(); + }; + + useHistoryBack((event, noAnimation) => { + setShouldSkipUiLoaderTransition({ shouldSkipUiLoaderTransition: noAnimation }); + returnToAuthPhoneNumber(); + }); const onCodeChange = useCallback((e: FormEvent) => { if (authError) { @@ -80,7 +98,7 @@ const AuthCode: FC = ({ {authPhoneNumber}
= ({ 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', + 'setShouldSkipUiLoaderTransition', + ]), )(AuthCode)); diff --git a/src/components/auth/AuthQrCode.tsx b/src/components/auth/AuthQrCode.tsx index ae4e3a029..14e178898 100644 --- a/src/components/auth/AuthQrCode.tsx +++ b/src/components/auth/AuthQrCode.tsx @@ -11,14 +11,18 @@ import Loading from '../ui/Loading'; import Button from '../ui/Button'; import buildClassName from '../../util/buildClassName'; import useHistoryBack from '../../hooks/useHistoryBack'; +import { HistoryWrapper } from '../../util/history'; type StateProps = Pick; -type DispatchProps = Pick; +type DispatchProps = Pick; const DATA_PREFIX = 'tg://login?token='; const AuthCode: FC = ({ - connectionState, authQrCode, returnToAuthPhoneNumber, + connectionState, + authQrCode, + returnToAuthPhoneNumber, + setShouldSkipUiLoaderTransition, }) => { // eslint-disable-next-line no-null/no-null const qrCodeRef = useRef(null); @@ -41,7 +45,15 @@ const AuthCode: FC = ({ }, container); }, [connectionState, authQrCode]); - useHistoryBack(returnToAuthPhoneNumber); + const handleBackToPhoneNumber = () => { + HistoryWrapper.back(); + returnToAuthPhoneNumber(); + }; + + useHistoryBack((event, noAnimation) => { + setShouldSkipUiLoaderTransition({ shouldSkipUiLoaderTransition: noAnimation }); + returnToAuthPhoneNumber(); + }); return (
@@ -55,7 +67,7 @@ const AuthCode: FC = ({
  • Go to Settings > Devices > Scan QR
  • Point your phone at this screen to confirm login
  • - +
    ); @@ -63,5 +75,5 @@ const AuthCode: FC = ({ export default memo(withGlobal( (global): StateProps => pick(global, ['connectionState', 'authQrCode']), - (setGlobal, actions): DispatchProps => pick(actions, ['returnToAuthPhoneNumber']), + (setGlobal, actions): DispatchProps => pick(actions, ['returnToAuthPhoneNumber', 'setShouldSkipUiLoaderTransition']), )(AuthCode)); diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index da0350458..f25e6b14c 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -7,6 +7,7 @@ import { GlobalActions, GlobalState } from '../../global/types'; import { getChatAvatarHash } from '../../modules/helpers/chats'; // Direct import for better module splitting import useFlag from '../../hooks/useFlag'; import useShowTransition from '../../hooks/useShowTransition'; +import useOnChange from '../../hooks/useOnChange'; import { pause } from '../../util/schedulers'; import { preloadImage } from '../../util/files'; import preloadFonts from '../../util/fonts'; @@ -28,17 +29,18 @@ type OwnProps = { children: any; }; -type StateProps = Pick & { +type StateProps = Pick & { hasCustomBackground?: boolean; hasCustomBackgroundColor: boolean; isRightColumnShown?: boolean; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const MAX_PRELOAD_DELAY = 700; const SECOND_STATE_DELAY = 1000; const AVATARS_TO_PRELOAD = 10; +const TRANSITION_TIME = 400; function preloadAvatars() { const { listIds, byId } = getGlobal().chats; @@ -82,7 +84,9 @@ const UiLoader: FC = ({ hasCustomBackground, hasCustomBackgroundColor, isRightColumnShown, + shouldSkipUiLoaderTransition, setIsUiReady, + setShouldSkipUiLoaderTransition, }) => { const [isReady, markReady] = useFlag(); const { @@ -123,10 +127,18 @@ const UiLoader: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useOnChange(() => { + if (shouldSkipUiLoaderTransition) { + setTimeout(() => { + setShouldSkipUiLoaderTransition({ shouldSkipUiLoaderTransition: false }); + }, TRANSITION_TIME); + } + }, [shouldSkipUiLoaderTransition]); + return (
    {children} - {shouldRenderMask && ( + {shouldRenderMask && !shouldSkipUiLoaderTransition && (
    {page === 'main' ? ( <> @@ -156,11 +168,12 @@ export default withGlobal( const { background, backgroundColor } = global.settings.themes[theme] || {}; return { + shouldSkipUiLoaderTransition: global.shouldSkipUiLoaderTransition, uiReadyState: global.uiReadyState, hasCustomBackground: Boolean(background), hasCustomBackgroundColor: Boolean(backgroundColor), isRightColumnShown: selectIsRightColumnShown(global), }; }, - (setGlobal, actions): DispatchProps => pick(actions, ['setIsUiReady']), + (setGlobal, actions): DispatchProps => pick(actions, ['setIsUiReady', 'setShouldSkipUiLoaderTransition']), )(UiLoader); diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index f585be440..25a2dea0a 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -6,15 +6,19 @@ import { withGlobal } from '../../lib/teact/teactn'; import { GlobalActions } from '../../global/types'; import { LeftColumnContent, SettingsScreens } from '../../types'; +import useHistoryBack from '../../hooks/useHistoryBack'; +import useFlag from '../../hooks/useFlag'; + import { IS_MOBILE_SCREEN } from '../../util/environment'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { pick } from '../../util/iteratees'; -import Transition from '../ui/Transition'; +import Transition, { ANIMATION_DURATION } from '../ui/Transition'; import LeftMain from './main/LeftMain'; import Settings from './settings/Settings.async'; import NewChat from './newChat/NewChat.async'; import ArchivedChats from './ArchivedChats.async'; +import { HistoryWrapper } from '../../util/history'; import './LeftColumn.scss'; @@ -57,6 +61,32 @@ const LeftColumn: FC = ({ const [content, setContent] = useState(LeftColumnContent.ChatList); const [settingsScreen, setSettingsScreen] = useState(SettingsScreens.Main); const [contactsFilter, setContactsFilter] = useState(''); + const [isMenuOpen, openMenu, closeMenu] = useFlag(); + + const setContentWithHistory = useCallback((contentKey: LeftColumnContent) => { + if (contentKey !== LeftColumnContent.ChatList + && contentKey !== LeftColumnContent.NewChannelStep2 + && contentKey !== LeftColumnContent.NewGroupStep2) { + HistoryWrapper.pushState({ + type: 'left', + contentKey, + isMenuOpen, + }); + } + setContent(contentKey); + }, [isMenuOpen]); + + const setSettingsScreenWithHistory = useCallback((screen: SettingsScreens, noPushState = false) => { + setSettingsScreen(screen); + if (!noPushState) { + HistoryWrapper.pushState({ + type: 'left', + contentKey: LeftColumnContent.Settings, + screen, + isMenuOpen, + }); + } + }, [isMenuOpen]); // Used to reset child components in background. const [lastResetTime, setLastResetTime] = useState(0); @@ -79,12 +109,13 @@ const LeftColumn: FC = ({ break; } - const handleReset = useCallback((forceReturnToChatList?: boolean) => { + const handleReset = useCallback((forceReturnToChatList?: boolean, noPushState = false) => { if ( content === LeftColumnContent.NewGroupStep2 && !forceReturnToChatList ) { - setContent(LeftColumnContent.NewGroupStep1); + if (!noPushState) HistoryWrapper.back(); + setContentWithHistory(LeftColumnContent.NewGroupStep1); return; } @@ -96,6 +127,9 @@ const LeftColumn: FC = ({ } if (content === LeftColumnContent.Settings) { + if (!noPushState) { + HistoryWrapper.back(); + } switch (settingsScreen) { case SettingsScreens.EditProfile: case SettingsScreens.Folders: @@ -103,14 +137,14 @@ const LeftColumn: FC = ({ case SettingsScreens.Notifications: case SettingsScreens.Privacy: case SettingsScreens.Language: - setSettingsScreen(SettingsScreens.Main); + setSettingsScreenWithHistory(SettingsScreens.Main, noPushState); return; case SettingsScreens.GeneralChatBackground: - setSettingsScreen(SettingsScreens.General); + setSettingsScreenWithHistory(SettingsScreens.General, noPushState); return; case SettingsScreens.GeneralChatBackgroundColor: - setSettingsScreen(SettingsScreens.GeneralChatBackground); + setSettingsScreenWithHistory(SettingsScreens.GeneralChatBackground, noPushState); return; case SettingsScreens.PrivacyPhoneNumber: @@ -123,79 +157,83 @@ const LeftColumn: FC = ({ case SettingsScreens.TwoFaDisabled: case SettingsScreens.TwoFaEnabled: case SettingsScreens.TwoFaCongratulations: - setSettingsScreen(SettingsScreens.Privacy); + setSettingsScreenWithHistory(SettingsScreens.Privacy, noPushState); return; case SettingsScreens.PrivacyPhoneNumberAllowedContacts: case SettingsScreens.PrivacyPhoneNumberDeniedContacts: - setSettingsScreen(SettingsScreens.PrivacyPhoneNumber); + setSettingsScreenWithHistory(SettingsScreens.PrivacyPhoneNumber, noPushState); return; case SettingsScreens.PrivacyLastSeenAllowedContacts: case SettingsScreens.PrivacyLastSeenDeniedContacts: - setSettingsScreen(SettingsScreens.PrivacyLastSeen); + setSettingsScreenWithHistory(SettingsScreens.PrivacyLastSeen, noPushState); return; case SettingsScreens.PrivacyProfilePhotoAllowedContacts: case SettingsScreens.PrivacyProfilePhotoDeniedContacts: - setSettingsScreen(SettingsScreens.PrivacyProfilePhoto); + setSettingsScreenWithHistory(SettingsScreens.PrivacyProfilePhoto, noPushState); return; case SettingsScreens.PrivacyForwardingAllowedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts: - setSettingsScreen(SettingsScreens.PrivacyForwarding); + setSettingsScreenWithHistory(SettingsScreens.PrivacyForwarding, noPushState); return; case SettingsScreens.PrivacyGroupChatsAllowedContacts: case SettingsScreens.PrivacyGroupChatsDeniedContacts: - setSettingsScreen(SettingsScreens.PrivacyGroupChats); + setSettingsScreenWithHistory(SettingsScreens.PrivacyGroupChats, noPushState); return; case SettingsScreens.TwoFaNewPassword: - setSettingsScreen(SettingsScreens.TwoFaDisabled); + setSettingsScreenWithHistory(SettingsScreens.TwoFaDisabled, noPushState); return; case SettingsScreens.TwoFaNewPasswordConfirm: - setSettingsScreen(SettingsScreens.TwoFaNewPassword); + setSettingsScreenWithHistory(SettingsScreens.TwoFaNewPassword, noPushState); return; case SettingsScreens.TwoFaNewPasswordHint: - setSettingsScreen(SettingsScreens.TwoFaNewPasswordConfirm); + setSettingsScreenWithHistory(SettingsScreens.TwoFaNewPasswordConfirm, noPushState); return; case SettingsScreens.TwoFaNewPasswordEmail: - setSettingsScreen(SettingsScreens.TwoFaNewPasswordHint); + setSettingsScreenWithHistory(SettingsScreens.TwoFaNewPasswordHint, noPushState); return; case SettingsScreens.TwoFaNewPasswordEmailCode: - setSettingsScreen(SettingsScreens.TwoFaNewPasswordEmail); + setSettingsScreenWithHistory(SettingsScreens.TwoFaNewPasswordEmail, noPushState); return; case SettingsScreens.TwoFaChangePasswordCurrent: case SettingsScreens.TwoFaTurnOff: case SettingsScreens.TwoFaRecoveryEmailCurrentPassword: - setSettingsScreen(SettingsScreens.TwoFaEnabled); + setSettingsScreenWithHistory(SettingsScreens.TwoFaEnabled, noPushState); return; case SettingsScreens.TwoFaChangePasswordNew: - setSettingsScreen(SettingsScreens.TwoFaChangePasswordCurrent); + setSettingsScreenWithHistory(SettingsScreens.TwoFaChangePasswordCurrent, noPushState); return; case SettingsScreens.TwoFaChangePasswordConfirm: - setSettingsScreen(SettingsScreens.TwoFaChangePasswordNew); + setSettingsScreenWithHistory(SettingsScreens.TwoFaChangePasswordNew, noPushState); return; case SettingsScreens.TwoFaChangePasswordHint: - setSettingsScreen(SettingsScreens.TwoFaChangePasswordConfirm); + setSettingsScreenWithHistory(SettingsScreens.TwoFaChangePasswordConfirm, noPushState); return; case SettingsScreens.TwoFaRecoveryEmail: - setSettingsScreen(SettingsScreens.TwoFaRecoveryEmailCurrentPassword); + setSettingsScreenWithHistory(SettingsScreens.TwoFaRecoveryEmailCurrentPassword, noPushState); return; case SettingsScreens.TwoFaRecoveryEmailCode: - setSettingsScreen(SettingsScreens.TwoFaRecoveryEmail); + setSettingsScreenWithHistory(SettingsScreens.TwoFaRecoveryEmail, noPushState); return; case SettingsScreens.FoldersCreateFolder: case SettingsScreens.FoldersEditFolder: - setSettingsScreen(SettingsScreens.Folders); + setSettingsScreenWithHistory(SettingsScreens.Folders, noPushState); return; default: break; } } + if (!noPushState) { + HistoryWrapper.back(); + } + if (content === LeftColumnContent.ChatList && activeChatFolder === 0) { - setContent(LeftColumnContent.GlobalSearch); + setContentWithHistory(LeftColumnContent.GlobalSearch); return; } - setContent(LeftColumnContent.ChatList); + setContentWithHistory(LeftColumnContent.ChatList); setContactsFilter(''); setGlobalSearchQuery({ query: '' }); setGlobalSearchDate({ date: undefined }); @@ -205,22 +243,35 @@ const LeftColumn: FC = ({ setLastResetTime(Date.now()); }, RESET_TRANSITION_DELAY_MS); }, [ - content, activeChatFolder, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId, resetChatCreation, - settingsScreen, + content, activeChatFolder, setContentWithHistory, settingsScreen, setSettingsScreenWithHistory, + setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId, resetChatCreation, ]); + const [shouldSkipTransition, setShouldSkipTransition] = useState(false); + useHistoryBack((event, noAnimation, previousHistoryState) => { + if (previousHistoryState && previousHistoryState.type === 'left') { + if (noAnimation) { + setShouldSkipTransition(true); + setTimeout(() => { + setShouldSkipTransition(false); + }, ANIMATION_DURATION[IS_MOBILE_SCREEN ? 'slide-layers' : 'push-slide']); + } + handleReset(false, true); + } + }); + const handleSearchQuery = useCallback((query: string) => { if (content === LeftColumnContent.Contacts) { setContactsFilter(query); return; } - setContent(LeftColumnContent.GlobalSearch); + setContentWithHistory(LeftColumnContent.GlobalSearch); if (query !== searchQuery) { setGlobalSearchQuery({ query }); } - }, [content, setGlobalSearchQuery, searchQuery]); + }, [content, setContentWithHistory, searchQuery, setGlobalSearchQuery]); useEffect( () => (content !== LeftColumnContent.ChatList || activeChatFolder === 0 @@ -240,7 +291,7 @@ const LeftColumn: FC = ({ return ( @@ -257,8 +308,9 @@ const LeftColumn: FC = ({ return ( ); case ContentType.NewChannel: @@ -267,7 +319,7 @@ const LeftColumn: FC = ({ key={lastResetTime} isChannel content={content} - onContentChange={setContent} + onContentChange={setContentWithHistory} onReset={handleReset} /> ); @@ -276,7 +328,7 @@ const LeftColumn: FC = ({ ); @@ -287,9 +339,12 @@ const LeftColumn: FC = ({ searchQuery={searchQuery} searchDate={searchDate} contactsFilter={contactsFilter} - onContentChange={setContent} + onContentChange={setContentWithHistory} onSearchQuery={handleSearchQuery} onReset={handleReset} + shouldSkipTransition={shouldSkipTransition} + onOpenMenu={openMenu} + onCloseMenu={closeMenu} /> ); } diff --git a/src/components/left/main/LeftMain.tsx b/src/components/left/main/LeftMain.tsx index 68af7bdda..146ef1737 100644 --- a/src/components/left/main/LeftMain.tsx +++ b/src/components/left/main/LeftMain.tsx @@ -21,9 +21,12 @@ type OwnProps = { searchQuery?: string; searchDate?: number; contactsFilter: string; + shouldSkipTransition?: boolean; onSearchQuery: (query: string) => void; onContentChange: (content: LeftColumnContent) => void; onReset: () => void; + onOpenMenu: NoneToVoidFunction; + onCloseMenu: NoneToVoidFunction; }; type StateProps = {}; @@ -37,9 +40,12 @@ const LeftMain: FC = ({ searchQuery, searchDate, contactsFilter, + shouldSkipTransition, onSearchQuery, onContentChange, onReset, + onOpenMenu, + onCloseMenu, }) => { const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV); @@ -119,10 +125,17 @@ const LeftMain: FC = ({ onSelectSettings={handleSelectSettings} onSelectContacts={handleSelectContacts} onSelectArchived={handleSelectArchived} + onOpenMenu={onOpenMenu} + onCloseMenu={onCloseMenu} onReset={onReset} + shouldSkipTransition={shouldSkipTransition} /> - + {(isActive) => { switch (content) { case LeftColumnContent.ChatList: diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index c1b8da106..e2ad5f9d2 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 6cc3d36ad..f5c12d92f 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -1,5 +1,5 @@ import React, { - FC, useCallback, useMemo, memo, + FC, useCallback, useMemo, memo, useState, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; @@ -15,6 +15,7 @@ import { isChatArchived } from '../../../modules/helpers'; import { formatDateToString } from '../../../util/dateFormat'; import switchTheme from '../../../util/switchTheme'; import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; import DropdownMenu from '../../ui/DropdownMenu'; import MenuItem from '../../ui/MenuItem'; @@ -28,11 +29,14 @@ import './LeftMainHeader.scss'; type OwnProps = { content: LeftColumnContent; contactsFilter: string; + shouldSkipTransition?: boolean; onSearchQuery: (query: string) => void; onSelectSettings: () => void; onSelectContacts: () => void; onSelectArchived: () => void; onReset: () => void; + onOpenMenu: NoneToVoidFunction; + onCloseMenu: NoneToVoidFunction; }; type StateProps = { @@ -51,6 +55,7 @@ type DispatchProps = Pick; const ANIMATION_LEVEL_OPTIONS = [0, 1, 2]; +const MENU_ANIMATION_DURATION = 300; const LEGACY_VERSION = 'https://web.telegram.org/'; const WEBK_VERSION = 'https://web.telegram.org/k/'; @@ -62,10 +67,13 @@ const LeftMainHeader: FC = ({ onSelectSettings, onSelectContacts, onSelectArchived, + onOpenMenu, + onCloseMenu, setGlobalSearchChatId, onReset, searchQuery, isLoading, + shouldSkipTransition, currentUserId, globalSearchChatId, searchDate, @@ -111,10 +119,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) { @@ -155,12 +168,25 @@ const LeftMainHeader: FC = ({ ? lang('SearchFriends') : lang('Search'); + const [forceOpenDropdown, setForceOpenDropdown] = useState(false); + + useHistoryBack((event, noAnimation, previousHistoryState) => { + if (previousHistoryState && previousHistoryState.type === 'left' && previousHistoryState.isMenuOpen + && noAnimation) { + setForceOpenDropdown(true); + setTimeout(() => setForceOpenDropdown(false), MENU_ANIMATION_DURATION); + } + }); + return (
    void; + shouldSkipTransition?: boolean; onReset: () => void; }; @@ -38,6 +39,7 @@ const Settings: FC = ({ currentScreen, onScreenSelect, onReset, + shouldSkipTransition, }) => { const [foldersState, foldersDispatch] = useFoldersReducer(); const [twoFaState, twoFaDispatch] = useTwoFaReducer(); @@ -213,7 +215,7 @@ const Settings: FC = ({ 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 3b5cd205e..5bb0213e7 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -1,4 +1,6 @@ -import React, { FC, useEffect, memo } from '../../lib/teact/teact'; +import React, { + FC, useEffect, memo, useState, +} from '../../lib/teact/teact'; import { getGlobal, withGlobal } from '../../lib/teact/teactn'; import { GlobalActions } from '../../global/types'; @@ -6,7 +8,7 @@ import { ApiMessage } from '../../api/types'; import '../../modules/actions/all'; import { - ANIMATION_END_DELAY, DEBUG, INACTIVE_MARKER, PAGE_TITLE, + ANIMATION_END_DELAY, DEBUG, INACTIVE_MARKER, PAGE_TITLE, SLIDE_TRANSITION_DURATION, } from '../../config'; import { pick } from '../../util/iteratees'; import { @@ -20,6 +22,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import buildClassName from '../../util/buildClassName'; import useShowTransition from '../../hooks/useShowTransition'; import useBackgroundMode from '../../hooks/useBackgroundMode'; +import useHistoryBack from '../../hooks/useHistoryBack'; import LeftColumn from '../left/LeftColumn'; import MiddleColumn from '../middle/MiddleColumn'; @@ -46,7 +49,7 @@ type StateProps = { safeLinkModalUrl?: string; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const ANIMATION_DURATION = 350; const NOTIFICATION_INTERVAL = 1000; @@ -59,6 +62,7 @@ let DEBUG_isLogged = false; const Main: FC = ({ lastSyncTime, loadAnimatedEmojis, + openChat, isLeftColumnShown, isRightColumnShown, isMediaViewerOpen, @@ -82,17 +86,21 @@ const Main: FC = ({ } }, [lastSyncTime, loadAnimatedEmojis]); + const [isHistoryAnimationDisabled, setIsHistoryAnimationDisabled] = useState(false); + const { transitionClassNames: middleColumnTransitionClassNames, - } = useShowTransition(!isLeftColumnShown, undefined, true); + } = useShowTransition(!isLeftColumnShown, undefined, true, undefined, isHistoryAnimationDisabled); const { transitionClassNames: rightColumnTransitionClassNames, - } = useShowTransition(isRightColumnShown, undefined, true); + } = useShowTransition(isRightColumnShown, undefined, true, undefined, isHistoryAnimationDisabled); + const className = buildClassName( middleColumnTransitionClassNames.replace(/([\w-]+)/g, 'middle-column-$1'), rightColumnTransitionClassNames.replace(/([\w-]+)/g, 'right-column-$1'), + isHistoryAnimationDisabled && 'history-animation-disabled', ); useEffect(() => { @@ -160,6 +168,34 @@ const Main: FC = ({ e.stopPropagation(); } + useHistoryBack((event, noAnimation) => { + const { state } = event; + + if (state.type !== 'right') { + if (state.type === 'chat') { + const { chatId: id, threadId, messageListType: type } = state; + + openChat({ + id, threadId, type, noPushState: true, + }, true); + } else { + openChat({ + id: undefined, + noPushState: true, + }, true); + } + } + + // Must disable pane closing animation for back/forward gestures on iOS + if (noAnimation) { + setIsHistoryAnimationDisabled(true); + + setTimeout(() => { + setIsHistoryAnimationDisabled(false); + }, SLIDE_TRANSITION_DURATION); + } + }); + return (
    @@ -208,5 +244,5 @@ export default memo(withGlobal( safeLinkModalUrl: global.safeLinkModalUrl, }; }, - (setGlobal, actions): DispatchProps => pick(actions, ['loadAnimatedEmojis']), + (setGlobal, actions): DispatchProps => pick(actions, ['loadAnimatedEmojis', 'openChat']), )(Main)); diff --git a/src/components/middle/MobileSearch.tsx b/src/components/middle/MobileSearch.tsx index 0f6c4d039..94906d5f9 100644 --- a/src/components/middle/MobileSearch.tsx +++ b/src/components/middle/MobileSearch.tsx @@ -153,7 +153,7 @@ const MobileSearchFooter: FC = ({ size="smaller" round color="translucent" - onClick={closeLocalTextSearch} + onClick={() => closeLocalTextSearch({ noPushState: true })} > diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index ec588169b..6ee95ab4a 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'; @@ -26,6 +27,7 @@ import Management from './management/Management.async'; import StickerSearch from './StickerSearch.async'; import GifSearch from './GifSearch.async'; import PollResults from './PollResults.async'; +import { HistoryWrapper } from '../../util/history'; import './RightColumn.scss'; @@ -88,26 +90,26 @@ const RightColumn: FC = ({ const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1; - const close = useCallback(() => { + const close = useCallback((noPushState = false) => { switch (contentKey) { case RightColumnContent.ChatInfo: if (isScrolledDown) { setProfileState(ProfileState.Profile); - break; + if (!noPushState) break; } - toggleChatInfo(); + toggleChatInfo({ noPushState }, true); break; case RightColumnContent.UserInfo: if (isScrolledDown) { setProfileState(ProfileState.Profile); - break; + if (!noPushState) break; } openUserInfo({ id: undefined }); break; case RightColumnContent.Management: { switch (managementScreen) { case ManagementScreens.Initial: - toggleManagement(); + toggleManagement({ noPushState }, true); break; case ManagementScreens.ChatPrivacyType: case ManagementScreens.Discussion: @@ -116,17 +118,28 @@ const RightColumn: FC = ({ case ManagementScreens.ChatAdministrators: case ManagementScreens.ChannelSubscribers: case ManagementScreens.GroupMembers: + if (!noPushState) { + HistoryWrapper.back(); + } setManagementScreen(ManagementScreens.Initial); break; case ManagementScreens.GroupUserPermissionsCreate: case ManagementScreens.GroupRemovedUsers: case ManagementScreens.GroupUserPermissions: + if (!noPushState) { + HistoryWrapper.back(); + } + setManagementScreen(ManagementScreens.GroupPermissions); setSelectedChatMemberId(undefined); setIsPromotedByCurrentUser(undefined); break; case ManagementScreens.ChatAdminRights: case ManagementScreens.GroupRecentActions: + if (!noPushState) { + HistoryWrapper.back(); + } + setManagementScreen(ManagementScreens.ChatAdministrators); break; } @@ -135,18 +148,20 @@ const RightColumn: FC = ({ } case RightColumnContent.Search: { blurSearchInput(); - closeLocalTextSearch(); + closeLocalTextSearch({ noPushState }); break; } case RightColumnContent.StickerSearch: + blurSearchInput(); + setStickerSearchQuery({ query: undefined, noPushState }); + break; case RightColumnContent.GifSearch: { blurSearchInput(); - setStickerSearchQuery({ query: undefined }); - setGifSearchQuery({ query: undefined }); + setGifSearchQuery({ query: undefined, noPushState }); break; } case RightColumnContent.PollResults: - closePollResults(); + closePollResults({ noPushState }); break; } }, [ @@ -154,11 +169,32 @@ const RightColumn: FC = ({ managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery, ]); + const handleClose = useCallback(() => { + close(false); + }, [close]); + + useHistoryBack((event, noAnimation, previousHistoryState) => { + if (previousHistoryState && previousHistoryState.type === 'right') { + if (noAnimation) { + setShouldSkipTransition(true); + setTimeout(() => setShouldSkipTransition(false), COLUMN_CLOSE_DELAY_MS); + } + close(true); + } + }); + const handleSelectChatMember = useCallback((memberId, isPromoted) => { setSelectedChatMemberId(memberId); setIsPromotedByCurrentUser(isPromoted); }, []); + const handleManagementScreenSelect = useCallback((screen: ManagementScreens) => { + if (screen !== managementScreen) { + HistoryWrapper.pushState({ type: 'right', contentKey }); + setManagementScreen(screen); + } + }, [contentKey, managementScreen]); + useEffect(() => (isOpen ? captureEscKeyListener(close) : undefined), [isOpen, close]); useEffect(() => { @@ -214,10 +250,11 @@ const RightColumn: FC = ({ currentScreen={managementScreen} isPromotedByCurrentUser={isPromotedByCurrentUser} selectedChatMemberId={selectedChatMemberId} - onScreenSelect={setManagementScreen} + onScreenSelect={handleManagementScreenSelect} onChatMemberSelect={handleSelectChatMember} /> ); + case RightColumnContent.StickerSearch: return ; case RightColumnContent.GifSearch: @@ -247,7 +284,8 @@ const RightColumn: FC = ({ isPollResults={isPollResults} profileState={profileState} managementScreen={managementScreen} - onClose={close} + onClose={handleClose} + shouldSkipAnimation={shouldSkipTransition} /> void; @@ -103,6 +104,7 @@ const RightHeader: FC = ({ searchTextMessagesLocal, toggleManagement, searchMessagesByDate, + shouldSkipAnimation, }) => { // eslint-disable-next-line no-null/no-null const backButtonRef = useRef(null); @@ -123,11 +125,11 @@ const RightHeader: FC = ({ }, [closeCalendar, searchMessagesByDate]); const handleStickerSearchQueryChange = useCallback((query: string) => { - setStickerSearchQuery({ query }); + setStickerSearchQuery({ query, noPushState: true }); }, [setStickerSearchQuery]); const handleGifSearchQueryChange = useCallback((query: string) => { - setGifSearchQuery({ query }); + setGifSearchQuery({ query, noPushState: true }); }, [setGifSearchQuery]); const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen); @@ -286,7 +288,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 @@ -307,7 +309,7 @@ const RightHeader: FC = ({
    {renderHeaderContent} 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 2bdce6995..64b5b7f3e 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -20,6 +20,7 @@ type OwnProps = { positionX?: 'left' | 'right'; positionY?: 'top' | 'bottom'; autoClose?: boolean; + shouldSkipTransition?: boolean; footer?: string; noCloseOnBackdrop?: boolean; onKeyDown?: (e: React.KeyboardEvent) => void; @@ -48,6 +49,7 @@ const Menu: FC = ({ onClose, onMouseEnter, onMouseLeave, + shouldSkipTransition, }) => { // eslint-disable-next-line no-null/no-null let menuRef = useRef(null); @@ -56,9 +58,20 @@ 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], + ); useEffectWithPrevDeps(([prevIsOpen]) => { if (prevIsOpen !== undefined) { diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index aadf052f6..e19bf2350 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,5 +1,8 @@ -import React, { FC, useEffect, useRef } from '../../lib/teact/teact'; +import React, { + FC, useEffect, useRef, useState, +} from '../../lib/teact/teact'; +import { HistoryWrapper } from '../../util/history'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import trapFocus from '../../util/trapFocus'; import buildClassName from '../../util/buildClassName'; @@ -7,6 +10,8 @@ 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 useOnChange from '../../hooks/useOnChange'; import Button from './Button'; import Portal from './Portal'; @@ -41,7 +46,13 @@ const Modal: FC = (props) => { onCloseAnimationEnd, onEnter, } = props; - const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd); + const [isClosedWithHistory, setIsClosedWithHistory] = useState(false); + const [noAnimations, setNoAnimations] = useState(false); + const [isFirstRender, setIsFirstRender] = useState(true); + const { + shouldRender, + transitionClassNames, + } = useShowTransition(isOpen, onCloseAnimationEnd, noAnimations, undefined, noAnimations); // eslint-disable-next-line no-null/no-null const modalRef = useRef(null); @@ -50,6 +61,33 @@ const Modal: FC = (props) => { : undefined), [isOpen, onClose, onEnter]); useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]); + useHistoryBack((event, noAnimation, previousHistoryState) => { + if (previousHistoryState && previousHistoryState.type === 'modal') { + setIsClosedWithHistory(true); + if (noAnimation) { + setNoAnimations(true); + setTimeout(() => setNoAnimations(false), ANIMATION_DURATION); + } + onClose(); + } + }); + + useOnChange(() => { + if (isFirstRender) { + setIsFirstRender(false); + return; + } + if (isOpen) { + HistoryWrapper.pushState({ + type: 'modal', + }); + } else if (!isClosedWithHistory) { + HistoryWrapper.back(); + } else { + setIsClosedWithHistory(false); + } + }, [isOpen]); + 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 e7a6eca7e..78f85b429 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -36,7 +36,7 @@ type StateProps = { animationLevel: number; }; -const ANIMATION_DURATION = { +export const ANIMATION_DURATION = { slide: 450, 'slide-reversed': 450, 'mv-slide': 400, diff --git a/src/global/types.ts b/src/global/types.ts index 0cb54493d..7e5d2642f 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -62,6 +62,7 @@ export type GlobalState = { isLeftColumnShown: boolean; isPollModalOpen?: boolean; uiReadyState: 0 | 1 | 2; + shouldSkipUiLoaderTransition?: boolean; connectionState?: ApiUpdateConnectionStateType; currentUserId?: number; lastSyncTime?: number; @@ -394,7 +395,7 @@ export type ActionTypes = ( 'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' | // ui 'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' | - 'toggleSafeLinkModal' | + 'toggleSafeLinkModal' | 'setShouldSkipUiLoaderTransition' | // auth 'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' | 'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'gotToAuthQrCode' | 'clearCache' | diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index f051e99f8..7d3355d7e 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -1,15 +1,69 @@ // This is unsafe and can be not chained as `popstate` event is asynchronous -export default function useHistoryBack(handler: NoneToVoidFunction) { - function handlePopState() { - handler(); +import { useEffect } from '../lib/teact/teact'; +import { IS_IOS } from '../util/environment'; +import { HistoryWrapper } from '../util/history'; + +type HistoryBackFunction = ((event: PopStateEvent, noAnimation: boolean, previousHistoryState: any) => void); + +// 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 = 200; +const SAFARI_EDGE_BACK_GESTURE_DURATION = 200; +let isEdge = false; + +const onTouchStart = (event: TouchEvent) => { + const x = event.touches[0].pageX; + + // eslint-disable-next-line no-console + console.log('starting touch from', x); + + 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 onTouchEnd = () => { + if (isEdge) { + // eslint-disable-next-line no-console + console.log('touchend'); + setTimeout(() => { + // eslint-disable-next-line no-console + console.log('setting isEdge to false'); + isEdge = false; + }, SAFARI_EDGE_BACK_GESTURE_DURATION); + } +}; - return () => { - window.removeEventListener('popstate', handlePopState); - window.history.back(); - }; +if (IS_IOS) { + // eslint-disable-next-line no-console + console.log('Adding event listeners for useHistoryBack'); + window.addEventListener('touchstart', onTouchStart); + window.addEventListener('touchend', onTouchEnd); +} + +export default function useHistoryBack(handler: NoneToVoidFunction | HistoryBackFunction) { + const onPopState = (event: PopStateEvent) => { + // eslint-disable-next-line no-console + console.log('onPopState, isEdge = ', isEdge, 'isHistoryChangedByUser = ', HistoryWrapper.isHistoryChangedByUser); + // Check if the event was caused by History API call or the user + if (!HistoryWrapper.isHistoryChangedByUser) { + // HACK: Handle multiple event listeners. + // onTickChange doesn't work on Safari for some reason + setTimeout(() => { + HistoryWrapper.isHistoryChangedByUser = true; + }, 0); + return; + } + handler(event, isEdge, HistoryWrapper.states[HistoryWrapper.states.length - 1]); + }; + + + useEffect(() => { + window.addEventListener('popstate', onPopState); + + return () => { + window.removeEventListener('popstate', onPopState); + }; + }); } diff --git a/src/hooks/useShowTransition.ts b/src/hooks/useShowTransition.ts index 2c46755d5..01d4d7bcf 100644 --- a/src/hooks/useShowTransition.ts +++ b/src/hooks/useShowTransition.ts @@ -44,7 +44,7 @@ export default ( const transitionClassNames = buildClassName( className && 'opacity-transition', className, - hasOpenClassName && 'open', + ((hasOpenClassName && !noCloseTransition) || (noCloseTransition && isOpen)) && 'open', shouldRender && 'shown', isClosing && 'closing', ); diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index 1b003db8b..96deb8a59 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/api/initial.ts b/src/modules/actions/api/initial.ts index d71341548..aaa97eeb5 100644 --- a/src/modules/actions/api/initial.ts +++ b/src/modules/actions/api/initial.ts @@ -16,6 +16,7 @@ import { import { initApi, callApi } from '../../../api/gramjs'; import { unsubscribe } from '../../../util/notifications'; import * as cacheApi from '../../../util/cacheApi'; +import { HistoryWrapper } from '../../../util/history'; import { updateAppBadge } from '../../../util/appBadge'; addReducer('initApi', (global: GlobalState, actions) => { @@ -102,6 +103,11 @@ addReducer('returnToAuthPhoneNumber', (global) => { addReducer('gotToAuthQrCode', (global) => { void callApi('restartAuthWithQr'); + HistoryWrapper.pushState({ + type: 'login', + contentKey: 'authQr', + }); + return { ...global, authIsLoadingQrCode: true, diff --git a/src/modules/actions/ui/chats.ts b/src/modules/actions/ui/chats.ts index d5831a626..7b8cca628 100644 --- a/src/modules/actions/ui/chats.ts +++ b/src/modules/actions/ui/chats.ts @@ -1,21 +1,10 @@ -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 { selectCurrentMessageList, selectRightColumnContentKey } from '../../selectors'; +import { HistoryWrapper } from '../../../util/history'; addReducer('openChat', (global, actions, payload) => { const { @@ -46,7 +35,16 @@ addReducer('openChat', (global, actions, payload) => { setGlobal(global); if (!noPushState) { - window.history.pushState({ chatId: id, threadId, messageListType: type }, ''); + if (id !== undefined) { + HistoryWrapper.pushState({ + type: 'chat', + chatId: id, + threadId, + messageListType: type, + }); + } else { + HistoryWrapper.back(); + } } } @@ -59,6 +57,11 @@ addReducer('openChatWithInfo', (global, actions, payload) => { isChatInfoShown: true, }); + HistoryWrapper.pushState({ + type: 'right', + contentKey: selectRightColumnContentKey(global), + }); + actions.openChat(payload); }); diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index 030ec893d..a21f79234 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -5,6 +5,7 @@ import { } from '../../../util/environment'; import { setLanguage } from '../../../util/langProvider'; import switchTheme from '../../../util/switchTheme'; +import { HistoryWrapper } from '../../../util/history'; addReducer('init', (global) => { const { @@ -45,6 +46,11 @@ addReducer('setIsUiReady', (global, actions, payload) => { addReducer('setAuthPhoneNumber', (global, actions, payload) => { const { phoneNumber } = payload!; + HistoryWrapper.pushState({ + type: 'login', + contentKey: 'authCode', + }); + return { ...global, authPhoneNumber: phoneNumber, @@ -64,3 +70,12 @@ addReducer('clearAuthError', (global) => { authError: undefined, }; }); + +addReducer('setShouldSkipUiLoaderTransition', (global, actions, payload) => { + const { shouldSkipUiLoaderTransition } = payload; + + return { + ...global, + shouldSkipUiLoaderTransition, + }; +}); diff --git a/src/modules/actions/ui/localSearch.ts b/src/modules/actions/ui/localSearch.ts index 3d83e45a2..504a201ae 100644 --- a/src/modules/actions/ui/localSearch.ts +++ b/src/modules/actions/ui/localSearch.ts @@ -8,6 +8,8 @@ import { import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { selectCurrentMessageList } from '../../selectors'; import { buildChatThreadKey } from '../../helpers'; +import { HistoryWrapper } from '../../../util/history'; +import { RightColumnContent } from '../../../types'; addReducer('openLocalTextSearch', (global) => { const { chatId, threadId } = selectCurrentMessageList(global) || {}; @@ -15,15 +17,25 @@ addReducer('openLocalTextSearch', (global) => { return undefined; } + HistoryWrapper.pushState({ + type: 'right', + contentKey: RightColumnContent.Search, + }); + return updateLocalTextSearch(global, chatId, threadId, true); }); -addReducer('closeLocalTextSearch', (global) => { +addReducer('closeLocalTextSearch', (global, actions, payload) => { + const { noPushState } = payload; const { chatId, threadId } = selectCurrentMessageList(global) || {}; if (!chatId || !threadId) { return undefined; } + if (!noPushState) { + HistoryWrapper.back(); + } + global = updateLocalTextSearch(global, chatId, threadId, false); global = replaceLocalTextSearchResults(global, chatId, threadId, undefined); return global; diff --git a/src/modules/actions/ui/messages.ts b/src/modules/actions/ui/messages.ts index 9e882ee32..2216c8b4f 100644 --- a/src/modules/actions/ui/messages.ts +++ b/src/modules/actions/ui/messages.ts @@ -1,7 +1,7 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; import { MAIN_THREAD_ID } from '../../../api/types'; -import { FocusDirection } from '../../../types'; +import { FocusDirection, RightColumnContent } from '../../../types'; import { enterMessageSelectMode, @@ -24,6 +24,7 @@ import { selectForwardedMessageIdsByGroupId, selectIsViewportNewest, selectReplyingToId, } from '../../selectors'; import { findLast } from '../../../util/iteratees'; +import { HistoryWrapper } from '../../../util/history'; const FOCUS_DURATION = 2000; const POLL_RESULT_OPEN_DELAY_MS = 450; @@ -174,11 +175,18 @@ addReducer('closeAudioPlayer', (global) => { }); addReducer('openPollResults', (global, actions, payload) => { - const { chatId, messageId } = payload!; + const { chatId, messageId, noPushState } = payload!; const shouldOpenInstantly = selectIsRightColumnShown(global); - if (!shouldOpenInstantly) { + if (!noPushState) { + HistoryWrapper.pushState({ + type: 'right', + contentKey: RightColumnContent.PollResults, + }); + } + + if (shouldOpenInstantly) { window.setTimeout(() => { const newGlobal = getGlobal(); @@ -203,7 +211,13 @@ addReducer('openPollResults', (global, actions, payload) => { } }); -addReducer('closePollResults', (global) => { +addReducer('closePollResults', (global, actions, payload) => { + const { noPushState } = payload; + + if (!noPushState) { + HistoryWrapper.back(); + } + setGlobal({ ...global, pollResults: {}, diff --git a/src/modules/actions/ui/misc.ts b/src/modules/actions/ui/misc.ts index 5f1073151..de30c0023 100644 --- a/src/modules/actions/ui/misc.ts +++ b/src/modules/actions/ui/misc.ts @@ -4,24 +4,52 @@ import { GlobalState } from '../../../global/types'; import { IS_MOBILE_SCREEN } from '../../../util/environment'; import getReadableErrorText from '../../../util/getReadableErrorText'; -import { selectCurrentMessageList } from '../../selectors'; +import { selectCurrentMessageList, selectRightColumnContentKey } from '../../selectors'; +import { HistoryWrapper } from '../../../util/history'; const MAX_STORED_EMOJIS = 18; // Represents two rows of recent emojis -addReducer('toggleChatInfo', (global) => { +addReducer('toggleChatInfo', (global, actions, payload) => { + const { noPushState } = payload; + + if (!noPushState) { + if (global.isChatInfoShown) { + HistoryWrapper.back(); + } else { + HistoryWrapper.pushState({ + type: 'right', + contentKey: selectRightColumnContentKey(global), + }); + } + } + return { ...global, isChatInfoShown: !global.isChatInfoShown, }; }); -addReducer('toggleManagement', (global): GlobalState | undefined => { +addReducer('toggleManagement', (global, actions, payload): GlobalState | undefined => { const { chatId } = selectCurrentMessageList(global) || {}; + const { noPushState } = payload; if (!chatId) { return undefined; } + const { isActive: prevIsActive } = global.management.byChatId[chatId] || {}; + + if (!noPushState) { + if (prevIsActive) { + HistoryWrapper.back(); + } else { + HistoryWrapper.pushState({ + type: 'right', + contentKey: selectRightColumnContentKey(global), + }); + } + } + return { ...global, management: { @@ -29,7 +57,7 @@ addReducer('toggleManagement', (global): GlobalState | undefined => { ...global.management.byChatId, [chatId]: { ...global.management.byChatId[chatId], - isActive: !(global.management.byChatId[chatId] || {}).isActive, + isActive: !prevIsActive, }, }, }, diff --git a/src/modules/actions/ui/stickerSearch.ts b/src/modules/actions/ui/stickerSearch.ts index 203d91302..747538689 100644 --- a/src/modules/actions/ui/stickerSearch.ts +++ b/src/modules/actions/ui/stickerSearch.ts @@ -1,7 +1,21 @@ import { addReducer } from '../../../lib/teact/teactn'; +import { HistoryWrapper } from '../../../util/history'; +import { RightColumnContent } from '../../../types'; addReducer('setStickerSearchQuery', (global, actions, payload) => { - const { query } = payload!; + const { query, noPushState } = payload!; + const previousQuery = global.stickers.search.query; + + if (!noPushState && previousQuery !== query) { + if (query !== undefined && previousQuery === undefined) { + HistoryWrapper.pushState({ + type: 'right', + contentKey: RightColumnContent.StickerSearch, + }); + } else { + HistoryWrapper.back(); + } + } return { ...global, @@ -16,7 +30,19 @@ addReducer('setStickerSearchQuery', (global, actions, payload) => { }); addReducer('setGifSearchQuery', (global, actions, payload) => { - const { query } = payload!; + const { query, noPushState } = payload!; + const previousQuery = global.gifs.search.query; + + if (!noPushState && previousQuery !== query) { + if (query !== undefined) { + HistoryWrapper.pushState({ + type: 'right', + contentKey: RightColumnContent.GifSearch, + }); + } else { + HistoryWrapper.back(); + } + } 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; diff --git a/src/util/history.ts b/src/util/history.ts new file mode 100644 index 000000000..162a397b4 --- /dev/null +++ b/src/util/history.ts @@ -0,0 +1,20 @@ + +export const HistoryWrapper: { + pushState(data: any): void; + back(): void; + states: any[]; + isHistoryChangedByUser: boolean; +} = { + states: [], + isHistoryChangedByUser: true, + pushState(data: any) { + this.states.push(data); + + window.history.pushState(data, ''); + }, + back() { + this.isHistoryChangedByUser = false; + window.history.back(); + this.states.pop(); + }, +};