Merge branch 'payments'

This commit is contained in:
Eduard Kuzmenko 2022-07-18 15:03:06 +02:00
commit 48ced05da6
129 changed files with 4672 additions and 1058 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_227_253)">
<rect width="32" height="32" fill="#016FD0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.21 5H17.2831L19.0649 8.98321V5H25.3283L26.4083 7.98423L27.4918 5H32.2742L32.2742 6.26337H28.4758L26.4312 11.8561L24.4002 6.27676L20.4535 6.26337V15.2556L16.4145 6.26477H13.1711L9.07687 15.3401H11.8849L12.6569 13.5227H16.842L17.614 15.3401H20.4915L20.4909 15.3387H22.9357V9.10448L25.2911 15.3387H27.5226L29.83 9.12563V15.3599L32.2742 15.3387L32.2742 15.3507L32.2742 16.6134H29.0673L26.4147 19.4918L23.7693 16.6134H20.4578V16.6127H12.6339V25.6958H20.4578V25.6958H23.6783L26.3431 22.7884L29.0351 25.6958H32.2742L32.2742 26.9908H28.3906L26.3266 24.749L24.277 27L11.2281 26.9725V16.6401H7L12.21 5ZM32.2742 25.5675L32.2742 23.7077L29.8415 21.1246L32.2742 18.5662L32.2742 16.6846L28.0375 21.1493L32.2742 25.5675ZM14.7502 8.59689L13.5227 11.4859H15.9769L14.7502 8.59689ZM20.4578 18.7051V16.6538L24.7475 21.1493L20.4578 25.6552V23.5787H15.1676V22.1836H20.3317V20.1257H15.1676V18.7051H20.4578Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_227_253">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 16C0 12.2801 0 10.4202 0.40889 8.89417C1.5185 4.75307 4.75307 1.5185 8.89417 0.40889C10.4202 0 12.2801 0 16 0C19.7199 0 21.5798 0 23.1058 0.40889C27.2469 1.5185 30.4815 4.75307 31.5911 8.89417C32 10.4202 32 12.2801 32 16C32 19.7199 32 21.5798 31.5911 23.1058C30.4815 27.2469 27.2469 30.4815 23.1058 31.5911C21.5798 32 19.7199 32 16 32C12.2801 32 10.4202 32 8.89417 31.5911C4.75307 30.4815 1.5185 27.2469 0.40889 23.1058C0 21.5798 0 19.7199 0 16Z" fill="#8A8E92"/>
<rect x="6" y="8" width="20" height="16" rx="4" stroke="white" stroke-width="2"/>
<path d="M6 14H26" stroke="white" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 714 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#2C67A5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 16.0781C28 20.988 23.2564 25.0239 18.0597 24.9999H13.5876C8.32866 25.0239 4 20.9891 4 16.0781C4 10.7103 8.32866 6.99846 13.5876 7H18.0597C23.2564 6.99846 28 10.7094 28 16.0781ZM14.9887 11.1098V20.859C16.9525 20.1059 18.3466 18.2098 18.35 15.9841C18.3466 13.7588 16.9525 11.8616 14.9887 11.1098ZM11.2427 11.11C9.27997 11.864 7.88805 13.7601 7.88364 15.9841C7.88805 18.2087 9.27997 20.1037 11.2427 20.857V11.11ZM4.86142 15.9841C4.86252 11.4333 8.55691 7.74563 13.116 7.74409C17.6767 7.74563 21.3717 11.4333 21.3724 15.9841C21.3717 20.5344 17.6767 24.2214 13.116 24.2227C8.55691 24.2214 4.86252 20.5344 4.86142 15.9841Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#232340"/>
<circle cx="16" cy="16" r="9" fill="#ED6B2C"/>
</svg>

After

Width:  |  Height:  |  Size: 196 B

View File

