Browse Source

Merge branch 'payments'

master
Eduard Kuzmenko 2 years ago
parent
commit
d0310569c0
  1. 2
      src/components/call/index.ts
  2. 111
      src/components/chat/bubbles.ts
  3. 9
      src/components/chat/input.ts
  4. 6
      src/components/confirmationPopup.ts
  5. 288
      src/components/countryInputField.ts
  6. 5
      src/components/groupCall/index.ts
  7. 155
      src/components/inputField.ts
  8. 76
      src/components/inputFieldAnimated.ts
  9. 4
      src/components/middleEllipsis.ts
  10. 2
      src/components/popups/avatar.ts
  11. 4
      src/components/popups/createContact.ts
  12. 6
      src/components/popups/createPoll.ts
  13. 26
      src/components/popups/datePicker.ts
  14. 66
      src/components/popups/index.ts
  15. 33
      src/components/popups/joinChatInvite.ts
  16. 49
      src/components/popups/mute.ts
  17. 5
      src/components/popups/newMedia.ts
  18. 787
      src/components/popups/payment.ts
  19. 545
      src/components/popups/paymentCard.ts
  20. 70
      src/components/popups/paymentCardConfirmation.ts
  21. 231
      src/components/popups/paymentShipping.ts
  22. 80
      src/components/popups/paymentShippingMethods.ts
  23. 64
      src/components/popups/paymentVerification.ts
  24. 23
      src/components/popups/peer.ts
  25. 2
      src/components/popups/pickUser.ts
  26. 5
      src/components/popups/reactedList.ts
  27. 3
      src/components/popups/schedule.ts
  28. 15
      src/components/popups/sponsored.ts
  29. 57
      src/components/popups/stickers.ts
  30. 18
      src/components/sidebarLeft/index.ts
  31. 2
      src/components/sidebarLeft/tabs/2fa/email.ts
  32. 2
      src/components/sidebarLeft/tabs/2fa/emailConfirmation.ts
  33. 2
      src/components/sidebarLeft/tabs/2fa/enterPassword.ts
  34. 2
      src/components/sidebarLeft/tabs/2fa/index.ts
  35. 2
      src/components/sidebarLeft/tabs/2fa/passwordSet.ts
  36. 45
      src/components/sidebarLeft/tabs/privacyAndSecurity.ts
  37. 2
      src/components/usernameInputField.ts
  38. 57
      src/components/wrappers/messageActionTextNewUnsafe.ts
  39. 5
      src/components/wrappers/messageForReply.ts
  40. 12
      src/components/wrappers/photo.ts
  41. 43
      src/components/wrappers/sticker.ts
  42. 2
      src/config/app.ts
  43. 18
      src/config/currencies.ts
  44. 9
      src/config/font.ts
  45. 5
      src/helpers/array/createArray.ts
  46. 9
      src/helpers/cacheCallback.ts
  47. 112
      src/helpers/cards/cardBrands.ts
  48. 78
      src/helpers/cards/cardFormattingPatterns.ts
  49. 25
      src/helpers/cards/formatInputValueByPattern.ts
  50. 102
      src/helpers/cards/formatValueByPattern.ts
  51. 85
      src/helpers/cards/patternCharacters.ts
  52. 82
      src/helpers/cards/validateCard.ts
  53. 17
      src/helpers/dom/loadScript.ts
  54. 8
      src/helpers/dom/placeCaretAtEnd.ts
  55. 7
      src/helpers/long/longFromInts.ts
  56. 7
      src/helpers/long/ulongFromInts.ts
  57. 9
      src/helpers/mediaSizes.ts
  58. 73
      src/helpers/paymentsWrapCurrencyAmount.ts
  59. 2
      src/helpers/scrollSaver.ts
  60. 16
      src/helpers/setAttachmentSize.ts
  61. 14
      src/helpers/string/buggedNumbers.ts
  62. 2
      src/helpers/string/nbsp.ts
  63. 3
      src/helpers/string/replaceNonLatin.ts
  64. 3
      src/helpers/string/replaceNonNumber.ts
  65. 8
      src/index.hbs
  66. 66
      src/lang.ts
  67. 2
      src/langSign.ts
  68. 8
      src/layer.d.ts
  69. 2
      src/lib/appManagers/appDownloadManager.ts
  70. 2
      src/lib/appManagers/appImManager.ts
  71. 53
      src/lib/appManagers/appInlineBotsManager.ts
  72. 43
      src/lib/appManagers/appMessagesManager.ts
  73. 75
      src/lib/appManagers/appPaymentsManager.ts
  74. 2
      src/lib/appManagers/appPeersManager.ts
  75. 23
      src/lib/appManagers/appWebDocsManager.ts
  76. 6
      src/lib/appManagers/createManagers.ts
  77. 4
      src/lib/appManagers/manager.ts
  78. 4
      src/lib/appManagers/uiNotificationsManager.ts
  79. 11
      src/lib/appManagers/utils/download/getDownloadMediaDetails.ts
  80. 12
      src/lib/appManagers/utils/photos/choosePhotoSize.ts
  81. 15
      src/lib/appManagers/utils/webDocs/getWebDocumentDownloadOptions.ts
  82. 5
      src/lib/appManagers/utils/webDocs/isWebDocument.ts
  83. 8
      src/lib/fileManager.ts
  84. 2
      src/lib/langPack.ts
  85. 19
      src/lib/mtproto/apiFileManager.ts
  86. 6
      src/lib/mtproto/passwordManager.ts
  87. 12
      src/lib/mtproto/telegramMeWebManager.ts
  88. 4
      src/lib/mtproto/timeManager.ts
  89. 42
      src/lib/mtproto/tl_utils.ts
  90. 4
      src/lib/rootScope.ts
  91. 19
      src/lib/storages/thumbs.ts
  92. 233
      src/pages/pageSignIn.ts
  93. 12
      src/scripts/in/schema_additional_params.json
  94. 8
      src/scss/components/_global.scss
  95. 4
      src/scss/mixins/_hover.scss
  96. 3
      src/scss/partials/_button.scss
  97. 15
      src/scss/partials/_chatBubble.scss
  98. 27
      src/scss/partials/_checkbox.scss
  99. 32
      src/scss/partials/_input.scss
  100. 27
      src/scss/partials/_row.scss
  101. Some files were not shown because too many files have changed in this diff Show More

2
src/components/call/index.ts

