Full browser history support (#1181)

This commit is contained in:
Alexander Zinchuk 2021-07-13 17:31:30 +03:00
parent c4e3a41ff1
commit 0a594a84e1
82 changed files with 1255 additions and 185 deletions

View File

@ -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<GlobalState, 'authState'>;
type DispatchProps = Pick<GlobalActions, 'reset' | 'initApi'>;
type DispatchProps = Pick<GlobalActions, 'reset' | 'initApi' | 'returnToAuthPhoneNumber' | 'goToAuthQrCode'>;
const Auth: FC<StateProps & DispatchProps> = ({ authState, reset, initApi }) => {
const Auth: FC<StateProps & DispatchProps> = ({
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 <UiLoader page="authCode" key="authCode"><AuthCode /></UiLoader>;
@ -37,7 +55,7 @@ const Auth: FC<StateProps & DispatchProps> = ({ authState, reset, initApi }) =>
case 'authorizationStateWaitQrCode':
return <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
default:
return PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android'
return isMobile
? <UiLoader page="authPhoneNumber" key="authPhoneNumber"><AuthPhoneNumber /></UiLoader>
: <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
}
@ -45,5 +63,5 @@ const Auth: FC<StateProps & DispatchProps> = ({ 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));

View File

@ -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<GlobalState, 'authPhoneNumber' | 'authIsCodeViaApp' | 'authIsLoading' | 'authError'>;
type DispatchProps = Pick<GlobalActions, 'setAuthCode' | 'returnToAuthPhoneNumber' | 'clearAuthError'>;
type DispatchProps = Pick<GlobalActions, (
'setAuthCode' | 'returnToAuthPhoneNumber' | 'clearAuthError'
)>;
const CODE_LENGTH = 5;
const AuthCode: FC<StateProps & DispatchProps> = ({
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<HTMLInputElement>(null);
@ -34,7 +42,7 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
}
}, []);
useHistoryBack(returnToAuthPhoneNumber);
useHistoryBack(true, returnToAuthPhoneNumber);
const onCodeChange = useCallback((e: FormEvent<HTMLInputElement>) => {
if (authError) {
@ -119,5 +127,9 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
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));

View File

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

View File

@ -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<GlobalState, 'connectionState' | 'authState' | 'authQrCode'>;
type DispatchProps = Pick<GlobalActions, 'returnToAuthPhoneNumber'>;
@ -17,7 +16,10 @@ type DispatchProps = Pick<GlobalActions, 'returnToAuthPhoneNumber'>;
const DATA_PREFIX = 'tg://login?token=';
const AuthCode: FC<StateProps & DispatchProps> = ({
connectionState, authState, authQrCode, returnToAuthPhoneNumber,
connectionState,
authState,
authQrCode,
returnToAuthPhoneNumber,
}) => {
// eslint-disable-next-line no-null/no-null
const qrCodeRef = useRef<HTMLDivElement>(null);
@ -41,8 +43,6 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
}, container);
}, [connectionState, authQrCode]);
useHistoryBack(returnToAuthPhoneNumber);
const isAuthReady = authState === 'authorizationStateWaitQrCode';
return (

View File

@ -28,7 +28,7 @@ type OwnProps = {
children: any;
};
type StateProps = Pick<GlobalState, 'uiReadyState'> & {
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
hasCustomBackground?: boolean;
hasCustomBackgroundColor: boolean;
isRightColumnShown?: boolean;
@ -82,6 +82,7 @@ const UiLoader: FC<OwnProps & StateProps & DispatchProps> = ({
hasCustomBackground,
hasCustomBackgroundColor,
isRightColumnShown,
shouldSkipHistoryAnimations,
setIsUiReady,
}) => {
const [isReady, markReady] = useFlag();
@ -126,7 +127,7 @@ const UiLoader: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<div id="UiLoader">
{children}
{shouldRenderMask && (
{shouldRenderMask && !shouldSkipHistoryAnimations && (
<div className={buildClassName('mask', transitionClassNames)}>
{page === 'main' ? (
<>
@ -156,6 +157,7 @@ export default withGlobal<OwnProps>(
const { background, backgroundColor } = global.settings.themes[theme] || {};
return {
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
uiReadyState: global.uiReadyState,
hasCustomBackground: Boolean(background),
hasCustomBackgroundColor: Boolean(backgroundColor),

View File

@ -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<OwnProps> = ({ isActive, onReset }) => {
const ArchivedChats: FC<OwnProps> = ({ isActive, onReset, onContentChange }) => {
const lang = useLang();
useHistoryBack(isActive, onReset, onContentChange, LeftColumnContent.Archived);
return (
<div className="ArchivedChats">
<div className="left-header">

View File

@ -22,6 +22,7 @@ type StateProps = {
searchQuery?: string;
searchDate?: number;
activeChatFolder: number;
shouldSkipHistoryAnimations?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
@ -47,6 +48,7 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
searchQuery,
searchDate,
activeChatFolder,
shouldSkipHistoryAnimations,
setGlobalSearchQuery,
setGlobalSearchChatId,
resetChatCreation,
@ -80,14 +82,20 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
}
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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
if (query !== searchQuery) {
setGlobalSearchQuery({ query });
}
}, [content, setGlobalSearchQuery, searchQuery]);
}, [content, searchQuery, setGlobalSearchQuery]);
useEffect(
() => (content !== LeftColumnContent.ChatList || activeChatFolder === 0
@ -237,10 +245,15 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
}
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
const handleSettingsScreenSelect = (screen: SettingsScreens) => {
setContent(LeftColumnContent.Settings);
setSettingsScreen(screen);
};
return (
<Transition
id="LeftColumn"
name={LAYERS_ANIMATION_NAME}
name={shouldSkipHistoryAnimations ? 'none' : LAYERS_ANIMATION_NAME}
renderCount={RENDER_COUNT}
activeKey={contentType}
shouldCleanup
@ -253,20 +266,24 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
<ArchivedChats
isActive={isActive}
onReset={handleReset}
onContentChange={setContent}
/>
);
case ContentType.Settings:
return (
<Settings
isActive={isActive}
currentScreen={settingsScreen}
onScreenSelect={setSettingsScreen}
onScreenSelect={handleSettingsScreenSelect}
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
/>
);
case ContentType.NewChannel:
return (
<NewChat
key={lastResetTime}
isActive={isActive}
isChannel
content={content}
onContentChange={setContent}
@ -277,6 +294,7 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
return (
<NewChat
key={lastResetTime}
isActive={isActive}
content={content}
onContentChange={setContent}
onReset={handleReset}
@ -292,6 +310,7 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
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',

View File

@ -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<StateProps & DispatchProps> = ({
}
}) : undefined), [activeChatFolder, setActiveChatFolder]);
useHistoryBack(activeChatFolder !== 0, () => setActiveChatFolder(0));
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.metaKey && e.code.startsWith('Digit') && folderTabs) {

View File

@ -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<GlobalActions, 'loadContactList' | 'openChat'>;
const runThrottled = throttle((cb) => cb(), 60000, true);
const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
});
});
useHistoryBack(isActive, onReset);
const handleClick = useCallback(
(id: number) => {
openChat({ id });

View File

@ -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<OwnProps & StateProps> = ({
searchQuery,
searchDate,
contactsFilter,
shouldSkipTransition,
onSearchQuery,
onContentChange,
onReset,
@ -140,12 +142,13 @@ const LeftMain: FC<OwnProps & StateProps> = ({
onSelectContacts={handleSelectContacts}
onSelectArchived={handleSelectArchived}
onReset={onReset}
shouldSkipTransition={shouldSkipTransition}
/>
<ShowTransition isOpen={isConnecting} isCustom className="connection-state-wrapper opacity-transition slow">
{() => <ConnectionState />}
</ShowTransition>
<Transition
name="zoom-fade"
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
renderCount={TRANSITION_RENDER_COUNT}
activeKey={content}
shouldCleanup
@ -166,7 +169,7 @@ const LeftMain: FC<OwnProps & StateProps> = ({
/>
);
case LeftColumnContent.Contacts:
return <ContactList filter={contactsFilter} />;
return <ContactList filter={contactsFilter} isActive={isActive} onReset={onReset} />;
default:
return undefined;
}

View File

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

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
onReset,
searchQuery,
isLoading,
shouldSkipTransition,
currentUserId,
globalSearchChatId,
searchDate,
@ -118,10 +120,15 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
onClick={hasMenu ? onTrigger : () => onReset()}
ariaLabel={hasMenu ? lang('AccDescrOpenMenu2') : 'Return to chat list'}
>
<div className={buildClassName('animated-menu-icon', !hasMenu && 'state-back')} />
<div className={buildClassName(
'animated-menu-icon',
!hasMenu && 'state-back',
shouldSkipTransition && 'no-animation',
)}
/>
</Button>
);
}, [hasMenu, lang, onReset]);
}, [hasMenu, lang, onReset, shouldSkipTransition]);
const handleSearchFocus = useCallback(() => {
if (!searchQuery) {

View File

@ -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<OwnProps> = ({
isActive,
isChannel = false,
content,
onContentChange,
@ -40,13 +42,14 @@ const NewChat: FC<OwnProps> = ({
renderCount={RENDER_COUNT}
activeKey={content}
>
{() => {
{(isStepActive) => {
switch (content) {
case LeftColumnContent.NewChannelStep1:
case LeftColumnContent.NewGroupStep1:
return (
<NewChatStep1
isChannel={isChannel}
isActive={isActive}
selectedMemberIds={newChatMemberIds}
onSelectedMemberIdsChange={setNewChatMemberIds}
onNextStep={handleNextStep}
@ -58,6 +61,7 @@ const NewChat: FC<OwnProps> = ({
return (
<NewChatStep2
isChannel={isChannel}
isActive={isStepActive && isActive}
memberIds={newChatMemberIds}
onReset={onReset}
/>

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isChannel,
isActive,
selectedMemberIds,
onSelectedMemberIdsChange,
onNextStep,
@ -64,6 +67,10 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
});
});
const lang = useLang();
useHistoryBack(isActive, onReset);
const handleFilterChange = useCallback((query: string) => {
setGlobalSearchQuery({ query });
}, [setGlobalSearchQuery]);
@ -108,8 +115,6 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [selectedMemberIds.length, isChannel, setGlobalSearchQuery, onNextStep]);
const lang = useLang();
return (
<div className="NewChat step-1">
<div className="left-header">

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isChannel,
isActive,
memberIds,
onReset,
creationProgress,
@ -44,6 +47,8 @@ const NewChatStep2: FC<OwnProps & StateProps & DispatchProps> = ({
}) => {
const lang = useLang();
useHistoryBack(isActive, onReset);
const [title, setTitle] = useState('');
const [about, setAbout] = useState('');
const [photo, setPhoto] = useState<File | undefined>();

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
setGlobalSearchDate({ date: value.getTime() / 1000 });
}, [setGlobalSearchDate]);
useHistoryBack(isActive, onReset, undefined, undefined, true);
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const handleKeyDown = useKeyboardListNavigation(containerRef, isActive, undefined, '.ListItem-button', true);

View File

@ -27,6 +27,7 @@
}
.settings-content {
background: var(--color-background);
height: calc(100% - var(--header-height));
overflow-y: auto;

View File

@ -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<OwnProps> = ({
isActive,
currentScreen,
onScreenSelect,
onReset,
shouldSkipTransition,
}) => {
const [foldersState, foldersDispatch] = useFoldersReducer();
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
@ -75,47 +136,93 @@ const Settings: FC<OwnProps> = ({
handleReset();
}, [foldersDispatch, handleReset]);
function renderCurrentSectionContent() {
function renderCurrentSectionContent(isScreenActive: boolean, screen: SettingsScreens) {
const privacyAllowScreens: Record<number, boolean> = {
[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 (
<SettingsMain onScreenSelect={onScreenSelect} />
<SettingsMain onScreenSelect={onScreenSelect} isActive={isActive} onReset={handleReset} />
);
case SettingsScreens.EditProfile:
return (
<SettingsEditProfile />
<SettingsEditProfile
onScreenSelect={onScreenSelect}
isActive={isActive && isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.General:
return (
<SettingsGeneral onScreenSelect={onScreenSelect} />
<SettingsGeneral
onScreenSelect={onScreenSelect}
isActive={isScreenActive
|| screen === SettingsScreens.GeneralChatBackgroundColor
|| screen === SettingsScreens.GeneralChatBackground
|| isPrivacyScreen || isFoldersScreen}
onReset={handleReset}
/>
);
case SettingsScreens.Notifications:
return (
<SettingsNotifications />
<SettingsNotifications onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
);
case SettingsScreens.Privacy:
return (
<SettingsPrivacy onScreenSelect={onScreenSelect} />
<SettingsPrivacy
onScreenSelect={onScreenSelect}
isActive={isScreenActive || isPrivacyScreen || isTwoFaScreen}
onReset={handleReset}
/>
);
case SettingsScreens.Language:
return (
<SettingsLanguage />
<SettingsLanguage onScreenSelect={onScreenSelect} isActive={isScreenActive} onReset={handleReset} />
);
case SettingsScreens.GeneralChatBackground:
return (
<SettingsGeneralBackground onScreenSelect={onScreenSelect} />
<SettingsGeneralBackground
onScreenSelect={onScreenSelect}
isActive={isScreenActive || screen === SettingsScreens.GeneralChatBackgroundColor}
onReset={handleReset}
/>
);
case SettingsScreens.GeneralChatBackgroundColor:
return (
<SettingsGeneralBackgroundColor onScreenSelect={onScreenSelect} />
<SettingsGeneralBackgroundColor
onScreenSelect={onScreenSelect}
isActive={isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.PrivacyActiveSessions:
return (
<SettingsPrivacyActiveSessions />
<SettingsPrivacyActiveSessions
onScreenSelect={onScreenSelect}
isActive={isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.PrivacyBlockedUsers:
return (
<SettingsPrivacyBlockedUsers />
<SettingsPrivacyBlockedUsers
onScreenSelect={onScreenSelect}
isActive={isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.PrivacyPhoneNumber:
case SettingsScreens.PrivacyLastSeen:
@ -123,7 +230,12 @@ const Settings: FC<OwnProps> = ({
case SettingsScreens.PrivacyForwarding:
case SettingsScreens.PrivacyGroupChats:
return (
<SettingsPrivacyVisibility screen={currentScreen} onScreenSelect={onScreenSelect} />
<SettingsPrivacyVisibility
screen={currentScreen}
onScreenSelect={onScreenSelect}
isActive={isScreenActive || privacyAllowScreens[currentScreen]}
onReset={handleReset}
/>
);
case SettingsScreens.PrivacyPhoneNumberAllowedContacts:
@ -136,6 +248,8 @@ const Settings: FC<OwnProps> = ({
isAllowList
screen={currentScreen}
onScreenSelect={onScreenSelect}
isActive={isScreenActive || privacyAllowScreens[currentScreen]}
onReset={handleReset}
/>
);
@ -148,6 +262,8 @@ const Settings: FC<OwnProps> = ({
<SettingsPrivacyVisibilityExceptionList
screen={currentScreen}
onScreenSelect={onScreenSelect}
isActive={isScreenActive}
onReset={handleReset}
/>
);
@ -159,8 +275,10 @@ const Settings: FC<OwnProps> = ({
return (
<SettingsFolders
currentScreen={currentScreen}
shownScreen={screen}
state={foldersState}
dispatch={foldersDispatch}
isActive={isScreenActive}
onScreenSelect={onScreenSelect}
onReset={handleReset}
/>
@ -187,7 +305,10 @@ const Settings: FC<OwnProps> = ({
currentScreen={currentScreen}
state={twoFaState}
dispatch={twoFaDispatch}
shownScreen={screen}
isActive={isScreenActive}
onScreenSelect={onScreenSelect}
onReset={handleReset}
/>
);
@ -196,7 +317,7 @@ const Settings: FC<OwnProps> = ({
}
}
function renderCurrentSection() {
function renderCurrentSection(isScreenActive: boolean, isFrom: boolean, currentKey: SettingsScreens) {
return (
<>
<SettingsHeader
@ -205,7 +326,7 @@ const Settings: FC<OwnProps> = ({
onSaveFilter={handleSaveFilter}
editedFolderId={foldersState.folderId}
/>
{renderCurrentSectionContent()}
{renderCurrentSectionContent(isScreenActive, currentKey)}
</>
);
}
@ -213,7 +334,7 @@ const Settings: FC<OwnProps> = ({
return (
<Transition
id="Settings"
name={LAYERS_ANIMATION_NAME}
name={shouldSkipTransition ? 'none' : LAYERS_ANIMATION_NAME}
activeKey={currentScreen}
renderCount={TRANSITION_RENDER_COUNT}
>

View File

@ -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<StateProps & DispatchProps> = ({
const SettingsEditProfile: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
currentAvatarHash,
currentFirstName,
currentLastName,
@ -55,6 +65,8 @@ const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
updateProfile,
checkUsername,
}) => {
const lang = useLang();
const [isUsernameTouched, setIsUsernameTouched] = useState(false);
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
const [error, setError] = useState<string | undefined>();
@ -78,6 +90,8 @@ const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
updateProfile,
]);
const lang = useLang();
return (
<div className="settings-fab-wrapper">
<div className="settings-content custom-scroll">
@ -242,7 +254,7 @@ const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
);
};
export default memo(withGlobal(
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { currentUserId } = global;
const { progress, isUsernameAvailable } = global.profileEdit || {};

View File

@ -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<ISettings, (
@ -52,7 +55,9 @@ const ANIMATION_LEVEL_OPTIONS = [
];
const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
stickerSetIds,
stickerSetsById,
messageTextSize,
@ -120,6 +125,8 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
return stickerSetsById && stickerSetsById[id] && stickerSetsById[id].installedDate ? stickerSetsById[id] : false;
}).filter<ApiStickerSet>(Boolean as any);
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.General);
return (
<div className="settings-content custom-scroll">
<div className="settings-item pt-3">

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
background,
isBlurred,
loadedWallpapers,
@ -106,6 +111,8 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackground);
const isUploading = loadedWallpapers && loadedWallpapers[0] && loadedWallpapers[0].slug === UPLOADING_WALLPAPER_SLUG;
return (

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
theme,
backgroundColor,
setThemeSettings,
@ -195,6 +201,8 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps & DispatchProps> = ({
isDragging && 'is-dragging',
);
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.GeneralChatBackgroundColor);
return (
<div ref={containerRef} className={className}>
<div className="settings-item pt-3">

View File

@ -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<ISettings, 'languages' | 'language'>;
type DispatchProps = Pick<GlobalActions, 'loadLanguages' | 'setSettingOption'>;
const SettingsLanguage: FC<StateProps & DispatchProps> = ({
const SettingsLanguage: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
languages,
language,
loadLanguages,
@ -47,6 +57,8 @@ const SettingsLanguage: FC<StateProps & DispatchProps> = ({
return languages ? buildOptions(languages) : undefined;
}, [languages]);
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Language);
return (
<div className="settings-content settings-item settings-language custom-scroll">
{options ? (
@ -77,7 +89,7 @@ function buildOptions(languages: ApiLanguage[]) {
});
}
export default memo(withGlobal(
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
languages: global.settings.byKey.languages,

View File

@ -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<OwnProps & StateProps> = ({
isActive,
onScreenSelect,
onReset,
currentUser,
}) => {
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main);
return (
<div className="settings-content custom-scroll">
<div className="settings-main-menu">

View File

@ -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<GlobalActions, (
'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings'
)>;
const SettingsNotifications: FC<StateProps & DispatchProps> = ({
const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
hasPrivateChatsNotifications,
hasPrivateChatsMessagePreview,
hasGroupNotifications,
@ -73,6 +84,8 @@ const SettingsNotifications: FC<StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Notifications);
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
@ -145,7 +158,7 @@ const SettingsNotifications: FC<StateProps & DispatchProps> = ({
);
};
export default memo(withGlobal((global): StateProps => {
export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
hasPrivateChatsNotifications: Boolean(global.settings.byKey.hasPrivateChatsNotifications),
hasPrivateChatsMessagePreview: Boolean(global.settings.byKey.hasPrivateChatsMessagePreview),

View File

@ -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<GlobalActions, (
)>;
const SettingsPrivacy: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
hasPassword,
blockedCount,
sessionsCount,
@ -58,6 +63,8 @@ const SettingsPrivacy: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Privacy);
function getVisibilityValue(visibility?: PrivacyVisibility) {
switch (visibility) {
case 'everybody':

View File

@ -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<GlobalActions, (
'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations'
)>;
const SettingsPrivacyActiveSessions: FC<StateProps & DispatchProps> = ({
const SettingsPrivacyActiveSessions: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
activeSessions,
loadAuthorizations,
terminateAuthorization,
@ -52,6 +63,8 @@ const SettingsPrivacyActiveSessions: FC<StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.PrivacyActiveSessions);
function renderCurrentSession(session: ApiSession) {
return (
<div className="settings-item">
@ -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<OwnProps>(
(global): StateProps => {
return {
activeSessions: global.activeSessions,

View File

@ -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<number, ApiChat>;
usersByIds: Record<number, ApiUser>;
@ -29,7 +37,10 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, 'unblockContact'>;
const SettingsPrivacyBlockedUsers: FC<StateProps & DispatchProps> = ({
const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
chatsByIds,
usersByIds,
blockedIds,
@ -41,6 +52,8 @@ const SettingsPrivacyBlockedUsers: FC<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
};
export default memo(withGlobal(
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chats: {

View File

@ -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<ApiPrivacySettings> & {
@ -28,7 +31,9 @@ type DispatchProps = Pick<GlobalActions, 'setPrivacyVisibility'>;
const SettingsPrivacyVisibility: FC<OwnProps & StateProps & DispatchProps> = ({
screen,
isActive,
onScreenSelect,
onReset,
visibility,
allowUserIds,
allowChatIds,
@ -81,6 +86,8 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [lang, screen]);
useHistoryBack(isActive, onReset, onScreenSelect, screen);
const descriptionText = useMemo(() => {
switch (screen) {
case SettingsScreens.PrivacyLastSeen:

View File

@ -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<OwnProps & StateProps & Dispatc
archivedListIds,
archivedPinnedIds,
setPrivacySettings,
isActive,
onScreenSelect,
onReset,
}) => {
const lang = useLang();
@ -122,6 +127,9 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps & Dispatc
onScreenSelect(SettingsScreens.Privacy);
}, [isAllowList, newSelectedContactIds, onScreenSelect, screen, setPrivacySettings]);
useHistoryBack(isActive, onReset, onScreenSelect, screen);
return (
<div className="NewChat-inner step-1">
<Picker

View File

@ -3,7 +3,7 @@ import React, { FC, memo, useCallback } from '../../../../lib/teact/teact';
import { ApiChatFolder } from '../../../../api/types';
import { SettingsScreens } from '../../../../types';
import { FoldersState, FolderEditDispatch } from '../../../../hooks/reducers/useFoldersReducer';
import { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
import SettingsFoldersMain from './SettingsFoldersMain';
import SettingsFoldersEdit from './SettingsFoldersEdit';
@ -15,16 +15,20 @@ const TRANSITION_DURATION = 200;
export type OwnProps = {
currentScreen: SettingsScreens;
shownScreen: SettingsScreens;
state: FoldersState;
dispatch: FolderEditDispatch;
isActive?: boolean;
onScreenSelect: (screen: SettingsScreens) => void;
onReset: () => void;
};
const SettingsFolders: FC<OwnProps> = ({
currentScreen,
shownScreen,
state,
dispatch,
isActive,
onScreenSelect,
onReset,
}) => {
@ -82,6 +86,14 @@ const SettingsFolders: FC<OwnProps> = ({
<SettingsFoldersMain
onCreateFolder={handleCreateFolder}
onEditFolder={handleEditFolder}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.FoldersCreateFolder,
SettingsScreens.FoldersEditFolder,
SettingsScreens.FoldersIncludedChats,
SettingsScreens.FoldersExcludedChats,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.FoldersCreateFolder:
@ -93,6 +105,12 @@ const SettingsFolders: FC<OwnProps> = ({
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<OwnProps> = ({
mode="included"
state={state}
dispatch={dispatch}
onReset={handleReset}
onScreenSelect={onScreenSelect}
isActive={isActive}
/>
);
case SettingsScreens.FoldersExcludedChats:
@ -109,6 +130,9 @@ const SettingsFolders: FC<OwnProps> = ({
mode="excluded"
state={state}
dispatch={dispatch}
onReset={handleReset}
onScreenSelect={onScreenSelect}
isActive={isActive}
/>
);

View File

@ -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<GlobalActions, 'loadMoreChats'>;
const SettingsFoldersChatFilters: FC<OwnProps & StateProps & DispatchProps> = ({
isActive,
onScreenSelect,
onReset,
mode,
state,
dispatch,
@ -132,6 +140,9 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [mode, selectedChatIds, dispatch]);
useHistoryBack(isActive, onReset, onScreenSelect,
mode === 'included' ? SettingsScreens.FoldersIncludedChats : SettingsScreens.FoldersExcludedChats);
if (!displayedIds) {
return <Loading />;
}

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
dispatch,
onAddIncludedChats,
onAddExcludedChats,
isActive,
onScreenSelect,
onReset,
onBack,
loadedActiveChatIds,
loadedArchivedChatIds,
editChatFolder,
@ -128,6 +136,10 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onBack, onScreenSelect, state.mode === 'edit'
? SettingsScreens.FoldersEditFolder
: SettingsScreens.FoldersCreateFolder);
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { currentTarget } = event;
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
onCreateFolder,
onEditFolder,
isActive,
onScreenSelect,
onReset,
chatsById,
usersById,
orderedFolderIds,
@ -90,6 +97,8 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Folders);
const userFolders = useMemo(() => {
if (!orderedFolderIds) {
return undefined;

View File

@ -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<GlobalActions, (
const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
currentScreen,
shownScreen,
state,
hint,
isLoading,
error,
waitingEmailCodeLength,
dispatch,
isActive,
onScreenSelect,
onReset,
updatePassword,
checkPassword,
clearTwoFaError,
@ -158,25 +164,54 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<SettingsTwoFaStart
onStart={handleStartWizard}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaNewPassword,
SettingsScreens.TwoFaNewPasswordConfirm,
SettingsScreens.TwoFaNewPasswordHint,
SettingsScreens.TwoFaNewPasswordEmail,
SettingsScreens.TwoFaNewPasswordEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaNewPassword:
return (
<SettingsTwoFaPassword
screen={currentScreen}
placeholder={lang('EnterPassword')}
submitLabel={lang('Continue')}
onSubmit={handleNewPassword}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaNewPasswordConfirm,
SettingsScreens.TwoFaNewPasswordHint,
SettingsScreens.TwoFaNewPasswordEmail,
SettingsScreens.TwoFaNewPasswordEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaNewPasswordConfirm:
return (
<SettingsTwoFaPassword
screen={currentScreen}
expectedPassword={state.password}
placeholder={lang('PleaseReEnterPassword')}
submitLabel={lang('Continue')}
onSubmit={handleNewPasswordConfirm}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaNewPasswordHint,
SettingsScreens.TwoFaNewPasswordEmail,
SettingsScreens.TwoFaNewPasswordEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
@ -186,6 +221,14 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
error={error}
clearError={clearTwoFaError}
onSubmit={handleEmailCode}
screen={currentScreen}
onScreenSelect={onScreenSelect}
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
onReset={onReset}
/>
);
@ -217,6 +271,8 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<SettingsTwoFaCongratulations
onScreenSelect={onScreenSelect}
isActive={isActive}
onReset={onReset}
/>
);
@ -224,34 +280,70 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<SettingsTwoFaEnabled
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordCurrent,
SettingsScreens.TwoFaChangePasswordNew,
SettingsScreens.TwoFaChangePasswordConfirm,
SettingsScreens.TwoFaChangePasswordHint,
SettingsScreens.TwoFaTurnOff,
SettingsScreens.TwoFaRecoveryEmailCurrentPassword,
SettingsScreens.TwoFaRecoveryEmail,
SettingsScreens.TwoFaRecoveryEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaChangePasswordCurrent:
return (
<SettingsTwoFaPassword
screen={currentScreen}
isLoading={isLoading}
error={error}
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleChangePasswordCurrent}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordNew,
SettingsScreens.TwoFaChangePasswordConfirm,
SettingsScreens.TwoFaChangePasswordHint,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaChangePasswordNew:
return (
<SettingsTwoFaPassword
screen={currentScreen}
placeholder={lang('PleaseEnterNewFirstPassword')}
onSubmit={handleChangePasswordNew}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordConfirm,
SettingsScreens.TwoFaChangePasswordHint,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaChangePasswordConfirm:
return (
<SettingsTwoFaPassword
screen={currentScreen}
expectedPassword={state.password}
placeholder={lang('PleaseReEnterPassword')}
onSubmit={handleChangePasswordConfirm}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordHint,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
@ -264,6 +356,10 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleTurnOff}
onScreenSelect={onScreenSelect}
isActive={isActive}
onReset={onReset}
screen={currentScreen}
/>
);
case SettingsScreens.TwoFaRecoveryEmailCurrentPassword:
return (
<SettingsTwoFaPassword
screen={currentScreen}
isLoading={isLoading}
error={error}
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleRecoveryEmailCurrentPassword}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaRecoveryEmail,
SettingsScreens.TwoFaRecoveryEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaRecoveryEmail:
return (
<SettingsTwoFaSkippableForm
screen={currentScreen}
icon="email"
type="email"
placeholder={lang('RecoveryEmailTitle')}
onSubmit={handleRecoveryEmail}
onScreenSelect={onScreenSelect}
isActive={isActive || [
SettingsScreens.TwoFaRecoveryEmailCode,
SettingsScreens.TwoFaCongratulations,
].includes(shownScreen)}
onReset={onReset}
/>
);
case SettingsScreens.TwoFaRecoveryEmailCode:
return (
<SettingsTwoFaEmailCode
screen={currentScreen}
isLoading={isLoading}
error={error}
clearError={clearTwoFaError}
onSubmit={handleEmailCode}
onScreenSelect={onScreenSelect}
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
onReset={onReset}
/>
);

View File

@ -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<OwnProps & StateProps> = ({ animatedEmoji, onScreenSelect }) => {
const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
isActive, onReset, animatedEmoji, onScreenSelect,
}) => {
const lang = useLang();
const handleClick = () => {
onScreenSelect(SettingsScreens.Privacy);
};
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaCongratulations);
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">

View File

@ -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<OwnProps & StateProps> = ({
error,
clearError,
onSubmit,
isActive,
onScreenSelect,
onReset,
screen,
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
@ -50,6 +60,8 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, screen);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (error && clearError) {
clearError();

View File

@ -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<OwnProps & StateProps> = ({ animatedEmoji, onScreenSelect }) => {
const SettingsTwoFaEnabled: FC<OwnProps & StateProps> = ({
isActive, onReset, animatedEmoji, onScreenSelect,
}) => {
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaEnabled);
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">

View File

@ -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<OwnProps> = ({
screen,
isActive,
onScreenSelect,
onReset,
error,
isLoading,
expectedPassword,
@ -50,6 +61,8 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, screen);
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">

View File

@ -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<OwnProps & StateProps> = ({
shouldConfirm,
clearError,
onSubmit,
isActive,
onScreenSelect,
onReset,
screen,
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
@ -86,6 +96,8 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, screen);
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">

View File

@ -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<OwnProps & StateProps> = ({ animatedEmoji, onStart }) => {
const SettingsTwoFaStart: FC<OwnProps & StateProps> = ({
isActive, onScreenSelect, onReset, animatedEmoji, onStart,
}) => {
const lang = useLang();
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.TwoFaDisabled);
return (
<div className="settings-content two-fa custom-scroll">
<div className="settings-content-header">

View File

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

View File

@ -49,6 +49,7 @@ type StateProps = {
audioMessage?: ApiMessage;
safeLinkModalUrl?: string;
isHistoryCalendarOpen: boolean;
shouldSkipHistoryAnimations?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
@ -75,6 +76,7 @@ const Main: FC<StateProps & DispatchProps> = ({
audioMessage,
safeLinkModalUrl,
isHistoryCalendarOpen,
shouldSkipHistoryAnimations,
loadAnimatedEmojis,
loadNotificationSettings,
loadNotificationExceptions,
@ -98,15 +100,17 @@ const Main: FC<StateProps & DispatchProps> = ({
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, [

View File

@ -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<StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isOpen, closeMediaViewer, openMediaViewer, {
chatId,
threadId,
messageId,
origin,
avatarOwnerId: avatarOwner && avatarOwner.id,
});
function renderSlide(isActive: boolean) {
if (isAvatar) {
return (

View File

@ -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<GlobalActions, 'openChat' | 'unpinAllMessages' | 'loadUser'>;
type DispatchProps = Pick<GlobalActions, 'openChat' | 'unpinAllMessages' | 'loadUser' |
'closeLocalTextSearch' | 'exitMessageSelectMode'>;
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
@ -94,6 +100,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
messageListType,
isPrivate,
isPinnedMessageList,
isScheduledMessageList,
canPost,
messageSendingRestrictionReason,
hasPinnedOrAudioMessage,
@ -108,9 +115,13 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
isMobileSearchActive,
isSelectModeActive,
animationLevel,
originChatId,
shouldSkipHistoryAnimations,
openChat,
unpinAllMessages,
loadUser,
closeLocalTextSearch,
exitMessageSelectMode,
}) => {
const { width: windowWidth } = useWindowSize();
@ -223,6 +234,31 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
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 (
<div
id="MiddleColumn"
@ -256,7 +292,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
messageListType={renderingMessageListType}
/>
<Transition
name={animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
name={shouldSkipHistoryAnimations ? 'none' : animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
activeKey={renderingMessageListType === 'thread' && renderingThreadId === MAIN_THREAD_ID ? 1 : 2}
shouldCleanup
>
@ -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));

View File

@ -94,6 +94,7 @@ type StateProps = {
lastSyncTime?: number;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
shouldSkipHistoryAnimations?: boolean;
};
type DispatchProps = Pick<GlobalActions, (
@ -123,6 +124,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
lastSyncTime,
notifySettings,
notifyExceptions,
shouldSkipHistoryAnimations,
openChatWithInfo,
pinMessage,
focusMessage,
@ -384,7 +386,10 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<div className="MiddleHeader" ref={componentRef}>
<Transition name="slide-fade" activeKey={messageListType === 'thread' ? threadId : 1}>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : 'slide-fade'}
activeKey={messageListType === 'thread' ? threadId : 1}
>
{renderInfo}
</Transition>
@ -421,7 +426,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
lastSyncTime,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
shouldSkipHistoryAnimations,
};
const messagesById = selectChatMessages(global, chatId);

View File

@ -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<GlobalActions, 'searchMoreGifs' | 'sendMessage' | 'set
const PRELOAD_BACKWARDS = 96; // GIF Search bot results are multiplied by 24
const INTERSECTION_DEBOUNCE = 300;
const GifSearch: FC<StateProps & DispatchProps> = ({
const GifSearch: FC<OwnProps & StateProps & DispatchProps> = ({
onClose,
isActive,
query,
results,
chat,
@ -67,6 +75,8 @@ const GifSearch: FC<StateProps & DispatchProps> = ({
const lang = useLang();
useHistoryBack(isActive, onClose);
function renderContent() {
if (query === undefined) {
return undefined;

View File

@ -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<StateProps> = ({
const PollResults: FC<OwnProps & StateProps> = ({
onClose,
isActive,
chat,
message,
lastSyncTime,
}) => {
const lang = useLang();
useHistoryBack(isActive, onClose);
if (!message || !chat) {
return <Loading />;
}

View File

@ -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<GlobalActions, (
@ -67,6 +69,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
setStickerSearchQuery,
setGifSearchQuery,
closePollResults,
shouldSkipHistoryAnimations,
}) => {
const { width: windowWidth } = useWindowSize();
const [profileState, setProfileState] = useState<ProfileState>(ProfileState.Profile);
@ -88,21 +91,21 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
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<StateProps & DispatchProps> = ({
}
}, [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<StateProps & DispatchProps> = ({
/>
);
case RightColumnContent.Search:
return <RightSearch chatId={chatId!} threadId={threadId!} />;
return <RightSearch chatId={chatId!} threadId={threadId!} onClose={close} isActive={isOpen && isActive} />;
case RightColumnContent.Management:
return (
<Management
@ -216,14 +226,17 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
selectedChatMemberId={selectedChatMemberId}
onScreenSelect={setManagementScreen}
onChatMemberSelect={handleSelectChatMember}
isActive={isOpen && isActive}
onClose={close}
/>
);
case RightColumnContent.StickerSearch:
return <StickerSearch />;
return <StickerSearch onClose={close} isActive={isOpen && isActive} />;
case RightColumnContent.GifSearch:
return <GifSearch />;
return <GifSearch onClose={close} isActive={isOpen && isActive} />;
case RightColumnContent.PollResults:
return <PollResults />;
return <PollResults onClose={close} isActive={isOpen && isActive} />;
}
}
@ -248,9 +261,10 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
profileState={profileState}
managementScreen={managementScreen}
onClose={close}
shouldSkipAnimation={shouldSkipTransition || shouldSkipHistoryAnimations}
/>
<Transition
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
name={(shouldSkipTransition || shouldSkipHistoryAnimations) ? 'none' : 'zoom-fade'}
renderCount={MAIN_SCREENS_COUNT + MANAGEMENT_SCREENS_COUNT}
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
shouldCleanup
@ -274,6 +288,7 @@ export default memo(withGlobal(
threadId,
currentProfileUserId: global.users.selectedId,
isChatSelected: Boolean(chatId && areActiveChatsLoaded),
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
searchTextMessagesLocal,
toggleManagement,
openHistoryCalendar,
shouldSkipAnimation,
}) => {
// eslint-disable-next-line no-null/no-null
const backButtonRef = useRef<HTMLDivElement>(null);
@ -278,7 +280,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
<div ref={backButtonRef} className={buttonClassName} />
</Button>
<Transition
name={shouldSkipTransition ? 'none' : 'slide-fade'}
name={(shouldSkipTransition || shouldSkipAnimation) ? 'none' : 'slide-fade'}
activeKey={renderingContentKey}
>
{renderHeaderContent}

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
chatId,
threadId,
onClose,
isActive,
chat,
messagesById,
query,
@ -125,6 +130,8 @@ const RightSearch: FC<OwnProps & StateProps & DispatchProps> = ({
);
};
useHistoryBack(isActive, onClose);
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {

View File

@ -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<StateProps & DispatchProps> = ({
const StickerSearch: FC<OwnProps & StateProps & DispatchProps> = ({
onClose,
isActive,
query,
featuredIds,
resultIds,
@ -53,6 +61,8 @@ const StickerSearch: FC<StateProps & DispatchProps> = ({
});
});
useHistoryBack(isActive, onClose);
function renderContent() {
if (query === undefined) {
return undefined;

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const lang = useLang();
useHistoryBack(isActive, onClose);
useEffect(() => {
if (progress === ManagementProgress.Complete) {
setIsProfileFieldsTouched(false);

View File

@ -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<OwnProps & StateProps> = ({
usersById,
onScreenSelect,
onChatMemberSelect,
onClose,
isActive,
}) => {
const lang = useLang();
useHistoryBack(isActive, onClose);
function handleRecentActionsClick() {
onScreenSelect(ManagementScreens.GroupRecentActions);
}

View File

@ -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<GlobalActions, (
const ManageChatPrivacyType: FC<OwnProps & StateProps & DispatchProps> = ({
chat,
onClose,
isActive,
isChannel,
progress,
isUsernameAvailable,
@ -60,6 +65,8 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps & DispatchProps> = ({
|| (privacyType === 'private' && isPublic)
);
useHistoryBack(isActive, onClose);
useEffect(() => {
if (privacyType && !privateLink) {
updatePrivateLink();

View File

@ -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<GlobalActions, 'loadGroupsForDiscussion' | 'linkDiscus
const ManageDiscussion: FC<OwnProps & StateProps & DispatchProps> = ({
chat,
onClose,
isActive,
chatId,
chatsByIds,
linkedChat,
@ -59,6 +64,8 @@ const ManageDiscussion: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang();
const linkedChatId = linkedChat && linkedChat.id;
useHistoryBack(isActive, onClose);
useEffect(() => {
loadGroupsForDiscussion();
}, [loadGroupsForDiscussion]);

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
deleteChannel,
closeManagement,
openChat,
onClose,
isActive,
}) => {
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const currentTitle = chat.title;
@ -82,6 +87,8 @@ const ManageGroup: FC<OwnProps & StateProps & DispatchProps> = ({
const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const lang = useLang();
useHistoryBack(isActive, onClose);
useEffect(() => {
if (progress === ManagementProgress.Complete) {
setIsProfileFieldsTouched(false);

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isChannel,
isFormFullyDisabled,
updateChatAdmin,
onClose,
isActive,
}) => {
const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
const [isTouched, setIsTouched] = useState(false);
@ -57,6 +62,8 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps & DispatchProps> = ({
const [customTitle, setCustomTitle] = useState('');
const lang = useLang();
useHistoryBack(isActive, onClose);
const selectedChatMember = useMemo(() => {
if (!chat.fullInfo || !chat.fullInfo.adminMembers) {
return undefined;

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
usersById,
isChannel,
openUserInfo,
onClose,
isActive,
serverTimeOffset,
}) => {
const memberIds = useMemo(() => {
@ -45,6 +50,8 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
openUserInfo({ id });
}, [openUserInfo]);
useHistoryBack(isActive, onClose);
return (
<div className="Management">
<div className="custom-scroll">

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
chat,
currentUserId,
updateChatDefaultBannedRights,
onClose,
isActive,
}) => {
const [permissions, setPermissions] = useState<ApiChatBannedRights>({});
const [havePermissionChanged, setHavePermissionChanged] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const lang = useLang();
useHistoryBack(isActive, onClose);
const handleRemovedUsersClick = useCallback(() => {
onScreenSelect(ManagementScreens.GroupRemovedUsers);
}, [onScreenSelect]);

View File

@ -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<OwnProps & StateProps> = ({ chat }) => {
const ManageGroupRecentActions: FC<OwnProps & StateProps> = ({ chat, onClose, isActive }) => {
const lang = useLang();
useHistoryBack(isActive, onClose);
const adminMembers = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.adminMembers) {
return [];

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
chat,
usersById,
updateChatMemberBannedRights,
onClose,
isActive,
}) => {
const lang = useLang();
useHistoryBack(isActive, onClose);
const removedMembers = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.kickedMembers) {
return [];

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
onScreenSelect,
updateChatMemberBannedRights,
isFormFullyDisabled,
onClose,
isActive,
}) => {
const [permissions, setPermissions] = useState<ApiChatBannedRights>({});
const [havePermissionChanged, setHavePermissionChanged] = useState(false);
@ -46,6 +51,8 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps & DispatchProps> = ({
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
const lang = useLang();
useHistoryBack(isActive, onClose);
const selectedChatMember = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.members) {
return undefined;

View File

@ -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<OwnProps & StateProps> = ({
isChannel,
onScreenSelect,
onChatMemberSelect,
onClose,
isActive,
serverTimeOffset,
}) => {
useHistoryBack(isActive, onClose);
const memberIds = useMemo(() => {
if (!members || !usersById) {
return undefined;

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
deleteHistory,
closeManagement,
openChat,
onClose,
isActive,
}) => {
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
const [error, setError] = useState<string | undefined>();
const lang = useLang();
useHistoryBack(isActive, onClose);
const currentFirstName = user ? (user.firstName || '') : '';
const currentLastName = user ? (user.lastName || '') : '';

View File

@ -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<OwnProps & StateProps> = ({
isPromotedByCurrentUser,
onScreenSelect,
onChatMemberSelect,
onClose,
isActive,
managementType,
}) => {
switch (currentScreen) {
case ManagementScreens.Initial: {
switch (managementType) {
case 'user':
return <ManageUser key={chatId} userId={chatId} />;
return (
<ManageUser
key={chatId}
userId={chatId}
onClose={onClose}
isActive={isActive}
/>
);
case 'group':
return <ManageGroup key={chatId} chatId={chatId} onScreenSelect={onScreenSelect} />;
return (
<ManageGroup
key={chatId}
chatId={chatId}
onScreenSelect={onScreenSelect}
onClose={onClose}
isActive={isActive || [
ManagementScreens.ChatPrivacyType,
ManagementScreens.Discussion,
ManagementScreens.GroupPermissions,
ManagementScreens.ChatAdministrators,
ManagementScreens.GroupRemovedUsers,
ManagementScreens.GroupUserPermissionsCreate,
ManagementScreens.GroupUserPermissions,
ManagementScreens.ChatAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
/>
);
case 'channel':
return <ManageChannel key={chatId} chatId={chatId} onScreenSelect={onScreenSelect} />;
return (
<ManageChannel
key={chatId}
chatId={chatId}
onScreenSelect={onScreenSelect}
onClose={onClose}
isActive={isActive || [
ManagementScreens.ChannelSubscribers,
ManagementScreens.ChatAdministrators,
ManagementScreens.Discussion,
ManagementScreens.ChatPrivacyType,
ManagementScreens.ChatAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
/>
);
}
break;
@ -57,7 +101,11 @@ const Management: FC<OwnProps & StateProps> = ({
case ManagementScreens.ChatPrivacyType:
return (
<ManageChatPrivacyType chatId={chatId} />
<ManageChatPrivacyType
chatId={chatId}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.Discussion:
@ -65,6 +113,8 @@ const Management: FC<OwnProps & StateProps> = ({
<ManageDiscussion
chatId={chatId}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
@ -74,12 +124,22 @@ const Management: FC<OwnProps & StateProps> = ({
chatId={chatId}
onScreenSelect={onScreenSelect}
onChatMemberSelect={onChatMemberSelect}
isActive={isActive || [
ManagementScreens.GroupRemovedUsers,
ManagementScreens.GroupUserPermissionsCreate,
ManagementScreens.GroupUserPermissions,
].includes(currentScreen)}
onClose={onClose}
/>
);
case ManagementScreens.GroupRemovedUsers:
return (
<ManageGroupRemovedUsers chatId={chatId} />
<ManageGroupRemovedUsers
chatId={chatId}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.GroupUserPermissionsCreate:
@ -88,6 +148,10 @@ const Management: FC<OwnProps & StateProps> = ({
chatId={chatId}
onChatMemberSelect={onChatMemberSelect}
onScreenSelect={onScreenSelect}
isActive={isActive || [
ManagementScreens.GroupUserPermissions,
].includes(currentScreen)}
onClose={onClose}
/>
);
@ -98,6 +162,8 @@ const Management: FC<OwnProps & StateProps> = ({
selectedChatMemberId={selectedChatMemberId}
isPromotedByCurrentUser={isPromotedByCurrentUser}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
@ -107,6 +173,11 @@ const Management: FC<OwnProps & StateProps> = ({
chatId={chatId}
onScreenSelect={onScreenSelect}
onChatMemberSelect={onChatMemberSelect}
isActive={isActive || [
ManagementScreens.ChatAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
onClose={onClose}
/>
);
@ -114,6 +185,8 @@ const Management: FC<OwnProps & StateProps> = ({
return (
<ManageGroupRecentActions
chatId={chatId}
isActive={isActive}
onClose={onClose}
/>
);
@ -124,13 +197,19 @@ const Management: FC<OwnProps & StateProps> = ({
selectedChatMemberId={selectedChatMemberId}
isPromotedByCurrentUser={isPromotedByCurrentUser}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.ChannelSubscribers:
case ManagementScreens.GroupMembers:
return (
<ManageGroupMembers chatId={chatId} />
<ManageGroupMembers
chatId={chatId}
isActive={isActive}
onClose={onClose}
/>
);
}

View File

@ -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<OwnProps> = ({
positionX = 'left',
positionY = 'top',
footer,
forceOpen,
onOpen,
onClose,
}) => {
// eslint-disable-next-line no-null/no-null
const menuRef = useRef<HTMLDivElement>(null);
@ -29,6 +35,9 @@ const DropdownMenu: FC<OwnProps> = ({
const toggleIsOpen = () => {
setIsOpen(!isOpen);
if (isOpen) {
if (onClose) onClose();
} else if (onOpen) onOpen();
};
const handleKeyDown = (e: React.KeyboardEvent<any>) => {
@ -48,6 +57,7 @@ const DropdownMenu: FC<OwnProps> = ({
const handleClose = () => {
setIsOpen(false);
if (onClose) onClose();
};
return (
@ -61,13 +71,14 @@ const DropdownMenu: FC<OwnProps> = ({
<Menu
ref={menuRef}
containerRef={dropdownRef}
isOpen={isOpen}
isOpen={isOpen || !!forceOpen}
className={className || ''}
positionX={positionX}
positionY={positionY}
footer={footer}
autoClose
onClose={handleClose}
shouldSkipTransition={forceOpen}
>
{children}
</Menu>

View File

@ -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<any>) => void;
@ -48,6 +50,7 @@ const Menu: FC<OwnProps> = ({
onClose,
onMouseEnter,
onMouseLeave,
shouldSkipTransition,
}) => {
// eslint-disable-next-line no-null/no-null
let menuRef = useRef<HTMLDivElement>(null);
@ -56,9 +59,22 @@ const Menu: FC<OwnProps> = ({
}
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) {

View File

@ -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<OwnProps> = (props) => {
type StateProps = {
shouldSkipHistoryAnimations?: boolean;
};
const Modal: FC<OwnProps & StateProps> = ({
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<HTMLDivElement>(null);
@ -50,6 +62,8 @@ const Modal: FC<OwnProps> = (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);

View File

@ -19,6 +19,10 @@
}
}
&.skip-slide-transition {
transition: none !important;
}
/*
* scroll-slide
*/

View File

@ -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<HTMLDivElement>;
activeKey: number;
@ -239,7 +239,9 @@ const Transition: FC<OwnProps> = ({
const render = renders[key];
return (
typeof render === 'function' ? <div key={key}>{render(key === activeKey, key === prevActiveKey)}</div> : undefined
typeof render === 'function'
? <div key={key}>{render(key === activeKey, key === prevActiveKey, activeKey)}</div>
: undefined
);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -8,26 +8,28 @@ const delegationRegistry: Record<string, Map<HTMLElement, Handler>> = {};
const delegatedEventsByElement = new Map<HTMLElement, Set<string>>();
const documentEventCounters: Record<string, number> = {};
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'
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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