diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 12cef395..c72dc894 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -55,12 +55,13 @@ class InputField { public container: HTMLElement; public input: HTMLElement; public inputFake: HTMLElement; + public label: HTMLLabelElement; //public onLengthChange: (length: number, isOverflow: boolean) => void; - private wasInputFakeClientHeight: number; - private showScrollDebounced: () => void; + protected wasInputFakeClientHeight: number; + protected showScrollDebounced: () => void; - constructor(private options: { + constructor(protected options: { placeholder?: string, label?: string, name?: string, @@ -131,6 +132,10 @@ class InputField { input.addEventListener('input', () => checkAndSetRTL(input)); } + if(label) { + this.label = this.container.lastElementChild as HTMLLabelElement; + } + let processInput: () => void; if(maxLength) { const labelEl = this.container.lastElementChild as HTMLLabelElement; diff --git a/src/components/monkeys/index.ts b/src/components/monkeys/index.ts new file mode 100644 index 00000000..aa6142e8 --- /dev/null +++ b/src/components/monkeys/index.ts @@ -0,0 +1,5 @@ +import InputField from "../inputField"; + +export default interface Monkey { + attachToInputField: (inputField: InputField) => void; +} diff --git a/src/components/monkeys/password.ts b/src/components/monkeys/password.ts new file mode 100644 index 00000000..1185c22d --- /dev/null +++ b/src/components/monkeys/password.ts @@ -0,0 +1,59 @@ +import lottieLoader, { RLottiePlayer } from "../../lib/lottieLoader"; +import PasswordInputField from "../passwordInputField"; + +export default class PasswordMonkey { + public container: HTMLElement; + public animation: RLottiePlayer; + public needFrame = 0; + protected loadPromise: Promise; + + constructor(protected passwordInputField: PasswordInputField, protected size: number) { + this.container = document.createElement('div'); + this.container.classList.add('media-sticker-wrapper'); + } + + public load() { + if(this.loadPromise) return this.loadPromise; + return this.loadPromise = lottieLoader.loadAnimationFromURL({ + container: this.container, + loop: false, + autoplay: false, + width: this.size, + height: this.size, + noCache: true + //}, 'assets/img/TwoFactorSetupMonkeyClose.tgs').then(_animation => { + }, 'assets/img/TwoFactorSetupMonkeyPeek.tgs').then(_animation => { + //return; + this.animation = _animation; + this.animation.addListener('enterFrame', currentFrame => { + //console.log('enterFrame', currentFrame, this.needFrame); + + if((this.animation.direction === 1 && currentFrame >= this.needFrame) || + (this.animation.direction === -1 && currentFrame <= this.needFrame)) { + this.animation.setSpeed(1); + this.animation.pause(); + } + }); + + this.passwordInputField.onVisibilityClickAdditional = () => { + if(this.passwordInputField.passwordVisible) { + this.animation.setDirection(1); + this.animation.curFrame = 0; + this.needFrame = 16; + this.animation.play(); + } else { + this.animation.setDirection(-1); + this.animation.curFrame = 16; + this.needFrame = 0; + this.animation.play(); + } + }; + }); + } + + public remove() { + if(this.animation) { + this.animation.remove(); + } + } +} diff --git a/src/components/passwordInputField.ts b/src/components/passwordInputField.ts new file mode 100644 index 00000000..376e5ece --- /dev/null +++ b/src/components/passwordInputField.ts @@ -0,0 +1,41 @@ +import { cancelEvent } from "../helpers/dom"; +import InputField from "./inputField"; + +export default class PasswordInputField extends InputField { + public passwordVisible = false; + public toggleVisible: HTMLElement; + public onVisibilityClickAdditional: () => void; + + constructor(options: { + label?: string, + name?: string + } = {}) { + super({ + plainText: true, + ...options + }); + + const input = this.input as HTMLInputElement; + input.type = 'password'; + input.setAttribute('required', ''); + input.autocomplete = 'off'; + + const toggleVisible = this.toggleVisible = document.createElement('span'); + toggleVisible.classList.add('toggle-visible', 'tgico'); + + this.container.classList.add('input-field-password'); + this.container.append(toggleVisible); + + toggleVisible.addEventListener('click', this.onVisibilityClick); + toggleVisible.addEventListener('touchend', this.onVisibilityClick); + } + + public onVisibilityClick = (e: Event) => { + cancelEvent(e); + this.passwordVisible = !this.passwordVisible; + + this.toggleVisible.classList.toggle('eye-hidden', this.passwordVisible); + (this.input as HTMLInputElement).type = this.passwordVisible ? 'text' : 'password'; + this.onVisibilityClickAdditional && this.onVisibilityClickAdditional(); + }; +} diff --git a/src/components/popups/confirmAction.ts b/src/components/popups/confirmAction.ts new file mode 100644 index 00000000..2e142dec --- /dev/null +++ b/src/components/popups/confirmAction.ts @@ -0,0 +1,18 @@ +import PopupElement, { addCancelButton, PopupButton, PopupOptions } from "."; + +export default class PopupConfirmAction extends PopupElement { + constructor(className: string, buttons: PopupButton[], options: PopupOptions & Partial<{title: string, text: string}> = {}) { + super('popup-peer popup-confirm-action ' + className, addCancelButton(buttons), { + overlayClosable: true, + ...options + }); + + this.title.innerHTML = options.title || 'Warning'; + + const p = document.createElement('p'); + p.classList.add('popup-description'); + p.innerHTML = options.text; + + this.container.insertBefore(p, this.header.nextElementSibling); + } +} diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 512079de..d34d3952 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -1,11 +1,23 @@ import rootScope from "../../lib/rootScope"; -import { blurActiveElement, cancelEvent, findUpClassName } from "../../helpers/dom"; +import { blurActiveElement, findUpClassName } from "../../helpers/dom"; import { ripple } from "../ripple"; import animationIntersector from "../animationIntersector"; import appNavigationController, { NavigationItem } from "../appNavigationController"; -import { isMobileSafari, isSafari } from "../../helpers/userAgent"; -export type PopupOptions = Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}>; +export type PopupButton = { + text: string, + callback?: () => void, + isDanger?: true, + isCancel?: true +}; + +export type PopupOptions = Partial<{ + closable: true, + overlayClosable: true, + withConfirm: string, + body: true +}>; + export default class PopupElement { protected element = document.createElement('div'); protected container = document.createElement('div'); @@ -140,9 +152,14 @@ export default class PopupElement { }; } -export type PopupButton = { - text: string, - callback?: () => void, - isDanger?: true, - isCancel?: true +export const addCancelButton = (buttons: PopupButton[]) => { + const button = buttons.find(b => b.isCancel); + if(!button) { + buttons.push({ + text: 'CANCEL', + isCancel: true + }); + } + + return buttons; }; diff --git a/src/components/row.ts b/src/components/row.ts index 2f9404b9..89e52c30 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -20,7 +20,7 @@ export default class Row { radioField: Row['radioField'], checkboxField: Row['checkboxField'], title: string, - clickable: boolean, + clickable: boolean | ((e: Event) => void), navigationTab: SliderSuperTab }> = {}) { this.container = document.createElement('div'); @@ -63,14 +63,17 @@ export default class Row { } if(options.navigationTab) { - this.container.addEventListener('click', () => { - if(this.freezed) return; - options.navigationTab.open(); - }); - options.clickable = true; + options.clickable = () => options.navigationTab.open(); } if(options.clickable) { + if(typeof(options.clickable) === 'function') { + this.container.addEventListener('click', (e) => { + if(this.freezed) return; + (options.clickable as any)(e); + }); + } + this.container.classList.add('row-clickable', 'hover-effect'); ripple(this.container); } diff --git a/src/components/sidebarLeft/tabs/2fa/enterPassword.ts b/src/components/sidebarLeft/tabs/2fa/enterPassword.ts new file mode 100644 index 00000000..2968da77 --- /dev/null +++ b/src/components/sidebarLeft/tabs/2fa/enterPassword.ts @@ -0,0 +1,135 @@ +import AppTwoStepVerificationTab from "."; +import { SettingSection } from "../.."; +import { attachClickEvent, cancelEvent } from "../../../../helpers/dom"; +import { AccountPassword } from "../../../../layer"; +import passwordManager from "../../../../lib/mtproto/passwordManager"; +import Button from "../../../button"; +import { putPreloader } from "../../../misc"; +import PasswordMonkey from "../../../monkeys/password"; +import PasswordInputField from "../../../passwordInputField"; +import { ripple } from "../../../ripple"; +import SidebarSlider, { SliderSuperTab } from "../../../slider"; +import AppTwoStepVerificationReEnterPasswordTab from "./reEnterPassword"; + +export default class AppTwoStepVerificationEnterPasswordTab extends SliderSuperTab { + public state: AccountPassword; + + constructor(slider: SidebarSlider) { + super(slider, true); + } + + protected init() { + this.container.classList.add('two-step-verification-enter-password'); + this.title.innerHTML = this.state.pFlags.has_password ? 'Enter Your Password' : 'Enter a Password'; + + const section = new SettingSection({ + noDelimiter: true + }); + + const inputWrapper = document.createElement('div'); + inputWrapper.classList.add('input-wrapper'); + + const passwordInputField = new PasswordInputField({ + name: 'first-password', + label: this.state.pFlags.has_password ? this.state.hint ?? 'Password' : 'Enter a Password' + }); + + const monkey = new PasswordMonkey(passwordInputField, 157); + monkey.load(); + + const btnContinue = Button('btn-primary', {text: 'CONTINUE'}); + + inputWrapper.append(passwordInputField.container, btnContinue); + section.content.append(monkey.container, inputWrapper); + + this.scrollable.container.append(section.container); + + passwordInputField.input.addEventListener('keypress', (e) => { + if(passwordInputField.input.classList.contains('error')) { + passwordInputField.input.classList.remove('error'); + btnContinue.innerText = 'CONTINUE'; + ripple(btnContinue); + } + + if(e.key === 'Enter') { + return btnContinue.click(); + } + }); + + const verifyInput = () => { + if(!passwordInputField.value.length) { + passwordInputField.input.classList.add('error'); + return false; + } + + return true; + }; + + if(this.state.pFlags.has_password) { + let getStateInterval: number; + + let getState = () => { + // * just to check session relevance + if(!getStateInterval) { + getStateInterval = window.setInterval(getState, 10e3); + } + + return passwordManager.getState().then(_state => { + this.state = _state; + + passwordInputField.label.innerText = this.state.hint ?? 'Password'; + }); + }; + + const submit = (e?: Event) => { + if(!verifyInput()) { + cancelEvent(e); + return; + } + + btnContinue.setAttribute('disabled', 'true'); + btnContinue.textContent = 'PLEASE WAIT...'; + putPreloader(btnContinue); + + const plainPassword = passwordInputField.value; + passwordManager.check(passwordInputField.value, this.state).then(auth => { + console.log(auth); + + if(auth._ === 'auth.authorization') { + clearInterval(getStateInterval); + if(monkey) monkey.remove(); + const tab = new AppTwoStepVerificationTab(this.slider); + tab.state = this.state; + tab.plainPassword = plainPassword; + tab.open(); + } + }, (err) => { + btnContinue.removeAttribute('disabled'); + passwordInputField.input.classList.add('error'); + + switch(err.type) { + default: + //btnContinue.innerText = err.type; + btnContinue.innerText = 'INVALID PASSWORD'; + break; + } + + getState(); + }); + }; + + attachClickEvent(btnContinue, submit); + + getState(); + } else { + attachClickEvent(btnContinue, (e) => { + cancelEvent(e); + if(!verifyInput()) return; + + const tab = new AppTwoStepVerificationReEnterPasswordTab(this.slider); + tab.state = this.state; + tab.open(); + }); + } + } +} diff --git a/src/components/sidebarLeft/tabs/2fa/index.ts b/src/components/sidebarLeft/tabs/2fa/index.ts index 590378e4..bd2fd14b 100644 --- a/src/components/sidebarLeft/tabs/2fa/index.ts +++ b/src/components/sidebarLeft/tabs/2fa/index.ts @@ -1,15 +1,20 @@ -import { generateSection, SettingSection } from "../.."; +import { SettingSection } from "../.."; +import { attachClickEvent } from "../../../../helpers/dom"; import { AccountPassword } from "../../../../layer"; import appStickersManager from "../../../../lib/appManagers/appStickersManager"; +import passwordManager from "../../../../lib/mtproto/passwordManager"; import Button from "../../../button"; +import PopupConfirmAction from "../../../popups/confirmAction"; import SidebarSlider, { SliderSuperTab } from "../../../slider"; import { wrapSticker } from "../../../wrappers"; +import AppTwoStepVerificationEnterPasswordTab from "./enterPassword"; export default class AppTwoStepVerificationTab extends SliderSuperTab { - public passwordState: AccountPassword; + public state: AccountPassword; + public plainPassword: string; constructor(slider: SidebarSlider) { - super(slider); + super(slider, true); } protected init() { @@ -40,19 +45,40 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab { section.content.append(stickerContainer); const c = section.generateContentElement(); - if(this.passwordState.pFlags.has_password) { + if(this.state.pFlags.has_password) { section.caption.innerHTML = 'You have enabled Two-Step verification.
You\'ll need the password you set up here to log in to your Telegram account'; const btnChangePassword = Button('btn-primary btn-transparent', {icon: 'edit', text: 'Change Password'}); - const btnRemovePassword = Button('btn-primary btn-transparent', {icon: 'passwordoff', text: 'Turn Password Off'}); + const btnDisablePassword = Button('btn-primary btn-transparent', {icon: 'passwordoff', text: 'Turn Password Off'}); const btnSetRecoveryEmail = Button('btn-primary btn-transparent', {icon: 'email', text: 'Set Recovery Email'}); - c.append(btnChangePassword, btnRemovePassword, btnSetRecoveryEmail); + attachClickEvent(btnDisablePassword, () => { + const popup = new PopupConfirmAction('popup-disable-password', [{ + text: 'DISABLE', + callback: () => { + passwordManager.updateSettings({currentPassword: this.plainPassword}); + }, + isDanger: true, + }], { + title: 'Warning', + text: 'Are you sure you want to disable your password?' + }); + + popup.show(); + }); + + c.append(btnChangePassword, btnDisablePassword, btnSetRecoveryEmail); } else { section.caption.innerHTML = 'You can set a password that will be required when you log in on a new device in addition to the code you get in the SMS.'; const btnSetPassword = Button('btn-primary', {text: 'SET PASSWORD'}); c.append(btnSetPassword); + + attachClickEvent(btnSetPassword, (e) => { + const tab = new AppTwoStepVerificationEnterPasswordTab(this.slider); + tab.state = this.state; + tab.open(); + }); } this.scrollable.container.append(section.container); diff --git a/src/components/sidebarLeft/tabs/2fa/reEnterPassword.ts b/src/components/sidebarLeft/tabs/2fa/reEnterPassword.ts new file mode 100644 index 00000000..ef62dfae --- /dev/null +++ b/src/components/sidebarLeft/tabs/2fa/reEnterPassword.ts @@ -0,0 +1,74 @@ +import AppTwoStepVerificationTab from "."; +import { SettingSection } from "../.."; +import { attachClickEvent, cancelEvent } from "../../../../helpers/dom"; +import { AccountPassword } from "../../../../layer"; +import passwordManager from "../../../../lib/mtproto/passwordManager"; +import Button from "../../../button"; +import { putPreloader } from "../../../misc"; +import PasswordMonkey from "../../../monkeys/password"; +import PasswordInputField from "../../../passwordInputField"; +import { ripple } from "../../../ripple"; +import SidebarSlider, { SliderSuperTab } from "../../../slider"; + +export default class AppTwoStepVerificationReEnterPasswordTab extends SliderSuperTab { + public state: AccountPassword; + + constructor(slider: SidebarSlider) { + super(slider, true); + } + + protected init() { + this.container.classList.add('two-step-verification-enter-password'); + this.title.innerHTML = 'Re-Enter your Password'; + + const section = new SettingSection({ + noDelimiter: true + }); + + const inputWrapper = document.createElement('div'); + inputWrapper.classList.add('input-wrapper'); + + const passwordInputField = new PasswordInputField({ + name: 're-enter-password', + label: 'Re-Enter your Password' + }); + + const monkey = new PasswordMonkey(passwordInputField, 157); + monkey.load(); + + const btnContinue = Button('btn-primary', {text: 'CONTINUE'}); + + inputWrapper.append(passwordInputField.container, btnContinue); + section.content.append(monkey.container, inputWrapper); + + this.scrollable.container.append(section.container); + + passwordInputField.input.addEventListener('keypress', (e) => { + if(passwordInputField.input.classList.contains('error')) { + passwordInputField.input.classList.remove('error'); + btnContinue.innerText = 'CONTINUE'; + ripple(btnContinue); + } + + if(e.key === 'Enter') { + return btnContinue.click(); + } + }); + + const verifyInput = () => { + if(!passwordInputField.value.length) { + passwordInputField.input.classList.add('error'); + return false; + } + + return true; + }; + + attachClickEvent(btnContinue, (e) => { + cancelEvent(e); + if(!verifyInput()) return; + + + }); + } +} diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index 80871d1d..68dffe83 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -1,11 +1,12 @@ import SidebarSlider, { SliderSuperTab } from "../../slider"; import { generateSection, SettingSection } from ".."; import Row from "../../row"; -import { InputPrivacyKey, PrivacyRule } from "../../../layer"; +import { AccountPassword, InputPrivacyKey, PrivacyRule } from "../../../layer"; import appPrivacyManager from "../../../lib/appManagers/appPrivacyManager"; import AppPrivacyPhoneNumberTab from "./privacy/phoneNumber"; import AppTwoStepVerificationTab from "./2fa"; import passwordManager from "../../../lib/mtproto/passwordManager"; +import AppTwoStepVerificationEnterPasswordTab from "./2fa/enterPassword"; export default class AppPrivacyAndSecurityTab extends SliderSuperTab { constructor(slider: SidebarSlider) { @@ -28,14 +29,25 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { clickable: true }); - const tab = new AppTwoStepVerificationTab(this.slider); - - const twoFactorRow = new Row({ + let passwordState: AccountPassword; + const twoFactorRowOptions = { icon: 'lock', title: 'Two-Step Verification', subtitle: 'Loading...', - navigationTab: tab - }); + clickable: (e: Event) => { + let tab: AppTwoStepVerificationTab | AppTwoStepVerificationEnterPasswordTab; + if(passwordState.pFlags.has_password) { + tab = new AppTwoStepVerificationEnterPasswordTab(this.slider); + } else { + tab = new AppTwoStepVerificationTab(this.slider); + } + + tab.state = passwordState; + tab.open(); + } + }; + + const twoFactorRow = new Row(twoFactorRowOptions); twoFactorRow.freezed = true; const activeSessionRow = new Row({ @@ -49,10 +61,11 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTab { this.scrollable.append(section.container); passwordManager.getState().then(state => { + passwordState = state; twoFactorRow.subtitle.innerText = state.pFlags.has_password ? 'On' : 'Off'; twoFactorRow.freezed = false; - tab.passwordState = state; - //console.log('password state', state); + + console.log('password state', state); }); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index fb1605d0..cc4403c0 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2465,7 +2465,7 @@ export class AppMessagesManager { if(document.type === 'video') { messageText = 'Video' + (message.message ? ', ' : '') + ''; } else if(document.type === 'voice') { - messageText = 'Voice message'; + messageText = 'Voice message' + (message.message ? ', ' : '') + ''; } else if(document.type === 'gif') { messageText = 'GIF' + (message.message ? ', ' : '') + ''; } else if(document.type === 'round') { diff --git a/src/lib/mtproto/passwordManager.ts b/src/lib/mtproto/passwordManager.ts index eadbc8b4..60cfd85a 100644 --- a/src/lib/mtproto/passwordManager.ts +++ b/src/lib/mtproto/passwordManager.ts @@ -1,5 +1,7 @@ +import type { AccountPassword, AccountPasswordInputSettings, AccountUpdatePasswordSettings, InputCheckPasswordSRP, PasswordKdfAlgo } from '../../layer'; +import type CryptoWorkerMethods from '../crypto/crypto_methods'; import { MOUNT_CLASS_TO } from '../../config/debug'; -import { AccountPassword } from '../../layer'; +import appUsersManager from '../appManagers/appUsersManager'; import apiManager from './mtprotoworker'; //import { computeCheck } from "../crypto/srp"; @@ -10,54 +12,71 @@ export class PasswordManager { }); } - /* public updateSettings(state: any, settings: any) { - var currentHashPromise; - var newHashPromise; - var params: any = { - new_settings: { - _: 'account.passwordInputSettings', - hint: settings.hint || '' - } - }; - - if(typeof settings.cur_password === 'string' && - settings.cur_password.length > 0) { - currentHashPromise = this.makePasswordHash(state.current_salt, settings.cur_password); - } else { - currentHashPromise = Promise.resolve([]); - } + public updateSettings(settings: { + hint?: string, + email?: string, + newPassword?: string, + currentPassword?: string + } = {}) { + //state = Object.assign({}, state); + //state.new_algo = Object.assign({}, state.new_algo); - if (typeof settings.new_password === 'string' && - settings.new_password.length > 0) { - var saltRandom = new Array(8); - var newSalt = bufferConcat(state.new_salt, saltRandom); - secureRandom.nextBytes(saltRandom); - newHashPromise = this.makePasswordHash(newSalt, settings.new_password); - params.new_settings.new_salt = newSalt; - } else { - if(typeof settings.new_password === 'string') { - params.new_settings.new_salt = []; + this.getState().then(state => { + let currentHashPromise: ReturnType; + let newHashPromise: Promise; + const params: AccountUpdatePasswordSettings = { + password: null, + new_settings: { + _: 'account.passwordInputSettings', + hint: settings.hint, + email: settings.email + } + }; + + if(settings.currentPassword) { + currentHashPromise = apiManager.computeSRP(settings.currentPassword, state); + } else { + currentHashPromise = Promise.resolve({ + _: 'inputCheckPasswordEmpty' + }); } - newHashPromise = Promise.resolve([]); - } - - if(typeof settings.email === 'string') { - params.new_settings.email = settings.email || ''; - } - - return Promise.all([currentHashPromise, newHashPromise]).then((hashes) => { - params.current_password_hash = hashes[0]; - params.new_settings.new_password_hash = hashes[1]; - - return apiManager.invokeApi('account.updatePasswordSettings', params); + + // * https://core.telegram.org/api/srp#setting-a-new-2fa-password, but still there is a mistake, TDesktop passes 'new_algo' everytime + const newAlgo = state.new_algo as PasswordKdfAlgo.passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow; + const salt1 = new Uint8Array(newAlgo.salt1.length + 32); + salt1.randomize(); + salt1.set(newAlgo.salt1, 0); + newAlgo.salt1 = salt1; + + if(settings.newPassword) { + newHashPromise = Promise.resolve(new Uint8Array()); + } else { + newHashPromise = Promise.resolve(new Uint8Array()); + } + + return Promise.all([currentHashPromise, newHashPromise]).then((hashes) => { + params.password = hashes[0]; + params.new_settings.new_algo = newAlgo; + params.new_settings.new_password_hash = hashes[1]; + + return apiManager.invokeApi('account.updatePasswordSettings', params); + }); }); - } */ + } public check(password: string, state: AccountPassword, options: any = {}) { return apiManager.computeSRP(password, state).then((inputCheckPassword) => { + //console.log('SRP', inputCheckPassword); return apiManager.invokeApi('auth.checkPassword', { password: inputCheckPassword - }, options); + }, options).then(auth => { + if(auth._ === 'auth.authorization') { + appUsersManager.saveApiUser(auth.user); + apiManager.setUserAuth(auth.user.id); + } + + return auth; + }); }); } diff --git a/src/pages/pagePassword.ts b/src/pages/pagePassword.ts index a92ed235..5a2749de 100644 --- a/src/pages/pagePassword.ts +++ b/src/pages/pagePassword.ts @@ -1,48 +1,28 @@ -//import CryptoWorker from '../lib/crypto/cryptoworker'; -//import apiManager from '../lib/mtproto/apiManager'; import { putPreloader } from '../components/misc'; import mediaSizes from '../helpers/mediaSizes'; -import { isAppleMobile, isSafari } from '../helpers/userAgent'; import { AccountPassword } from '../layer'; import appStateManager from '../lib/appManagers/appStateManager'; -import LottieLoader, { RLottiePlayer } from '../lib/lottieLoader'; -//import passwordManager from '../lib/mtproto/passwordManager'; import apiManager from '../lib/mtproto/mtprotoworker'; import passwordManager from '../lib/mtproto/passwordManager'; -import { cancelEvent } from '../helpers/dom'; import Page from './page'; import pageIm from './pageIm'; -import InputField from '../components/inputField'; import Button from '../components/button'; +import PasswordInputField from '../components/passwordInputField'; +import PasswordMonkey from '../components/monkeys/password'; +import { ripple } from '../components/ripple'; const TEST = false; let passwordInput: HTMLInputElement; let onFirstMount = (): Promise => { - let needFrame = 0; - let animation: RLottiePlayer; - - let passwordVisible = false; - const btnNext = Button('btn-primary', {text: 'NEXT'}); - const passwordInputField = new InputField({ + const passwordInputField = new PasswordInputField({ label: 'Password', - name: 'password', - plainText: true + name: 'password' }); passwordInput = passwordInputField.input as HTMLInputElement; - passwordInput.type = 'password'; - passwordInput.setAttribute('required', ''); - passwordInput.autocomplete = 'off'; - - const passwordInputLabel = passwordInput.nextElementSibling as HTMLLabelElement; - - const toggleVisible = document.createElement('span'); - toggleVisible.classList.add('toggle-visible', 'tgico'); - - passwordInputField.container.append(toggleVisible); page.pageEl.querySelector('.input-wrapper').append(passwordInputField.container, btnNext); @@ -57,13 +37,13 @@ let onFirstMount = (): Promise => { return !TEST && passwordManager.getState().then(_state => { state = _state; - passwordInputLabel.innerText = state.hint ?? 'Password'; + passwordInputField.label.innerText = state.hint ?? 'Password'; }); }; let handleError = (err: any) => { btnNext.removeAttribute('disabled'); - passwordInput.classList.add('error'); + passwordInputField.input.classList.add('error'); switch(err.type) { default: @@ -75,29 +55,6 @@ let onFirstMount = (): Promise => { getState(); }; - const onVisibilityClick = function(this: typeof toggleVisible, e: Event) { - cancelEvent(e); - passwordVisible = !passwordVisible; - - this.classList.toggle('eye-hidden', passwordVisible); - if(passwordVisible) { - passwordInput.setAttribute('type', 'text'); - animation.setDirection(1); - animation.curFrame = 0; - needFrame = 16; - animation.play(); - } else { - passwordInput.setAttribute('type', 'password'); - animation.setDirection(-1); - animation.curFrame = 16; - needFrame = 0; - animation.play(); - } - }; - - toggleVisible.addEventListener('click', onVisibilityClick); - toggleVisible.addEventListener('touchend', onVisibilityClick); - let state: AccountPassword; btnNext.addEventListener('click', function(this, e) { @@ -117,15 +74,14 @@ let onFirstMount = (): Promise => { switch(response._) { case 'auth.authorization': - apiManager.setUserAuth(response.user.id); - clearInterval(getStateInterval); pageIm.mount(); - if(animation) animation.remove(); + if(monkey) monkey.remove(); break; default: btnNext.removeAttribute('disabled'); btnNext.innerText = response._; + ripple(btnNext); break; } }).catch(handleError); @@ -134,42 +90,18 @@ let onFirstMount = (): Promise => { passwordInput.addEventListener('keypress', function(this, e) { this.classList.remove('error'); btnNext.innerText = 'NEXT'; + ripple(btnNext); if(e.key === 'Enter') { return btnNext.click(); } }); - /* passwordInput.addEventListener('input', function(this, e) { - - }); */ const size = mediaSizes.isMobile ? 100 : 166; + const monkey = new PasswordMonkey(passwordInputField, size); + page.pageEl.querySelector('.auth-image').append(monkey.container); return Promise.all([ - LottieLoader.loadAnimationFromURL({ - container: page.pageEl.querySelector('.auth-image'), - loop: false, - autoplay: false, - width: size, - height: size, - noCache: true - //}, 'assets/img/TwoFactorSetupMonkeyClose.tgs').then(_animation => { - }, 'assets/img/TwoFactorSetupMonkeyPeek.tgs').then(_animation => { - //return; - animation = _animation; - animation.addListener('enterFrame', currentFrame => { - //console.log('enterFrame', e, needFrame); - - if((animation.direction === 1 && currentFrame >= needFrame) || - (animation.direction === -1 && currentFrame <= needFrame)) { - animation.setSpeed(1); - animation.pause(); - } - }); - - needFrame = 49; - //animation.play(); - }), - + monkey.load(), getState() ]); }; diff --git a/src/scss/partials/_input.scss b/src/scss/partials/_input.scss index 45c24439..d64874db 100644 --- a/src/scss/partials/_input.scss +++ b/src/scss/partials/_input.scss @@ -51,6 +51,7 @@ transform-origin: left center; pointer-events: none; margin-top: calc((var(--height) - 1.5rem) / 2); // * Center of first line + user-select: none; body.animation-level-0 & { transition: none; @@ -301,4 +302,54 @@ input:focus, button:focus { margin-right: -1px; } } -} \ No newline at end of file +} + +.input-field-password { + .input-field-input { + padding-right: 2.5rem; + max-height: 54px; + + &[type="password"] { + font-size: 2.25rem; + padding-left: calc(.875rem - var(--border-width)); + + @media (-webkit-min-device-pixel-ratio: 2) { + font-size: 1.75rem; + letter-spacing: .125rem; + } + + html.is-ios & { + // ! меньше 1rem будет зумить поле + font-size: 1rem; + } + } + } + + .toggle-visible { + position: absolute; + right: .375rem; + z-index: 2; + font-size: 1.5rem; + color: $placeholder-color; + cursor: pointer; + transition: color .2s; + padding: .5rem; + display: flex; + align-items: center; + justify-content: center; + top: 50%; + transform: translateY(-50%); + + &:before { + content: $tgico-eye1; + } + + &.eye-hidden:before { + content: $tgico-eye2; + } + + @include hover() { + color: #000; + } + } +} diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index a21b6967..957dda02 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -843,6 +843,10 @@ @include respond-to(not-handhelds) { margin: 0 .625rem; } + + > .btn-primary { + margin: 0; + } } &-name { @@ -857,7 +861,7 @@ padding: 0 .875rem; } - .btn-primary, .checkbox-field, .radio-field { + .checkbox-field, .radio-field { margin: 0; } @@ -905,6 +909,13 @@ } } +.two-step-verification-enter-password { + .media-sticker-wrapper { + width: 157px; + height: 157px; + } +} + .range-setting-selector { padding: 1rem .875rem; diff --git a/src/scss/partials/pages/_chats.scss b/src/scss/partials/pages/_chats.scss index 98298ff8..1ce49e15 100644 --- a/src/scss/partials/pages/_chats.scss +++ b/src/scss/partials/pages/_chats.scss @@ -91,7 +91,8 @@ left: 50%; transform: translate3d(-50%, -50%, 0); - &-path { + // ! do not change it to &-path + .preloader-path { stroke: $button-primary-background; } } diff --git a/src/scss/partials/pages/_password.scss b/src/scss/partials/pages/_password.scss index f12ed46a..189993ed 100644 --- a/src/scss/partials/pages/_password.scss +++ b/src/scss/partials/pages/_password.scss @@ -1,49 +1,3 @@ -.page-password { - .input-field-input { - padding-right: 2.5rem; - max-height: 54px; - - &[type="password"] { - font-size: 2.25rem; - padding-left: 10px; - - @media (-webkit-min-device-pixel-ratio: 2) { - font-size: 1.75rem; - letter-spacing: .125rem; - } - - html.is-ios & { - // ! меньше 1rem будет зумить поле - font-size: 1rem; - } - } - } +/* .page-password { - .toggle-visible { - position: absolute; - right: .25rem; - z-index: 2; - font-size: 1.25rem; - color: $placeholder-color; - cursor: pointer; - transition: color .2s; - padding: .5rem; - display: flex; - align-items: center; - justify-content: center; - top: 50%; - transform: translateY(-50%); - - &:before { - content: $tgico-eye1; - } - - &.eye-hidden:before { - content: $tgico-eye2; - } - - @include hover() { - color: #000; - } - } -} \ No newline at end of file +} */ diff --git a/src/scss/partials/popups/_peer.scss b/src/scss/partials/popups/_peer.scss index 55d0ab5b..17a275ec 100644 --- a/src/scss/partials/popups/_peer.scss +++ b/src/scss/partials/popups/_peer.scss @@ -4,25 +4,28 @@ #{$parent} { &-header { display: flex; - margin-bottom: 0.4375rem; + margin-bottom: .4375rem; align-items: center; - padding: 0.125rem 0.25rem; + padding: .125rem .25rem; } &-container { - padding: 1rem 1.5rem 0.75rem 1rem; + padding: 1rem 1.5rem .75rem 1rem; } &-title { - padding-left: 0.75rem; font-size: 1.25rem; font-weight: 500; - margin-bottom: 0.125rem; + margin-bottom: .125rem; text-transform: capitalize; + + &:not(:first-child) { + padding-left: .75rem; + } } &-description { - padding: 0 0.25rem; + padding: 0 .25rem; margin-top: 0; margin-bottom: 1.625rem; min-width: 15rem; @@ -33,13 +36,13 @@ } &-buttons { - margin-right: -0.75rem; + margin-right: -.75rem; .btn { font-weight: 500; & + .btn { - margin-top: 0.625rem; + margin-top: .625rem; } } }