@ -77,7 +77,7 @@ export default class PopupCall extends PopupElement { @@ -77,7 +77,7 @@ export default class PopupCall extends PopupElement {
private controlsHover: ControlsHover;
constructor(private instance: CallInstance) {
super('popup-call', undefined, {
super('popup-call', {
withoutOverlay: true,
closable: true
});

111
src/components/chat/bubbles.ts

@ -108,6 +108,8 @@ import { EmoticonsDropdown } from "../emoticonsDropdown"; @@ -108,6 +108,8 @@ import { EmoticonsDropdown } from "../emoticonsDropdown";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import noop from "../../helpers/noop";
import getAlbumText from "../../lib/appManagers/utils/messages/getAlbumText";
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
import PopupPayment from "../popups/payment";
const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -546,12 +548,15 @@ export default class ChatBubbles { @@ -546,12 +548,15 @@ export default class ChatBubbles {
});
});
this.listenerSetter.add(rootScope)('message_edit', ({storageKey, message}) => {
this.listenerSetter.add(rootScope)('message_edit', async({storageKey, message}) => {
if(storageKey !== this.chat.messagesStorageKey) return;
const bubble = this.bubbles[message.mid];
if(!bubble) return;
await getHeavyAnimationPromise();
if(this.bubbles[message.mid] !== bubble) return;
this.safeRenderMessage(message, true, bubble);
});
@ -1472,6 +1477,18 @@ export default class ChatBubbles { @@ -1472,6 +1477,18 @@ export default class ChatBubbles {
return;
}
const buyButton: HTMLElement = findUpClassName(target, 'is-buy');
if(buyButton) {
const message = await this.chat.getMessage(+bubble.dataset.mid);
if(!message) {
return;
}
new PopupPayment(message as Message.message);
return;
}
const spoiler: HTMLElement = findUpClassName(target, 'spoiler');
if(spoiler) {
const messageDiv = findUpClassName(spoiler, 'message');
@ -1903,8 +1920,6 @@ export default class ChatBubbles { @@ -1903,8 +1920,6 @@ export default class ChatBubbles {
}
public loadMoreHistory(top: boolean, justLoad = false) {
// return;
//this.log('loadMoreHistory', top);
if(
!this.peerId ||
@ -3089,6 +3104,10 @@ export default class ChatBubbles { @@ -3089,6 +3104,10 @@ export default class ChatBubbles {
const processQueue = async(): Promise<void> => {
log('start');
// if(!this.chat.setPeerPromise) {
// await pause(10000000);
// }
const renderQueue = this.messagesQueue.slice();
this.messagesQueue.length = 0;
@ -3591,7 +3610,7 @@ export default class ChatBubbles { @@ -3591,7 +3610,7 @@ export default class ChatBubbles {
rowDiv.classList.add('reply-markup-row');
buttons.forEach((button) => {
const text = wrapRichText(button.text, {noLinks: true, noLinebreaks: true});
let text: DocumentFragment | HTMLElement | string = wrapRichText(button.text, {noLinks: true, noLinebreaks: true});
let buttonEl: HTMLButtonElement | HTMLAnchorElement;
@ -3607,14 +3626,14 @@ export default class ChatBubbles { @@ -3607,14 +3626,14 @@ export default class ChatBubbles {
});
buttonEl = htmlToDocumentFragment(r).firstElementChild as HTMLAnchorElement;
buttonEl.classList.add('is-link', 'tgico');
buttonEl.classList.add('is-link');
break;
}
case 'keyboardButtonSwitchInline': {
buttonEl = document.createElement('button');
buttonEl.classList.add('is-switch-inline', 'tgico');
buttonEl.classList.add('is-switch-inline');
attachClickEvent(buttonEl, (e) => {
cancelEvent(e);
@ -3648,13 +3667,26 @@ export default class ChatBubbles { @@ -3648,13 +3667,26 @@ export default class ChatBubbles {
break;
}
case 'keyboardButtonBuy': {
buttonEl = document.createElement('button');
buttonEl.classList.add('is-buy');
if(messageMedia?._ === 'messageMediaInvoice') {
if(messageMedia.receipt_msg_id) {
text = i18n('Message.ReplyActionButtonShowReceipt');
}
}
break;
}
default: {
buttonEl = document.createElement('button');
break;
}
}
buttonEl.classList.add('reply-markup-button', 'rp');
buttonEl.classList.add('reply-markup-button', 'rp', 'tgico');
if(typeof(text) === 'string') {
buttonEl.insertAdjacentHTML('beforeend', text);
} else {
@ -3673,7 +3705,12 @@ export default class ChatBubbles { @@ -3673,7 +3705,12 @@ export default class ChatBubbles {
let target = e.target as HTMLElement;
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
if(!target || target.classList.contains('is-link') || target.classList.contains('is-switch-inline')) return;
if(
!target
|| target.classList.contains('is-link')
|| target.classList.contains('is-switch-inline')
|| target.classList.contains('is-buy')
) return;
cancelEvent(e);
@ -4186,6 +4223,62 @@ export default class ChatBubbles { @@ -4186,6 +4223,62 @@ export default class ChatBubbles {
break;
}
case 'messageMediaInvoice': {
const isTest = messageMedia.pFlags.test;
const photo = messageMedia.photo;
const priceEl = document.createElement(photo ? 'span' : 'div');
const f = document.createDocumentFragment();
const l = i18n(messageMedia.receipt_msg_id ? 'PaymentReceipt' : (isTest ? 'PaymentTestInvoice' : 'PaymentInvoice'));
l.classList.add('text-uppercase');
const joiner = ' ';
const p = document.createElement('span');
p.classList.add('text-bold');
p.textContent = paymentsWrapCurrencyAmount(messageMedia.total_amount, messageMedia.currency) + joiner;
f.append(p, l);
if(isTest && messageMedia.receipt_msg_id) {
const a = document.createElement('span');
a.classList.add('text-uppercase', 'pre-wrap');
a.append(joiner + '(Test)');
f.append(a);
}
setInnerHTML(priceEl, f);
if(photo) {
const mediaSize = mediaSizes.active.invoice;
wrapPhoto({
photo,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height
});
bubble.classList.add('photo');
priceEl.classList.add('video-time');
attachmentDiv.append(priceEl);
} else {
attachmentDiv = undefined;
}
const titleDiv = document.createElement('div');
titleDiv.classList.add('bubble-primary-color');
setInnerHTML(titleDiv, wrapEmojiText(messageMedia.title));
const richText = wrapEmojiText(messageMedia.description);
messageDiv.prepend(...[titleDiv, !photo && priceEl, richText].filter(Boolean));
bubble.classList.remove('is-message-empty');
bubble.classList.add('is-invoice');
break;
}
default:
attachmentDiv = undefined;
@ -4885,7 +4978,7 @@ export default class ChatBubbles { @@ -4885,7 +4978,7 @@ export default class ChatBubbles {
const isSponsored = !!(message as Message.message).pFlags.sponsored;
const middleware = this.getMiddleware();
const m = middlewarePromise(middleware);
return this.safeRenderMessage(message, isSponsored ? false : true, undefined, false, async(result) => {
return this.safeRenderMessage(message, isSponsored ? false : true, undefined, isSponsored, async(result) => {
const {bubble} = await m(result);
if(!bubble) {
return result;

9
src/components/chat/input.ts

@ -93,6 +93,7 @@ import { emojiFromCodePoints } from "../../vendor/emoji"; @@ -93,6 +93,7 @@ import { emojiFromCodePoints } from "../../vendor/emoji";
import { modifyAckedPromise } from "../../helpers/modifyAckedResult";
import ChatSendAs from "./sendAs";
import filterAsync from "../../helpers/array/filterAsync";
import InputFieldAnimated from "../inputFieldAnimated";
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -103,7 +104,7 @@ export default class ChatInput { @@ -103,7 +104,7 @@ export default class ChatInput {
// private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|^[^:@\/])(?!.*[:@\/]).*)$/;
public messageInput: HTMLElement;
public messageInputField: InputField;
public messageInputField: InputFieldAnimated;
private fileInput: HTMLInputElement;
private inputMessageContainer: HTMLDivElement;
private btnSend: HTMLButtonElement;
@ -1441,10 +1442,10 @@ export default class ChatInput { @@ -1441,10 +1442,10 @@ export default class ChatInput {
private attachMessageInputField() {
const oldInputField = this.messageInputField;
this.messageInputField = new InputField({
this.messageInputField = new InputFieldAnimated({
placeholder: 'Message',
name: 'message',
animate: true
withLinebreaks: true
});
this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
@ -1907,7 +1908,7 @@ export default class ChatInput { @@ -1907,7 +1908,7 @@ export default class ChatInput {
//const saveExecuted = this.prepareDocumentExecute();
// can't exec .value here because it will instantly check for autocomplete
const value = documentFragmentToHTML(wrapDraftText(newValue, {entities}));
this.messageInputField.setValueSilently(value, true);
this.messageInputField.setValueSilently(value);
const caret = this.messageInput.querySelector('.composer-sel');
if(caret) {

6
src/components/confirmationPopup.ts

@ -8,7 +8,7 @@ import { addCancelButton } from "./popups"; @@ -8,7 +8,7 @@ import { addCancelButton } from "./popups";
import PopupPeer, { PopupPeerOptions } from "./popups/peer";
// type PopupConfirmationOptions = Pick<PopupPeerOptions, 'titleLangKey'>;
type PopupConfirmationOptions = PopupPeerOptions & {
export type PopupConfirmationOptions = PopupPeerOptions & {
button: PopupPeerOptions['buttons'][0],
checkbox?: PopupPeerOptions['checkboxes'][0]
};
@ -20,14 +20,14 @@ export default function confirmationPopup(options: PopupConfirmationOptions) { @@ -20,14 +20,14 @@ export default function confirmationPopup(options: PopupConfirmationOptions) {
resolve(set ? !!set.size : undefined);
};
const buttons = addCancelButton([button]);
const buttons = addCancelButton(options.buttons || [button]);
const cancelButton = buttons.find((button) => button.isCancel);
cancelButton.callback = () => {
reject();
};
options.buttons = buttons;
options.checkboxes = checkbox && [checkbox];
options.checkboxes ??= checkbox && [checkbox];
new PopupPeer('popup-confirmation', options).show();
});

288
src/components/countryInputField.ts

@ -0,0 +1,288 @@ @@ -0,0 +1,288 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import IS_EMOJI_SUPPORTED from "../environment/emojiSupport";
import cancelEvent from "../helpers/dom/cancelEvent";
import simulateEvent from "../helpers/dom/dispatchEvent";
import findUpClassName from "../helpers/dom/findUpClassName";
import findUpTag from "../helpers/dom/findUpTag";
import replaceContent from "../helpers/dom/replaceContent";
import setInnerHTML from "../helpers/dom/setInnerHTML";
import fastSmoothScroll from "../helpers/fastSmoothScroll";
import { randomLong } from "../helpers/random";
import { HelpCountry, HelpCountryCode } from "../layer";
import I18n, { i18n } from "../lib/langPack";
import wrapEmojiText from "../lib/richTextProcessor/wrapEmojiText";
import rootScope from "../lib/rootScope";
import { getCountryEmoji } from "../vendor/emoji";
import InputField, { InputFieldOptions } from "./inputField";
import Scrollable from "./scrollable";
let countries: HelpCountry.helpCountry[];
const setCountries = () => {
countries = I18n.countriesList
.filter((country) => !country.pFlags?.hidden)
.sort((a, b) => (a.name || a.default_name).localeCompare(b.name || b.default_name));
};
let init = () => {
setCountries();
rootScope.addEventListener('language_change', () => {
setCountries();
});
};
export default class CountryInputField extends InputField {
private lastCountrySelected: HelpCountry;
private lastCountryCodeSelected: HelpCountryCode;
private hideTimeout: number;
private selectWrapper: HTMLElement;
private liMap: Map<string, HTMLLIElement[]>;
constructor(public options: InputFieldOptions & {
onCountryChange?: (country: HelpCountry.helpCountry, code: HelpCountryCode.helpCountryCode) => void,
noPhoneCodes?: boolean
} = {}) {
super({
label: 'Country',
name: randomLong(),
...options,
});
if(init) {
init();
init = undefined;
}
this.liMap = new Map();
this.container.classList.add('input-select');
const selectWrapper = this.selectWrapper = document.createElement('div');
selectWrapper.classList.add('select-wrapper', 'z-depth-3', 'hide');
const arrowDown = document.createElement('span');
arrowDown.classList.add('arrow', 'arrow-down');
this.container.append(arrowDown);
const selectList = document.createElement('ul');
selectWrapper.appendChild(selectList);
const scroll = new Scrollable(selectWrapper);
let initSelect = () => {
initSelect = null;
countries.forEach((c) => {
const emoji = getCountryEmoji(c.iso2);
const liArr: Array<HTMLLIElement> = [];
for(let i = 0, length = Math.min(c.country_codes.length, options.noPhoneCodes ? 1 : Infinity); i < length; ++i) {
const countryCode = c.country_codes[i];
const li = document.createElement('li');
let wrapped = wrapEmojiText(emoji);
if(IS_EMOJI_SUPPORTED) {
const spanEmoji = document.createElement('span');
setInnerHTML(spanEmoji, wrapped);
li.append(spanEmoji);
} else {
setInnerHTML(li, wrapped);
}
const el = i18n(c.default_name as any);
el.dataset.defaultName = c.default_name;
li.append(el);
if(!options.noPhoneCodes) {
const span = document.createElement('span');
span.classList.add('phone-code');
span.innerText = '+' + countryCode.country_code;
li.appendChild(span);
}
liArr.push(li);
selectList.append(li);
}
this.liMap.set(c.iso2, liArr);
});
selectList.addEventListener('mousedown', (e) => {
if(e.button !== 0) { // other buttons but left shall not pass
return;
}
const target = findUpTag(e.target, 'LI')
this.selectCountryByTarget(target);
//console.log('clicked', e, countryName, phoneCode);
});
this.container.appendChild(selectWrapper);
};
initSelect();
this.input.addEventListener('focus', (e) => {
if(initSelect) {
initSelect();
} else {
countries.forEach((c) => {
this.liMap.get(c.iso2).forEach((li) => li.style.display = '');
});
}
clearTimeout(this.hideTimeout);
this.hideTimeout = undefined;
selectWrapper.classList.remove('hide');
void selectWrapper.offsetWidth; // reflow
selectWrapper.classList.add('active');
this.select();
fastSmoothScroll({
// container: page.pageEl.parentElement.parentElement,
container: findUpClassName(this.container, 'scrollable-y'),
element: this.input,
position: 'start',
margin: 4
});
setTimeout(() => {
if(!mouseDownHandlerAttached) {
document.addEventListener('mousedown', onMouseDown, {capture: true});
mouseDownHandlerAttached = true;
}
}, 0);
});
let mouseDownHandlerAttached = false;
const onMouseDown = (e: MouseEvent) => {
if(findUpClassName(e.target, 'input-select')) {
return;
}
if(e.target === this.input) {
return;
}
this.hidePicker();
document.removeEventListener('mousedown', onMouseDown, {capture: true});
mouseDownHandlerAttached = false;
};
/* false && this.input.addEventListener('blur', function(this: typeof this.input, e) {
hidePicker();
e.cancelBubble = true;
}, {capture: true}); */
const onKeyPress = (e: KeyboardEvent) => {
const key = e.key;
if(e.ctrlKey || key === 'Control') return false;
//let i = new RegExp('^' + this.value, 'i');
let _value = this.value.toLowerCase();
let matches: HelpCountry[] = [];
countries.forEach((c) => {
const names = [
c.name,
c.default_name,
c.iso2
];
names.filter(Boolean).forEach((name) => {
const abbr = name.split(' ').filter((word) => /\w/.test(word)).map((word) => word[0]).join('');
if(abbr.length > 1) {
names.push(abbr);
}
});
let good = !!names.filter(Boolean).find((str) => str.toLowerCase().indexOf(_value) !== -1)/* === 0 */;//i.test(c.name);
this.liMap.get(c.iso2).forEach((li) => li.style.display = good ? '' : 'none');
if(good) matches.push(c);
});
// Код ниже автоматически выберет страну если она осталась одна при поиске
/* if(matches.length === 1 && matches[0].li.length === 1) {
if(matches[0].name === lastCountrySelected) return false;
//console.log('clicking', matches[0]);
var clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent('mousedown', true, true);
matches[0].li[0].dispatchEvent(clickEvent);
return false;
} else */if(matches.length === 0) {
countries.forEach((c) => {
this.liMap.get(c.iso2).forEach((li) => li.style.display = '');
});
} else if(matches.length === 1 && key === 'Enter') {
cancelEvent(e);
this.selectCountryByTarget(this.liMap.get(matches[0].iso2)[0]);
}
};
this.input.addEventListener('keyup', onKeyPress);
this.input.addEventListener('keydown', (e) => {
if(e.key === 'Enter') {
onKeyPress(e);
}
});
arrowDown.addEventListener('mousedown', (e) => {
if(this.input.matches(':focus')) {
this.hidePicker();
this.input.blur();
} else {
e.cancelBubble = true;
e.preventDefault();
this.input.focus();
}
});
}
public getSelected() {
return {country: this.lastCountrySelected, code: this.lastCountryCodeSelected};
}
public hidePicker = () => {
if(this.hideTimeout !== undefined) return;
this.selectWrapper.classList.remove('active');
this.hideTimeout = window.setTimeout(() => {
this.selectWrapper.classList.add('hide');
this.hideTimeout = undefined;
}, 200);
}
public selectCountryByTarget = (target: HTMLElement) => {
const defaultName = target.querySelector<HTMLElement>('[data-default-name]').dataset.defaultName;
const phoneCodeEl = target.querySelector<HTMLElement>('.phone-code');
const phoneCode = phoneCodeEl?.innerText;
const countryCode = phoneCode && phoneCode.replace(/\D/g, '');
replaceContent(this.input, i18n(defaultName as any));
simulateEvent(this.input, 'input');
this.lastCountrySelected = countries.find((c) => c.default_name === defaultName);
this.lastCountryCodeSelected = countryCode && this.lastCountrySelected.country_codes.find((_countryCode) => _countryCode.country_code === countryCode);
this.options.onCountryChange?.(this.lastCountrySelected, this.lastCountryCodeSelected);
this.hidePicker();
}
public selectCountryByIso2(iso2: string) {
this.selectCountryByTarget(this.liMap.get(iso2)[0]);
}
public override(country: HelpCountry, code: HelpCountryCode, countryName?: string) {
replaceContent(this.input, country ? i18n(country.default_name as any) : countryName);
this.lastCountrySelected = country;
this.lastCountryCodeSelected = code;
this.options.onCountryChange?.(this.lastCountrySelected, this.lastCountryCodeSelected);
}
}

5
src/components/groupCall/index.ts

@ -140,10 +140,11 @@ export default class PopupGroupCall extends PopupElement { @@ -140,10 +140,11 @@ export default class PopupGroupCall extends PopupElement {
private btnScreen: HTMLDivElement;
constructor() {
super('popup-group-call', undefined, {
super('popup-group-call', {
body: true,
withoutOverlay: true,
closable: true
closable: true,
title: true
});
this.videosCount = 0;

155
src/components/inputField.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import cancelEvent from "../helpers/dom/cancelEvent";
import simulateEvent from "../helpers/dom/dispatchEvent";
import documentFragmentToHTML from "../helpers/dom/documentFragmentToHTML";
import findUpAttribute from "../helpers/dom/findUpAttribute";
@ -16,14 +17,15 @@ import { i18n, LangPackKey, _i18n } from "../lib/langPack"; @@ -16,14 +17,15 @@ import { i18n, LangPackKey, _i18n } from "../lib/langPack";
import mergeEntities from "../lib/richTextProcessor/mergeEntities";
import parseEntities from "../lib/richTextProcessor/parseEntities";
import wrapDraftText from "../lib/richTextProcessor/wrapDraftText";
import SetTransition from "./singleTransition";
let init = () => {
document.addEventListener('paste', (e) => {
if(!findUpAttribute(e.target, 'contenteditable="true"')) {
const input = findUpAttribute(e.target, 'contenteditable="true"');
if(!input) {
return;
}
const noLinebreaks = !!input.dataset.noLinebreaks;
e.preventDefault();
let text: string, entities: MessageEntity[];
@ -33,6 +35,14 @@ let init = () => { @@ -33,6 +35,14 @@ let init = () => {
// @ts-ignore
let html: string = (e.originalEvent || e).clipboardData.getData('text/html');
const filterEntity = (e: MessageEntity) => e._ === 'messageEntityEmoji' || (e._ === 'messageEntityLinebreak' && !noLinebreaks);
if(noLinebreaks) {
const regExp = /[\r\n]/g;
plainText = plainText.replace(regExp, '');
html = html.replace(regExp, '');
}
if(html.trim()) {
html = html.replace(/<style([\s\S]*)<\/style>/, '');
html = html.replace(/<!--([\s\S]*)-->/, '');
@ -64,7 +74,7 @@ let init = () => { @@ -64,7 +74,7 @@ let init = () => {
usePlainText = false;
let entities2 = parseEntities(text);
entities2 = entities2.filter((e) => e._ === 'messageEntityEmoji' || e._ === 'messageEntityLinebreak');
entities2 = entities2.filter(filterEntity);
mergeEntities(entities, entities2);
}
}
@ -72,7 +82,7 @@ let init = () => { @@ -72,7 +82,7 @@ let init = () => {
if(usePlainText) {
text = plainText;
entities = parseEntities(text);
entities = entities.filter((e) => e._ === 'messageEntityEmoji' || e._ === 'messageEntityLinebreak');
entities = entities.filter(filterEntity);
}
const fragment = wrapDraftText(text, {entities});
@ -116,16 +126,17 @@ export type InputFieldOptions = { @@ -116,16 +126,17 @@ export type InputFieldOptions = {
maxLength?: number,
showLengthOn?: number,
plainText?: true,
animate?: boolean,
required?: boolean,
canBeEdited?: boolean,
validate?: () => boolean
validate?: () => boolean,
inputMode?: 'tel' | 'numeric',
withLinebreaks?: boolean,
autocomplete?: string
};
class InputField {
export default class InputField {
public container: HTMLElement;
public input: HTMLElement;
public inputFake: HTMLElement;
public label: HTMLLabelElement;
public originalValue: string;
@ -133,10 +144,6 @@ class InputField { @@ -133,10 +144,6 @@ class InputField {
public required: boolean;
public validate: () => boolean;
//public onLengthChange: (length: number, isOverflow: boolean) => void;
// protected wasInputFakeClientHeight: number;
// protected showScrollDebounced: () => void;
constructor(public options: InputFieldOptions = {}) {
this.container = document.createElement('div');
this.container.classList.add('input-field');
@ -148,10 +155,10 @@ class InputField { @@ -148,10 +155,10 @@ class InputField {
options.showLengthOn = Math.min(40, Math.round(options.maxLength / 3));
}
const {placeholder, maxLength, showLengthOn, name, plainText, canBeEdited = true} = options;
let label = options.label || options.labelText;
const {placeholder, maxLength, showLengthOn, name, plainText, canBeEdited = true, autocomplete} = options;
const label = options.label || options.labelText;
const onInputCallbacks: Array<() => void> = [];
let input: HTMLElement;
if(!plainText) {
if(init) {
@ -163,40 +170,26 @@ class InputField { @@ -163,40 +170,26 @@ class InputField {
`;
input = this.container.firstElementChild as HTMLElement;
const observer = new MutationObserver(() => {
//checkAndSetRTL(input);
// const observer = new MutationObserver(() => {
// //checkAndSetRTL(input);
if(processInput) {
processInput();
}
});
// if(processInput) {
// processInput();
// }
// });
// * because if delete all characters there will br left
input.addEventListener('input', () => {
onInputCallbacks.push(() => {
// * because if delete all characters there will br left
if(isInputEmpty(input)) {
input.innerHTML = '';
}
if(this.inputFake) {
this.inputFake.innerHTML = input.innerHTML;
this.onFakeInput();
input.textContent = '';
}
});
// ! childList for paste first symbol
observer.observe(input, {characterData: true, childList: true, subtree: true});
if(options.animate) {
input.classList.add('scrollable', 'scrollable-y');
// this.wasInputFakeClientHeight = 0;
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
this.inputFake = document.createElement('div');
this.inputFake.setAttribute('contenteditable', 'true');
this.inputFake.className = input.className + ' input-field-input-fake';
}
// observer.observe(input, {characterData: true, childList: true, subtree: true});
} else {
this.container.innerHTML = `
<input type="text" ${name ? `name="${name}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
<input type="text" ${name ? `name="${name}"` : ''} autocomplete="${autocomplete ?? 'off'}" ${label ? 'required=""' : ''} class="input-field-input">
`;
input = this.container.firstElementChild as HTMLElement;
@ -204,13 +197,13 @@ class InputField { @@ -204,13 +197,13 @@ class InputField {
}
input.setAttribute('dir', 'auto');
if(options.inputMode) {
input.inputMode = options.inputMode;
}
if(placeholder) {
_i18n(input, placeholder, undefined, 'placeholder');
if(this.inputFake) {
_i18n(this.inputFake, placeholder, undefined, 'placeholder');
}
}
if(label || placeholder) {
@ -225,12 +218,11 @@ class InputField { @@ -225,12 +218,11 @@ class InputField {
this.container.append(this.label);
}
let processInput: () => void;
if(maxLength) {
const labelEl = this.container.lastElementChild as HTMLLabelElement;
let showingLength = false;
processInput = () => {
const onInput = () => {
const wasError = input.classList.contains('error');
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input, false).value].length;
@ -250,7 +242,24 @@ class InputField { @@ -250,7 +242,24 @@ class InputField {
}
};
input.addEventListener('input', processInput);
onInputCallbacks.push(onInput);
}
const noLinebreaks = !options.withLinebreaks;
if(noLinebreaks && !plainText) {
input.dataset.noLinebreaks = '1';
input.addEventListener('keypress', (e) => {
if(e.key === 'Enter') {
e.preventDefault();
return false;
}
});
}
if(onInputCallbacks.length) {
input.addEventListener('input', () => {
onInputCallbacks.forEach((callback) => callback());
});
}
this.input = input;
@ -277,60 +286,22 @@ class InputField { @@ -277,60 +286,22 @@ class InputField {
}
}
public onFakeInput(setHeight = true) {
const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake;
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
this.showScrollDebounced();
} */
const currentHeight = +this.input.style.height.replace('px', '');
if(currentHeight === newHeight) {
return;
}
const TRANSITION_DURATION_FACTOR = 50;
const transitionDuration = Math.round(
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
);
// this.wasInputFakeClientHeight = clientHeight;
this.input.style.transitionDuration = `${transitionDuration}ms`;
if(setHeight) {
this.input.style.height = newHeight ? newHeight + 'px' : '';
}
const className = 'is-changing-height';
SetTransition(this.input, className, true, transitionDuration, () => {
this.input.classList.remove(className);
});
}
get value() {
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input, false).value;
//return getRichValue(this.input);
}
set value(value: string) {
this.setValueSilently(value, false);
this.setValueSilently(value, true);
simulateEvent(this.input, 'input');
}
public setValueSilently(value: string, fireFakeInput = true) {
public setValueSilently(value: string, fromSet?: boolean) {
if(this.options.plainText) {
(this.input as HTMLInputElement).value = value;
} else {
this.input.innerHTML = value;
if(this.inputFake) {
this.inputFake.innerHTML = value;
if(fireFakeInput) {
this.onFakeInput();
}
}
}
}
@ -348,7 +319,7 @@ class InputField { @@ -348,7 +319,7 @@ class InputField {
return this.isValid() && this.isChanged();
}
public setDraftValue(value = '', silent = false) {
public setDraftValue(value = '', silent?: boolean) {
if(!this.options.plainText) {
value = documentFragmentToHTML(wrapDraftText(value));
}
@ -360,7 +331,7 @@ class InputField { @@ -360,7 +331,7 @@ class InputField {
}
}
public setOriginalValue(value: InputField['originalValue'] = '', silent = false) {
public setOriginalValue(value: InputField['originalValue'] = '', silent?: boolean) {
this.originalValue = value;
this.setDraftValue(value, silent);
}
@ -369,6 +340,8 @@ class InputField { @@ -369,6 +340,8 @@ class InputField {
if(label) {
this.label.textContent = '';
this.label.append(i18n(label, this.options.labelOptions));
} else {
this.setLabel();
}
this.input.classList.toggle('error', !!(state & InputState.Error));
@ -379,5 +352,3 @@ class InputField { @@ -379,5 +352,3 @@ class InputField {
this.setState(InputState.Error, label);
}
}
export default InputField;

76
src/components/inputFieldAnimated.ts

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { _i18n } from "../lib/langPack";
import InputField, { InputFieldOptions } from "./inputField";
import SetTransition from "./singleTransition";
export default class InputFieldAnimated extends InputField {
public inputFake: HTMLElement;
//public onLengthChange: (length: number, isOverflow: boolean) => void;
// protected wasInputFakeClientHeight: number;
// protected showScrollDebounced: () => void;
constructor(options?: InputFieldOptions) {
super(options);
this.input.addEventListener('input', () => {
this.inputFake.innerHTML = this.input.innerHTML;
this.onFakeInput();
});
if(options.placeholder) {
_i18n(this.inputFake, options.placeholder, undefined, 'placeholder');
}
this.input.classList.add('scrollable', 'scrollable-y');
// this.wasInputFakeClientHeight = 0;
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
this.inputFake = document.createElement('div');
this.inputFake.setAttribute('contenteditable', 'true');
this.inputFake.className = this.input.className + ' input-field-input-fake';
}
public onFakeInput(setHeight = true) {
const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake;
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
this.showScrollDebounced();
} */
const currentHeight = +this.input.style.height.replace('px', '');
if(currentHeight === newHeight) {
return;
}
const TRANSITION_DURATION_FACTOR = 50;
const transitionDuration = Math.round(
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
);
// this.wasInputFakeClientHeight = clientHeight;
this.input.style.transitionDuration = `${transitionDuration}ms`;
if(setHeight) {
this.input.style.height = newHeight ? newHeight + 'px' : '';
}
const className = 'is-changing-height';
SetTransition(this.input, className, true, transitionDuration, () => {
this.input.classList.remove(className);
});
}
public setValueSilently(value: string, fromSet?: boolean) {
super.setValueSilently(value, fromSet);
this.inputFake.innerHTML = value;
if(!fromSet) {
this.onFakeInput();
}
}
}

4
src/components/middleEllipsis.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { FontFamily, FontSize, FontWeight } from "../config/font";
import getTextWidth from "../helpers/canvas/getTextWidth";
import mediaSizes from "../helpers/mediaSizes";
import clamp from "../helpers/number/clamp";
@ -33,7 +34,6 @@ const map: Map<HTMLElement, { @@ -33,7 +34,6 @@ const map: Map<HTMLElement, {
}> = new Map();
const testQueue: Set<HTMLElement> = new Set();
export const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
const fontSize = '16px';
let pendingTest = false;
@ -90,7 +90,7 @@ function testElement(element: HTMLElement) { @@ -90,7 +90,7 @@ function testElement(element: HTMLElement) {
multiplier = from > 0 && from / 100;
//const perf = performance.now();
font = `${element.dataset.fontWeight || 400} ${fontSize} ${fontFamily}`;
font = `${element.dataset.fontWeight || FontWeight} ${FontSize} ${FontFamily}`;
/* const computedStyle = window.getComputedStyle(elm, null);
font = `${computedStyle.getPropertyValue('font-weight')} ${computedStyle.getPropertyValue('font-size')} ${computedStyle.getPropertyValue('font-family')}`; */
//console.log('testMiddleEllipsis get computed style:', performance.now() - perf, font);

2
src/components/popups/avatar.ts

@ -30,7 +30,7 @@ export default class PopupAvatar extends PopupElement { @@ -30,7 +30,7 @@ export default class PopupAvatar extends PopupElement {
private onCrop: (upload: () => ReturnType<AppDownloadManager['upload']>) => void;
constructor() {
super('popup-avatar', null, {closable: true, withConfirm: true});
super('popup-avatar', {closable: true, withConfirm: true});
this.h6 = document.createElement('h6');
_i18n(this.h6, 'Popup.Avatar.Title');

4
src/components/popups/createContact.ts

@ -15,13 +15,11 @@ import { toastNew } from "../toast"; @@ -15,13 +15,11 @@ import { toastNew } from "../toast";
export default class PopupCreateContact extends PopupElement {
constructor() {
super('popup-create-contact popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Add'});
super('popup-create-contact popup-send-photo popup-new-media', {closable: true, withConfirm: 'Add', title: 'AddContactTitle'});
this.construct();
}
private async construct() {
_i18n(this.title, 'AddContactTitle');
attachClickEvent(this.btnConfirm, () => {
const promise = this.managers.appUsersManager.importContact(nameInputField.value, lastNameInputField.value, telInputField.value);

6
src/components/popups/createPoll.ts

@ -27,7 +27,7 @@ const MAX_LENGTH_SOLUTION = 200; @@ -27,7 +27,7 @@ const MAX_LENGTH_SOLUTION = 200;
export default class PopupCreatePoll extends PopupElement {
private questionInputField: InputField;
private questions: HTMLElement;
private scrollable: Scrollable;
protected scrollable: Scrollable;
private tempId = 0;
private anonymousCheckboxField: CheckboxField;
@ -39,13 +39,11 @@ export default class PopupCreatePoll extends PopupElement { @@ -39,13 +39,11 @@ export default class PopupCreatePoll extends PopupElement {
private optionInputFields: InputField[];
constructor(private chat: Chat) {
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'Create', body: true});
super('popup-create-poll popup-new-media', {closable: true, withConfirm: 'Create', body: true, title: 'NewPoll'});
this.construct();
}
private async construct() {
_i18n(this.title, 'NewPoll');
this.questionInputField = new InputField({
placeholder: 'AskAQuestion',
label: 'AskAQuestion',

26
src/components/popups/datePicker.ts

@ -39,17 +39,23 @@ export default class PopupDatePicker extends PopupElement { @@ -39,17 +39,23 @@ export default class PopupDatePicker extends PopupElement {
withTime: true,
showOverflowMonths: true
}> & PopupOptions = {}) {
super('popup-date-picker', options.noButtons ? [] : [{
langKey: 'JumpToDate',
callback: () => {
if(this.onPick) {
this.onPick(this.selectedDate.getTime() / 1000 | 0);
super('popup-date-picker', {
body: true,
overlayClosable: true,
buttons: options.noButtons ? [] : [{
langKey: 'JumpToDate',
callback: () => {
if(this.onPick) {
this.onPick(this.selectedDate.getTime() / 1000 | 0);
}
}
}
}, {
langKey: 'Cancel',
isCancel: true
}], {body: true, overlayClosable: true, ...options});
}, {
langKey: 'Cancel',
isCancel: true
}],
title: true,
...options
});
this.minDate = options.minDate || new Date('2013-08-01T00:00:00');

66
src/components/popups/index.ts

@ -4,11 +4,10 @@ @@ -4,11 +4,10 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import rootScope from "../../lib/rootScope";
import ripple from "../ripple";
import animationIntersector from "../animationIntersector";
import appNavigationController, { NavigationItem } from "../appNavigationController";
import { i18n, LangPackKey } from "../../lib/langPack";
import { i18n, LangPackKey, _i18n } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName";
import blurActiveElement from "../../helpers/dom/blurActiveElement";
import ListenerSetter from "../../helpers/listenerSetter";
@ -20,6 +19,7 @@ import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/f @@ -20,6 +19,7 @@ import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/f
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { AppManagers } from "../../lib/appManagers/managers";
import overlayCounter from "../../helpers/overlayCounter";
import Scrollable from "../scrollable";
export type PopupButton = {
text?: string,
@ -37,7 +37,10 @@ export type PopupOptions = Partial<{ @@ -37,7 +37,10 @@ export type PopupOptions = Partial<{
withConfirm: LangPackKey | boolean,
body: boolean,
confirmShortcutIsSendShortcut: boolean,
withoutOverlay: boolean
withoutOverlay: boolean,
scrollable: boolean,
buttons: Array<PopupButton>,
title: boolean | LangPackKey
}>;
export interface PopupElementConstructable<T extends PopupElement = any> {
@ -79,22 +82,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -79,22 +82,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
protected listenerSetter: ListenerSetter;
protected confirmShortcutIsSendShortcut: boolean;
protected btnConfirmOnEnter: HTMLButtonElement;
protected btnConfirmOnEnter: HTMLElement;
protected withoutOverlay: boolean;
protected managers: AppManagers;
constructor(className: string, protected buttons?: Array<PopupButton>, options: PopupOptions = {}) {
protected scrollable: Scrollable;
protected buttons: Array<PopupButton>;
constructor(className: string, options: PopupOptions = {}) {
super(false);
this.element.classList.add('popup');
this.element.className = 'popup' + (className ? ' ' + className : '');
this.container.classList.add('popup-container', 'z-depth-1');
this.header.classList.add('popup-header');
this.title.classList.add('popup-title');
this.header.append(this.title);
if(options.title) {
this.title.classList.add('popup-title');
if(typeof(options.title) === 'string') {
_i18n(this.title, options.title);
}
this.header.append(this.title);
}
this.listenerSetter = new ListenerSetter();
this.managers = PopupElement.MANAGERS;
@ -140,14 +153,25 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -140,14 +153,25 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.container.append(this.body);
}
if(options.scrollable) {
const scrollable = this.scrollable = new Scrollable(this.body);
scrollable.onAdditionalScroll = () => {
scrollable.container.classList.toggle('scrolled-top', !scrollable.scrollTop);
scrollable.container.classList.toggle('scrolled-bottom', scrollable.isScrolledDown);
};
scrollable.container.classList.add('scrolled-top', 'scrolled-bottom', 'scrollable-y-bordered');
if(!this.body) {
this.container.insertBefore(scrollable.container, this.header.nextSibling);
}
}
let btnConfirmOnEnter = this.btnConfirm;
const buttons = this.buttons = options.buttons;
if(buttons?.length) {
const buttonsDiv = this.buttonsEl = document.createElement('div');
buttonsDiv.classList.add('popup-buttons');
if(buttons.length === 2) {
buttonsDiv.classList.add('popup-buttons-row');
}
const buttonsElements = buttons.map((b) => {
const button = document.createElement('button');
@ -187,6 +211,12 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -187,6 +211,12 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
PopupElement.POPUPS.push(this);
}
protected onContentUpdate() {
if(this.scrollable) {
this.scrollable.onAdditionalScroll();
}
}
public show() {
this.navigationItem = {
type: 'popup',
@ -201,22 +231,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -201,22 +231,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
void this.element.offsetWidth; // reflow
this.element.classList.add('active');
this.onContentUpdate();
if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = true;
animationIntersector.checkAnimations(true);
}
// cannot add event instantly because keydown propagation will fire it
if(this.btnConfirmOnEnter) {
// if(this.btnConfirmOnEnter) {
setTimeout(() => {
if(!this.element.classList.contains('active')) {
return;
}
this.listenerSetter.add(document.body)('keydown', (e) => {
if(PopupElement.POPUPS[PopupElement.POPUPS.length - 1] !== this) {
return;
}
if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') {
simulateClickEvent(this.btnConfirmOnEnter);
cancelEvent(e);
}
});
}, 0);
}
// }
}
public hide = () => {

33
src/components/popups/joinChatInvite.ts

@ -24,20 +24,25 @@ export default class PopupJoinChatInvite extends PopupElement { @@ -24,20 +24,25 @@ export default class PopupJoinChatInvite extends PopupElement {
private hash: string,
private chatInvite: ChatInvite.chatInvite,
) {
super('popup-join-chat-invite', addCancelButton([{
langKey: chatInvite.pFlags.request_needed ? 'RequestJoin.Button' : (chatInvite.pFlags.broadcast ? 'JoinByPeekChannelTitle' : 'JoinByPeekGroupTitle'),
callback: () => {
this.managers.appChatsManager.importChatInvite(hash)
.then((chatId) => {
const peerId = chatId.toPeerId(true);
appImManager.setInnerPeer({peerId});
}, (error) => {
if(error.type === 'INVITE_REQUEST_SENT') {
toastNew({langPackKey: 'RequestToJoinSent'});
}
});
}
}]), {closable: true, overlayClosable: true, body: true});
super('popup-join-chat-invite', {
closable: true,
overlayClosable: true,
body: true,
buttons: addCancelButton([{
langKey: chatInvite.pFlags.request_needed ? 'RequestJoin.Button' : (chatInvite.pFlags.broadcast ? 'JoinByPeekChannelTitle' : 'JoinByPeekGroupTitle'),
callback: () => {
this.managers.appChatsManager.importChatInvite(hash)
.then((chatId) => {
const peerId = chatId.toPeerId(true);
appImManager.setInnerPeer({peerId});
}, (error) => {
if(error.type === 'INVITE_REQUEST_SENT') {
toastNew({langPackKey: 'RequestToJoinSent'});
}
});
}
}])
});
this.construct();
}

49
src/components/popups/mute.ts

@ -9,9 +9,29 @@ import { LangPackKey } from "../../lib/langPack"; @@ -9,9 +9,29 @@ import { LangPackKey } from "../../lib/langPack";
import { MUTE_UNTIL } from "../../lib/mtproto/mtproto_config";
import RadioField from "../radioField";
import Row, { RadioFormFromRows } from "../row";
import { SettingSection } from "../sidebarLeft";
import PopupPeer from "./peer";
const ONE_HOUR = 3600;
const times: {time: number, langKey: LangPackKey}[] = [{
time: ONE_HOUR,
langKey: 'ChatList.Mute.1Hour'
}, {
time: ONE_HOUR * 4,
langKey: 'ChatList.Mute.4Hours'
}, {
time: ONE_HOUR * 8,
langKey: 'ChatList.Mute.8Hours'
}, {
time: ONE_HOUR * 24,
langKey: 'ChatList.Mute.1Day'
}, {
time: ONE_HOUR * 24 * 3,
langKey: 'ChatList.Mute.3Days'
}, {
time: -1,
langKey: 'ChatList.Mute.Forever'
}];
export default class PopupMute extends PopupPeer {
constructor(peerId: PeerId) {
super('popup-mute', {
@ -26,27 +46,6 @@ export default class PopupMute extends PopupPeer { @@ -26,27 +46,6 @@ export default class PopupMute extends PopupPeer {
body: true
});
const ONE_HOUR = 3600;
const times: {time: number, langKey: LangPackKey}[] = [{
time: ONE_HOUR,
langKey: 'ChatList.Mute.1Hour'
}, {
time: ONE_HOUR * 4,
langKey: 'ChatList.Mute.4Hours'
}, {
time: ONE_HOUR * 8,
langKey: 'ChatList.Mute.8Hours'
}, {
time: ONE_HOUR * 24,
langKey: 'ChatList.Mute.1Day'
}, {
time: ONE_HOUR * 24 * 3,
langKey: 'ChatList.Mute.3Days'
}, {
time: -1,
langKey: 'ChatList.Mute.Forever'
}];
const name = 'mute-time';
const rows = times.map((time) => {
const row = new Row({
@ -65,11 +64,9 @@ export default class PopupMute extends PopupPeer { @@ -65,11 +64,9 @@ export default class PopupMute extends PopupPeer {
time = +value;
});
rows[rows.length - 1].radioField.checked = true;
this.body.append(radioForm);
const section = new SettingSection({noShadow: true, noDelimiter: true});
section.content.append(radioForm);
this.body.append(section.container);
rows[rows.length - 1].radioField.checked = true;
this.show();
}

5
src/components/popups/newMedia.ts

@ -66,7 +66,7 @@ export default class PopupNewMedia extends PopupElement { @@ -66,7 +66,7 @@ export default class PopupNewMedia extends PopupElement {
private captionLengthMax: number;
constructor(private chat: Chat, private files: File[], willAttachType: PopupNewMedia['willAttach']['type']) {
super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true, body: true});
super('popup-send-photo popup-new-media', {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true, body: true, title: true});
this.construct(willAttachType);
}
@ -112,7 +112,8 @@ export default class PopupNewMedia extends PopupElement { @@ -112,7 +112,8 @@ export default class PopupNewMedia extends PopupElement {
placeholder: 'PreviewSender.CaptionPlaceholder',
label: 'Caption',
name: 'photo-caption',
maxLength: this.captionLengthMax
maxLength: this.captionLengthMax,
withLinebreaks: true
});
this.input = this.inputField.input;

787
src/components/popups/payment.ts

@ -0,0 +1,787 @@ @@ -0,0 +1,787 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import Currencies from "../../config/currencies";
import { FontFamily, FontSize } from "../../config/font";
import accumulate from "../../helpers/array/accumulate";
import getTextWidth from "../../helpers/canvas/getTextWidth";
import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import findUpAsChild from "../../helpers/dom/findUpAsChild";
import findUpClassName from "../../helpers/dom/findUpClassName";
import loadScript from "../../helpers/dom/loadScript";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
import replaceContent from "../../helpers/dom/replaceContent";
import setInnerHTML from "../../helpers/dom/setInnerHTML";
import toggleDisability from "../../helpers/dom/toggleDisability";
import { formatPhoneNumber } from "../../helpers/formatPhoneNumber";
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
import ScrollSaver from "../../helpers/scrollSaver";
import tsNow from "../../helpers/tsNow";
import { AccountTmpPassword, InputPaymentCredentials, LabeledPrice, Message, MessageMedia, PaymentRequestedInfo, PaymentSavedCredentials, PaymentsPaymentForm, PaymentsPaymentReceipt, PaymentsValidatedRequestedInfo, PostAddress, ShippingOption } from "../../layer";
import I18n, { i18n, LangPackKey, _i18n } from "../../lib/langPack";
import { ApiError } from "../../lib/mtproto/apiManager";
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
import rootScope from "../../lib/rootScope";
import AvatarElement from "../avatar";
import Button from "../button";
import PeerTitle from "../peerTitle";
import { putPreloader } from "../putPreloader";
import Row from "../row";
import { toastNew } from "../toast";
import { wrapPhoto } from "../wrappers";
import wrapPeerTitle from "../wrappers/peerTitle";
import PopupPaymentCard, { PaymentCardDetails, PaymentCardDetailsResult } from "./paymentCard";
import PopupPaymentCardConfirmation from "./paymentCardConfirmation";
import PopupPaymentShipping, { PaymentShippingAddress } from "./paymentShipping";
import PopupPaymentShippingMethods from "./paymentShippingMethods";
import PopupPaymentVerification from "./paymentVerification";
const iconPath = 'assets/img/';
const icons = [
'amex',
'card',
'diners',
'discover',
'jcb',
'mastercard',
'visa',
'unionpay',
'logo',
];
export function getPaymentBrandIconPath(brand: string) {
if(!icons.includes(brand)) {
return;
}
return `${iconPath}${brand}.svg`;
}
export function PaymentButton(options: {
onClick: () => Promise<any> | void,
key?: LangPackKey,
textEl?: I18n.IntlElement
}) {
const textEl = options.textEl ?? new I18n.IntlElement({key: options.key ?? 'PaymentInfo.Done'});
const key = textEl.key;
const payButton = Button('btn-primary btn-color-primary payment-item-pay');
payButton.append(textEl.element);
attachClickEvent(payButton, async() => {
const result = options.onClick();
if(!(result instanceof Promise)) {
return;
}
const d = putPreloader(payButton);
const toggle = toggleDisability([payButton], true);
textEl.compareAndUpdate({key: 'PleaseWait'});
try {
await result;
} catch(err) {
if(!(err as any).handled) {
console.error('payment button error', err);
}
toggle();
textEl.compareAndUpdate({key});
d.remove();
}
});
return payButton;
}
export type PaymentsCredentialsToken = {type: 'card', token?: string, id?: string};
export default class PopupPayment extends PopupElement {
private currency: string;
private tipButtonsMap: Map<number, HTMLElement>;
constructor(private message: Message.message) {
super('popup-payment', {
closable: true,
overlayClosable: true,
body: true,
scrollable: true,
title: true
});
this.tipButtonsMap = new Map();
this.d();
}
private async d() {
this.element.classList.add('is-loading');
this.show();
let confirmed = false;
const onConfirmed = () => {
if(confirmed) {
return;
}
confirmed = true;
if(popupPaymentVerification) {
popupPaymentVerification.hide();
}
this.hide();
showSuccessToast();
};
const showSuccessToast = () => {
toastNew({
langPackKey: 'PaymentInfoHint',
langPackArguments: [
paymentsWrapCurrencyAmount(getTotalTotal(), currency),
wrapEmojiText(mediaInvoice.title)
]
});
};
this.listenerSetter.add(rootScope)('payment_sent', ({peerId, mid}) => {
if(this.message.peerId === peerId && this.message.mid === mid) {
onConfirmed();
}
});
const {message} = this;
const mediaInvoice = message.media as MessageMedia.messageMediaInvoice;
_i18n(this.title, mediaInvoice.receipt_msg_id ? 'PaymentReceipt' : 'PaymentCheckout');
if(mediaInvoice.pFlags.test) {
this.title.append(' (Test)');
}
const className = 'payment-item';
const itemEl = document.createElement('div');
itemEl.classList.add(className);
const detailsClassName = className + '-details';
const details = document.createElement('div');
details.classList.add(detailsClassName);
let photoEl: HTMLElement;
if(mediaInvoice.photo) {
photoEl = document.createElement('div');
photoEl.classList.add(detailsClassName + '-photo', 'media-container-cover');
wrapPhoto({
photo: mediaInvoice.photo,
container: photoEl,
boxWidth: 100,
boxHeight: 100,
size: {_: 'photoSizeEmpty', type: ''}
});
details.append(photoEl);
}
const linesClassName = detailsClassName + '-lines';
const lines = document.createElement('div');
lines.classList.add(linesClassName);
const title = document.createElement('div');
title.classList.add(linesClassName + '-title');
const description = document.createElement('div');
description.classList.add(linesClassName + '-description');
const botName = document.createElement('div');
botName.classList.add(linesClassName + '-bot-name');
lines.append(title, description, botName);
setInnerHTML(title, wrapEmojiText(mediaInvoice.title));
setInnerHTML(description, wrapEmojiText(mediaInvoice.description));
const peerTitle = new PeerTitle();
botName.append(peerTitle.element);
details.append(lines);
itemEl.append(details);
this.scrollable.append(itemEl);
const preloaderContainer = document.createElement('div');
preloaderContainer.classList.add(className + '-preloader-container');
const preloader = putPreloader(preloaderContainer, true);
this.scrollable.container.append(preloaderContainer);
let paymentForm: PaymentsPaymentForm | PaymentsPaymentReceipt;
const isReceipt = !!mediaInvoice.receipt_msg_id;
if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(message.peerId, mediaInvoice.receipt_msg_id);
else paymentForm = await this.managers.appPaymentsManager.getPaymentForm(message.peerId, message.mid);
let savedInfo = (paymentForm as PaymentsPaymentForm).saved_info || (paymentForm as PaymentsPaymentReceipt).info;
const savedCredentials = (paymentForm as PaymentsPaymentForm).saved_credentials;
let [lastRequestedInfo, passwordState, providerPeerTitle] = await Promise.all([
!isReceipt && savedInfo && this.managers.appPaymentsManager.validateRequestedInfo(message.peerId, message.mid, savedInfo),
savedCredentials && this.managers.passwordManager.getState(),
wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()})
]);
console.log(paymentForm, lastRequestedInfo);
await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()});
preloaderContainer.remove();
this.element.classList.remove('is-loading');
const wrapAmount = (amount: string | number, skipSymbol?: boolean) => {
return paymentsWrapCurrencyAmount(amount, currency, skipSymbol);
};
const {invoice} = paymentForm;
const currency = this.currency = invoice.currency;
const makeLabel = () => {
const labelEl = document.createElement('div');
labelEl.classList.add(pricesClassName + '-price');
const left = document.createElement('span');
const right = document.createElement('span');
labelEl.append(left, right);
return {label: labelEl, left, right};
};
const pricesClassName = className + '-prices';
const prices = document.createElement('div');
prices.classList.add(pricesClassName);
const makePricesElements = (prices: LabeledPrice[]) => {
return prices.map((price) => {
const {amount, label} = price;
const _label = makeLabel();
_label.left.textContent = label;
const wrappedAmount = wrapAmount(Math.abs(+amount));
_label.right.textContent = (amount < 0 ? '-' : '') + wrappedAmount;
return _label.label;
});
};
const pricesElements = makePricesElements(invoice.prices);
let getTipsAmount = (): number => 0;
let shippingAmount = 0;
const getTotalTotal = () => totalAmount + getTipsAmount() + shippingAmount;
const setTotal = () => {
const wrapped = wrapAmount(getTotalTotal());
totalLabel.right.textContent = wrapped;
payI18n.compareAndUpdate({
key: 'PaymentCheckoutPay',
args: [wrapped]
});
};
const payI18n = new I18n.IntlElement();
const totalLabel = makeLabel();
totalLabel.label.classList.add('is-total');
_i18n(totalLabel.left, 'PaymentTransactionTotal');
const totalAmount = accumulate(invoice.prices.map(({amount}) => +amount), 0);
const canTip = invoice.max_tip_amount !== undefined;
if(canTip) {
const tipsClassName = className + '-tips';
const currencyData = Currencies[currency];
getTipsAmount = () => +getInputValue().replace(/\D/g, '');
const getInputValue = () => {
// return input.textContent;
return input.value;
};
const setInputWidth = () => {
const width = getTextWidth(getInputValue(), `500 ${FontSize} ${FontFamily}`);
input.style.width = width + 'px';
};
const setInputValue = (amount: string | number) => {
amount = Math.min(+amount, +invoice.max_tip_amount);
const wrapped = wrapAmount(amount, true);
input.value = wrapped;
// input.textContent = wrapped;
if(document.activeElement === input) {
placeCaretAtEnd(input);
}
unsetActiveTip();
const tipEl = this.tipButtonsMap.get(amount);
if(tipEl) {
tipEl.classList.add('active');
}
setInputWidth();
setTotal();
};
const tipsLabel = makeLabel();
_i18n(tipsLabel.left, mediaInvoice.receipt_msg_id ? 'PaymentTip' : 'PaymentTipOptional');
const input = document.createElement('input');
input.type = 'tel';
// const input: HTMLElement = document.createElement('div');
// input.contentEditable = 'true';
input.classList.add('input-clear', tipsClassName + '-input');
tipsLabel.right.append(input);
tipsLabel.label.style.cursor = 'text';
tipsLabel.label.addEventListener('mousedown', (e) => {
if(!findUpAsChild(e.target, input)) {
placeCaretAtEnd(input);
}
});
const haveToIgnoreEvents = input instanceof HTMLInputElement ? 1 : 2;
const onSelectionChange = () => {
if(ignoreNextSelectionChange) {
--ignoreNextSelectionChange;
return;
}
// setTimeout(() => {
ignoreNextSelectionChange = haveToIgnoreEvents;
placeCaretAtEnd(input);
// }, 0);
};
const onFocus = () => {
// cancelEvent(e);
setTimeout(() => {
ignoreNextSelectionChange = haveToIgnoreEvents;
placeCaretAtEnd(input);
document.addEventListener('selectionchange', onSelectionChange);
}, 0);
};
const onFocusOut = () => {
input.addEventListener('focus', onFocus, {once: true});
document.removeEventListener('selectionchange', onSelectionChange);
};
let ignoreNextSelectionChange: number;
input.addEventListener('focusout', onFocusOut);
onFocusOut();
input.addEventListener('input', () => {
setInputValue(getTipsAmount());
});
let s = [currencyData.symbol, currencyData.space_between ? ' ' : ''];
if(!currencyData.symbol_left) s.reverse();
tipsLabel.right[currencyData.symbol_left ? 'prepend' : 'append'](s.join(''));
pricesElements.push(tipsLabel.label);
///
const tipsEl = document.createElement('div');
tipsEl.classList.add(tipsClassName);
const tipClassName = tipsClassName + '-tip';
const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => {
const button = Button(tipClassName, {noRipple: true});
button.textContent = wrapAmount(tipAmount);
this.tipButtonsMap.set(+tipAmount, button);
return button;
});
const unsetActiveTip = () => {
const prevTipEl = tipsEl.querySelector('.active');
if(prevTipEl) {
prevTipEl.classList.remove('active');
}
};
attachClickEvent(tipsEl, (e) => {
const tipEl = findUpClassName(e.target, tipClassName);
if(!tipEl) {
return;
}
let tipAmount = 0;
if(tipEl.classList.contains('active')) {
tipEl.classList.remove('active');
} else {
unsetActiveTip();
tipEl.classList.add('active');
for(const [amount, el] of this.tipButtonsMap) {
if(el === tipEl) {
tipAmount = amount;
break;
}
}
}
setInputValue(tipAmount);
});
setInputValue(0);
tipsEl.append(...tipButtons);
pricesElements.push(tipsEl);
} else {
setTotal();
}
pricesElements.push(totalLabel.label);
prices.append(...pricesElements);
itemEl.append(prices);
///
const setRowIcon = async(row: Row, icon?: string) => {
const img = document.createElement('img');
img.classList.add('media-photo');
await renderImageFromUrlPromise(img, getPaymentBrandIconPath(icon));
let container = row.media;
if(!container) {
container = row.createMedia('small');
container.classList.add('media-container-cover');
container.append(img);
} else {
replaceContent(container, img);
}
};
const createRow = (options: ConstructorParameters<typeof Row>[0]) => {
if(options.titleLangKey) {
options.subtitleLangKey = options.titleLangKey;
}
const row = new Row(options);
row.container.classList.add(className + '-row');
if(options.titleLangKey) {
row.subtitle.classList.add('hide');
}
return row;
};
const setRowTitle = (row: Row, textContent: string) => {
row.title.textContent = textContent;
if(!textContent) {
const e = I18n.weakMap.get(row.subtitle) as I18n.IntlElement;
row.title.append(i18n(e.key));
}
row.subtitle.classList.toggle('hide', !textContent);
};
const setCardSubtitle = (card: PaymentCardDetailsResult) => {
let brand: string;
let str: string;
let icon: string;
if('title' in card) {
brand = card.title.split(' ').shift();
str = card.title;
icon = card.icon;
} else {
brand = detectUnifiedCardBrand(card.cardNumber);
str = brand + ' *' + card.cardNumber.split(' ').pop();
}
methodRow.title.classList.remove('tgico', 'tgico-card_outline');
setRowIcon(methodRow, icon || brand.toLowerCase());
setRowTitle(methodRow, str);
};
const onMethodClick = () => {
new PopupPaymentCard(paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => {
previousToken = token, previousCardDetails = card;
setCardSubtitle(card);
});
};
let previousCardDetails: PaymentCardDetailsResult, previousToken: PaymentsCredentialsToken;
const methodRow = createRow({
titleLangKey: 'PaymentCheckoutMethod',
clickable: isReceipt ? undefined : onMethodClick,
icon: 'card_outline'
});
methodRow.container.classList.add(className + '-method-row');
if(savedCredentials) {
setCardSubtitle(savedCredentials);
} else if((paymentForm as PaymentsPaymentReceipt).credentials_title) {
setCardSubtitle({title: (paymentForm as PaymentsPaymentReceipt).credentials_title});
}
const providerRow = createRow({
title: providerPeerTitle,
subtitleLangKey: 'PaymentCheckoutProvider'
});
const providerAvatar = new AvatarElement();
providerAvatar.classList.add('avatar-32');
providerRow.createMedia('small').append(providerAvatar);
/* await */ providerAvatar.updateWithOptions({peerId: paymentForm.provider_id.toPeerId()});
let shippingAddressRow: Row, shippingNameRow: Row, shippingEmailRow: Row, shippingPhoneRow: Row, shippingMethodRow: Row;
let lastShippingOption: ShippingOption, onShippingAddressClick: (focus?: ConstructorParameters<typeof PopupPaymentShipping>[2]) => void, onShippingMethodClick: () => void;
const setShippingTitle = invoice.pFlags.shipping_address_requested ? (shippingAddress?: PaymentShippingAddress) => {
if(!shippingAddress) {
shippingMethodRow.subtitle.classList.add('hide');
replaceContent(shippingMethodRow.title, i18n('PaymentShippingAddress'));
return;
}
const postAddress = shippingAddress.shipping_address;
setRowTitle(shippingAddressRow, [postAddress.city, postAddress.street_line1, postAddress.street_line2].filter(Boolean).join(', '));
shippingMethodRow.container.classList.remove('hide');
} : undefined;
const setShippingInfo = (info: PaymentRequestedInfo) => {
setShippingTitle && setShippingTitle(info);
shippingNameRow && setRowTitle(shippingNameRow, info.name);
shippingEmailRow && setRowTitle(shippingEmailRow, info.email);
shippingPhoneRow && setRowTitle(shippingPhoneRow, info.phone && ('+' + formatPhoneNumber(info.phone).formatted));
};
if(!isReceipt) {
onShippingAddressClick = (focus) => {
new PopupPaymentShipping(paymentForm as PaymentsPaymentForm, message, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => {
lastRequestedInfo = requestedInfo;
savedInfo = (paymentForm as PaymentsPaymentForm).saved_info = shippingAddress;
setShippingInfo(shippingAddress);
});
};
}
if(invoice.pFlags.shipping_address_requested) {
const setShippingOption = (shippingOption?: ShippingOption) => {
const scrollSaver = new ScrollSaver(this.scrollable, undefined, true);
scrollSaver.save();
if(lastShippingPricesElements) {
lastShippingPricesElements.forEach((node) => node.remove());
}
if(!shippingOption) {
shippingAmount = 0;
setTotal();
scrollSaver.restore();
this.onContentUpdate();
return;
}
lastShippingOption = shippingOption;
setRowTitle(shippingMethodRow, shippingOption.title);
shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0);
lastShippingPricesElements = makePricesElements(shippingOption.prices);
let l = totalLabel.label;
if(canTip) l = l.previousElementSibling.previousElementSibling as any;
lastShippingPricesElements.forEach((element) => l.parentElement.insertBefore(element, l));
setTotal();
scrollSaver.restore();
this.onContentUpdate();
};
shippingAddressRow = createRow({
icon: 'location',
titleLangKey: 'PaymentShippingAddress',
clickable: !isReceipt && onShippingAddressClick.bind(null, undefined)
});
let lastShippingPricesElements: HTMLElement[];
shippingMethodRow = createRow({
icon: 'car',
titleLangKey: 'PaymentCheckoutShippingMethod',
clickable: !isReceipt && (onShippingMethodClick = () => {
new PopupPaymentShippingMethods(paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => {
setShippingOption(shippingOption);
});
})
});
shippingMethodRow.container.classList.add('hide');
const shippingOption = (paymentForm as PaymentsPaymentReceipt).shipping;
if(shippingOption) {
setShippingOption(shippingOption);
}
}
if(invoice.pFlags.name_requested) {
shippingNameRow = createRow({
icon: 'newprivate',
titleLangKey: 'PaymentCheckoutName',
clickable: !isReceipt && onShippingAddressClick.bind(null, 'name')
});
}
if(invoice.pFlags.email_requested) {
shippingEmailRow = createRow({
icon: 'mention',
titleLangKey: 'PaymentShippingEmailPlaceholder',
clickable: !isReceipt && onShippingAddressClick.bind(null, 'email')
});
}
if(invoice.pFlags.phone_requested) {
shippingPhoneRow = createRow({
icon: 'phone',
titleLangKey: 'PaymentCheckoutPhoneNumber',
clickable: !isReceipt && onShippingAddressClick.bind(null, 'phone')
});
}
if(savedInfo) {
setShippingInfo(savedInfo);
}
const rows = [
methodRow,
providerRow,
shippingAddressRow,
shippingMethodRow,
shippingNameRow,
shippingEmailRow,
shippingPhoneRow,
].filter(Boolean);
this.scrollable.append(...[
document.createElement('hr'),
...rows.map((row) => row.container)
].filter(Boolean));
///
let popupPaymentVerification: PopupPaymentVerification, lastTmpPasword: AccountTmpPassword;
const onClick = () => {
const missingInfo = invoice.pFlags.name_requested && !savedInfo?.name ? 'name' : (invoice.pFlags.email_requested && !savedInfo?.email ? 'email' : (invoice.pFlags.phone_requested && !savedInfo?.phone ? 'phone' : undefined));
if(invoice.pFlags.shipping_address_requested) {
if(!lastRequestedInfo) {
onShippingAddressClick();
return;
} else if(!lastShippingOption) {
onShippingMethodClick();
return;
}
} else if(missingInfo) {
onShippingAddressClick(missingInfo);
return;
}
if(!previousCardDetails && !lastTmpPasword) {
if(!savedCredentials) {
onMethodClick();
return;
}
Promise.resolve(passwordState ?? this.managers.passwordManager.getState()).then((_passwordState) => {
new PopupPaymentCardConfirmation(savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => {
passwordState = undefined;
lastTmpPasword = tmpPassword;
simulateClickEvent(payButton);
// * reserve 5 seconds
const diff = tmpPassword.valid_until - tsNow(true) - 5;
setTimeout(() => {
if(lastTmpPasword === tmpPassword) {
lastTmpPasword = undefined;
}
}, diff * 1000);
});
});
return;
}
return Promise.resolve().then(async() => {
const credentials: InputPaymentCredentials = lastTmpPasword ? {
_: 'inputPaymentCredentialsSaved',
id: savedCredentials.id,
tmp_password: lastTmpPasword.tmp_password
} : {
_: 'inputPaymentCredentials',
data: {
_: 'dataJSON',
data: JSON.stringify(previousToken.token ? previousToken : {type: previousToken.type, id: previousToken.id})
},
pFlags: {
save: previousCardDetails.save || undefined
}
};
try {
const paymentResult = await this.managers.appPaymentsManager.sendPaymentForm(
message.peerId,
message.mid,
(paymentForm as PaymentsPaymentForm).form_id,
lastRequestedInfo?.id,
lastShippingOption?.id,
credentials,
getTipsAmount()
);
if(paymentResult._ === 'payments.paymentResult') {
onConfirmed();
} else {
popupPaymentVerification = new PopupPaymentVerification(paymentResult.url);
await new Promise<void>((resolve, reject) => {
popupPaymentVerification.addEventListener('close', () => {
if(confirmed) {
resolve();
} else {
const err = new Error('payment not finished');
(err as ApiError).handled = true;
reject(err);
}
});
});
popupPaymentVerification.addEventListener('finish', () => {
onConfirmed();
});
}
this.hide();
} catch(err) {
if((err as ApiError).type === 'BOT_PRECHECKOUT_TIMEOUT') {
toastNew({langPackKey: 'Error.AnError'});
(err as ApiError).handled = true;
} else if((err as ApiError).type === 'TMP_PASSWORD_INVALID') {
passwordState = lastTmpPasword = undefined;
simulateClickEvent(payButton);
(err as ApiError).handled = true;
}
throw err;
}
});
};
let payButton: HTMLElement;
if(isReceipt) {
payButton = PaymentButton({
onClick: () => this.hide(),
key: 'Done'
});
} else {
payButton = PaymentButton({
onClick: onClick,
textEl: payI18n
});
}
this.body.append(this.btnConfirmOnEnter = payButton);
this.onContentUpdate();
}
}

545
src/components/popups/paymentCard.ts

@ -0,0 +1,545 @@ @@ -0,0 +1,545 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import cardFormattingPatterns from "../../helpers/cards/cardFormattingPatterns";
import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
import formatInputValueByPattern from "../../helpers/cards/formatInputValueByPattern";
import { validateAnyIncomplete, validateCardExpiry, validateCardNumber } from "../../helpers/cards/validateCard";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
import noop from "../../helpers/noop";
import { PaymentsPaymentForm } from "../../layer";
import { LangPackKey, _i18n } from "../../lib/langPack";
import { TelegramWebviewEvent } from "../../types";
import CheckboxField from "../checkboxField";
import confirmationPopup from "../confirmationPopup";
import CountryInputField from "../countryInputField";
import InputField, { InputFieldOptions, InputState } from "../inputField";
import Row from "../row";
import { SettingSection } from "../sidebarLeft";
import { getPaymentBrandIconPath, PaymentButton, PaymentsCredentialsToken } from "./payment";
import { createVerificationIframe } from "./paymentVerification";
export type PaymentCardDetails = {
cardNumber: string;
cardholderName: string;
expiryFull: string;
expiryMonth: string;
expiryYear: string;
cvc: string;
country: string;
zip: string;
save?: boolean;
};
export type PaymentCardDetailsShort = {
title: string,
save?: boolean;
icon?: string;
};
export type PaymentCardDetailsResult = PaymentCardDetails | PaymentCardDetailsShort;
export class InputFieldCorrected extends InputField {
private lastKeyDown: string;
private lastTransformed: ReturnType<typeof formatInputValueByPattern>;
constructor(public options: InputFieldOptions & {
formatMethod: typeof cardFormattingPatterns['cardNumber'],
validateMethod?: typeof validateCardNumber,
errorKeys?: {[code: string]: LangPackKey},
optional?: boolean,
onChange?: (transformed: InputFieldCorrected['lastTransformed']) => void,
onKeyDown?: (e: KeyboardEvent) => void
}) {
super(options);
// const handleIncomplete = (t?: any) => {
// if(
// (!lastTransformed.value && t) ||
// lastTransformed.meta.autocorrectComplete ||
// lastTransformed.meta.error ||
// optional
// ) {
// return;
// }
// };
this.input.addEventListener('keydown', this.onKeyDown);
this.input.addEventListener('input', this.onInput);
this.input.addEventListener('blur', this.onBlur);
}
private onKeyDown = (e: KeyboardEvent) => {
this.lastKeyDown = e.key;
this.options.onKeyDown?.(e);
};
private onInput = () => {
const value = this.value;
const deleting = this.lastKeyDown === 'Backspace' && (((this.lastTransformed && this.lastTransformed.value.length) || 0) - value.length) === 1;
const result = this.lastTransformed = formatInputValueByPattern({
value: value,
getPattern: this.options.formatMethod,
deleting,
input: this.input
});
const transformedValue = result.value;
if(transformedValue !== value) {
this.setValueSilently(transformedValue);
if(result.selection) {
(this.input as HTMLInputElement).selectionStart = result.selection.selectionStart;
(this.input as HTMLInputElement).selectionEnd = result.selection.selectionEnd;
}
}
this.validateNew(transformedValue, {ignoreIncomplete: true/* !result.meta.autocorrectComplete */});
this.options.onChange?.(result);
};
private onBlur = () => {
const value = this.lastTransformed?.value;
if(value) {
this.validateNew(value);
}
};
public update() {
this.onInput();
}
public validate = () => {
return this.validateNew();
};
public validateNew(
value = this.lastTransformed?.value ?? '',
t: any = {},
justReturn?: boolean
) {
let result: ReturnType<InputFieldCorrected['options']['validateMethod']>;
if(this.options.validateMethod) {
result = this.options.validateMethod?.(value, t);
} else {
result = validateAnyIncomplete(this.lastTransformed, value, t);
}
if(result?.code) {
const langPackKey: LangPackKey = this.options.errorKeys?.[result.code];
!justReturn && this.setState(InputState.Error, langPackKey);
return false;
}
!justReturn && this.setState(InputState.Neutral);
return true;
}
}
export function handleInputFieldsOnChange(inputFields: (CountryInputField | InputField | InputFieldCorrected)[], _onChange: (valid: boolean) => void) {
const onChange = () => {
const valid = inputFields.every((inputField) => {
return 'validateNew' in inputField ? inputField.validateNew(undefined, undefined, true) : inputField.isValid();
});
_onChange(valid);
};
inputFields.forEach((inputField) => {
if(inputField instanceof InputFieldCorrected) {
const original = inputField.options.onChange;
inputField.options.onChange = (...args: any[]) => {
// @ts-ignore
original?.(...args);
onChange();
};
if('update' in inputField) {
inputField.update();
}
} else {
inputField.input.addEventListener('input', onChange);
}
});
return {validate: onChange};
}
export function createCountryZipFields(country?: boolean, zip?: boolean) {
let countryInputField: CountryInputField, postcodeInputField: InputFieldCorrected;
if(country || zip) {
if(country) countryInputField = new CountryInputField({
noPhoneCodes: true,
onCountryChange: () => {
postcodeInputField?.update();
},
required: true,
autocomplete: 'country',
});
if(zip) postcodeInputField = new InputFieldCorrected({
label: 'PaymentShippingZipPlaceholder',
plainText: true,
inputMode: 'numeric',
autocomplete: 'postal-code',
formatMethod: (/* ...args */) => {
const {country} = countryInputField.getSelected();
const iso2 = country?.iso2;
return cardFormattingPatterns.postalCodeFromCountry(iso2 && iso2.toUpperCase());
}
});
}
return {countryInputField, postcodeInputField};
}
export type PaymentsNativeProvider = 'stripe' | 'smartglocal';
export type PaymentsNativeParams = {
need_country?: boolean,
need_zip?: boolean,
need_cardholder_name?: boolean,
publishable_key?: string, // stripe
public_token?: string, // smartglocal
gpay_params: string,
};
const SUPPORTED_NATIVE_PROVIDERS: Set<PaymentsNativeProvider> = new Set(['stripe', 'smartglocal']);
export default class PopupPaymentCard extends PopupElement<{
finish: (obj: {token: any, card: PaymentCardDetailsResult}) => void
}> {
constructor(private paymentForm: PaymentsPaymentForm, private savedCard?: PaymentCardDetails) {
super('popup-payment popup-payment-card', {
closable: true,
overlayClosable: true,
body: true,
scrollable: SUPPORTED_NATIVE_PROVIDERS.has(paymentForm.native_provider as PaymentsNativeProvider),
title: 'PaymentCardInfo'
});
if(SUPPORTED_NATIVE_PROVIDERS.has(paymentForm.native_provider as PaymentsNativeProvider)) {
this.d();
} else {
const iframe = createVerificationIframe(paymentForm.url, (event) => {
if(event.eventType !== 'payment_form_submit') {
return;
}
const data = event.eventData;
const cardOut = {title: data.title, save: false} as any as PaymentCardDetails;
this.dispatchEvent('finish', {
token: data.credentials,
card: cardOut
});
this.hide();
if(paymentForm.pFlags.can_save_credentials) {
confirmationPopup({
titleLangKey: 'PaymentCardSavePaymentInformation',
descriptionLangKey: 'PaymentCardSavePaymentInformationInfoLine1',
button: {
langKey: 'Save'
}
}).then(() => {
cardOut.save = true;
}, noop);
}
});
this.body.append(iframe);
this.show();
}
}
private d() {
const savedCard = this.savedCard;
const cardSection = new SettingSection({name: 'PaymentInfo.Card.Title', noDelimiter: true, noShadow: true});
const nativeParams: PaymentsNativeParams = JSON.parse(this.paymentForm.native_params.data);
let lastBrand: string, brandIconTempId = 0, lastBrandImg: HTMLImageElement;
const setBrandIcon = (brand: string) => {
if(lastBrand === brand) {
return;
}
const tempId = ++brandIconTempId;
lastBrand = brand;
const path = getPaymentBrandIconPath(brand);
if(!path) {
if(lastBrandImg) {
lastBrandImg.remove();
lastBrandImg = undefined;
}
return;
}
const img = new Image();
img.classList.add('input-field-icon');
renderImageFromUrlPromise(img, path, false).then(() => {
if(brandIconTempId !== tempId) {
return;
}
if(lastBrandImg) {
lastBrandImg.replaceWith(img);
} else {
cardInputField.container.append(img);
}
lastBrandImg = img;
});
};
const cardInputField = new InputFieldCorrected({
label: 'PaymentCardNumber',
plainText: true,
inputMode: 'numeric',
autocomplete: 'cc-number',
formatMethod: cardFormattingPatterns.cardNumber,
validateMethod: validateCardNumber,
errorKeys: {
invalid: 'PaymentCard.Error.Invalid',
incomplete: 'PaymentCard.Error.Incomplete'
},
onChange: (transformed) => {
setBrandIcon(detectUnifiedCardBrand(transformed.value));
cvcInputField.update(); // format cvc
}
});
let nameInputField: InputField;
if(nativeParams.need_cardholder_name) nameInputField = new InputField({
label: 'Checkout.NewCard.CardholderNamePlaceholder',
maxLength: 255,
required: true,
autocomplete: 'cc-name',
});
const expireInputField = new InputFieldCorrected({
label: 'SecureId.Identity.Placeholder.ExpiryDate',
plainText: true,
inputMode: 'numeric',
autocomplete: 'cc-exp',
formatMethod: cardFormattingPatterns.cardExpiry,
validateMethod: validateCardExpiry
});
const cvcInputField = new InputFieldCorrected({
labelText: 'CVC',
plainText: true,
inputMode: 'numeric',
autocomplete: 'cc-csc',
formatMethod: () => cardFormattingPatterns.cardCvc(cardInputField.value),
// validateMethod: (...args) => _5AH3.a.cardCvc(cardInputField.value)(...args)
});
const switchFocusOrder: (InputFieldCorrected | InputField)[] = [
cardInputField,
expireInputField,
cvcInputField,
nameInputField
].filter(Boolean);
switchFocusOrder.forEach((inputField) => {
const onKeyDown = (e: KeyboardEvent) => {
if(!inputField.value && e.key === 'Backspace') {
const previousInputField = switchFocusOrder[switchFocusOrder.indexOf(inputField) - 1];
if(previousInputField) {
// previousInputField.value = previousInputField.value.slice(0, -1);
placeCaretAtEnd(previousInputField.input);
}
}
};
if(inputField instanceof InputFieldCorrected) {
inputField.options.onKeyDown = onKeyDown;
const original = inputField.options.onChange;
inputField.options.onChange = (transformed) => {
original?.(transformed);
if(document.activeElement === inputField.input && transformed.meta.autocorrectComplete) {
for(let i = switchFocusOrder.indexOf(inputField), length = switchFocusOrder.length; i < length; ++i) {
const nextInputField = switchFocusOrder[i];
if(
nextInputField instanceof InputFieldCorrected ?
!nextInputField.validateNew(undefined, undefined, true) :
!nextInputField.value
) {
placeCaretAtEnd(nextInputField.input);
break;
}
}
}
};
} else {
inputField.input.addEventListener('keydown', onKeyDown);
}
});
const inputFieldsRow = document.createElement('div');
inputFieldsRow.classList.add('input-fields-row');
inputFieldsRow.append(expireInputField.container, cvcInputField.container);
cardSection.content.append(...[
cardInputField.container,
inputFieldsRow,
nameInputField?.container
].filter(Boolean));
let billingSection: SettingSection;
let saveCheckboxField: CheckboxField;
const {countryInputField, postcodeInputField} = createCountryZipFields(nativeParams.need_country, nativeParams.need_zip);
if(nativeParams.need_country || nativeParams.need_zip) {
billingSection = new SettingSection({name: 'PaymentInfo.Billing.Title', noDelimiter: true, noShadow: true});
// const inputFieldsRow2 = inputFieldsRow.cloneNode() as HTMLElement;
// inputFieldsRow2.append(countryInputField.container, postcodeInputField.container);
// billingSection.content.append(inputFieldsRow2);
billingSection.content.append(...[countryInputField, postcodeInputField].filter(Boolean).map((i) => i.container));
}
const canSave = !!this.paymentForm.pFlags.can_save_credentials;
saveCheckboxField = new CheckboxField({
text: 'PaymentCardSavePaymentInformation',
checked: !!canSave
});
const saveRow = new Row({
checkboxField: saveCheckboxField,
subtitleLangKey: canSave ? 'PaymentCardSavePaymentInformationInfoLine1' : 'Checkout.2FA.Text',
noCheckboxSubtitle: true
});
if(!canSave) {
saveRow.container.classList.add('is-disabled');
}
(billingSection || cardSection).content.append(saveRow.container);
this.scrollable.append(...[cardSection, billingSection].filter(Boolean).map((s) => s.container));
const payButton = PaymentButton({
key: 'PaymentInfo.Done',
onClick: async() => {
const data: PaymentCardDetails = {
cardNumber: cardInputField.value,
expiryFull: expireInputField.value,
expiryMonth: expireInputField.value.split('/')[0],
expiryYear: expireInputField.value.split('/')[1],
cvc: cvcInputField.value,
cardholderName: nameInputField?.value,
country: countryInputField?.value,
zip: postcodeInputField?.value,
save: saveCheckboxField?.checked
};
const nativeProvider: PaymentsNativeProvider = this.paymentForm.native_provider as any;
let out: PaymentsCredentialsToken;
if(nativeProvider === 'stripe') {
const url = new URL('https://api.stripe.com/v1/tokens');
url.search = new URLSearchParams({
'card[number]': data.cardNumber,
'card[exp_month]': data.expiryMonth,
'card[exp_year]': data.expiryYear,
'card[cvc]': data.cvc,
'card[address_zip]': data.zip,
'card[address_country]': data.country,
'card[name]': data.cardholderName,
}).toString();
const response = await fetch(url.toString(), {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${nativeParams.publishable_key}`,
},
});
out = await response.json();
} else if(nativeProvider === 'smartglocal') {
const params = {
card: {
number: data.cardNumber.replace(/[^\d]+/g, ''),
expiration_month: data.expiryMonth,
expiration_year: data.expiryYear,
security_code: data.cvc.replace(/[^\d]+/g, ''),
},
};
const url = /* DEBUG_PAYMENT_SMART_GLOCAL */false
? 'https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card'
: 'https://tgb.smart-glocal.com/cds/v1/tokenize/card';
const response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-PUBLIC-TOKEN': nativeParams.public_token,
},
body: JSON.stringify(params),
});
const json: { // smartglocal
data: {
info: {
card_network: string,
card_type: string,
masked_card_number: string
},
token: string
},
status: 'ok'
} = await response.json();
out = {type: 'card', token: json.data.token}
}
this.dispatchEvent('finish', {token: out, card: data});
this.hide();
}
});
const inputFields = ([
cardInputField,
nameInputField,
expireInputField,
cvcInputField,
countryInputField,
postcodeInputField
] as const).filter(Boolean);
handleInputFieldsOnChange(inputFields, (valid) => {
payButton.disabled = !valid;
// payButton.classList.toggle('btn-disabled', !valid);
});
if(savedCard) {
cardInputField.value = savedCard.cardNumber;
expireInputField.value = savedCard.expiryFull;
cvcInputField.value = savedCard.cvc;
nameInputField && (nameInputField.value = savedCard.cardholderName);
countryInputField && (countryInputField.value = savedCard.country);
postcodeInputField && (postcodeInputField.value = savedCard.zip);
}
this.body.append(this.btnConfirmOnEnter = payButton);
this.show();
if(!cardInputField.validateNew(undefined, undefined, true)) {
placeCaretAtEnd(cardInputField.input);
}
}
}

70
src/components/popups/paymentCardConfirmation.ts

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import { AccountPassword, AccountTmpPassword } from "../../layer";
import { ApiError } from "../../lib/mtproto/apiManager";
import { InputState } from "../inputField";
import PasswordInputField from "../passwordInputField";
import { SettingSection } from "../sidebarLeft";
import { PaymentButton } from "./payment";
export default class PopupPaymentCardConfirmation extends PopupElement<{
finish: (tmpPassword: AccountTmpPassword) => void
}> {
constructor(card: string, passwordState: AccountPassword) {
super('popup-payment popup-payment-card-confirmation', {
closable: true,
overlayClosable: true,
body: true,
scrollable: true,
title: 'Checkout.PasswordEntry.Title'
});
const section = new SettingSection({noDelimiter: true, noShadow: true, caption: 'Checkout.PasswordEntry.Text', captionArgs: [card]});
const passwordInputField = new PasswordInputField({labelText: passwordState.hint});
section.content.append(passwordInputField.container);
this.scrollable.append(section.container);
const onInput = () => {
payButton.disabled = !passwordInputField.value;
passwordInputField.setState(InputState.Neutral);
};
passwordInputField.input.addEventListener('input', onInput);
const payButton = PaymentButton({
key: 'Checkout.PasswordEntry.Pay',
onClick: async() => {
try {
const inputCheckPassword = await this.managers.passwordManager.getInputCheckPassword(passwordInputField.value, passwordState);
const tmpPassword = await this.managers.apiManager.invokeApi('account.getTmpPassword', {
password: inputCheckPassword,
period: 60
});
this.dispatchEvent('finish', tmpPassword);
this.hide();
} catch(err) {
if((err as ApiError).type === 'PASSWORD_HASH_INVALID') {
(err as ApiError).handled = true;
passwordInputField.setError('PASSWORD_HASH_INVALID');
}
throw err;
}
}
});
this.body.append(this.btnConfirmOnEnter = payButton);
onInput();
this.show();
placeCaretAtEnd(passwordInputField.input);
}
}

231
src/components/popups/paymentShipping.ts

@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import toggleDisability from "../../helpers/dom/toggleDisability";
import { Message, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
import getServerMessageId from "../../lib/appManagers/utils/messageId/getServerMessageId";
import { ApiError } from "../../lib/mtproto/apiManager";
import matchEmail from "../../lib/richTextProcessor/matchEmail";
import Button from "../button";
import CheckboxField from "../checkboxField";
import CountryInputField from "../countryInputField";
import InputField from "../inputField";
import Row from "../row";
import { SettingSection } from "../sidebarLeft";
import TelInputField from "../telInputField";
import { PaymentButton } from "./payment";
import { createCountryZipFields, handleInputFieldsOnChange, InputFieldCorrected } from "./paymentCard";
export type PaymentShippingAddress = PaymentRequestedInfo;
type ShippingFocusField = 'name' | 'email' | 'phone';
export default class PopupPaymentShipping extends PopupElement<{
finish: (o: {shippingAddress: PaymentShippingAddress, requestedInfo: PaymentsValidatedRequestedInfo}) => void
}> {
constructor(
private paymentForm: PaymentsPaymentForm,
private message: Message.message,
private focus?: ShippingFocusField
) {
super('popup-payment popup-payment-shipping', {
closable: true,
overlayClosable: true,
body: true,
scrollable: true,
title: 'PaymentShippingInfo'
});
this.d();
}
private d() {
const paymentForm = this.paymentForm;
const invoice = paymentForm.invoice;
const savedInfo = this.paymentForm.saved_info;
let addressSection: SettingSection,
address1InputField: InputField,
address2InputField: InputField,
cityInputField: InputField,
stateInputField: InputField,
countryInputField: CountryInputField,
postcodeInputField: InputFieldCorrected;
if(invoice.pFlags.shipping_address_requested) {
addressSection = new SettingSection({name: 'PaymentShippingAddress', noDelimiter: true, noShadow: true});
address1InputField = new InputField({label: 'PaymentShippingAddress1Placeholder', maxLength: 64, required: true});
address2InputField = new InputField({label: 'PaymentShippingAddress2Placeholder', maxLength: 64});
cityInputField = new InputField({label: 'PaymentShippingCityPlaceholder', maxLength: 64, required: true});
stateInputField = new InputField({label: 'PaymentShippingStatePlaceholder', maxLength: 64});
const res = createCountryZipFields(true, true);
countryInputField = res.countryInputField;
postcodeInputField = res.postcodeInputField;
addressSection.content.append(...[
address1InputField,
address2InputField,
cityInputField,
stateInputField,
countryInputField,
postcodeInputField
].filter(Boolean).map((inputField) => inputField.container));
}
let receiverSection: SettingSection;
let nameInputField: InputField, emailInputField: InputField, telInputField: TelInputField;
if([invoice.pFlags.name_requested, invoice.pFlags.email_requested, invoice.pFlags.phone_requested].includes(true)) {
receiverSection = new SettingSection({name: 'PaymentShippingReceiver', noDelimiter: true, noShadow: true});
const validateEmail = () => {
const value = emailInputField.value;
const match = matchEmail(value);
if(!match || match[0].length !== value.length) {
return false;
}
return true;
};
const validatePhone = () => {
return !!telInputField.value.match(/\d/);
};
if(invoice.pFlags.name_requested) nameInputField = new InputField({label: 'PaymentShippingName', maxLength: 256, required: true});
if(invoice.pFlags.email_requested) emailInputField = new InputField({label: 'PaymentShippingEmailPlaceholder', maxLength: 64, required: true, validate: validateEmail});
if(invoice.pFlags.phone_requested) telInputField = new TelInputField({required: true, validate: validatePhone});
receiverSection.content.append(...[
nameInputField,
emailInputField,
telInputField,
].filter(Boolean).map((inputField) => inputField.container));
}
const saveCheckboxField = new CheckboxField({
text: 'PaymentShippingSave',
checked: true
});
const saveRow = new Row({
checkboxField: saveCheckboxField,
subtitleLangKey: 'PaymentShippingSaveInfo',
noCheckboxSubtitle: true
});
(receiverSection || addressSection).content.append(saveRow.container);
this.scrollable.append(...[addressSection, receiverSection].filter(Boolean).map((section) => section.container));
const payButton = PaymentButton({
key: 'PaymentInfo.Done',
onClick: async() => {
const selectedCountry = countryInputField && countryInputField.getSelected().country;
const data: PaymentShippingAddress = {
_: 'paymentRequestedInfo',
shipping_address: selectedCountry && {
_: 'postAddress',
street_line1: address1InputField.value,
street_line2: address2InputField.value,
city: cityInputField.value,
state: stateInputField.value,
// country: countryInputField.value,
country_iso2: selectedCountry?.iso2,
post_code: postcodeInputField.value,
},
name: nameInputField?.value,
email: emailInputField?.value,
phone: telInputField?.value
};
try {
const requestedInfo = await this.managers.appPaymentsManager.validateRequestedInfo(this.message.peerId, this.message.mid, data, saveCheckboxField?.checked);
this.dispatchEvent('finish', {
shippingAddress: data,
requestedInfo
});
this.hide();
} catch(err: any) {
const errorMap: {[err: string]: InputField} = {
ADDRESS_STREET_LINE1_INVALID: address1InputField,
ADDRESS_STREET_LINE2_INVALID: address2InputField,
ADDRESS_COUNTRY_INVALID: countryInputField,
ADDRESS_CITY_INVALID: cityInputField,
ADDRESS_STATE_INVALID: stateInputField,
ADDRESS_POSTCODE_INVALID: postcodeInputField,
REQ_INFO_NAME_INVALID: nameInputField,
REQ_INFO_EMAIL_INVALID: emailInputField,
REQ_INFO_PHONE_INVALID: telInputField
};
const inputField = errorMap[(err as ApiError).type];
if(inputField) {
inputField.setError();
(err as any).handled = true;
}
throw err;
}
}
});
this.body.append(this.btnConfirmOnEnter = payButton);
if(savedInfo) {
const shippingAddress = savedInfo.shipping_address;
if(shippingAddress) {
address1InputField.value = shippingAddress.street_line1;
address2InputField.value = shippingAddress.street_line2;
cityInputField.value = shippingAddress.city;
stateInputField.value = shippingAddress.state;
countryInputField.selectCountryByIso2(shippingAddress.country_iso2);
postcodeInputField.value = shippingAddress.post_code;
}
savedInfo.name && nameInputField && (nameInputField.value = savedInfo.name);
savedInfo.email && emailInputField && (emailInputField.value = savedInfo.email);
savedInfo.phone && telInputField && (telInputField.value = savedInfo.phone);
}
const {validate} = handleInputFieldsOnChange([
address1InputField,
address2InputField,
cityInputField,
stateInputField,
countryInputField,
postcodeInputField,
nameInputField,
emailInputField,
telInputField
].filter(Boolean), (valid) => {
payButton.disabled = !valid;
});
validate();
this.show();
let focusField: InputField;
if(this.focus) {
const focusMap: {[field in ShippingFocusField]?: InputField} = {
name: nameInputField,
email: emailInputField,
phone: telInputField
};
focusField = focusMap[this.focus];
} else {
focusField = address1InputField;
}
if(focusField) {
placeCaretAtEnd(focusField.input);
}
}
}

80
src/components/popups/paymentShippingMethods.ts

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import accumulate from "../../helpers/array/accumulate";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
import { PaymentsPaymentForm, PaymentsValidatedRequestedInfo, ShippingOption } from "../../layer";
import Button from "../button";
import RadioField from "../radioField";
import Row, { RadioFormFromRows } from "../row";
import { SettingSection } from "../sidebarLeft";
import { PaymentButton } from "./payment";
export default class PopupPaymentShippingMethods extends PopupElement<{
finish: (shippingOption: ShippingOption) => void
}> {
constructor(
private paymentForm: PaymentsPaymentForm,
private requestedInfo: PaymentsValidatedRequestedInfo,
private shippingOption: ShippingOption
) {
super('popup-payment popup-payment-shipping-methods', {
closable: true,
overlayClosable: true,
body: true,
scrollable: true,
title: 'PaymentShippingMethod'
});
this.d();
}
private d() {
const section = new SettingSection({name: 'PaymentCheckoutShippingMethod', noDelimiter: true, noShadow: true});
const rows = this.requestedInfo.shipping_options.map((shippingOption) => {
return new Row({
radioField: new RadioField({
text: shippingOption.title,
name: 'shipping-method',
value: shippingOption.id
}),
subtitle: paymentsWrapCurrencyAmount(
accumulate(shippingOption.prices.map(({amount}) => +amount), 0),
this.paymentForm.invoice.currency
)
});
});
let lastShippingId: string;
const form = RadioFormFromRows(rows, (value) => {
lastShippingId = value;
});
if(this.shippingOption) {
rows.find((row) => row.radioField.input.value === this.shippingOption.id).radioField.checked = true;
} else {
rows[0].radioField.checked = true;
}
section.content.append(form);
this.scrollable.append(section.container);
const payButton = PaymentButton({
key: 'PaymentInfo.Done',
onClick: () => {
this.dispatchEvent('finish', this.requestedInfo.shipping_options.find((option) => option.id === lastShippingId));
this.hide();
}
});
this.body.append(this.btnConfirmOnEnter = payButton);
this.show();
}
}

64
src/components/popups/paymentVerification.ts

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PopupElement from ".";
import appImManager from "../../lib/appManagers/appImManager";
import { TelegramWebviewEventCallback } from "../../types";
const weakMap: WeakMap<Window, TelegramWebviewEventCallback> = new WeakMap();
window.addEventListener('message', (e) => {
const callback = weakMap.get(e.source as Window);
if(!callback) {
return;
}
callback(JSON.parse(e.data));
});
export function createVerificationIframe(url: string, callback: TelegramWebviewEventCallback) {
const iframe = document.createElement('iframe');
// iframe.title = 'Complete Payment';
iframe.allow = 'payment';
iframe.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin allow-top-navigation allow-modals');
iframe.classList.add('payment-verification');
iframe.src = url;
iframe.addEventListener('load', () => {
weakMap.set(iframe.contentWindow, callback);
}, {once: true});
return iframe;
}
export default class PopupPaymentVerification extends PopupElement<{
finish: () => void
}> {
constructor(private url: string) {
super('popup-payment popup-payment-verification', {
closable: true,
overlayClosable: true,
body: true,
title: 'Checkout.WebConfirmation.Title'
});
this.d();
}
private d() {
const iframe = createVerificationIframe(this.url, (event) => {
if(event.eventType !== 'web_app_open_tg_link') {
return;
}
this.dispatchEvent('finish');
this.hide();
appImManager.openUrl('https://t.me' + event.eventData.path_full);
});
this.body.append(iframe);
this.show();
}
}

23
src/components/popups/peer.ts

@ -15,23 +15,28 @@ export type PopupPeerButtonCallbackCheckboxes = Set<LangPackKey>; @@ -15,23 +15,28 @@ export type PopupPeerButtonCallbackCheckboxes = Set<LangPackKey>;
export type PopupPeerButtonCallback = (checkboxes?: PopupPeerButtonCallbackCheckboxes) => void;
export type PopupPeerCheckboxOptions = CheckboxFieldOptions & {checkboxField?: CheckboxField};
export type PopupPeerOptions = PopupOptions & Partial<{
export type PopupPeerOptions = Omit<PopupOptions, 'buttons' | 'title'> & Partial<{
peerId: PeerId,
title: string | HTMLElement,
titleLangKey?: LangPackKey,
titleLangArgs?: any[],
noTitle?: boolean,
titleLangKey: LangPackKey,
titleLangArgs: any[],
noTitle: boolean,
description: string | DocumentFragment,
descriptionLangKey?: LangPackKey,
descriptionLangArgs?: any[],
buttons?: Array<PopupPeerButton>,
descriptionLangKey: LangPackKey,
descriptionLangArgs: any[],
buttons: Array<PopupPeerButton>,
checkboxes: Array<PopupPeerCheckboxOptions>
}>;
export default class PopupPeer extends PopupElement {
protected description: HTMLParagraphElement;
constructor(private className: string, options: PopupPeerOptions = {}) {
super('popup-peer' + (className ? ' ' + className : ''), options.buttons && addCancelButton(options.buttons), {overlayClosable: true, ...options});
super('popup-peer' + (className ? ' ' + className : ''), {
overlayClosable: true,
...options,
title: true,
buttons: options.buttons && addCancelButton(options.buttons),
});
if(options.peerId) {
const avatarEl = new AvatarElement();
@ -65,7 +70,7 @@ export default class PopupPeer extends PopupElement { @@ -65,7 +70,7 @@ export default class PopupPeer extends PopupElement {
this.container.classList.add('have-checkbox');
options.checkboxes.forEach((o) => {
o.withRipple = false;
o.withRipple = true;
const checkboxField = new CheckboxField(o);
o.checkboxField = checkboxField;
fragment.append(checkboxField.label);

2
src/components/popups/pickUser.ts

@ -20,7 +20,7 @@ export default class PopupPickUser extends PopupElement { @@ -20,7 +20,7 @@ export default class PopupPickUser extends PopupElement {
peerId?: number,
selfPresence?: LangPackKey
}) {
super('popup-forward', null, {closable: true, overlayClosable: true, body: true});
super('popup-forward', {closable: true, overlayClosable: true, body: true, title: true});
this.selector = new AppSelectPeers({
appendTo: this.body,

5
src/components/popups/reactedList.ts

@ -21,10 +21,7 @@ export default class PopupReactedList extends PopupElement { @@ -21,10 +21,7 @@ export default class PopupReactedList extends PopupElement {
constructor(
private message: Message.message
) {
super('popup-reacted-list', /* [{
langKey: 'Close',
isCancel: true
}] */null, {closable: true, overlayClosable: true, body: true});
super('popup-reacted-list', {closable: true, overlayClosable: true, body: true});
this.init();
}

3
src/components/popups/schedule.ts

@ -38,7 +38,8 @@ export default class PopupSchedule extends PopupDatePicker { @@ -38,7 +38,8 @@ export default class PopupSchedule extends PopupDatePicker {
maxDate: getMaxDate(),
withTime: true,
showOverflowMonths: true,
confirmShortcutIsSendShortcut: true
confirmShortcutIsSendShortcut: true,
title: true
});
this.element.classList.add('popup-schedule');

15
src/components/popups/sponsored.ts

@ -5,7 +5,6 @@ @@ -5,7 +5,6 @@
*/
import I18n, { i18n } from "../../lib/langPack";
import Scrollable from "../scrollable";
import PopupPeer from "./peer";
export default class PopupSponsored extends PopupPeer {
@ -23,19 +22,11 @@ export default class PopupSponsored extends PopupPeer { @@ -23,19 +22,11 @@ export default class PopupSponsored extends PopupPeer {
window.open(I18n.format('Chat.Message.Sponsored.Link', true));
},
isCancel: true
}]
}],
scrollable: true
});
const scrollable = new Scrollable(undefined);
scrollable.onAdditionalScroll = () => {
scrollable.container.classList.toggle('scrolled-top', !scrollable.scrollTop);
scrollable.container.classList.toggle('scrolled-bottom', scrollable.isScrolledDown);
};
this.description.replaceWith(scrollable.container);
scrollable.container.append(this.description);
scrollable.container.classList.add('scrolled-top');
this.scrollable.append(this.description);
this.show();
}

57
src/components/popups/stickers.ts

@ -6,13 +6,11 @@ @@ -6,13 +6,11 @@
import PopupElement from ".";
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
import Scrollable from "../scrollable";
import { wrapSticker } from "../wrappers";
import LazyLoadQueue from "../lazyLoadQueue";
import { putPreloader } from "../putPreloader";
import animationIntersector from "../animationIntersector";
import appImManager from "../../lib/appManagers/appImManager";
import { StickerSet } from "../../layer";
import mediaSizes from "../../helpers/mediaSizes";
import { i18n } from "../../lib/langPack";
import Button from "../button";
@ -28,17 +26,11 @@ const ANIMATION_GROUP = 'STICKERS-POPUP'; @@ -28,17 +26,11 @@ const ANIMATION_GROUP = 'STICKERS-POPUP';
export default class PopupStickers extends PopupElement {
private stickersFooter: HTMLElement;
private stickersDiv: HTMLElement;
private h6: HTMLElement;
private set: StickerSet.stickerSet;
constructor(private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0]) {
super('popup-stickers', null, {closable: true, overlayClosable: true, body: true});
this.h6 = document.createElement('h6');
this.h6.append(i18n('Loading'));
super('popup-stickers', {closable: true, overlayClosable: true, body: true, scrollable: true, title: true});
this.header.append(this.h6);
this.title.append(i18n('Loading'));
this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup('');
@ -62,8 +54,7 @@ export default class PopupStickers extends PopupElement { @@ -62,8 +54,7 @@ export default class PopupStickers extends PopupElement {
const btn = Button('btn-primary btn-primary-transparent disable-hover', {noRipple: true, text: 'Loading'});
this.stickersFooter.append(btn);
this.body.append(div);
const scrollable = new Scrollable(this.body);
this.scrollable.append(div);
this.body.append(this.stickersFooter);
// const editButton = document.createElement('button');
@ -87,37 +78,29 @@ export default class PopupStickers extends PopupElement { @@ -87,37 +78,29 @@ export default class PopupStickers extends PopupElement {
};
private loadStickerSet() {
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then((set) => {
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then(async(set) => {
if(!set) {
toastNew({langPackKey: 'StickerSet.DontExist'});
this.hide();
return;
}
//console.log('PopupStickers loadStickerSet got set:', set);
this.set = set.set;
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
setInnerHTML(this.h6, wrapEmojiText(set.set.title));
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
let button: HTMLElement;
const s = i18n('Stickers', [set.set.count]);
if(set.set.installed_date) {
button = Button('btn-primary btn-primary-transparent danger', {noRipple: true});
button.append(i18n('RemoveStickersCount', [i18n('Stickers', [set.set.count])]));
button.append(i18n('RemoveStickersCount', [s]));
} else {
button = Button('btn-primary btn-color-primary', {noRipple: true});
button.append(i18n('AddStickersCount', [i18n('Stickers', [set.set.count])]));
button.append(i18n('AddStickersCount', [s]));
}
this.stickersFooter.textContent = '';
this.stickersFooter.append(button);
attachClickEvent(button, () => {
const toggle = toggleDisability([button], true);
this.managers.appStickersManager.toggleStickerSet(this.set).then(() => {
this.managers.appStickersManager.toggleStickerSet(set.set).then(() => {
this.hide();
}).catch(() => {
toggle();
@ -125,12 +108,9 @@ export default class PopupStickers extends PopupElement { @@ -125,12 +108,9 @@ export default class PopupStickers extends PopupElement {
});
const lazyLoadQueue = new LazyLoadQueue();
this.stickersDiv.classList.remove('is-loading');
this.stickersDiv.innerHTML = '';
for(let doc of set.documents) {
const divs = await Promise.all(set.documents.map(async(doc) => {
if(doc._ === 'documentEmpty') {
continue;
return;
}
const div = document.createElement('div');
@ -138,7 +118,7 @@ export default class PopupStickers extends PopupElement { @@ -138,7 +118,7 @@ export default class PopupStickers extends PopupElement {
const size = mediaSizes.active.esgSticker.width;
wrapSticker({
await wrapSticker({
doc,
div,
lazyLoadQueue,
@ -149,8 +129,19 @@ export default class PopupStickers extends PopupElement { @@ -149,8 +129,19 @@ export default class PopupStickers extends PopupElement {
height: size
});
this.stickersDiv.append(div);
}
return div;
}));
setInnerHTML(this.title, wrapEmojiText(set.set.title));
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
this.stickersFooter.textContent = '';
this.stickersFooter.append(button);
this.stickersDiv.classList.remove('is-loading');
this.stickersDiv.innerHTML = '';
this.stickersDiv.append(...divs.filter(Boolean));
this.scrollable.onAdditionalScroll();
});
}
}

18
src/components/sidebarLeft/index.ts

@ -664,6 +664,8 @@ export type SettingSectionOptions = { @@ -664,6 +664,8 @@ export type SettingSectionOptions = {
name?: LangPackKey,
nameArgs?: FormatterArguments,
caption?: LangPackKey | true,
captionArgs?: FormatterArguments,
captionOld?: SettingSectionOptions['caption'],
noDelimiter?: boolean,
fakeGradientDelimiter?: boolean,
noShadow?: boolean,
@ -721,13 +723,17 @@ export class SettingSection { @@ -721,13 +723,17 @@ export class SettingSection {
container.append(innerContainer);
if(options.caption) {
const caption = this.caption = this.generateContentElement();
caption.classList.add(className + '-caption');
container.append(caption);
const caption = options.caption ?? options.captionOld;
if(caption) {
const el = this.caption = this.generateContentElement();
el.classList.add(className + '-caption');
if(options.caption !== true) {
i18n_({element: caption, key: options.caption});
if(!options.captionOld) {
container.append(el);
}
if(caption !== true) {
i18n_({element: el, key: caption, args: options.captionArgs});
}
}
}

2
src/components/sidebarLeft/tabs/2fa/email.ts

@ -32,7 +32,7 @@ export default class AppTwoStepVerificationEmailTab extends SliderSuperTab { @@ -32,7 +32,7 @@ export default class AppTwoStepVerificationEmailTab extends SliderSuperTab {
this.setTitle('RecoveryEmailTitle');
const section = new SettingSection({
caption: true,
captionOld: true,
noDelimiter: true
});

2
src/components/sidebarLeft/tabs/2fa/emailConfirmation.ts

@ -31,7 +31,7 @@ export default class AppTwoStepVerificationEmailConfirmationTab extends SliderSu @@ -31,7 +31,7 @@ export default class AppTwoStepVerificationEmailConfirmationTab extends SliderSu
this.setTitle('TwoStepAuth.RecoveryTitle');
const section = new SettingSection({
caption: true,
captionOld: true,
noDelimiter: true
});

2
src/components/sidebarLeft/tabs/2fa/enterPassword.ts

@ -130,7 +130,7 @@ export default class AppTwoStepVerificationEnterPasswordTab extends SliderSuperT @@ -130,7 +130,7 @@ export default class AppTwoStepVerificationEnterPasswordTab extends SliderSuperT
switch(err.type) {
default:
//btnContinue.innerText = err.type;
textEl.key = 'TwoStepAuth.InvalidPassword';
textEl.key = 'PASSWORD_HASH_INVALID';
textEl.update();
preloader.remove();
passwordInputField.select();

2
src/components/sidebarLeft/tabs/2fa/index.ts

@ -25,7 +25,7 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab { @@ -25,7 +25,7 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab {
this.setTitle('TwoStepVerificationTitle');
const section = new SettingSection({
caption: true,
captionOld: true,
noDelimiter: true
});

2
src/components/sidebarLeft/tabs/2fa/passwordSet.ts

@ -17,7 +17,7 @@ export default class AppTwoStepVerificationSetTab extends SliderSuperTab { @@ -17,7 +17,7 @@ export default class AppTwoStepVerificationSetTab extends SliderSuperTab {
this.setTitle('TwoStepVerificationPasswordSet');
const section = new SettingSection({
caption: 'TwoStepVerificationPasswordSetInfo',
captionOld: 'TwoStepVerificationPasswordSetInfo',
noDelimiter: true
});

45
src/components/sidebarLeft/tabs/privacyAndSecurity.ts

@ -29,6 +29,9 @@ import toggleDisability from "../../../helpers/dom/toggleDisability"; @@ -29,6 +29,9 @@ import toggleDisability from "../../../helpers/dom/toggleDisability";
import convertKeyToInputKey from "../../../helpers/string/convertKeyToInputKey";
import getPrivacyRulesDetails from "../../../lib/appManagers/utils/privacy/getPrivacyRulesDetails";
import PrivacyType from "../../../lib/appManagers/utils/privacy/privacyType";
import confirmationPopup, { PopupConfirmationOptions } from "../../confirmationPopup";
import noop from "../../../helpers/noop";
import { toastNew } from "../../toast";
export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
private activeSessionsRow: Row;
@ -315,6 +318,48 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { @@ -315,6 +318,48 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
this.scrollable.append(section.container);
}
{
const section = new SettingSection({name: 'PrivacyPayments', caption: 'PrivacyPaymentsClearInfo'});
const onClearClick = () => {
const options: PopupConfirmationOptions = {
titleLangKey: 'PrivacyPaymentsClearAlertTitle',
descriptionLangKey: 'PrivacyPaymentsClearAlertText',
button: {
langKey: 'Clear'
},
checkboxes: [{
text: 'PrivacyClearShipping',
checked: true
}, {
text: 'PrivacyClearPayment',
checked: true
}]
};
confirmationPopup(options).then(() => {
const [info, payment] = options.checkboxes.map((c) => c.checkboxField.checked);
const toggle = toggleDisability([clearButton], true);
this.managers.appPaymentsManager.clearSavedInfo(info, payment).then(() => {
if(!info && !payment) {
return;
}
toggle();
toastNew({
langPackKey: info && payment ? 'PrivacyPaymentsPaymentShippingCleared' : (info ? 'PrivacyPaymentsShippingInfoCleared' : 'PrivacyPaymentsPaymentInfoCleared')
});
});
}, noop);
};
const clearButton = Button('btn-primary btn-transparent', {icon: 'delete', text: 'PrivacyPaymentsClear'});
this.listenerSetter.add(clearButton)('click', onClearClick);
section.content.append(clearButton);
this.scrollable.append(section.container);
}
return Promise.all(promises);
}

2
src/components/usernameInputField.ts

@ -37,7 +37,7 @@ export class UsernameInputField extends InputField { @@ -37,7 +37,7 @@ export class UsernameInputField extends InputField {
//console.log('userNameInput:', value);
if(value === this.originalValue || !value.length) {
this.setState(InputState.Neutral, this.options.label);
this.setState(InputState.Neutral);
this.options.onChange && this.options.onChange();
return;
} else if(!isUsernameValid(value)) { // does not check the last underscore

57
src/components/wrappers/messageActionTextNewUnsafe.ts

@ -9,8 +9,10 @@ import { formatTime } from "../../helpers/date"; @@ -9,8 +9,10 @@ import { formatTime } from "../../helpers/date";
import htmlToSpan from "../../helpers/dom/htmlToSpan";
import setInnerHTML from "../../helpers/dom/setInnerHTML";
import formatCallDuration from "../../helpers/formatCallDuration";
import { MessageAction } from "../../layer";
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
import { Message, MessageAction } from "../../layer";
import { MyMessage } from "../../lib/appManagers/appMessagesManager";
import getPeerId from "../../lib/appManagers/utils/peers/getPeerId";
import I18n, { FormatterArgument, FormatterArguments, i18n, join, langPack, LangPackKey, _i18n } from "../../lib/langPack";
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
import wrapPlainText from "../../lib/richTextProcessor/wrapPlainText";
@ -21,6 +23,14 @@ import getPeerTitle from "./getPeerTitle"; @@ -21,6 +23,14 @@ import getPeerTitle from "./getPeerTitle";
import wrapJoinVoiceChatAnchor from "./joinVoiceChatAnchor";
import wrapMessageForReply from "./messageForReply";
async function wrapLinkToMessage(message: Message.message | Message.messageService, plain?: boolean) {
const a = document.createElement('i');
a.dataset.savedFrom = message.peerId + '_' + message.mid;
a.dir = 'auto';
a.append(await wrapMessageForReply(message, undefined, undefined, plain as any));
return a;
}
export default async function wrapMessageActionTextNewUnsafe(message: MyMessage, plain?: boolean) {
const element: HTMLElement = plain ? undefined : document.createElement('span');
const action = 'action' in message && message.action;
@ -150,29 +160,10 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage, @@ -150,29 +160,10 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage,
langPackKey = 'ActionPinnedNoText';
if(message.reply_to_mid) { // refresh original message
managers.appMessagesManager.fetchMessageReplyTo(message).then(async(originalMessage) => {
if(originalMessage && message) {
rootScope.dispatchEvent('message_edit', {
storageKey: `${peerId}_history`,
peerId: peerId,
mid: message.mid,
message
});
if(managers.appMessagesManager.isMessageIsTopMessage(message)) {
rootScope.dispatchEvent('dialogs_multiupdate', {
[peerId]: await managers.appMessagesManager.getDialogOnly(peerId)
});
}
}
});
managers.appMessagesManager.fetchMessageReplyTo(message);
}
} else {
const a = document.createElement('i');
a.dataset.savedFrom = pinnedMessage.peerId + '_' + pinnedMessage.mid;
a.dir = 'auto';
a.append(await wrapMessageForReply(pinnedMessage, undefined, undefined, plain as any));
args.push(a);
args.push(wrapLinkToMessage(pinnedMessage, plain));
}
break;
@ -258,6 +249,28 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage, @@ -258,6 +249,28 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage,
break;
}
case 'messageActionPaymentSent': {
langPackKey = 'PaymentSuccessfullyPaidNoItem';
const price = paymentsWrapCurrencyAmount(action.total_amount, action.currency);
args = [price, getNameDivHTML(message.peerId, plain)];
if(message.reply_to_mid) {
const invoiceMessage = await managers.appMessagesManager.getMessageByPeer(
message.reply_to?.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : message.peerId,
message.reply_to_mid
);
if(!invoiceMessage) {
managers.appMessagesManager.fetchMessageReplyTo(message);
} else {
langPackKey = 'PaymentSuccessfullyPaid';
args.push(wrapLinkToMessage(invoiceMessage, plain));
}
}
break;
}
default:
langPackKey = (langPack[_] || `[${action._}]`) as any;
break;

5
src/components/wrappers/messageForReply.ts

@ -158,6 +158,11 @@ export default async function wrapMessageForReply(message: MyMessage | MyDraftMe @@ -158,6 +158,11 @@ export default async function wrapMessageForReply(message: MyMessage | MyDraftMe
break;
}
case 'messageMediaInvoice': {
addPart(undefined, plain ? media.title : wrapEmojiText(media.title));
break;
}
case 'messageMediaUnsupported': {
addPart(UNSUPPORTED_LANG_PACK_KEY);
break;

12
src/components/wrappers/photo.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import renderImageWithFadeIn from "../../helpers/dom/renderImageWithFadeIn";
import mediaSizes from "../../helpers/mediaSizes";
import { Message, PhotoSize } from "../../layer";
import { Message, PhotoSize, WebDocument } from "../../layer";
import { MyDocument } from "../../lib/appManagers/appDocsManager";
import { MyPhoto } from "../../lib/appManagers/appPhotosManager";
import rootScope from "../../lib/rootScope";
@ -19,9 +19,10 @@ import setAttachmentSize from "../../helpers/setAttachmentSize"; @@ -19,9 +19,10 @@ import setAttachmentSize from "../../helpers/setAttachmentSize";
import choosePhotoSize from "../../lib/appManagers/utils/photos/choosePhotoSize";
import type { ThumbCache } from "../../lib/storages/thumbs";
import appDownloadManager from "../../lib/appManagers/appDownloadManager";
import isWebDocument from "../../lib/appManagers/utils/webDocs/isWebDocument";
export default async function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, autoDownloadSize, noBlur, noThumb, noFadeIn, blurAfter, managers = rootScope.managers}: {
photo: MyPhoto | MyDocument,
photo: MyPhoto | MyDocument | WebDocument,
message?: Message.message | Message.messageService,
container: HTMLElement,
boxWidth?: number,
@ -40,7 +41,8 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo @@ -40,7 +41,8 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
blurAfter?: boolean,
managers?: AppManagers,
}) {
if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) {
const isWebDoc = isWebDocument(photo);
if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs) && !isWebDoc) {
if(boxWidth && boxHeight && !size && photo._ === 'document') {
setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message);
}
@ -92,7 +94,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo @@ -92,7 +94,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
isFit = set.isFit;
cacheContext = await managers.thumbsStorage.getCacheContext(photo, size.type);
if(!isFit) {
if(!isFit && !isWebDoc) {
aspecter = document.createElement('div');
aspecter.classList.add('media-container-aspecter');
aspecter.style.width = set.size.width + 'px';
@ -141,7 +143,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo @@ -141,7 +143,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
cacheContext = await managers.thumbsStorage.getCacheContext(photo, size?.type);
}
if(!noThumb) {
if(!noThumb && !isWebDoc) {
const gotThumb = getStrippedThumbIfNeeded(photo, cacheContext, !noBlur);
if(gotThumb) {
loadThumbPromise = Promise.all([loadThumbPromise, gotThumb.loadPromise]);

43
src/components/wrappers/sticker.ts

@ -168,11 +168,48 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -168,11 +168,48 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
if(thumb._ === 'photoPathSize') {
if(thumb.bytes.length) {
const d = getPathFromBytes(thumb.bytes);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.classList.add('rlottie-vector', 'media-sticker', 'thumbnail');
svg.setAttributeNS(null, 'viewBox', `0 0 ${doc.w || 512} ${doc.h || 512}`);
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
// const defs = document.createElementNS(ns, 'defs');
// const linearGradient = document.createElementNS(ns, 'linearGradient');
// linearGradient.setAttributeNS(null, 'id', 'g');
// linearGradient.setAttributeNS(null, 'x1', '-300%');
// linearGradient.setAttributeNS(null, 'x2', '-200%');
// linearGradient.setAttributeNS(null, 'y1', '0');
// linearGradient.setAttributeNS(null, 'y2', '0');
// const stops = [
// ['-10%', '.1'],
// ['30%', '.07'],
// ['70%', '.07'],
// ['110%', '.1']
// ].map(([offset, stopOpacity]) => {
// const stop = document.createElementNS(ns, 'stop');
// stop.setAttributeNS(null, 'offset', offset);
// stop.setAttributeNS(null, 'stop-opacity', stopOpacity);
// return stop;
// });
// const animates = [
// ['-300%', '1200%'],
// ['-200%', '1300%']
// ].map(([from, to], idx) => {
// const animate = document.createElementNS(ns, 'animate');
// animate.setAttributeNS(null, 'attributeName', 'x' + (idx + 1));
// animate.setAttributeNS(null, 'from', from);
// animate.setAttributeNS(null, 'to', to);
// animate.setAttributeNS(null, 'dur', '3s');
// animate.setAttributeNS(null, 'repeatCount', 'indefinite');
// return animate;
// });
// linearGradient.append(...stops, ...animates);
// defs.append(linearGradient);
// svg.append(defs);
const path = document.createElementNS(ns, 'path');
path.setAttributeNS(null, 'd', d);
if(rootScope.settings.animationsEnabled) path.setAttributeNS(null, 'fill', 'url(#g)');
svg.append(path);
div.append(svg);
} else {
@ -237,7 +274,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -237,7 +274,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
loadPromises.push(loadThumbPromise);
}
if(onlyThumb) { // for sticker panel
if(onlyThumb/* || true */) { // for sticker panel
return;
}

2
src/config/app.ts

@ -19,7 +19,7 @@ const App = { @@ -19,7 +19,7 @@ const App = {
version: process.env.VERSION,
versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD,
langPackVersion: '0.4.1',
langPackVersion: '0.4.4',
langPack: 'macos',
langPackCode: 'en',
domains: [MAIN_DOMAIN] as string[],

18
src/config/currencies.ts

File diff suppressed because one or more lines are too long

9
src/config/font.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
export const FontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
export const FontSize = '16px';
export const FontWeight = '400';

5
src/helpers/array/createArray.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
export default function createArray<T1>(length: number, fill?: T1, map?: any): T1[] {
const arr = new Array<T1>(length);
arr.fill(fill);
return map ? arr.map(map) : arr;
}

9
src/helpers/cacheCallback.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
function cacheCallback<A, T>(callback: (str: A) => T) {
const stringResults: any = {}, numberResults: any = {};
return (value: A): T => {
const key = '_' + value;
return (typeof(value) === 'string' ? stringResults : numberResults)[key] ??= callback(value);
};
}
export default cacheCallback;

112
src/helpers/cards/cardBrands.ts

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
import cacheCallback from "../cacheCallback";
import replaceNonNumber from "../string/replaceNonNumber";
const CARD_BRAND_REGEXP: {[brand: string]: RegExp} = {
visa: /^4/,
mastercard: /^(51|52|53|54|55|22|23|24|25|26|27)/,
amex: /^(34|37)/,
discover: /^(60|64|65)/,
diners: /^(30|38|39)/,
diners14: /^(36)/,
jcb: /^(35)/,
unionpay: /^(62[0-6,8-9]|627[0-6,8-9]|6277[0-7,9]|62778[1-9]|81)/,
elo: /^(5067|509|636368|627780)/
};
// * taken from Stripe
export const CARD_BRANDS: {[b: string]: {
minLength: number,
maxLength: number,
cvcMaxLength: number,
cvcMinLength: number | null
}} = {
visa: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
mastercard: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
amex: {
minLength: 15,
maxLength: 15,
cvcMaxLength: 4,
cvcMinLength: 3
},
unionpay: {
minLength: 13,
maxLength: 19,
cvcMaxLength: 3,
cvcMinLength: null
},
diners: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
diners14: {
minLength: 14,
maxLength: 14,
cvcMaxLength: 3,
cvcMinLength: null
},
discover: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
jcb: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
elo: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
unknown: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 4,
cvcMinLength: 3
}
};
export const detectCardBrand = cacheCallback((card: string = '') => {
const keys = Object.keys(CARD_BRAND_REGEXP);
const sanitizedCard = replaceNonNumber(card);
let brand: string;
let last = 0;
keys.forEach((key) => {
const regExp = CARD_BRAND_REGEXP[key];
const match = sanitizedCard.match(regExp);
if(match) {
const result = match[0];
if(result && result.length > last) {
brand = key;
last = result.length;
}
}
});
return brand || 'unknown';
});
export function cardBrandToUnifiedBrand(brand: string) {
return brand === 'diners14' ? 'diners' : brand;
}
export function detectUnifiedCardBrand(card = '') {
const brand = detectCardBrand(card);
return cardBrandToUnifiedBrand(brand);
}

78
src/helpers/cards/cardFormattingPatterns.ts

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
import { IS_ANDROID } from "../../environment/userAgent";
import createArray from "../array/createArray";
import cacheCallback from "../cacheCallback";
import replaceNonNumber from "../string/replaceNonNumber";
import { CARD_BRANDS, detectCardBrand } from "./cardBrands";
import patternCharacters from "./patternCharacters";
const digit = patternCharacters.digit;
const capitalCharacter = patternCharacters.capitalCharacter;
const spaceCharacter = patternCharacters.formattingCharacter(' ');
const yearOptionalPattern = patternCharacters.optionalPattern(/\d\d/);
const sixteenPattern = [digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit];
const fifteenPattern = [digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit];
const requiredPostcodes = new Set(['DZ', 'AR', 'AM', 'AU', 'AT', 'AZ', 'PT', 'BD', 'BY', 'BE', 'BA', 'BR', 'BN', 'BG', 'CA', 'IC', 'CN', 'CO', 'HR', 'CY', 'CZ', 'DK', 'EC', 'GB', 'EE', 'FO', 'FI', 'FR', 'GE', 'DE', 'GR', 'GL', 'GU', 'GG', 'NL', 'HU', 'IN', 'ID', 'IL', 'IT', 'JP', 'JE', 'KZ', 'KR', 'FM', 'KG', 'LV', 'LI', 'LT', 'LU', 'MK', 'MG', 'PT', 'MY', 'MH', 'MQ', 'YT', 'MX', 'MN', 'ME', 'NL', 'NZ', 'GB', 'NO', 'PK', 'PH', 'PL', 'FM', 'PT', 'PR', 'RE', 'RU', 'SA', 'SF', 'RS', 'SG', 'SK', 'SI', 'ZA', 'ES', 'LK', 'SX', 'VI', 'VI', 'SE', 'CH', 'TW', 'TJ', 'TH', 'TU', 'TN', 'TR', 'TM', 'VI', 'UA', 'GB', 'US', 'UY', 'UZ', 'VA', 'VN', 'GB', 'FM']);
const generateFourPattern = cacheCallback((length: number) => {
const out: Array<typeof digit | typeof spaceCharacter> = [];
for(let i = 0, k = 0; i < length;) {
if(k === 4) {
out.push(spaceCharacter);
k = 0;
} else {
out.push(digit);
++i;
++k;
}
}
return out;
});
function generateCardNumberPattern(card: string) {
const brand = detectCardBrand(card);
if(brand === 'amex') return sixteenPattern;
if(brand === 'diners14') return fifteenPattern;
const {minLength, maxLength} = CARD_BRANDS[brand];
const s = replaceNonNumber(card).length;
const d = Math.min(Math.max(minLength, s), maxLength);
return generateFourPattern(d);
}
const cardFormattingPatterns = {
cardNumber: generateCardNumberPattern,
cardExpiry: () => [patternCharacters.month, patternCharacters.formattingCharacter('/'), digit, digit, yearOptionalPattern],
cardCvc: (card?: string) => cardFormattingPatterns.cardCvcFromBrand(detectCardBrand(card)),
cardCvcFromBrand: cacheCallback((brand: string) => {
const info = CARD_BRANDS[brand];
const {cvcMinLength, cvcMaxLength} = info;
const pattern = createArray(cvcMinLength || cvcMaxLength, digit);
if(cvcMinLength && cvcMinLength < cvcMaxLength) {
const i = cvcMaxLength - cvcMinLength;
const h = patternCharacters.optionalPattern(/\d/);
if(i) {
pattern.push(...createArray(i, h));
}
}
return pattern;
}),
postalCodeFromCountry: cacheCallback((iso2: string) => {
switch(iso2) {
case 'US':
return createArray(5, digit);
case 'CA':
return IS_ANDROID ? null : [capitalCharacter, capitalCharacter, capitalCharacter, spaceCharacter, capitalCharacter, capitalCharacter, capitalCharacter];
default:
const optionalDigits = createArray(10, patternCharacters.optionalPattern(/\d/));
if(requiredPostcodes.has(iso2)) {
optionalDigits[0] = digit;
}
return optionalDigits;
}
})
};
export default cardFormattingPatterns;

25
src/helpers/cards/formatInputValueByPattern.ts

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
import formatValueByPattern from "./formatValueByPattern";
export default function formatInputValueByPattern(options: {
value: string,
getPattern: Parameters<typeof formatValueByPattern>[0],
deleting?: boolean,
input?: HTMLElement
}) {
const {value: originalValue, getPattern, deleting, input} = options;
const pushRest = !deleting && !!originalValue.length;
const result = formatValueByPattern(getPattern, originalValue, {
selectionStart: input ? (input as HTMLInputElement).selectionStart : 0,
selectionEnd: input ? (input as HTMLInputElement).selectionEnd : 0
}, pushRest)
const {value, selection} = result;
return {
value,
meta: {
autocorrectComplete: result.autocorrectComplete,
empty: !value
},
selection
};
}

102
src/helpers/cards/formatValueByPattern.ts

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
import accumulate from "../array/accumulate";
import { PatternFunction } from "./patternCharacters";
function accumulateLengths(strs: string[]) {
return accumulate(strs.map((str) => str.length), 0);
}
function formatValueByPattern(
getPattern: PatternFunction,
value: string,
options: Partial<{
selectionStart: number,
selectionEnd: number
}> = {},
pushRest?: boolean
) {
const pattern = getPattern(value);
if(!pattern) {
return {
value: value,
selection: null as typeof options,
autocorrectComplete: !!value
};
}
const length = pattern.length;
const c: string[] = [];
const s: string[] = [];
let l = 0;
let i = 0;
let f = options.selectionStart === 0 ? 0 : null;
let d = options.selectionEnd === 0 ? 0 : null;
const p = () => {
if(f === null && (i + 1) >= options.selectionStart) f = accumulateLengths(c) + (pushRest ? s.length : 0);
if(d === null && (i + 1) >= options.selectionEnd) d = accumulateLengths(c) + (pushRest ? s.length : 0);
};
const m = (e: number) => {
if(e > 0) {
p();
i += e;
}
};
for(; l < length;) {
const getCharacter = pattern[l];
const character = getCharacter(value.slice(i));
const {type, result, consumed} = character;
if(type === 'required') {
if(result) {
c.push(...s, result);
s.length = 0;
++l;
if(character.partial) {
m(value.length - i);
break;
}
m(consumed);
} else {
if(!consumed) {
break;
}
m(1);
}
} else if(type === 'optional') {
if(result) {
c.push(...s, result);
s.length = 0;
m(consumed);
}
++l;
} else if(type === 'formatting') {
if(!pushRest && i >= value.length) {
break;
}
s.push(result);
++l;
m(consumed);
}
}
if(pushRest) {
c.push(...s);
}
return {
value: c.join(''),
selection: {
selectionStart: f === null || value.length && options.selectionStart === value.length ? accumulateLengths(c) : f,
selectionEnd: d === null || value.length && options.selectionEnd === value.length ? accumulateLengths(c) : d
},
autocorrectComplete: l === length
};
}
export default formatValueByPattern;

85
src/helpers/cards/patternCharacters.ts

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
import { fixBuggedNumbers } from "../string/buggedNumbers";
import replaceNonNumber from "../string/replaceNonNumber";
export type PatternCharacter = {
type: 'optional',
result: string,
consumed: number
} | {
type: 'required',
result: string,
consumed: number,
partial?: boolean
} | {
type: 'formatting',
result: string,
consumed: number
};
export type PatternFunction = (str: string) => ((str: string) => PatternCharacter)[];
function makeOptionalCharacter(result: string, consumed: number): PatternCharacter {
return {type: 'optional', result, consumed};
}
function makeRequiredCharacter(result: string, consumed: number, partial?: boolean): PatternCharacter {
return {type: 'required', result, consumed, partial};
}
function makeFormattingCharacter(result: string, consumed: number): PatternCharacter {
return {type: 'formatting', result, consumed};
}
function wrapCharacterRegExpFactory(regExp: RegExp, optional?: boolean) {
return (str: string) => {
const _regExp = new RegExp('^'.concat(regExp.source.replace(/^\^/, '')));
const match = str.match(_regExp);
const makeCharacter = optional ? makeOptionalCharacter : makeRequiredCharacter;
if(match) {
const result = match[0];
return makeCharacter(result, match.index + result.length);
}
return makeCharacter('', str.length);
};
}
function makeCapitalPatternCharacter(str: string) {
const char = wrapCharacterRegExpFactory(/\w/)(str);
return char.result ? makeRequiredCharacter(char.result.toUpperCase(), char.consumed) : char;
}
const makeMonthDigitPatternCharacter = wrapCharacterRegExpFactory(/1[0-2]|0?[1-9]|0/);
function digit(str: string) {
return wrapCharacterRegExpFactory(/[0-9]/)(fixBuggedNumbers(str));
}
const patternCharacters = {
digit,
capitalCharacter: makeCapitalPatternCharacter,
month: (str: string) => {
const char = makeMonthDigitPatternCharacter(fixBuggedNumbers(str));
const cleanedResult = replaceNonNumber(char.result);
const isPartial = ['0', '1'].includes(char.result) && str.length === 1;
if(isPartial || (char.result === '0' && str.length >= 2)) {
return makeRequiredCharacter(char.result, str.length, true);
}
return makeRequiredCharacter(cleanedResult.length === 1 ? '0' + cleanedResult : cleanedResult, char.consumed);
},
formattingCharacter: (str: string) => {
return (str1: string) => {
const consumed = str === str1[0] ? 1 : 0;
return makeFormattingCharacter(str, consumed);
}
},
optionalPattern: (regExp: RegExp) => {
return (str: string) => {
const char = wrapCharacterRegExpFactory(regExp, true)(str);
return char.result ? char : makeOptionalCharacter('', 0);
};
}
};
export default patternCharacters;

82
src/helpers/cards/validateCard.ts

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
import { CARD_BRANDS, detectCardBrand } from "./cardBrands";
import formatInputValueByPattern from './formatInputValueByPattern';
import NBSP from "../string/nbsp";
import replaceNonNumber from "../string/replaceNonNumber";
export type PatternValidationOptions = Partial<{
ignoreIncomplete: boolean
}>;
const nbspRegExp = new RegExp(NBSP, 'g');
function makeValidationError(code?: string) {
return code ? {
type: 'invalid',
code
} : null;
}
function validateCompleteCardNumber(card: string) {
const t = '0'.charCodeAt(0);
const n = card.length % 2;
let a = 0;
for(let i = card.length - 1; i >= 0; --i) {
const c = n === (i % 2);
let o = card.charCodeAt(i) - t;
if(c) o *= 2;
if(o > 9) o -= 9;
a += o;
}
return !(a % 10);
}
function validateExpiry(year: number, month: number, options?: PatternValidationOptions) {
const date = new Date(Date.now());
const _year = year < 100 ? date.getFullYear() % 100 : date.getFullYear();
const nextMonth = date.getMonth() + 1;
if(isNaN(year) || isNaN(month)) {
return options?.ignoreIncomplete ? null : 'incomplete';
}
if((year - _year) < 0) {
return 'invalid_expiry_year_past';
}
if((year - _year) > 50) {
return 'invalid_expiry_year';
}
return !(year - _year) && month < nextMonth ? 'invalid_expiry_month_past' : null;
}
function getCardInfoByNumber(card: string) {
const sanitized = replaceNonNumber(card);
const brand = detectCardBrand(card);
return {
sanitized,
brand,
minLength: CARD_BRANDS[brand].minLength
};
}
function makeCardNumberError(str: string, length: number, ignoreIncomplete: boolean) {
return str.length >= length ? (validateCompleteCardNumber(str) ? null : makeValidationError('invalid')) : (ignoreIncomplete ? null : makeValidationError('incomplete'));
}
export function validateCardNumber(str: string, options: PatternValidationOptions = {}) {
const {sanitized, minLength} = getCardInfoByNumber(str);
return makeCardNumberError(sanitized, minLength, options.ignoreIncomplete);
}
export function validateCardExpiry(str: string, options: PatternValidationOptions = {}) {
const sanitized = str.replace(nbspRegExp, '').split(/ ?\/ ?/);
const [monthStr, yearStr = ''] = sanitized;
const [month, year] = [monthStr, yearStr].map((str) => +str);
const s = yearStr.length === 2 ? year % 100 : year;
return yearStr.length < 2 || yearStr.length === 3 ? (options.ignoreIncomplete ? null : makeValidationError('incomplete')) : makeValidationError(validateExpiry(s, month, options));
}
export function validateAnyIncomplete(formatted: ReturnType<typeof formatInputValueByPattern>, str: string, options: PatternValidationOptions = {}) {
return formatted.meta.autocorrectComplete || options.ignoreIncomplete ? null : makeValidationError('incomplete');
}

17
src/helpers/dom/loadScript.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
export default function loadScript(url: string) {
const script = document.createElement('script');
const promise = new Promise<HTMLScriptElement>((resolve) => {
script.onload = script.onerror = () => {
resolve(script);
};
});
script.src = url;
document.body.appendChild(script);
return promise;
}

8
src/helpers/dom/placeCaretAtEnd.ts

@ -17,7 +17,11 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals @@ -17,7 +17,11 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals
}
el.focus();
if(typeof window.getSelection !== "undefined" && typeof document.createRange !== "undefined") {
if(el instanceof HTMLInputElement) {
const length = el.value.length;
el.selectionStart = length;
el.selectionEnd = length;
} else if(typeof window.getSelection !== "undefined" && typeof document.createRange !== "undefined") {
var range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
@ -33,3 +37,5 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals @@ -33,3 +37,5 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals
textRange.select();
}
}
(window as any).placeCaretAtEnd = placeCaretAtEnd;

7
src/helpers/long/longFromInts.ts

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
import bigInt from "big-integer";
import intToUint from "../number/intToUint";
export default function longFromInts(high: number, low: number): string {
high = intToUint(high), low = intToUint(low);
return bigInt(high).shiftLeft(32).add(bigInt(low)).toString(10);
}

7
src/helpers/long/ulongFromInts.ts

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
import bigInt from "big-integer";
import intToUint from "../number/intToUint";
export default function ulongFromInts(high: number, low: number) {
high = intToUint(high), low = intToUint(low);
return bigInt(high).shiftLeft(32).add(bigInt(low));
}

9
src/helpers/mediaSizes.ts

@ -18,7 +18,8 @@ type MediaTypeSizes = { @@ -18,7 +18,8 @@ type MediaTypeSizes = {
emojiSticker: MediaSize,
poll: MediaSize,
round: MediaSize,
documentName: MediaSize
documentName: MediaSize,
invoice: MediaSize
};
export type MediaSizeType = keyof MediaTypeSizes;
@ -54,7 +55,8 @@ class MediaSizes extends EventListenerBase<{ @@ -54,7 +55,8 @@ class MediaSizes extends EventListenerBase<{
emojiSticker: makeMediaSize(112, 112),
poll: makeMediaSize(240, 0),
round: makeMediaSize(200, 200),
documentName: makeMediaSize(200, 0)
documentName: makeMediaSize(200, 0),
invoice: makeMediaSize(240, 240)
},
desktop: {
regular: makeMediaSize(420, 340),
@ -66,7 +68,8 @@ class MediaSizes extends EventListenerBase<{ @@ -66,7 +68,8 @@ class MediaSizes extends EventListenerBase<{
emojiSticker: makeMediaSize(112, 112),
poll: makeMediaSize(330, 0),
round: makeMediaSize(280, 280),
documentName: makeMediaSize(240, 0)
documentName: makeMediaSize(240, 0),
invoice: makeMediaSize(320, 260)
}
};

73
src/helpers/paymentsWrapCurrencyAmount.ts

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import Currencies from "../config/currencies";
// https://stackoverflow.com/a/34141813
function number_format(number: any, decimals: any, dec_point: any, thousands_sep: any): string {
// Strip all characters but numerical ones.
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number,
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
s: any = '',
toFixedFix = function(n: number, prec: number) {
var k = Math.pow(10, prec);
return '' + Math.round(n * k) / k;
};
// Fix for IE parseFloat(0.55).toFixed(0) = 0;
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (s[0].length > 3) {
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
export default function paymentsWrapCurrencyAmount($amount: number | string, $currency: string, $skipSymbol?: boolean) {
$amount = +$amount;
const $currency_data = Currencies[$currency]; // вытащить из json
if(!$currency_data) {
throw new Error('CURRENCY_WRAP_INVALID');
}
const $amount_exp = $amount / Math.pow(10, $currency_data['exp']);
let $decimals = $currency_data['exp'];
if($currency == 'IRR' &&
Math.floor($amount_exp) == $amount_exp) {
$decimals = 0; // у иранцев копейки почти всегда = 0 и не показываются в UI
}
const $formatted = number_format($amount_exp, $decimals, $currency_data['decimal_sep'], $currency_data['thousands_sep']);
if($skipSymbol) {
return $formatted;
}
const $splitter = $currency_data['space_between'] ? " " : '';
let $formatted_intern: string;
if($currency_data['symbol_left']) {
$formatted_intern = $currency_data['symbol'] + $splitter + $formatted;
} else {
$formatted_intern = $formatted + $splitter + $currency_data['symbol'];
}
return $formatted_intern;
}
function paymentsGetCurrencyExp($currency: string) {
if($currency == 'CLF') {
return 4;
}
if(['BHD','IQD','JOD','KWD','LYD','OMR','TND'].includes($currency)) {
return 3;
}
if(['BIF','BYR','CLP','CVE','DJF','GNF','ISK','JPY','KMF','KRW','MGA', 'PYG','RWF','UGX','UYI','VND','VUV','XAF','XOF','XPF'].includes($currency)) {
return 0;
}
if($currency == 'MRO') {
return 1;
}
return 2;
}

2
src/helpers/scrollSaver.ts

@ -43,6 +43,8 @@ export default class ScrollSaver { @@ -43,6 +43,8 @@ export default class ScrollSaver {
}
public findElements() {
if(!this.query) return [];
const {container} = this;
const containerRect = container.getBoundingClientRect();
const bubbles = Array.from(container.querySelectorAll(this.query)) as HTMLElement[];

16
src/helpers/setAttachmentSize.ts

@ -4,15 +4,16 @@ @@ -4,15 +4,16 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { PhotoSize } from "../layer";
import { PhotoSize, WebDocument } from "../layer";
import { REPLIES_HIDDEN_CHANNEL_ID } from "../lib/mtproto/mtproto_config";
import { MyDocument } from "../lib/appManagers/appDocsManager";
import { MyPhoto } from "../lib/appManagers/appPhotosManager";
import choosePhotoSize from "../lib/appManagers/utils/photos/choosePhotoSize";
import { MediaSize, makeMediaSize } from "./mediaSize";
import isWebDocument from "../lib/appManagers/utils/webDocs/isWebDocument";
export default function setAttachmentSize(
photo: MyPhoto | MyDocument,
photo: MyPhoto | MyDocument | WebDocument,
element: HTMLElement | SVGForeignObjectElement,
boxWidth: number,
boxHeight: number,
@ -21,6 +22,11 @@ export default function setAttachmentSize( @@ -21,6 +22,11 @@ export default function setAttachmentSize(
pushDocumentSize?: boolean,
photoSize?: ReturnType<typeof choosePhotoSize>
) {
const _isWebDocument = isWebDocument(photo);
// if(_isWebDocument && pushDocumentSize === undefined) {
// pushDocumentSize = true;
// }
if(!photoSize) {
photoSize = choosePhotoSize(photo, boxWidth, boxHeight, undefined, pushDocumentSize);
}
@ -28,8 +34,8 @@ export default function setAttachmentSize( @@ -28,8 +34,8 @@ export default function setAttachmentSize(
let size: MediaSize;
const isDocument = photo._ === 'document';
if(isDocument) {
size = makeMediaSize((photo as MyDocument).w || (photoSize as PhotoSize.photoSize).w || 512, (photo as MyDocument).h || (photoSize as PhotoSize.photoSize).h || 512);
if(isDocument || _isWebDocument) {
size = makeMediaSize(photo.w || (photoSize as PhotoSize.photoSize).w || 512, photo.h || (photoSize as PhotoSize.photoSize).h || 512);
} else {
size = makeMediaSize((photoSize as PhotoSize.photoSize).w || 100, (photoSize as PhotoSize.photoSize).h || 100);
}
@ -40,7 +46,7 @@ export default function setAttachmentSize( @@ -40,7 +46,7 @@ export default function setAttachmentSize(
let isFit = true;
if(!isDocument || ['video', 'gif'].includes((photo as MyDocument).type)) {
if(!isDocument || ['video', 'gif'].includes(photo.type) || _isWebDocument) {
if(boxSize.width < 200 && boxSize.height < 200) { // make at least one side this big
boxSize = size = size.aspectCovered(makeMediaSize(200, 200));
}

14
src/helpers/string/buggedNumbers.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
const delta = '0'.charCodeAt(0) - '0'.charCodeAt(0);
const buggedRegExp = /[0-9]/g;
// function hasBuggedNumbers(str: string) {
// return !!str.match(a);
// }
function getDistanceFromBuggedToNormal(char: string) {
return String.fromCharCode(char.charCodeAt(0) - delta);
}
export function fixBuggedNumbers(str: string) {
return str.replace(buggedRegExp, getDistanceFromBuggedToNormal);
}

2
src/helpers/string/nbsp.ts

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
const NBSP = '';
export default NBSP;

3
src/helpers/string/replaceNonLatin.ts

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
export default function replaceNonLatin(str: string) {
return str.replace(/[^A-Za-z0-9]/g, "");
}

3
src/helpers/string/replaceNonNumber.ts

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
export default function replaceNonNumber(str: string) {
return str.replace(/\D/g, '');
}

8
src/index.hbs

@ -102,6 +102,14 @@ @@ -102,6 +102,14 @@
<path d="M-6 16h6v17c-.193-2.84-.876-5.767-2.05-8.782-.904-2.325-2.446-4.485-4.625-6.48A1 1 0 01-6 16z" transform="matrix(1 0 0 -1 0 49)" id="corner-fill" fill="inherit"/>
</g>
</symbol>
<linearGradient id="g" x1="-300%" x2="-200%" y1="0" y2="0">
<stop offset="-10%" stop-opacity=".1"/>
<stop offset="30%" stop-opacity=".07"/>
<stop offset="70%" stop-opacity=".07"/>
<stop offset="110%" stop-opacity=".1"/>
<animate attributeName="x1" from="-300%" to="1200%" dur="3s" repeatCount="indefinite"/>
<animate attributeName="x2" from="-200%" to="1300%" dur="3s" repeatCount="indefinite"/>
</linearGradient>
</defs>
</svg>
<div id="main-columns" class="tabs-container" data-animation="navigation">

66
src/lang.ts

@ -111,7 +111,6 @@ const lang = { @@ -111,7 +111,6 @@ const lang = {
"Popup.Unpin.HideTitle": "Hide pinned messages",
"Popup.Unpin.HideDescription": "Do you want to hide the pinned message bar? It wil stay hidden until a new message is pinned.",
"Popup.Unpin.Hide": "Hide",
"TwoStepAuth.InvalidPassword": "Invalid password",
"TwoStepAuth.EmailCodeChangeEmail": "Change Email",
"MarkupTooltip.LinkPlaceholder": "Enter URL...",
"MediaViewer.Context.Download": "Download",
@ -137,6 +136,12 @@ const lang = { @@ -137,6 +136,12 @@ const lang = {
//"PushNotification.Action.Mute1d.Success": "Notification settings were successfully saved.",
//it is from iOS
"VoiceChat.DiscussionGroup": "discussion group",
"PaymentInfo.CVV": "CVV Code",
"PaymentInfo.Card.Title": "Enter your card information",
"PaymentInfo.Billing.Title": "Enter your billing address",
"PaymentInfo.Done": "PROCEED TO CHECKOUT",
"PaymentCard.Error.Invalid": "Invalid card number",
"PaymentCard.Error.Incomplete": "Incomplete card number",
// * android
"AccDescrEditing": "Editing",
@ -695,6 +700,56 @@ const lang = { @@ -695,6 +700,56 @@ const lang = {
"ScamMessage": "SCAM",
"FakeMessage": "FAKE",
"TextCopied": "Text copied to clipboard",
"PaymentInvoice": "INVOICE",
"PaymentTestInvoice": "TEST INVOICE",
"PaymentReceipt": "Receipt",
"PaymentSuccessfullyPaid": "You successfully transferred %1$s to %2$s for %3$s",
"PaymentSuccessfullyPaidNoItem": "You successfully transferred %1$s to %2$s",
// "PaymentSuccessfullyPaidRecurrent": "You successfully transferred %1$s to %2$s for %3$s and allowed future recurring payments",
// "PaymentSuccessfullyPaidNoItemRecurrent": "You successfully transferred %1$s to %2$s and allowed future recurring payments",
"PaymentCheckout": "Checkout",
"PaymentTransactionTotal": "Total",
"PaymentTip": "Tip",
"PaymentTipOptional": "Tip (Optional)",
"PaymentCheckoutPay": "PAY %1$s",
"PaymentCheckoutMethod": "Payment method",
"PaymentCheckoutProvider": "Payment provider",
"PaymentCardNumber": "Card Number",
"PaymentCardSavePaymentInformation": "Save Payment Information",
"PaymentCardInfo": "Payment info",
"PaymentCardSavePaymentInformationInfoLine1": "You can save your payment info for future use. It will be stored directly with the payment provider. Telegram has no access to your credit card data.",
"Done": "Done",
"PaymentShippingMethod": "Shipping methods",
"PaymentNoShippingMethod": "Sorry, it is not possible to deliver to your address.",
"PaymentShippingInfo": "Shipping Information",
"PaymentShippingAddress": "Shipping address",
"PaymentShippingAddress1Placeholder": "Address 1 (Street)",
"PaymentShippingAddress2Placeholder": "Address 2 (Street)",
"PaymentShippingCityPlaceholder": "City",
"PaymentShippingStatePlaceholder": "State",
"PaymentShippingCountry": "Country",
"PaymentShippingZipPlaceholder": "Postcode",
"PaymentShippingReceiver": "Receiver",
"PaymentShippingName": "Full Name",
"PaymentShippingEmailPlaceholder": "Email",
"PaymentCheckoutPhoneNumber": "Phone number",
"PaymentCheckoutShippingMethod": "Shipping method",
"PaymentShippingSave": "Save Shipping Information",
"PaymentShippingSaveInfo": "You can save your shipping info for future use.",
"PaymentInfoHint": "You paid **%1$s** for **%2$s**.",
"PrivacyPayments": "Payments",
"PrivacyPaymentsClearInfo": "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data.",
"PrivacyPaymentsClear": "Clear Payment and Shipping Info",
"PrivacyPaymentsClearAlertTitle": "Clear payment info",
"PrivacyPaymentsClearAlertText": "Are you sure you want to clear your payment and shipping info?",
"PrivacyPaymentsPaymentInfoCleared": "Payment info cleared.",
"PrivacyPaymentsShippingInfoCleared": "Shipping info cleared.",
"PrivacyPaymentsPaymentShippingCleared": "Payment and shipping info cleared.",
"PrivacyClearShipping": "Shipping info",
"PrivacyClearPayment": "Payment info",
"Clear": "Clear",
"Save": "Save",
"PaymentCheckoutName": "Name",
// * macos
"AccountSettings.Filters": "Chat Folders",
@ -841,6 +896,12 @@ const lang = { @@ -841,6 +896,12 @@ const lang = {
"Chat.Message.ViewGroup": "VIEW GROUP",
"Chat.Message.Sponsored.What": "What are sponsored messages?",
"Chat.Message.Sponsored.Link": "https://promote.telegram.org",
"Checkout.2FA.Text": "Saving payment details is only available with 2-Step Verification.",
"Checkout.NewCard.CardholderNamePlaceholder": "Cardholder Name",
"Checkout.PasswordEntry.Title": "Payment Confirmation",
"Checkout.PasswordEntry.Pay": "Pay",
"Checkout.PasswordEntry.Text": "Your card %@ is on file. To pay with this card, please enter your 2-Step-Verification password.",
"Checkout.WebConfirmation.Title": "Complete Payment",
"ChatList.Context.Mute": "Mute",
"ChatList.Context.Unmute": "Unmute",
"ChatList.Context.Pin": "Pin",
@ -899,6 +960,7 @@ const lang = { @@ -899,6 +960,7 @@ const lang = {
"Emoji.Objects": "Objects",
//"Emoji.Symbols": "Symbols",
"Emoji.Flags": "Flags",
"Error.AnError": "An error occurred. Please try again later.",
"FileSize.B": "%@ B",
"FileSize.KB": "%@ KB",
"FileSize.MB": "%@ MB",
@ -913,6 +975,7 @@ const lang = { @@ -913,6 +975,7 @@ const lang = {
"Message.Context.Pin": "Pin",
"Message.Context.Unpin": "Unpin",
"Message.Context.Goto": "Show Message",
"Message.ReplyActionButtonShowReceipt": "Show Receipt",
"MessageContext.CopyMessageLink1": "Copy Message Link",
"Modal.Send": "Send",
"NewPoll.Anonymous": "Anonymous Voting",
@ -1044,6 +1107,7 @@ const lang = { @@ -1044,6 +1107,7 @@ const lang = {
"GeneralSettings.EmojiPrediction": "Suggest Emoji",
"GroupPermission.Delete": "Delete Exception",
"Search.Confirm.ClearHistory": "Are you sure you want to clear your search history?",
"SecureId.Identity.Placeholder.ExpiryDate": "Expiry Date",
"Separator.ShowMore": "show more",
"Separator.ShowLess": "show less",
"ScheduleController.at": "at",

2
src/langSign.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
const lang = {
"Login.Title": "Sign in to Telegram",
"Login.CountrySelectorLabel": "Country",
"Login.PhoneLabel": "Phone Number",
"Login.PhoneLabelInvalid": "Phone Number Invalid",
"Login.KeepSigned": "Keep me signed in",
@ -21,6 +20,7 @@ const lang = { @@ -21,6 +20,7 @@ const lang = {
"FirstName": "First name (required)",
"LastName": "Last name (optional)",
"StartMessaging": "Start Messaging",
"Country": "Country",
// * macos
"Contacts.PhoneNumber.Placeholder": "Phone Number",

8
src/layer.d.ts vendored

@ -5881,7 +5881,9 @@ export namespace WebDocument { @@ -5881,7 +5881,9 @@ export namespace WebDocument {
access_hash: string | number,
size: number,
mime_type: string,
attributes: Array<DocumentAttribute>
attributes: Array<DocumentAttribute>,
h?: number,
w?: number
};
export type webDocumentNoProxy = {
@ -5889,7 +5891,9 @@ export namespace WebDocument { @@ -5889,7 +5891,9 @@ export namespace WebDocument {
url: string,
size: number,
mime_type: string,
attributes: Array<DocumentAttribute>
attributes: Array<DocumentAttribute>,
h?: number,
w?: number
};
}

2
src/lib/appManagers/appDownloadManager.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import type { ApiFileManager, DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import { Document, InputFile, Photo, PhotoSize } from "../../layer";
import { Document, InputFile, Photo, PhotoSize, WebDocument } from "../../layer";
import { getFileNameByLocation } from "../../helpers/fileName";
import getFileNameForUpload from "../../helpers/getFileNameForUpload";
import { AppManagers } from "./managers";

2
src/lib/appManagers/appImManager.ts

@ -294,7 +294,7 @@ export class AppImManager extends EventListenerBase<{ @@ -294,7 +294,7 @@ export class AppImManager extends EventListenerBase<{
const onInstanceDeactivated = (reason: InstanceDeactivateReason) => {
const isUpdated = reason === 'version';
const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true});
const popup = new PopupElement('popup-instance-deactivated', {overlayClosable: true});
const c = document.createElement('div');
c.classList.add('instance-deactivated-container');
(popup as any).container.replaceWith(c);

53
src/lib/appManagers/appInlineBotsManager.ts

@ -12,13 +12,14 @@ @@ -12,13 +12,14 @@
import type { MyDocument } from "./appDocsManager";
import type { MyPhoto } from "./appPhotosManager";
import type { MyTopPeer } from "./appUsersManager";
import { BotInlineResult, GeoPoint, InputGeoPoint, InputMedia, MessageEntity, MessagesBotResults, ReplyMarkup } from "../../layer";
import { BotInlineResult, GeoPoint, InputGeoPoint, InputMedia, MessageEntity, MessageMedia, MessagesBotResults, ReplyMarkup } from "../../layer";
import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray";
import { AppManager } from "./manager";
import getPhotoMediaInput from "./utils/photos/getPhotoMediaInput";
import getServerMessageId from "./utils/messageId/getServerMessageId";
import generateQId from "./utils/inlineBots/generateQId";
import getDocumentMediaInput from "./utils/docs/getDocumentMediaInput";
import { AppMessagesManager } from "./appMessagesManager";
export class AppInlineBotsManager extends AppManager {
private inlineResults: {[queryAndResultIds: string]: BotInlineResult} = {};
@ -286,7 +287,7 @@ export class AppInlineBotsManager extends AppManager { @@ -286,7 +287,7 @@ export class AppInlineBotsManager extends AppManager {
this.appMessagesManager.sendText(peerId, inlineResult.send_message.message, options);
} else {
let caption = '';
let inputMedia: InputMedia;
let inputMedia: Parameters<AppMessagesManager['sendOther']>[1], messageMedia: MessageMedia;
const sendMessage = inlineResult.send_message;
switch(sendMessage._) {
case 'botInlineMessageMediaAuto': {
@ -342,18 +343,50 @@ export class AppInlineBotsManager extends AppManager { @@ -342,18 +343,50 @@ export class AppInlineBotsManager extends AppManager {
break;
}
case 'botInlineMessageMediaInvoice': {
// const photo = sendMessage.photo;
// inputMedia = {
// _: 'inputMediaInvoice',
// description: sendMessage.description,
// title: sendMessage.title,
// photo: photo && {
// _: 'inputWebDocument',
// attributes: photo.attributes,
// mime_type: photo.mime_type,
// size: photo.size,
// url: photo.url
// },
// invoice: undefined,
// payload: undefined,
// provider: undefined,
// provider_data: undefined,
// start_param: undefined
// };
messageMedia = {
_: 'messageMediaInvoice',
title: sendMessage.title,
description: sendMessage.description,
photo: sendMessage.photo,
currency: sendMessage.currency,
total_amount: sendMessage.total_amount,
pFlags: {
shipping_address_requested: sendMessage.pFlags.shipping_address_requested,
test: sendMessage.pFlags.test
},
start_param: undefined
};
break;
}
}
if(!inputMedia) {
if(!inputMedia && messageMedia) {
inputMedia = {
_: 'messageMediaPending',
type: inlineResult.type,
file_name: inlineResult.title ||
(inlineResult as BotInlineResult.botInlineResult).content?.url ||
(inlineResult as BotInlineResult.botInlineResult).url,
size: 0,
progress: {percent: 30, total: 0}
} as any;
messageMedia
};
}
this.appMessagesManager.sendOther(peerId, inputMedia, options);

43
src/lib/appManagers/appMessagesManager.ts

@ -1268,7 +1268,7 @@ export class AppMessagesManager extends AppManager { @@ -1268,7 +1268,7 @@ export class AppMessagesManager extends AppManager {
return this.sendOther(peerId, this.appUsersManager.getContactMediaInput(contactPeerId));
}
public sendOther(peerId: PeerId, inputMedia: InputMedia, options: Partial<{
public sendOther(peerId: PeerId, inputMedia: InputMedia | {_: 'messageMediaPending', messageMedia: MessageMedia}, options: Partial<{
replyToMsgId: number,
threadId: number,
viaBotId: BotId,
@ -1363,9 +1363,8 @@ export class AppMessagesManager extends AppManager { @@ -1363,9 +1363,8 @@ export class AppMessagesManager extends AppManager {
break;
}
// @ts-ignore
case 'messageMediaPending': {
media = inputMedia;
media = (inputMedia as any).messageMedia;
break;
}
}
@ -1411,7 +1410,7 @@ export class AppMessagesManager extends AppManager { @@ -1411,7 +1410,7 @@ export class AppMessagesManager extends AppManager {
} else {
apiPromise = this.apiManager.invokeApiAfter('messages.sendMedia', {
peer: this.appPeersManager.getInputPeerById(peerId),
media: inputMedia,
media: inputMedia as InputMedia,
random_id: message.random_id,
reply_to_msg_id: replyToMsgId || undefined,
message: '',
@ -2770,8 +2769,7 @@ export class AppMessagesManager extends AppManager { @@ -2770,8 +2769,7 @@ export class AppMessagesManager extends AppManager {
break; */
case 'messageMediaInvoice': {
unsupported = true;
message.media = {_: 'messageMediaUnsupported'};
message.media.photo = this.appWebDocsManager.saveWebDocument(message.media.photo);
break;
}
@ -4029,6 +4027,13 @@ export class AppMessagesManager extends AppManager { @@ -4029,6 +4027,13 @@ export class AppMessagesManager extends AppManager {
this.onUpdateNewMessage(update);
}
if(message._ === 'messageService' && message.action._ === 'messageActionPaymentSent') {
this.rootScope.dispatchEvent('payment_sent', {
peerId: message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : message.peerId,
mid: message.reply_to_mid
});
}
if(!dialog && !isLocalThreadUpdate) {
let good = true;
if(peerId.isAnyChat()) {
@ -4968,6 +4973,16 @@ export class AppMessagesManager extends AppManager { @@ -4968,6 +4973,16 @@ export class AppMessagesManager extends AppManager {
const tempMessage = this.getMessageFromStorage(storage, tempId);
storage.delete(tempId);
if(!(tempMessage as Message.message).reply_markup && (message as Message.message).reply_markup) {
setTimeout(() => { // TODO: refactor it to normal buttons adding
if(!this.getMessageFromStorage(storage, message.mid)) {
return;
}
this.rootScope.dispatchEvent('message_edit', {storageKey: storage.key, peerId: message.peerId, mid: message.mid, message});
}, 0);
}
this.handleReleasingMessage(tempMessage, storage);
@ -5623,6 +5638,22 @@ export class AppMessagesManager extends AppManager { @@ -5623,6 +5638,22 @@ export class AppMessagesManager extends AppManager {
delete message.reply_to_mid; // ! WARNING!
}
if(message._ === 'messageService') {
const peerId = message.peerId;
this.rootScope.dispatchEvent('message_edit', {
storageKey: `${peerId}_history`,
peerId: peerId,
mid: message.mid,
message
});
if(this.isMessageIsTopMessage(message)) {
this.rootScope.dispatchEvent('dialogs_multiupdate', {
[peerId]: this.getDialogOnly(peerId)
});
}
}
return originalMessage;
});
}

75
src/lib/appManagers/appPaymentsManager.ts

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { InputPaymentCredentials, PaymentRequestedInfo, PaymentsPaymentForm } from "../../layer";
import { AppManager } from "./manager";
import getServerMessageId from "./utils/messageId/getServerMessageId";
export default class AppPaymentsManager extends AppManager {
public getPaymentForm(peerId: PeerId, mid: number) {
return this.apiManager.invokeApi('payments.getPaymentForm', {
peer: this.appPeersManager.getInputPeerById(peerId),
msg_id: getServerMessageId(mid)
}).then((paymentForm) => {
this.appUsersManager.saveApiUsers(paymentForm.users);
return paymentForm;
});
}
public getPaymentReceipt(peerId: PeerId, mid: number) {
return this.apiManager.invokeApi('payments.getPaymentReceipt', {
peer: this.appPeersManager.getInputPeerById(peerId),
msg_id: getServerMessageId(mid)
}).then((paymentForm) => {
this.appUsersManager.saveApiUsers(paymentForm.users);
return paymentForm;
});
}
public validateRequestedInfo(peerId: PeerId, mid: number, info: PaymentRequestedInfo, save?: boolean) {
return this.apiManager.invokeApi('payments.validateRequestedInfo', {
save,
peer: this.appPeersManager.getInputPeerById(peerId),
msg_id: getServerMessageId(mid),
info
});
}
public sendPaymentForm(
peerId: PeerId,
mid: number,
formId: PaymentsPaymentForm['form_id'],
requestedInfoId: string,
shippingOptionId: string,
credentials: InputPaymentCredentials,
tipAmount?: number
) {
return this.apiManager.invokeApi('payments.sendPaymentForm', {
form_id: formId,
peer: this.appPeersManager.getInputPeerById(peerId),
msg_id: getServerMessageId(mid),
requested_info_id: requestedInfoId,
shipping_option_id: shippingOptionId,
credentials,
tip_amount: tipAmount || undefined
}).then((result) => {
if(result._ === 'payments.paymentResult') {
this.apiUpdatesManager.processUpdateMessage(result.updates);
}
return result;
});
}
public clearSavedInfo(info?: boolean, credentials?: boolean) {
return this.apiManager.invokeApi('payments.clearSavedInfo', {
info,
credentials
});
}
}

2
src/lib/appManagers/appPeersManager.ts

@ -242,7 +242,7 @@ export class AppPeersManager extends AppManager { @@ -242,7 +242,7 @@ export class AppPeersManager extends AppManager {
}
}
public getDeleteButtonText(peerId: PeerId): LangPackKey {
public getDeleteButtonText(peerId: PeerId): Extract<LangPackKey, 'ChannelDelete' | 'ChatList.Context.LeaveChannel' | 'DeleteMega' | 'ChatList.Context.LeaveGroup' | 'ChatList.Context.DeleteChat'> {
switch(this.getDialogType(peerId)) {
case 'channel':
return this.appChatsManager.hasRights(peerId.toChatId(), 'delete_chat') ? 'ChannelDelete' : 'ChatList.Context.LeaveChannel';

23
src/lib/appManagers/appWebDocsManager.ts

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { DocumentAttribute, WebDocument } from "../../layer";
export default class AppWebDocsManager {
public saveWebDocument(webDocument: WebDocument) {
if(!webDocument) {
return;
}
const attribute: DocumentAttribute.documentAttributeImageSize = webDocument.attributes.find((attribute) => attribute._ === 'documentAttributeImageSize') as any;
if(attribute) {
webDocument.w = attribute.w;
webDocument.h = attribute.h;
}
return webDocument;
}
}

6
src/lib/appManagers/createManagers.ts

@ -43,6 +43,8 @@ import { AppStoragesManager } from "./appStoragesManager"; @@ -43,6 +43,8 @@ import { AppStoragesManager } from "./appStoragesManager";
import cryptoMessagePort from "../crypto/cryptoMessagePort";
import appStateManager from "./appStateManager";
import filterUnique from "../../helpers/array/filterUnique";
import AppWebDocsManager from "./appWebDocsManager";
import AppPaymentsManager from "./appPaymentsManager";
export default function createManagers(appStoragesManager: AppStoragesManager, userId: UserId) {
const managers = {
@ -82,7 +84,9 @@ export default function createManagers(appStoragesManager: AppStoragesManager, u @@ -82,7 +84,9 @@ export default function createManagers(appStoragesManager: AppStoragesManager, u
dcConfigurator: new DcConfigurator,
timeManager: new TimeManager,
appStoragesManager: appStoragesManager,
appStateManager: appStateManager
appStateManager: appStateManager,
appWebDocsManager: new AppWebDocsManager,
appPaymentsManager: new AppPaymentsManager
};
type T = typeof managers;

4
src/lib/appManagers/manager.ts

@ -30,6 +30,7 @@ import type { AppInlineBotsManager } from "./appInlineBotsManager"; @@ -30,6 +30,7 @@ import type { AppInlineBotsManager } from "./appInlineBotsManager";
import type { AppMessagesIdsManager } from "./appMessagesIdsManager";
import type { AppMessagesManager } from "./appMessagesManager";
import type { AppNotificationsManager } from "./appNotificationsManager";
import type AppPaymentsManager from "./appPaymentsManager";
import type { AppPeersManager } from "./appPeersManager";
import type { AppPhotosManager } from "./appPhotosManager";
import type { AppPollsManager } from "./appPollsManager";
@ -40,6 +41,7 @@ import type { AppStateManager } from "./appStateManager"; @@ -40,6 +41,7 @@ import type { AppStateManager } from "./appStateManager";
import type { AppStickersManager } from "./appStickersManager";
import type { AppStoragesManager } from "./appStoragesManager";
import type { AppUsersManager } from "./appUsersManager";
import type AppWebDocsManager from "./appWebDocsManager";
import type { AppWebPagesManager } from "./appWebPagesManager";
import type { AppManagers } from "./managers";
@ -82,6 +84,8 @@ export class AppManager { @@ -82,6 +84,8 @@ export class AppManager {
protected timeManager: TimeManager;
protected appStoragesManager: AppStoragesManager;
protected appStateManager: AppStateManager;
protected appWebDocsManager: AppWebDocsManager;
protected appPaymentsManager: AppPaymentsManager;
public clear: (init?: boolean) => void;

4
src/lib/appManagers/uiNotificationsManager.ts

@ -4,10 +4,10 @@ @@ -4,10 +4,10 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { fontFamily } from "../../components/middleEllipsis";
import getPeerTitle from "../../components/wrappers/getPeerTitle";
import wrapMessageForReply from "../../components/wrappers/messageForReply";
import { MOUNT_CLASS_TO } from "../../config/debug";
import { FontFamily } from "../../config/font";
import { IS_MOBILE } from "../../environment/userAgent";
import IS_VIBRATE_SUPPORTED from "../../environment/vibrateSupport";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
@ -343,7 +343,7 @@ export class UiNotificationsManager { @@ -343,7 +343,7 @@ export class UiNotificationsManager {
fontSize *= window.devicePixelRatio;
ctx.font = `700 ${fontSize}px ${fontFamily}`;
ctx.font = `700 ${fontSize}px ${FontFamily}`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillStyle = 'white';

11
src/lib/appManagers/utils/download/getDownloadMediaDetails.ts

@ -4,14 +4,21 @@ @@ -4,14 +4,21 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { DownloadMediaOptions } from "../../../mtproto/apiFileManager";
import type { DownloadMediaOptions, DownloadOptions } from "../../../mtproto/apiFileManager";
import getDocumentDownloadOptions from "../docs/getDocumentDownloadOptions";
import getPhotoDownloadOptions from "../photos/getPhotoDownloadOptions";
import getWebDocumentDownloadOptions from "../webDocs/getWebDocumentDownloadOptions";
import isWebDocument from "../webDocs/isWebDocument";
import getDownloadFileNameFromOptions from "./getDownloadFileNameFromOptions";
export default function getDownloadMediaDetails(options: DownloadMediaOptions) {
const {media, thumb, queueId, onlyCache} = options;
const downloadOptions = media._ === 'document' ? getDocumentDownloadOptions(media, thumb as any, queueId, onlyCache) : getPhotoDownloadOptions(media as any, thumb, queueId, onlyCache);
let downloadOptions: DownloadOptions;
if(media._ === 'document') downloadOptions = getDocumentDownloadOptions(media, thumb as any, queueId, onlyCache);
else if(media._ === 'photo') downloadOptions = getPhotoDownloadOptions(media, thumb, queueId, onlyCache);
else if(isWebDocument(media)) downloadOptions = getWebDocumentDownloadOptions(media);
const fileName = getDownloadFileNameFromOptions(downloadOptions);
return {fileName, downloadOptions};
}

12
src/lib/appManagers/utils/photos/choosePhotoSize.ts

@ -6,11 +6,11 @@ @@ -6,11 +6,11 @@
import type { MyDocument } from "../../appDocsManager";
import type { MyPhoto } from "../../appPhotosManager";
import type { PhotoSize, WebDocument } from "../../../../layer";
import calcImageInBox from "../../../../helpers/calcImageInBox";
import { PhotoSize } from "../../../../layer";
export default function choosePhotoSize(
photo: MyPhoto | MyDocument,
photo: MyPhoto | MyDocument | WebDocument,
boxWidth = 0,
boxHeight = 0,
useBytes = false,
@ -34,12 +34,12 @@ export default function choosePhotoSize( @@ -34,12 +34,12 @@ export default function choosePhotoSize(
let bestPhotoSize: PhotoSize = {_: 'photoSizeEmpty', type: ''};
let sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs as PhotoSize[];
if(pushDocumentSize && sizes && photo._ === 'document') {
if(pushDocumentSize && sizes && photo._ !== 'photo') {
sizes = sizes.concat({
_: 'photoSize',
w: (photo as MyDocument).w,
h: (photo as MyDocument).h,
size: (photo as MyDocument).size,
w: photo.w,
h: photo.h,
size: photo.size,
type: undefined
});
}

15
src/lib/appManagers/utils/webDocs/getWebDocumentDownloadOptions.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
import { WebDocument } from "../../../../layer";
import { DownloadOptions } from "../../../mtproto/apiFileManager";
export default function getWebDocumentDownloadOptions(webDocument: WebDocument): DownloadOptions {
return {
dcId: 4,
location: {
_: 'inputWebFileLocation',
access_hash: (webDocument as WebDocument.webDocument).access_hash,
url: webDocument.url
},
size: webDocument.size,
mimeType: webDocument.mime_type
};
}

5
src/lib/appManagers/utils/webDocs/isWebDocument.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
import { WebDocument } from "../../../../layer";
export default function isWebDocument(webDocument: any): webDocument is WebDocument {
return !!(webDocument && (webDocument._ === 'webDocument' || webDocument._ === 'webDocumentNoProxy'));
}

8
src/lib/fileManager.ts

@ -34,6 +34,14 @@ export class FileManager { @@ -34,6 +34,14 @@ export class FileManager {
throw false;
}
// sometimes file size can be bigger than the prov
const endOffset = offset + part.byteLength;
if(endOffset > bytes.byteLength) {
const newBytes = new Uint8Array(endOffset);
newBytes.set(bytes, 0);
bytes = newBytes;
}
bytes.set(part, offset);
},
truncate: () => {

2
src/lib/langPack.ts

@ -64,7 +64,7 @@ export const langPack: {[actionType: string]: LangPackKey} = { @@ -64,7 +64,7 @@ export const langPack: {[actionType: string]: LangPackKey} = {
"messageActionGroupCall.ended_by": "Chat.Service.VoiceChatFinished",
"messageActionGroupCall.ended_byYou": "Chat.Service.VoiceChatFinishedYou",
"messageActionBotAllowed": "Chat.Service.BotPermissionAllowed"
"messageActionBotAllowed": "Chat.Service.BotPermissionAllowed",
};
export type LangPackKey = /* string | */keyof typeof lang | keyof typeof langSign;

19
src/lib/mtproto/apiFileManager.ts

@ -14,7 +14,7 @@ import Modes from "../../config/modes"; @@ -14,7 +14,7 @@ import Modes from "../../config/modes";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import { getFileNameByLocation } from "../../helpers/fileName";
import { randomLong } from "../../helpers/random";
import { Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile } from "../../layer";
import { Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, WebDocument } from "../../layer";
import { DcId } from "../../types";
import CacheStorageController from "../cacheStorage";
import fileManager from "../fileManager";
@ -54,7 +54,7 @@ export type DownloadOptions = { @@ -54,7 +54,7 @@ export type DownloadOptions = {
};
export type DownloadMediaOptions = {
media: Photo | Document.document,
media: Photo.photo | Document.document | WebDocument,
thumb?: PhotoSize,
queueId?: number,
onlyCache?: boolean
@ -275,6 +275,10 @@ export class ApiFileManager extends AppManager { @@ -275,6 +275,10 @@ export class ApiFileManager extends AppManager {
}
private getLimitPart(size: number): number {
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
return 512 * 1024;
}
let bytes = 128 * 1024;
while((size / bytes) > 2000) {
@ -599,14 +603,15 @@ export class ApiFileManager extends AppManager { @@ -599,14 +603,15 @@ export class ApiFileManager extends AppManager {
public downloadMedia(options: DownloadMediaOptions): DownloadPromise {
let {media, thumb} = options;
const isPhoto = media._ === 'photo';
if(media._ === 'photoEmpty' || (isPhoto && !thumb)) {
if(isPhoto && !thumb) {
return Promise.reject('preloadPhoto photoEmpty!');
}
// get original instance with correct file_reference instead of using copies
const isDocument = media._ === 'document';
if(isDocument) media = this.appDocsManager.getDoc(media.id);
else if(isPhoto) media = this.appPhotosManager.getPhoto(media.id);
// const isWebDocument = media._ === 'webDocument';
if(isDocument) media = this.appDocsManager.getDoc((media as Photo.photo).id);
else if(isPhoto) media = this.appPhotosManager.getPhoto((media as Document.document).id);
const {fileName, downloadOptions} = getDownloadMediaDetails(options);
@ -615,9 +620,9 @@ export class ApiFileManager extends AppManager { @@ -615,9 +620,9 @@ export class ApiFileManager extends AppManager {
promise = this.download(downloadOptions);
if(isDocument && !thumb) {
this.rootScope.dispatchEvent('document_downloading', media.id);
this.rootScope.dispatchEvent('document_downloading', (media as Document.document).id);
promise.catch(noop).finally(() => {
this.rootScope.dispatchEvent('document_downloaded', media.id);
this.rootScope.dispatchEvent('document_downloaded', (media as Document.document).id);
});
}
}

6
src/lib/mtproto/passwordManager.ts

@ -72,8 +72,12 @@ export class PasswordManager extends AppManager { @@ -72,8 +72,12 @@ export class PasswordManager extends AppManager {
});
}
public getInputCheckPassword(password: string, state: AccountPassword) {
return this.cryptoWorker.invokeCrypto('computeSRP', password, state, false) as Promise<InputCheckPasswordSRP.inputCheckPasswordSRP>;
}
public check(password: string, state: AccountPassword, options: any = {}) {
return this.cryptoWorker.invokeCrypto('computeSRP', password, state, false).then((inputCheckPassword) => {
return this.getInputCheckPassword(password, state).then((inputCheckPassword) => {
//console.log('SRP', inputCheckPassword);
return this.apiManager.invokeApi('auth.checkPassword', {
password: inputCheckPassword as InputCheckPasswordSRP.inputCheckPasswordSRP

12
src/lib/mtproto/telegramMeWebManager.ts

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug";
import Modes from "../../config/modes";
import loadScript from "../../helpers/dom/loadScript";
import tsNow from "../../helpers/tsNow";
import sessionStorage from '../sessionStorage';
@ -47,16 +48,9 @@ export class TelegramMeWebManager { @@ -47,16 +48,9 @@ export class TelegramMeWebManager {
];
const promises = urls.map((url) => {
const script = document.createElement('script');
const promise = new Promise<void>((resolve) => {
script.onload = script.onerror = () => {
script.remove();
resolve();
};
return loadScript(url).then((script) => {
script.remove();
});
script.src = url;
document.body.appendChild(script);
return promise;
});
return Promise.all(promises);

4
src/lib/mtproto/timeManager.ts

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
import sessionStorage from '../sessionStorage';
import { nextRandomUint } from '../../helpers/random';
import { WorkerTaskVoidTemplate } from '../../types';
import longFromInts from '../../helpers/long/longFromInts';
import ulongFromInts from '../../helpers/long/ulongFromInts';
import { AppManager } from '../appManagers/manager';
/*
@ -85,7 +85,7 @@ export class TimeManager extends AppManager { @@ -85,7 +85,7 @@ export class TimeManager extends AppManager {
this.lastMessageId = messageId;
const ret = longFromInts(messageId[0], messageId[1]);
const ret = ulongFromInts(messageId[0], messageId[1]).toString(10);
// if(lol[ret]) {
// console.error('[TimeManager]: Generated SAME msg id', messageId, this.timeOffset, ret);

42
src/lib/mtproto/tl_utils.ts

@ -16,25 +16,16 @@ import bytesToHex from '../../helpers/bytes/bytesToHex'; @@ -16,25 +16,16 @@ import bytesToHex from '../../helpers/bytes/bytesToHex';
import isObject from '../../helpers/object/isObject';
import gzipUncompress from '../../helpers/gzipUncompress';
import bigInt from 'big-integer';
import longFromInts from '../../helpers/long/longFromInts';
// @ts-ignore
/* import {BigInteger} from 'jsbn';
export function bigint(num: number) {
return new BigInteger(num.toString(16), 16);
}
function bigStringInt(strNum: string) {
return new BigInteger(strNum, 10)
} */
import ulongFromInts from '../../helpers/long/ulongFromInts';
const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id;
const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id;
const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id;
const gzipPacked = +Schema.MTProto.constructors.find((c) => c.predicate === 'gzip_packed').id;
//console.log('boolFalse', boolFalse === 0xbc799737);
const safeBigInt = bigInt(Number.MAX_SAFE_INTEGER);
const ulongBigInt = bigInt(bigInt[2]).pow(64);
const longBigInt = ulongBigInt.divide(bigInt[2]);
class TLSerialization {
private maxLength = 2048; // 2Kb
@ -159,12 +150,13 @@ class TLSerialization { @@ -159,12 +150,13 @@ class TLSerialization {
return this.storeIntBytes(sLong, 64, field);
}
}
if(typeof sLong !== 'string') {
sLong = sLong ? sLong.toString() : '0';
let _bigInt = bigInt(sLong as string);
if(_bigInt.isNegative()) { // make it unsigned
_bigInt = ulongBigInt.add(_bigInt);
}
const {quotient, remainder} = bigInt(sLong).divmod(0x100000000);
const {quotient, remainder} = _bigInt.divmod(0x100000000);
const high = quotient.toJSNumber();
const low = remainder.toJSNumber();
@ -506,23 +498,25 @@ class TLDeserialization<FetchLongAs extends Long> { @@ -506,23 +498,25 @@ class TLDeserialization<FetchLongAs extends Long> {
return doubleView[0];
}
// ! it should've been signed
public fetchLong(field?: string): FetchLongAs {
const iLow = this.readInt((field || '') + ':long[low]');
const iHigh = this.readInt((field || '') + ':long[high]');
//const longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString();
const longDec = longFromInts(iHigh, iLow);
let ulong = ulongFromInts(iHigh, iLow);
if(/* !unsigned && */!this.mtproto && ulong.greater(longBigInt)) { // make it signed
ulong = ulong.minus(ulongBigInt);
}
if(!this.mtproto) {
const num = +longDec;
if(Number.isSafeInteger(num)) {
if(safeBigInt.greaterOrEquals(ulong.abs())) {
// @ts-ignore
return num;
return ulong.toJSNumber();
}
}
// @ts-ignore
return longDec;
return ulong.toString(10);
}
public fetchBool(field?: string): boolean {

4
src/lib/rootScope.ts

@ -136,7 +136,9 @@ export type BroadcastEvents = { @@ -136,7 +136,9 @@ export type BroadcastEvents = {
'service_notification': Update.updateServiceNotification,
'logging_out': void
'logging_out': void,
'payment_sent': {peerId: PeerId, mid: number}
};
export type BroadcastEventsListeners = {

19
src/lib/storages/thumbs.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { WebDocument } from "../../layer";
import type { MyDocument } from "../appManagers/appDocsManager";
import type { MyPhoto } from "../appManagers/appPhotosManager";
@ -21,29 +22,33 @@ export type ThumbsCache = { @@ -21,29 +22,33 @@ export type ThumbsCache = {
const thumbFullSize = 'full';
export type ThumbStorageMedia = MyPhoto | MyDocument | WebDocument;
export default class ThumbsStorage {
private thumbsCache: ThumbsCache = {};
public getCacheContext(media: MyPhoto | MyDocument, thumbSize: string = thumbFullSize): ThumbCache {
private getKey(media: ThumbStorageMedia) {
return media._ + ((media as MyPhoto).id ?? (media as WebDocument).url);
}
public getCacheContext(media: ThumbStorageMedia, thumbSize: string = thumbFullSize): ThumbCache {
/* if(media._ === 'photo' && thumbSize !== 'i') {
thumbSize = thumbFullSize;
} */
const key = media._ + media.id;
const cache = this.thumbsCache[key] ??= {};
const cache = this.thumbsCache[this.getKey(media)] ??= {};
return cache[thumbSize] ??= {downloaded: 0, url: '', type: thumbSize};
}
public setCacheContextURL(media: MyPhoto | MyDocument, thumbSize: string = thumbFullSize, url: string, downloaded: number = 0) {
public setCacheContextURL(media: ThumbStorageMedia, thumbSize: string = thumbFullSize, url: string, downloaded: number = 0) {
const cacheContext = this.getCacheContext(media, thumbSize);
cacheContext.url = url;
cacheContext.downloaded = downloaded;
return cacheContext;
}
public deleteCacheContext(media: MyPhoto | MyDocument, thumbSize: string = thumbFullSize) {
const key = media._ + media.id;
const cache = this.thumbsCache[key];
public deleteCacheContext(media: ThumbStorageMedia, thumbSize: string = thumbFullSize) {
const cache = this.thumbsCache[this.getKey(media)];
if(cache) {
delete cache[thumbSize];
}

233
src/pages/pageSignIn.ts

@ -19,8 +19,6 @@ import ripple from "../components/ripple"; @@ -19,8 +19,6 @@ import ripple from "../components/ripple";
import findUpTag from "../helpers/dom/findUpTag";
import findUpClassName from "../helpers/dom/findUpClassName";
import { randomLong } from "../helpers/random";
import AppStorage from "../lib/storage";
import CacheStorageController from "../lib/cacheStorage";
import pageSignQR from "./pageSignQR";
import getLanguageChangeButton from "../components/languageChangeButton";
import cancelEvent from "../helpers/dom/cancelEvent";
@ -40,6 +38,7 @@ import IS_EMOJI_SUPPORTED from "../environment/emojiSupport"; @@ -40,6 +38,7 @@ import IS_EMOJI_SUPPORTED from "../environment/emojiSupport";
import setInnerHTML from "../helpers/dom/setInnerHTML";
import wrapEmojiText from "../lib/richTextProcessor/wrapEmojiText";
import apiManagerProxy from "../lib/mtproto/mtprotoworker";
import CountryInputField from "../components/countryInputField";
//import _countries from '../countries_pretty.json';
let btnNext: HTMLButtonElement = null, btnQr: HTMLButtonElement;
@ -63,231 +62,27 @@ let onFirstMount = () => { @@ -63,231 +62,27 @@ let onFirstMount = () => {
//const countries: Country[] = _countries.default.filter((c) => c.emoji);
// const countries: Country[] = Countries.filter((c) => c.emoji).sort((a, b) => a.name.localeCompare(b.name));
// const countries = I18n.countriesList.filter((country) => !country.pFlags?.hidden);
const setCountries = () => {
countries = I18n.countriesList
.filter((country) => !country.pFlags?.hidden)
.sort((a, b) => (a.name || a.default_name).localeCompare(b.name || b.default_name));
};
let countries: HelpCountry.helpCountry[];
setCountries();
rootScope.addEventListener('language_change', () => {
setCountries();
});
const liMap: Map<string, HTMLLIElement[]> = new Map();
let lastCountrySelected: HelpCountry, lastCountryCodeSelected: HelpCountryCode;
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
const countryInputField = new InputField({
label: 'Login.CountrySelectorLabel',
name: randomLong()
});
countryInputField.container.classList.add('input-select');
const countryInput = countryInputField.input;
// countryInput.autocomplete = randomLong();
const selectWrapper = document.createElement('div');
selectWrapper.classList.add('select-wrapper', 'z-depth-3', 'hide');
const arrowDown = document.createElement('span');
arrowDown.classList.add('arrow', 'arrow-down');
countryInputField.container.append(arrowDown);
const selectList = document.createElement('ul');
selectWrapper.appendChild(selectList);
const scroll = new Scrollable(selectWrapper);
let initSelect = () => {
initSelect = null;
countries.forEach((c) => {
const emoji = getCountryEmoji(c.iso2);
const liArr: Array<HTMLLIElement> = [];
c.country_codes.forEach((countryCode) => {
const li = document.createElement('li');
let wrapped = wrapEmojiText(emoji);
if(IS_EMOJI_SUPPORTED) {
const spanEmoji = document.createElement('span');
setInnerHTML(spanEmoji, wrapped);
li.append(spanEmoji);
} else {
setInnerHTML(li, wrapped);
}
const el = i18n(c.default_name as any);
el.dataset.defaultName = c.default_name;
li.append(el);
const span = document.createElement('span');
span.classList.add('phone-code');
span.innerText = '+' + countryCode.country_code;
li.appendChild(span);
liArr.push(li);
selectList.append(li);
});
let lastCountrySelected: HelpCountry, lastCountryCodeSelected: HelpCountryCode;
const countryInputField = new CountryInputField({
onCountryChange: (country, code) => {
lastCountrySelected = country, lastCountryCodeSelected = code;
liMap.set(c.iso2, liArr);
});
selectList.addEventListener('mousedown', (e) => {
if(e.button !== 0) { // other buttons but left shall not pass
if(!code) {
return;
}
const target = findUpTag(e.target, 'LI')
selectCountryByTarget(target);
//console.log('clicked', e, countryName, phoneCode);
});
countryInputField.container.appendChild(selectWrapper);
};
const selectCountryByTarget = (target: HTMLElement) => {
const defaultName = (target.childNodes[1] as HTMLElement).dataset.defaultName;
const phoneCode = target.querySelector<HTMLElement>('.phone-code').innerText;
const countryCode = phoneCode.replace(/\D/g, '');
replaceContent(countryInput, i18n(defaultName as any));
simulateEvent(countryInput, 'input');
lastCountrySelected = countries.find((c) => c.default_name === defaultName);
lastCountryCodeSelected = lastCountrySelected.country_codes.find((_countryCode) => _countryCode.country_code === countryCode);
telInputField.value = telInputField.lastValue = phoneCode;
hidePicker();
setTimeout(() => {
telEl.focus();
placeCaretAtEnd(telEl, true);
}, 0);
};
initSelect();
let hideTimeout: number;
countryInput.addEventListener('focus', function(this: typeof countryInput, e) {
if(initSelect) {
initSelect();
} else {
countries.forEach((c) => {
liMap.get(c.iso2).forEach((li) => li.style.display = '');
});
}
clearTimeout(hideTimeout);
hideTimeout = undefined;
selectWrapper.classList.remove('hide');
void selectWrapper.offsetWidth; // reflow
selectWrapper.classList.add('active');
countryInputField.select();
fastSmoothScroll({
container: page.pageEl.parentElement.parentElement,
element: countryInput,
position: 'start',
margin: 4
});
setTimeout(() => {
if(!mouseDownHandlerAttached) {
document.addEventListener('mousedown', onMouseDown, {capture: true});
mouseDownHandlerAttached = true;
}
}, 0);
});
let mouseDownHandlerAttached = false;
const onMouseDown = (e: MouseEvent) => {
if(findUpClassName(e.target, 'input-select')) {
return;
}
if(e.target === countryInput) {
return;
}
hidePicker();
document.removeEventListener('mousedown', onMouseDown, {capture: true});
mouseDownHandlerAttached = false;
};
const hidePicker = () => {
if(hideTimeout !== undefined) return;
selectWrapper.classList.remove('active');
hideTimeout = window.setTimeout(() => {
selectWrapper.classList.add('hide');
hideTimeout = undefined;
}, 200);
};
/* false && countryInput.addEventListener('blur', function(this: typeof countryInput, e) {
hidePicker();
e.cancelBubble = true;
}, {capture: true}); */
countryInput.addEventListener('keyup', (e) => {
const key = e.key;
if(e.ctrlKey || key === 'Control') return false;
//let i = new RegExp('^' + this.value, 'i');
let _value = countryInputField.value.toLowerCase();
let matches: HelpCountry[] = [];
countries.forEach((c) => {
const names = [
c.name,
c.default_name,
c.iso2
];
names.filter(Boolean).forEach((name) => {
const abbr = name.split(' ').filter((word) => /\w/.test(word)).map((word) => word[0]).join('');
if(abbr.length > 1) {
names.push(abbr);
}
});
let good = !!names.filter(Boolean).find((str) => str.toLowerCase().indexOf(_value) !== -1)/* === 0 */;//i.test(c.name);
liMap.get(c.iso2).forEach((li) => li.style.display = good ? '' : 'none');
if(good) matches.push(c);
});
// Код ниже автоматически выберет страну если она осталась одна при поиске
/* if(matches.length === 1 && matches[0].li.length === 1) {
if(matches[0].name === lastCountrySelected) return false;
//console.log('clicking', matches[0]);
var clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent('mousedown', true, true);
matches[0].li[0].dispatchEvent(clickEvent);
return false;
} else */if(matches.length === 0) {
countries.forEach((c) => {
liMap.get(c.iso2).forEach((li) => li.style.display = '');
});
} else if(matches.length === 1 && key === 'Enter') {
selectCountryByTarget(liMap.get(matches[0].iso2)[0]);
telInputField.value = telInputField.lastValue = '+' + code.country_code;
setTimeout(() => {
telEl.focus();
placeCaretAtEnd(telEl, true);
}, 0);
}
});
arrowDown.addEventListener('mousedown', function(this: typeof arrowDown, e) {
e.cancelBubble = true;
e.preventDefault();
if(countryInput.matches(':focus')) countryInput.blur();
else countryInput.focus();
});
const telInputField = new TelInputField({
onInput: (formatted) => {
lottieLoader.loadLottieWorkers();
@ -303,9 +98,7 @@ let onFirstMount = () => { @@ -303,9 +98,7 @@ let onFirstMount = () => {
)
)
) {
replaceContent(countryInput, country ? i18n(country.default_name as any) : countryName);
lastCountrySelected = country;
lastCountryCodeSelected = code;
countryInputField.override(country, code, countryName);
}
//if(country && (telInputField.value.length - 1) >= (country.pattern ? country.pattern.length : 9)) {
@ -485,7 +278,7 @@ let onFirstMount = () => { @@ -485,7 +278,7 @@ let onFirstMount = () => {
return nearestDcResult;
}).then((nearestDcResult) => {
if(!countryInputField.value.length && !telInputField.value.length) {
selectCountryByTarget(liMap.get(nearestDcResult.country)[0]);
countryInputField.selectCountryByIso2(nearestDcResult.country);
}
//console.log('woohoo', nearestDcResult, country);

12
src/scripts/in/schema_additional_params.json

@ -338,4 +338,16 @@ @@ -338,4 +338,16 @@
{"name": "file_size_max", "type": "number"},
{"name": "video_size_max", "type": "number"}
]
}, {
"predicate": "webDocument",
"params": [
{"name": "h", "type": "number"},
{"name": "w", "type": "number"}
]
}, {
"predicate": "webDocumentNoProxy",
"params": [
{"name": "h", "type": "number"},
{"name": "w", "type": "number"}
]
}]

8
src/scss/components/_global.scss

@ -25,6 +25,14 @@ a { @@ -25,6 +25,14 @@ a {
-webkit-tap-highlight-color: transparent;
}
button {
background: none;
outline: none;
border: none;
cursor: pointer;
padding: 0;
}
img,
video {
-webkit-user-drag: none;

4
src/scss/mixins/_hover.scss

@ -45,7 +45,9 @@ @@ -45,7 +45,9 @@
@mixin btn-hoverable {
@include hover-background-effect();
&.primary, &.blue, &.active {
&.primary,
&.blue,
&.active {
@include hover-background-effect(primary);
}

3
src/scss/partials/_button.scss

@ -4,7 +4,8 @@ @@ -4,7 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.btn, .btn-icon {
.btn,
.btn-icon {
background: none;
outline: none;
border: none;

15
src/scss/partials/_chatBubble.scss

@ -2147,6 +2147,12 @@ $bubble-beside-button-width: 38px; @@ -2147,6 +2147,12 @@ $bubble-beside-button-width: 38px;
code {
cursor: pointer;
}
&.is-invoice {
.attachment {
background-color: inherit !important;
}
}
}
// * fix scroll with only 1 bubble
@ -2851,8 +2857,17 @@ $bubble-beside-button-width: 38px; @@ -2851,8 +2857,17 @@ $bubble-beside-button-width: 38px;
transform: rotate(-45deg);
}
&.is-buy:before {
content: $tgico-card;
}
&.is-switch-inline:before {
content: $tgico-forward_filled;
}
}
}
.bubble-primary-color {
color: var(--message-primary-color);
font-weight: var(--font-weight-bold);
}

27
src/scss/partials/_checkbox.scss

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
.checkbox-field {
--size: 1.25rem;
--offset-left: 0px;
margin: 1.5rem 1.1875rem;
display: block;
text-align: left;
@ -29,19 +30,21 @@ @@ -29,19 +30,21 @@
.checkbox-box {
position: absolute;
left: 0;
left: var(--offset-left);
top: 50%;
transform: translateY(-50%);
width: var(--size);
height: var(--size);
border-radius: .25rem;
border-radius: .3125rem;
overflow: hidden;
html.is-safari & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow
}
&-check, &-background, &-border {
&-check,
&-background,
&-border {
position: absolute;
top: 0;
left: 0;
@ -73,7 +76,7 @@ @@ -73,7 +76,7 @@
}
&-check {
--offset: 3px;
--offset: 7px;
width: calc(var(--size) - var(--offset));
height: calc(var(--size) - var(--offset));
top: 50%;
@ -82,7 +85,7 @@ @@ -82,7 +85,7 @@
use {
stroke: #fff;
stroke-width: 2.75;
stroke-width: 3.75;
stroke-linecap: round;
stroke-dasharray: 24.19, 24.19;
stroke-dashoffset: 0;
@ -99,15 +102,10 @@ @@ -99,15 +102,10 @@
.checkbox-caption {
position: relative;
padding-left: 3.375rem;
cursor: pointer;
display: inline-block;
min-height: 24px;
margin-top: 1px;
line-height: 26px;
user-select: none;
transition: .2s opacity;
// color: var(--primary-text-color);
color: inherit;
pointer-events: none;
line-height: var(--line-height);
@include animation-level(0) {
transition: none;
@ -168,6 +166,7 @@ @@ -168,6 +166,7 @@
.radio-field {
--size: 1.375rem;
--offset-left: 0px;
position: relative;
text-align: left;
margin: 1.25rem 0;
@ -210,7 +209,7 @@ @@ -210,7 +209,7 @@
content: '';
display: block;
position: absolute;
left: 0;
left: var(--offset-left);
top: 50%;
width: var(--size);
height: var(--size);
@ -228,7 +227,7 @@ @@ -228,7 +227,7 @@
}
&::after {
left: .3125rem;
left: calc(var(--offset-left) + .3125rem);
width: .75rem;
height: .75rem;
border-radius: 50%;

32
src/scss/partials/_input.scss

@ -81,6 +81,18 @@ @@ -81,6 +81,18 @@
transition: opacity .2s;
}
}
&-icon {
position: absolute;
right: 1rem;
z-index: 1;
top: 50%;
transform: translateY(-50%);
width: 1.5rem;
height: 1.5rem;
border-radius: .375rem;
pointer-events: none;
}
&-input {
--padding: 1rem;
@ -90,7 +102,7 @@ @@ -90,7 +102,7 @@
border-radius: var(--border-radius);
background-color: var(--surface-color);
//padding: 0 1rem;
padding: calc(var(--padding) - var(--border-width));
padding: calc(var(--padding) - var(--border-width)) calc(var(--padding-horizontal) - var(--border-width));
box-sizing: border-box;
width: 100%;
min-height: var(--height);
@ -123,6 +135,7 @@ @@ -123,6 +135,7 @@
@include respond-to(handhelds) {
--padding: .9375rem;
--padding-horizontal: .9375rem;
}
@include animation-level(0) {
@ -215,8 +228,8 @@ @@ -215,8 +228,8 @@
&:valid ~ label,
&:not(:empty) ~ label,
&:disabled ~ label {
transform: translate(-.25rem, calc(var(--height) / -2 + .125rem)) scale(.75);
padding: 0 6px;
transform: translate(-.1875rem, calc(var(--height) / -2 + .0625rem)) scale(.75);
padding: 0 .3125rem;
opacity: 1;
}
}
@ -237,6 +250,19 @@ @@ -237,6 +250,19 @@
}
}
.input-fields-row {
display: flex;
.input-field {
flex: 1 1 auto;
width: 1%; // fix width because of contenteditable
// &:not(:first-child) {
// margin-left: 1rem;
// }
}
}
.input-wrapper > * + * {
margin-top: 1.5rem;
}

27
src/scss/partials/_row.scss

@ -4,10 +4,12 @@ @@ -4,10 +4,12 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
$row-border-radius: $border-radius-medium;
.row {
min-height: 3.5rem;
position: relative;
padding: .6875rem 1rem;
padding: .4375rem 1rem;
display: flex;
flex-direction: column;
justify-content: center;
@ -80,7 +82,7 @@ @@ -80,7 +82,7 @@
margin-top: -.125rem;
}
.row-subtitle:not(:empty) + .row-title.tgico:before {
.row-subtitle:not(:empty):not(.hide) + .row-title.tgico:before {
margin-top: .25rem;
}
}
@ -90,7 +92,7 @@ @@ -90,7 +92,7 @@
overflow: hidden;
@include respond-to(not-handhelds) {
border-radius: $border-radius-medium;
border-radius: $row-border-radius;
}
}
@ -100,6 +102,22 @@ @@ -100,6 +102,22 @@
margin-left: -3.375rem;
}
.radio-field,
.radio-field-main,
.checkbox-field {
position: unset;
}
.radio-field,
.checkbox-field {
--offset-left: 1rem;
}
.radio-field {
margin-top: 0;
margin-bottom: 0;
}
.checkbox-field {
margin-right: 0;
height: auto;
@ -134,6 +152,9 @@ @@ -134,6 +152,9 @@
position: absolute !important;
margin: 0 !important;
left: .5rem;
display: flex;
align-items: center;
justify-content: center;
&-small {
width: 2rem !important;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save