diff --git a/package-lock.json b/package-lock.json index 756f730dd..d1d4e7c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 19c30fe11..59bb09fe8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index b8c1fc499..9285be41a 100644 --- a/src/App.tsx +++ b/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; +type StateProps = { + authState: GlobalState['authState']; + isScreenLocked?: boolean; +}; -const App: FC = ({ authState }) => { +enum AppScreens { + auth, + lock, + main, + inactive, +} + +const App: FC = ({ + 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 = ({ authState }) => { // return ; - if (isInactive) { - return ; - } + 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 ; + 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 ; + case AppScreens.main: + return
; + case AppScreens.lock: + return ; + case AppScreens.inactive: + return ; } } - return hasStoredSession(true) ? renderMain() : ; -}; - -function renderMain() { return ( - -
+ + + {renderContent} + ); -} +}; export default withGlobal( - (global): StateProps => pick(global, ['authState']), + (global): StateProps => { + return { + authState: global.authState, + isScreenLocked: global.passcode?.isScreenLocked, + }; + }, )(App); diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index ccabb12ad..33172a5e5 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -13,7 +13,7 @@ interface LocalDb { webDocuments: Record; } -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); +} diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 0a1844935..0a01f6a9a 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -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(); } diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 746609e42..524f03810 100644 Binary files a/src/assets/fonts/icomoon.woff and b/src/assets/fonts/icomoon.woff differ diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index 8370fbe9e..30e6da39d 100644 Binary files a/src/assets/fonts/icomoon.woff2 and b/src/assets/fonts/icomoon.woff2 differ diff --git a/src/assets/lock.png b/src/assets/lock.png new file mode 100644 index 000000000..a9789c1f7 Binary files /dev/null and b/src/assets/lock.png differ diff --git a/src/assets/mastercard.svg b/src/assets/mastercard.svg index 73959ae78..43158cbb5 100644 --- a/src/assets/mastercard.svg +++ b/src/assets/mastercard.svg @@ -1,2 +1 @@ - -image/svg+xml + \ No newline at end of file diff --git a/src/assets/tgs/settings/Congratulations.tgs b/src/assets/tgs/settings/Congratulations.tgs new file mode 100644 index 000000000..fe9704d9d Binary files /dev/null and b/src/assets/tgs/settings/Congratulations.tgs differ diff --git a/src/assets/tgs/settings/Lock.tgs b/src/assets/tgs/settings/Lock.tgs new file mode 100644 index 000000000..853827521 Binary files /dev/null and b/src/assets/tgs/settings/Lock.tgs differ diff --git a/src/assets/visa.svg b/src/assets/visa.svg index b0953f5b6..ec5e4adca 100644 --- a/src/assets/visa.svg +++ b/src/assets/visa.svg @@ -1,2 +1 @@ - -image/svg+xml + \ No newline at end of file diff --git a/src/bundles/main.ts b/src/bundles/main.ts index ed00dcae0..9669ee2b7 100644 --- a/src/bundles/main.ts +++ b/src/bundles/main.ts @@ -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(); } diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx index bc8c83bca..e885fe47e 100644 --- a/src/components/auth/Auth.tsx +++ b/src/components/auth/Auth.tsx @@ -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; -const Auth: FC = ({ - authState, +const Auth: FC = ({ + 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 = ({ }; }, []); - switch (authState) { + // For animation purposes + const renderingAuthState = useCurrentOrPrev( + authState !== 'authorizationStateReady' ? authState : undefined, + true, + ); + + switch (renderingAuthState) { case 'authorizationStateWaitCode': - return ; + return ; case 'authorizationStateWaitPassword': - return ; + return ; case 'authorizationStateWaitRegistration': return ; case 'authorizationStateWaitPhoneNumber': - return ; + return ; case 'authorizationStateWaitQrCode': - return ; + return ; default: - return isMobile - ? - : ; + return isMobile ? : ; } }; -export default memo(withGlobal( +export default memo(withGlobal( (global): StateProps => pick(global, ['authState']), )(Auth)); diff --git a/src/components/calls/group/GroupCallParticipantMenu.tsx b/src/components/calls/group/GroupCallParticipantMenu.tsx index bbe6d9eca..ca35475da 100644 --- a/src/components/calls/group/GroupCallParticipantMenu.tsx +++ b/src/components/calls/group/GroupCallParticipantMenu.tsx @@ -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 = ({ 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" > diff --git a/src/components/common/AnimatedEmoji.tsx b/src/components/common/AnimatedEmoji.tsx index 2c128e370..2385e8584 100644 --- a/src/components/common/AnimatedEmoji.tsx +++ b/src/components/common/AnimatedEmoji.tsx @@ -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 = ({ 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 = ({ return (
diff --git a/src/components/common/PasswordForm.tsx b/src/components/common/PasswordForm.tsx index fffb8c4f3..03975acf1 100644 --- a/src/components/common/PasswordForm.tsx +++ b/src/components/common/PasswordForm.tsx @@ -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 = ({ hint, placeholder = 'Password', submitLabel = 'Next', + shouldShowSubmit, + shouldResetValue, + shouldDisablePasswordManager = false, + noRipple = false, clearError, onChangePasswordVisibility, onInputChange, @@ -46,6 +54,12 @@ const PasswordForm: FC = ({ 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 = ({ 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" /> @@ -117,8 +132,8 @@ const PasswordForm: FC = ({
- {canSubmit && ( - )} diff --git a/src/components/common/UiLoader.module.scss b/src/components/common/UiLoader.module.scss new file mode 100644 index 000000000..905eaa8cc --- /dev/null +++ b/src/components/common/UiLoader.module.scss @@ -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); +} diff --git a/src/components/common/UiLoader.scss b/src/components/common/UiLoader.scss deleted file mode 100644 index 2f2d98cac..000000000 --- a/src/components/common/UiLoader.scss +++ /dev/null @@ -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); - } -} diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index ea73c92a6..ba851a4f6 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -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 - & { - isRightColumnShown?: boolean; - leftColumnWidth?: number; - isBackgroundBlurred?: boolean; - theme: ThemeKey; - customBackground?: string; - backgroundColor?: string; - }; +type StateProps = Pick & { + 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 = ({ @@ -90,9 +96,6 @@ const UiLoader: FC = ({ shouldSkipHistoryAnimations, leftColumnWidth, theme, - backgroundColor, - customBackground, - isBackgroundBlurred, }) => { const { setIsUiReady } = getActions(); @@ -101,14 +104,12 @@ const UiLoader: FC = ({ 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 = ({ Promise.race([ pause(MAX_PRELOAD_DELAY), - safePreload(), + page ? safePreload() : Promise.resolve(), ]).then(() => { markReady(); setIsUiReady({ uiReadyState: 1 }); @@ -137,38 +138,26 @@ const UiLoader: FC = ({ // 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 ( -
+
{children} - {shouldRenderMask && !shouldSkipHistoryAnimations && ( -
+ {shouldRenderMask && !shouldSkipHistoryAnimations && Boolean(page) && ( +
{page === 'main' ? ( <>
-
- {isRightColumnShown &&
} +
+ {isRightColumnShown &&
} ) : ( -
+
)}
)} @@ -179,9 +168,6 @@ const UiLoader: FC = ({ export default withGlobal( (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( isRightColumnShown: selectIsRightColumnShown(global), leftColumnWidth: global.leftColumnWidth, theme, - customBackground, - isBackgroundBlurred, - backgroundColor, }; }, )(UiLoader); diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 2becfc365..c8d5924bf 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -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) { diff --git a/src/components/left/LeftColumn.scss b/src/components/left/LeftColumn.scss index 13fe30c67..d8edb72af 100644 --- a/src/components/left/LeftColumn.scss +++ b/src/components/left/LeftColumn.scss @@ -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) { diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 948a6fb32..872edc766 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -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 = ({ shouldSkipHistoryAnimations, leftColumnWidth, currentUserId, + hasPasscode, + nextSettingsScreen, }) => { const { setGlobalSearchQuery, @@ -61,6 +66,7 @@ const LeftColumn: FC = ({ setLeftColumnWidth, resetLeftColumnWidth, openChat, + requestNextSettingsScreen, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -91,17 +97,30 @@ const LeftColumn: FC = ({ 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 = ({ 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 = ({ 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 = ({ } }, [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)); diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index 4768d7cb4..f612e80e6 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -92,6 +92,10 @@ @include overflow-y-overlay(); } + .passcode-lock { + margin-left: 0.8125rem; + } + // @optimization @include while-transition() { .Menu .bubble { diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 3498ccecb..7a8d15ad8 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -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; @@ -93,6 +95,7 @@ const LeftMainHeader: FC = ({ isMessageListOpen, isConnectionStatusMinimized, areChatsLoaded, + hasPasscode, }) => { const { openChat, @@ -100,6 +103,8 @@ const LeftMainHeader: FC = ({ setSettingOption, setGlobalSearchChatId, openChatByUsername, + lockScreen, + requestNextSettingsScreen, } = getActions(); const lang = useLang(); @@ -129,6 +134,23 @@ const LeftMainHeader: FC = ({ 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 = ({ openChat({ id: currentUserId, shouldReplaceHistory: true }); }, [currentUserId, openChat]); + const handleSelectPasscode = useCallback(() => { + requestNextSettingsScreen( + hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled, + ); + }, [hasPasscode, requestNextSettingsScreen]); + const handleDarkModeToggle = useCallback((e: React.SyntheticEvent) => { e.stopPropagation(); const newTheme = theme === 'light' ? 'dark' : 'light'; @@ -197,6 +225,10 @@ const LeftMainHeader: FC = ({ 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 = ({ > {lang('Settings')} + + {lang('Passcode')} + {lang('New')} + = ({ /> )} + {hasPasscode && ( + + )} ( isMessageListOpen: Boolean(selectCurrentMessageList(global)), isConnectionStatusMinimized, areChatsLoaded: Boolean(global.chats.listIds.active), + hasPasscode: Boolean(global.passcode.hasPasscode), }; }, )(LeftMainHeader)); diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index 1d3b9809d..8985e0aeb 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -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; } diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 89f875d28..bb788d42e 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -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 = ({ @@ -121,8 +127,14 @@ const Settings: FC = ({ shouldSkipTransition, }) => { const [twoFaState, twoFaDispatch] = useTwoFaReducer(); + const [privacyPasscode, setPrivacyPasscode] = useState(''); + + 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 = ({ }; 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 = ({ ); @@ -214,7 +228,7 @@ const Settings: FC = ({ return ( ); @@ -348,6 +362,27 @@ const Settings: FC = ({ /> ); + 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 ( + + ); + default: return undefined; } diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index acfab32bf..a2415bd77 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -157,6 +157,23 @@ const SettingsHeader: FC = ({ case SettingsScreens.TwoFaRecoveryEmailCurrentPassword: return

{lang('PleaseEnterCurrentPassword')}

; + case SettingsScreens.PasscodeDisabled: + case SettingsScreens.PasscodeEnabled: + case SettingsScreens.PasscodeNewPasscode: + case SettingsScreens.PasscodeNewPasscodeConfirm: + case SettingsScreens.PasscodeCongratulations: + return

{lang('Passcode')}

; + + case SettingsScreens.PasscodeTurnOff: + return

{lang('PasscodeController.Disable.Title')}

; + + case SettingsScreens.PasscodeChangePasscodeCurrent: + case SettingsScreens.PasscodeChangePasscodeNew: + return

{lang('PasscodeController.Change.Title')}

; + + case SettingsScreens.PasscodeChangePasscodeConfirm: + return

{lang('PasscodeController.ReEnterPasscode.Placeholder')}

; + case SettingsScreens.Folders: return

{lang('Filters')}

; case SettingsScreens.FoldersCreateFolder: diff --git a/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx b/src/components/left/settings/SettingsPasswordForm.tsx similarity index 71% rename from src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx rename to src/components/left/settings/SettingsPasswordForm.tsx index 35361f08a..f49af800c 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaPassword.tsx +++ b/src/components/left/settings/SettingsPasswordForm.tsx @@ -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 = ({ +const SettingsPasswordForm: FC = ({ isActive, onReset, error, isLoading, + shouldDisablePasswordManager, expectedPassword, placeholder = 'Current Password', hint, @@ -60,7 +62,7 @@ const SettingsTwoFaPassword: FC = ({ }); return ( -
+
@@ -70,10 +72,12 @@ const SettingsTwoFaPassword: FC = ({ 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 = ({ ); }; -export default memo(SettingsTwoFaPassword); +export default memo(SettingsPasswordForm); diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 561583edc..2aaf0912e 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -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 = ({ onScreenSelect, onReset, hasPassword, + hasPasscode, blockedCount, isSensitiveEnabled, canChangeSensitive, @@ -112,6 +114,21 @@ const SettingsPrivacy: FC = ({ )}
+ onScreenSelect( + hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled, + )} + > +
+ {lang('Passcode')} + + {lang(hasPasscode ? 'PasswordOn' : 'PasswordOff')} + +
+
( (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, diff --git a/src/components/left/settings/passcode/SettingsPasscode.tsx b/src/components/left/settings/passcode/SettingsPasscode.tsx new file mode 100644 index 000000000..0b5938c57 --- /dev/null +++ b/src/components/left/settings/passcode/SettingsPasscode.tsx @@ -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 = ({ + 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 ( + + ); + + case SettingsScreens.PasscodeNewPasscode: + return ( + + ); + + case SettingsScreens.PasscodeNewPasscodeConfirm: + return ( + + ); + + case SettingsScreens.PasscodeCongratulations: + return ( + + ); + + case SettingsScreens.PasscodeEnabled: + return ( + + ); + + case SettingsScreens.PasscodeChangePasscodeCurrent: + return ( + + ); + + case SettingsScreens.PasscodeChangePasscodeNew: + return ( + + ); + + case SettingsScreens.PasscodeChangePasscodeConfirm: + return ( + + ); + + case SettingsScreens.PasscodeTurnOff: + return ( + + ); + + default: + return undefined; + } +}; + +export default memo(withGlobal( + (global): StateProps => ({ ...global.passcode }), +)(SettingsPasscode)); diff --git a/src/components/left/settings/passcode/SettingsPasscodeCongratulations.tsx b/src/components/left/settings/passcode/SettingsPasscodeCongratulations.tsx new file mode 100644 index 000000000..46070378b --- /dev/null +++ b/src/components/left/settings/passcode/SettingsPasscodeCongratulations.tsx @@ -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 = ({ + isActive, onReset, +}) => { + const lang = useLang(); + + const fullReset = useCallback(() => { + onReset(true); + }, [onReset]); + + useHistoryBack({ isActive, onBack: onReset }); + + return ( +
+
+ + +

+ Congratulations! +

+

+ Now you can lock the app with a passcode so that others can't open it. +

+
+ +
+ +
+
+ ); +}; + +export default memo(SettingsPasscodeCongratulations); diff --git a/src/components/left/settings/passcode/SettingsPasscodeEnabled.tsx b/src/components/left/settings/passcode/SettingsPasscodeEnabled.tsx new file mode 100644 index 000000000..6815b04eb --- /dev/null +++ b/src/components/left/settings/passcode/SettingsPasscodeEnabled.tsx @@ -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 = ({ + isActive, onReset, animatedEmoji, onScreenSelect, +}) => { + const lang = useLang(); + + useHistoryBack({ isActive, onBack: onReset }); + + return ( +
+
+ + +

+ Local passcode is enabled. +

+
+ +
+ onScreenSelect(SettingsScreens.PasscodeChangePasscodeCurrent)} + > + {lang('Passcode.Change')} + + onScreenSelect(SettingsScreens.PasscodeTurnOff)} + > + {lang('Passcode.TurnOff')} + +
+
+ ); +}; + +export default memo(withGlobal((global) => { + return { + animatedEmoji: selectAnimatedEmoji(global, '🔐'), + }; +})(SettingsPasscodeEnabled)); diff --git a/src/components/left/settings/passcode/SettingsPasscodeStart.tsx b/src/components/left/settings/passcode/SettingsPasscodeStart.tsx new file mode 100644 index 000000000..a073c3d0f --- /dev/null +++ b/src/components/left/settings/passcode/SettingsPasscodeStart.tsx @@ -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 = ({ + isActive, onReset, onStart, +}) => { + const lang = useLang(); + + useHistoryBack({ isActive, onBack: onReset }); + + return ( +
+
+ + +

+ 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. +

+

+ Note: if you forget your local passcode, you'll need to log out of Telegram WebZ and log in again. +

+
+ +
+ +
+
+ ); +}; + +export default memo(SettingsPasscodeStart); diff --git a/src/components/left/settings/twoFa/SettingsTwoFa.tsx b/src/components/left/settings/twoFa/SettingsTwoFa.tsx index 71e03133b..f806c3d74 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFa.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFa.tsx @@ -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'; diff --git a/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx b/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx index 60259040d..9151ac428 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFaCongratulations.tsx @@ -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 = ({ - isActive, onReset, animatedEmoji, onScreenSelect, +const SettingsTwoFaCongratulations: FC = ({ + isActive, onReset, onScreenSelect, }) => { const lang = useLang(); @@ -39,7 +33,7 @@ const SettingsTwoFaCongratulations: FC = ({ return (
- +

{lang('TwoStepVerificationPasswordSetInfo')} @@ -53,8 +47,4 @@ const SettingsTwoFaCongratulations: FC = ({ ); }; -export default memo(withGlobal((global) => { - return { - animatedEmoji: selectAnimatedEmoji(global, '🥳'), - }; -})(SettingsTwoFaCongratulations)); +export default memo(SettingsTwoFaCongratulations); diff --git a/src/components/main/AppInactive.scss b/src/components/main/AppInactive.scss index e2dad3c4f..8c7fbbf8c 100644 --- a/src/components/main/AppInactive.scss +++ b/src/components/main/AppInactive.scss @@ -10,6 +10,8 @@ margin: auto; padding: 1.5rem; text-align: center; + background: var(--color-background); + border-radius: var(--border-radius-default); } .title { diff --git a/src/components/main/LockScreen.async.tsx b/src/components/main/LockScreen.async.tsx new file mode 100644 index 000000000..aaeee70a8 --- /dev/null +++ b/src/components/main/LockScreen.async.tsx @@ -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 = (props) => { + const { isLocked } = props; + const LockScreen = useModuleLoader(Bundles.Main, 'LockScreen', !isLocked); + + // eslint-disable-next-line react/jsx-props-no-spreading + return LockScreen ? : undefined; +}; + +export default memo(LockScreenAsync); diff --git a/src/components/main/LockScreen.module.scss b/src/components/main/LockScreen.module.scss new file mode 100644 index 000000000..4ab21963a --- /dev/null +++ b/src/components/main/LockScreen.module.scss @@ -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; +} diff --git a/src/components/main/LockScreen.tsx b/src/components/main/LockScreen.tsx new file mode 100644 index 000000000..5c81e310b --- /dev/null +++ b/src/components/main/LockScreen.tsx @@ -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 = ({ + isLocked, + passcodeSettings, +}) => { + const { + unlockScreen, + signOut, + logInvalidUnlockAttempt, + resetInvalidUnlockAttempts, + } = getActions(); + + const { + invalidAttemptsCount, + isLoading, + } = passcodeSettings; + + const lang = useLang(); + const [validationError, setValidationError] = useState(''); + const [shouldShowPasscode, setShouldShowPasscode] = useState(false); + const [isSignOutDialogOpen, openSignOutConfirmation, closeSignOutConfirmation] = useFlag(false); + const { transitionClassNames, shouldRender } = useShowTransition(isLocked); + const [animationData, setAnimationData] = useState(); + 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 ( +

+

+ Log out{' '} + if you don't remember your passcode. +

+

+ +

+
+ ); + } + + return ( +
+
+
+ {shouldRenderStatic && ( +
+ )} + {shouldRenderAnimated && ( + + )} +
+ + + + {renderLogoutPrompt()} +
+ + +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + return { + passcodeSettings: global.passcode, + }; + }, +)(LockScreen)); diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index c542bb029..27ba295b3 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -33,6 +33,7 @@ max-width: 26.5rem; height: 100%; position: relative; + background-color: var(--color-background); & > div { height: 100%; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 114ffce9c..dc7d0b7e4 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -356,7 +356,6 @@ const Main: FC = ({ // 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) diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index d0bf8d788..eaa31b95d 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -153,7 +153,7 @@ const HeaderActions: FC = ({ }, [canSearch, handleSearchClick]); useHotkeys({ - 'meta+F': handleHotkeySearchClick, + 'Meta+F': handleHotkeySearchClick, }); const lang = useLang(); diff --git a/src/styles/_bg.scss b/src/components/middle/MiddleColumn.module.scss similarity index 53% rename from src/styles/_bg.scss rename to src/components/middle/MiddleColumn.module.scss index 7112ef7d4..ba9f1c19a 100644 --- a/src/styles/_bg.scss +++ b/src/components/middle/MiddleColumn.module.scss @@ -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; } } diff --git a/src/components/middle/MiddleColumn.scss b/src/components/middle/MiddleColumn.scss index 1f7a2bec1..a596abbc9 100644 --- a/src/components/middle/MiddleColumn.scss +++ b/src/components/middle/MiddleColumn.scss @@ -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; diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 706431fe8..0f20ddce6 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -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 = ({ ); 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 = ({ onClick={(IS_TABLET_COLUMN_LAYOUT && isLeftColumnShown) ? handleTabletFocus : undefined} >
diff --git a/src/components/middle/hooks/useCopySelectedMessages.ts b/src/components/middle/hooks/useCopySelectedMessages.ts index 983bfb083..d9406efc6 100644 --- a/src/components/middle/hooks/useCopySelectedMessages.ts +++ b/src/components/middle/hooks/useCopySelectedMessages.ts @@ -10,7 +10,7 @@ const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneTo copySelectedMessages(); } - useHotkeys({ 'meta+C': handleCopy }); + useHotkeys({ 'Meta+C': handleCopy }); }; export default useCopySelectedMessages; diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index f716a58b4..a488d327b 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -319,7 +319,10 @@ const RightColumn: FC = ({ 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} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 02d3d7c6d..7140643fb 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -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 = ({ title={ariaLabel} tabIndex={tabIndex} dir={isRtl ? 'rtl' : undefined} - style={backgroundImage ? `background-image: url(${backgroundImage})` : style} + style={buildStyle(backgroundImage && `background-image: url(${backgroundImage})`)} > {isLoading ? (
diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx index aa6718d6d..37ec3ad27 100644 --- a/src/components/ui/InfiniteScroll.tsx +++ b/src/components/ui/InfiniteScroll.tsx @@ -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; @@ -226,7 +227,7 @@ const InfiniteScroll: FC = ({ {withAbsolutePositioning && items?.length ? (
{children}
diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx index 0556512b6..35df0989a 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -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 = ({
{children} diff --git a/src/components/ui/MenuItem.scss b/src/components/ui/MenuItem.scss index 82220d3a0..49b330137 100644 --- a/src/components/ui/MenuItem.scss +++ b/src/components/ui/MenuItem.scss @@ -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; diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index b161a832b..24cf1a86c 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -2,6 +2,10 @@ position: relative; z-index: var(--z-modal); + &.confirm { + z-index: var(--z-lock-screen); + } + &.delete, &.error, &.confirm, diff --git a/src/config.ts b/src/config.ts index 08788f0aa..3cff920fb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; diff --git a/src/global/actions/all.ts b/src/global/actions/all.ts index fff32607d..d5fe1e2ad 100644 --- a/src/global/actions/all.ts +++ b/src/global/actions/all.ts @@ -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'; diff --git a/src/global/actions/api/initial.ts b/src/global/actions/api/initial.ts index a8b9b14a2..a0fadfab6 100644 --- a/src/global/actions/api/initial.ts +++ b/src/global/actions/api/initial.ts @@ -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); +}); diff --git a/src/global/actions/ui/passcode.ts b/src/global/actions/ui/passcode.ts new file mode 100644 index 000000000..8af9946ec --- /dev/null +++ b/src/global/actions/ui/passcode.ts @@ -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 }); +}); diff --git a/src/global/actions/ui/settings.ts b/src/global/actions/ui/settings.ts index d7d61f180..c8ece1860 100644 --- a/src/global/actions/ui/settings.ts +++ b/src/global/actions/ui/settings.ts @@ -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, + }, + }; +}); diff --git a/src/global/cache.ts b/src/global/cache.ts index 87e0f9e04..fe40d7deb 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -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: {}, }; } diff --git a/src/global/init.ts b/src/global/init.ts index 27c6cd4ed..e9d2bc473 100644 --- a/src/global/init.ts +++ b/src/global/init.ts @@ -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; }); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index e7004b523..91e2223a2 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -192,6 +192,7 @@ export const INITIAL_STATE: GlobalState = { }, twoFaSettings: {}, + passcode: {}, activeReactions: {}, shouldShowContextMenuHint: true, diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index e944c6ae7..52e487131 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -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'; diff --git a/src/global/reducers/passcode.ts b/src/global/reducers/passcode.ts new file mode 100644 index 000000000..a9f00c5de --- /dev/null +++ b/src/global/reducers/passcode.ts @@ -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, + }, + }, + }; +} diff --git a/src/global/types.ts b/src/global/types.ts index 6f899d6fa..2b88f1c2d 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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>; privacy: Partial>; notifyExceptions?: Record; + 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 = ( diff --git a/src/hooks/useNativeCopySelectedMessages.ts b/src/hooks/useNativeCopySelectedMessages.ts index f90d27425..9127e2fc8 100644 --- a/src/hooks/useNativeCopySelectedMessages.ts +++ b/src/hooks/useNativeCopySelectedMessages.ts @@ -11,7 +11,7 @@ const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { mes } } - useHotkeys({ 'meta+C': handleCopy }); + useHotkeys({ 'Meta+C': handleCopy }); }; export default useNativeCopySelectedMessages; diff --git a/src/index.tsx b/src/index.tsx index f038bd4a2..f8f18c091 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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()); + }); +} diff --git a/src/styles/Telegram T.json b/src/styles/Telegram T.json index 4b96e83e3..5595d7df1 100644 --- a/src/styles/Telegram T.json +++ b/src/styles/Telegram T.json @@ -2,7 +2,7 @@ "metadata": { "name": "Telegram T", "lastOpened": 0, - "created": 1651573979756 + "created": 1652356199388 }, "iconSets": [ { @@ -157,13 +157,21 @@ }, { "selection": [ + { + "order": 715, + "id": 62, + "name": "key", + "prevSize": 32, + "code": 59802, + "tempChar": "" + }, { "order": 714, "id": 61, "name": "heart-outline", "prevSize": 32, "code": 59806, - "tempChar": "" + "tempChar": "" }, { "order": 713, @@ -171,7 +179,7 @@ "name": "heart", "prevSize": 32, "code": 59807, - "tempChar": "" + "tempChar": "" }, { "order": 712, @@ -179,7 +187,7 @@ "name": "word-wrap", "prevSize": 32, "code": 59805, - "tempChar": "" + "tempChar": "" }, { "order": 708, @@ -187,7 +195,7 @@ "name": "webapp", "prevSize": 32, "code": 59795, - "tempChar": "" + "tempChar": "" }, { "order": 707, @@ -195,7 +203,7 @@ "name": "reload", "prevSize": 32, "code": 59796, - "tempChar": "" + "tempChar": "" }, { "order": 706, @@ -203,7 +211,7 @@ "name": "install", "prevSize": 32, "code": 59801, - "tempChar": "" + "tempChar": "" }, { "order": 705, @@ -211,7 +219,7 @@ "name": "favorite-filled", "prevSize": 32, "code": 59800, - "tempChar": "" + "tempChar": "" }, { "order": 702, @@ -219,7 +227,7 @@ "name": "share-screen", "prevSize": 32, "code": 59770, - "tempChar": "" + "tempChar": "" }, { "order": 701, @@ -227,7 +235,7 @@ "name": "video-outlined", "prevSize": 32, "code": 59799, - "tempChar": "" + "tempChar": "" }, { "order": 700, @@ -235,7 +243,7 @@ "name": "stats", "prevSize": 32, "code": 59798, - "tempChar": "" + "tempChar": "" }, { "order": 699, @@ -243,7 +251,7 @@ "name": "copy-media", "prevSize": 32, "code": 59797, - "tempChar": "" + "tempChar": "" }, { "order": 704, @@ -251,7 +259,7 @@ "name": "sidebar", "prevSize": 32, "code": 59794, - "tempChar": "" + "tempChar": "" }, { "order": 690, @@ -259,7 +267,7 @@ "name": "video-stop", "prevSize": 32, "code": 59787, - "tempChar": "" + "tempChar": "" }, { "order": 678, @@ -267,7 +275,7 @@ "name": "speaker", "prevSize": 32, "code": 59777, - "tempChar": "" + "tempChar": "" }, { "order": 679, @@ -275,7 +283,7 @@ "name": "speaker-outline", "prevSize": 32, "code": 59778, - "tempChar": "" + "tempChar": "" }, { "order": 680, @@ -283,7 +291,7 @@ "name": "phone-discard-outline", "prevSize": 32, "code": 59779, - "tempChar": "" + "tempChar": "" }, { "order": 681, @@ -291,7 +299,7 @@ "name": "allow-speak", "prevSize": 32, "code": 59780, - "tempChar": "" + "tempChar": "" }, { "order": 682, @@ -299,7 +307,7 @@ "name": "stop-raising-hand", "prevSize": 32, "code": 59781, - "tempChar": "" + "tempChar": "" }, { "order": 683, @@ -307,7 +315,7 @@ "name": "share-screen-outlined", "prevSize": 32, "code": 59782, - "tempChar": "" + "tempChar": "" }, { "order": 684, @@ -315,7 +323,7 @@ "name": "voice-chat", "prevSize": 32, "code": 59783, - "tempChar": "" + "tempChar": "" }, { "order": 689, @@ -323,7 +331,7 @@ "name": "video", "prevSize": 32, "code": 59784, - "tempChar": "" + "tempChar": "" }, { "order": 686, @@ -331,7 +339,7 @@ "name": "noise-suppression", "prevSize": 32, "code": 59785, - "tempChar": "" + "tempChar": "" }, { "order": 703, @@ -339,7 +347,7 @@ "name": "phone-discard", "prevSize": 32, "code": 59786, - "tempChar": "" + "tempChar": "" }, { "order": 667, @@ -347,7 +355,7 @@ "name": "bot-commands-filled", "prevSize": 32, "code": 59775, - "tempChar": "" + "tempChar": "" }, { "order": 664, @@ -355,7 +363,7 @@ "name": "reply-filled", "prevSize": 32, "code": 59776, - "tempChar": "" + "tempChar": "" }, { "order": 656, @@ -363,7 +371,7 @@ "name": "bug", "prevSize": 32, "code": 59774, - "tempChar": "" + "tempChar": "" }, { "order": 619, @@ -371,7 +379,7 @@ "name": "data", "prevSize": 32, "code": 59773, - "tempChar": "" + "tempChar": "" }, { "order": 622, @@ -379,7 +387,7 @@ "name": "darkmode", "prevSize": 32, "code": 59769, - "tempChar": "" + "tempChar": "" }, { "order": 711, @@ -387,7 +395,7 @@ "name": "animations", "prevSize": 32, "code": 59804, - "tempChar": "" + "tempChar": "" }, { "order": 626, @@ -395,7 +403,7 @@ "name": "enter", "prevSize": 32, "code": 59771, - "tempChar": "" + "tempChar": "" }, { "order": 627, @@ -403,7 +411,7 @@ "name": "fontsize", "prevSize": 32, "code": 59772, - "tempChar": "" + "tempChar": "" }, { "order": 630, @@ -411,7 +419,7 @@ "name": "permissions", "prevSize": 32, "code": 59766, - "tempChar": "" + "tempChar": "" }, { "order": 631, @@ -419,7 +427,7 @@ "name": "card", "prevSize": 32, "code": 59767, - "tempChar": "" + "tempChar": "" }, { "order": 634, @@ -427,7 +435,7 @@ "name": "truck", "prevSize": 32, "code": 59768, - "tempChar": "" + "tempChar": "" }, { "order": 663, @@ -435,7 +443,7 @@ "name": "share-filled", "prevSize": 32, "code": 59738, - "tempChar": "" + "tempChar": "" }, { "order": 638, @@ -443,7 +451,7 @@ "name": "bold", "prevSize": 32, "code": 59745, - "tempChar": "" + "tempChar": "" }, { "order": 639, @@ -451,7 +459,7 @@ "name": "bot-command", "prevSize": 32, "code": 59746, - "tempChar": "" + "tempChar": "" }, { "order": 642, @@ -459,7 +467,7 @@ "name": "calendar-filter", "prevSize": 32, "code": 59747, - "tempChar": "" + "tempChar": "" }, { "order": 643, @@ -467,7 +475,7 @@ "name": "comments", "prevSize": 32, "code": 59748, - "tempChar": "" + "tempChar": "" }, { "order": 645, @@ -475,7 +483,7 @@ "name": "comments-sticker", "prevSize": 32, "code": 59749, - "tempChar": "" + "tempChar": "" }, { "order": 646, @@ -483,7 +491,7 @@ "name": "arrow-down", "prevSize": 32, "code": 59750, - "tempChar": "" + "tempChar": "" }, { "order": 668, @@ -491,7 +499,7 @@ "name": "email", "prevSize": 32, "code": 59751, - "tempChar": "" + "tempChar": "" }, { "order": 648, @@ -499,7 +507,7 @@ "name": "italic", "prevSize": 32, "code": 59752, - "tempChar": "" + "tempChar": "" }, { "order": 620, @@ -507,7 +515,7 @@ "name": "link", "prevSize": 32, "code": 59753, - "tempChar": "" + "tempChar": "" }, { "order": 621, @@ -515,7 +523,7 @@ "name": "mention", "prevSize": 32, "code": 59754, - "tempChar": "" + "tempChar": "" }, { "order": 624, @@ -523,7 +531,7 @@ "name": "monospace", "prevSize": 32, "code": 59755, - "tempChar": "" + "tempChar": "" }, { "order": 625, @@ -531,7 +539,7 @@ "name": "next", "prevSize": 32, "code": 59756, - "tempChar": "" + "tempChar": "" }, { "order": 628, @@ -539,7 +547,7 @@ "name": "password-off", "prevSize": 32, "code": 59757, - "tempChar": "" + "tempChar": "" }, { "order": 629, @@ -547,7 +555,7 @@ "name": "pin-list", "prevSize": 32, "code": 59758, - "tempChar": "" + "tempChar": "" }, { "order": 632, @@ -555,7 +563,7 @@ "name": "previous", "prevSize": 32, "code": 59759, - "tempChar": "" + "tempChar": "" }, { "order": 633, @@ -563,7 +571,7 @@ "name": "replace", "prevSize": 32, "code": 59760, - "tempChar": "" + "tempChar": "" }, { "order": 636, @@ -571,7 +579,7 @@ "name": "schedule", "prevSize": 32, "code": 59761, - "tempChar": "" + "tempChar": "" }, { "order": 691, @@ -579,7 +587,7 @@ "name": "strikethrough", "prevSize": 32, "code": 59762, - "tempChar": "" + "tempChar": "" }, { "order": 692, @@ -587,7 +595,7 @@ "name": "underlined", "prevSize": 32, "code": 59763, - "tempChar": "" + "tempChar": "" }, { "order": 641, @@ -595,7 +603,7 @@ "name": "zoom-in", "prevSize": 32, "code": 59764, - "tempChar": "" + "tempChar": "" }, { "order": 649, @@ -603,20 +611,38 @@ "name": "zoom-out", "prevSize": 32, "code": 59765, - "tempChar": "" + "tempChar": "" } ], "id": 2, "metadata": { "name": "Untitled Set", "importSize": { - "width": 768, + "width": 737, "height": 768 } }, "height": 1024, "prevSize": 32, "icons": [ + { + "id": 62, + "paths": [ + "M784 324c0 47.128-38.205 85.333-85.333 85.333s-85.333-38.205-85.333-85.333c0-47.128 38.205-85.333 85.333-85.333s85.333 38.205 85.333 85.333z", + "M659.333 63.733c-166.133 0-300.8 134.667-300.8 300.8 0 33.867 5.6 66.267 15.867 96.667l-301.867 302c-8 8-12.533 18.8-12.533 30.133v128c0 11.333 4.533 22.133 12.533 30.133s18.8 12.533 30.133 12.533h128c11.333 0 22.133-4.533 30.133-12.533l30.267-30.267h81.867c23.467 0 42.4-18.933 42.4-42.4v-70.533l73.2-1.333c23.467 0 42.4-18.933 42.4-42.4v-83.2l31.867-31.867c30.267 10.267 62.8 15.867 96.533 15.867 166.133 0 300.8-134.667 300.8-300.8s-134.667-300.8-300.8-300.8zM544.667 547.067l-88.133 88.133c0.267 0.133 0.4 0.267 0.667 0.4 3.067-3.2 6.8-6 10.8-8.133-13.067 7.2-22 21.2-22 37.2v57.6l-67.467 1.333c-1.867-0.267-3.733-0.4-5.733-0.4-23.2 0-42.133 18.667-42.4 41.733 0 0.267 0 0.4 0 0.667v70.933h-58.533c-16.133 0-30.133 8.933-37.333 22.267 0.667-1.333 1.333-2.533 2.133-3.733l-23.733 23.6h-67.6v-67.6l331.6-331.733c-21.467-34-33.067-73.6-33.067-114.8 0-57.6 22.4-111.6 63.067-152.4 40.667-40.667 94.8-63.067 152.4-63.067s111.6 22.4 152.4 63.067c40.667 40.667 63.067 94.8 63.067 152.4s-22.4 111.6-63.067 152.4c-40.667 40.667-94.8 63.067-152.4 63.067-41.2 0-80.667-11.467-114.667-32.933z" + ], + "attrs": [ + {}, + {} + ], + "width": 983, + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "key" + ] + }, { "id": 61, "paths": [ @@ -3060,7 +3086,7 @@ "name": "select", "prevSize": 32, "code": 59744, - "tempChar": "" + "tempChar": "" }, { "order": 480, @@ -3068,7 +3094,7 @@ "name": "folder", "prevSize": 32, "code": 59667, - "tempChar": "" + "tempChar": "" }, { "order": 481, @@ -3076,7 +3102,7 @@ "name": "bots", "prevSize": 32, "code": 59669, - "tempChar": "" + "tempChar": "" }, { "order": 482, @@ -3084,7 +3110,7 @@ "name": "calendar", "prevSize": 32, "code": 59670, - "tempChar": "" + "tempChar": "" }, { "order": 483, @@ -3092,7 +3118,7 @@ "name": "cloud-download", "prevSize": 32, "code": 59671, - "tempChar": "" + "tempChar": "" }, { "order": 484, @@ -3100,7 +3126,7 @@ "name": "colorize", "prevSize": 32, "code": 59672, - "tempChar": "" + "tempChar": "" }, { "order": 651, @@ -3108,7 +3134,7 @@ "name": "forward", "prevSize": 32, "code": 59687, - "tempChar": "" + "tempChar": "" }, { "order": 650, @@ -3116,7 +3142,7 @@ "name": "reply", "prevSize": 32, "code": 59719, - "tempChar": "" + "tempChar": "" }, { "order": 487, @@ -3124,7 +3150,7 @@ "name": "help", "prevSize": 32, "code": 59690, - "tempChar": "" + "tempChar": "" }, { "order": 488, @@ -3132,7 +3158,7 @@ "name": "info", "prevSize": 32, "code": 59691, - "tempChar": "" + "tempChar": "" }, { "order": 489, @@ -3140,7 +3166,7 @@ "name": "info-filled", "prevSize": 32, "code": 59675, - "tempChar": "" + "tempChar": "" }, { "order": 490, @@ -3148,7 +3174,7 @@ "name": "delete-filled", "prevSize": 32, "code": 59676, - "tempChar": "" + "tempChar": "" }, { "order": 491, @@ -3156,7 +3182,7 @@ "name": "delete", "prevSize": 32, "code": 59677, - "tempChar": "" + "tempChar": "" }, { "order": 492, @@ -3164,7 +3190,7 @@ "name": "edit", "prevSize": 32, "code": 59683, - "tempChar": "" + "tempChar": "" }, { "order": 493, @@ -3172,7 +3198,7 @@ "name": "new-chat-filled", "prevSize": 32, "code": 59705, - "tempChar": "" + "tempChar": "" }, { "order": 494, @@ -3180,7 +3206,7 @@ "name": "send", "prevSize": 32, "code": 59722, - "tempChar": "" + "tempChar": "" }, { "order": 495, @@ -3188,7 +3214,7 @@ "name": "send-outline", "prevSize": 32, "code": 59723, - "tempChar": "" + "tempChar": "" }, { "order": 496, @@ -3196,7 +3222,7 @@ "name": "add-user-filled", "prevSize": 32, "code": 59652, - "tempChar": "" + "tempChar": "" }, { "order": 497, @@ -3204,7 +3230,7 @@ "name": "add-user", "prevSize": 32, "code": 59653, - "tempChar": "" + "tempChar": "" }, { "order": 498, @@ -3212,7 +3238,7 @@ "name": "delete-user", "prevSize": 32, "code": 59678, - "tempChar": "" + "tempChar": "" }, { "order": 499, @@ -3220,7 +3246,7 @@ "name": "microphone", "prevSize": 32, "code": 59701, - "tempChar": "" + "tempChar": "" }, { "order": 500, @@ -3228,7 +3254,7 @@ "name": "microphone-alt", "prevSize": 32, "code": 59707, - "tempChar": "" + "tempChar": "" }, { "order": 501, @@ -3236,7 +3262,7 @@ "name": "poll", "prevSize": 32, "code": 59704, - "tempChar": "" + "tempChar": "" }, { "order": 502, @@ -3244,7 +3270,7 @@ "name": "revote", "prevSize": 32, "code": 59706, - "tempChar": "" + "tempChar": "" }, { "order": 503, @@ -3252,7 +3278,7 @@ "name": "photo", "prevSize": 32, "code": 59712, - "tempChar": "" + "tempChar": "" }, { "order": 504, @@ -3260,7 +3286,7 @@ "name": "document", "prevSize": 32, "code": 59679, - "tempChar": "" + "tempChar": "" }, { "order": 505, @@ -3268,7 +3294,7 @@ "name": "camera", "prevSize": 32, "code": 59662, - "tempChar": "" + "tempChar": "" }, { "order": 506, @@ -3276,7 +3302,7 @@ "name": "camera-add", "prevSize": 32, "code": 59663, - "tempChar": "" + "tempChar": "" }, { "order": 507, @@ -3284,7 +3310,7 @@ "name": "logout", "prevSize": 32, "code": 59698, - "tempChar": "" + "tempChar": "" }, { "order": 508, @@ -3292,7 +3318,7 @@ "name": "saved-messages", "prevSize": 32, "code": 59720, - "tempChar": "" + "tempChar": "" }, { "order": 509, @@ -3300,7 +3326,7 @@ "name": "settings", "prevSize": 32, "code": 59726, - "tempChar": "" + "tempChar": "" }, { "order": 652, @@ -3308,7 +3334,7 @@ "name": "phone", "prevSize": 32, "code": 59711, - "tempChar": "" + "tempChar": "" }, { "order": 653, @@ -3316,7 +3342,7 @@ "name": "attach", "prevSize": 32, "code": 59657, - "tempChar": "" + "tempChar": "" }, { "order": 512, @@ -3324,7 +3350,7 @@ "name": "copy", "prevSize": 32, "code": 59674, - "tempChar": "" + "tempChar": "" }, { "order": 513, @@ -3332,7 +3358,7 @@ "name": "channel", "prevSize": 32, "code": 59665, - "tempChar": "" + "tempChar": "" }, { "order": 514, @@ -3340,7 +3366,7 @@ "name": "group", "prevSize": 32, "code": 59689, - "tempChar": "" + "tempChar": "" }, { "order": 515, @@ -3348,7 +3374,7 @@ "name": "user", "prevSize": 32, "code": 59737, - "tempChar": "" + "tempChar": "" }, { "order": 516, @@ -3356,7 +3382,7 @@ "name": "non-contacts", "prevSize": 32, "code": 59688, - "tempChar": "" + "tempChar": "" }, { "order": 517, @@ -3364,7 +3390,7 @@ "name": "active-sessions", "prevSize": 32, "code": 59650, - "tempChar": "" + "tempChar": "" }, { "order": 518, @@ -3372,7 +3398,7 @@ "name": "admin", "prevSize": 32, "code": 59654, - "tempChar": "" + "tempChar": "" }, { "order": 519, @@ -3380,7 +3406,7 @@ "name": "download", "prevSize": 32, "code": 59681, - "tempChar": "" + "tempChar": "" }, { "order": 520, @@ -3388,7 +3414,7 @@ "name": "location", "prevSize": 32, "code": 59696, - "tempChar": "" + "tempChar": "" }, { "order": 521, @@ -3396,7 +3422,7 @@ "name": "stop", "prevSize": 32, "code": 59730, - "tempChar": "" + "tempChar": "" }, { "order": 523, @@ -3404,7 +3430,7 @@ "name": "archive", "prevSize": 32, "code": 59656, - "tempChar": "" + "tempChar": "" }, { "order": 524, @@ -3412,7 +3438,7 @@ "name": "unarchive", "prevSize": 32, "code": 59731, - "tempChar": "" + "tempChar": "" }, { "order": 525, @@ -3420,7 +3446,7 @@ "name": "readchats", "prevSize": 32, "code": 59699, - "tempChar": "" + "tempChar": "" }, { "order": 526, @@ -3428,7 +3454,7 @@ "name": "unread", "prevSize": 32, "code": 59735, - "tempChar": "" + "tempChar": "" }, { "order": 654, @@ -3436,7 +3462,7 @@ "name": "message", "prevSize": 32, "code": 59700, - "tempChar": "" + "tempChar": "" }, { "order": 659, @@ -3444,7 +3470,7 @@ "name": "lock", "prevSize": 32, "code": 59697, - "tempChar": "" + "tempChar": "" }, { "order": 529, @@ -3452,7 +3478,7 @@ "name": "unlock", "prevSize": 32, "code": 59732, - "tempChar": "" + "tempChar": "" }, { "order": 530, @@ -3460,7 +3486,7 @@ "name": "mute", "prevSize": 32, "code": 59703, - "tempChar": "" + "tempChar": "" }, { "order": 531, @@ -3468,7 +3494,7 @@ "name": "unmute", "prevSize": 32, "code": 59733, - "tempChar": "" + "tempChar": "" }, { "order": 532, @@ -3476,7 +3502,7 @@ "name": "pin", "prevSize": 32, "code": 59713, - "tempChar": "" + "tempChar": "" }, { "order": 533, @@ -3484,7 +3510,7 @@ "name": "unpin", "prevSize": 32, "code": 59734, - "tempChar": "" + "tempChar": "" }, { "order": 534, @@ -3492,7 +3518,7 @@ "name": "smallscreen", "prevSize": 32, "code": 59742, - "tempChar": "" + "tempChar": "" }, { "order": 535, @@ -3500,7 +3526,7 @@ "name": "fullscreen", "prevSize": 32, "code": 59743, - "tempChar": "" + "tempChar": "" }, { "order": 536, @@ -3508,7 +3534,7 @@ "name": "large-pause", "prevSize": 32, "code": 59694, - "tempChar": "" + "tempChar": "" }, { "order": 537, @@ -3516,7 +3542,7 @@ "name": "large-play", "prevSize": 32, "code": 59695, - "tempChar": "" + "tempChar": "" }, { "order": 538, @@ -3524,7 +3550,7 @@ "name": "pause", "prevSize": 32, "code": 59709, - "tempChar": "" + "tempChar": "" }, { "order": 539, @@ -3532,7 +3558,7 @@ "name": "play", "prevSize": 32, "code": 59715, - "tempChar": "" + "tempChar": "" }, { "order": 540, @@ -3540,7 +3566,7 @@ "name": "channelviews", "prevSize": 32, "code": 59666, - "tempChar": "" + "tempChar": "" }, { "order": 541, @@ -3548,7 +3574,7 @@ "name": "message-succeeded", "prevSize": 32, "code": 59648, - "tempChar": "" + "tempChar": "" }, { "order": 657, @@ -3556,7 +3582,7 @@ "name": "message-read", "prevSize": 32, "code": 59649, - "tempChar": "" + "tempChar": "" }, { "order": 543, @@ -3564,7 +3590,7 @@ "name": "message-pending", "prevSize": 32, "code": 59724, - "tempChar": "" + "tempChar": "" }, { "order": 544, @@ -3572,7 +3598,7 @@ "name": "message-failed", "prevSize": 32, "code": 59725, - "tempChar": "" + "tempChar": "" }, { "order": 545, @@ -3580,7 +3606,7 @@ "name": "favorite", "prevSize": 32, "code": 59710, - "tempChar": "" + "tempChar": "" }, { "order": 546, @@ -3588,7 +3614,7 @@ "name": "keyboard", "prevSize": 32, "code": 59716, - "tempChar": "" + "tempChar": "" }, { "order": 547, @@ -3596,7 +3622,7 @@ "name": "delete-left", "prevSize": 32, "code": 59717, - "tempChar": "" + "tempChar": "" }, { "order": 548, @@ -3604,7 +3630,7 @@ "name": "recent", "prevSize": 32, "code": 59718, - "tempChar": "" + "tempChar": "" }, { "order": 549, @@ -3612,7 +3638,7 @@ "name": "gifs", "prevSize": 32, "code": 59727, - "tempChar": "" + "tempChar": "" }, { "order": 550, @@ -3620,7 +3646,7 @@ "name": "stickers", "prevSize": 32, "code": 59739, - "tempChar": "" + "tempChar": "" }, { "order": 551, @@ -3628,7 +3654,7 @@ "name": "smile", "prevSize": 32, "code": 59728, - "tempChar": "" + "tempChar": "" }, { "order": 552, @@ -3636,7 +3662,7 @@ "name": "animals", "prevSize": 32, "code": 59655, - "tempChar": "" + "tempChar": "" }, { "order": 553, @@ -3644,7 +3670,7 @@ "name": "eats", "prevSize": 32, "code": 59682, - "tempChar": "" + "tempChar": "" }, { "order": 554, @@ -3652,7 +3678,7 @@ "name": "sport", "prevSize": 32, "code": 59729, - "tempChar": "" + "tempChar": "" }, { "order": 555, @@ -3660,7 +3686,7 @@ "name": "car", "prevSize": 32, "code": 59664, - "tempChar": "" + "tempChar": "" }, { "order": 556, @@ -3668,7 +3694,7 @@ "name": "lamp", "prevSize": 32, "code": 59692, - "tempChar": "" + "tempChar": "" }, { "order": 557, @@ -3676,7 +3702,7 @@ "name": "language", "prevSize": 32, "code": 59693, - "tempChar": "" + "tempChar": "" }, { "order": 558, @@ -3684,7 +3710,7 @@ "name": "flag", "prevSize": 32, "code": 59686, - "tempChar": "" + "tempChar": "" }, { "order": 559, @@ -3692,7 +3718,7 @@ "name": "more", "prevSize": 32, "code": 59702, - "tempChar": "" + "tempChar": "" }, { "order": 560, @@ -3700,7 +3726,7 @@ "name": "search", "prevSize": 32, "code": 59721, - "tempChar": "" + "tempChar": "" }, { "order": 561, @@ -3708,7 +3734,7 @@ "name": "remove", "prevSize": 32, "code": 59740, - "tempChar": "" + "tempChar": "" }, { "order": 562, @@ -3716,7 +3742,7 @@ "name": "add", "prevSize": 32, "code": 59651, - "tempChar": "" + "tempChar": "" }, { "order": 563, @@ -3724,7 +3750,7 @@ "name": "check", "prevSize": 32, "code": 59668, - "tempChar": "" + "tempChar": "" }, { "order": 564, @@ -3732,7 +3758,7 @@ "name": "close", "prevSize": 32, "code": 59673, - "tempChar": "" + "tempChar": "" }, { "order": 610, @@ -3740,7 +3766,7 @@ "name": "arrow-left", "prevSize": 32, "code": 59661, - "tempChar": "" + "tempChar": "" }, { "order": 566, @@ -3748,7 +3774,7 @@ "name": "arrow-right", "prevSize": 32, "code": 59708, - "tempChar": "" + "tempChar": "" }, { "order": 567, @@ -3756,7 +3782,7 @@ "name": "down", "prevSize": 32, "code": 59680, - "tempChar": "" + "tempChar": "" }, { "order": 568, @@ -3764,7 +3790,7 @@ "name": "up", "prevSize": 32, "code": 59736, - "tempChar": "" + "tempChar": "" }, { "order": 569, @@ -3772,7 +3798,7 @@ "name": "eye-closed", "prevSize": 32, "code": 59685, - "tempChar": "" + "tempChar": "" }, { "order": 570, @@ -3780,7 +3806,7 @@ "name": "eye", "prevSize": 32, "code": 59684, - "tempChar": "" + "tempChar": "" }, { "order": 571, @@ -3788,7 +3814,7 @@ "name": "muted", "prevSize": 32, "code": 59741, - "tempChar": "" + "tempChar": "" }, { "order": 572, @@ -3796,7 +3822,7 @@ "name": "avatar-archived-chats", "prevSize": 32, "code": 59658, - "tempChar": "" + "tempChar": "" }, { "order": 573, @@ -3804,7 +3830,7 @@ "name": "avatar-deleted-account", "prevSize": 32, "code": 59659, - "tempChar": "" + "tempChar": "" }, { "order": 574, @@ -3812,7 +3838,7 @@ "name": "avatar-saved-messages", "prevSize": 32, "code": 59660, - "tempChar": "" + "tempChar": "" }, { "order": 575, @@ -3820,7 +3846,7 @@ "name": "pinned-chat", "prevSize": 32, "code": 59714, - "tempChar": "" + "tempChar": "" } ], "prevSize": 32, @@ -3872,4 +3898,4 @@ "showLiga": false }, "uid": -1 -} \ No newline at end of file +} diff --git a/src/styles/_forms.scss b/src/styles/_forms.scss index d681a5cfa..d1e57043d 100644 --- a/src/styles/_forms.scss +++ b/src/styles/_forms.scss @@ -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 { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 6a9cde07a..5ac2e4ca2 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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; diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 651f21bde..c17218d91 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -51,6 +51,9 @@ .icon-volume-3:before { content: "\e991"; } +.icon-key:before { + content: "\e99a"; +} .icon-heart-outline:before { content: "\e99e"; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 1a5e109b1..c8dd171fe 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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, diff --git a/src/styles/print.scss b/src/styles/print.scss index 2d5e7c8da..c9414c3be 100644 --- a/src/styles/print.scss +++ b/src/styles/print.scss @@ -12,7 +12,6 @@ .Modal, .ActiveCallHeader, .unread-count, - #middle-column-bg, #middle-column-portals, .header-tools, .ScrollDownButton, diff --git a/src/types/index.ts b/src/types/index.ts index 297a2bf2d..52854b6bd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -214,6 +214,15 @@ export enum SettingsScreens { TwoFaRecoveryEmailCode, TwoFaCongratulations, QuickReaction, + PasscodeDisabled, + PasscodeNewPasscode, + PasscodeNewPasscodeConfirm, + PasscodeEnabled, + PasscodeChangePasscodeCurrent, + PasscodeChangePasscodeNew, + PasscodeChangePasscodeConfirm, + PasscodeTurnOff, + PasscodeCongratulations, } export type StickerSetOrRecent = Pick = {}; const hashes: Record = {}; @@ -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); +} diff --git a/src/util/switchTheme.ts b/src/util/switchTheme.ts index 721c0f2c9..9c834a403 100644 --- a/src/util/switchTheme.ts +++ b/src/util/switchTheme.ts @@ -32,6 +32,10 @@ const colors = (Object.keys(themeColors) as Array).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'); } diff --git a/webpack.config.js b/webpack.config.js index 8f7946e17..0a6ed2a9e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,6 +51,7 @@ module.exports = (env = {}, argv = {}) => { stats: 'minimal', }, }, + output: { filename: '[name].[contenthash].js', chunkFilename: '[id].[chunkhash].js',