From 017170c2e278631f46a83fc01c038410d0bf2616 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 31 May 2022 20:58:35 +0400 Subject: [PATCH] Introduce Lock Screen and Passcode (#1839) --- package-lock.json | 2 +- package.json | 2 +- src/App.tsx | 109 +++++- src/api/gramjs/localDb.ts | 12 +- src/api/gramjs/methods/client.ts | 13 +- src/assets/fonts/icomoon.woff | Bin 43744 -> 44092 bytes src/assets/fonts/icomoon.woff2 | Bin 20408 -> 20660 bytes src/assets/lock.png | Bin 0 -> 5631 bytes src/assets/mastercard.svg | 3 +- src/assets/tgs/settings/Congratulations.tgs | Bin 0 -> 21615 bytes src/assets/tgs/settings/Lock.tgs | Bin 0 -> 13857 bytes src/assets/visa.svg | 3 +- src/bundles/main.ts | 4 +- src/components/auth/Auth.tsx | 40 ++- .../calls/group/GroupCallParticipantMenu.tsx | 3 +- src/components/common/AnimatedEmoji.tsx | 10 +- src/components/common/PasswordForm.tsx | 21 +- src/components/common/UiLoader.module.scss | 115 ++++++ src/components/common/UiLoader.scss | 91 ----- src/components/common/UiLoader.tsx | 85 ++--- .../common/helpers/animatedAssets.ts | 4 + src/components/left/LeftColumn.scss | 4 +- src/components/left/LeftColumn.tsx | 86 ++++- src/components/left/main/LeftMainHeader.scss | 4 + src/components/left/main/LeftMainHeader.tsx | 57 ++- src/components/left/settings/Settings.scss | 10 +- src/components/left/settings/Settings.tsx | 51 ++- .../left/settings/SettingsHeader.tsx | 17 + ...aPassword.tsx => SettingsPasswordForm.tsx} | 22 +- .../left/settings/SettingsPrivacy.tsx | 25 +- .../settings/passcode/SettingsPasscode.tsx | 225 ++++++++++++ .../SettingsPasscodeCongratulations.tsx | 47 +++ .../passcode/SettingsPasscodeEnabled.tsx | 66 ++++ .../passcode/SettingsPasscodeStart.tsx | 45 +++ .../left/settings/twoFa/SettingsTwoFa.tsx | 2 +- .../twoFa/SettingsTwoFaCongratulations.tsx | 22 +- src/components/main/AppInactive.scss | 2 + src/components/main/LockScreen.async.tsx | 17 + src/components/main/LockScreen.module.scss | 51 +++ src/components/main/LockScreen.tsx | 184 ++++++++++ src/components/main/Main.scss | 1 + src/components/main/Main.tsx | 9 +- src/components/middle/HeaderActions.tsx | 2 +- .../middle/MiddleColumn.module.scss} | 54 +-- src/components/middle/MiddleColumn.scss | 10 - src/components/middle/MiddleColumn.tsx | 13 +- .../middle/hooks/useCopySelectedMessages.ts | 2 +- src/components/right/RightColumn.tsx | 5 +- src/components/ui/Button.tsx | 3 +- src/components/ui/InfiniteScroll.tsx | 3 +- src/components/ui/Menu.tsx | 7 +- src/components/ui/MenuItem.scss | 10 +- src/components/ui/Modal.scss | 4 + src/config.ts | 4 + src/global/actions/all.ts | 1 + src/global/actions/api/initial.ts | 43 +++ src/global/actions/ui/passcode.ts | 69 ++++ src/global/actions/ui/settings.ts | 10 + src/global/cache.ts | 66 ++-- src/global/init.ts | 17 +- src/global/initialState.ts | 1 + src/global/reducers/index.ts | 1 + src/global/reducers/passcode.ts | 46 +++ src/global/types.ts | 25 ++ src/hooks/useNativeCopySelectedMessages.ts | 2 +- src/index.tsx | 10 +- src/styles/Telegram T.json | 336 ++++++++++-------- src/styles/_forms.scss | 20 ++ src/styles/_variables.scss | 1 + src/styles/icons.scss | 3 + src/styles/index.scss | 8 +- src/styles/print.scss | 1 - src/types/index.ts | 9 + src/util/cacheApi.ts | 24 +- src/util/notifications.ts | 10 +- src/util/passcode.ts | 94 +++++ src/util/sessions.ts | 21 +- src/util/switchTheme.ts | 6 +- webpack.config.js | 1 + 79 files changed, 1905 insertions(+), 501 deletions(-) create mode 100644 src/assets/lock.png create mode 100644 src/assets/tgs/settings/Congratulations.tgs create mode 100644 src/assets/tgs/settings/Lock.tgs create mode 100644 src/components/common/UiLoader.module.scss delete mode 100644 src/components/common/UiLoader.scss rename src/components/left/settings/{twoFa/SettingsTwoFaPassword.tsx => SettingsPasswordForm.tsx} (71%) create mode 100644 src/components/left/settings/passcode/SettingsPasscode.tsx create mode 100644 src/components/left/settings/passcode/SettingsPasscodeCongratulations.tsx create mode 100644 src/components/left/settings/passcode/SettingsPasscodeEnabled.tsx create mode 100644 src/components/left/settings/passcode/SettingsPasscodeStart.tsx create mode 100644 src/components/main/LockScreen.async.tsx create mode 100644 src/components/main/LockScreen.module.scss create mode 100644 src/components/main/LockScreen.tsx rename src/{styles/_bg.scss => components/middle/MiddleColumn.module.scss} (53%) create mode 100644 src/global/actions/ui/passcode.ts create mode 100644 src/global/reducers/passcode.ts create mode 100644 src/util/passcode.ts 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 746609e423b953980306e598ba37c170b86cb80f..524f03810375cece469c4a29cc2b343e3547d608 100644 GIT binary patch delta 660 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�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 diff --git a/src/assets/lock.png b/src/assets/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..a9789c1f7f03de10c2c44c297fb8195165fff910 GIT binary patch literal 5631 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe9704d9d39c6076ec79c29c7caaef1fb9bcc219 GIT binary patch literal 21615 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

n2zqk7kYlUPhpTe3Dsflyn5i$cjz zYqqXf!oY+UL1rq3nv5nq^8kI68SqEQo(|{z zOcBSs)iJ?qP^z3IQ<0yepN_;W$@O4}RpGlDPBn5)v+?k;Y0ur7Kh}Y|A##nj9b}*vZ;wDjpD6 zxG;vnt22Lu$2OqC)AKf>fCfX#CmuX*)$;&&Rrh4JE^`=?O7$2VHb1Wi(Ea`>(nNgIx)%S+D#Oa)Oq> zy0V+-5qm52ULzhS0CWbg0%E@B>P0rp(;r z1<({}ZmS1d%yiPN$2*wK(Yv5urwhEt>!)am@xt?Hhab0IUNw7_Ec4{Cijl7N zm%ozNclFWE<&5*aputb$S6B`StSUHp){t@C^4mVNkbYkuZ33`v^PN+` zzX5J;#xURv&C`5Fs^`)FH(#EUnqZSp5y!?hErhJO!qY~Lv)e~viSqsB80B{=UwszS z%1vUczcvW;!J#hmPPQ!j23m@{h=FU6keZRGtLTo(P?tq{+fiSPMAZ}tUSE~_^C#!dirU1jN0s>B_vE8}jEc!s!>8G-g)^4C6Tq=A-~=+}H> zcG7E~Ut7Kzmt!@Z#P_lvZ#I%p^@S^z(xDO672&o2ka%!RnxdW4jO-O(6G3mzC$p$nRElGv_>i1d{RcCQr+vBF+-hazV+EV^ z^wDDZu#=-mR2}zoBBn8Vl3e~%0;?+fHR{AW{!_VU_I1fM&rkksWI&Q*`a@#DVN8Bg z^7{9e*OjFK!c*a!h(*=yQYk2EWPN@;xK%Ym-U z)oI5{*h2y$9@o5>pSQpN-te{v#oK#K&-cS)lqnu_AJtVdq;&NE2}l#S?8@B#O5)^C z7HsVV8;;~3X^u$XM^O-$WAEAILp;7gUBR=l=C_i=gu;CYYOwM?xRG05euE}=O8z$7 zI8y962+#l!!SyC4#Y=6@E^0%wlS*z9sS-inM&KY7IVH(nZWWylWX=X@6@o5vG9X^f z?iZ27Q!dDfv*KJk2u|PwI6=U&swM^T(GT?`VmCoyi`&@C%Yg_44&my`tZ`tl4UN5qsZ3fIu4TZ9r#H`sy3hq zyEI_x5L4xZLZ*ULhRGHLMukww z6Vox33jY(P>#f`roRjbdh44YNcZ?bs=w^tl*Q^ZF>6_|fi>ZqvZ zg5y$t8xuC&bjS=R+7-FSwcNxnV`(&*xm>ati32ISp*TQ4Y$9^pFc)2`z-ihXM`Pxh>K$Eofz6Zh5qXewtPl)A&-EFBCnQ-C`a|Q z61R&^uuQr;$_oNtgo=<+TiTriqs9XESn%az%U4F?sxk@Rax@KEOy(t_6#pUVSIZac87a{F5_Q!1|Tu-I%f#__d za0G;u#8v!biVJ59F@nT5(7oP`&r~$m`3yaeF871qlc=Ly;wJMZ$0hm6(t5ZdNE4at zzH{v5;2H;tq5urh++-~M!^BS9jou=du^#J{V|_kd>|trF&92cRI_# zE9VfdDfBWAQaGhhiB1ni<};lNQ5HZY_-FyqLlRnm_wy|MadpzE8;7a{H#qJ~(iiFF zVljkITIBz1Ky*q)cnO3sUCIP<@Z#?rKZp~;JV5g-(Vj7T*%(vGtx?U`T9LD}7=LkrY&uM{RxA|xrP z{}kslVlGg6GY~Mi>KBork^ytdpr**dC~7b&;&DHt*2)KQ5i{Hu^55N$t+0pkdl-1D zg#L6^4%1rzB@y0dl+-^10(j=-C=zu-v|eFYn)%<6_&tpBXVS z8k;SyL3yBxLmxpL8c9P#VQ3_oj6|W+NESLR2}7rmG;|t^L#L5EbQ%dnqgm(TQ18Nwm4n{HQaKibrc%&pA_kq7Q1o&TV~9PAK?WI(rk8?Z0zOt5=pYC*5P_x=&}k|Hot9;w(^LpLjisQm z5VR-*Atjy$I}ykMqs&oRi14inKu;8a%yj{1_q`X2WN-w~nE-hX%LcOP6FbLdNyAeo&KwpxVGZuaPTk-+uWoj$S$fIY$f6Sm zh{y+sh=LFDNPH><`YpRrrcZEd^LyFB$p;A|R^4ev)! z{qPw@7H&4nuNs(Yp`FZdAlT9Z^Q4hEFsC8Lw$X)(B=3zR7A--z$`AJjDM-?dp8DYd zlFV;5%dZ+BiQ*z^Gb}+T3c)vU9WzH0@Tgo_&AX!sa_prK75?bZq#Hf;!vi##-)xp& zb+)W}uEsOPfk+yTm{3rO4EI10Y?fbj zMiQjy+c}b?)=Mm*4jh6#dm?p(A1ji)Ba#^1W{F1UKgx#pqo;oOj3f&;o8?#aNRm)S zEzD8C5;qaoPVn4EU?#hA>}*AncSaJ|ddrq1{-Z;ZZuHa-50GSjvsr%C8A&u(u9+c8 z?D~sn2!Fyu?JRCTz@<)da-RP+w9$>}%X0!aNGnSZkuHteyiCo7#$B)cr`HCNJ#E+Xz z-?o~*-)Q>QBy_J@zNZhomf>5M&cdDC*a9xMt9kpoF>h<1$Hwb| z0iAU_=CSp=NU7Dd{k@pBb#POk{bLg}yGpS%PxcS5 zz3uk=u&WiRdzY7Pa(Z}q=;9FLnasoMGJB9Fpl)@kgWU^@fmH$y<2S8EG0cT$ER(CdlY~W; z)Y>Urx}sTn=Uzgy%Qf073VWz_q}^8B%8 zeVViJ3TYsrAs*(Ws4ALY+Xnr^veY9+w`-eYgR;S{tBjZn{=6Z_TxweoB~GYqVI;?# zxMr4oQx?uL8V8b3x( z18Yf1n$0LT6=sicN`kpJ)(i^HYv96DO{{)qVl|hlg)Ou^z0g{7*jcl))oF{`(x}x$ zJ8eRemYToyy63sS=GF$DOD$FD?XETG)uUA=RUzyIEH z67AZc^wo(t%fMY;jz1Uc$1FUzcPPD@H0hX|iq7g7Qk{Zy>OyV*gw|l1RK0UtQ8h9= z-CT5S^K2p#q0qtp_0gjm(c zvIQ%&k~l5zhy4w0(>3)Px8t%LZF43mC_Re2xY9>aFVGoa#>amMF~<*IMLmy5nRrR#E6MLh*u_G8TzDQD+%bGMYIW5q`5?mQgOq zGAtzHw<080tf1} zXQg%9v^dmXcclw>$1gRyE_*xF;&Nl#5Qfx|TLI)biX^?M@3X4X)zIF?2eTZ@>z@nf8BV2r4>-ImO>xI4 zSLg=GL)H~MNzx`71SI?O7(W*?ItX;;rodu*Mndz#Sb3k;<+tdQ6e^A#gjZpk+|U>> z!ZF%2$m%%_dlLsV3buSXxcaZF{Bw|ETYHmX(O-ME|Oen*4W z?C5AqOX-JQ<8>HvI*Sh7X+Y~F^R)ErcH9(RZ<6;HF>QhwJQg|mjpcpo_tXJ$F^apS z5dq{845Wg#j7~}V+&HULs_3))Fv$n z)Y2U*5S6)6S|TPXl!vHRQkBZQ8(K(jp@A)cI9W6e7{eJW0@N`(#2rsZoS9zQA87n>1#+c%OAZ)?* zeSz_f8o76EW^K!RB2ATkyqA=Mh)Kyma6>tTS5w=$)tc2UJ@BYTk#=nnbhdlWmXK$P z6_50sRWkDk)Ag4Bo)9vD1rMVkNVmpLZQ8%hXE+U{;0y29A~z2-I_@c|hLmkF+zZk3 zXbDRA=X)!8f;7z0B@055xk+%kMcs4RbP+5!niPLUE@ ze^Op{u%sD98^jvHm&QUHsmJJ^V>F|>%co+AoG()2NYY^Juu_O@?0kj}hH^m;dWm9$ z2Jc^?*^hHi+mca|l4c{H_B=qR#y1n{T`_BeEp@`GI~{VY8Vq>-SO{2DHmmn@V!*tqMSA4&lrQo#bcn(CoqBwv5!o_YdMZ9FozyI+OmV!+=ksuG(| z8H3teL<3+jBBr7YoP5b@8;$o_MO}NR&(lGJ5Cb)+ zxJN+#^tXSbo1gyj+poX>yZSUq51o86mfiSCYm|Q?)6fuPgs9Dio;FMX2&ssO(-)IW17dvQ|!Cg379^P`TZp ziUq1%po$lv(pRDKdqIUVZwyrwsBnD&DsvU8up3l)0@cMcXzlaPAkH4sD^=)KFc-mc z&qMc+I*6W2{wi2;FR)@cg{jcdc=;4wr|#*~h2;MN6%n9iPPhGdcB|-JIiHtT zG2RaE-Q1|0Pi+d>FqaQ#dLG#TLuRmSp|Kda09Jk;-LTyWwFT+y#S`k(?MGpEq%$47 zrV}gZnbdfbLXQ2=}3|s)q9N_F)-EDi}7xp5parSv<*LU)*FXEBEDBwDGyMVj< zGKP-UghFe=MOx$h^8|9=(V9;7^%rT43%AR@yDy{eIEg8oL|^12EbW& s3|s&!-7XgIPa8WTWD5~8mx)k*o){n9L}`sZSYQ79|B_b4nNFz#0M7#%KmY&$ literal 0 HcmV?d00001 diff --git a/src/assets/tgs/settings/Lock.tgs b/src/assets/tgs/settings/Lock.tgs new file mode 100644 index 0000000000000000000000000000000000000000..8538275214026a81fa8b5c21ae38528523394db1 GIT binary patch literal 13857 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 -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',