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-config-recommended-scss": "^6.0.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",
|
||||
"telegraph-node": "^1.0.4",
|
||||
"typescript": "^4.6.3",
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.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",
|
||||
"telegraph-node": "^1.0.4",
|
||||
"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 type { GlobalState } from './global/types';
|
||||
import type { UiLoaderPage } from './components/common/UiLoader';
|
||||
|
||||
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
|
||||
import { pick } from './util/iteratees';
|
||||
import { PLATFORM_ENV } from './util/environment';
|
||||
import { updateSizes } from './util/windowSize';
|
||||
import { addActiveTabChangeListener } from './util/activeTabMonitor';
|
||||
import { hasStoredSession } from './util/sessions';
|
||||
import buildClassName from './util/buildClassName';
|
||||
import useFlag from './hooks/useFlag';
|
||||
import usePrevious from './hooks/usePrevious';
|
||||
|
||||
import Auth from './components/auth/Auth';
|
||||
import UiLoader from './components/common/UiLoader';
|
||||
import Main from './components/main/Main.async';
|
||||
import LockScreen from './components/main/LockScreen.async';
|
||||
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';
|
||||
|
||||
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 [isInactive, markInactive] = useFlag(false);
|
||||
const isMobile = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||
|
||||
useEffect(() => {
|
||||
updateSizes();
|
||||
@ -36,37 +55,89 @@ const App: FC<StateProps> = ({ authState }) => {
|
||||
|
||||
// return <Test />;
|
||||
|
||||
if (isInactive) {
|
||||
return <AppInactive />;
|
||||
}
|
||||
let activeKey: number;
|
||||
let page: UiLoaderPage | undefined;
|
||||
|
||||
if (authState) {
|
||||
if (isInactive) {
|
||||
activeKey = AppScreens.inactive;
|
||||
} else if (isScreenLocked) {
|
||||
page = 'lock';
|
||||
activeKey = AppScreens.lock;
|
||||
} else if (authState) {
|
||||
switch (authState) {
|
||||
case 'authorizationStateWaitPhoneNumber':
|
||||
page = 'authPhoneNumber';
|
||||
activeKey = AppScreens.auth;
|
||||
break;
|
||||
case 'authorizationStateWaitCode':
|
||||
page = 'authCode';
|
||||
activeKey = AppScreens.auth;
|
||||
break;
|
||||
case 'authorizationStateWaitPassword':
|
||||
page = 'authPassword';
|
||||
activeKey = AppScreens.auth;
|
||||
break;
|
||||
case 'authorizationStateWaitRegistration':
|
||||
activeKey = AppScreens.auth;
|
||||
break;
|
||||
case 'authorizationStateWaitQrCode':
|
||||
return <Auth />;
|
||||
page = 'authQrCode';
|
||||
activeKey = AppScreens.auth;
|
||||
break;
|
||||
case 'authorizationStateClosed':
|
||||
case 'authorizationStateClosing':
|
||||
case 'authorizationStateLoggingOut':
|
||||
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 (
|
||||
<UiLoader page="main" key="main">
|
||||
<Main />
|
||||
<UiLoader key="Loader" page={page}>
|
||||
<Transition
|
||||
name="fade"
|
||||
activeKey={activeKey}
|
||||
shouldCleanup
|
||||
className={buildClassName(
|
||||
'full-height',
|
||||
(activeKey === AppScreens.auth || prevActiveKey === AppScreens.auth) && 'is-auth',
|
||||
)}
|
||||
>
|
||||
{renderContent}
|
||||
</Transition>
|
||||
</UiLoader>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withGlobal(
|
||||
(global): StateProps => pick(global, ['authState']),
|
||||
(global): StateProps => {
|
||||
return {
|
||||
authState: global.authState,
|
||||
isScreenLocked: global.passcode?.isScreenLocked,
|
||||
};
|
||||
},
|
||||
)(App);
|
||||
|
||||
@ -13,7 +13,7 @@ interface LocalDb {
|
||||
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||
}
|
||||
|
||||
export default {
|
||||
const LOCAL_DB_INITIAL = {
|
||||
localMessages: {},
|
||||
chats: {},
|
||||
users: {},
|
||||
@ -22,4 +22,12 @@ export default {
|
||||
stickerSets: {},
|
||||
photos: {},
|
||||
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 downloadMediaWithClient, { parseMediaUrl } from './media';
|
||||
import { buildApiUserFromFull } from '../apiBuilders/users';
|
||||
import localDb from '../localDb';
|
||||
import localDb, { clearLocalDb } from '../localDb';
|
||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||
import { addMessageToLocalDb } from '../helpers';
|
||||
|
||||
@ -102,7 +102,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
if (err.message !== 'Disconnect') {
|
||||
if (err.message !== 'Disconnect' && err.message !== 'Cannot send requests while disconnected') {
|
||||
onUpdate({
|
||||
'@type': 'updateConnectionState',
|
||||
connectionState: 'connectionStateBroken',
|
||||
@ -134,8 +134,13 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
||||
}
|
||||
}
|
||||
|
||||
export async function destroy() {
|
||||
await invokeRequest(new GramJs.auth.LogOut());
|
||||
export async function destroy(noLogOut = false) {
|
||||
if (!noLogOut) {
|
||||
await invokeRequest(new GramJs.auth.LogOut());
|
||||
}
|
||||
|
||||
clearLocalDb();
|
||||
|
||||
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="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>
|
||||
<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>
|
||||
|
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" 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>
|
||||
<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>
|
||||
|
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
|
||||
export { default as Main } from '../components/main/Main';
|
||||
export { default as LockScreen } from '../components/main/LockScreen';
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('>>> FINISH LOAD MAIN BUNDLE');
|
||||
}
|
||||
|
||||
if (!getGlobal().connectionState) {
|
||||
const { connectionState, passcode: { isScreenLocked } } = getGlobal();
|
||||
if (!connectionState && !isScreenLocked) {
|
||||
getActions().initApi();
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ import { pick } from '../../util/iteratees';
|
||||
import { PLATFORM_ENV } from '../../util/environment';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
|
||||
import UiLoader from '../common/UiLoader';
|
||||
import AuthPhoneNumber from './AuthPhoneNumber';
|
||||
import AuthCode from './AuthCode.async';
|
||||
import AuthPassword from './AuthPassword.async';
|
||||
@ -19,19 +19,25 @@ import AuthQrCode from './AuthQrCode';
|
||||
|
||||
import './Auth.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = Pick<GlobalState, 'authState'>;
|
||||
|
||||
const Auth: FC<StateProps> = ({
|
||||
authState,
|
||||
const Auth: FC<OwnProps & StateProps> = ({
|
||||
isActive, authState,
|
||||
}) => {
|
||||
const {
|
||||
reset, initApi, returnToAuthPhoneNumber, goToAuthQrCode,
|
||||
} = getActions();
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
initApi();
|
||||
}, [reset, initApi]);
|
||||
if (isActive) {
|
||||
reset();
|
||||
initApi();
|
||||
}
|
||||
}, [isActive, reset, initApi]);
|
||||
|
||||
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':
|
||||
return <UiLoader page="authCode" key="authCode"><AuthCode /></UiLoader>;
|
||||
return <AuthCode />;
|
||||
case 'authorizationStateWaitPassword':
|
||||
return <UiLoader page="authPassword" key="authPassword"><AuthPassword /></UiLoader>;
|
||||
return <AuthPassword />;
|
||||
case 'authorizationStateWaitRegistration':
|
||||
return <AuthRegister />;
|
||||
case 'authorizationStateWaitPhoneNumber':
|
||||
return <UiLoader page="authPhoneNumber" key="authPhoneNumber"><AuthPhoneNumber /></UiLoader>;
|
||||
return <AuthPhoneNumber />;
|
||||
case 'authorizationStateWaitQrCode':
|
||||
return <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
|
||||
return <AuthQrCode />;
|
||||
default:
|
||||
return isMobile
|
||||
? <UiLoader page="authPhoneNumber" key="authPhoneNumber"><AuthPhoneNumber /></UiLoader>
|
||||
: <UiLoader page="authQrCode" key="authQrCode"><AuthQrCode /></UiLoader>;
|
||||
return isMobile ? <AuthPhoneNumber /> : <AuthQrCode />;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => pick(global, ['authState']),
|
||||
)(Auth));
|
||||
|
||||
@ -8,6 +8,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import useRunThrottled from '../../../hooks/useRunThrottled';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -147,7 +148,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||
isOpen={isDropdownOpen}
|
||||
positionX="right"
|
||||
autoClose
|
||||
style={anchor ? `right: 1rem; top: ${anchor.y}px;` : undefined}
|
||||
style={buildStyle(anchor && `right: 1rem; top: ${anchor.y}px`)}
|
||||
onClose={closeDropdown}
|
||||
className="participant-menu"
|
||||
>
|
||||
|
||||
@ -18,7 +18,7 @@ import AnimatedSticker from './AnimatedSticker';
|
||||
import './AnimatedEmoji.scss';
|
||||
|
||||
type OwnProps = {
|
||||
sticker: ApiSticker;
|
||||
sticker?: ApiSticker;
|
||||
effect?: ApiSticker;
|
||||
isOwn?: boolean;
|
||||
soundId?: string;
|
||||
@ -56,13 +56,13 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
||||
playKey,
|
||||
} = 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 thumbDataUri = sticker.thumbnail?.dataUri;
|
||||
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
||||
const previewBlobUrl = useMedia(
|
||||
`${localMediaHash}?size=m`,
|
||||
sticker ? `${localMediaHash}?size=m` : undefined,
|
||||
!isIntersecting && !forceLoadPreview,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
lastSyncTime,
|
||||
@ -75,7 +75,7 @@ const AnimatedEmoji: FC<OwnProps> = ({
|
||||
return (
|
||||
<div
|
||||
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}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
@ -18,8 +18,12 @@ type OwnProps = {
|
||||
hint?: string;
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
shouldDisablePasswordManager?: boolean;
|
||||
shouldShowSubmit?: boolean;
|
||||
shouldResetValue?: boolean;
|
||||
isPasswordVisible?: boolean;
|
||||
clearError: NoneToVoidFunction;
|
||||
noRipple?: boolean;
|
||||
onChangePasswordVisibility: (state: boolean) => void;
|
||||
onInputChange?: (password: string) => void;
|
||||
onSubmit: (password: string) => void;
|
||||
@ -34,6 +38,10 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
hint,
|
||||
placeholder = 'Password',
|
||||
submitLabel = 'Next',
|
||||
shouldShowSubmit,
|
||||
shouldResetValue,
|
||||
shouldDisablePasswordManager = false,
|
||||
noRipple = false,
|
||||
clearError,
|
||||
onChangePasswordVisibility,
|
||||
onInputChange,
|
||||
@ -46,6 +54,12 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
const [password, setPassword] = useState('');
|
||||
const [canSubmit, setCanSubmit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldResetValue) {
|
||||
setPassword('');
|
||||
}
|
||||
}, [shouldResetValue]);
|
||||
|
||||
useTimeout(() => {
|
||||
if (!IS_TOUCH_ENV) {
|
||||
inputRef.current!.focus();
|
||||
@ -102,8 +116,9 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
type={isPasswordVisible ? 'text' : 'password'}
|
||||
id="sign-in-password"
|
||||
value={password || ''}
|
||||
autoComplete="current-password"
|
||||
autoComplete={shouldDisablePasswordManager ? 'one-time-code' : 'current-password'}
|
||||
onChange={onPasswordChange}
|
||||
maxLength={256}
|
||||
dir="auto"
|
||||
/>
|
||||
<label>{error || hint || placeholder}</label>
|
||||
@ -117,8 +132,8 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
<i className={isPasswordVisible ? 'icon-eye' : 'icon-eye-closed'} />
|
||||
</div>
|
||||
</div>
|
||||
{canSubmit && (
|
||||
<Button type="submit" ripple isLoading={isLoading}>
|
||||
{(canSubmit || shouldShowSubmit) && (
|
||||
<Button type="submit" ripple={!noRipple} isLoading={isLoading} disabled={!canSubmit}>
|
||||
{submitLabel}
|
||||
</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 { 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 { selectIsRightColumnShown, selectTheme } from '../../global/selectors';
|
||||
import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import { pause } from '../../util/schedulers';
|
||||
@ -17,30 +17,32 @@ import preloadFonts from '../../util/fonts';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
import { Bundles, loadModule } from '../../util/moduleLoader';
|
||||
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 reactionThumbsPath from '../../assets/reaction-thumbs.png';
|
||||
import lockPreviewPath from '../../assets/lock.png';
|
||||
import monkeyPath from '../../assets/monkey.svg';
|
||||
|
||||
export type UiLoaderPage =
|
||||
'main'
|
||||
| 'lock'
|
||||
| 'authCode'
|
||||
| 'authPassword'
|
||||
| 'authPhoneNumber'
|
||||
| 'authQrCode';
|
||||
|
||||
type OwnProps = {
|
||||
page: 'main' | 'authCode' | 'authPassword' | 'authPhoneNumber' | 'authQrCode';
|
||||
page?: UiLoaderPage;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type StateProps =
|
||||
Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'>
|
||||
& {
|
||||
isRightColumnShown?: boolean;
|
||||
leftColumnWidth?: number;
|
||||
isBackgroundBlurred?: boolean;
|
||||
theme: ThemeKey;
|
||||
customBackground?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
type StateProps = Pick<GlobalState, 'uiReadyState' | 'shouldSkipHistoryAnimations'> & {
|
||||
isRightColumnShown?: boolean;
|
||||
leftColumnWidth?: number;
|
||||
theme: ThemeKey;
|
||||
};
|
||||
|
||||
const MAX_PRELOAD_DELAY = 700;
|
||||
const SECOND_STATE_DELAY = 1000;
|
||||
@ -81,6 +83,10 @@ const preloadTasks = {
|
||||
authCode: () => preloadImage(monkeyPath),
|
||||
authPassword: () => preloadImage(monkeyPath),
|
||||
authQrCode: preloadFonts,
|
||||
lock: () => Promise.all([
|
||||
preloadFonts(),
|
||||
preloadImage(lockPreviewPath),
|
||||
]),
|
||||
};
|
||||
|
||||
const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
@ -90,9 +96,6 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
shouldSkipHistoryAnimations,
|
||||
leftColumnWidth,
|
||||
theme,
|
||||
backgroundColor,
|
||||
customBackground,
|
||||
isBackgroundBlurred,
|
||||
}) => {
|
||||
const { setIsUiReady } = getActions();
|
||||
|
||||
@ -101,14 +104,12 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
shouldRender: shouldRenderMask, transitionClassNames,
|
||||
} = useShowTransition(!isReady, undefined, true);
|
||||
|
||||
const customBackgroundValue = useCustomBackground(theme, customBackground);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: number | undefined;
|
||||
|
||||
const safePreload = async () => {
|
||||
try {
|
||||
await preloadTasks[page]();
|
||||
await preloadTasks[page!]();
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
@ -116,7 +117,7 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
Promise.race([
|
||||
pause(MAX_PRELOAD_DELAY),
|
||||
safePreload(),
|
||||
page ? safePreload() : Promise.resolve(),
|
||||
]).then(() => {
|
||||
markReady();
|
||||
setIsUiReady({ uiReadyState: 1 });
|
||||
@ -137,38 +138,26 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
// 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 (
|
||||
<div id="UiLoader">
|
||||
<div
|
||||
id="UiLoader"
|
||||
className={styles.root}
|
||||
style={`--theme-background-color: ${theme === 'dark' ? DARK_THEME_BG_COLOR : LIGHT_THEME_BG_COLOR}`}
|
||||
>
|
||||
{children}
|
||||
{shouldRenderMask && !shouldSkipHistoryAnimations && (
|
||||
<div className={buildClassName('mask', transitionClassNames)}>
|
||||
{shouldRenderMask && !shouldSkipHistoryAnimations && Boolean(page) && (
|
||||
<div className={buildClassName(styles.mask, transitionClassNames)}>
|
||||
{page === 'main' ? (
|
||||
<>
|
||||
<div
|
||||
className="left"
|
||||
className={styles.left}
|
||||
style={leftColumnWidth ? `width: ${leftColumnWidth}px` : undefined}
|
||||
/>
|
||||
<div
|
||||
className={middleClassName}
|
||||
style={buildStyle(...inlineStyles)}
|
||||
/>
|
||||
{isRightColumnShown && <div className="right" />}
|
||||
<div className={buildClassName(styles.middle, transitionClassNames)} />
|
||||
{isRightColumnShown && <div className={styles.right} />}
|
||||
</>
|
||||
) : (
|
||||
<div className="blank" />
|
||||
<div className={styles.blank} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -179,9 +168,6 @@ const UiLoader: FC<OwnProps & StateProps> = ({
|
||||
export default withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const theme = selectTheme(global);
|
||||
const {
|
||||
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor,
|
||||
} = global.settings.themes[theme] || {};
|
||||
|
||||
return {
|
||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||
@ -189,9 +175,6 @@ export default withGlobal<OwnProps>(
|
||||
isRightColumnShown: selectIsRightColumnShown(global),
|
||||
leftColumnWidth: global.leftColumnWidth,
|
||||
theme,
|
||||
customBackground,
|
||||
isBackgroundBlurred,
|
||||
backgroundColor,
|
||||
};
|
||||
},
|
||||
)(UiLoader);
|
||||
|
||||
@ -10,6 +10,8 @@ import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs
|
||||
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.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 HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
||||
@ -37,6 +39,7 @@ export const ANIMATED_STICKERS_PATHS = {
|
||||
FoldersAll,
|
||||
FoldersNew,
|
||||
DiscussionGroups,
|
||||
Lock,
|
||||
CameraFlip,
|
||||
HandFilled,
|
||||
HandOutline,
|
||||
@ -51,6 +54,7 @@ export const ANIMATED_STICKERS_PATHS = {
|
||||
JoinRequest,
|
||||
Invite,
|
||||
QrPlane,
|
||||
Congratulations,
|
||||
};
|
||||
|
||||
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
.left-header {
|
||||
height: var(--header-height);
|
||||
padding: 0.375rem 1rem 0.5rem 0.8125rem;
|
||||
padding: 0.375rem 0.8125rem 0.5rem 0.8125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
margin-left: 0.875rem;
|
||||
margin-left: 0.8125rem;
|
||||
max-width: calc(100% - 3.25rem);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
@ -11,6 +11,7 @@ import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||
import { useResize } from '../../hooks/useResize';
|
||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||
import useOnChange from '../../hooks/useOnChange';
|
||||
|
||||
import Transition from '../ui/Transition';
|
||||
import LeftMain from './main/LeftMain';
|
||||
@ -27,6 +28,8 @@ type StateProps = {
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
leftColumnWidth?: number;
|
||||
currentUserId?: string;
|
||||
hasPasscode?: boolean;
|
||||
nextSettingsScreen?: SettingsScreens;
|
||||
};
|
||||
|
||||
enum ContentType {
|
||||
@ -50,6 +53,8 @@ const LeftColumn: FC<StateProps> = ({
|
||||
shouldSkipHistoryAnimations,
|
||||
leftColumnWidth,
|
||||
currentUserId,
|
||||
hasPasscode,
|
||||
nextSettingsScreen,
|
||||
}) => {
|
||||
const {
|
||||
setGlobalSearchQuery,
|
||||
@ -61,6 +66,7 @@ const LeftColumn: FC<StateProps> = ({
|
||||
setLeftColumnWidth,
|
||||
resetLeftColumnWidth,
|
||||
openChat,
|
||||
requestNextSettingsScreen,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -91,17 +97,30 @@ const LeftColumn: FC<StateProps> = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const handleReset = useCallback((forceReturnToChatList?: boolean) => {
|
||||
if (content === LeftColumnContent.NewGroupStep2
|
||||
&& !forceReturnToChatList
|
||||
) {
|
||||
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||
function fullReset() {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content === LeftColumnContent.NewChannelStep2
|
||||
&& !forceReturnToChatList
|
||||
) {
|
||||
if (content === LeftColumnContent.NewChannelStep2) {
|
||||
setContent(LeftColumnContent.NewChannelStep1);
|
||||
return;
|
||||
}
|
||||
@ -145,8 +164,33 @@ const LeftColumn: FC<StateProps> = ({
|
||||
case SettingsScreens.TwoFaDisabled:
|
||||
case SettingsScreens.TwoFaEnabled:
|
||||
case SettingsScreens.TwoFaCongratulations:
|
||||
case SettingsScreens.PasscodeDisabled:
|
||||
case SettingsScreens.PasscodeEnabled:
|
||||
case SettingsScreens.PasscodeCongratulations:
|
||||
setSettingsScreen(SettingsScreens.Privacy);
|
||||
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.PrivacyPhoneNumberDeniedContacts:
|
||||
setSettingsScreen(SettingsScreens.PrivacyPhoneNumber);
|
||||
@ -235,18 +279,10 @@ const LeftColumn: FC<StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setContent(LeftColumnContent.ChatList);
|
||||
setContactsFilter('');
|
||||
setGlobalSearchQuery({ query: '' });
|
||||
setGlobalSearchDate({ date: undefined });
|
||||
setGlobalSearchChatId({ id: undefined });
|
||||
resetChatCreation();
|
||||
setTimeout(() => {
|
||||
setLastResetTime(Date.now());
|
||||
}, RESET_TRANSITION_DELAY_MS);
|
||||
fullReset();
|
||||
}, [
|
||||
content, activeChatFolder, settingsScreen, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId,
|
||||
resetChatCreation,
|
||||
resetChatCreation, hasPasscode,
|
||||
]);
|
||||
|
||||
const handleSearchQuery = useCallback((query: string) => {
|
||||
@ -296,6 +332,14 @@ const LeftColumn: FC<StateProps> = ({
|
||||
}
|
||||
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
||||
|
||||
useOnChange(() => {
|
||||
if (nextSettingsScreen) {
|
||||
setContent(LeftColumnContent.Settings);
|
||||
setSettingsScreen(nextSettingsScreen);
|
||||
requestNextSettingsScreen(undefined);
|
||||
}
|
||||
}, [nextSettingsScreen, requestNextSettingsScreen]);
|
||||
|
||||
const {
|
||||
initResize, resetResize, handleMouseUp,
|
||||
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth);
|
||||
@ -401,6 +445,12 @@ export default memo(withGlobal(
|
||||
shouldSkipHistoryAnimations,
|
||||
leftColumnWidth,
|
||||
currentUserId,
|
||||
passcode: {
|
||||
hasPasscode,
|
||||
},
|
||||
settings: {
|
||||
nextScreen: nextSettingsScreen,
|
||||
},
|
||||
} = global;
|
||||
|
||||
return {
|
||||
@ -410,6 +460,8 @@ export default memo(withGlobal(
|
||||
shouldSkipHistoryAnimations,
|
||||
leftColumnWidth,
|
||||
currentUserId,
|
||||
hasPasscode,
|
||||
nextSettingsScreen,
|
||||
};
|
||||
},
|
||||
)(LeftColumn));
|
||||
|
||||
@ -92,6 +92,10 @@
|
||||
@include overflow-y-overlay();
|
||||
}
|
||||
|
||||
.passcode-lock {
|
||||
margin-left: 0.8125rem;
|
||||
}
|
||||
|
||||
// @optimization
|
||||
@include while-transition() {
|
||||
.Menu .bubble {
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ISettings } from '../../../types';
|
||||
import { LeftColumnContent } from '../../../types';
|
||||
import { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||
import type { ApiChat } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
IS_BETA,
|
||||
IS_TEST,
|
||||
} 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 { formatDateToString } from '../../../util/dateFormat';
|
||||
import switchTheme from '../../../util/switchTheme';
|
||||
@ -28,6 +28,7 @@ import { selectCurrentMessageList, selectTheme } from '../../../global/selectors
|
||||
import { isChatArchived } from '../../../global/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
@ -64,6 +65,7 @@ type StateProps =
|
||||
isMessageListOpen: boolean;
|
||||
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||
areChatsLoaded?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
}
|
||||
& Pick<GlobalState, 'connectionState' | 'isSyncing'>;
|
||||
|
||||
@ -93,6 +95,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
isMessageListOpen,
|
||||
isConnectionStatusMinimized,
|
||||
areChatsLoaded,
|
||||
hasPasscode,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -100,6 +103,8 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
setSettingOption,
|
||||
setGlobalSearchChatId,
|
||||
openChatByUsername,
|
||||
lockScreen,
|
||||
requestNextSettingsScreen,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
@ -129,6 +134,23 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
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 MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
@ -167,6 +189,12 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
||||
}, [currentUserId, openChat]);
|
||||
|
||||
const handleSelectPasscode = useCallback(() => {
|
||||
requestNextSettingsScreen(
|
||||
hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled,
|
||||
);
|
||||
}, [hasPasscode, requestNextSettingsScreen]);
|
||||
|
||||
const handleDarkModeToggle = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
@ -197,6 +225,10 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
openChatByUsername({ username: lang('Settings.TipsUsername') });
|
||||
}, [lang, openChatByUsername]);
|
||||
|
||||
const handleLockScreen = useCallback(() => {
|
||||
lockScreen();
|
||||
}, [lockScreen]);
|
||||
|
||||
const isSearchFocused = (
|
||||
Boolean(globalSearchChatId)
|
||||
|| content === LeftColumnContent.GlobalSearch
|
||||
@ -243,6 +275,13 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{lang('Settings')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="lock"
|
||||
onClick={handleSelectPasscode}
|
||||
>
|
||||
{lang('Passcode')}
|
||||
<span className="menu-item-badge">{lang('New')}</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="darkmode"
|
||||
onClick={handleDarkModeToggle}
|
||||
@ -344,6 +383,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
isOpen={connectionStatusPosition === 'overlay'}
|
||||
isCustom
|
||||
@ -383,6 +435,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||
isConnectionStatusMinimized,
|
||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||
};
|
||||
},
|
||||
)(LeftMainHeader));
|
||||
|
||||
@ -40,10 +40,14 @@
|
||||
overflow-y: auto;
|
||||
@include overflow-y-overlay();
|
||||
|
||||
&.no-border, &.two-fa {
|
||||
&.no-border, &.two-fa, &.local-passcode, &.password-form {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.password-form .input-group.error label::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.infinite-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -152,7 +156,9 @@
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.settings-content.two-fa & {
|
||||
.settings-content.two-fa &,
|
||||
.settings-content.password-form &,
|
||||
.settings-content.local-passcode & {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
||||
@ -25,6 +25,7 @@ import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
||||
import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
||||
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
||||
import SettingsQuickReaction from './SettingsQuickReaction';
|
||||
import SettingsPasscode from './passcode/SettingsPasscode';
|
||||
|
||||
import './Settings.scss';
|
||||
|
||||
@ -50,6 +51,11 @@ const TWO_FA_SCREENS = [
|
||||
SettingsScreens.TwoFaRecoveryEmailCode,
|
||||
];
|
||||
|
||||
const PASSCODE_SCREENS = [
|
||||
SettingsScreens.PasscodeDisabled,
|
||||
SettingsScreens.PasscodeEnabled,
|
||||
];
|
||||
|
||||
const FOLDERS_SCREENS = [
|
||||
SettingsScreens.Folders,
|
||||
SettingsScreens.FoldersCreateFolder,
|
||||
@ -108,7 +114,7 @@ export type OwnProps = {
|
||||
foldersDispatch: FolderEditDispatch;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
shouldSkipTransition?: boolean;
|
||||
onReset: () => void;
|
||||
onReset: (forceReturnToChatList?: true | Event) => void;
|
||||
};
|
||||
|
||||
const Settings: FC<OwnProps> = ({
|
||||
@ -121,8 +127,14 @@ const Settings: FC<OwnProps> = ({
|
||||
shouldSkipTransition,
|
||||
}) => {
|
||||
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 (
|
||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||
@ -168,9 +180,11 @@ const Settings: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
const isTwoFaScreen = TWO_FA_SCREENS.includes(screen);
|
||||
const isPasscodeScreen = PASSCODE_SCREENS.includes(screen);
|
||||
const isFoldersScreen = FOLDERS_SCREENS.includes(screen);
|
||||
const isPrivacyScreen = PRIVACY_SCREENS.includes(screen)
|
||||
|| isTwoFaScreen
|
||||
|| isPasscodeScreen
|
||||
|| Object.keys(privacyAllowScreens).includes(screen.toString())
|
||||
|| Object.values(privacyAllowScreens).find((key) => key === true);
|
||||
|
||||
@ -191,10 +205,10 @@ const Settings: FC<OwnProps> = ({
|
||||
<SettingsGeneral
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive
|
||||
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
||||
|| screen === SettingsScreens.GeneralChatBackground
|
||||
|| screen === SettingsScreens.QuickReaction
|
||||
|| isPrivacyScreen || isFoldersScreen}
|
||||
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
||||
|| screen === SettingsScreens.GeneralChatBackground
|
||||
|| screen === SettingsScreens.QuickReaction
|
||||
|| isPrivacyScreen || isFoldersScreen}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
);
|
||||
@ -214,7 +228,7 @@ const Settings: FC<OwnProps> = ({
|
||||
return (
|
||||
<SettingsPrivacy
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive || isPrivacyScreen || isTwoFaScreen}
|
||||
isActive={isScreenActive || isPrivacyScreen}
|
||||
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:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -157,6 +157,23 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
case SettingsScreens.TwoFaRecoveryEmailCurrentPassword:
|
||||
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:
|
||||
return <h3>{lang('Filters')}</h3>;
|
||||
case SettingsScreens.FoldersCreateFolder:
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useState } from '../../../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
import PasswordMonkey from '../../../common/PasswordMonkey';
|
||||
import PasswordForm from '../../../common/PasswordForm';
|
||||
import PasswordMonkey from '../../common/PasswordMonkey';
|
||||
import PasswordForm from '../../common/PasswordForm';
|
||||
|
||||
type OwnProps = {
|
||||
error?: string;
|
||||
isLoading?: boolean;
|
||||
shouldDisablePasswordManager?: boolean;
|
||||
expectedPassword?: string;
|
||||
placeholder?: string;
|
||||
hint?: string;
|
||||
@ -22,11 +23,12 @@ type OwnProps = {
|
||||
|
||||
const EQUAL_PASSWORD_ERROR = 'Passwords Should Be Equal';
|
||||
|
||||
const SettingsTwoFaPassword: FC<OwnProps> = ({
|
||||
const SettingsPasswordForm: FC<OwnProps> = ({
|
||||
isActive,
|
||||
onReset,
|
||||
error,
|
||||
isLoading,
|
||||
shouldDisablePasswordManager,
|
||||
expectedPassword,
|
||||
placeholder = 'Current Password',
|
||||
hint,
|
||||
@ -60,7 +62,7 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
<div className="settings-content password-form custom-scroll">
|
||||
<div className="settings-content-header no-border">
|
||||
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
|
||||
</div>
|
||||
@ -70,10 +72,12 @@ const SettingsTwoFaPassword: FC<OwnProps> = ({
|
||||
error={validationError || error}
|
||||
hint={hint}
|
||||
placeholder={placeholder}
|
||||
shouldDisablePasswordManager={shouldDisablePasswordManager}
|
||||
submitLabel={submitLabel || lang('Next')}
|
||||
clearError={handleClearError}
|
||||
isLoading={isLoading}
|
||||
isPasswordVisible={shouldShowPassword}
|
||||
shouldResetValue={isActive}
|
||||
onChangePasswordVisibility={setShouldShowPassword}
|
||||
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 = {
|
||||
hasPassword?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
blockedCount: number;
|
||||
isSensitiveEnabled?: boolean;
|
||||
canChangeSensitive?: boolean;
|
||||
@ -36,6 +37,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
hasPassword,
|
||||
hasPasscode,
|
||||
blockedCount,
|
||||
isSensitiveEnabled,
|
||||
canChangeSensitive,
|
||||
@ -112,6 +114,21 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</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
|
||||
icon="lock"
|
||||
narrow
|
||||
@ -247,14 +264,20 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
settings: {
|
||||
byKey: { hasPassword, isSensitiveEnabled, canChangeSensitive },
|
||||
byKey: {
|
||||
hasPassword, isSensitiveEnabled, canChangeSensitive,
|
||||
},
|
||||
privacy,
|
||||
},
|
||||
blocked,
|
||||
passcode: {
|
||||
hasPasscode,
|
||||
},
|
||||
} = global;
|
||||
|
||||
return {
|
||||
hasPassword,
|
||||
hasPasscode: Boolean(hasPasscode),
|
||||
blockedCount: blocked.totalCount,
|
||||
isSensitiveEnabled,
|
||||
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 SettingsTwoFaEnabled from './SettingsTwoFaEnabled';
|
||||
import SettingsTwoFaPassword from './SettingsTwoFaPassword';
|
||||
import SettingsTwoFaPassword from '../SettingsPasswordForm';
|
||||
import SettingsTwoFaStart from './SettingsTwoFaStart';
|
||||
import SettingsTwoFaSkippableForm from './SettingsTwoFaSkippableForm';
|
||||
import SettingsTwoFaCongratulations from './SettingsTwoFaCongratulations';
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import type { FC } 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 { selectAnimatedEmoji } from '../../../../global/selectors';
|
||||
import { STICKER_SIZE_TWO_FA } from '../../../../config';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
|
||||
import Button from '../../../ui/Button';
|
||||
import AnimatedEmoji from '../../../common/AnimatedEmoji';
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
@ -18,12 +16,8 @@ type OwnProps = {
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
animatedEmoji: ApiSticker;
|
||||
};
|
||||
|
||||
const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
||||
isActive, onReset, animatedEmoji, onScreenSelect,
|
||||
const SettingsTwoFaCongratulations: FC<OwnProps> = ({
|
||||
isActive, onReset, onScreenSelect,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
@ -39,7 +33,7 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<div className="settings-content two-fa custom-scroll">
|
||||
<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">
|
||||
{lang('TwoStepVerificationPasswordSetInfo')}
|
||||
@ -53,8 +47,4 @@ const SettingsTwoFaCongratulations: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global) => {
|
||||
return {
|
||||
animatedEmoji: selectAnimatedEmoji(global, '🥳'),
|
||||
};
|
||||
})(SettingsTwoFaCongratulations));
|
||||
export default memo(SettingsTwoFaCongratulations);
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
margin: auto;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
background: var(--color-background);
|
||||
border-radius: var(--border-radius-default);
|
||||
}
|
||||
|
||||
.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;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: var(--color-background);
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
|
||||
@ -356,7 +356,6 @@ const Main: FC<StateProps> = ({
|
||||
// Online status and browser tab indicators
|
||||
useBackgroundMode(handleBlur, handleFocus);
|
||||
useBeforeUnload(handleBlur);
|
||||
|
||||
usePreventPinchZoomGesture(isMediaViewerOpen);
|
||||
|
||||
return (
|
||||
@ -419,7 +418,13 @@ function updatePageTitle(nextTitle: string) {
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const { settings: { byKey: { animationLevel, language, wasTimeFormatSetManually } } } = global;
|
||||
const {
|
||||
settings: {
|
||||
byKey: {
|
||||
animationLevel, language, wasTimeFormatSetManually,
|
||||
},
|
||||
},
|
||||
} = global;
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
||||
const audioMessage = audioChatId && audioMessageId
|
||||
? selectChatMessage(global, audioChatId, audioMessageId)
|
||||
|
||||
@ -153,7 +153,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
}, [canSearch, handleSearchClick]);
|
||||
|
||||
useHotkeys({
|
||||
'meta+F': handleHotkeySearchClick,
|
||||
'Meta+F': handleHotkeySearchClick,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
body:not(.animation-level-0) &.with-transition {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -17,67 +20,70 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
html.theme-light &:not(.custom-bg-image)::before {
|
||||
background-image: url('../assets/chat-bg-br.png');
|
||||
:global(html.theme-light) &:not(.customBgImage)::before {
|
||||
background-image: url('../../assets/chat-bg-br.png');
|
||||
}
|
||||
|
||||
&:not(.custom-bg-image).custom-bg-color::before {
|
||||
&:not(.customBgImage).customBgColor::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.custom-bg-image::before {
|
||||
&.customBgImage::before {
|
||||
background-image: var(--custom-background) !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
body:not(.animation-level-0) &.custom-bg-image.with-transition::before {
|
||||
transition: background-image var(--layer-transition);
|
||||
:global(body:not(.animation-level-0)) &.withTransition {
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&.customBgImage::before {
|
||||
transition: background-image var(--layer-transition);
|
||||
}
|
||||
}
|
||||
|
||||
&.custom-bg-image.blurred::before {
|
||||
&.customBgImage.blurred::before {
|
||||
filter: blur(12px);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1276px) {
|
||||
body.animation-level-2 &::before {
|
||||
:global(body.animation-level-2) &:not(.customBgImage)::before {
|
||||
overflow: hidden;
|
||||
transform: scale(1);
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
|
||||
html.theme-light body.animation-level-2 &:not(.custom-bg-image).with-right-column::before {
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
:global(html.theme-light body.animation-level-2) &:not(.customBgImage).withRightColumn::before {
|
||||
@media screen and (min-width: 1276px) {
|
||||
transform: scaleX(0.67) !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1921px) {
|
||||
transform: scaleX(0.8) !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 2600px) {
|
||||
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: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background-image: url('../assets/chat-bg-pattern-light.png');
|
||||
background-position: top left;
|
||||
background-image: url('../../assets/chat-bg-pattern-light.png');
|
||||
background-position: top right;
|
||||
background-size: 510px auto;
|
||||
background-repeat: repeat;
|
||||
mix-blend-mode: overlay;
|
||||
|
||||
html.theme-dark & {
|
||||
background-image: url('../assets/chat-bg-pattern-dark.png');
|
||||
:global(html.theme-dark) & {
|
||||
background-image: url('../../assets/chat-bg-pattern-dark.png');
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -75,6 +75,7 @@ import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
||||
import ReactorListModal from './ReactorListModal.async';
|
||||
|
||||
import './MiddleColumn.scss';
|
||||
import styles from './MiddleColumn.module.scss';
|
||||
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
@ -319,11 +320,12 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
);
|
||||
|
||||
const bgClassName = buildClassName(
|
||||
'bg-layers with-transition',
|
||||
customBackground && 'custom-bg-image',
|
||||
backgroundColor && 'custom-bg-color',
|
||||
customBackground && isBackgroundBlurred && 'blurred',
|
||||
isRightColumnShown && 'with-right-column',
|
||||
styles.background,
|
||||
styles.withTransition,
|
||||
customBackground && styles.customBgImage,
|
||||
backgroundColor && styles.customBgColor,
|
||||
customBackground && isBackgroundBlurred && styles.blurred,
|
||||
isRightColumnShown && styles.withRightColumn,
|
||||
);
|
||||
|
||||
const messagingDisabledClassName = buildClassName(
|
||||
@ -389,7 +391,6 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
onClick={(IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown) ? handleTabletFocus : undefined}
|
||||
>
|
||||
<div
|
||||
id="middle-column-bg"
|
||||
className={bgClassName}
|
||||
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
||||
/>
|
||||
|
||||
@ -10,7 +10,7 @@ const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneTo
|
||||
copySelectedMessages();
|
||||
}
|
||||
|
||||
useHotkeys({ 'meta+C': handleCopy });
|
||||
useHotkeys({ 'Meta+C': handleCopy });
|
||||
};
|
||||
|
||||
export default useCopySelectedMessages;
|
||||
|
||||
@ -319,7 +319,10 @@ const RightColumn: FC<StateProps> = ({
|
||||
renderCount={MAIN_SCREENS_COUNT + MANAGEMENT_SCREENS_COUNT}
|
||||
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
||||
shouldCleanup
|
||||
cleanupExceptionKey={renderingContentKey === RightColumnContent.MessageStatistics ? RightColumnContent.Statistics : undefined}
|
||||
cleanupExceptionKey={
|
||||
renderingContentKey === RightColumnContent.MessageStatistics
|
||||
? RightColumnContent.Statistics : undefined
|
||||
}
|
||||
>
|
||||
{renderContent}
|
||||
</Transition>
|
||||
|
||||
@ -4,6 +4,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef, useCallback, useState } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
import Spinner from './Spinner';
|
||||
import RippleEffect from './RippleEffect';
|
||||
@ -171,7 +172,7 @@ const Button: FC<OwnProps> = ({
|
||||
title={ariaLabel}
|
||||
tabIndex={tabIndex}
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
style={backgroundImage ? `background-image: url(${backgroundImage})` : style}
|
||||
style={buildStyle(backgroundImage && `background-image: url(${backgroundImage})`)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div>
|
||||
|
||||
@ -9,6 +9,7 @@ import React, {
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import resetScroll from '../../util/resetScroll';
|
||||
import { IS_ANDROID } from '../../util/environment';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
@ -226,7 +227,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
{withAbsolutePositioning && items?.length ? (
|
||||
<div
|
||||
teactFastList={!noFastList}
|
||||
style={`position: relative;${IS_ANDROID ? ` height: ${maxHeight}px;` : undefined}`}
|
||||
style={buildStyle('position: relative', IS_ANDROID && `height: ${maxHeight}px`)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,7 @@ import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
@ -143,8 +144,10 @@ const Menu: FC<OwnProps> = ({
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={bubbleClassName}
|
||||
style={`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY};${
|
||||
bubbleStyle || ''}`}
|
||||
style={buildStyle(
|
||||
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
||||
bubbleStyle,
|
||||
)}
|
||||
onClick={autoClose ? onClose : undefined}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -69,10 +69,18 @@
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
& > .Switcher {
|
||||
.Switcher, .menu-item-badge {
|
||||
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"] {
|
||||
i {
|
||||
margin-left: 2rem;
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
position: relative;
|
||||
z-index: var(--z-modal);
|
||||
|
||||
&.confirm {
|
||||
z-index: var(--z-lock-screen);
|
||||
}
|
||||
|
||||
&.delete,
|
||||
&.error,
|
||||
&.confirm,
|
||||
|
||||
@ -23,6 +23,7 @@ export const DEBUG_PAYMENT_SMART_GLOCAL = false;
|
||||
|
||||
export const SESSION_USER_KEY = 'user_auth';
|
||||
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_KEY = 'tt-global-state';
|
||||
@ -122,6 +123,8 @@ export const API_THROTTLE_RESET_UPDATES = new Set([
|
||||
'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_MOBILE_FACTOR = 11;
|
||||
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_MODAL = 64;
|
||||
export const STICKER_SIZE_TWO_FA = 160;
|
||||
export const STICKER_SIZE_PASSCODE = 160;
|
||||
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
||||
export const STICKER_SIZE_FOLDER_SETTINGS = 100;
|
||||
export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
||||
|
||||
@ -10,6 +10,7 @@ import './ui/misc';
|
||||
import './ui/payments';
|
||||
import './ui/calls';
|
||||
import './ui/mediaViewer';
|
||||
import './ui/passcode';
|
||||
|
||||
import './api/initial';
|
||||
import './api/chats';
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
MEDIA_CACHE_NAME_AVATARS,
|
||||
MEDIA_PROGRESSIVE_CACHE_NAME,
|
||||
IS_TEST,
|
||||
LOCK_SCREEN_ANIMATION_DURATION_MS,
|
||||
} from '../../../config';
|
||||
import { IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, PLATFORM_ENV } from '../../../util/environment';
|
||||
import { unsubscribe } from '../../../util/notifications';
|
||||
@ -24,6 +25,9 @@ import {
|
||||
clearLegacySessions,
|
||||
} from '../../../util/sessions';
|
||||
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) => {
|
||||
if (!IS_TEST) {
|
||||
@ -142,6 +146,7 @@ addActionHandler('signOut', async (_global, _actions, payload) => {
|
||||
|
||||
addActionHandler('reset', () => {
|
||||
clearStoredSession();
|
||||
clearEncryptedSession();
|
||||
|
||||
void cacheApi.clear(MEDIA_CACHE_NAME);
|
||||
void cacheApi.clear(MEDIA_CACHE_NAME_AVATARS);
|
||||
@ -161,6 +166,18 @@ addActionHandler('reset', () => {
|
||||
getActions().init();
|
||||
});
|
||||
|
||||
addActionHandler('softReset', () => {
|
||||
clearStoredSession();
|
||||
|
||||
void clearLegacySessions();
|
||||
|
||||
updateAppBadge(0);
|
||||
|
||||
let global = getGlobal();
|
||||
global = clearGlobalForLockScreen(global);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('disconnect', () => {
|
||||
void callApi('disconnect');
|
||||
});
|
||||
@ -194,3 +211,29 @@ addActionHandler('deleteDeviceToken', (global) => {
|
||||
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);
|
||||
});
|
||||
|
||||
addActionHandler('requestNextSettingsScreen', (global, actions, nextScreen) => {
|
||||
return {
|
||||
...global,
|
||||
settings: {
|
||||
...global.settings,
|
||||
nextScreen,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -27,11 +27,13 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectVisibleUsers,
|
||||
} from './selectors';
|
||||
import { hasStoredSession } from '../util/sessions';
|
||||
import { hasStoredSession, loadStoredSession } from '../util/sessions';
|
||||
import { INITIAL_STATE } from './initialState';
|
||||
import { parseLocationHash } from '../util/routing';
|
||||
import { isUserId } from './helpers';
|
||||
import { getOrderedIds } from '../util/folderManager';
|
||||
import { clearGlobalForLockScreen } from './reducers';
|
||||
import { encryptSession } from '../util/passcode';
|
||||
|
||||
const UPDATE_THROTTLE = 5000;
|
||||
|
||||
@ -45,6 +47,16 @@ export function initCache() {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetCache = () => {
|
||||
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
||||
|
||||
if (!isCaching) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCaching();
|
||||
};
|
||||
|
||||
addActionHandler('saveSession', () => {
|
||||
if (isCaching) {
|
||||
return;
|
||||
@ -53,15 +65,7 @@ export function initCache() {
|
||||
setupCaching();
|
||||
});
|
||||
|
||||
addActionHandler('reset', () => {
|
||||
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
|
||||
|
||||
if (!isCaching) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCaching();
|
||||
});
|
||||
addActionHandler('reset', resetCache);
|
||||
}
|
||||
|
||||
export function loadCache(initialState: GlobalState): GlobalState | undefined {
|
||||
@ -236,6 +240,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
cached.trustedBotIds = [];
|
||||
}
|
||||
|
||||
if (!cached.passcode) {
|
||||
cached.passcode = {};
|
||||
}
|
||||
|
||||
if (cached.activeSessions?.byHash === undefined) {
|
||||
cached.activeSessions = {
|
||||
byHash: {},
|
||||
@ -245,16 +253,30 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
}
|
||||
|
||||
function updateCache() {
|
||||
if (!isCaching || isHeavyAnimating()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const global = getGlobal();
|
||||
|
||||
if (global.isLoggingOut) {
|
||||
if (!isCaching || global.isLoggingOut || isHeavyAnimating()) {
|
||||
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 = {
|
||||
...INITIAL_STATE,
|
||||
...pick(global, [
|
||||
@ -295,10 +317,14 @@ function updateCache() {
|
||||
availableReactions: reduceAvailableReactions(global),
|
||||
isCallPanelVisible: undefined,
|
||||
trustedBotIds: global.trustedBotIds,
|
||||
passcode: pick(global.passcode, [
|
||||
'isScreenLocked',
|
||||
'hasPasscode',
|
||||
'invalidAttemptsCount',
|
||||
]),
|
||||
};
|
||||
|
||||
const json = JSON.stringify(reducedGlobal);
|
||||
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, json);
|
||||
return JSON.stringify(reducedGlobal);
|
||||
}
|
||||
|
||||
function reduceShowChatInfo(global: GlobalState): boolean {
|
||||
@ -352,7 +378,7 @@ function reduceChats(global: GlobalState): GlobalState['chats'] {
|
||||
function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
||||
const { currentUserId } = global;
|
||||
const byChatId: GlobalState['messages']['byChatId'] = {};
|
||||
const { chatId: currentChatId } = selectCurrentMessageList(global) || {};
|
||||
const { chatId: currentChatId, threadId, type } = selectCurrentMessageList(global) || {};
|
||||
const chatIdsToSave = [
|
||||
...currentChatId ? [currentChatId] : [],
|
||||
...currentUserId ? [currentUserId] : [],
|
||||
@ -380,7 +406,7 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
||||
|
||||
return {
|
||||
byChatId,
|
||||
messageLists: [],
|
||||
messageLists: currentChatId && threadId && type ? [{ chatId: currentChatId, threadId, type }] : [],
|
||||
sponsoredByChatId: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
import { addActionHandler } from './index';
|
||||
|
||||
import { INITIAL_STATE } from './initialState';
|
||||
import { IS_MOCKED_CLIENT } from '../config';
|
||||
import { initCache, loadCache } from './cache';
|
||||
import { cloneDeep } from '../util/iteratees';
|
||||
import { IS_MOCKED_CLIENT } from '../config';
|
||||
import { updatePasscodeSettings } from './reducers';
|
||||
|
||||
initCache();
|
||||
|
||||
addActionHandler('init', () => {
|
||||
const initial = cloneDeep(INITIAL_STATE);
|
||||
const state = loadCache(initial) || initial;
|
||||
if (IS_MOCKED_CLIENT) state.authState = 'authorizationStateReady';
|
||||
return state;
|
||||
let global = loadCache(initial) || initial;
|
||||
if (IS_MOCKED_CLIENT) global.authState = 'authorizationStateReady';
|
||||
|
||||
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: {},
|
||||
passcode: {},
|
||||
activeReactions: {},
|
||||
|
||||
shouldShowContextMenuHint: true,
|
||||
|
||||
@ -7,5 +7,6 @@ export * from './localSearch';
|
||||
export * from './management';
|
||||
export * from './settings';
|
||||
export * from './twoFaSettings';
|
||||
export * from './passcode';
|
||||
export * from './payments';
|
||||
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,
|
||||
AudioOrigin,
|
||||
ManagementState,
|
||||
SettingsScreens,
|
||||
} from '../types';
|
||||
import { typify } from '../lib/teact/teactn';
|
||||
import type { P2pMessage } from '../lib/secret-sauce';
|
||||
@ -494,6 +495,7 @@ export type GlobalState = {
|
||||
themes: Partial<Record<ThemeKey, IThemeSettings>>;
|
||||
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
|
||||
notifyExceptions?: Record<number, NotifyException>;
|
||||
nextScreen?: SettingsScreens;
|
||||
};
|
||||
|
||||
twoFaSettings: {
|
||||
@ -503,6 +505,14 @@ export type GlobalState = {
|
||||
waitingEmailCodeLength?: number;
|
||||
};
|
||||
|
||||
passcode: {
|
||||
isScreenLocked?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
error?: string;
|
||||
invalidAttemptsCount?: number;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
push?: {
|
||||
deviceToken: string;
|
||||
subscribedAt: number;
|
||||
@ -861,6 +871,21 @@ export interface ActionPayloads {
|
||||
sound: CallSound;
|
||||
};
|
||||
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 = (
|
||||
|
||||
@ -11,7 +11,7 @@ const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { mes
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys({ 'meta+C': handleCopy });
|
||||
useHotkeys({ 'Meta+C': handleCopy });
|
||||
};
|
||||
|
||||
export default useNativeCopySelectedMessages;
|
||||
|
||||
@ -38,7 +38,9 @@ if (DEBUG) {
|
||||
console.log('>>> FINISH INITIAL RENDER');
|
||||
}
|
||||
|
||||
document.addEventListener('dblclick', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('GLOBAL STATE', getGlobal());
|
||||
});
|
||||
if (DEBUG) {
|
||||
document.addEventListener('dblclick', () => {
|
||||
// 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);
|
||||
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 {
|
||||
|
||||
@ -199,6 +199,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--symbol-menu-height: 14.6875rem;
|
||||
}
|
||||
|
||||
--z-lock-screen: 3000;
|
||||
--z-ui-loader-mask: 2000;
|
||||
--z-notification: 1520;
|
||||
--z-right-column: 900;
|
||||
|
||||
@ -51,6 +51,9 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-key:before {
|
||||
content: "\e99a";
|
||||
}
|
||||
.icon-heart-outline:before {
|
||||
content: "\e99e";
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
@import "forms";
|
||||
@import "icons";
|
||||
@import "common";
|
||||
@import "bg";
|
||||
@import "../assets/fonts/roboto.css";
|
||||
@import "./print";
|
||||
|
||||
@ -66,12 +65,17 @@ body.cursor-ew-resize {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
|
||||
#root {
|
||||
#root,
|
||||
.full-height {
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
&.is-auth {
|
||||
background: var(--color-background);
|
||||
}
|
||||
}
|
||||
|
||||
#middle-column-portals,
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
.Modal,
|
||||
.ActiveCallHeader,
|
||||
.unread-count,
|
||||
#middle-column-bg,
|
||||
#middle-column-portals,
|
||||
.header-tools,
|
||||
.ScrollDownButton,
|
||||
|
||||
@ -214,6 +214,15 @@ export enum SettingsScreens {
|
||||
TwoFaRecoveryEmailCode,
|
||||
TwoFaCongratulations,
|
||||
QuickReaction,
|
||||
PasscodeDisabled,
|
||||
PasscodeNewPasscode,
|
||||
PasscodeNewPasscodeConfirm,
|
||||
PasscodeEnabled,
|
||||
PasscodeChangePasscodeCurrent,
|
||||
PasscodeChangePasscodeNew,
|
||||
PasscodeChangePasscodeConfirm,
|
||||
PasscodeTurnOff,
|
||||
PasscodeCongratulations,
|
||||
}
|
||||
|
||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||
|
||||
@ -5,6 +5,7 @@ export enum Type {
|
||||
Text,
|
||||
Blob,
|
||||
Json,
|
||||
ArrayBuffer,
|
||||
}
|
||||
|
||||
export async function fetch(
|
||||
@ -52,6 +53,8 @@ export async function fetch(
|
||||
}
|
||||
case Type.Json:
|
||||
return await response.json();
|
||||
case Type.ArrayBuffer:
|
||||
return await response.arrayBuffer();
|
||||
default:
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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"
|
||||
const request = new Request(key.replace(/:/g, '_'));
|
||||
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) {
|
||||
try {
|
||||
if (!cacheApi) {
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
} from '../api/types';
|
||||
import { ApiMediaFormat } from '../api/types';
|
||||
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 {
|
||||
getChatAvatarHash,
|
||||
@ -275,6 +275,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
||||
} = message;
|
||||
if (reaction) senderId = reaction.userId;
|
||||
|
||||
const { isScreenLocked } = global.passcode;
|
||||
const messageSender = senderId ? selectUser(global, senderId) : undefined;
|
||||
const messageAction = getMessageAction(message as ApiMessage);
|
||||
const actionTargetMessage = messageAction && replyToMessageId
|
||||
@ -293,7 +294,10 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
||||
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||
|
||||
let body: string;
|
||||
if (selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))) {
|
||||
if (
|
||||
!isScreenLocked
|
||||
&& selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))
|
||||
) {
|
||||
if (isActionMessage(message)) {
|
||||
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
|
||||
|
||||
@ -318,7 +322,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
||||
}
|
||||
|
||||
return {
|
||||
title: getChatTitle(getTranslation, chat, privateChatUser),
|
||||
title: isScreenLocked ? APP_NAME : getChatTitle(getTranslation, chat, privateChatUser),
|
||||
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 { 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';
|
||||
|
||||
const DC_IDS = [1, 2, 3, 4, 5];
|
||||
@ -12,8 +14,14 @@ export function hasStoredSession(withLegacy = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkSessionLocked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userAuthJson = localStorage.getItem(SESSION_USER_KEY);
|
||||
if (!userAuthJson) return false;
|
||||
if (!userAuthJson) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const userAuth = JSON.parse(userAuthJson);
|
||||
@ -57,6 +65,9 @@ export function loadStoredSession(): ApiSessionData | undefined {
|
||||
}
|
||||
|
||||
const userAuth = JSON.parse(localStorage.getItem(SESSION_USER_KEY)!);
|
||||
if (!userAuth) {
|
||||
return undefined;
|
||||
}
|
||||
const mainDcId = Number(userAuth.dcID);
|
||||
const keys: Record<number, string> = {};
|
||||
const hashes: Record<number, string> = {};
|
||||
@ -134,3 +145,9 @@ export function importTestSession() {
|
||||
// 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 themeClassName = `theme-${theme}`;
|
||||
if (document.documentElement.classList.contains(themeClassName)) {
|
||||
return;
|
||||
}
|
||||
const isDarkTheme = theme === 'dark';
|
||||
const shouldAnimate = isInitialized && withAnimation;
|
||||
const startIndex = isDarkTheme ? 0 : 1;
|
||||
@ -43,7 +47,7 @@ const switchTheme = (theme: ISettings['theme'], withAnimation: boolean) => {
|
||||
if (isInitialized) {
|
||||
document.documentElement.classList.add('no-animations');
|
||||
}
|
||||
document.documentElement.classList.add(`theme-${theme}`);
|
||||
document.documentElement.classList.add(themeClassName);
|
||||
if (themeColorTag) {
|
||||
themeColorTag.setAttribute('content', isDarkTheme ? '#212121' : '#fff');
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ module.exports = (env = {}, argv = {}) => {
|
||||
stats: 'minimal',
|
||||
},
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[id].[chunkhash].js',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user