@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#016FD0"/>
<path d="M28 21.3714C28 23.3714 26.3276 25 24.2738 25H4V10.6286C4 8.62857 5.67237 7 7.72616 7H28V21.3714Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.7628 8.21423C21.2959 8.21423 20.0929 9.37138 20.0929 10.8142V13.5142H23.8631C23.9511 13.5142 24.0538 13.5142 24.1271 13.5285C24.978 13.5714 25.6088 13.9999 25.6088 14.7428C25.6088 15.3285 25.1834 15.8285 24.3912 15.9285V15.9571C25.2567 16.0142 25.9169 16.4857 25.9169 17.2142C25.9169 17.9999 25.1834 18.5142 24.2152 18.5142H20.0782V23.7999H23.9951C25.4621 23.7999 26.665 22.6428 26.665 21.1999V8.21423H22.7628ZM21.3984 17.6857H22.9388C22.9608 17.6857 22.9975 17.6821 23.0341 17.6786C23.0708 17.675 23.1075 17.6714 23.1295 17.6714C23.4229 17.6143 23.6723 17.3571 23.6723 17C23.6723 16.6571 23.4229 16.4 23.1295 16.3286C23.0855 16.3143 22.9975 16.3143 22.9388 16.3143H21.3984V17.6857ZM22.9388 14.2999C23.2322 14.3428 23.4816 14.5714 23.4816 14.9142C23.4816 15.2571 23.2322 15.4857 22.9388 15.5285C22.9241 15.5428 22.8361 15.5428 22.7921 15.5428H21.3984V14.2857H22.7921C22.8163 14.2857 22.8494 14.29 22.8792 14.2939C22.9035 14.2971 22.9256 14.2999 22.9388 14.2999Z" fill="#5BB532"/>
<path d="M8.00479 8.21423C6.53779 8.21423 5.33486 9.37138 5.33486 10.8142V17.2285C6.08303 17.5857 6.86053 17.8142 7.63804 17.8142C8.56224 17.8142 9.06102 17.2714 9.06102 16.5285V13.4999H11.3495V16.5142C11.3495 17.6857 10.6014 18.6428 8.06347 18.6428C6.52312 18.6428 5.32019 18.3142 5.32019 18.3142V23.7857H9.23706C10.7041 23.7857 11.907 22.6285 11.907 21.1857V8.21423H8.00479Z" fill="#016DB9"/>
<path d="M15.3839 8.21423C13.9169 8.21423 12.714 9.37138 12.714 10.8142V14.2142C13.3888 13.6571 14.5624 13.2999 16.4548 13.3857C17.467 13.4285 18.5526 13.6999 18.5526 13.6999V14.7999C18.0098 14.5285 17.3644 14.2857 16.5282 14.2285C15.0905 14.1285 14.225 14.8142 14.225 16.0142C14.225 17.2285 15.0905 17.9142 16.5282 17.7999C17.3644 17.7428 18.0098 17.4857 18.5526 17.2285V18.3285C18.5526 18.3285 17.4817 18.5999 16.4548 18.6428C14.5624 18.7285 13.3888 18.3714 12.714 17.8142V23.8142H16.6309C18.0979 23.8142 19.3008 22.6571 19.3008 21.2142V8.21423H15.3839Z" fill="#E7013A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="black"/>
<circle cx="11.5" cy="16" r="7.5" fill="#D72E2B"/>
<circle cx="20.5" cy="16" r="7.5" fill="#EBA140"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 22.0006C17.8217 20.6323 19 18.4538 19 16.0001C19 13.5463 17.8217 11.3678 16 9.99951C14.1783 11.3678 13 13.5463 13 16.0001C13 18.4538 14.1783 20.6323 16 22.0006Z" fill="#ED692C"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#1A1F71"/>
<path d="M12.3895 12.0138L9.04695 19.9782H6.85983L5.20917 13.6232C5.12664 13.2518 5.00284 13.0867 4.71398 12.9217C4.21878 12.6741 3.39345 12.3852 2.69193 12.2614L2.73319 12.0138H6.24083C6.69476 12.0138 7.10743 12.3027 7.18996 12.8391L8.05655 17.461L10.2024 12.0551H12.3895V12.0138ZM20.9317 17.3784C20.9317 15.2739 18.043 15.1501 18.043 14.2422C18.043 13.9533 18.3319 13.6645 18.9096 13.5819C19.1985 13.5407 20.0238 13.4994 20.9317 13.9533L21.3031 12.2614C20.8079 12.0963 20.1889 11.89 19.3635 11.89C17.3415 11.89 15.8972 12.9629 15.8972 14.5311C15.8972 15.6865 16.9288 16.3055 17.7129 16.6769C18.4969 17.0483 18.7858 17.2959 18.7858 17.6673C18.7858 18.2038 18.1668 18.4514 17.5478 18.4514C16.5162 18.4514 15.8972 18.1625 15.4432 17.9562L15.0718 19.6894C15.567 19.8957 16.4336 20.102 17.3415 20.102C19.4873 20.102 20.8904 19.0291 20.9317 17.3784ZM26.2963 19.9782H28.1945L26.5439 12.0138H24.7694C24.3568 12.0138 24.0266 12.2614 23.9028 12.5915L20.8079 19.9782H22.9537L23.3664 18.7815H26.0074L26.2963 19.9782ZM23.9854 17.1721L25.0583 14.2009L25.6773 17.1721H23.9854ZM15.3194 12.0138L13.6275 19.9782H11.5642L13.2561 12.0138H15.3194Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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
});

View File

