Introduce Lock Screen and Passcode (#1839)
This commit is contained in:
parent
fa6eec434f
commit
017170c2e2
2
package-lock.json
generated
2
package-lock.json
generated
@ -86,7 +86,7 @@
|
|||||||
"stylelint": "^14.6.1",
|
"stylelint": "^14.6.1",
|
||||||
"stylelint-config-recommended-scss": "^6.0.0",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
|
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
|
||||||
"stylelint-group-selectors": "^1.0.8",
|
"stylelint-group-selectors": "^1.0.6",
|
||||||
"stylelint-high-performance-animation": "^1.6.0",
|
"stylelint-high-performance-animation": "^1.6.0",
|
||||||
"telegraph-node": "^1.0.4",
|
"telegraph-node": "^1.0.4",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
"stylelint": "^14.6.1",
|
"stylelint": "^14.6.1",
|
||||||
"stylelint-config-recommended-scss": "^6.0.0",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
|
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
|
||||||
"stylelint-group-selectors": "^1.0.8",
|
"stylelint-group-selectors": "^1.0.6",
|
||||||
"stylelint-high-performance-animation": "^1.6.0",
|
"stylelint-high-performance-animation": "^1.6.0",
|
||||||
"telegraph-node": "^1.0.4",
|
"telegraph-node": "^1.0.4",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
|
|||||||
109
src/App.tsx
109
src/App.tsx
@ -3,26 +3,45 @@ import React, { useEffect } from './lib/teact/teact';
|
|||||||
import { getActions, withGlobal } from './global';
|
import { getActions, withGlobal } from './global';
|
||||||
|
|
||||||
import type { GlobalState } from './global/types';
|
import type { GlobalState } from './global/types';
|
||||||
|
import type { UiLoaderPage } from './components/common/UiLoader';
|
||||||
|
|
||||||
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
|
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
|
||||||
import { pick } from './util/iteratees';
|
import { PLATFORM_ENV } from './util/environment';
|
||||||
import { updateSizes } from './util/windowSize';
|
import { updateSizes } from './util/windowSize';
|
||||||
import { addActiveTabChangeListener } from './util/activeTabMonitor';
|
import { addActiveTabChangeListener } from './util/activeTabMonitor';
|
||||||
|
import { hasStoredSession } from './util/sessions';
|
||||||
|
import buildClassName from './util/buildClassName';
|
||||||
import useFlag from './hooks/useFlag';
|
import useFlag from './hooks/useFlag';
|
||||||
|
import usePrevious from './hooks/usePrevious';
|
||||||
|
|
||||||
import Auth from './components/auth/Auth';
|
import Auth from './components/auth/Auth';
|
||||||
import UiLoader from './components/common/UiLoader';
|
|
||||||
import Main from './components/main/Main.async';
|
import Main from './components/main/Main.async';
|
||||||
|
import LockScreen from './components/main/LockScreen.async';
|
||||||
import AppInactive from './components/main/AppInactive';
|
import AppInactive from './components/main/AppInactive';
|
||||||
import { hasStoredSession } from './util/sessions';
|
import Transition from './components/ui/Transition';
|
||||||
|
import UiLoader from './components/common/UiLoader';
|
||||||
// import Test from './components/test/TestNoRedundancy';
|
// import Test from './components/test/TestNoRedundancy';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authState'>;
|
type StateProps = {
|
||||||
|
authState: GlobalState['authState'];
|
||||||
|
isScreenLocked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const App: FC<StateProps> = ({ authState }) => {
|
enum AppScreens {
|
||||||
|
auth,
|
||||||
|
lock,
|
||||||
|
main,
|
||||||
|
inactive,
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: FC<StateProps> = ({
|
||||||
|
authState,
|
||||||
|
isScreenLocked,
|
||||||
|
}) => {
|
||||||
const { disconnect } = getActions();
|
const { disconnect } = getActions();
|
||||||
|
|
||||||
const [isInactive, markInactive] = useFlag(false);
|
const [isInactive, markInactive] = useFlag(false);
|
||||||
|
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateSizes();
|
updateSizes();
|
||||||
@ -36,37 +55,89 @@ const App: FC<StateProps> = ({ authState }) => {
|
|||||||
|
|
||||||
// return <Test />;
|
// return <Test />;
|
||||||
|
|
||||||
if (isInactive) {
|
let activeKey: number;
|
||||||
return <AppInactive />;
|
let page: UiLoaderPage | undefined;
|
||||||
}
|
|
||||||
|
|
||||||
if (authState) {
|
if (isInactive) {
|
||||||
|
activeKey = AppScreens.inactive;
|
||||||
|
} else if (isScreenLocked) {
|
||||||
|
page = 'lock';
|
||||||
|
activeKey = AppScreens.lock;
|
||||||
|
} else if (authState) {
|
||||||
switch (authState) {
|
switch (authState) {
|
||||||
case 'authorizationStateWaitPhoneNumber':
|
case 'authorizationStateWaitPhoneNumber':
|
||||||
|
page = 'authPhoneNumber';
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
break;
|
||||||
case 'authorizationStateWaitCode':
|
case 'authorizationStateWaitCode':
|
||||||
|
page = 'authCode';
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
break;
|
||||||
case 'authorizationStateWaitPassword':
|
case 'authorizationStateWaitPassword':
|
||||||
|
page = 'authPassword';
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
break;
|
||||||
case 'authorizationStateWaitRegistration':
|
case 'authorizationStateWaitRegistration':
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
break;
|
||||||
case 'authorizationStateWaitQrCode':
|
case 'authorizationStateWaitQrCode':
|
||||||
return <Auth />;
|
page = 'authQrCode';
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
break;
|
||||||
case 'authorizationStateClosed':
|
case 'authorizationStateClosed':
|
||||||
case 'authorizationStateClosing':
|
case 'authorizationStateClosing':
|
||||||
case 'authorizationStateLoggingOut':
|
case 'authorizationStateLoggingOut':
|
||||||
case 'authorizationStateReady':
|
case 'authorizationStateReady':
|
||||||
return renderMain();
|
page = 'main';
|
||||||
|
activeKey = AppScreens.main;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (hasStoredSession(true)) {
|
||||||
|
page = 'main';
|
||||||
|
activeKey = AppScreens.main;
|
||||||
|
} else {
|
||||||
|
page = isMobile ? 'authPhoneNumber' : 'authQrCode';
|
||||||
|
activeKey = AppScreens.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevActiveKey = usePrevious(activeKey);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
function renderContent(isActive: boolean) {
|
||||||
|
switch (activeKey) {
|
||||||
|
case AppScreens.auth:
|
||||||
|
return <Auth isActive={isActive} />;
|
||||||
|
case AppScreens.main:
|
||||||
|
return <Main />;
|
||||||
|
case AppScreens.lock:
|
||||||
|
return <LockScreen isLocked={isScreenLocked} />;
|
||||||
|
case AppScreens.inactive:
|
||||||
|
return <AppInactive />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasStoredSession(true) ? renderMain() : <Auth />;
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderMain() {
|
|
||||||
return (
|
return (
|
||||||
<UiLoader page="main" key="main">
|
<UiLoader key="Loader" page={page}>
|
||||||
<Main />
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
activeKey={activeKey}
|
||||||
|
shouldCleanup
|
||||||
|
className={buildClassName(
|
||||||
|
'full-height',
|
||||||
|
(activeKey === AppScreens.auth || prevActiveKey === AppScreens.auth) && 'is-auth',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderContent}
|
||||||
|
</Transition>
|
||||||
</UiLoader>
|
</UiLoader>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default withGlobal(
|
export default withGlobal(
|
||||||
(global): StateProps => pick(global, ['authState']),
|
(global): StateProps => {
|
||||||
|
return {
|
||||||
|
authState: global.authState,
|
||||||
|
isScreenLocked: global.passcode?.isScreenLocked,
|
||||||
|
};
|
||||||
|
},
|
||||||
)(App);
|
)(App);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ interface LocalDb {
|
|||||||
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const LOCAL_DB_INITIAL = {
|
||||||
localMessages: {},
|
localMessages: {},
|
||||||
chats: {},
|
chats: {},
|
||||||
users: {},
|
users: {},
|
||||||
@ -22,4 +22,12 @@ export default {
|
|||||||
stickerSets: {},
|
stickerSets: {},
|
||||||
photos: {},
|
photos: {},
|
||||||
webDocuments: {},
|
webDocuments: {},
|
||||||
} as LocalDb;
|
};
|
||||||
|
|
||||||
|
const localDb: LocalDb = LOCAL_DB_INITIAL;
|
||||||
|
|
||||||
|
export default localDb;
|
||||||
|
|
||||||
|
export function clearLocalDb() {
|
||||||
|
Object.assign(localDb, LOCAL_DB_INITIAL);
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { updater } from '../updater';
|
|||||||
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
|
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
|
||||||
import downloadMediaWithClient, { parseMediaUrl } from './media';
|
import downloadMediaWithClient, { parseMediaUrl } from './media';
|
||||||
import { buildApiUserFromFull } from '../apiBuilders/users';
|
import { buildApiUserFromFull } from '../apiBuilders/users';
|
||||||
import localDb from '../localDb';
|
import localDb, { clearLocalDb } from '../localDb';
|
||||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||||
import { addMessageToLocalDb } from '../helpers';
|
import { addMessageToLocalDb } from '../helpers';
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
if (err.message !== 'Disconnect') {
|
if (err.message !== 'Disconnect' && err.message !== 'Cannot send requests while disconnected') {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateConnectionState',
|
'@type': 'updateConnectionState',
|
||||||
connectionState: 'connectionStateBroken',
|
connectionState: 'connectionStateBroken',
|
||||||
@ -134,8 +134,13 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy() {
|
export async function destroy(noLogOut = false) {
|
||||||
await invokeRequest(new GramJs.auth.LogOut());
|
if (!noLogOut) {
|
||||||
|
await invokeRequest(new GramJs.auth.LogOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLocalDb();
|
||||||
|
|
||||||
await client.destroy();
|
await client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
src/assets/lock.png
Normal file
BIN
src/assets/lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@ -1,2 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg width="87.5" height="56.115" viewBox="0 0 87.499 56.115" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M78.46 56.114H9.041C4.048 56.114 0 52.678 0 48.439V7.675C0 3.436 4.05 0 9.042 0h69.417c4.994 0 9.042 3.436 9.042 7.675v40.764c0 4.24-4.048 7.675-9.042 7.675z" fill="#fff"/><path d="M61.802 6.681c12.762 0 23.111 10.347 23.111 23.111s-10.349 23.11-23.11 23.11c-7.418 0-14.02-3.493-18.246-8.925a23 23 0 0 0 4.866-14.185 23 23 0 0 0-4.866-14.185c4.225-5.432 10.828-8.926 18.245-8.926z" fill="#f79f1a"/><path d="M43.557 15.607a23 23 0 0 1 4.866 14.185 23 23 0 0 1-4.866 14.185 23 23 0 0 1-4.865-14.185 23 23 0 0 1 4.865-14.185z" fill="#ff5f01"/><path d="M25.312 6.681c7.417 0 14.02 3.494 18.245 8.926a23 23 0 0 0-4.865 14.185 23 23 0 0 0 4.865 14.185c-4.225 5.432-10.828 8.926-18.245 8.926-12.765 0-23.11-10.347-23.11-23.11S12.546 6.68 25.311 6.68z" fill="#ea001b"/><path d="M43.48 30.318h.155v-.896h.37v-.155h-.895v.155h.37zm36.31-1.898c-1.593-2.117-5.81-.99-5.81 2.7 0 3.77 4.372 4.78 5.81 2.701v.894h1.535V24.67H79.79zm-2.06 4.973c-1.32 0-2.176-1.01-2.176-2.272 0-1.263.856-2.273 2.176-2.273 1.283 0 2.176 1.01 2.176 2.273 0 1.321-.893 2.272-2.176 2.272zM21.585 28.42c-.525-.641-1.263-1.05-2.273-1.05-2.001 0-3.536 1.594-3.536 3.75 0 2.176 1.535 3.75 3.536 3.75 1.01 0 1.748-.37 2.273-1.05v.895h1.535v-7.15h-1.535zm-2.06 4.973c-1.379 0-2.175-1.01-2.175-2.272 0-1.263.796-2.273 2.176-2.273 1.262 0 2.098 1.01 2.117 2.273 0 1.321-.854 2.272-2.117 2.272z"/><path d="M42.411 27.352c-2.058 0-3.535 1.535-3.535 3.75 0 3.983 4.39 4.643 6.566 2.759l-.738-1.166c-1.515 1.244-3.808 1.127-4.254-.952h5.342c0-3.147-1.438-4.39-3.38-4.39zm-1.903 3.128c.155-1.01.796-1.69 1.845-1.69 1.01 0 1.69.583 1.865 1.69zm24.846-2.06c-1.613-2.137-5.81-.933-5.81 2.7 0 3.73 4.353 4.8 5.81 2.701v.894h1.593v-7.15h-1.593zm-2.06 4.973c-1.32 0-2.117-1.01-2.117-2.272 0-1.263.797-2.273 2.118-2.273s2.175 1.01 2.175 2.273c0 1.321-.855 2.272-2.175 2.272zM11.54 27.372c-.893 0-1.845.272-2.487 1.262-.466-.796-1.262-1.262-2.37-1.262-.738 0-1.476.272-2.059 1.049v-.855H3.031v7.13h1.593c0-3.672-.486-5.867 1.748-5.867 1.982 0 1.594 1.981 1.594 5.867H9.5c0-3.555-.486-5.867 1.749-5.867 1.981 0 1.593 1.943 1.593 5.867h1.593v-4.468h-.04c0-1.691-1.106-2.798-2.855-2.856zm14.998 2.213c0-1.107 2.312-.932 3.594-.213l.641-1.263c-1.826-1.185-5.867-1.166-5.867 1.593 0 2.778 4.449 1.613 4.449 2.914 0 1.224-2.623 1.127-4.021.156l-.68 1.224c2.175 1.476 6.332 1.166 6.332-1.457 0-2.914-4.448-1.594-4.448-2.954zm8.529 2.642v-3.225h2.545v-1.437h-2.545v-2.176h-1.593v2.176h-1.477v1.418h1.477v3.244c0 3.42 3.36 2.798 4.39 2.118l-.427-1.321c-.738.408-2.37.855-2.37-.797zm13.91-3.807v-.855h-1.593v7.13h1.593v-4.021c0-2.254 1.846-1.962 2.487-1.632l.466-1.477c-.894-.388-2.253-.35-2.953.855zm9.035 1.068.738-1.263c-2.253-1.768-6.352-.796-6.352 2.915 0 3.846 4.351 4.623 6.352 2.913l-.738-1.262c-1.787 1.262-4.022.505-4.022-1.67 0-2.216 2.254-2.935 4.022-1.633zm12.531-1.068v-.855h-1.535v7.13h1.535v-4.021c0-2.138 1.749-2.001 2.487-1.632l.466-1.477c-.466-.233-2.137-.563-2.953.855zm13.482 5.4c-.058-.057-.097-.155-.156-.213s-.155-.097-.213-.156c-.097 0-.214-.058-.311-.058-.058 0-.156.058-.272.058a1.004 1.004 0 0 0-.214.156c-.097.058-.155.156-.155.214-.058.097-.058.214-.058.31 0 .059 0 .156.058.272 0 .058.058.156.155.214a.674.674 0 0 0 .214.156.527.527 0 0 0 .272.058c.097 0 .214 0 .31-.058.059-.059.156-.097.214-.156s.097-.156.156-.214c.058-.116.058-.213.058-.271 0-.097 0-.214-.058-.311z"/><path d="M42.973 29.267v1.05h.156v-.797l.311.68h.214l.273-.68v.798h.214v-1.051h-.272l-.312.68-.311-.68z"/></svg>
|
||||||
<svg width="23.151mm" height="14.847mm" version="1.1" viewBox="0 0 87.499 56.115" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g id="_x32_06-mastercard_x2C__Credit_card" transform="matrix(.24327 0 0 .24327 -18.72 -32.485)"><path d="m399.47 364.2h-285.35c-20.527 0-37.167-14.124-37.167-31.547v-167.57c0-17.423 16.641-31.547 37.167-31.547h285.35c20.528 0 37.167 14.124 37.167 31.547v167.57c0 17.424-16.64 31.547-37.167 31.547z" fill="#fff" stroke-width=".73639"/><path d="m331 161c52.46 0 95 42.53 95 95s-42.54 95-95 95c-30.49 0-57.631-14.36-75-36.69 12.54-16.1 20-36.329 20-58.31 0-21.98-7.46-42.21-20-58.31 17.369-22.33 44.51-36.69 75-36.69z" fill="#f79f1a"/><path d="m256 197.69c12.54 16.1 20 36.33 20 58.31s-7.46 42.21-20 58.31c-12.54-16.1-20-36.329-20-58.31 0-21.98 7.46-42.21 20-58.31z" fill="#ff5f01"/><path d="m181 161c30.49 0 57.63 14.36 75 36.69-12.54 16.1-20 36.33-20 58.31s7.46 42.21 20 58.31c-17.37 22.33-44.51 36.69-75 36.69-52.47 0-95-42.53-95-95s42.53-95 95-95z" fill="#ea001b"/><polygon points="255.68 258.16 256.32 258.16 256.32 254.48 257.84 254.48 257.84 253.84 254.16 253.84 254.16 254.48 255.68 254.48"/><path d="m404.94 250.36c-6.549-8.704-23.879-4.073-23.879 11.101 0 15.494 17.969 19.646 23.879 11.101v3.674h6.309v-41.287h-6.309zm-8.466 20.444c-5.43 0-8.944-4.152-8.944-9.343 0-5.192 3.515-9.343 8.944-9.343 5.272 0 8.945 4.15 8.945 9.343 0 5.432-3.673 9.343-8.945 9.343z"/><path d="m165.68 250.36c-2.156-2.636-5.19-4.313-9.344-4.313-8.226 0-14.534 6.549-14.534 15.413 0 8.945 6.309 15.414 14.534 15.414 4.154 0 7.188-1.518 9.344-4.313v3.674h6.31v-29.39h-6.31zm-8.465 20.444c-5.671 0-8.944-4.152-8.944-9.343 0-5.192 3.273-9.343 8.944-9.343 5.19 0 8.625 4.15 8.704 9.343 0 5.432-3.513 9.343-8.704 9.343z"/><path d="m251.29 245.97c-8.463 0-14.534 6.311-14.534 15.414 0 16.372 18.049 19.088 26.993 11.341l-3.034-4.792c-6.229 5.112-15.654 4.632-17.489-3.913h21.961c0-12.938-5.91-18.05-13.897-18.05zm-7.825 12.859c0.639-4.153 3.273-6.947 7.586-6.947 4.151 0 6.949 2.395 7.667 6.947z"/><path d="m345.6 250.36c-6.629-8.785-23.88-3.834-23.88 11.101 0 15.333 17.891 19.725 23.88 11.101v3.674h6.549v-29.39h-6.549zm-8.466 20.444c-5.43 0-8.704-4.152-8.704-9.343 0-5.192 3.274-9.343 8.704-9.343s8.944 4.15 8.944 9.343c0 5.432-3.515 9.343-8.944 9.343z"/><path d="m124.39 246.05c-3.674 0-7.586 1.118-10.223 5.189-1.917-3.272-5.19-5.189-9.743-5.189-3.035 0-6.069 1.118-8.464 4.313v-3.515h-6.55v29.31h6.549c0-15.094-1.997-24.117 7.187-24.117 8.146 0 6.549 8.145 6.549 24.117h6.309c0-14.613-1.997-24.117 7.187-24.117 8.146 0 6.549 7.985 6.549 24.117h6.548v-18.368h-0.16c0-6.951-4.55-11.501-11.738-11.74z"/><path d="m186.04 255.15c0-4.553 9.503-3.833 14.773-0.878l2.636-5.191c-7.507-4.871-24.118-4.791-24.118 6.55 0 11.419 18.288 6.628 18.288 11.979 0 5.031-10.781 4.631-16.53 0.639l-2.796 5.032c8.944 6.067 26.033 4.792 26.033-5.991 1e-3 -11.978-18.286-6.55-18.286-12.14z"/><path d="m221.1 266.01v-13.256h10.461v-5.91h-10.461v-8.942h-6.549v8.942h-6.07v5.829h6.07v13.337c0 14.058 13.815 11.5 18.047 8.706l-1.757-5.431c-3.033 1.676-9.741 3.513-9.741-3.275z"/><path d="m278.28 250.36v-3.515h-6.551v29.31h6.551v-16.531c0-9.264 7.587-8.066 10.222-6.708l1.915-6.07c-3.672-1.596-9.262-1.438-12.137 3.514z"/><path d="m315.42 254.75 3.033-5.191c-9.262-7.267-26.111-3.274-26.111 11.98 0 15.812 17.887 19.006 26.111 11.978l-3.033-5.19c-7.348 5.19-16.532 2.077-16.532-6.868 0-9.105 9.264-12.062 16.532-6.709z"/><path d="m366.93 250.36v-3.515h-6.309v29.31h6.309v-16.531c0-8.787 7.188-8.226 10.223-6.708l1.917-6.07c-1.918-0.958-8.786-2.316-12.14 3.514z"/><path d="m422.35 272.56c-0.24-0.239-0.4-0.64-0.64-0.879s-0.64-0.397-0.878-0.639c-0.399 0-0.879-0.24-1.279-0.24-0.238 0-0.639 0.24-1.116 0.24-0.401 0.241-0.641 0.399-0.879 0.639-0.399 0.239-0.639 0.64-0.639 0.879-0.24 0.399-0.24 0.88-0.24 1.278 0 0.239 0 0.639 0.24 1.116 0 0.24 0.239 0.641 0.639 0.88 0.238 0.239 0.4 0.399 0.879 0.639 0.398 0.239 0.878 0.239 1.116 0.239 0.4 0 0.88 0 1.279-0.239 0.238-0.239 0.639-0.399 0.878-0.639s0.399-0.64 0.64-0.88c0.239-0.478 0.239-0.877 0.239-1.116 0-0.399 0-0.879-0.239-1.278z"/><polygon points="253.6 253.84 253.6 258.16 254.24 258.16 254.24 254.88 255.52 257.68 256.4 257.68 257.52 254.88 257.52 258.16 258.4 258.16 258.4 253.84 257.28 253.84 256 256.64 254.72 253.84"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/tgs/settings/Congratulations.tgs
Normal file
BIN
src/assets/tgs/settings/Congratulations.tgs
Normal file
Binary file not shown.
BIN
src/assets/tgs/settings/Lock.tgs
Normal file
BIN
src/assets/tgs/settings/Lock.tgs
Normal file
Binary file not shown.
@ -1,2 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg width="108.54" height="84.42" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M108.541 9.044v66.33c0 4.995-4.051 9.046-9.045 9.046H9.046C4.052 84.42 0 80.37 0 75.375V9.045C0 4.05 4.052-.002 9.046-.002h90.45c4.994 0 9.045 4.052 9.045 9.045z" fill="#0353a5"/><path d="M93.39 27.135h-5.86c-1.809 0-3.185.528-3.957 2.431l-11.25 26.852h7.952s1.3-3.617 1.583-4.39h9.723c.226 1.036.904 4.39.904 4.39h7.01zm-9.346 18.9c.622-1.676 3.015-8.196 3.015-8.196-.038.056.622-1.715.998-2.808l.528 2.525s1.432 7.01 1.753 8.48h-6.294zm-51.406-18.9L25.232 47.11l-.81-4.052-2.639-13.454c-.433-1.866-1.771-2.393-3.43-2.469H6.164l-.133.585c2.977.753 5.634 1.846 7.952 3.222l6.746 25.439h8.009l11.909-29.245h-8.008zm11.062 0-4.731 29.283h7.557l4.748-29.283zm22.838 11.739c-2.657-1.338-4.277-2.242-4.277-3.618.037-1.243 1.375-2.525 4.353-2.525 2.468-.057 4.277.528 5.634 1.112l.678.32 1.037-6.331c-1.49-.585-3.863-1.244-6.784-1.244-7.481 0-12.739 3.995-12.776 9.686-.057 4.202 3.768 6.539 6.633 7.952 2.92 1.431 3.92 2.374 3.92 3.636-.038 1.96-2.375 2.865-4.542 2.865-3.015 0-4.636-.471-7.104-1.564l-1-.471-1.054 6.576c1.77.81 5.05 1.527 8.442 1.565 7.952.018 13.134-3.92 13.19-9.988.039-3.335-1.997-5.88-6.35-7.97z" fill="#fff"/></svg>
|
||||||
<svg width="108.54" height="84.42" version="1.1" viewBox="0 0 108.54 84.42" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g id="_x33_63-visa_x2C__Credit_card" transform="matrix(.23596 0 0 .23596 -6.1352 -18.195)"><path d="m486 115.44v281.11c0 21.165-17.17 38.335-38.333 38.335h-383.33c-21.163 0-38.333-17.17-38.333-38.335v-281.11c0-21.164 17.17-38.334 38.333-38.334h383.33c21.163 0 38.333 17.171 38.333 38.334z" fill="#0353a5"/><g fill="#fff"><path d="m421.79 192.11h-24.837c-7.666 0-13.498 2.237-16.771 10.302l-47.678 113.8h33.702s5.511-15.332 6.708-18.606h41.209c0.956 4.392 3.832 18.606 3.832 18.606h29.709zm-39.612 80.1c2.637-7.106 12.778-34.738 12.778-34.738-0.16 0.239 2.636-7.268 4.233-11.9l2.235 10.702s6.07 29.708 7.428 35.937h-26.674z"/><path d="m164.32 192.11-31.385 84.653-3.435-17.171-11.181-57.021c-1.837-7.906-7.507-10.142-14.535-10.461h-51.668l-0.56 2.476c12.618 3.195 23.879 7.827 33.701 13.656l28.591 107.81h33.94l50.471-123.94h-33.939z"/><polygon points="211.2 192.11 191.15 316.21 223.18 316.21 243.3 192.11"/><path d="m307.99 241.86c-11.26-5.67-18.128-9.504-18.128-15.333 0.159-5.271 5.829-10.702 18.448-10.702 10.461-0.24 18.128 2.237 23.877 4.711l2.876 1.358 4.392-26.833c-6.31-2.477-16.371-5.272-28.75-5.272-31.704 0-53.985 16.932-54.145 41.049-0.24 17.809 15.971 27.713 28.11 33.701 12.379 6.068 16.611 10.063 16.611 15.413-0.159 8.307-10.063 12.14-19.247 12.14-12.777 0-19.646-1.998-30.105-6.627l-4.234-1.999-4.472 27.871c7.505 3.435 21.402 6.47 35.777 6.631 33.701 0.078 55.663-16.614 55.902-42.328 0.162-14.134-8.463-24.916-26.912-33.78z"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.2 KiB |
@ -4,12 +4,14 @@ import { DEBUG } from '../config';
|
|||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as Main } from '../components/main/Main';
|
export { default as Main } from '../components/main/Main';
|
||||||
|
export { default as LockScreen } from '../components/main/LockScreen';
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('>>> FINISH LOAD MAIN BUNDLE');
|
console.log('>>> FINISH LOAD MAIN BUNDLE');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getGlobal().connectionState) {
|
const { connectionState, passcode: { isScreenLocked } } = getGlobal();
|
||||||
|
if (!connectionState && !isScreenLocked) {
|
||||||
getActions().initApi();
|
getActions().initApi();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { pick } from '../../util/iteratees';
|
|||||||
import { PLATFORM_ENV } from '../../util/environment';
|
import { PLATFORM_ENV } from '../../util/environment';
|
||||||
import windowSize from '../../util/windowSize';
|
import windowSize from '../../util/windowSize';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
|
|
||||||
import UiLoader from '../common/UiLoader';
|
|
||||||
import AuthPhoneNumber from './AuthPhoneNumber';
|
import AuthPhoneNumber from './AuthPhoneNumber';
|
||||||
import AuthCode from './AuthCode.async';
|
import AuthCode from './AuthCode.async';
|
||||||
import AuthPassword from './AuthPassword.async';
|
import AuthPassword from './AuthPassword.async';
|
||||||
@ -19,19 +19,25 @@ import AuthQrCode from './AuthQrCode';
|
|||||||
|
|
||||||
import './Auth.scss';
|
import './Auth.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authState'>;
|
type StateProps = Pick<GlobalState, 'authState'>;
|
||||||
|
|
||||||
const Auth: FC<StateProps> = ({
|
const Auth: FC<OwnProps & StateProps> = ({
|
||||||
authState,
|
isActive, authState,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode,
|
reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
if (isActive) {
|
||||||
initApi();
|
reset();
|
||||||
}, [reset, initApi]);
|
initApi();
|
||||||
|
}
|
||||||
|
}, [isActive, reset, initApi]);
|
||||||
|
|
||||||
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||||
|
|
||||||
@ -58,24 +64,28 @@ const Auth: FC<StateProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
switch (authState) {
|
// For animation purposes
|
||||||
|
const renderingAuthState = useCurrentOrPrev(
|
||||||
|
authState !== 'authorizationStateReady' ? authState : undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (renderingAuthState) {
|
||||||
case 'authorizationStateWaitCode':
|
case 'authorizationStateWaitCode':
|
||||||
return <UiLoader page="authCode" key="authCode"><AuthCode /></UiLoader>;
|
return <AuthCode />;
|
||||||
case 'authorizationStateWaitPassword':
|
case 'authorizationStateWaitPassword':
|
||||||
return <UiLoader page="authPassword" key="authPassword"><AuthPassword /></UiLoader>;
|
return <AuthPassword />;
|
||||||
case 'authorizationStateWaitRegistration':
|
case 'authorizationStateWaitRegistration':
|
||||||
return <AuthRegister />;
|
return <AuthRegister />;
|
||||||
case 'authorizationStateWaitPhoneNumber':
|
case 'authorizationStateWaitPhoneNumber':
|
||||||
return <UiLoader page="authPhoneNumber" key="authPhoneNumber"><AuthPhoneNumber /></UiLoader>;
|
return <AuthPhoneNumber />;
|
||||||
case 'authorizationStateWaitQrCode':
|
case 'authorizationStateWaitQrCode':
|
||||||
return <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
|
return <AuthQrCode />;
|
||||||
default:
|
default:
|
||||||
return isMobile
|
return isMobile ? <AuthPhoneNumber /> : <AuthQrCode />;
|
||||||
? <UiLoader page="authPhoneNumber" key="authPhoneNumber"><AuthPhoneNumber /></UiLoader>
|
|
||||||
: <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => pick(global, ['authState']),
|
(global): StateProps => pick(global, ['authState']),
|
||||||
)(Auth));
|
)(Auth));
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
import type { IAnchorPosition } from '../../../types';
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
import useRunThrottled from '../../../hooks/useRunThrottled';
|
import useRunThrottled from '../../../hooks/useRunThrottled';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
@ -147,7 +148,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
|||||||
isOpen={isDropdownOpen}
|
isOpen={isDropdownOpen}
|
||||||
positionX="right"
|
positionX="right"
|
||||||
autoClose
|
autoClose
|
||||||
style={anchor ? `right: 1rem; top: ${anchor.y}px;` : undefined}
|
style={buildStyle(anchor && `right: 1rem; top: ${anchor.y}px`)}
|
||||||
onClose={closeDropdown}
|
onClose={closeDropdown}
|
||||||
className="participant-menu"
|
className="participant-menu"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import AnimatedSticker from './AnimatedSticker';
|
|||||||
import './AnimatedEmoji.scss';
|
import './AnimatedEmoji.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
sticker: ApiSticker;
|
sticker?: ApiSticker;
|
||||||
effect?: ApiSticker;
|
effect?: ApiSticker;
|
||||||
isOwn?: boolean;
|
isOwn?: boolean;
|
||||||
soundId?: string;
|
soundId?: string;
|
||||||
@ -56,13 +56,13 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
playKey,
|
playKey,
|
||||||
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteractions, isOwn, undefined, effect?.emoji);
|
} = useAnimatedEmoji(size, chatId, messageId, soundId, activeEmojiInteractions, isOwn, undefined, effect?.emoji);
|
||||||
|
|
||||||
const localMediaHash = `sticker${sticker.id}`;
|
const localMediaHash = `sticker${sticker?.id}`;
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const thumbDataUri = sticker.thumbnail?.dataUri;
|
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
||||||
const previewBlobUrl = useMedia(
|
const previewBlobUrl = useMedia(
|
||||||
`${localMediaHash}?size=m`,
|
sticker ? `${localMediaHash}?size=m` : undefined,
|
||||||
!isIntersecting && !forceLoadPreview,
|
!isIntersecting && !forceLoadPreview,
|
||||||
ApiMediaFormat.BlobUrl,
|
ApiMediaFormat.BlobUrl,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
@ -75,7 +75,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={buildClassName('AnimatedEmoji media-inner', sticker.id === LIKE_STICKER_ID && 'like-sticker-thumb')}
|
className={buildClassName('AnimatedEmoji media-inner', sticker?.id === LIKE_STICKER_ID && 'like-sticker-thumb')}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -18,8 +18,12 @@ type OwnProps = {
|
|||||||
hint?: string;
|
hint?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
shouldDisablePasswordManager?: boolean;
|
||||||
|
shouldShowSubmit?: boolean;
|
||||||
|
shouldResetValue?: boolean;
|
||||||
isPasswordVisible?: boolean;
|
isPasswordVisible?: boolean;
|
||||||
clearError: NoneToVoidFunction;
|
clearError: NoneToVoidFunction;
|
||||||
|
noRipple?: boolean;
|
||||||
onChangePasswordVisibility: (state: boolean) => void;
|
onChangePasswordVisibility: (state: boolean) => void;
|
||||||
onInputChange?: (password: string) => void;
|
onInputChange?: (password: string) => void;
|
||||||
onSubmit: (password: string) => void;
|
onSubmit: (password: string) => void;
|
||||||
@ -34,6 +38,10 @@ const PasswordForm: FC<OwnProps> = ({
|
|||||||
hint,
|
hint,
|
||||||
placeholder = 'Password',
|
placeholder = 'Password',
|
||||||
submitLabel = 'Next',
|
submitLabel = 'Next',
|
||||||
|
shouldShowSubmit,
|
||||||
|
shouldResetValue,
|
||||||
|
shouldDisablePasswordManager = false,
|
||||||
|
noRipple = false,
|
||||||
clearError,
|
clearError,
|
||||||
onChangePasswordVisibility,
|
onChangePasswordVisibility,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
@ -46,6 +54,12 @@ const PasswordForm: FC<OwnProps> = ({
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [canSubmit, setCanSubmit] = useState(false);
|
const [canSubmit, setCanSubmit] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldResetValue) {
|
||||||
|
setPassword('');
|
||||||
|
}
|
||||||
|
}, [shouldResetValue]);
|
||||||
|
|
||||||
useTimeout(() => {
|
useTimeout(() => {
|
||||||
if (!IS_TOUCH_ENV) {
|
if (!IS_TOUCH_ENV) {
|
||||||
inputRef.current!.focus();
|
inputRef.current!.focus();
|
||||||
@ -102,8 +116,9 @@ const PasswordForm: FC<OwnProps> = ({
|
|||||||
type={isPasswordVisible ? 'text' : 'password'}
|
type={isPasswordVisible ? 'text' : 'password'}
|
||||||
id="sign-in-password"
|
id="sign-in-password"
|
||||||
value={password || ''}
|
value={password || ''}
|
||||||
autoComplete="current-password"
|
autoComplete={shouldDisablePasswordManager ? 'one-time-code' : 'current-password'}
|
||||||
onChange={onPasswordChange}
|
onChange={onPasswordChange}
|
||||||
|
maxLength={256}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
<label>{error || hint || placeholder}</label>
|
<label>{error || hint || placeholder}</label>
|
||||||
@ -117,8 +132,8 @@ const PasswordForm: FC<OwnProps> = ({
|
|||||||
<i className={isPasswordVisible ? 'icon-eye' : 'icon-eye-closed'} />
|
<i className={isPasswordVisible ? 'icon-eye' : 'icon-eye-closed'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canSubmit && (
|
{(canSubmit || shouldShowSubmit) && (
|
||||||
<Button type="submit" ripple isLoading={isLoading}>
|
<Button type="submit" ripple={!noRipple} isLoading={isLoading} disabled={!canSubmit}>
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
115
src/components/common/UiLoader.module.scss
Normal file
115
src/components/common/UiLoader.module.scss
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--theme-background-color);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-light) & {
|
||||||
|
background-image: url('../../assets/chat-bg-br.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-image: url('../../assets/chat-bg-pattern-light.png');
|
||||||
|
background-position: top right;
|
||||||
|
background-size: 510px auto;
|
||||||
|
background-repeat: repeat;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
|
||||||
|
:global(html.theme-dark) & {
|
||||||
|
background-image: url('../../assets/chat-bg-pattern-dark.png');
|
||||||
|
mix-blend-mode: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: var(--z-ui-loader-mask);
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--color-background);
|
||||||
|
min-width: 12rem;
|
||||||
|
width: 33vw;
|
||||||
|
max-width: 26.5rem;
|
||||||
|
|
||||||
|
@media (min-width: 926px) {
|
||||||
|
max-width: 40vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1276px) {
|
||||||
|
width: 25vw;
|
||||||
|
max-width: 33vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1275px) {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 925px) {
|
||||||
|
width: 26.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
max-width: none;
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle {
|
||||||
|
flex: 3;
|
||||||
|
border-left: 1px solid var(--color-borders);
|
||||||
|
border-right: 1px solid var(--color-borders);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (max-width: 1275px) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--right-column-width);
|
||||||
|
border-left: 1px solid var(--color-borders);
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blank {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
@ -1,91 +0,0 @@
|
|||||||
#UiLoader {
|
|
||||||
height: 100%;
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .wrapper {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: var(--z-ui-loader-mask);
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
grid-template-rows: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
flex: 1;
|
|
||||||
background: var(--color-background);
|
|
||||||
min-width: 12rem;
|
|
||||||
width: 33vw;
|
|
||||||
max-width: 26.5rem;
|
|
||||||
|
|
||||||
@media (min-width: 926px) {
|
|
||||||
max-width: 40vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1276px) {
|
|
||||||
width: 25vw;
|
|
||||||
max-width: 33vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1275px) {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 925px) {
|
|
||||||
width: 26.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
max-width: none;
|
|
||||||
width: 100vw !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.middle {
|
|
||||||
flex: 3;
|
|
||||||
border-left: 1px solid var(--color-borders);
|
|
||||||
border-right: 1px solid var(--color-borders);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
@media (max-width: 1275px) {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
height: 100%;
|
|
||||||
width: var(--right-column-width);
|
|
||||||
border-left: 1px solid var(--color-borders);
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.blank {
|
|
||||||
flex: 1;
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,9 +6,9 @@ import { ApiMediaFormat } from '../../api/types';
|
|||||||
import type { GlobalState } from '../../global/types';
|
import type { GlobalState } from '../../global/types';
|
||||||
import type { ThemeKey } from '../../types';
|
import type { ThemeKey } from '../../types';
|
||||||
|
|
||||||
import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config';
|
|
||||||
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
|
import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
|
||||||
import { selectIsRightColumnShown, selectTheme } from '../../global/selectors';
|
import { selectIsRightColumnShown, selectTheme } from '../../global/selectors';
|
||||||
|
import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import { pause } from '../../util/schedulers';
|
import { pause } from '../../util/schedulers';
|
||||||
@ -17,30 +17,32 @@ import preloadFonts from '../../util/fonts';
|
|||||||
import * as mediaLoader from '../../util/mediaLoader';
|
import * as mediaLoader from '../../util/mediaLoader';
|
||||||
import { Bundles, loadModule } from '../../util/moduleLoader';
|
import { Bundles, loadModule } from '../../util/moduleLoader';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import buildStyle from '../../util/buildStyle';
|
|
||||||
import useCustomBackground from '../../hooks/useCustomBackground';
|
|
||||||
|
|
||||||
import './UiLoader.scss';
|
import styles from './UiLoader.module.scss';
|
||||||
|
|
||||||
import telegramLogoPath from '../../assets/telegram-logo.svg';
|
import telegramLogoPath from '../../assets/telegram-logo.svg';
|
||||||
import reactionThumbsPath from '../../assets/reaction-thumbs.png';
|
import reactionThumbsPath from '../../assets/reaction-thumbs.png';
|
||||||
|
import lockPreviewPath from '../../assets/lock.png';
|
||||||
import monkeyPath from '../../assets/monkey.svg';
|
import monkeyPath from '../../assets/monkey.svg';
|
||||||
|
|
||||||
|
export type UiLoaderPage =
|
||||||
|
'main'
|
||||||
|
| 'lock'
|
||||||
|
| 'authCode'
|
||||||
|
| 'authPassword'
|
||||||
|
| 'authPhoneNumber'
|
||||||
|
| 'authQrCode';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
page: 'main' | 'authCode' | 'authPassword' | 'authPhoneNumber' | 'authQrCode';
|
page?: UiLoaderPage;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps =
|
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
|
||||||
Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'>
|
isRightColumnShown?: boolean;
|
||||||
& {
|
leftColumnWidth?: number;
|
||||||
isRightColumnShown?: boolean;
|
theme: ThemeKey;
|
||||||
leftColumnWidth?: number;
|
};
|
||||||
isBackgroundBlurred?: boolean;
|
|
||||||
theme: ThemeKey;
|
|
||||||
customBackground?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_PRELOAD_DELAY = 700;
|
const MAX_PRELOAD_DELAY = 700;
|
||||||
const SECOND_STATE_DELAY = 1000;
|
const SECOND_STATE_DELAY = 1000;
|
||||||
@ -81,6 +83,10 @@ const preloadTasks = {
|
|||||||
authCode: () => preloadImage(monkeyPath),
|
authCode: () => preloadImage(monkeyPath),
|
||||||
authPassword: () => preloadImage(monkeyPath),
|
authPassword: () => preloadImage(monkeyPath),
|
||||||
authQrCode: preloadFonts,
|
authQrCode: preloadFonts,
|
||||||
|
lock: () => Promise.all([
|
||||||
|
preloadFonts(),
|
||||||
|
preloadImage(lockPreviewPath),
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const UiLoader: FC<OwnProps & StateProps> = ({
|
const UiLoader: FC<OwnProps & StateProps> = ({
|
||||||
@ -90,9 +96,6 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
theme,
|
theme,
|
||||||
backgroundColor,
|
|
||||||
customBackground,
|
|
||||||
isBackgroundBlurred,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { setIsUiReady } = getActions();
|
const { setIsUiReady } = getActions();
|
||||||
|
|
||||||
@ -101,14 +104,12 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
|||||||
shouldRender: shouldRenderMask, transitionClassNames,
|
shouldRender: shouldRenderMask, transitionClassNames,
|
||||||
} = useShowTransition(!isReady, undefined, true);
|
} = useShowTransition(!isReady, undefined, true);
|
||||||
|
|
||||||
const customBackgroundValue = useCustomBackground(theme, customBackground);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
|
|
||||||
const safePreload = async () => {
|
const safePreload = async () => {
|
||||||
try {
|
try {
|
||||||
await preloadTasks[page]();
|
await preloadTasks[page!]();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@ -116,7 +117,7 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
Promise.race([
|
Promise.race([
|
||||||
pause(MAX_PRELOAD_DELAY),
|
pause(MAX_PRELOAD_DELAY),
|
||||||
safePreload(),
|
page ? safePreload() : Promise.resolve(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
markReady();
|
markReady();
|
||||||
setIsUiReady({ uiReadyState: 1 });
|
setIsUiReady({ uiReadyState: 1 });
|
||||||
@ -137,38 +138,26 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const middleClassName = buildClassName(
|
|
||||||
'middle bg-layers',
|
|
||||||
transitionClassNames,
|
|
||||||
customBackground && 'custom-bg-image',
|
|
||||||
backgroundColor && 'custom-bg-color',
|
|
||||||
customBackground && isBackgroundBlurred && 'blurred',
|
|
||||||
isRightColumnShown && 'with-right-column',
|
|
||||||
);
|
|
||||||
const inlineStyles = [
|
|
||||||
`--theme-background-color: ${backgroundColor || (theme === 'dark' ? DARK_THEME_BG_COLOR : LIGHT_THEME_BG_COLOR)}`,
|
|
||||||
customBackgroundValue && `--custom-background: ${customBackgroundValue}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="UiLoader">
|
<div
|
||||||
|
id="UiLoader"
|
||||||
|
className={styles.root}
|
||||||
|
style={`--theme-background-color: ${theme === 'dark' ? DARK_THEME_BG_COLOR : LIGHT_THEME_BG_COLOR}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
{shouldRenderMask && !shouldSkipHistoryAnimations && (
|
{shouldRenderMask && !shouldSkipHistoryAnimations && Boolean(page) && (
|
||||||
<div className={buildClassName('mask', transitionClassNames)}>
|
<div className={buildClassName(styles.mask, transitionClassNames)}>
|
||||||
{page === 'main' ? (
|
{page === 'main' ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="left"
|
className={styles.left}
|
||||||
style={leftColumnWidth ? `width: ${leftColumnWidth}px` : undefined}
|
style={leftColumnWidth ? `width: ${leftColumnWidth}px` : undefined}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className={buildClassName(styles.middle, transitionClassNames)} />
|
||||||
className={middleClassName}
|
{isRightColumnShown && <div className={styles.right} />}
|
||||||
style={buildStyle(...inlineStyles)}
|
|
||||||
/>
|
|
||||||
{isRightColumnShown && <div className="right" />}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="blank" />
|
<div className={styles.blank} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -179,9 +168,6 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
|||||||
export default withGlobal<OwnProps>(
|
export default withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const theme = selectTheme(global);
|
const theme = selectTheme(global);
|
||||||
const {
|
|
||||||
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor,
|
|
||||||
} = global.settings.themes[theme] || {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||||
@ -189,9 +175,6 @@ export default withGlobal<OwnProps>(
|
|||||||
isRightColumnShown: selectIsRightColumnShown(global),
|
isRightColumnShown: selectIsRightColumnShown(global),
|
||||||
leftColumnWidth: global.leftColumnWidth,
|
leftColumnWidth: global.leftColumnWidth,
|
||||||
theme,
|
theme,
|
||||||
customBackground,
|
|
||||||
isBackgroundBlurred,
|
|
||||||
backgroundColor,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(UiLoader);
|
)(UiLoader);
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs
|
|||||||
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||||
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||||
|
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||||
|
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||||
|
|
||||||
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
|
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
|
||||||
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
||||||
@ -37,6 +39,7 @@ export const ANIMATED_STICKERS_PATHS = {
|
|||||||
FoldersAll,
|
FoldersAll,
|
||||||
FoldersNew,
|
FoldersNew,
|
||||||
DiscussionGroups,
|
DiscussionGroups,
|
||||||
|
Lock,
|
||||||
CameraFlip,
|
CameraFlip,
|
||||||
HandFilled,
|
HandFilled,
|
||||||
HandOutline,
|
HandOutline,
|
||||||
@ -51,6 +54,7 @@ export const ANIMATED_STICKERS_PATHS = {
|
|||||||
JoinRequest,
|
JoinRequest,
|
||||||
Invite,
|
Invite,
|
||||||
QrPlane,
|
QrPlane,
|
||||||
|
Congratulations,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {
|
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.left-header {
|
.left-header {
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
padding: 0.375rem 1rem 0.5rem 0.8125rem;
|
padding: 0.375rem 0.8125rem 0.5rem 0.8125rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.SearchInput {
|
.SearchInput {
|
||||||
margin-left: 0.875rem;
|
margin-left: 0.8125rem;
|
||||||
max-width: calc(100% - 3.25rem);
|
max-width: calc(100% - 3.25rem);
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import captureEscKeyListener from '../../util/captureEscKeyListener';
|
|||||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||||
import { useResize } from '../../hooks/useResize';
|
import { useResize } from '../../hooks/useResize';
|
||||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||||
|
import useOnChange from '../../hooks/useOnChange';
|
||||||
|
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
import LeftMain from './main/LeftMain';
|
import LeftMain from './main/LeftMain';
|
||||||
@ -27,6 +28,8 @@ type StateProps = {
|
|||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
leftColumnWidth?: number;
|
leftColumnWidth?: number;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
|
hasPasscode?: boolean;
|
||||||
|
nextSettingsScreen?: SettingsScreens;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ContentType {
|
enum ContentType {
|
||||||
@ -50,6 +53,8 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
hasPasscode,
|
||||||
|
nextSettingsScreen,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setGlobalSearchQuery,
|
setGlobalSearchQuery,
|
||||||
@ -61,6 +66,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
setLeftColumnWidth,
|
setLeftColumnWidth,
|
||||||
resetLeftColumnWidth,
|
resetLeftColumnWidth,
|
||||||
openChat,
|
openChat,
|
||||||
|
requestNextSettingsScreen,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -91,17 +97,30 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = useCallback((forceReturnToChatList?: boolean) => {
|
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||||
if (content === LeftColumnContent.NewGroupStep2
|
function fullReset() {
|
||||||
&& !forceReturnToChatList
|
setContent(LeftColumnContent.ChatList);
|
||||||
) {
|
setContactsFilter('');
|
||||||
|
setGlobalSearchQuery({ query: '' });
|
||||||
|
setGlobalSearchDate({ date: undefined });
|
||||||
|
setGlobalSearchChatId({ id: undefined });
|
||||||
|
resetChatCreation();
|
||||||
|
setTimeout(() => {
|
||||||
|
setLastResetTime(Date.now());
|
||||||
|
}, RESET_TRANSITION_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceReturnToChatList === true) {
|
||||||
|
fullReset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content === LeftColumnContent.NewGroupStep2) {
|
||||||
setContent(LeftColumnContent.NewGroupStep1);
|
setContent(LeftColumnContent.NewGroupStep1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content === LeftColumnContent.NewChannelStep2
|
if (content === LeftColumnContent.NewChannelStep2) {
|
||||||
&& !forceReturnToChatList
|
|
||||||
) {
|
|
||||||
setContent(LeftColumnContent.NewChannelStep1);
|
setContent(LeftColumnContent.NewChannelStep1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -145,8 +164,33 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
case SettingsScreens.TwoFaDisabled:
|
case SettingsScreens.TwoFaDisabled:
|
||||||
case SettingsScreens.TwoFaEnabled:
|
case SettingsScreens.TwoFaEnabled:
|
||||||
case SettingsScreens.TwoFaCongratulations:
|
case SettingsScreens.TwoFaCongratulations:
|
||||||
|
case SettingsScreens.PasscodeDisabled:
|
||||||
|
case SettingsScreens.PasscodeEnabled:
|
||||||
|
case SettingsScreens.PasscodeCongratulations:
|
||||||
setSettingsScreen(SettingsScreens.Privacy);
|
setSettingsScreen(SettingsScreens.Privacy);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeNewPasscode:
|
||||||
|
setSettingsScreen(hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeCurrent:
|
||||||
|
case SettingsScreens.PasscodeTurnOff:
|
||||||
|
setSettingsScreen(SettingsScreens.PasscodeEnabled);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeNewPasscodeConfirm:
|
||||||
|
setSettingsScreen(SettingsScreens.PasscodeNewPasscode);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeNew:
|
||||||
|
setSettingsScreen(SettingsScreens.PasscodeChangePasscodeCurrent);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeConfirm:
|
||||||
|
setSettingsScreen(SettingsScreens.PasscodeChangePasscodeNew);
|
||||||
|
return;
|
||||||
|
|
||||||
case SettingsScreens.PrivacyPhoneNumberAllowedContacts:
|
case SettingsScreens.PrivacyPhoneNumberAllowedContacts:
|
||||||
case SettingsScreens.PrivacyPhoneNumberDeniedContacts:
|
case SettingsScreens.PrivacyPhoneNumberDeniedContacts:
|
||||||
setSettingsScreen(SettingsScreens.PrivacyPhoneNumber);
|
setSettingsScreen(SettingsScreens.PrivacyPhoneNumber);
|
||||||
@ -235,18 +279,10 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(LeftColumnContent.ChatList);
|
fullReset();
|
||||||
setContactsFilter('');
|
|
||||||
setGlobalSearchQuery({ query: '' });
|
|
||||||
setGlobalSearchDate({ date: undefined });
|
|
||||||
setGlobalSearchChatId({ id: undefined });
|
|
||||||
resetChatCreation();
|
|
||||||
setTimeout(() => {
|
|
||||||
setLastResetTime(Date.now());
|
|
||||||
}, RESET_TRANSITION_DELAY_MS);
|
|
||||||
}, [
|
}, [
|
||||||
content, activeChatFolder, settingsScreen, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId,
|
content, activeChatFolder, settingsScreen, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId,
|
||||||
resetChatCreation,
|
resetChatCreation, hasPasscode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSearchQuery = useCallback((query: string) => {
|
const handleSearchQuery = useCallback((query: string) => {
|
||||||
@ -296,6 +332,14 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
||||||
|
|
||||||
|
useOnChange(() => {
|
||||||
|
if (nextSettingsScreen) {
|
||||||
|
setContent(LeftColumnContent.Settings);
|
||||||
|
setSettingsScreen(nextSettingsScreen);
|
||||||
|
requestNextSettingsScreen(undefined);
|
||||||
|
}
|
||||||
|
}, [nextSettingsScreen, requestNextSettingsScreen]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initResize, resetResize, handleMouseUp,
|
initResize, resetResize, handleMouseUp,
|
||||||
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth);
|
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth);
|
||||||
@ -401,6 +445,12 @@ export default memo(withGlobal(
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
passcode: {
|
||||||
|
hasPasscode,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
nextScreen: nextSettingsScreen,
|
||||||
|
},
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -410,6 +460,8 @@ export default memo(withGlobal(
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
hasPasscode,
|
||||||
|
nextSettingsScreen,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftColumn));
|
)(LeftColumn));
|
||||||
|
|||||||
@ -92,6 +92,10 @@
|
|||||||
@include overflow-y-overlay();
|
@include overflow-y-overlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.passcode-lock {
|
||||||
|
margin-left: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
// @optimization
|
// @optimization
|
||||||
@include while-transition() {
|
@include while-transition() {
|
||||||
.Menu .bubble {
|
.Menu .bubble {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ISettings } from '../../../types';
|
import type { ISettings } from '../../../types';
|
||||||
import { LeftColumnContent } from '../../../types';
|
import { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||||
import type { ApiChat } from '../../../api/types';
|
import type { ApiChat } from '../../../api/types';
|
||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
IS_BETA,
|
IS_BETA,
|
||||||
IS_TEST,
|
IS_TEST,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_PWA, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatDateToString } from '../../../util/dateFormat';
|
import { formatDateToString } from '../../../util/dateFormat';
|
||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
@ -28,6 +28,7 @@ import { selectCurrentMessageList, selectTheme } from '../../../global/selectors
|
|||||||
import { isChatArchived } from '../../../global/helpers';
|
import { isChatArchived } from '../../../global/helpers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||||
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
|
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
@ -64,6 +65,7 @@ type StateProps =
|
|||||||
isMessageListOpen: boolean;
|
isMessageListOpen: boolean;
|
||||||
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||||
areChatsLoaded?: boolean;
|
areChatsLoaded?: boolean;
|
||||||
|
hasPasscode?: boolean;
|
||||||
}
|
}
|
||||||
& Pick<GlobalState, 'connectionState' | 'isSyncing'>;
|
& Pick<GlobalState, 'connectionState' | 'isSyncing'>;
|
||||||
|
|
||||||
@ -93,6 +95,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
isMessageListOpen,
|
isMessageListOpen,
|
||||||
isConnectionStatusMinimized,
|
isConnectionStatusMinimized,
|
||||||
areChatsLoaded,
|
areChatsLoaded,
|
||||||
|
hasPasscode,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
@ -100,6 +103,8 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
setSettingOption,
|
setSettingOption,
|
||||||
setGlobalSearchChatId,
|
setGlobalSearchChatId,
|
||||||
openChatByUsername,
|
openChatByUsername,
|
||||||
|
lockScreen,
|
||||||
|
requestNextSettingsScreen,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -129,6 +134,23 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, !areChatsLoaded,
|
lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, !areChatsLoaded,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleLockScreenHotkey = useCallback((e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (hasPasscode) {
|
||||||
|
lockScreen();
|
||||||
|
} else {
|
||||||
|
requestNextSettingsScreen(SettingsScreens.PasscodeDisabled);
|
||||||
|
}
|
||||||
|
}, [hasPasscode, lockScreen, requestNextSettingsScreen]);
|
||||||
|
|
||||||
|
useHotkeys({
|
||||||
|
'Ctrl+Shift+L': handleLockScreenHotkey,
|
||||||
|
'Alt+Shift+L': handleLockScreenHotkey,
|
||||||
|
'Meta+Shift+L': handleLockScreenHotkey,
|
||||||
|
...(IS_PWA && { 'Meta+L': handleLockScreenHotkey }),
|
||||||
|
});
|
||||||
|
|
||||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
||||||
|
|
||||||
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
@ -167,6 +189,12 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
||||||
}, [currentUserId, openChat]);
|
}, [currentUserId, openChat]);
|
||||||
|
|
||||||
|
const handleSelectPasscode = useCallback(() => {
|
||||||
|
requestNextSettingsScreen(
|
||||||
|
hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled,
|
||||||
|
);
|
||||||
|
}, [hasPasscode, requestNextSettingsScreen]);
|
||||||
|
|
||||||
const handleDarkModeToggle = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
|
const handleDarkModeToggle = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||||
@ -197,6 +225,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
openChatByUsername({ username: lang('Settings.TipsUsername') });
|
openChatByUsername({ username: lang('Settings.TipsUsername') });
|
||||||
}, [lang, openChatByUsername]);
|
}, [lang, openChatByUsername]);
|
||||||
|
|
||||||
|
const handleLockScreen = useCallback(() => {
|
||||||
|
lockScreen();
|
||||||
|
}, [lockScreen]);
|
||||||
|
|
||||||
const isSearchFocused = (
|
const isSearchFocused = (
|
||||||
Boolean(globalSearchChatId)
|
Boolean(globalSearchChatId)
|
||||||
|| content === LeftColumnContent.GlobalSearch
|
|| content === LeftColumnContent.GlobalSearch
|
||||||
@ -243,6 +275,13 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{lang('Settings')}
|
{lang('Settings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="lock"
|
||||||
|
onClick={handleSelectPasscode}
|
||||||
|
>
|
||||||
|
{lang('Passcode')}
|
||||||
|
<span className="menu-item-badge">{lang('New')}</span>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="darkmode"
|
icon="darkmode"
|
||||||
onClick={handleDarkModeToggle}
|
onClick={handleDarkModeToggle}
|
||||||
@ -344,6 +383,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SearchInput>
|
</SearchInput>
|
||||||
|
{hasPasscode && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
ariaLabel={`${lang('ShortcutsController.Others.LockByPasscode')} (Ctrl+Shift+L)`}
|
||||||
|
onClick={handleLockScreen}
|
||||||
|
className="passcode-lock"
|
||||||
|
>
|
||||||
|
<i className="icon-lock" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<ShowTransition
|
<ShowTransition
|
||||||
isOpen={connectionStatusPosition === 'overlay'}
|
isOpen={connectionStatusPosition === 'overlay'}
|
||||||
isCustom
|
isCustom
|
||||||
@ -383,6 +435,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||||
isConnectionStatusMinimized,
|
isConnectionStatusMinimized,
|
||||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||||
|
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftMainHeader));
|
)(LeftMainHeader));
|
||||||
|
|||||||
@ -40,10 +40,14 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@include overflow-y-overlay();
|
@include overflow-y-overlay();
|
||||||
|
|
||||||
&.no-border, &.two-fa {
|
&.no-border, &.two-fa, &.local-passcode, &.password-form {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.password-form .input-group.error label::first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
&.infinite-scroll {
|
&.infinite-scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -152,7 +156,9 @@
|
|||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
.settings-content.two-fa & {
|
.settings-content.two-fa &,
|
||||||
|
.settings-content.password-form &,
|
||||||
|
.settings-content.local-passcode & {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
import type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
@ -25,6 +25,7 @@ import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
|||||||
import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
||||||
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
||||||
import SettingsQuickReaction from './SettingsQuickReaction';
|
import SettingsQuickReaction from './SettingsQuickReaction';
|
||||||
|
import SettingsPasscode from './passcode/SettingsPasscode';
|
||||||
|
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
|
|
||||||
@ -50,6 +51,11 @@ const TWO_FA_SCREENS = [
|
|||||||
SettingsScreens.TwoFaRecoveryEmailCode,
|
SettingsScreens.TwoFaRecoveryEmailCode,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PASSCODE_SCREENS = [
|
||||||
|
SettingsScreens.PasscodeDisabled,
|
||||||
|
SettingsScreens.PasscodeEnabled,
|
||||||
|
];
|
||||||
|
|
||||||
const FOLDERS_SCREENS = [
|
const FOLDERS_SCREENS = [
|
||||||
SettingsScreens.Folders,
|
SettingsScreens.Folders,
|
||||||
SettingsScreens.FoldersCreateFolder,
|
SettingsScreens.FoldersCreateFolder,
|
||||||
@ -108,7 +114,7 @@ export type OwnProps = {
|
|||||||
foldersDispatch: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
onScreenSelect: (screen: SettingsScreens) => void;
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
shouldSkipTransition?: boolean;
|
shouldSkipTransition?: boolean;
|
||||||
onReset: () => void;
|
onReset: (forceReturnToChatList?: true | Event) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings: FC<OwnProps> = ({
|
const Settings: FC<OwnProps> = ({
|
||||||
@ -121,8 +127,14 @@ const Settings: FC<OwnProps> = ({
|
|||||||
shouldSkipTransition,
|
shouldSkipTransition,
|
||||||
}) => {
|
}) => {
|
||||||
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
|
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
|
||||||
|
const [privacyPasscode, setPrivacyPasscode] = useState<string>('');
|
||||||
|
|
||||||
|
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||||
|
if (forceReturnToChatList === true) {
|
||||||
|
onReset(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
|
||||||
if (
|
if (
|
||||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||||
@ -168,9 +180,11 @@ const Settings: FC<OwnProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isTwoFaScreen = TWO_FA_SCREENS.includes(screen);
|
const isTwoFaScreen = TWO_FA_SCREENS.includes(screen);
|
||||||
|
const isPasscodeScreen = PASSCODE_SCREENS.includes(screen);
|
||||||
const isFoldersScreen = FOLDERS_SCREENS.includes(screen);
|
const isFoldersScreen = FOLDERS_SCREENS.includes(screen);
|
||||||
const isPrivacyScreen = PRIVACY_SCREENS.includes(screen)
|
const isPrivacyScreen = PRIVACY_SCREENS.includes(screen)
|
||||||
|| isTwoFaScreen
|
|| isTwoFaScreen
|
||||||
|
|| isPasscodeScreen
|
||||||
|| Object.keys(privacyAllowScreens).includes(screen.toString())
|
|| Object.keys(privacyAllowScreens).includes(screen.toString())
|
||||||
|| Object.values(privacyAllowScreens).find((key) => key === true);
|
|| Object.values(privacyAllowScreens).find((key) => key === true);
|
||||||
|
|
||||||
@ -191,10 +205,10 @@ const Settings: FC<OwnProps> = ({
|
|||||||
<SettingsGeneral
|
<SettingsGeneral
|
||||||
onScreenSelect={onScreenSelect}
|
onScreenSelect={onScreenSelect}
|
||||||
isActive={isScreenActive
|
isActive={isScreenActive
|
||||||
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
||||||
|| screen === SettingsScreens.GeneralChatBackground
|
|| screen === SettingsScreens.GeneralChatBackground
|
||||||
|| screen === SettingsScreens.QuickReaction
|
|| screen === SettingsScreens.QuickReaction
|
||||||
|| isPrivacyScreen || isFoldersScreen}
|
|| isPrivacyScreen || isFoldersScreen}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -214,7 +228,7 @@ const Settings: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SettingsPrivacy
|
<SettingsPrivacy
|
||||||
onScreenSelect={onScreenSelect}
|
onScreenSelect={onScreenSelect}
|
||||||
isActive={isScreenActive || isPrivacyScreen || isTwoFaScreen}
|
isActive={isScreenActive || isPrivacyScreen}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -348,6 +362,27 @@ const Settings: FC<OwnProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeDisabled:
|
||||||
|
case SettingsScreens.PasscodeNewPasscode:
|
||||||
|
case SettingsScreens.PasscodeNewPasscodeConfirm:
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeCurrent:
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeNew:
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeConfirm:
|
||||||
|
case SettingsScreens.PasscodeCongratulations:
|
||||||
|
case SettingsScreens.PasscodeEnabled:
|
||||||
|
case SettingsScreens.PasscodeTurnOff:
|
||||||
|
return (
|
||||||
|
<SettingsPasscode
|
||||||
|
currentScreen={currentScreen}
|
||||||
|
passcode={privacyPasscode}
|
||||||
|
onSetPasscode={setPrivacyPasscode}
|
||||||
|
shownScreen={screen}
|
||||||
|
isActive={isScreenActive}
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,6 +157,23 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
case SettingsScreens.TwoFaRecoveryEmailCurrentPassword:
|
case SettingsScreens.TwoFaRecoveryEmailCurrentPassword:
|
||||||
return <h3>{lang('PleaseEnterCurrentPassword')}</h3>;
|
return <h3>{lang('PleaseEnterCurrentPassword')}</h3>;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeDisabled:
|
||||||
|
case SettingsScreens.PasscodeEnabled:
|
||||||
|
case SettingsScreens.PasscodeNewPasscode:
|
||||||
|
case SettingsScreens.PasscodeNewPasscodeConfirm:
|
||||||
|
case SettingsScreens.PasscodeCongratulations:
|
||||||
|
return <h3>{lang('Passcode')}</h3>;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeTurnOff:
|
||||||
|
return <h3>{lang('PasscodeController.Disable.Title')}</h3>;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeCurrent:
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeNew:
|
||||||
|
return <h3>{lang('PasscodeController.Change.Title')}</h3>;
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeConfirm:
|
||||||
|
return <h3>{lang('PasscodeController.ReEnterPasscode.Placeholder')}</h3>;
|
||||||
|
|
||||||
case SettingsScreens.Folders:
|
case SettingsScreens.Folders:
|
||||||
return <h3>{lang('Filters')}</h3>;
|
return <h3>{lang('Filters')}</h3>;
|
||||||
case SettingsScreens.FoldersCreateFolder:
|
case SettingsScreens.FoldersCreateFolder:
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import type { FC } from '../../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, { memo, useCallback, useState } from '../../../../lib/teact/teact';
|
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import PasswordMonkey from '../../../common/PasswordMonkey';
|
import PasswordMonkey from '../../common/PasswordMonkey';
|
||||||
import PasswordForm from '../../../common/PasswordForm';
|
import PasswordForm from '../../common/PasswordForm';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
error?: string;
|
error?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
shouldDisablePasswordManager?: boolean;
|
||||||
expectedPassword?: string;
|
expectedPassword?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
@ -22,11 +23,12 @@ type OwnProps = {
|
|||||||
|
|
||||||
const EQUAL_PASSWORD_ERROR = 'Passwords Should Be Equal';
|
const EQUAL_PASSWORD_ERROR = 'Passwords Should Be Equal';
|
||||||
|
|
||||||
const SettingsTwoFaPassword: FC<OwnProps> = ({
|
const SettingsPasswordForm: FC<OwnProps> = ({
|
||||||
isActive,
|
isActive,
|
||||||
onReset,
|
onReset,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
shouldDisablePasswordManager,
|
||||||
expectedPassword,
|
expectedPassword,
|
||||||
placeholder = 'Current Password',
|
placeholder = 'Current Password',
|
||||||
hint,
|
hint,
|
||||||
@ -60,7 +62,7 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-content two-fa custom-scroll">
|
<div className="settings-content password-form custom-scroll">
|
||||||
<div className="settings-content-header no-border">
|
<div className="settings-content-header no-border">
|
||||||
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
|
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
|
||||||
</div>
|
</div>
|
||||||
@ -70,10 +72,12 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
|||||||
error={validationError || error}
|
error={validationError || error}
|
||||||
hint={hint}
|
hint={hint}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
shouldDisablePasswordManager={shouldDisablePasswordManager}
|
||||||
submitLabel={submitLabel || lang('Next')}
|
submitLabel={submitLabel || lang('Next')}
|
||||||
clearError={handleClearError}
|
clearError={handleClearError}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isPasswordVisible={shouldShowPassword}
|
isPasswordVisible={shouldShowPassword}
|
||||||
|
shouldResetValue={isActive}
|
||||||
onChangePasswordVisibility={setShouldShowPassword}
|
onChangePasswordVisibility={setShouldShowPassword}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
@ -82,4 +86,4 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(SettingsTwoFaPassword);
|
export default memo(SettingsPasswordForm);
|
||||||
@ -19,6 +19,7 @@ type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
hasPassword?: boolean;
|
hasPassword?: boolean;
|
||||||
|
hasPasscode?: boolean;
|
||||||
blockedCount: number;
|
blockedCount: number;
|
||||||
isSensitiveEnabled?: boolean;
|
isSensitiveEnabled?: boolean;
|
||||||
canChangeSensitive?: boolean;
|
canChangeSensitive?: boolean;
|
||||||
@ -36,6 +37,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
hasPassword,
|
hasPassword,
|
||||||
|
hasPasscode,
|
||||||
blockedCount,
|
blockedCount,
|
||||||
isSensitiveEnabled,
|
isSensitiveEnabled,
|
||||||
canChangeSensitive,
|
canChangeSensitive,
|
||||||
@ -112,6 +114,21 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
icon="key"
|
||||||
|
narrow
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(
|
||||||
|
hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="multiline-menu-item">
|
||||||
|
<span className="title">{lang('Passcode')}</span>
|
||||||
|
<span className="subtitle" dir="auto">
|
||||||
|
{lang(hasPasscode ? 'PasswordOn' : 'PasswordOff')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="lock"
|
icon="lock"
|
||||||
narrow
|
narrow
|
||||||
@ -247,14 +264,20 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const {
|
const {
|
||||||
settings: {
|
settings: {
|
||||||
byKey: { hasPassword, isSensitiveEnabled, canChangeSensitive },
|
byKey: {
|
||||||
|
hasPassword, isSensitiveEnabled, canChangeSensitive,
|
||||||
|
},
|
||||||
privacy,
|
privacy,
|
||||||
},
|
},
|
||||||
blocked,
|
blocked,
|
||||||
|
passcode: {
|
||||||
|
hasPasscode,
|
||||||
|
},
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasPassword,
|
hasPassword,
|
||||||
|
hasPasscode: Boolean(hasPasscode),
|
||||||
blockedCount: blocked.totalCount,
|
blockedCount: blocked.totalCount,
|
||||||
isSensitiveEnabled,
|
isSensitiveEnabled,
|
||||||
canChangeSensitive,
|
canChangeSensitive,
|
||||||
|
|||||||
225
src/components/left/settings/passcode/SettingsPasscode.tsx
Normal file
225
src/components/left/settings/passcode/SettingsPasscode.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import type { FC } from '../../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { GlobalState } from '../../../../global/types';
|
||||||
|
import { SettingsScreens } from '../../../../types';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import { decryptSession } from '../../../../util/passcode';
|
||||||
|
|
||||||
|
import SettingsPasscodeStart from './SettingsPasscodeStart';
|
||||||
|
import SettingsPasscodeForm from '../SettingsPasswordForm';
|
||||||
|
import SettingsPasscodeEnabled from './SettingsPasscodeEnabled';
|
||||||
|
import SettingsPasscodeCongratulations from './SettingsPasscodeCongratulations';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
passcode: string;
|
||||||
|
currentScreen: SettingsScreens;
|
||||||
|
shownScreen: SettingsScreens;
|
||||||
|
isActive?: boolean;
|
||||||
|
onSetPasscode: (passcode: string) => void;
|
||||||
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = GlobalState['passcode'];
|
||||||
|
|
||||||
|
const SettingsPasscode: FC<OwnProps & StateProps> = ({
|
||||||
|
passcode,
|
||||||
|
currentScreen,
|
||||||
|
shownScreen,
|
||||||
|
error,
|
||||||
|
isActive,
|
||||||
|
isLoading,
|
||||||
|
onScreenSelect,
|
||||||
|
onSetPasscode,
|
||||||
|
onReset,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
setPasscode,
|
||||||
|
clearPasscode,
|
||||||
|
setPasscodeError,
|
||||||
|
clearPasscodeError,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleStartWizard = useCallback(() => {
|
||||||
|
onSetPasscode('');
|
||||||
|
onScreenSelect(SettingsScreens.PasscodeNewPasscode);
|
||||||
|
}, [onScreenSelect, onSetPasscode]);
|
||||||
|
|
||||||
|
const handleNewPassword = useCallback((value: string) => {
|
||||||
|
onSetPasscode(value);
|
||||||
|
onScreenSelect(SettingsScreens.PasscodeNewPasscodeConfirm);
|
||||||
|
}, [onScreenSelect, onSetPasscode]);
|
||||||
|
|
||||||
|
const handleNewPasswordConfirm = useCallback(() => {
|
||||||
|
setPasscode({ passcode });
|
||||||
|
onSetPasscode('');
|
||||||
|
onScreenSelect(SettingsScreens.PasscodeCongratulations);
|
||||||
|
}, [onScreenSelect, onSetPasscode, passcode, setPasscode]);
|
||||||
|
|
||||||
|
const handleChangePasswordCurrent = useCallback((currentPasscode: string) => {
|
||||||
|
onSetPasscode('');
|
||||||
|
decryptSession(currentPasscode).then(() => {
|
||||||
|
onScreenSelect(SettingsScreens.PasscodeChangePasscodeNew);
|
||||||
|
}, () => {
|
||||||
|
setPasscodeError({
|
||||||
|
error: lang('PasscodeController.Error.Current'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [lang, onScreenSelect, onSetPasscode, setPasscodeError]);
|
||||||
|
|
||||||
|
const handleChangePasswordNew = useCallback((value: string) => {
|
||||||
|
onSetPasscode(value);
|
||||||
|
onScreenSelect(SettingsScreens.PasscodeChangePasscodeConfirm);
|
||||||
|
}, [onScreenSelect, onSetPasscode]);
|
||||||
|
|
||||||
|
const handleTurnOff = useCallback((currentPasscode: string) => {
|
||||||
|
decryptSession(currentPasscode).then(() => {
|
||||||
|
clearPasscode();
|
||||||
|
onScreenSelect(SettingsScreens.Privacy);
|
||||||
|
}, () => {
|
||||||
|
setPasscodeError({
|
||||||
|
error: lang('PasscodeController.Error.Current'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [clearPasscode, lang, onScreenSelect, setPasscodeError]);
|
||||||
|
|
||||||
|
switch (currentScreen) {
|
||||||
|
case SettingsScreens.PasscodeDisabled:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeStart
|
||||||
|
onStart={handleStartWizard}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeNewPasscode,
|
||||||
|
SettingsScreens.PasscodeNewPasscodeConfirm,
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeNewPasscode:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
placeholder={lang('EnterNewPasscode')}
|
||||||
|
submitLabel={lang('Continue')}
|
||||||
|
onSubmit={handleNewPassword}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeNewPasscodeConfirm,
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeNewPasscodeConfirm:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
expectedPassword={passcode}
|
||||||
|
placeholder={lang('ReEnterYourPasscode')}
|
||||||
|
submitLabel={lang('Continue')}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onSubmit={handleNewPasswordConfirm}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeCongratulations:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeCongratulations
|
||||||
|
isActive={isActive}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeEnabled:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeEnabled
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeChangePasscodeCurrent,
|
||||||
|
SettingsScreens.PasscodeChangePasscodeNew,
|
||||||
|
SettingsScreens.PasscodeChangePasscodeConfirm,
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
SettingsScreens.PasscodeTurnOff,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeCurrent:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
error={error}
|
||||||
|
clearError={clearPasscodeError}
|
||||||
|
placeholder={lang('PasscodeController.Current.Placeholder')}
|
||||||
|
onSubmit={handleChangePasswordCurrent}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeChangePasscodeNew,
|
||||||
|
SettingsScreens.PasscodeChangePasscodeConfirm,
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeNew:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
placeholder={lang('PleaseEnterNewFirstPassword')}
|
||||||
|
onSubmit={handleChangePasswordNew}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeChangePasscodeConfirm,
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeChangePasscodeConfirm:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
expectedPassword={passcode}
|
||||||
|
placeholder={lang('PasscodeController.ReEnterPasscode.Placeholder')}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onSubmit={handleNewPasswordConfirm}
|
||||||
|
isActive={isActive || [
|
||||||
|
SettingsScreens.PasscodeCongratulations,
|
||||||
|
].includes(shownScreen)}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.PasscodeTurnOff:
|
||||||
|
return (
|
||||||
|
<SettingsPasscodeForm
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
error={error ? lang(error) : undefined}
|
||||||
|
clearError={clearPasscodeError}
|
||||||
|
placeholder={lang('PasscodeController.Current.Placeholder')}
|
||||||
|
onSubmit={handleTurnOff}
|
||||||
|
isActive={isActive}
|
||||||
|
onReset={onReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => ({ ...global.passcode }),
|
||||||
|
)(SettingsPasscode));
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import type { FC } from '../../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { STICKER_SIZE_PASSCODE } from '../../../../config';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import Button from '../../../ui/Button';
|
||||||
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: (forceReturnToChatList?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsPasscodeCongratulations: FC<OwnProps> = ({
|
||||||
|
isActive, onReset,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const fullReset = useCallback(() => {
|
||||||
|
onReset(true);
|
||||||
|
}, [onReset]);
|
||||||
|
|
||||||
|
useHistoryBack({ isActive, onBack: onReset });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content local-passcode custom-scroll">
|
||||||
|
<div className="settings-content-header no-border">
|
||||||
|
<AnimatedIcon size={STICKER_SIZE_PASSCODE} name="Congratulations" />
|
||||||
|
|
||||||
|
<p className="settings-item-description mb-3" dir="auto">
|
||||||
|
Congratulations!
|
||||||
|
</p>
|
||||||
|
<p className="settings-item-description mb-3" dir="auto">
|
||||||
|
Now you can lock the app with a passcode so that others can't open it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-item pt-0">
|
||||||
|
<Button onClick={fullReset}>{lang('Back')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SettingsPasscodeCongratulations);
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import type { FC } from '../../../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiSticker } from '../../../../api/types';
|
||||||
|
import { SettingsScreens } from '../../../../types';
|
||||||
|
|
||||||
|
import { selectAnimatedEmoji } from '../../../../global/selectors';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import ListItem from '../../../ui/ListItem';
|
||||||
|
import AnimatedEmoji from '../../../common/AnimatedEmoji';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
animatedEmoji: ApiSticker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsPasscodeEnabled: FC<OwnProps & StateProps> = ({
|
||||||
|
isActive, onReset, animatedEmoji, onScreenSelect,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
useHistoryBack({ isActive, onBack: onReset });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content local-passcode custom-scroll">
|
||||||
|
<div className="settings-content-header no-border">
|
||||||
|
<AnimatedEmoji sticker={animatedEmoji} size="large" />
|
||||||
|
|
||||||
|
<p className="settings-item-description mb-3" dir="auto">
|
||||||
|
Local passcode is enabled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-item pt-0">
|
||||||
|
<ListItem
|
||||||
|
icon="edit"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.PasscodeChangePasscodeCurrent)}
|
||||||
|
>
|
||||||
|
{lang('Passcode.Change')}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
icon="password-off"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.PasscodeTurnOff)}
|
||||||
|
>
|
||||||
|
{lang('Passcode.TurnOff')}
|
||||||
|
</ListItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global) => {
|
||||||
|
return {
|
||||||
|
animatedEmoji: selectAnimatedEmoji(global, '🔐'),
|
||||||
|
};
|
||||||
|
})(SettingsPasscodeEnabled));
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import type { FC } from '../../../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { STICKER_SIZE_PASSCODE } from '../../../../config';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import Button from '../../../ui/Button';
|
||||||
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
onStart: NoneToVoidFunction;
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsPasscodeStart: FC<OwnProps> = ({
|
||||||
|
isActive, onReset, onStart,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
useHistoryBack({ isActive, onBack: onReset });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content local-passcode custom-scroll">
|
||||||
|
<div className="settings-content-header no-border">
|
||||||
|
<AnimatedIcon size={STICKER_SIZE_PASSCODE} name="Lock" />
|
||||||
|
|
||||||
|
<p className="settings-item-description" dir="auto">
|
||||||
|
When you set up an additional passcode, a lock icon will appear on the chats page.
|
||||||
|
Tap it to lock and unlock your Telegram WebZ.
|
||||||
|
</p>
|
||||||
|
<p className="settings-item-description mb-3" dir="auto">
|
||||||
|
Note: if you forget your local passcode, you'll need to log out of Telegram WebZ and log in again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-item pt-0">
|
||||||
|
<Button onClick={onStart}>{lang('EnablePasscode')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SettingsPasscodeStart);
|
||||||
@ -9,7 +9,7 @@ import type { TwoFaDispatch, TwoFaState } from '../../../../hooks/reducers/useTw
|
|||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
|
||||||
import SettingsTwoFaEnabled from './SettingsTwoFaEnabled';
|
import SettingsTwoFaEnabled from './SettingsTwoFaEnabled';
|
||||||
import SettingsTwoFaPassword from './SettingsTwoFaPassword';
|
import SettingsTwoFaPassword from '../SettingsPasswordForm';
|
||||||
import SettingsTwoFaStart from './SettingsTwoFaStart';
|
import SettingsTwoFaStart from './SettingsTwoFaStart';
|
||||||
import SettingsTwoFaSkippableForm from './SettingsTwoFaSkippableForm';
|
import SettingsTwoFaSkippableForm from './SettingsTwoFaSkippableForm';
|
||||||
import SettingsTwoFaCongratulations from './SettingsTwoFaCongratulations';
|
import SettingsTwoFaCongratulations from './SettingsTwoFaCongratulations';
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import type { FC } from '../../../../lib/teact/teact';
|
import type { FC } from '../../../../lib/teact/teact';
|
||||||
import React, { memo, useCallback } from '../../../../lib/teact/teact';
|
import React, { memo, useCallback } from '../../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../../global';
|
|
||||||
|
|
||||||
import type { ApiSticker } from '../../../../api/types';
|
|
||||||
import { SettingsScreens } from '../../../../types';
|
import { SettingsScreens } from '../../../../types';
|
||||||
|
|
||||||
import { selectAnimatedEmoji } from '../../../../global/selectors';
|
import { STICKER_SIZE_TWO_FA } from '../../../../config';
|
||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import Button from '../../../ui/Button';
|
import Button from '../../../ui/Button';
|
||||||
import AnimatedEmoji from '../../../common/AnimatedEmoji';
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -18,12 +16,8 @@ type OwnProps = {
|
|||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
const SettingsTwoFaCongratulations: FC<OwnProps> = ({
|
||||||
animatedEmoji: ApiSticker;
|
isActive, onReset, onScreenSelect,
|
||||||
};
|
|
||||||
|
|
||||||
const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
|
||||||
isActive, onReset, animatedEmoji, onScreenSelect,
|
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -39,7 +33,7 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="settings-content two-fa custom-scroll">
|
<div className="settings-content two-fa custom-scroll">
|
||||||
<div className="settings-content-header no-border">
|
<div className="settings-content-header no-border">
|
||||||
<AnimatedEmoji sticker={animatedEmoji} size="large" />
|
<AnimatedIcon size={STICKER_SIZE_TWO_FA} name="Congratulations" />
|
||||||
|
|
||||||
<p className="settings-item-description mb-3" dir="auto">
|
<p className="settings-item-description mb-3" dir="auto">
|
||||||
{lang('TwoStepVerificationPasswordSetInfo')}
|
{lang('TwoStepVerificationPasswordSetInfo')}
|
||||||
@ -53,8 +47,4 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>((global) => {
|
export default memo(SettingsTwoFaCongratulations);
|
||||||
return {
|
|
||||||
animatedEmoji: selectAnimatedEmoji(global, '🥳'),
|
|
||||||
};
|
|
||||||
})(SettingsTwoFaCongratulations));
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
17
src/components/main/LockScreen.async.tsx
Normal file
17
src/components/main/LockScreen.async.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
|
import type { OwnProps } from './LockScreen';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const LockScreenAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isLocked } = props;
|
||||||
|
const LockScreen = useModuleLoader(Bundles.Main, 'LockScreen', !isLocked);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return LockScreen ? <LockScreen {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(LockScreenAsync);
|
||||||
51
src/components/main/LockScreen.module.scss
Normal file
51
src/components/main/LockScreen.module.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.container {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
max-width: 20rem;
|
||||||
|
padding: 1.5rem 1rem 0;
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
width: 10rem;
|
||||||
|
height: 10rem;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconAnimated,
|
||||||
|
.iconStatic {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 10rem;
|
||||||
|
height: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconStatic {
|
||||||
|
background: url("../../assets/lock.png") no-repeat center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
184
src/components/main/LockScreen.tsx
Normal file
184
src/components/main/LockScreen.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { GlobalState } from '../../global/types';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { decryptSession } from '../../util/passcode';
|
||||||
|
import getAnimationData from '../common/helpers/animatedAssets';
|
||||||
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
import useTimeout from '../../hooks/useTimeout';
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
|
||||||
|
import AnimatedSticker from '../common/AnimatedSticker';
|
||||||
|
import PasswordForm from '../common/PasswordForm';
|
||||||
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import Link from '../ui/Link';
|
||||||
|
|
||||||
|
import styles from './LockScreen.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isLocked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
passcodeSettings: GlobalState['passcode'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_INVALID_ATTEMPTS = 5;
|
||||||
|
const TIMEOUT_RESET_INVALID_ATTEMPTS_MS = 180000; // 3 minutes
|
||||||
|
const ICON_SIZE = 160;
|
||||||
|
|
||||||
|
const LockScreen: FC<OwnProps & StateProps> = ({
|
||||||
|
isLocked,
|
||||||
|
passcodeSettings,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
unlockScreen,
|
||||||
|
signOut,
|
||||||
|
logInvalidUnlockAttempt,
|
||||||
|
resetInvalidUnlockAttempts,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const {
|
||||||
|
invalidAttemptsCount,
|
||||||
|
isLoading,
|
||||||
|
} = passcodeSettings;
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [validationError, setValidationError] = useState<string>('');
|
||||||
|
const [shouldShowPasscode, setShouldShowPasscode] = useState(false);
|
||||||
|
const [isSignOutDialogOpen, openSignOutConfirmation, closeSignOutConfirmation] = useFlag(false);
|
||||||
|
const { transitionClassNames, shouldRender } = useShowTransition(isLocked);
|
||||||
|
const [animationData, setAnimationData] = useState<string>();
|
||||||
|
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
||||||
|
const shouldRenderAnimated = Boolean(animationData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAnimationData('Lock').then(setAnimationData);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { transitionClassNames: animatedClassNames } = useShowTransition(shouldRenderAnimated);
|
||||||
|
const { shouldRender: shouldRenderStatic, transitionClassNames: staticClassNames } = useShowTransition(
|
||||||
|
!isAnimationLoaded, undefined, true,
|
||||||
|
);
|
||||||
|
|
||||||
|
useTimeout(
|
||||||
|
resetInvalidUnlockAttempts,
|
||||||
|
invalidAttemptsCount && invalidAttemptsCount >= MAX_INVALID_ATTEMPTS
|
||||||
|
? TIMEOUT_RESET_INVALID_ATTEMPTS_MS
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearError = useCallback(() => {
|
||||||
|
setValidationError('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback((passcode: string) => {
|
||||||
|
if (invalidAttemptsCount && invalidAttemptsCount >= MAX_INVALID_ATTEMPTS) {
|
||||||
|
setValidationError(lang('FloodWait'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationError('');
|
||||||
|
decryptSession(passcode).then(unlockScreen, () => {
|
||||||
|
logInvalidUnlockAttempt();
|
||||||
|
setValidationError(lang('lng_passcode_wrong'));
|
||||||
|
});
|
||||||
|
}, [invalidAttemptsCount, lang, logInvalidUnlockAttempt, unlockScreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (invalidAttemptsCount && invalidAttemptsCount >= MAX_INVALID_ATTEMPTS) {
|
||||||
|
setValidationError(lang('FloodWait'));
|
||||||
|
} else if (invalidAttemptsCount === 0) {
|
||||||
|
setValidationError('');
|
||||||
|
}
|
||||||
|
}, [invalidAttemptsCount, lang]);
|
||||||
|
|
||||||
|
const handleSignOutMessage = useCallback(() => {
|
||||||
|
closeSignOutConfirmation();
|
||||||
|
signOut();
|
||||||
|
}, [closeSignOutConfirmation, signOut]);
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLogoutPrompt() {
|
||||||
|
return (
|
||||||
|
<div className={styles.help}>
|
||||||
|
<p>
|
||||||
|
<Link onClick={openSignOutConfirmation}>Log out</Link>{' '}
|
||||||
|
if you don't remember your passcode.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Button color="translucent" size="tiny" isText onClick={openSignOutConfirmation}>
|
||||||
|
{lang('AccountSettings.Logout')}
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.container, transitionClassNames)}>
|
||||||
|
<div className={styles.wrapper} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
{shouldRenderStatic && (
|
||||||
|
<div className={buildClassName(styles.iconStatic, staticClassNames)} />
|
||||||
|
)}
|
||||||
|
{shouldRenderAnimated && (
|
||||||
|
<AnimatedSticker
|
||||||
|
id="lock_screen_icon"
|
||||||
|
animationData={animationData}
|
||||||
|
className={buildClassName(styles.iconAnimated, animatedClassNames)}
|
||||||
|
play
|
||||||
|
noLoop
|
||||||
|
size={ICON_SIZE}
|
||||||
|
onLoad={markAnimationLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordForm
|
||||||
|
key="password-form"
|
||||||
|
shouldShowSubmit
|
||||||
|
shouldDisablePasswordManager
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={validationError}
|
||||||
|
placeholder={lang('Passcode.EnterPasscodePlaceholder')}
|
||||||
|
submitLabel={lang('Next')}
|
||||||
|
clearError={handleClearError}
|
||||||
|
isPasswordVisible={shouldShowPasscode}
|
||||||
|
noRipple
|
||||||
|
onChangePasswordVisibility={setShouldShowPasscode}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{renderLogoutPrompt()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isSignOutDialogOpen}
|
||||||
|
onClose={closeSignOutConfirmation}
|
||||||
|
text={lang('lng_sure_logout')}
|
||||||
|
confirmLabel={lang('AccountSettings.Logout')}
|
||||||
|
confirmHandler={handleSignOutMessage}
|
||||||
|
confirmIsDestructive
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
return {
|
||||||
|
passcodeSettings: global.passcode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(LockScreen));
|
||||||
@ -33,6 +33,7 @@
|
|||||||
max-width: 26.5rem;
|
max-width: 26.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -356,7 +356,6 @@ const Main: FC<StateProps> = ({
|
|||||||
// Online status and browser tab indicators
|
// Online status and browser tab indicators
|
||||||
useBackgroundMode(handleBlur, handleFocus);
|
useBackgroundMode(handleBlur, handleFocus);
|
||||||
useBeforeUnload(handleBlur);
|
useBeforeUnload(handleBlur);
|
||||||
|
|
||||||
usePreventPinchZoomGesture(isMediaViewerOpen);
|
usePreventPinchZoomGesture(isMediaViewerOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -419,7 +418,13 @@ function updatePageTitle(nextTitle: string) {
|
|||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const { settings: { byKey: { animationLevel, language, wasTimeFormatSetManually } } } = global;
|
const {
|
||||||
|
settings: {
|
||||||
|
byKey: {
|
||||||
|
animationLevel, language, wasTimeFormatSetManually,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = global;
|
||||||
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
||||||
const audioMessage = audioChatId && audioMessageId
|
const audioMessage = audioChatId && audioMessageId
|
||||||
? selectChatMessage(global, audioChatId, audioMessageId)
|
? selectChatMessage(global, audioChatId, audioMessageId)
|
||||||
|
|||||||
@ -153,7 +153,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
}, [canSearch, handleSearchClick]);
|
}, [canSearch, handleSearchClick]);
|
||||||
|
|
||||||
useHotkeys({
|
useHotkeys({
|
||||||
'meta+F': handleHotkeySearchClick,
|
'Meta+F': handleHotkeySearchClick,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
.bg-layers {
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: -1;
|
||||||
|
overflow: hidden;
|
||||||
background-color: var(--theme-background-color);
|
background-color: var(--theme-background-color);
|
||||||
|
|
||||||
body:not(.animation-level-0) &.with-transition {
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -17,67 +20,70 @@
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.theme-light &:not(.custom-bg-image)::before {
|
:global(html.theme-light) &:not(.customBgImage)::before {
|
||||||
background-image: url('../assets/chat-bg-br.png');
|
background-image: url('../../assets/chat-bg-br.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.custom-bg-image).custom-bg-color::before {
|
&:not(.customBgImage).customBgColor::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.custom-bg-image::before {
|
&.customBgImage::before {
|
||||||
background-image: var(--custom-background) !important;
|
background-image: var(--custom-background) !important;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.animation-level-0) &.custom-bg-image.with-transition::before {
|
:global(body:not(.animation-level-0)) &.withTransition {
|
||||||
transition: background-image var(--layer-transition);
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&.customBgImage::before {
|
||||||
|
transition: background-image var(--layer-transition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.custom-bg-image.blurred::before {
|
&.customBgImage.blurred::before {
|
||||||
filter: blur(12px);
|
filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1276px) {
|
@media screen and (min-width: 1276px) {
|
||||||
body.animation-level-2 &::before {
|
:global(body.animation-level-2) &:not(.customBgImage)::before {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.theme-light body.animation-level-2 &:not(.custom-bg-image).with-right-column::before {
|
:global(html.theme-light body.animation-level-2) &:not(.customBgImage).withRightColumn::before {
|
||||||
transition: transform var(--layer-transition);
|
|
||||||
|
|
||||||
@media screen and (min-width: 1276px) {
|
@media screen and (min-width: 1276px) {
|
||||||
transform: scaleX(0.67) !important;
|
transform: scaleX(0.67) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1921px) {
|
@media screen and (min-width: 1921px) {
|
||||||
transform: scaleX(0.8) !important;
|
transform: scaleX(0.8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 2600px) {
|
@media screen and (min-width: 2600px) {
|
||||||
transform: scaleX(0.95) !important;
|
transform: scaleX(0.95) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.custom-bg-image):not(.custom-bg-color)::after {
|
:global(html.theme-light body.animation-level-2) &:not(.customBgImage).withRightColumn.withTransition::before {
|
||||||
|
transition: transform var(--layer-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.customBgImage):not(.customBgColor)::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
background-image: url('../../assets/chat-bg-pattern-light.png');
|
||||||
background-image: url('../assets/chat-bg-pattern-light.png');
|
background-position: top right;
|
||||||
background-position: top left;
|
|
||||||
background-size: 510px auto;
|
background-size: 510px auto;
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
|
|
||||||
html.theme-dark & {
|
:global(html.theme-dark) & {
|
||||||
background-image: url('../assets/chat-bg-pattern-dark.png');
|
background-image: url('../../assets/chat-bg-pattern-dark.png');
|
||||||
mix-blend-mode: unset;
|
mix-blend-mode: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,3 @@
|
|||||||
#middle-column-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#MiddleColumn {
|
#MiddleColumn {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@ -75,6 +75,7 @@ import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
|||||||
import ReactorListModal from './ReactorListModal.async';
|
import ReactorListModal from './ReactorListModal.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
|
import styles from './MiddleColumn.module.scss';
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
@ -319,11 +320,12 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const bgClassName = buildClassName(
|
const bgClassName = buildClassName(
|
||||||
'bg-layers with-transition',
|
styles.background,
|
||||||
customBackground && 'custom-bg-image',
|
styles.withTransition,
|
||||||
backgroundColor && 'custom-bg-color',
|
customBackground && styles.customBgImage,
|
||||||
customBackground && isBackgroundBlurred && 'blurred',
|
backgroundColor && styles.customBgColor,
|
||||||
isRightColumnShown && 'with-right-column',
|
customBackground && isBackgroundBlurred && styles.blurred,
|
||||||
|
isRightColumnShown && styles.withRightColumn,
|
||||||
);
|
);
|
||||||
|
|
||||||
const messagingDisabledClassName = buildClassName(
|
const messagingDisabledClassName = buildClassName(
|
||||||
@ -389,7 +391,6 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
onClick={(IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown) ? handleTabletFocus : undefined}
|
onClick={(IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown) ? handleTabletFocus : undefined}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="middle-column-bg"
|
|
||||||
className={bgClassName}
|
className={bgClassName}
|
||||||
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneTo
|
|||||||
copySelectedMessages();
|
copySelectedMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys({ 'meta+C': handleCopy });
|
useHotkeys({ 'Meta+C': handleCopy });
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCopySelectedMessages;
|
export default useCopySelectedMessages;
|
||||||
|
|||||||
@ -319,7 +319,10 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
renderCount={MAIN_SCREENS_COUNT + MANAGEMENT_SCREENS_COUNT}
|
renderCount={MAIN_SCREENS_COUNT + MANAGEMENT_SCREENS_COUNT}
|
||||||
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
||||||
shouldCleanup
|
shouldCleanup
|
||||||
cleanupExceptionKey={renderingContentKey === RightColumnContent.MessageStatistics ? RightColumnContent.Statistics : undefined}
|
cleanupExceptionKey={
|
||||||
|
renderingContentKey === RightColumnContent.MessageStatistics
|
||||||
|
? RightColumnContent.Statistics : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{renderContent}
|
{renderContent}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { FC } from '../../lib/teact/teact';
|
|||||||
import React, { useRef, useCallback, useState } from '../../lib/teact/teact';
|
import React, { useRef, useCallback, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import buildStyle from '../../util/buildStyle';
|
||||||
|
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import RippleEffect from './RippleEffect';
|
import RippleEffect from './RippleEffect';
|
||||||
@ -171,7 +172,7 @@ const Button: FC<OwnProps> = ({
|
|||||||
title={ariaLabel}
|
title={ariaLabel}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
dir={isRtl ? 'rtl' : undefined}
|
dir={isRtl ? 'rtl' : undefined}
|
||||||
style={backgroundImage ? `background-image: url(${backgroundImage})` : style}
|
style={buildStyle(backgroundImage && `background-image: url(${backgroundImage})`)}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import React, {
|
|||||||
import { debounce } from '../../util/schedulers';
|
import { debounce } from '../../util/schedulers';
|
||||||
import resetScroll from '../../util/resetScroll';
|
import resetScroll from '../../util/resetScroll';
|
||||||
import { IS_ANDROID } from '../../util/environment';
|
import { IS_ANDROID } from '../../util/environment';
|
||||||
|
import buildStyle from '../../util/buildStyle';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
@ -226,7 +227,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
{withAbsolutePositioning && items?.length ? (
|
{withAbsolutePositioning && items?.length ? (
|
||||||
<div
|
<div
|
||||||
teactFastList={!noFastList}
|
teactFastList={!noFastList}
|
||||||
style={`position: relative;${IS_ANDROID ? ` height: ${maxHeight}px;` : undefined}`}
|
style={buildStyle('position: relative', IS_ANDROID && `height: ${maxHeight}px`)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
|||||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import buildStyle from '../../util/buildStyle';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
@ -143,8 +144,10 @@ const Menu: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={bubbleClassName}
|
className={bubbleClassName}
|
||||||
style={`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY};${
|
style={buildStyle(
|
||||||
bubbleStyle || ''}`}
|
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
||||||
|
bubbleStyle,
|
||||||
|
)}
|
||||||
onClick={autoClose ? onClose : undefined}
|
onClick={autoClose ? onClose : undefined}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -69,10 +69,18 @@
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .Switcher {
|
.Switcher, .menu-item-badge {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item-badge {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
i {
|
i {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
|
|
||||||
|
&.confirm {
|
||||||
|
z-index: var(--z-lock-screen);
|
||||||
|
}
|
||||||
|
|
||||||
&.delete,
|
&.delete,
|
||||||
&.error,
|
&.error,
|
||||||
&.confirm,
|
&.confirm,
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export const DEBUG_PAYMENT_SMART_GLOCAL = false;
|
|||||||
|
|
||||||
export const SESSION_USER_KEY = 'user_auth';
|
export const SESSION_USER_KEY = 'user_auth';
|
||||||
export const LEGACY_SESSION_KEY = 'GramJs:sessionId';
|
export const LEGACY_SESSION_KEY = 'GramJs:sessionId';
|
||||||
|
export const PASSCODE_CACHE_NAME = 'tt-passcode';
|
||||||
|
|
||||||
export const GLOBAL_STATE_CACHE_DISABLED = false;
|
export const GLOBAL_STATE_CACHE_DISABLED = false;
|
||||||
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
|
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
|
||||||
@ -122,6 +123,8 @@ export const API_THROTTLE_RESET_UPDATES = new Set([
|
|||||||
'newMessage', 'newScheduledMessage', 'deleteMessages', 'deleteScheduledMessages', 'deleteHistory',
|
'newMessage', 'newScheduledMessage', 'deleteMessages', 'deleteScheduledMessages', 'deleteHistory',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const LOCK_SCREEN_ANIMATION_DURATION_MS = 200;
|
||||||
|
|
||||||
export const STICKER_SIZE_INLINE_DESKTOP_FACTOR = 13;
|
export const STICKER_SIZE_INLINE_DESKTOP_FACTOR = 13;
|
||||||
export const STICKER_SIZE_INLINE_MOBILE_FACTOR = 11;
|
export const STICKER_SIZE_INLINE_MOBILE_FACTOR = 11;
|
||||||
export const STICKER_SIZE_AUTH = 160;
|
export const STICKER_SIZE_AUTH = 160;
|
||||||
@ -132,6 +135,7 @@ export const STICKER_SIZE_PICKER_HEADER = 32;
|
|||||||
export const STICKER_SIZE_SEARCH = 64;
|
export const STICKER_SIZE_SEARCH = 64;
|
||||||
export const STICKER_SIZE_MODAL = 64;
|
export const STICKER_SIZE_MODAL = 64;
|
||||||
export const STICKER_SIZE_TWO_FA = 160;
|
export const STICKER_SIZE_TWO_FA = 160;
|
||||||
|
export const STICKER_SIZE_PASSCODE = 160;
|
||||||
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
||||||
export const STICKER_SIZE_FOLDER_SETTINGS = 100;
|
export const STICKER_SIZE_FOLDER_SETTINGS = 100;
|
||||||
export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import './ui/misc';
|
|||||||
import './ui/payments';
|
import './ui/payments';
|
||||||
import './ui/calls';
|
import './ui/calls';
|
||||||
import './ui/mediaViewer';
|
import './ui/mediaViewer';
|
||||||
|
import './ui/passcode';
|
||||||
|
|
||||||
import './api/initial';
|
import './api/initial';
|
||||||
import './api/chats';
|
import './api/chats';
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
MEDIA_CACHE_NAME_AVATARS,
|
MEDIA_CACHE_NAME_AVATARS,
|
||||||
MEDIA_PROGRESSIVE_CACHE_NAME,
|
MEDIA_PROGRESSIVE_CACHE_NAME,
|
||||||
IS_TEST,
|
IS_TEST,
|
||||||
|
LOCK_SCREEN_ANIMATION_DURATION_MS,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, PLATFORM_ENV } from '../../../util/environment';
|
import { IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, PLATFORM_ENV } from '../../../util/environment';
|
||||||
import { unsubscribe } from '../../../util/notifications';
|
import { unsubscribe } from '../../../util/notifications';
|
||||||
@ -24,6 +25,9 @@ import {
|
|||||||
clearLegacySessions,
|
clearLegacySessions,
|
||||||
} from '../../../util/sessions';
|
} from '../../../util/sessions';
|
||||||
import { forceWebsync } from '../../../util/websync';
|
import { forceWebsync } from '../../../util/websync';
|
||||||
|
import { clearGlobalForLockScreen, updatePasscodeSettings } from '../../reducers';
|
||||||
|
import { clearEncryptedSession, encryptSession, forgetPasscode } from '../../../util/passcode';
|
||||||
|
import { serializeGlobal } from '../../cache';
|
||||||
|
|
||||||
addActionHandler('initApi', async (global, actions) => {
|
addActionHandler('initApi', async (global, actions) => {
|
||||||
if (!IS_TEST) {
|
if (!IS_TEST) {
|
||||||
@ -142,6 +146,7 @@ addActionHandler('signOut', async (_global, _actions, payload) => {
|
|||||||
|
|
||||||
addActionHandler('reset', () => {
|
addActionHandler('reset', () => {
|
||||||
clearStoredSession();
|
clearStoredSession();
|
||||||
|
clearEncryptedSession();
|
||||||
|
|
||||||
void cacheApi.clear(MEDIA_CACHE_NAME);
|
void cacheApi.clear(MEDIA_CACHE_NAME);
|
||||||
void cacheApi.clear(MEDIA_CACHE_NAME_AVATARS);
|
void cacheApi.clear(MEDIA_CACHE_NAME_AVATARS);
|
||||||
@ -161,6 +166,18 @@ addActionHandler('reset', () => {
|
|||||||
getActions().init();
|
getActions().init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('softReset', () => {
|
||||||
|
clearStoredSession();
|
||||||
|
|
||||||
|
void clearLegacySessions();
|
||||||
|
|
||||||
|
updateAppBadge(0);
|
||||||
|
|
||||||
|
let global = getGlobal();
|
||||||
|
global = clearGlobalForLockScreen(global);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('disconnect', () => {
|
addActionHandler('disconnect', () => {
|
||||||
void callApi('disconnect');
|
void callApi('disconnect');
|
||||||
});
|
});
|
||||||
@ -194,3 +211,29 @@ addActionHandler('deleteDeviceToken', (global) => {
|
|||||||
push: undefined,
|
push: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('lockScreen', async (global, { softReset }) => {
|
||||||
|
const sessionJson = JSON.stringify({ ...loadStoredSession(), userId: global.currentUserId });
|
||||||
|
const globalJson = serializeGlobal();
|
||||||
|
|
||||||
|
await encryptSession(sessionJson, globalJson);
|
||||||
|
forgetPasscode();
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
setGlobal(updatePasscodeSettings(
|
||||||
|
global,
|
||||||
|
{
|
||||||
|
isScreenLocked: true,
|
||||||
|
invalidAttemptsCount: 0,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unsubscribe();
|
||||||
|
await callApi('destroy', true);
|
||||||
|
} catch (err) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => softReset(), LOCK_SCREEN_ANIMATION_DURATION_MS);
|
||||||
|
});
|
||||||
|
|||||||
69
src/global/actions/ui/passcode.ts
Normal file
69
src/global/actions/ui/passcode.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { addActionHandler, setGlobal, getGlobal } from '../../index';
|
||||||
|
|
||||||
|
import { clearPasscodeSettings, updatePasscodeSettings } from '../../reducers';
|
||||||
|
import { loadStoredSession, storeSession } from '../../../util/sessions';
|
||||||
|
import { clearEncryptedSession, encryptSession, setupPasscode } from '../../../util/passcode';
|
||||||
|
import { serializeGlobal } from '../../cache';
|
||||||
|
|
||||||
|
addActionHandler('setPasscode', async (global, actions, { passcode }) => {
|
||||||
|
setGlobal(updatePasscodeSettings(global, {
|
||||||
|
isLoading: true,
|
||||||
|
}));
|
||||||
|
await setupPasscode(passcode);
|
||||||
|
|
||||||
|
const sessionJson = JSON.stringify({ ...loadStoredSession(), userId: global.currentUserId });
|
||||||
|
const globalJson = serializeGlobal();
|
||||||
|
|
||||||
|
await encryptSession(sessionJson, globalJson);
|
||||||
|
|
||||||
|
setGlobal(updatePasscodeSettings(getGlobal(), {
|
||||||
|
hasPasscode: true,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearPasscode', (global) => {
|
||||||
|
void clearEncryptedSession();
|
||||||
|
|
||||||
|
return clearPasscodeSettings(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('unlockScreen', (global, actions, { sessionJson, globalJson }) => {
|
||||||
|
const session = JSON.parse(sessionJson);
|
||||||
|
storeSession(session, session.userId);
|
||||||
|
|
||||||
|
global = JSON.parse(globalJson);
|
||||||
|
setGlobal(updatePasscodeSettings(
|
||||||
|
global,
|
||||||
|
{
|
||||||
|
isScreenLocked: false,
|
||||||
|
error: undefined,
|
||||||
|
invalidAttemptsCount: 0,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
actions.initApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('logInvalidUnlockAttempt', (global) => {
|
||||||
|
return updatePasscodeSettings(global, {
|
||||||
|
invalidAttemptsCount: (global.passcode?.invalidAttemptsCount ?? 0) + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('resetInvalidUnlockAttempts', (global) => {
|
||||||
|
return updatePasscodeSettings(global, {
|
||||||
|
invalidAttemptsCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setPasscodeError', (global, actions, payload) => {
|
||||||
|
const { error } = payload;
|
||||||
|
|
||||||
|
return updatePasscodeSettings(global, { error });
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearPasscodeError', (global) => {
|
||||||
|
return updatePasscodeSettings(global, { error: undefined });
|
||||||
|
});
|
||||||
@ -11,3 +11,13 @@ addActionHandler('setThemeSettings', (global, actions, payload: { theme: ThemeKe
|
|||||||
|
|
||||||
return replaceThemeSettings(global, theme, settings);
|
return replaceThemeSettings(global, theme, settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('requestNextSettingsScreen', (global, actions, nextScreen) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
settings: {
|
||||||
|
...global.settings,
|
||||||
|
nextScreen,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -27,11 +27,13 @@ import {
|
|||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectVisibleUsers,
|
selectVisibleUsers,
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { hasStoredSession } from '../util/sessions';
|
import { hasStoredSession, loadStoredSession } from '../util/sessions';
|
||||||
import { INITIAL_STATE } from './initialState';
|
import { INITIAL_STATE } from './initialState';
|
||||||
import { parseLocationHash } from '../util/routing';
|
import { parseLocationHash } from '../util/routing';
|
||||||
import { isUserId } from './helpers';
|
import { isUserId } from './helpers';
|
||||||
import { getOrderedIds } from '../util/folderManager';
|
import { getOrderedIds } from '../util/folderManager';
|
||||||
|
import { clearGlobalForLockScreen } from './reducers';
|
||||||
|
import { encryptSession } from '../util/passcode';
|
||||||
|
|
||||||
const UPDATE_THROTTLE = 5000;
|
const UPDATE_THROTTLE = 5000;
|
||||||
|
|
||||||
@ -45,6 +47,16 @@ export function initCache() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetCache = () => {
|
||||||
|
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
||||||
|
|
||||||
|
if (!isCaching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCaching();
|
||||||
|
};
|
||||||
|
|
||||||
addActionHandler('saveSession', () => {
|
addActionHandler('saveSession', () => {
|
||||||
if (isCaching) {
|
if (isCaching) {
|
||||||
return;
|
return;
|
||||||
@ -53,15 +65,7 @@ export function initCache() {
|
|||||||
setupCaching();
|
setupCaching();
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('reset', () => {
|
addActionHandler('reset', resetCache);
|
||||||
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
|
||||||
|
|
||||||
if (!isCaching) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCaching();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadCache(initialState: GlobalState): GlobalState | undefined {
|
export function loadCache(initialState: GlobalState): GlobalState | undefined {
|
||||||
@ -236,6 +240,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
cached.trustedBotIds = [];
|
cached.trustedBotIds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.passcode) {
|
||||||
|
cached.passcode = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (cached.activeSessions?.byHash === undefined) {
|
if (cached.activeSessions?.byHash === undefined) {
|
||||||
cached.activeSessions = {
|
cached.activeSessions = {
|
||||||
byHash: {},
|
byHash: {},
|
||||||
@ -245,16 +253,30 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
if (!isCaching || isHeavyAnimating()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
|
if (!isCaching || global.isLoggingOut || isHeavyAnimating()) {
|
||||||
if (global.isLoggingOut) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { hasPasscode, isScreenLocked } = global.passcode;
|
||||||
|
const serializedGlobal = serializeGlobal();
|
||||||
|
|
||||||
|
if (hasPasscode) {
|
||||||
|
if (!isScreenLocked) {
|
||||||
|
const sessionJson = JSON.stringify({ ...loadStoredSession(), userId: global.currentUserId });
|
||||||
|
void encryptSession(sessionJson, serializedGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, JSON.stringify(clearGlobalForLockScreen(global)));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, serializedGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeGlobal() {
|
||||||
|
const global = getGlobal();
|
||||||
const reducedGlobal: GlobalState = {
|
const reducedGlobal: GlobalState = {
|
||||||
...INITIAL_STATE,
|
...INITIAL_STATE,
|
||||||
...pick(global, [
|
...pick(global, [
|
||||||
@ -295,10 +317,14 @@ function updateCache() {
|
|||||||
availableReactions: reduceAvailableReactions(global),
|
availableReactions: reduceAvailableReactions(global),
|
||||||
isCallPanelVisible: undefined,
|
isCallPanelVisible: undefined,
|
||||||
trustedBotIds: global.trustedBotIds,
|
trustedBotIds: global.trustedBotIds,
|
||||||
|
passcode: pick(global.passcode, [
|
||||||
|
'isScreenLocked',
|
||||||
|
'hasPasscode',
|
||||||
|
'invalidAttemptsCount',
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = JSON.stringify(reducedGlobal);
|
return JSON.stringify(reducedGlobal);
|
||||||
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduceShowChatInfo(global: GlobalState): boolean {
|
function reduceShowChatInfo(global: GlobalState): boolean {
|
||||||
@ -352,7 +378,7 @@ function reduceChats(global: GlobalState): GlobalState['chats'] {
|
|||||||
function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
const byChatId: GlobalState['messages']['byChatId'] = {};
|
const byChatId: GlobalState['messages']['byChatId'] = {};
|
||||||
const { chatId: currentChatId } = selectCurrentMessageList(global) || {};
|
const { chatId: currentChatId, threadId, type } = selectCurrentMessageList(global) || {};
|
||||||
const chatIdsToSave = [
|
const chatIdsToSave = [
|
||||||
...currentChatId ? [currentChatId] : [],
|
...currentChatId ? [currentChatId] : [],
|
||||||
...currentUserId ? [currentUserId] : [],
|
...currentUserId ? [currentUserId] : [],
|
||||||
@ -380,7 +406,7 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
byChatId,
|
byChatId,
|
||||||
messageLists: [],
|
messageLists: currentChatId && threadId && type ? [{ chatId: currentChatId, threadId, type }] : [],
|
||||||
sponsoredByChatId: {},
|
sponsoredByChatId: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
import { addActionHandler } from './index';
|
import { addActionHandler } from './index';
|
||||||
|
|
||||||
import { INITIAL_STATE } from './initialState';
|
import { INITIAL_STATE } from './initialState';
|
||||||
|
import { IS_MOCKED_CLIENT } from '../config';
|
||||||
import { initCache, loadCache } from './cache';
|
import { initCache, loadCache } from './cache';
|
||||||
import { cloneDeep } from '../util/iteratees';
|
import { cloneDeep } from '../util/iteratees';
|
||||||
import { IS_MOCKED_CLIENT } from '../config';
|
import { updatePasscodeSettings } from './reducers';
|
||||||
|
|
||||||
initCache();
|
initCache();
|
||||||
|
|
||||||
addActionHandler('init', () => {
|
addActionHandler('init', () => {
|
||||||
const initial = cloneDeep(INITIAL_STATE);
|
const initial = cloneDeep(INITIAL_STATE);
|
||||||
const state = loadCache(initial) || initial;
|
let global = loadCache(initial) || initial;
|
||||||
if (IS_MOCKED_CLIENT) state.authState = 'authorizationStateReady';
|
if (IS_MOCKED_CLIENT) global.authState = 'authorizationStateReady';
|
||||||
return state;
|
|
||||||
|
const { hasPasscode, isScreenLocked } = global.passcode;
|
||||||
|
if (hasPasscode && !isScreenLocked) {
|
||||||
|
global = updatePasscodeSettings(global, {
|
||||||
|
isScreenLocked: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return global;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -192,6 +192,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
twoFaSettings: {},
|
twoFaSettings: {},
|
||||||
|
passcode: {},
|
||||||
activeReactions: {},
|
activeReactions: {},
|
||||||
|
|
||||||
shouldShowContextMenuHint: true,
|
shouldShowContextMenuHint: true,
|
||||||
|
|||||||
@ -7,5 +7,6 @@ export * from './localSearch';
|
|||||||
export * from './management';
|
export * from './management';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './twoFaSettings';
|
export * from './twoFaSettings';
|
||||||
|
export * from './passcode';
|
||||||
export * from './payments';
|
export * from './payments';
|
||||||
export * from './statistics';
|
export * from './statistics';
|
||||||
|
|||||||
46
src/global/reducers/passcode.ts
Normal file
46
src/global/reducers/passcode.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { GlobalState } from '../types';
|
||||||
|
import { INITIAL_STATE } from '../initialState';
|
||||||
|
|
||||||
|
export function updatePasscodeSettings(
|
||||||
|
global: GlobalState,
|
||||||
|
update: GlobalState['passcode'],
|
||||||
|
): GlobalState {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
passcode: {
|
||||||
|
...global.passcode,
|
||||||
|
...update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPasscodeSettings(global: GlobalState): GlobalState {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
passcode: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearGlobalForLockScreen(global: GlobalState): GlobalState {
|
||||||
|
const {
|
||||||
|
theme,
|
||||||
|
shouldUseSystemTheme,
|
||||||
|
animationLevel,
|
||||||
|
language,
|
||||||
|
} = global.settings.byKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...INITIAL_STATE,
|
||||||
|
passcode: global.passcode,
|
||||||
|
settings: {
|
||||||
|
...INITIAL_STATE.settings,
|
||||||
|
byKey: {
|
||||||
|
...INITIAL_STATE.settings.byKey,
|
||||||
|
theme,
|
||||||
|
shouldUseSystemTheme,
|
||||||
|
animationLevel,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -62,6 +62,7 @@ import type {
|
|||||||
NewChatMembersProgress,
|
NewChatMembersProgress,
|
||||||
AudioOrigin,
|
AudioOrigin,
|
||||||
ManagementState,
|
ManagementState,
|
||||||
|
SettingsScreens,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { typify } from '../lib/teact/teactn';
|
import { typify } from '../lib/teact/teactn';
|
||||||
import type { P2pMessage } from '../lib/secret-sauce';
|
import type { P2pMessage } from '../lib/secret-sauce';
|
||||||
@ -494,6 +495,7 @@ export type GlobalState = {
|
|||||||
themes: Partial<Record<ThemeKey, IThemeSettings>>;
|
themes: Partial<Record<ThemeKey, IThemeSettings>>;
|
||||||
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
|
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
|
||||||
notifyExceptions?: Record<number, NotifyException>;
|
notifyExceptions?: Record<number, NotifyException>;
|
||||||
|
nextScreen?: SettingsScreens;
|
||||||
};
|
};
|
||||||
|
|
||||||
twoFaSettings: {
|
twoFaSettings: {
|
||||||
@ -503,6 +505,14 @@ export type GlobalState = {
|
|||||||
waitingEmailCodeLength?: number;
|
waitingEmailCodeLength?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
passcode: {
|
||||||
|
isScreenLocked?: boolean;
|
||||||
|
hasPasscode?: boolean;
|
||||||
|
error?: string;
|
||||||
|
invalidAttemptsCount?: number;
|
||||||
|
isLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
push?: {
|
push?: {
|
||||||
deviceToken: string;
|
deviceToken: string;
|
||||||
subscribedAt: number;
|
subscribedAt: number;
|
||||||
@ -861,6 +871,21 @@ export interface ActionPayloads {
|
|||||||
sound: CallSound;
|
sound: CallSound;
|
||||||
};
|
};
|
||||||
connectToActivePhoneCall: {};
|
connectToActivePhoneCall: {};
|
||||||
|
|
||||||
|
// Passcode
|
||||||
|
setPasscode: { passcode: string };
|
||||||
|
clearPasscode: never;
|
||||||
|
lockScreen: never;
|
||||||
|
unlockScreen: { sessionJson: string; globalJson: string };
|
||||||
|
softSignIn: never;
|
||||||
|
softReset: never;
|
||||||
|
logInvalidUnlockAttempt: never;
|
||||||
|
resetInvalidUnlockAttempts: never;
|
||||||
|
setPasscodeError: { error: string };
|
||||||
|
clearPasscodeError: never;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
requestNextSettingsScreen: SettingsScreens;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NonTypedActionNames = (
|
export type NonTypedActionNames = (
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { mes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys({ 'meta+C': handleCopy });
|
useHotkeys({ 'Meta+C': handleCopy });
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useNativeCopySelectedMessages;
|
export default useNativeCopySelectedMessages;
|
||||||
|
|||||||
@ -38,7 +38,9 @@ if (DEBUG) {
|
|||||||
console.log('>>> FINISH INITIAL RENDER');
|
console.log('>>> FINISH INITIAL RENDER');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('dblclick', () => {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
document.addEventListener('dblclick', () => {
|
||||||
console.warn('GLOBAL STATE', getGlobal());
|
// eslint-disable-next-line no-console
|
||||||
});
|
console.warn('GLOBAL STATE', getGlobal());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -148,6 +148,26 @@
|
|||||||
box-shadow: inset 0 0 0 1px var(--color-text-green);
|
box-shadow: inset 0 0 0 1px var(--color-text-green);
|
||||||
caret-color: var(--color-text-green);
|
caret-color: var(--color-text-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable yellow highlight on autofill
|
||||||
|
&:autofill,
|
||||||
|
&:-webkit-autofill-strong-password,
|
||||||
|
&:-webkit-autofill-strong-password-viewable,
|
||||||
|
&:-webkit-autofill-and-obscured {
|
||||||
|
box-shadow: inset 0 0 0 10rem var(--color-background);
|
||||||
|
-webkit-text-fill-color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide hint for Safari password strength meter
|
||||||
|
&::-webkit-strong-password-auto-fill-button {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
max-width: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select.form-control {
|
select.form-control {
|
||||||
|
|||||||
@ -199,6 +199,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--symbol-menu-height: 14.6875rem;
|
--symbol-menu-height: 14.6875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--z-lock-screen: 3000;
|
||||||
--z-ui-loader-mask: 2000;
|
--z-ui-loader-mask: 2000;
|
||||||
--z-notification: 1520;
|
--z-notification: 1520;
|
||||||
--z-right-column: 900;
|
--z-right-column: 900;
|
||||||
|
|||||||
@ -51,6 +51,9 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
|
.icon-key:before {
|
||||||
|
content: "\e99a";
|
||||||
|
}
|
||||||
.icon-heart-outline:before {
|
.icon-heart-outline:before {
|
||||||
content: "\e99e";
|
content: "\e99e";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
@import "forms";
|
@import "forms";
|
||||||
@import "icons";
|
@import "icons";
|
||||||
@import "common";
|
@import "common";
|
||||||
@import "bg";
|
|
||||||
@import "../assets/fonts/roboto.css";
|
@import "../assets/fonts/roboto.css";
|
||||||
@import "./print";
|
@import "./print";
|
||||||
|
|
||||||
@ -66,12 +65,17 @@ body.cursor-ew-resize {
|
|||||||
cursor: ew-resize !important;
|
cursor: ew-resize !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root,
|
||||||
|
.full-height {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-auth {
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#middle-column-portals,
|
#middle-column-portals,
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
.Modal,
|
.Modal,
|
||||||
.ActiveCallHeader,
|
.ActiveCallHeader,
|
||||||
.unread-count,
|
.unread-count,
|
||||||
#middle-column-bg,
|
|
||||||
#middle-column-portals,
|
#middle-column-portals,
|
||||||
.header-tools,
|
.header-tools,
|
||||||
.ScrollDownButton,
|
.ScrollDownButton,
|
||||||
|
|||||||
@ -214,6 +214,15 @@ export enum SettingsScreens {
|
|||||||
TwoFaRecoveryEmailCode,
|
TwoFaRecoveryEmailCode,
|
||||||
TwoFaCongratulations,
|
TwoFaCongratulations,
|
||||||
QuickReaction,
|
QuickReaction,
|
||||||
|
PasscodeDisabled,
|
||||||
|
PasscodeNewPasscode,
|
||||||
|
PasscodeNewPasscodeConfirm,
|
||||||
|
PasscodeEnabled,
|
||||||
|
PasscodeChangePasscodeCurrent,
|
||||||
|
PasscodeChangePasscodeNew,
|
||||||
|
PasscodeChangePasscodeConfirm,
|
||||||
|
PasscodeTurnOff,
|
||||||
|
PasscodeCongratulations,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum Type {
|
|||||||
Text,
|
Text,
|
||||||
Blob,
|
Blob,
|
||||||
Json,
|
Json,
|
||||||
|
ArrayBuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(
|
export async function fetch(
|
||||||
@ -52,6 +53,8 @@ export async function fetch(
|
|||||||
}
|
}
|
||||||
case Type.Json:
|
case Type.Json:
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
case Type.ArrayBuffer:
|
||||||
|
return await response.arrayBuffer();
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -62,13 +65,15 @@ export async function fetch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(cacheName: string, key: string, data: AnyLiteral | Blob | string) {
|
export async function save(cacheName: string, key: string, data: AnyLiteral | Blob | ArrayBuffer | string) {
|
||||||
if (!cacheApi) {
|
if (!cacheApi) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheData = typeof data === 'string' || data instanceof Blob ? data : JSON.stringify(data);
|
const cacheData = typeof data === 'string' || data instanceof Blob || data instanceof ArrayBuffer
|
||||||
|
? data
|
||||||
|
: JSON.stringify(data);
|
||||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
const request = new Request(key.replace(/:/g, '_'));
|
const request = new Request(key.replace(/:/g, '_'));
|
||||||
const response = new Response(cacheData);
|
const response = new Response(cacheData);
|
||||||
@ -81,6 +86,21 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remove(cacheName: string, key: string) {
|
||||||
|
try {
|
||||||
|
if (!cacheApi) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = await cacheApi.open(cacheName);
|
||||||
|
return await cache.delete(key);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function clear(cacheName: string) {
|
export async function clear(cacheName: string) {
|
||||||
try {
|
try {
|
||||||
if (!cacheApi) {
|
if (!cacheApi) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type {
|
|||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import { ApiMediaFormat } from '../api/types';
|
import { ApiMediaFormat } from '../api/types';
|
||||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||||
import { DEBUG, IS_TEST } from '../config';
|
import { APP_NAME, DEBUG, IS_TEST } from '../config';
|
||||||
import { getActions, getGlobal, setGlobal } from '../global';
|
import { getActions, getGlobal, setGlobal } from '../global';
|
||||||
import {
|
import {
|
||||||
getChatAvatarHash,
|
getChatAvatarHash,
|
||||||
@ -275,6 +275,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
|||||||
} = message;
|
} = message;
|
||||||
if (reaction) senderId = reaction.userId;
|
if (reaction) senderId = reaction.userId;
|
||||||
|
|
||||||
|
const { isScreenLocked } = global.passcode;
|
||||||
const messageSender = senderId ? selectUser(global, senderId) : undefined;
|
const messageSender = senderId ? selectUser(global, senderId) : undefined;
|
||||||
const messageAction = getMessageAction(message as ApiMessage);
|
const messageAction = getMessageAction(message as ApiMessage);
|
||||||
const actionTargetMessage = messageAction && replyToMessageId
|
const actionTargetMessage = messageAction && replyToMessageId
|
||||||
@ -293,7 +294,10 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
|||||||
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||||
|
|
||||||
let body: string;
|
let body: string;
|
||||||
if (selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))) {
|
if (
|
||||||
|
!isScreenLocked
|
||||||
|
&& selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))
|
||||||
|
) {
|
||||||
if (isActionMessage(message)) {
|
if (isActionMessage(message)) {
|
||||||
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
|
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
|
||||||
|
|
||||||
@ -318,7 +322,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: getChatTitle(getTranslation, chat, privateChatUser),
|
title: isScreenLocked ? APP_NAME : getChatTitle(getTranslation, chat, privateChatUser),
|
||||||
body,
|
body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/util/passcode.ts
Normal file
94
src/util/passcode.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import * as cacheApi from './cacheApi';
|
||||||
|
import { PASSCODE_CACHE_NAME } from '../config';
|
||||||
|
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
const SALT = 'harder better faster stronger';
|
||||||
|
|
||||||
|
let currentPasscodeHash: ArrayBuffer | undefined;
|
||||||
|
|
||||||
|
export async function setupPasscode(passcode: string) {
|
||||||
|
currentPasscodeHash = await sha256(passcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encryptSession(sessionJson: string, globalJson: string) {
|
||||||
|
if (!currentPasscodeHash) {
|
||||||
|
throw new Error('[api/passcode] Missing current passcode');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sessionEncrypted, globalEncrypted] = await Promise.all([
|
||||||
|
aesEncrypt(sessionJson, currentPasscodeHash),
|
||||||
|
aesEncrypt(globalJson, currentPasscodeHash),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
store('sessionEncrypted', sessionEncrypted),
|
||||||
|
store('globalEncrypted', globalEncrypted),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptSession(passcode: string) {
|
||||||
|
const passcodeHash = await sha256(passcode);
|
||||||
|
|
||||||
|
const [sessionEncrypted, globalEncrypted] = await Promise.all([
|
||||||
|
load('sessionEncrypted'),
|
||||||
|
load('globalEncrypted'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!sessionEncrypted || !globalEncrypted) {
|
||||||
|
throw new Error('[api/passcode] Missing required stored fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sessionJson, globalJson] = await Promise.all([
|
||||||
|
aesDecrypt(sessionEncrypted, passcodeHash),
|
||||||
|
aesDecrypt(globalEncrypted, passcodeHash),
|
||||||
|
]);
|
||||||
|
|
||||||
|
currentPasscodeHash = passcodeHash;
|
||||||
|
|
||||||
|
return { sessionJson, globalJson };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forgetPasscode() {
|
||||||
|
currentPasscodeHash = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearEncryptedSession() {
|
||||||
|
forgetPasscode();
|
||||||
|
return cacheApi.clear(PASSCODE_CACHE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(plaintext: string) {
|
||||||
|
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${plaintext}${SALT}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function store(key: string, value: ArrayBuffer) {
|
||||||
|
await cacheApi.save(PASSCODE_CACHE_NAME, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(key: string) {
|
||||||
|
return cacheApi.fetch(PASSCODE_CACHE_NAME, key, cacheApi.Type.ArrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aesEncrypt(plaintext: string, pwHash: ArrayBuffer) {
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||||
|
const alg = { name: 'AES-GCM', iv };
|
||||||
|
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']);
|
||||||
|
const ptUint8 = new TextEncoder().encode(plaintext);
|
||||||
|
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8);
|
||||||
|
const ct = new Uint8Array(ctBuffer);
|
||||||
|
const result = new Uint8Array(IV_LENGTH + ct.length);
|
||||||
|
result.set(iv, 0);
|
||||||
|
result.set(ct, IV_LENGTH);
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aesDecrypt(data: ArrayBuffer, pwHash: ArrayBuffer) {
|
||||||
|
const dataArray = new Uint8Array(data);
|
||||||
|
const iv = dataArray.slice(0, IV_LENGTH);
|
||||||
|
const alg = { name: 'AES-GCM', iv };
|
||||||
|
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
|
||||||
|
const ct = dataArray.slice(IV_LENGTH);
|
||||||
|
const plainBuffer = await crypto.subtle.decrypt(alg, key, ct);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(plainBuffer);
|
||||||
|
}
|
||||||
@ -2,7 +2,9 @@ import * as idb from 'idb-keyval';
|
|||||||
|
|
||||||
import type { ApiSessionData } from '../api/types';
|
import type { ApiSessionData } from '../api/types';
|
||||||
|
|
||||||
import { DEBUG, LEGACY_SESSION_KEY, SESSION_USER_KEY } from '../config';
|
import {
|
||||||
|
DEBUG, GLOBAL_STATE_CACHE_KEY, LEGACY_SESSION_KEY, SESSION_USER_KEY,
|
||||||
|
} from '../config';
|
||||||
import * as cacheApi from './cacheApi';
|
import * as cacheApi from './cacheApi';
|
||||||
|
|
||||||
const DC_IDS = [1, 2, 3, 4, 5];
|
const DC_IDS = [1, 2, 3, 4, 5];
|
||||||
@ -12,8 +14,14 @@ export function hasStoredSession(withLegacy = false) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkSessionLocked()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const userAuthJson = localStorage.getItem(SESSION_USER_KEY);
|
const userAuthJson = localStorage.getItem(SESSION_USER_KEY);
|
||||||
if (!userAuthJson) return false;
|
if (!userAuthJson) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userAuth = JSON.parse(userAuthJson);
|
const userAuth = JSON.parse(userAuthJson);
|
||||||
@ -57,6 +65,9 @@ export function loadStoredSession(): ApiSessionData | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userAuth = JSON.parse(localStorage.getItem(SESSION_USER_KEY)!);
|
const userAuth = JSON.parse(localStorage.getItem(SESSION_USER_KEY)!);
|
||||||
|
if (!userAuth) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const mainDcId = Number(userAuth.dcID);
|
const mainDcId = Number(userAuth.dcID);
|
||||||
const keys: Record<number, string> = {};
|
const keys: Record<number, string> = {};
|
||||||
const hashes: Record<number, string> = {};
|
const hashes: Record<number, string> = {};
|
||||||
@ -134,3 +145,9 @@ export function importTestSession() {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkSessionLocked() {
|
||||||
|
const stateFromCache = JSON.parse(localStorage.getItem(GLOBAL_STATE_CACHE_KEY) || '{}');
|
||||||
|
|
||||||
|
return Boolean(stateFromCache?.passcode?.isScreenLocked);
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,10 @@ const colors = (Object.keys(themeColors) as Array<keyof typeof themeColors>).map
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const switchTheme = (theme: ISettings['theme'], withAnimation: boolean) => {
|
const switchTheme = (theme: ISettings['theme'], withAnimation: boolean) => {
|
||||||
|
const themeClassName = `theme-${theme}`;
|
||||||
|
if (document.documentElement.classList.contains(themeClassName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const isDarkTheme = theme === 'dark';
|
const isDarkTheme = theme === 'dark';
|
||||||
const shouldAnimate = isInitialized && withAnimation;
|
const shouldAnimate = isInitialized && withAnimation;
|
||||||
const startIndex = isDarkTheme ? 0 : 1;
|
const startIndex = isDarkTheme ? 0 : 1;
|
||||||
@ -43,7 +47,7 @@ const switchTheme = (theme: ISettings['theme'], withAnimation: boolean) => {
|
|||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
document.documentElement.classList.add('no-animations');
|
document.documentElement.classList.add('no-animations');
|
||||||
}
|
}
|
||||||
document.documentElement.classList.add(`theme-${theme}`);
|
document.documentElement.classList.add(themeClassName);
|
||||||
if (themeColorTag) {
|
if (themeColorTag) {
|
||||||
themeColorTag.setAttribute('content', isDarkTheme ? '#212121' : '#fff');
|
themeColorTag.setAttribute('content', isDarkTheme ? '#212121' : '#fff');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ module.exports = (env = {}, argv = {}) => {
|
|||||||
stats: 'minimal',
|
stats: 'minimal',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].[contenthash].js',
|
filename: '[name].[contenthash].js',
|
||||||
chunkFilename: '[id].[chunkhash].js',
|
chunkFilename: '[id].[chunkhash].js',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user