@ -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 {
});
});
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 {
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 {
}
public loadMoreHistory(top: boolean, justLoad = false) {
// return;
//this.log('loadMoreHistory', top);
if(
!this.peerId ||
@ -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 {
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 {
});
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 {
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 {
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 {
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 {
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;

View File

@ -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 {
// 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 {
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 {
//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) {

View File

@ -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) {
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();
});

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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";
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 = () => {
// @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 = () => {
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 = () => {
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 = {
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 {
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 {
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 {
`;
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';
}
// ! childList for paste first symbol
// 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 {
}
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 {
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 {
}
};
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 {
}
}
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 {
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 {
}
}
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 {
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 {
this.setState(InputState.Error, label);
}
}
export default InputField;

View File

@ -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();
}
}
}

View File

@ -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, {
}> = 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) {
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);

View File

@ -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');

View File

@ -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);

View File

@ -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 {
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',

View File

@ -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');

View File

@ -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
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<{
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
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
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
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
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 = () => {

View File

@ -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();
}

View File

@ -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 {
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 {
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();
}

View File

@ -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 {
placeholder: 'PreviewSender.CaptionPlaceholder',
label: 'Caption',
name: 'photo-caption',
maxLength: this.captionLengthMax
maxLength: this.captionLengthMax,
withLinebreaks: true
});
this.input = this.inputField.input;

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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 {
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);

View File

@ -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,

View File

@ -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();
}

View File

@ -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');

View File

@ -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 {
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();
}

View File

@ -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';
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});
super('popup-stickers', {closable: true, overlayClosable: true, body: true, scrollable: true, title: true});
this.h6 = document.createElement('h6');
this.h6.append(i18n('Loading'));
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 {
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 {
};
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 {
});
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 {
const size = mediaSizes.active.esgSticker.width;
wrapSticker({
await wrapSticker({
doc,
div,
lazyLoadQueue,
@ -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();
});
}
}

View File

@ -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 {
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});
}
}
}

View File

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

View File

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

View File

@ -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();

View File

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

View File

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

View File

@ -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 {
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);
}

View File

@ -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

View File

@ -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";
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,
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,
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;

View File

@ -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;

View File

@ -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";
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
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
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
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]);

View File

@ -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,
loadPromises.push(loadThumbPromise);
}
if(onlyThumb) { // for sticker panel
if(onlyThumb/* || true */) { // for sticker panel
return;
}

View File

@ -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 Normal file

File diff suppressed because one or more lines are too long

9
src/config/font.ts Normal file
View File

@ -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';

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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');
}

View File

@ -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;
}

View File

@ -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
textRange.select();
}
}
(window as any).placeCaretAtEnd = placeCaretAtEnd;

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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<{
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<{
emojiSticker: makeMediaSize(112, 112),
poll: makeMediaSize(330, 0),
round: makeMediaSize(280, 280),
documentName: makeMediaSize(240, 0)
documentName: makeMediaSize(240, 0),
invoice: makeMediaSize(320, 260)
}
};

View File

@ -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;
}

View File

@ -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[];

View File

@ -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(
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(
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(
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));
}

View File

@ -0,0 +1,14 @@
const delta = ''.charCodeAt(0) - '0'.charCodeAt(0);
const buggedRegExp = /[-]/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);
}

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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 = {
//"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 = {
"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 = {
"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 = {
"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 = {
"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 = {
"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",

View File

@ -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 = {
"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
View File

@ -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 {
url: string,
size: number,
mime_type: string,
attributes: Array<DocumentAttribute>
attributes: Array<DocumentAttribute>,
h?: number,
w?: number
};
}

View File

@ -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";

View File

@ -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);

View File

@ -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 {
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 {
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);

View File

@ -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 {
break;
}
// @ts-ignore
case 'messageMediaPending': {
media = inputMedia;
media = (inputMedia as any).messageMedia;
break;
}
}
@ -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 {
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 {
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 {
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 {
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;
});
}

View File

@ -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
});
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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
dcConfigurator: new DcConfigurator,
timeManager: new TimeManager,
appStoragesManager: appStoragesManager,
appStateManager: appStateManager
appStateManager: appStateManager,
appWebDocsManager: new AppWebDocsManager,
appPaymentsManager: new AppPaymentsManager
};
type T = typeof managers;

View File

@ -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";
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 {
protected timeManager: TimeManager;
protected appStoragesManager: AppStoragesManager;
protected appStateManager: AppStateManager;
protected appWebDocsManager: AppWebDocsManager;
protected appPaymentsManager: AppPaymentsManager;
public clear: (init?: boolean) => void;

View File

@ -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 {
fontSize *= window.devicePixelRatio;
ctx.font = `700 ${fontSize}px ${fontFamily}`;
ctx.font = `700 ${fontSize}px ${FontFamily}`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillStyle = 'white';

View File

@ -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};
}

View File

@ -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(
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
});
}

View File

@ -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
};
}

View File

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

View File

@ -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: () => {

View File

@ -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;

View File

@ -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 = {
};
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 {
}
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 {
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 {
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);
});
}
}

View File

@ -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

View File

@ -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 {
];
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);

View File

@ -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 {
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);

View File

@ -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 {
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> {
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 {

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