diff --git a/public/assets/fonts/tgico.svg b/public/assets/fonts/tgico.svg
index 8bdaa672..31c5bb4c 100644
--- a/public/assets/fonts/tgico.svg
+++ b/public/assets/fonts/tgico.svg
@@ -87,135 +87,136 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/assets/fonts/tgico.ttf b/public/assets/fonts/tgico.ttf
index eede64f8..2a9f4501 100644
Binary files a/public/assets/fonts/tgico.ttf and b/public/assets/fonts/tgico.ttf differ
diff --git a/public/assets/fonts/tgico.woff b/public/assets/fonts/tgico.woff
index f987876d..2b4717ae 100644
Binary files a/public/assets/fonts/tgico.woff and b/public/assets/fonts/tgico.woff differ
diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts
index 8669bd3b..a34f1d4a 100644
--- a/src/components/appMediaViewer.ts
+++ b/src/components/appMediaViewer.ts
@@ -29,6 +29,7 @@ import PopupForward from './popups/forward';
import Scrollable from './scrollable';
import appSidebarRight from './sidebarRight';
import AppSharedMediaTab from './sidebarRight/tabs/sharedMedia';
+import PopupElement from './popups';
type AppMediaViewerTargetType = {
element: HTMLElement,
@@ -193,7 +194,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
onDeleteClick = () => {
const target = this.target;
- new PopupDeleteMessages(target.peerId, [target.mid], 'chat', () => {
+ PopupElement.createPopup(PopupDeleteMessages, target.peerId, [target.mid], 'chat', () => {
this.target = {element: this.content.media} as any;
this.close();
});
@@ -203,7 +204,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const target = this.target;
if(target.mid) {
// appSidebarRight.forwardTab.open([target.mid]);
- new PopupForward({
+ PopupElement.createPopup(PopupForward, {
[target.peerId]: [target.mid]
}, () => {
return this.close();
diff --git a/src/components/appMediaViewerNew.ts b/src/components/appMediaViewerNew.ts
index d47b69cf..25c46809 100644
--- a/src/components/appMediaViewerNew.ts
+++ b/src/components/appMediaViewerNew.ts
@@ -1151,7 +1151,7 @@
// onForwardClick = () => {
// if(this.currentMessageId) {
// //appSidebarRight.forwardTab.open([this.currentMessageId]);
-// new PopupForward(this.currentPeerId, [this.currentMessageId], () => {
+// PopupElement.createPopup(PopupForward(this.currentPeerId, [this.currentMessageId], , ) => {
// return this.close();
// });
// }
diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts
index c5de3257..4477c4d5 100644
--- a/src/components/appSearchSuper..ts
+++ b/src/components/appSearchSuper..ts
@@ -77,6 +77,7 @@ import noop from '../helpers/noop';
import wrapMediaSpoiler, {onMediaSpoilerClick} from './wrappers/mediaSpoiler';
import filterAsync from '../helpers/array/filterAsync';
import ChatContextMenu from './chat/contextMenu';
+import PopupElement from './popups';
// const testScroll = false;
@@ -257,7 +258,7 @@ class SearchContextMenu {
if(this.searchSuper.selection.isSelecting) {
simulateClickEvent(this.searchSuper.selection.selectionForwardBtn);
} else {
- new PopupForward({
+ PopupElement.createPopup(PopupForward, {
[this.peerId]: [this.mid]
});
}
@@ -275,7 +276,7 @@ class SearchContextMenu {
if(this.searchSuper.selection.isSelecting) {
simulateClickEvent(this.searchSuper.selection.selectionDeleteBtn);
} else {
- new PopupDeleteMessages(this.peerId, [this.mid], 'chat');
+ PopupElement.createPopup(PopupDeleteMessages, this.peerId, [this.mid], 'chat');
}
};
}
diff --git a/src/components/button.ts b/src/components/button.ts
index e371bfff..dd6640e8 100644
--- a/src/components/button.ts
+++ b/src/components/button.ts
@@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
-import {i18n, LangPackKey} from '../lib/langPack';
+import {FormatterArguments, i18n, LangPackKey} from '../lib/langPack';
import ripple from './ripple';
export type ButtonOptions = Partial<{
@@ -13,6 +13,7 @@ export type ButtonOptions = Partial<{
icon: string,
rippleSquare: true,
text: LangPackKey,
+ textArgs?: FormatterArguments,
disabled: boolean,
asDiv: boolean,
asLink: boolean
@@ -39,7 +40,7 @@ export default function Button(className: string, optio
}
if(options.text) {
- button.append(i18n(options.text));
+ button.append(i18n(options.text, options.textArgs));
}
return button as any;
diff --git a/src/components/buttonCorner.ts b/src/components/buttonCorner.ts
index 5d5a49ff..00a26f31 100644
--- a/src/components/buttonCorner.ts
+++ b/src/components/buttonCorner.ts
@@ -8,6 +8,7 @@ import Button from './button';
const ButtonCorner = (options: Partial<{className: string, icon: string, noRipple: true, onlyMobile: true, asDiv: boolean}> = {}) => {
const button = Button('btn-circle btn-corner z-depth-1' + (options.className ? ' ' + options.className : ''), options);
+ button.tabIndex = -1;
return button;
};
diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts
index 798a2ec1..3e3c89f5 100644
--- a/src/components/chat/bubbles.ts
+++ b/src/components/chat/bubbles.ts
@@ -785,13 +785,30 @@ export default class ChatBubbles {
const bubble = this.bubbles[mid];
if(!bubble) return;
- const message = (await this.chat.getMessage(mid)) as Message.message;
+ const [message, originalMessage] = await Promise.all([
+ (await this.chat.getMessage(mid)) as Message.message,
+ (await this.managers.appMessagesManager.getMessageByPeer(replyToPeerId, replyMid)) as Message.message
+ ]);
+ if(!middleware()) return;
MessageRender.setReply({
chat: this.chat,
bubble,
message
});
+
+ let maxMediaTimestamp: number;
+ const timestamps = bubble.querySelectorAll('.timestamp');
+ if(originalMessage && (maxMediaTimestamp = getMediaDurationFromMessage(originalMessage))) {
+ timestamps.forEach((timestamp) => {
+ const value = +timestamp.dataset.timestamp;
+ if(value < maxMediaTimestamp) {
+ timestamp.classList.remove('is-disabled');
+ } else {
+ timestamp.removeAttribute('href');
+ }
+ });
+ }
});
});
});
@@ -1787,7 +1804,8 @@ export default class ChatBubbles {
return;
}
- new PopupPayment(
+ PopupElement.createPopup(
+ PopupPayment,
message as Message.message,
await this.managers.appPaymentsManager.getInputInvoiceByPeerId(message.peerId, message.mid)
);
@@ -1878,7 +1896,7 @@ export default class ChatBubbles {
const message = await this.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), +mid);
if(message) {
const inputInvoice = await this.managers.appPaymentsManager.getInputInvoiceByPeerId(this.peerId, +bubble.dataset.mid);
- new PopupPayment(message as Message.message, inputInvoice, undefined, true);
+ PopupElement.createPopup(PopupPayment, message as Message.message, inputInvoice, undefined, true);
}
} else {
this.chat.appImManager.setInnerPeer({
@@ -1916,7 +1934,7 @@ export default class ChatBubbles {
const doc = ((message as Message.message).media as MessageMedia.messageMediaDocument)?.document as Document.document;
if(doc?.stickerSetInput) {
- new PopupStickers(doc.stickerSetInput).show();
+ PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show();
}
return;
@@ -1941,7 +1959,7 @@ export default class ChatBubbles {
} else if(target.classList.contains('forward')) {
const mid = +bubble.dataset.mid;
const message = await this.managers.appMessagesManager.getMessageByPeer(this.peerId, mid);
- new PopupForward({
+ PopupElement.createPopup(PopupForward, {
[this.peerId]: await this.managers.appMessagesManager.getMidsByMessage(message)
});
// appSidebarRight.forwardTab.open([mid]);
@@ -3024,6 +3042,16 @@ export default class ChatBubbles {
this.chat.input.setStartParam(startParam);
}
+ if(options.mediaTimestamp) {
+ getHeavyAnimationPromise().then(() => {
+ this.playMediaWithTimestampAndMid({
+ lastMsgId,
+ middleware,
+ mediaTimestamp: options.mediaTimestamp
+ });
+ });
+ }
+
return null;
}
} else {
@@ -3251,13 +3279,11 @@ export default class ChatBubbles {
pause(400) :
Promise.resolve();
p.then(() => {
- return this.getMountedBubble(lastMsgId);
- }).then((mounted) => {
- if(!middleware() || !mounted) {
- return;
- }
-
- this.playMediaWithTimestamp(mounted.bubble, options.mediaTimestamp);
+ return this.playMediaWithTimestampAndMid({
+ lastMsgId,
+ middleware,
+ mediaTimestamp: options.mediaTimestamp
+ });
});
}
@@ -3305,6 +3331,24 @@ export default class ChatBubbles {
return {cached, promise: setPeerPromise};
}
+ public playMediaWithTimestampAndMid({
+ middleware,
+ lastMsgId,
+ mediaTimestamp
+ }: {
+ middleware: () => boolean,
+ lastMsgId: number,
+ mediaTimestamp: number
+ }) {
+ this.getMountedBubble(lastMsgId).then((mounted) => {
+ if(!middleware() || !mounted) {
+ return;
+ }
+
+ this.playMediaWithTimestamp(mounted.bubble, mediaTimestamp);
+ });
+ }
+
public playMediaWithTimestamp(element: HTMLElement, timestamp: number) {
const bubble = findUpClassName(element, 'bubble');
const groupedItem = findUpClassName(element, 'grouped-item');
@@ -3325,6 +3369,19 @@ export default class ChatBubbles {
audio.playWithTimestamp(timestamp);
return;
}
+
+ const replyToPeerId = bubble.dataset.replyToPeerId.toPeerId();
+ const replyToMid = +bubble.dataset.replyToMid;
+ if(replyToPeerId && replyToMid) {
+ if(replyToPeerId === this.peerId) {
+ this.chat.setMessageId(replyToMid, timestamp);
+ } else {
+ this.chat.appImManager.setInnerPeer({
+ peerId: replyToPeerId,
+ mediaTimestamp: timestamp
+ });
+ }
+ }
}
private async setFetchReactionsInterval(afterSetPromise: Promise) {
@@ -3937,11 +3994,28 @@ export default class ChatBubbles {
customEmojiSize ??= this.chat.appImManager.customEmojiSize;
- const maxMediaTimestamp = getMediaDurationFromMessage(albumTextMessage || message as Message.message);
+ let maxMediaTimestamp = getMediaDurationFromMessage(albumTextMessage || message as Message.message);
if(albumTextMessage && needToSetHTML) {
bubble.dataset.textMid = '' + albumTextMessage.mid;
}
+ if(message.reply_to) {
+ const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
+ bubble.dataset.replyToPeerId = '' + replyToPeerId;
+ bubble.dataset.replyToMid = '' + message.reply_to_mid;
+
+ if(maxMediaTimestamp === undefined) {
+ const originalMessage = await rootScope.managers.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid);
+ if(originalMessage) {
+ maxMediaTimestamp = getMediaDurationFromMessage(originalMessage as Message.message);
+ } else {
+ // this.managers.appMessagesManager.fetchMessageReplyTo(message);
+ // this.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid});
+ maxMediaTimestamp = Infinity;
+ }
+ }
+ }
+
const richTextOptions: Parameters[1] = {
entities: totalEntities,
passEntities: this.passEntities,
@@ -4095,7 +4169,7 @@ export default class ChatBubbles {
}
return new Promise((resolve, reject) => {
- const popup = new PopupForward(undefined, (peerId) => {
+ const popup = PopupElement.createPopup(PopupForward, undefined, (peerId) => {
resolve(peerId);
});
@@ -5726,7 +5800,7 @@ export default class ChatBubbles {
if(sponsoredMessage.chat_invite) {
callback = () => {
- new PopupJoinChatInvite(sponsoredMessage.chat_invite_hash, sponsoredMessage.chat_invite as ChatInvite.chatInvite);
+ PopupElement.createPopup(PopupJoinChatInvite, sponsoredMessage.chat_invite_hash, sponsoredMessage.chat_invite as ChatInvite.chatInvite);
};
} else if(sponsoredMessage.chat_invite_hash) {
callback = () => {
@@ -6147,6 +6221,11 @@ export default class ChatBubbles {
return result;
}
+ // private async getDiscussionMessages() {
+ // const mids = await this.chat.getMidsByMid(this.chat.threadId);
+ // return Promise.all(mids.map((mid) => this.chat.getMessage(mid)));
+ // }
+
/**
* Load and render history
* @param maxId max message id
diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts
index 176b749c..15e195fa 100644
--- a/src/components/chat/chat.ts
+++ b/src/components/chat/chat.ts
@@ -571,11 +571,12 @@ export default class Chat extends EventListenerBase<{
this.autoDownload = await getAutoDownloadSettingsByPeerId(this.peerId);
}
- public setMessageId(messageId?: number) {
+ public setMessageId(messageId?: number, mediaTimestamp?: number) {
return this.setPeer({
peerId: this.peerId,
threadId: this.threadId,
- lastMsgId: messageId
+ lastMsgId: messageId,
+ mediaTimestamp
});
}
@@ -711,4 +712,9 @@ export default class Chat extends EventListenerBase<{
public isPinnedMessagesNeeded() {
return this.type === 'chat' || this.isForum;
}
+
+ public canGiftPremium() {
+ const peerId = this.peerId;
+ return peerId.isUser() && this.managers.appProfileManager.canGiftPremium(this.peerId.toUserId());
+ }
}
diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts
index d07c427d..d56ca3ff 100644
--- a/src/components/chat/contextMenu.ts
+++ b/src/components/chat/contextMenu.ts
@@ -48,6 +48,7 @@ import PopupStickers from '../popups/stickers';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
import canSaveMessageMedia from '../../lib/appManagers/utils/messages/canSaveMessageMedia';
import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText';
+import PopupElement from '../popups';
export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise, notDirect?: () => boolean, withSelection?: true, isSponsored?: true, localName?: 'views' | 'emojis'})[];
@@ -490,7 +491,7 @@ export default class ChatContextMenu {
icon: 'flag',
text: 'ReportChat',
onClick: () => {
- new PopupReportMessages(this.peerId, [this.mid]);
+ PopupElement.createPopup(PopupReportMessages, this.peerId, [this.mid]);
},
verify: async() => !this.message.pFlags.out && this.message._ === 'message' && !this.message.pFlags.is_outgoing && await this.managers.appPeersManager.isChannel(this.peerId),
notDirect: () => true,
@@ -516,7 +517,7 @@ export default class ChatContextMenu {
peerId: this.viewerPeerId
});
} else if(this.canOpenReactedList) {
- new PopupReactedList(this.message as Message.message);
+ PopupElement.createPopup(PopupReactedList, this.message as Message.message);
} else {
return false;
}
@@ -540,7 +541,7 @@ export default class ChatContextMenu {
icon: 'info',
text: 'Chat.Message.Sponsored.What',
onClick: () => {
- new PopupSponsored();
+ PopupElement.createPopup(PopupSponsored);
},
verify: () => false,
isSponsored: true
@@ -549,7 +550,7 @@ export default class ChatContextMenu {
text: 'Loading',
onClick: () => {
this.emojiInputsPromise.then((inputs) => {
- new PopupStickers(inputs, true).show();
+ PopupElement.createPopup(PopupStickers, inputs, true).show();
});
},
verify: () => !!this.getUniqueCustomEmojisFromMessage().length,
@@ -890,7 +891,7 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) {
simulateClickEvent(this.chat.selection.selectionSendNowBtn);
} else {
- new PopupSendNow(this.peerId, await this.chat.getMidsByMid(this.mid));
+ PopupElement.createPopup(PopupSendNow, this.peerId, await this.chat.getMidsByMid(this.mid));
}
};
@@ -929,11 +930,11 @@ export default class ChatContextMenu {
};
private onPinClick = () => {
- new PopupPinMessage(this.peerId, this.mid);
+ PopupElement.createPopup(PopupPinMessage, this.peerId, this.mid);
};
private onUnpinClick = () => {
- new PopupPinMessage(this.peerId, this.mid, true);
+ PopupElement.createPopup(PopupPinMessage, this.peerId, this.mid, true);
};
private onRetractVote = () => {
@@ -968,7 +969,7 @@ export default class ChatContextMenu {
if(this.chat.selection.isSelecting) {
simulateClickEvent(this.chat.selection.selectionDeleteBtn);
} else {
- new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type);
+ PopupElement.createPopup(PopupDeleteMessages, this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type);
}
};
diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts
index b9a554ac..f3733487 100644
--- a/src/components/chat/input.ts
+++ b/src/components/chat/input.ts
@@ -111,6 +111,7 @@ import {MARKDOWN_ENTITIES} from '../../lib/richTextProcessor';
import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport';
import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport';
import {ChatRights} from '../../lib/appManagers/appChatsManager';
+import PopupGiftPremium from '../popups/giftPremium';
const RECORD_MIN_TIME = 500;
@@ -644,6 +645,11 @@ export default class ChatInput {
this.fileInput.click();
}
// verify: () => this.chat.canSend('send_docs')
+ }, {
+ icon: 'gift',
+ text: 'GiftPremium',
+ onClick: () => this.chat.appImManager.giftPremium(this.chat.peerId),
+ verify: () => this.chat.canGiftPremium()
}, {
icon: 'poll',
text: 'Poll',
@@ -1049,7 +1055,7 @@ export default class ChatInput {
this.listenerSetter.add(this.pinnedControlBtn)('click', () => {
const peerId = this.chat.peerId;
- new PopupPinMessage(peerId, 0, true, () => {
+ PopupElement.createPopup(PopupPinMessage, peerId, 0, true, () => {
this.chat.appImManager.setPeer(); // * close tab
// ! костыль, это скроет закреплённые сообщения сразу, вместо того, чтобы ждать пока анимация перехода закончится
@@ -1219,7 +1225,7 @@ export default class ChatInput {
const middleware = this.chat.bubbles.getMiddleware();
const canSendWhenOnline = rootScope.myId !== peerId && peerId.isUser() && await this.managers.appUsersManager.isUserOnlineVisible(peerId);
- new PopupSchedule(initDate, (timestamp) => {
+ PopupElement.createPopup(PopupSchedule, initDate, (timestamp) => {
if(!middleware()) {
return;
}
@@ -2470,7 +2476,7 @@ export default class ChatInput {
opusDecodeController.setKeepAlive(true);
const showDiscardPopup = () => {
- new PopupPeer('popup-cancel-record', {
+ PopupElement.createPopup(PopupPeer, 'popup-cancel-record', {
titleLangKey: 'DiscardVoiceMessageTitle',
descriptionLangKey: 'DiscardVoiceMessageDescription',
buttons: [{
@@ -2614,7 +2620,7 @@ export default class ChatInput {
}
if(!draftsAreEqual(draft, originalDraft)) {
- new PopupPeer('discard-editing', {
+ PopupElement.createPopup(PopupPeer, 'discard-editing', {
buttons: [{
langKey: 'Alert.Confirm.Discard',
callback: () => {
@@ -2657,7 +2663,7 @@ export default class ChatInput {
this.clearHelper();
this.updateSendBtn();
let selected = false;
- const popup = new PopupForward(forwarding, () => {
+ const popup = PopupElement.createPopup(PopupForward, forwarding, () => {
selected = true;
});
@@ -2800,7 +2806,7 @@ export default class ChatInput {
this.onMessageSent();
} else {
- new PopupDeleteMessages(peerId, [editMsgId], chat.type);
+ PopupElement.createPopup(PopupDeleteMessages, peerId, [editMsgId], chat.type);
return;
}
diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts
index 1e2bb806..77cdbc86 100644
--- a/src/components/chat/pinnedMessage.ts
+++ b/src/components/chat/pinnedMessage.ts
@@ -23,6 +23,7 @@ import throttle from '../../helpers/schedulers/throttle';
import {AppManagers} from '../../lib/appManagers/managers';
import {Message} from '../../layer';
import {logger} from '../../lib/logger';
+import PopupElement from '../popups';
class AnimatedSuper {
static DURATION = 200;
@@ -275,9 +276,9 @@ export default class ChatPinnedMessage {
divAndCaption: dAC,
onClose: async() => {
if(await managers.appPeersManager.canPinMessage(this.chat.peerId)) {
- new PopupPinMessage(this.chat.peerId, this.pinnedMid, true);
+ PopupElement.createPopup(PopupPinMessage, this.chat.peerId, this.pinnedMid, true);
} else {
- new PopupPinMessage(this.chat.peerId, 0, true);
+ PopupElement.createPopup(PopupPinMessage, this.chat.peerId, 0, true);
}
return false;
diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts
index d53d129c..8efcdccb 100644
--- a/src/components/chat/selection.ts
+++ b/src/components/chat/selection.ts
@@ -38,6 +38,7 @@ import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuList
import filterUnique from '../../helpers/array/filterUnique';
import appImManager from '../../lib/appManagers/appImManager';
import {Message} from '../../layer';
+import PopupElement from '../popups';
const accumulateMapSet = (map: Map>) => {
return [...map.values()].reduce((acc, v) => acc + v.size, 0);
@@ -676,7 +677,7 @@ export class SearchSelection extends AppSelection {
obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b);
}
- new PopupForward(obj, () => {
+ PopupElement.createPopup(PopupForward, obj, () => {
this.cancelSelection();
});
}, attachClickOptions);
@@ -685,7 +686,7 @@ export class SearchSelection extends AppSelection {
this.selectionDeleteBtn = ButtonIcon(`delete danger ${BASE_CLASS}-delete`);
attachClickEvent(this.selectionDeleteBtn, () => {
const peerId = [...this.selectedMids.keys()][0];
- new PopupDeleteMessages(peerId, [...this.selectedMids.get(peerId)], 'chat', () => {
+ PopupElement.createPopup(PopupDeleteMessages, peerId, [...this.selectedMids.get(peerId)], 'chat', () => {
this.cancelSelection();
});
}, attachClickOptions);
@@ -949,7 +950,7 @@ export default class ChatSelection extends AppSelection {
this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'});
this.selectionSendNowBtn.append(i18n('MessageScheduleSend'));
attachClickEvent(this.selectionSendNowBtn, () => {
- new PopupSendNow(this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], () => {
+ PopupElement.createPopup(PopupSendNow, this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], () => {
this.cancelSelection();
});
}, attachClickOptions);
@@ -962,7 +963,7 @@ export default class ChatSelection extends AppSelection {
obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b);
}
- new PopupForward(obj, () => {
+ PopupElement.createPopup(PopupForward, obj, () => {
this.cancelSelection();
});
}, attachClickOptions);
@@ -971,7 +972,7 @@ export default class ChatSelection extends AppSelection {
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger text-bold selection-container-delete', {icon: 'delete'});
this.selectionDeleteBtn.append(i18n('Delete'));
attachClickEvent(this.selectionDeleteBtn, () => {
- new PopupDeleteMessages(this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], this.chat.type, () => {
+ PopupElement.createPopup(PopupDeleteMessages, this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], this.chat.type, () => {
this.cancelSelection();
});
}, attachClickOptions);
diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts
index 5609f9b0..65c8d334 100644
--- a/src/components/chat/topbar.ts
+++ b/src/components/chat/topbar.ts
@@ -47,6 +47,7 @@ import apiManagerProxy from '../../lib/mtproto/mtprotoworker';
import {makeMediaSize} from '../../helpers/mediaSize';
import {FOLDER_ID_ALL} from '../../lib/mtproto/mtproto_config';
import formatNumber from '../../helpers/number/formatNumber';
+import PopupElement from '../popups';
type ButtonToVerify = {element?: HTMLElement, verify: () => boolean | Promise};
@@ -419,11 +420,11 @@ export default class ChatTopbar {
text: 'ShareContact',
onClick: () => {
const contactPeerId = this.peerId;
- new PopupPickUser({
+ PopupElement.createPopup(PopupPickUser, {
peerTypes: ['dialogs', 'contacts'],
onSelect: (peerId) => {
return new Promise((resolve, reject) => {
- new PopupPeer('', {
+ PopupElement.createPopup(PopupPeer, '', {
titleLangKey: 'SendMessageTitle',
descriptionLangKey: 'SendContactToGroupText',
descriptionLangArgs: [new PeerTitle({peerId, dialog: true}).element],
@@ -453,6 +454,11 @@ export default class ChatTopbar {
});
},
verify: async() => rootScope.myId !== this.peerId && this.peerId.isUser() && (await this.managers.appPeersManager.isContact(this.peerId)) && !!(await this.managers.appUsersManager.getUser(this.peerId.toUserId())).phone
+ }, {
+ icon: 'gift',
+ text: 'GiftPremium',
+ onClick: () => this.chat.appImManager.giftPremium(this.peerId),
+ verify: () => this.chat.canGiftPremium()
}, {
icon: 'bots',
text: 'Settings',
@@ -471,7 +477,7 @@ export default class ChatTopbar {
icon: 'lock',
text: 'BlockUser',
onClick: () => {
- new PopupPeer('', {
+ PopupElement.createPopup(PopupPeer, '', {
peerId: this.peerId,
titleLangKey: 'BlockUser',
descriptionLangKey: 'AreYouSureBlockContact2',
@@ -512,7 +518,7 @@ export default class ChatTopbar {
icon: 'delete danger',
text: 'Delete',
onClick: () => {
- new PopupDeleteDialog(this.peerId/* , 'leave' */);
+ PopupElement.createPopup(PopupDeleteDialog, this.peerId/* , 'leave' */);
},
verify: async() => this.chat.type === 'chat' && !!(await this.managers.appMessagesManager.getDialogOnly(this.peerId))
}];
@@ -680,7 +686,7 @@ export default class ChatTopbar {
}
private onMuteClick = () => {
- new PopupMute(this.peerId);
+ PopupElement.createPopup(PopupMute, this.peerId);
};
private onResize = () => {
diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts
index bcc1439e..ceabba4b 100644
--- a/src/components/checkboxField.ts
+++ b/src/components/checkboxField.ts
@@ -26,7 +26,8 @@ export type CheckboxFieldOptions = {
restriction?: boolean,
withRipple?: boolean,
withHover?: boolean,
- listenerSetter?: ListenerSetter
+ listenerSetter?: ListenerSetter,
+ asRadio?: boolean
};
export default class CheckboxField {
public input: HTMLInputElement;
@@ -54,9 +55,9 @@ export default class CheckboxField {
const input = this.input = document.createElement('input');
input.classList.add('checkbox-field-input');
- input.type = 'checkbox';
+ input.type = options.asRadio ? 'radio' : 'checkbox';
if(options.name) {
- input.id = 'input-' + options.name;
+ input[options.asRadio ? 'name' : 'id'] = 'input-' + options.name;
}
if(options.checked) {
diff --git a/src/components/confirmationPopup.ts b/src/components/confirmationPopup.ts
index f9331485..14ec6dce 100644
--- a/src/components/confirmationPopup.ts
+++ b/src/components/confirmationPopup.ts
@@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
-import {addCancelButton} from './popups';
+import PopupElement, {addCancelButton} from './popups';
import PopupPeer, {PopupPeerCheckboxOptions, PopupPeerOptions} from './popups/peer';
// type PopupConfirmationOptions = Pick;
@@ -35,6 +35,6 @@ export default function confirmationPopup(
options.buttons = buttons;
options.checkboxes ??= checkbox && [checkbox];
- new PopupPeer('popup-confirmation', options).show();
+ PopupElement.createPopup(PopupPeer, 'popup-confirmation', options).show();
});
}
diff --git a/src/components/dialogsContextMenu.ts b/src/components/dialogsContextMenu.ts
index 2c385a5b..e60eba05 100644
--- a/src/components/dialogsContextMenu.ts
+++ b/src/components/dialogsContextMenu.ts
@@ -18,6 +18,7 @@ import {AppManagers} from '../lib/appManagers/managers';
import {GENERAL_TOPIC_ID} from '../lib/mtproto/mtproto_config';
import showLimitPopup from './popups/limit';
import createContextMenu from '../helpers/dom/createContextMenu';
+import PopupElement from './popups';
export default class DialogsContextMenu {
private buttons: ButtonMenuItemOptionsVerifiable[];
@@ -216,7 +217,7 @@ export default class DialogsContextMenu {
};
private onMuteClick = () => {
- new PopupMute(this.peerId, this.threadId);
+ PopupElement.createPopup(PopupMute, this.peerId, this.threadId);
};
private onUnreadClick = async() => {
@@ -233,6 +234,6 @@ export default class DialogsContextMenu {
};
private onDeleteClick = () => {
- new PopupDeleteDialog(this.peerId, undefined, undefined, this.threadId);
+ PopupElement.createPopup(PopupDeleteDialog, this.peerId, undefined, undefined, this.threadId);
};
}
diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts
index 3d3b2c25..9a1e7107 100644
--- a/src/components/emoticonsDropdown/tabs/emoji.ts
+++ b/src/components/emoticonsDropdown/tabs/emoji.ts
@@ -40,6 +40,7 @@ import {hideToast, toastNew} from '../../toast';
import safeAssign from '../../../helpers/object/safeAssign';
import type {AppStickersManager} from '../../../lib/appManagers/appStickersManager';
import liteMode from '../../../helpers/liteMode';
+import PopupElement from '../../popups';
const loadedURLs: Set = new Set();
export function appendEmoji(emoji: string, container?: HTMLElement, prepend = false, unify = false) {
@@ -703,7 +704,7 @@ export default class EmojiTab extends EmoticonsTabC {
return;
}
- new PopupStickers({id: category.set.id, access_hash: category.set.access_hash}, true).show();
+ PopupElement.createPopup(PopupStickers, {id: category.set.id, access_hash: category.set.access_hash}, true).show();
return;
}
diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts
index f13badda..bfe4cf13 100644
--- a/src/components/emoticonsDropdown/tabs/stickers.ts
+++ b/src/components/emoticonsDropdown/tabs/stickers.ts
@@ -38,6 +38,7 @@ import {AnyFunction} from '../../../types';
import {IgnoreMouseOutType} from '../../../helpers/dropdownHover';
import customProperties from '../../../helpers/dom/customProperties';
import windowSize from '../../../helpers/windowSize';
+import PopupElement from '../../popups';
export class SuperStickerRenderer {
public lazyLoadQueue: LazyLoadQueueRepeat;
@@ -617,7 +618,7 @@ export default class StickersTab extends EmoticonsTabC a.months - b.months)[0];
+
+ const wrapCurrency = (amount: number | string) => paymentsWrapCurrencyAmount(amount, shortestOption.currency, false, true);
+
+ const rows = this.giftOptions.map((giftOption, idx) => {
+ let subtitle = i18n('PricePerMonth', [wrapCurrency(+giftOption.amount / giftOption.months)]);
+ if(giftOption !== shortestOption) {
+ const span = document.createElement('span');
+ const badge = document.createElement('span');
+ badge.classList.add(className + '-discount');
+ const shortestAmount = +shortestOption.amount * giftOption.months / shortestOption.months;
+ const discount = Math.round((1 - +giftOption.amount / shortestAmount) * 100);
+ badge.textContent = '-' + discount + '%';
+ span.append(badge, subtitle);
+ subtitle = span;
+ }
+
+ const isYears = !(giftOption.months % 12);
+ const checkboxField = new CheckboxField({
+ // text: 'Months',
+ // textArgs: [giftOption.months],
+ checked: idx === 0,
+ round: true,
+ name: 'gift-months',
+ asRadio: true
+ });
+
+ const row = new Row({
+ title: i18n(isYears ? 'Years' : 'Months', [isYears ? giftOption.months / 12 : giftOption.months]),
+ checkboxField,
+ clickable: true,
+ subtitle,
+ titleRightSecondary: wrapCurrency(giftOption.amount)
+ });
+
+ row.container.classList.add(className + '-option');
+
+ return row;
+ });
+
+ const form = document.createElement('form');
+ form.classList.add(className + '-options');
+ form.append(...rows.map((row) => row.container));
+
+ const buttonText = new I18n.IntlElement({key: 'GiftSubscriptionFor', args: [wrapCurrency(giftOptions[0].amount)]});
+
+ const getSelectedOption = () => giftOptions[rows.findIndex((row) => row.checkboxField.checked)];
+
+ this.listenerSetter.add(form)('change', () => {
+ buttonText.compareAndUpdate({
+ args: [
+ wrapCurrency(getSelectedOption().amount)
+ ]
+ });
+ });
+
+ const giftButton = Button(`btn-primary ${className}-confirm shimmer`);
+ giftButton.append(buttonText.element);
+
+ attachClickEvent(giftButton, () => {
+ const giftOption = getSelectedOption();
+ appImManager.openUrl(giftOption.bot_url);
+ this.hide();
+ }, {listenerSetter: this.listenerSetter});
+
+ this.scrollable.append(
+ avatar,
+ title,
+ subtitle,
+ form,
+ giftButton
+ );
+
+ this.show();
+ }
+}
diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts
index 283643f8..05f4f216 100644
--- a/src/components/popups/index.ts
+++ b/src/components/popups/index.ts
@@ -318,7 +318,7 @@ export default class PopupElement extends
return this.POPUPS.filter((element) => element instanceof popupConstructor) as T[];
}
- public static createPopup>(ctor: {new(...args: A): T}, ...args: A) {
+ public static createPopup>(ctor: {new(...args: A): T}, ...args: A) {
const popup = new ctor(...args);
return popup;
}
diff --git a/src/components/popups/payment.ts b/src/components/popups/payment.ts
index c6460743..cf7fbf91 100644
--- a/src/components/popups/payment.ts
+++ b/src/components/popups/payment.ts
@@ -531,7 +531,7 @@ export default class PopupPayment extends PopupElement {
};
const onMethodClick = () => {
- new PopupPaymentCard(paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => {
+ PopupElement.createPopup(PopupPaymentCard, paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => {
previousToken = token, previousCardDetails = card;
setCardSubtitle(card);
@@ -587,7 +587,7 @@ export default class PopupPayment extends PopupElement {
if(!isReceipt) {
onShippingAddressClick = (focus) => {
- new PopupPaymentShipping(paymentForm as PaymentsPaymentForm, inputInvoice, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => {
+ PopupElement.createPopup(PopupPaymentShipping, paymentForm as PaymentsPaymentForm, inputInvoice, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => {
lastRequestedInfo = requestedInfo;
savedInfo = (paymentForm as PaymentsPaymentForm).saved_info = shippingAddress;
setShippingInfo(shippingAddress);
@@ -643,7 +643,7 @@ export default class PopupPayment extends PopupElement {
icon: 'shipping',
titleLangKey: 'PaymentCheckoutShippingMethod',
clickable: !isReceipt && (onShippingMethodClick = () => {
- new PopupPaymentShippingMethods(paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => {
+ PopupElement.createPopup(PopupPaymentShippingMethods, paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => {
setShippingOption(shippingOption);
});
})
@@ -736,7 +736,7 @@ export default class PopupPayment extends PopupElement {
}
Promise.resolve(passwordState ?? this.managers.passwordManager.getState()).then((_passwordState) => {
- new PopupPaymentCardConfirmation(savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => {
+ PopupElement.createPopup(PopupPaymentCardConfirmation, savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => {
passwordState = undefined;
lastTmpPasword = tmpPassword;
simulateClickEvent(payButton);
@@ -783,7 +783,7 @@ export default class PopupPayment extends PopupElement {
if(paymentResult._ === 'payments.paymentResult') {
onConfirmed();
} else {
- popupPaymentVerification = new PopupPaymentVerification(paymentResult.url, !mediaInvoice.extended_media);
+ popupPaymentVerification = PopupElement.createPopup(PopupPaymentVerification, paymentResult.url, !mediaInvoice.extended_media);
popupPaymentVerification.addEventListener('finish', () => {
popupPaymentVerification = undefined;
diff --git a/src/components/popups/reportMessages.ts b/src/components/popups/reportMessages.ts
index 436212f6..8fac2eb2 100644
--- a/src/components/popups/reportMessages.ts
+++ b/src/components/popups/reportMessages.ts
@@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
+import PopupElement from '.';
import {attachClickEvent} from '../../helpers/dom/clickEvent';
import findUpClassName from '../../helpers/dom/findUpClassName';
import whichChild from '../../helpers/dom/whichChild';
@@ -45,7 +46,7 @@ export default class PopupReportMessages extends PopupPeer {
preloadStickerPromise.then(() => {
this.hide();
- new PopupReportMessagesConfirm(peerId, mids, reason, onConfirm);
+ PopupElement.createPopup(PopupReportMessagesConfirm, peerId, mids, reason, onConfirm);
});
}, {listenerSetter: this.listenerSetter});
diff --git a/src/components/popups/unpinMessage.ts b/src/components/popups/unpinMessage.ts
index 2f733a39..5d76dbef 100644
--- a/src/components/popups/unpinMessage.ts
+++ b/src/components/popups/unpinMessage.ts
@@ -110,7 +110,7 @@ export default class PopupPinMessage {
addCancelButton(buttons);
- const popup = new PopupPeer('popup-delete-chat', {
+ const popup = PopupElement.createPopup(PopupPeer, 'popup-delete-chat', {
peerId,
titleLangKey: title,
descriptionLangKey: description,
diff --git a/src/components/row.ts b/src/components/row.ts
index 816932e1..3eeda311 100644
--- a/src/components/row.ts
+++ b/src/components/row.ts
@@ -33,7 +33,6 @@ export type RowMediaSizeType = 'small' | 'medium' | 'big' | 'abitbigger' | 'bigg
export default class Row {
public container: HTMLElement;
- public title: HTMLElement;
public titleRow: HTMLElement;
public titleRight: HTMLElement;
public media: HTMLElement;
@@ -48,6 +47,7 @@ export default class Row {
public buttonRight: HTMLElement;
+ private _title: HTMLElement;
private _subtitle: HTMLElement;
private _midtitle: HTMLElement;
@@ -127,6 +127,9 @@ export default class Row {
options.titleRight = this.checkboxField.label;
} else {
havePadding = true;
+ if(!this.checkboxField.span) {
+ this.checkboxField.label.classList.add('checkbox-field-absolute');
+ }
this.container.append(this.checkboxField.label);
}
@@ -144,7 +147,7 @@ export default class Row {
i.label.classList.add('disable-hover');
}
- if(options.title || options.titleLangKey) {
+ if(options.title || options.titleLangKey || options.titleRight || options.titleRightSecondary) {
let c: HTMLElement;
const titleRightContent = options.titleRight || options.titleRightSecondary;
if(titleRightContent) {
@@ -154,7 +157,7 @@ export default class Row {
c = this.container;
}
- this.title = this.createTitle();
+ this._title = this.createTitle();
if(options.noWrap) this.title.classList.add('no-wrap');
if(options.title) {
setContent(this.title, options.title);
@@ -227,6 +230,10 @@ export default class Row {
}
}
+ public get title() {
+ return this._title;
+ }
+
public get subtitle() {
return this._subtitle ??= this.createSubtitle();
}
diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts
index 7cea8180..5f238c30 100644
--- a/src/components/sidebarLeft/index.ts
+++ b/src/components/sidebarLeft/index.ts
@@ -288,6 +288,7 @@ export class AppSidebarLeft extends SidebarSlider {
}]
});
this.newBtnMenu.className = 'btn-circle rp btn-corner z-depth-1 btn-menu-toggle animated-button-icon';
+ this.newBtnMenu.tabIndex = -1;
this.newBtnMenu.insertAdjacentHTML('afterbegin', `
@@ -297,6 +298,7 @@ export class AppSidebarLeft extends SidebarSlider {
this.updateBtn = document.createElement('div');
this.updateBtn.className = 'btn-circle rp btn-corner z-depth-1 btn-update is-hidden';
+ this.updateBtn.tabIndex = -1;
ripple(this.updateBtn);
this.updateBtn.append(i18n('Update'));
diff --git a/src/components/sidebarLeft/tabs/2fa/email.ts b/src/components/sidebarLeft/tabs/2fa/email.ts
index f0753007..01f74fa2 100644
--- a/src/components/sidebarLeft/tabs/2fa/email.ts
+++ b/src/components/sidebarLeft/tabs/2fa/email.ts
@@ -18,6 +18,7 @@ import {attachClickEvent} from '../../../../helpers/dom/clickEvent';
import matchEmail from '../../../../lib/richTextProcessor/matchEmail';
import wrapStickerEmoji from '../../../wrappers/stickerEmoji';
import SettingSection from '../../../settingSection';
+import PopupElement from '../../../popups';
export default class AppTwoStepVerificationEmailTab extends SliderSuperTab {
public inputField: InputField;
@@ -125,7 +126,7 @@ export default class AppTwoStepVerificationEmailTab extends SliderSuperTab {
};
attachClickEvent(btnSkip, (e) => {
- const popup = new PopupPeer('popup-skip-email', {
+ const popup = PopupElement.createPopup(PopupPeer, 'popup-skip-email', {
buttons: [{
langKey: 'Cancel',
isCancel: true
diff --git a/src/components/sidebarLeft/tabs/2fa/index.ts b/src/components/sidebarLeft/tabs/2fa/index.ts
index 1cd17ac3..65e5cd1b 100644
--- a/src/components/sidebarLeft/tabs/2fa/index.ts
+++ b/src/components/sidebarLeft/tabs/2fa/index.ts
@@ -8,6 +8,7 @@ import {attachClickEvent} from '../../../../helpers/dom/clickEvent';
import {AccountPassword} from '../../../../layer';
import {_i18n} from '../../../../lib/langPack';
import Button from '../../../button';
+import PopupElement from '../../../popups';
import PopupPeer from '../../../popups/peer';
import SettingSection from '../../../settingSection';
import {SliderSuperTab} from '../../../slider';
@@ -57,7 +58,7 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab {
});
attachClickEvent(btnDisablePassword, () => {
- const popup = new PopupPeer('popup-disable-password', {
+ const popup = PopupElement.createPopup(PopupPeer, 'popup-disable-password', {
buttons: [{
langKey: 'Disable',
callback: () => {
diff --git a/src/components/sidebarLeft/tabs/activeSessions.ts b/src/components/sidebarLeft/tabs/activeSessions.ts
index 106f0de7..a43ef7b6 100644
--- a/src/components/sidebarLeft/tabs/activeSessions.ts
+++ b/src/components/sidebarLeft/tabs/activeSessions.ts
@@ -21,6 +21,7 @@ import {attachContextMenuListener} from '../../../helpers/dom/attachContextMenuL
import positionMenu from '../../../helpers/positionMenu';
import contextMenuController from '../../../helpers/contextMenuController';
import SettingSection from '../../settingSection';
+import PopupElement from '../../popups';
export default class AppActiveSessionsTab extends SliderSuperTabEventable {
public authorizations: Authorization.authorization[];
@@ -61,7 +62,7 @@ export default class AppActiveSessionsTab extends SliderSuperTabEventable {
if(authorizations.length) {
const btnTerminate = Button('btn-primary btn-transparent danger', {icon: 'stop', text: 'TerminateAllSessions'});
attachClickEvent(btnTerminate, (e) => {
- new PopupPeer('revoke-session', {
+ PopupElement.createPopup(PopupPeer, 'revoke-session', {
buttons: [{
langKey: 'Terminate',
isDanger: true,
@@ -112,7 +113,7 @@ export default class AppActiveSessionsTab extends SliderSuperTabEventable {
const onTerminateClick = () => {
const hash = target.dataset.hash;
- new PopupPeer('revoke-session', {
+ PopupElement.createPopup(PopupPeer, 'revoke-session', {
buttons: [{
langKey: 'Terminate',
isDanger: true,
diff --git a/src/components/sidebarLeft/tabs/blockedUsers.ts b/src/components/sidebarLeft/tabs/blockedUsers.ts
index 5f38dbff..863c8415 100644
--- a/src/components/sidebarLeft/tabs/blockedUsers.ts
+++ b/src/components/sidebarLeft/tabs/blockedUsers.ts
@@ -19,6 +19,7 @@ import positionMenu from '../../../helpers/positionMenu';
import contextMenuController from '../../../helpers/contextMenuController';
import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames';
import SettingSection from '../../settingSection';
+import PopupElement from '../../popups';
export default class AppBlockedUsersTab extends SliderSuperTab {
public peerIds: PeerId[];
@@ -40,7 +41,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab {
this.content.append(btnAdd);
attachClickEvent(btnAdd, (e) => {
- new PopupPickUser({
+ PopupElement.createPopup(PopupPickUser, {
peerTypes: ['contacts'],
placeholder: 'BlockModal.Search.Placeholder',
onSelect: (peerId) => {
diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts
index 26e7af7a..6942d099 100644
--- a/src/components/sidebarLeft/tabs/editFolder.ts
+++ b/src/components/sidebarLeft/tabs/editFolder.ts
@@ -24,6 +24,7 @@ import wrapDraftText from '../../../lib/richTextProcessor/wrapDraftText';
import filterAsync from '../../../helpers/array/filterAsync';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import SettingSection from '../../settingSection';
+import PopupElement from '../../popups';
const MAX_FOLDER_NAME_LENGTH = 12;
@@ -66,7 +67,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
icon: 'delete danger',
text: 'FilterMenuDelete',
onClick: () => {
- new PopupPeer('filter-delete', {
+ PopupElement.createPopup(PopupPeer, 'filter-delete', {
titleLangKey: 'ChatList.Filter.Confirm.Remove.Header',
descriptionLangKey: 'ChatList.Filter.Confirm.Remove.Text',
buttons: [{
diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts
index 04bb68ac..26d793f9 100644
--- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts
+++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts
@@ -34,6 +34,7 @@ import {toastNew} from '../../toast';
import AppPrivacyVoicesTab from './privacy/voices';
import SettingSection from '../../settingSection';
import AppActiveWebSessionsTab from './activeWebSessions';
+import PopupElement from '../../popups';
export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
private activeSessionsRow: Row;
@@ -424,7 +425,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
const section = new SettingSection({name: 'FilterChats'});
const onDeleteClick = () => {
- const popup = new PopupPeer('popup-delete-drafts', {
+ const popup = PopupElement.createPopup(PopupPeer, 'popup-delete-drafts', {
buttons: [{
langKey: 'Delete',
callback: () => {
diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts
index 33ac51ee..1264add5 100644
--- a/src/components/sidebarLeft/tabs/settings.ts
+++ b/src/components/sidebarLeft/tabs/settings.ts
@@ -29,6 +29,7 @@ import PopupElement from '../../popups';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import SettingSection from '../../settingSection';
import AppStickersAndEmojiTab from './stickersAndEmoji';
+import ButtonCorner from '../../buttonCorner';
export default class AppSettingsTab extends SliderSuperTab {
private buttons: {
@@ -58,7 +59,7 @@ export default class AppSettingsTab extends SliderSuperTab {
icon: 'logout',
text: 'EditAccount.Logout',
onClick: () => {
- new PopupPeer('logout', {
+ PopupElement.createPopup(PopupPeer, 'logout', {
titleLangKey: 'LogOut',
descriptionLangKey: 'LogOut.Description',
buttons: [{
@@ -82,7 +83,7 @@ export default class AppSettingsTab extends SliderSuperTab {
this.profile.setPeer(rootScope.myId);
const fillPromise = this.profile.fillProfileElements();
- const changeAvatarBtn = Button('btn-circle btn-corner z-depth-1 profile-change-avatar', {icon: 'cameraadd'});
+ const changeAvatarBtn = ButtonCorner({icon: 'cameraadd', className: 'profile-change-avatar'});
attachClickEvent(changeAvatarBtn, () => {
const canvas = document.createElement('canvas');
PopupElement.createPopup(PopupAvatar).open(canvas, (upload) => {
diff --git a/src/components/sidebarLeft/tabs/stickersAndEmoji.ts b/src/components/sidebarLeft/tabs/stickersAndEmoji.ts
index 64ddb4ee..2ad0a20d 100644
--- a/src/components/sidebarLeft/tabs/stickersAndEmoji.ts
+++ b/src/components/sidebarLeft/tabs/stickersAndEmoji.ts
@@ -15,6 +15,7 @@ import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText';
import rootScope from '../../../lib/rootScope';
import CheckboxField from '../../checkboxField';
import LazyLoadQueue from '../../lazyLoadQueue';
+import PopupElement from '../../popups';
import PopupStickers from '../../popups/stickers';
import Row, {CreateRowFromCheckboxField} from '../../row';
import SettingSection from '../../settingSection';
@@ -209,7 +210,7 @@ export default class AppStickersAndEmojiTab extends SliderSuperTab {
subtitleLangArgs: [stickerSet.count],
havePadding: true,
clickable: () => {
- new PopupStickers({id: stickerSet.id, access_hash: stickerSet.access_hash}).show();
+ PopupElement.createPopup(PopupStickers, {id: stickerSet.id, access_hash: stickerSet.access_hash}).show();
},
listenerSetter: this.listenerSetter
});
diff --git a/src/components/sidebarRight/tabs/chatType.ts b/src/components/sidebarRight/tabs/chatType.ts
index aa3e576f..eccf635f 100644
--- a/src/components/sidebarRight/tabs/chatType.ts
+++ b/src/components/sidebarRight/tabs/chatType.ts
@@ -27,6 +27,7 @@ import getPeerEditableUsername from '../../../lib/appManagers/utils/peers/getPee
import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames';
import {purchaseUsernameCaption} from '../../sidebarLeft/tabs/editProfile';
import confirmationPopup from '../../confirmationPopup';
+import PopupElement from '../../popups';
export default class AppChatTypeTab extends SliderSuperTabEventable {
public chatId: ChatId;
@@ -102,7 +103,7 @@ export default class AppChatTypeTab extends SliderSuperTabEventable {
const btnRevoke = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'RevokeLink'});
attachClickEvent(btnRevoke, () => {
- new PopupPeer('revoke-link', {
+ PopupElement.createPopup(PopupPeer, 'revoke-link', {
buttons: [{
langKey: 'RevokeButton',
callback: () => {
diff --git a/src/components/sidebarRight/tabs/editChat.ts b/src/components/sidebarRight/tabs/editChat.ts
index 25022d06..be45b8d4 100644
--- a/src/components/sidebarRight/tabs/editChat.ts
+++ b/src/components/sidebarRight/tabs/editChat.ts
@@ -24,6 +24,7 @@ import hasRights from '../../../lib/appManagers/utils/chats/hasRights';
import replaceContent from '../../../helpers/dom/replaceContent';
import SettingSection from '../../settingSection';
import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames';
+import PopupElement from '../../popups';
export default class AppEditChatTab extends SliderSuperTab {
private chatNameInputField: InputField;
@@ -377,7 +378,7 @@ export default class AppEditChatTab extends SliderSuperTab {
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: isBroadcast ? 'PeerInfo.DeleteChannel' : 'DeleteAndExitButton'});
attachClickEvent(btnDelete, () => {
- new PopupDeleteDialog(peerId/* , 'delete' */, undefined, (promise) => {
+ PopupElement.createPopup(PopupDeleteDialog, peerId/* , 'delete' */, undefined, (promise) => {
const toggle = toggleDisability([btnDelete], true);
promise.then(() => {
this.close();
diff --git a/src/components/sidebarRight/tabs/editContact.ts b/src/components/sidebarRight/tabs/editContact.ts
index 1b58f685..ffa6aef9 100644
--- a/src/components/sidebarRight/tabs/editContact.ts
+++ b/src/components/sidebarRight/tabs/editContact.ts
@@ -13,7 +13,7 @@ import Button from '../../button';
import PeerTitle from '../../peerTitle';
import rootScope from '../../../lib/rootScope';
import PopupPeer from '../../popups/peer';
-import {addCancelButton} from '../../popups';
+import PopupElement, {addCancelButton} from '../../popups';
import {i18n} from '../../../lib/langPack';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import toggleDisability from '../../../helpers/dom/toggleDisability';
@@ -160,7 +160,7 @@ export default class AppEditContactTab extends SliderSuperTab {
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'PeerInfo.DeleteContact'});
attachClickEvent(btnDelete, () => {
- new PopupPeer('popup-delete-contact', {
+ PopupElement.createPopup(PopupPeer, 'popup-delete-contact', {
peerId: peerId,
titleLangKey: 'DeleteContact',
descriptionLangKey: 'AreYouSureDeleteContact',
diff --git a/src/components/sidebarRight/tabs/groupPermissions.ts b/src/components/sidebarRight/tabs/groupPermissions.ts
index 2ebd0d54..5d8b78a2 100644
--- a/src/components/sidebarRight/tabs/groupPermissions.ts
+++ b/src/components/sidebarRight/tabs/groupPermissions.ts
@@ -26,6 +26,7 @@ import {SliderSuperTabEventable} from '../../sliderTab';
import {toast} from '../../toast';
import AppUserPermissionsTab from './userPermissions';
import CheckboxFields, {CheckboxFieldsField} from '../../checkboxFields';
+import PopupElement from '../../popups';
type PermissionsCheckboxFieldsField = CheckboxFieldsField & {
flags: ChatRights[],
@@ -186,7 +187,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
subtitleLangKey: 'Loading',
icon: 'adduser',
clickable: () => {
- new PopupPickUser({
+ PopupElement.createPopup(PopupPickUser, {
peerTypes: ['channelParticipants'],
onSelect: (peerId) => {
setTimeout(() => {
diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts
index 3e78ddb9..f386baba 100644
--- a/src/components/sidebarRight/tabs/sharedMedia.ts
+++ b/src/components/sidebarRight/tabs/sharedMedia.ts
@@ -25,6 +25,7 @@ import {Message} from '../../../layer';
import getMessageThreadId from '../../../lib/appManagers/utils/messages/getMessageThreadId';
import AppEditTopicTab from './editTopic';
import liteMode from '../../../helpers/liteMode';
+import PopupElement from '../../popups';
type SharedMediaHistoryStorage = Partial<{
[type in SearchSuperType]: {mid: number, peerId: PeerId}[]
@@ -296,7 +297,7 @@ export default class AppSharedMediaTab extends SliderSuperTab {
peerId
}).element);
- new PopupPeer('popup-add-members', {
+ PopupElement.createPopup(PopupPeer, 'popup-add-members', {
peerId,
titleLangKey,
descriptionLangKey,
@@ -333,7 +334,7 @@ export default class AppSharedMediaTab extends SliderSuperTab {
placeholder: 'SendMessageTo'
});
} else {
- new PopupPickUser({
+ PopupElement.createPopup(PopupPickUser, {
peerTypes: ['contacts'],
placeholder: 'Search',
onSelect: (peerId) => {
diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts
index c24f5569..261c3d5c 100644
--- a/src/components/sidebarRight/tabs/stickers.ts
+++ b/src/components/sidebarRight/tabs/stickers.ts
@@ -20,6 +20,7 @@ import setInnerHTML from '../../../helpers/dom/setInnerHTML';
import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText';
import attachStickerViewerListeners from '../../stickerViewer';
import wrapSticker from '../../wrappers/sticker';
+import PopupElement from '../../popups';
export default class AppStickersTab extends SliderSuperTab {
private inputSearch: InputSearch;
@@ -79,7 +80,7 @@ export default class AppStickersTab extends SliderSuperTab {
});
} else {
this.managers.appStickersManager.getStickerSet({id, access_hash}).then((full) => {
- new PopupStickers(full.set).show();
+ PopupElement.createPopup(PopupStickers, full.set).show();
});
}
}, {listenerSetter: this.listenerSetter});
diff --git a/src/components/topbarCall.ts b/src/components/topbarCall.ts
index 6c47743a..4ce28541 100644
--- a/src/components/topbarCall.ts
+++ b/src/components/topbarCall.ts
@@ -263,14 +263,14 @@ export default class TopbarCall {
return;
}
- new PopupGroupCall().show();
+ PopupElement.createPopup(PopupGroupCall).show();
} else if(this.instance instanceof CallInstance) {
const popups = PopupElement.getPopups(PopupCall);
if(popups.find((popup) => popup.getCallInstance() === this.instance)) {
return;
}
- new PopupCall(this.instance).show();
+ PopupElement.createPopup(PopupCall, this.instance).show();
}
}, {listenerSetter});
diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts
index 42e50a31..6234bc62 100644
--- a/src/components/wrappers/sticker.ts
+++ b/src/components/wrappers/sticker.ts
@@ -47,6 +47,7 @@ import wrapStickerAnimation from './stickerAnimation';
import framesCache from '../../helpers/framesCache';
import {IS_MOBILE} from '../../environment/userAgent';
import liteMode, {LiteModeKey} from '../../helpers/liteMode';
+import PopupElement from '../popups';
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L40
export const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2;
@@ -699,7 +700,7 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut
const a = document.createElement('a');
a.onclick = () => {
hideToast();
- new PopupStickers(doc.stickerSetInput).show();
+ PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show();
};
toastNew({
@@ -782,7 +783,7 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m
}
const activeAnimations: Set<{}> = (container as any).activeAnimations ??= new Set();
- if(activeAnimations.size >= (IS_MOBILE ? 3 : 5)) {
+ if(activeAnimations.size >= 3) {
return;
}
diff --git a/src/config/app.ts b/src/config/app.ts
index a8784273..92b98154 100644
--- a/src/config/app.ts
+++ b/src/config/app.ts
@@ -21,7 +21,7 @@ const App = {
version: process.env.VERSION,
versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD,
- langPackVersion: '1.0.1',
+ langPackVersion: '1.0.3',
langPack: 'webk',
langPackCode: 'en',
domains: MAIN_DOMAINS,
diff --git a/src/helpers/addAnchorListener.ts b/src/helpers/addAnchorListener.ts
index d917c69c..e9fef34d 100644
--- a/src/helpers/addAnchorListener.ts
+++ b/src/helpers/addAnchorListener.ts
@@ -22,6 +22,10 @@ export default function addAnchorListener new PopupStickers(doc.stickerSetInput).show(),
+ onClick: () => PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show(),
verify: () => !isStickerPack
}, {
icon: 'favourites',
diff --git a/src/helpers/paymentsWrapCurrencyAmount.ts b/src/helpers/paymentsWrapCurrencyAmount.ts
index 24f509c9..4d79542b 100644
--- a/src/helpers/paymentsWrapCurrencyAmount.ts
+++ b/src/helpers/paymentsWrapCurrencyAmount.ts
@@ -25,7 +25,12 @@ function number_format(number: any, decimals: any, dec_point: any, thousands_sep
return s.join(dec);
}
-export default function paymentsWrapCurrencyAmount(amount: number | string, currency: string, skipSymbol?: boolean) {
+export default function paymentsWrapCurrencyAmount(
+ amount: number | string,
+ currency: string,
+ skipSymbol?: boolean,
+ useNative?: boolean
+) {
amount = +amount;
const isNegative = amount < 0;
@@ -47,7 +52,7 @@ export default function paymentsWrapCurrencyAmount(amount: number | string, curr
return formatted;
}
- let symbol = currencyData.symbol;
+ let symbol = useNative ? currencyData.native || currencyData.symbol : currencyData.symbol;
if(isNegative && !currencyData.space_between && currencyData.symbol_left) {
symbol = '-' + symbol;
formatted = formatted.replace('-', '');
diff --git a/src/lang.ts b/src/lang.ts
index 497eb274..1597ed34 100644
--- a/src/lang.ts
+++ b/src/lang.ts
@@ -940,6 +940,11 @@ const lang = {
'SuggestStickersNone': 'None',
'DynamicPackOrder': 'Dynamic Pack Order',
'DynamicPackOrderInfo': 'Automatically place recently used sticker packs at the front of the panel.',
+ 'GiftPremium': 'Gift Premium',
+ 'GiftTelegramPremiumTitle': 'Gift Telegram Premium',
+ 'GiftTelegramPremiumDescription': 'Give **%1$s** access to exclusive features with **Telegram Premium**.',
+ 'PricePerMonth': '%1$s / month',
+ 'GiftSubscriptionFor': 'Gift Subscription for %1$s',
// * macos
'AccountSettings.Filters': 'Chat Folders',
diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts
index 1a0ab5fa..107c659a 100644
--- a/src/lib/appManagers/appImManager.ts
+++ b/src/lib/appManagers/appImManager.ts
@@ -107,6 +107,7 @@ import partition from '../../helpers/array/partition';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import liteMode, {LiteModeKey} from '../../helpers/liteMode';
import RLottiePlayer from '../rlottie/rlottiePlayer';
+import PopupGiftPremium from '../../components/popups/giftPremium';
export type ChatSavedPosition = {
mids: number[],
@@ -345,7 +346,7 @@ export class AppImManager extends EventListenerBase<{
const onInstanceDeactivated = (reason: InstanceDeactivateReason) => {
const isUpdated = reason === 'version';
- const popup = new PopupElement('popup-instance-deactivated', {overlayClosable: true});
+ const popup = PopupElement.createPopup(PopupElement, 'popup-instance-deactivated', {overlayClosable: true});
const c = document.createElement('div');
c.classList.add('instance-deactivated-container');
(popup as any).container.replaceWith(c);
@@ -530,7 +531,7 @@ export class AppImManager extends EventListenerBase<{
// return;
// }
- const popup = new PopupCall(instance);
+ const popup = PopupElement.createPopup(PopupCall, instance);
instance.addEventListener('acceptCallOverride', () => {
return this.discardCurrentCall(instance.interlocutorUserId.toPeerId(), undefined, instance)
@@ -582,7 +583,7 @@ export class AppImManager extends EventListenerBase<{
a.innerText = href;
a.removeAttribute('onclick');
- new PopupPeer('popup-masked-url', {
+ PopupElement.createPopup(PopupPeer, 'popup-masked-url', {
titleLangKey: 'OpenUrlTitle',
descriptionLangKey: 'OpenUrlAlert2',
descriptionLangArgs: [a],
@@ -862,11 +863,11 @@ export class AppImManager extends EventListenerBase<{
const share = apiManagerProxy.share;
if(share) {
apiManagerProxy.share = undefined;
- new PopupForward(undefined, async(peerId) => {
+ PopupElement.createPopup(PopupForward, undefined, async(peerId) => {
await this.setPeer({peerId});
if(share.files?.length) {
const foundMedia = share.files.some((file) => MEDIA_MIME_TYPES_SUPPORTED.has(file.type));
- new PopupNewMedia(this.chat, share.files, foundMedia ? 'media' : 'document');
+ PopupElement.createPopup(PopupNewMedia, this.chat, share.files, foundMedia ? 'media' : 'document');
} else {
this.managers.appMessagesManager.sendText(peerId, share.text);
}
@@ -1184,7 +1185,7 @@ export class AppImManager extends EventListenerBase<{
case INTERNAL_LINK_TYPE.EMOJI_SET:
case INTERNAL_LINK_TYPE.STICKER_SET: {
- new PopupStickers({id: link.set}, link._ === INTERNAL_LINK_TYPE.EMOJI_SET).show();
+ PopupElement.createPopup(PopupStickers, {id: link.set}, link._ === INTERNAL_LINK_TYPE.EMOJI_SET).show();
break;
}
@@ -1204,7 +1205,7 @@ export class AppImManager extends EventListenerBase<{
return;
}
- new PopupJoinChatInvite(link.invite, chatInvite);
+ PopupElement.createPopup(PopupJoinChatInvite, link.invite, chatInvite);
}, (err) => {
if(err.type === 'INVITE_HASH_EXPIRED') {
toast(i18n('InviteExpired'));
@@ -1252,7 +1253,7 @@ export class AppImManager extends EventListenerBase<{
// }
// };
- new PopupPayment(undefined, inputInvoice, paymentForm);
+ PopupElement.createPopup(PopupPayment, undefined, inputInvoice, paymentForm);
});
});
break;
@@ -2546,6 +2547,12 @@ export class AppImManager extends EventListenerBase<{
options1.threadId === options2.threadId &&
(typeof(options1.type) !== typeof(options2.type) || options1.type === options2.type);
}
+
+ public giftPremium(peerId: PeerId) {
+ this.managers.appProfileManager.getProfile(peerId.toUserId()).then((profile) => {
+ PopupElement.createPopup(PopupGiftPremium, peerId, profile.premium_gifts);
+ });
+ }
}
const appImManager = new AppImManager();
diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts
index 978da890..852c1f9d 100644
--- a/src/lib/appManagers/appMessagesManager.ts
+++ b/src/lib/appManagers/appMessagesManager.ts
@@ -5904,18 +5904,54 @@ export class AppMessagesManager extends AppManager {
});
}
- public isHistoryResultEnd(historyResult: Exclude, limit: number, add_offset: number) {
+ public isHistoryResultEnd(
+ historyResult: Exclude,
+ limit: number,
+ add_offset: number,
+ offset_id: number
+ ) {
const {offset_id_offset, messages} = historyResult as MessagesMessages.messagesMessagesSlice;
+ const mids = messages.map((message) => {
+ return (message as MyMessage).mid;
+ });
+
const count = (historyResult as MessagesMessages.messagesMessagesSlice).count || messages.length;
- const offsetIdOffset = offset_id_offset ?? count - 1;
const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit;
+ const bottomWasMeantToLoad = Math.abs(add_offset);
- const isTopEnd = offsetIdOffset >= (count - topWasMeantToLoad) || count < topWasMeantToLoad;
- const isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0);
+ let offsetIdOffset = offset_id_offset;
+ let isTopEnd = false, isBottomEnd = false;
- return {count, offsetIdOffset, isTopEnd, isBottomEnd};
+ // if(offsetIdOffset === undefined && !bottomWasMeantToLoad) {
+ // offsetIdOffset = 0;
+ // }
+
+ if(offsetIdOffset !== undefined) {
+ isTopEnd = offsetIdOffset >= (count - topWasMeantToLoad) || count < topWasMeantToLoad;
+ isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0);
+ } else if(offset_id && getServerMessageId(offset_id)) {
+ let i = 0;
+ for(const length = mids.length; i < length; ++i) {
+ if(offset_id > mids[i]) {
+ break;
+ }
+ }
+
+ const topLoaded = messages.length - i;
+ const bottomLoaded = mids.includes(offset_id) ? i - 1 : i;
+ if(topWasMeantToLoad) isTopEnd = topLoaded < topWasMeantToLoad;
+ if(bottomWasMeantToLoad) isBottomEnd = bottomLoaded < bottomWasMeantToLoad;
+
+ if(isTopEnd || isBottomEnd) {
+ offsetIdOffset = isTopEnd ? count - topLoaded : bottomLoaded;
+ }
+ }
+
+ offsetIdOffset ??= 0;
+
+ return {count, offsetIdOffset, isTopEnd, isBottomEnd, mids};
}
public mergeHistoryResult(
@@ -5926,11 +5962,8 @@ export class AppMessagesManager extends AppManager {
add_offset: number
) {
const {messages} = historyResult as MessagesMessages.messagesMessagesSlice;
- const isEnd = this.isHistoryResultEnd(historyResult, limit, add_offset);
- const {count, offsetIdOffset, isTopEnd, isBottomEnd} = isEnd;
- const mids = messages.map((message) => {
- return (message as MyMessage).mid;
- });
+ const isEnd = this.isHistoryResultEnd(historyResult, limit, add_offset, offset_id);
+ const {count, offsetIdOffset, isTopEnd, isBottomEnd, mids} = isEnd;
// * add bound manually.
// * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id')
diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts
index d4daa67b..d5d4a272 100644
--- a/src/lib/appManagers/appProfileManager.ts
+++ b/src/lib/appManagers/appProfileManager.ts
@@ -22,6 +22,7 @@ import {ReferenceContext} from '../mtproto/referenceDatabase';
import generateMessageId from './utils/messageId/generateMessageId';
import assumeType from '../../helpers/assumeType';
import makeError from '../../helpers/makeError';
+import callbackify from '../../helpers/callbackify';
export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, timeout: number}>;
@@ -669,6 +670,18 @@ export class AppProfileManager extends AppManager {
return this.typingsInPeer[this.getTypingsKey(peerId, threadId)];
}
+ public canGiftPremium(userId: UserId) {
+ const user = this.appUsersManager.getUser(userId);
+ if(user?.pFlags?.premium || true) {
+ return false;
+ }
+
+ return callbackify(this.getProfile(userId), (userFull) => {
+ const user = this.appUsersManager.getUser(userId);
+ return !!userFull.premium_gifts && !user?.pFlags?.premium;
+ });
+ }
+
private onUpdateChatParticipants = (update: Update.updateChatParticipants) => {
const participants = update.participants;
if(participants._ !== 'chatParticipants') {
diff --git a/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts b/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts
index e786bec9..8914a9ef 100644
--- a/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts
+++ b/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts
@@ -1,6 +1,7 @@
import {Document, Message, MessageMedia} from '../../../../layer';
export default function getMediaDurationFromMessage(message: Message.message) {
+ if(!message) return undefined;
const doc = (message.media as MessageMedia.messageMediaDocument)?.document as Document.document;
const duration = ((['voice', 'audio', 'video'] as Document.document['type'][]).includes(doc?.type) && doc.duration) || undefined;
return duration;
diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts
index 45229224..7f4c3855 100644
--- a/src/lib/richTextProcessor/wrapRichText.ts
+++ b/src/lib/richTextProcessor/wrapRichText.ts
@@ -1475,6 +1475,10 @@ export default function wrapRichText(text: string, options: Partial<{
(element as HTMLAnchorElement).href = '#';
element.setAttribute('onclick', 'setMediaTimestamp(this)');
+ if(options.maxMediaTimestamp === Infinity) {
+ element.classList.add('is-disabled');
+ }
+
break;
}
}
diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss
index a8c086b3..5423ca02 100644
--- a/src/scss/partials/_button.scss
+++ b/src/scss/partials/_button.scss
@@ -686,6 +686,31 @@ $btn-menu-z-index: 4;
pointer-events: none !important;
opacity: var(--disabled-opacity);
}
+
+ &.shimmer {
+ &:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(to right, transparent 0%, rgba(var(--surface-color-rgb), .2) 50%, transparent 100%);
+ animation: wave 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+
+ @keyframes wave {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 50% {
+ transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+ }
+ }
+ }
}
.btn-control {
diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss
index 5143aad8..234be2a4 100644
--- a/src/scss/partials/_chatBubble.scss
+++ b/src/scss/partials/_chatBubble.scss
@@ -2475,6 +2475,12 @@ $bubble-border-radius-big: 12px;
// }
}
+.timestamp.is-disabled {
+ color: inherit;
+ text-decoration: none !important;
+ cursor: inherit;
+}
+
@keyframes audio-dots {
0% {
content: "";
diff --git a/src/scss/partials/_row.scss b/src/scss/partials/_row.scss
index f9b4cfa1..9b8aea96 100644
--- a/src/scss/partials/_row.scss
+++ b/src/scss/partials/_row.scss
@@ -254,6 +254,19 @@ $row-border-radius: $border-radius-medium;
margin-inline: .125rem .125rem;
padding: 0;
}
+
+ &-absolute {
+ position: absolute;
+ margin: 0 !important;
+ padding: 0 !important;
+ left: 0;
+ }
+
+ &-round {
+ .checkbox-box-border {
+ z-index: unset;
+ }
+ }
}
&-subtitle {
diff --git a/src/scss/partials/popups/_giftPremium.scss b/src/scss/partials/popups/_giftPremium.scss
new file mode 100644
index 00000000..fe14fbcc
--- /dev/null
+++ b/src/scss/partials/popups/_giftPremium.scss
@@ -0,0 +1,103 @@
+/*
+ * https://github.com/morethanwords/tweb
+ * Copyright (C) 2019-2021 Eduard Kuzmenko
+ * https://github.com/morethanwords/tweb/blob/master/LICENSE
+ */
+
+.popup-gift-premium {
+ $parent: ".popup";
+
+ #{$parent} {
+ &-container {
+ padding: 0;
+ width: 26.25rem;
+ max-width: 26.25rem;
+
+ max-height: unquote('min(100%, 43.5rem)');
+ border-radius: $border-radius-huge;
+ }
+
+ &-header {
+ height: 3.5rem;
+ margin: 0;
+ padding: 0 1rem;
+ margin-bottom: -2rem;
+ }
+ }
+
+ .scrollable-y {
+ flex: 1 1 auto;
+ padding: 0 1rem 1rem;
+ }
+
+ &-avatar {
+ display: block;
+ margin: 0 auto;
+ }
+
+ &-title,
+ &-subtitle {
+ text-align: center;
+ display: block;
+ }
+
+ &-title {
+ font-size: var(--font-size-20);
+ font-weight: var(--font-weight-bold);
+ margin: .75rem 0;
+ }
+
+ &-options {
+ display: flex;
+ flex-direction: column;
+ margin: .5rem 0 1rem;
+ }
+
+ &-option {
+ margin-top: .5rem;
+
+ &:nth-child(1) {
+ --primary-color: #C564F3;
+ }
+
+ &:nth-child(2) {
+ --primary-color: #AC64F3;
+ }
+
+ &:nth-child(3) {
+ --primary-color: #9377FF;
+ }
+ }
+
+ &-discount {
+ background-color: var(--primary-color);
+ border-radius: 6px;
+ color: #fff;
+ margin-right: .375rem;
+ padding: 0 0.3125rem;
+ height: 20px;
+ display: inline-block;
+ line-height: 20px;
+ }
+
+ &-confirm {
+ --ripple-color: rgba(255, 255, 255, #{$hover-alpha});
+ background: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%) !important;
+ font-weight: var(--font-weight-bold);
+ color: #fff;
+ text-transform: uppercase;
+
+ @include hover() {
+ &:after {
+ content: " ";
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: #fff;
+ opacity: $hover-alpha;
+ }
+ }
+ }
+}
diff --git a/src/scss/partials/popups/_popup.scss b/src/scss/partials/popups/_popup.scss
index b92d26ef..5700b7eb 100644
--- a/src/scss/partials/popups/_popup.scss
+++ b/src/scss/partials/popups/_popup.scss
@@ -119,6 +119,12 @@
.scrollable {
position: relative;
}
+
+ .scrollable-y-bordered {
+ &:last-child {
+ border-bottom: none;
+ }
+ }
}
&-buttons {
diff --git a/src/scss/style.scss b/src/scss/style.scss
index 6cf0c11c..77a707c9 100644
--- a/src/scss/style.scss
+++ b/src/scss/style.scss
@@ -411,6 +411,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/popups/paymentVerification";
@import "partials/popups/paymentCardConfirmation";
@import "partials/popups/limit";
+@import "partials/popups/giftPremium";
@import "partials/pages/pages";
@import "partials/pages/authCode";
diff --git a/src/scss/tgico/_style.scss b/src/scss/tgico/_style.scss
index cf5ac435..132c5fb8 100644
--- a/src/scss/tgico/_style.scss
+++ b/src/scss/tgico/_style.scss
@@ -3,9 +3,9 @@
@font-face {
font-family: '#{$tgico-font-family}';
src:
- url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?2fcrrv') format('truetype'),
- url('#{$tgico-font-path}/#{$tgico-font-family}.woff?2fcrrv') format('woff'),
- url('#{$tgico-font-path}/#{$tgico-font-family}.svg?2fcrrv##{$tgico-font-family}') format('svg');
+ url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?bv435t') format('truetype'),
+ url('#{$tgico-font-path}/#{$tgico-font-family}.woff?bv435t') format('woff'),
+ url('#{$tgico-font-path}/#{$tgico-font-family}.svg?bv435t##{$tgico-font-family}') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -432,6 +432,11 @@
content: $tgico-gifs;
}
}
+.tgico-gift {
+ &:before {
+ content: $tgico-gift;
+ }
+}
.tgico-group {
&:before {
content: $tgico-group;
diff --git a/src/scss/tgico/_variables.scss b/src/scss/tgico/_variables.scss
index bc53fc65..8d062b12 100644
--- a/src/scss/tgico/_variables.scss
+++ b/src/scss/tgico/_variables.scss
@@ -78,135 +78,136 @@ $tgico-fullscreen: "\e94c";
$tgico-gc_microphone: "\e94d";
$tgico-gc_microphoneoff: "\e94e";
$tgico-gifs: "\e94f";
-$tgico-group: "\e950";
-$tgico-groupmedia: "\e951";
-$tgico-groupmediaoff: "\e952";
-$tgico-help: "\e953";
-$tgico-hide: "\e954";
-$tgico-image: "\e955";
-$tgico-info: "\e956";
-$tgico-info2: "\e957";
-$tgico-italic: "\e958";
-$tgico-keyboard: "\e959";
-$tgico-lamp: "\e95a";
-$tgico-language: "\e95b";
-$tgico-largepause: "\e95c";
-$tgico-largeplay: "\e95d";
-$tgico-left: "\e95e";
-$tgico-limit_chat: "\e95f";
-$tgico-limit_chats: "\e960";
-$tgico-limit_file: "\e961";
-$tgico-limit_folders: "\e962";
-$tgico-limit_link: "\e963";
-$tgico-limit_pin: "\e964";
-$tgico-link: "\e965";
-$tgico-listscreenshare: "\e966";
-$tgico-livelocation: "\e967";
-$tgico-location: "\e968";
-$tgico-lock: "\e969";
-$tgico-lockoff: "\e96a";
-$tgico-loginlogodesktop: "\e96b";
-$tgico-loginlogomobile: "\e96c";
-$tgico-logout: "\e96d";
-$tgico-mediaspoiler: "\e96e";
-$tgico-mediaspoileroff: "\e96f";
-$tgico-mention: "\e970";
-$tgico-menu: "\e971";
-$tgico-message: "\e972";
-$tgico-messageunread: "\e973";
-$tgico-microphone: "\e974";
-$tgico-microphone_crossed: "\e975";
-$tgico-microphone_crossed_filled: "\e976";
-$tgico-microphone_filled: "\e977";
-$tgico-minus: "\e978";
-$tgico-monospace: "\e979";
-$tgico-more: "\e97a";
-$tgico-mute: "\e97b";
-$tgico-muted: "\e97c";
-$tgico-newchannel: "\e97d";
-$tgico-newchat_filled: "\e97e";
-$tgico-newgroup: "\e97f";
-$tgico-newprivate: "\e980";
-$tgico-next: "\e981";
-$tgico-noncontacts: "\e982";
-$tgico-nosound: "\e983";
-$tgico-passwordoff: "\e984";
-$tgico-pause: "\e985";
-$tgico-permissions: "\e986";
-$tgico-phone: "\e987";
-$tgico-pin: "\e988";
-$tgico-pinlist: "\e989";
-$tgico-pinned_filled: "\e98a";
-$tgico-pinnedchat: "\e98b";
-$tgico-pip: "\e98c";
-$tgico-play: "\e98d";
-$tgico-playback_05: "\e98e";
-$tgico-playback_15: "\e98f";
-$tgico-playback_1x: "\e990";
-$tgico-playback_2x: "\e991";
-$tgico-plus: "\e992";
-$tgico-poll: "\e993";
-$tgico-premium_addone: "\e994";
-$tgico-premium_double: "\e995";
-$tgico-premium_lock: "\e996";
-$tgico-premium_unlock: "\e997";
-$tgico-previous: "\e998";
-$tgico-radiooff: "\e999";
-$tgico-radioon: "\e99a";
-$tgico-reactions: "\e99b";
-$tgico-readchats: "\e99c";
-$tgico-recent: "\e99d";
-$tgico-replace: "\e99e";
-$tgico-reply: "\e99f";
-$tgico-reply_filled: "\e9a0";
-$tgico-rightpanel: "\e9a1";
-$tgico-rotate_left: "\e9a2";
-$tgico-rotate_right: "\e9a3";
-$tgico-saved: "\e9a4";
-$tgico-savedmessages: "\e9a5";
-$tgico-schedule: "\e9a6";
-$tgico-scheduled: "\e9a7";
-$tgico-search: "\e9a8";
-$tgico-select: "\e9a9";
-$tgico-send: "\e9aa";
-$tgico-send2: "\e9ab";
-$tgico-sending: "\e9ac";
-$tgico-sendingerror: "\e9ad";
-$tgico-settings: "\e9ae";
-$tgico-settings_filled: "\e9af";
-$tgico-sharescreen_filled: "\e9b0";
-$tgico-shipping: "\e9b1";
-$tgico-shuffle: "\e9b2";
-$tgico-smallscreen: "\e9b3";
-$tgico-smile: "\e9b4";
-$tgico-spoiler: "\e9b5";
-$tgico-sport: "\e9b6";
-$tgico-star: "\e9b7";
-$tgico-stickers: "\e9b8";
-$tgico-stickers_face: "\e9b9";
-$tgico-stop: "\e9ba";
-$tgico-strikethrough: "\e9bb";
-$tgico-textedit: "\e9bc";
-$tgico-tip: "\e9bd";
-$tgico-tools: "\e9be";
-$tgico-topics: "\e9bf";
-$tgico-transcribe: "\e9c0";
-$tgico-unarchive: "\e9c1";
-$tgico-underline: "\e9c2";
-$tgico-unmute: "\e9c3";
-$tgico-unpin: "\e9c4";
-$tgico-unread: "\e9c5";
-$tgico-up: "\e9c6";
-$tgico-user: "\e9c7";
-$tgico-username: "\e9c8";
-$tgico-videocamera: "\e9c9";
-$tgico-videocamera_crossed_filled: "\e9ca";
-$tgico-videocamera_filled: "\e9cb";
-$tgico-videochat: "\e9cc";
-$tgico-volume_down: "\e9cd";
-$tgico-volume_mute: "\e9ce";
-$tgico-volume_off: "\e9cf";
-$tgico-volume_up: "\e9d0";
-$tgico-zoomin: "\e9d1";
-$tgico-zoomout: "\e9d2";
+$tgico-gift: "\e950";
+$tgico-group: "\e951";
+$tgico-groupmedia: "\e952";
+$tgico-groupmediaoff: "\e953";
+$tgico-help: "\e954";
+$tgico-hide: "\e955";
+$tgico-image: "\e956";
+$tgico-info: "\e957";
+$tgico-info2: "\e958";
+$tgico-italic: "\e959";
+$tgico-keyboard: "\e95a";
+$tgico-lamp: "\e95b";
+$tgico-language: "\e95c";
+$tgico-largepause: "\e95d";
+$tgico-largeplay: "\e95e";
+$tgico-left: "\e95f";
+$tgico-limit_chat: "\e960";
+$tgico-limit_chats: "\e961";
+$tgico-limit_file: "\e962";
+$tgico-limit_folders: "\e963";
+$tgico-limit_link: "\e964";
+$tgico-limit_pin: "\e965";
+$tgico-link: "\e966";
+$tgico-listscreenshare: "\e967";
+$tgico-livelocation: "\e968";
+$tgico-location: "\e969";
+$tgico-lock: "\e96a";
+$tgico-lockoff: "\e96b";
+$tgico-loginlogodesktop: "\e96c";
+$tgico-loginlogomobile: "\e96d";
+$tgico-logout: "\e96e";
+$tgico-mediaspoiler: "\e96f";
+$tgico-mediaspoileroff: "\e970";
+$tgico-mention: "\e971";
+$tgico-menu: "\e972";
+$tgico-message: "\e973";
+$tgico-messageunread: "\e974";
+$tgico-microphone: "\e975";
+$tgico-microphone_crossed: "\e976";
+$tgico-microphone_crossed_filled: "\e977";
+$tgico-microphone_filled: "\e978";
+$tgico-minus: "\e979";
+$tgico-monospace: "\e97a";
+$tgico-more: "\e97b";
+$tgico-mute: "\e97c";
+$tgico-muted: "\e97d";
+$tgico-newchannel: "\e97e";
+$tgico-newchat_filled: "\e97f";
+$tgico-newgroup: "\e980";
+$tgico-newprivate: "\e981";
+$tgico-next: "\e982";
+$tgico-noncontacts: "\e983";
+$tgico-nosound: "\e984";
+$tgico-passwordoff: "\e985";
+$tgico-pause: "\e986";
+$tgico-permissions: "\e987";
+$tgico-phone: "\e988";
+$tgico-pin: "\e989";
+$tgico-pinlist: "\e98a";
+$tgico-pinned_filled: "\e98b";
+$tgico-pinnedchat: "\e98c";
+$tgico-pip: "\e98d";
+$tgico-play: "\e98e";
+$tgico-playback_05: "\e98f";
+$tgico-playback_15: "\e990";
+$tgico-playback_1x: "\e991";
+$tgico-playback_2x: "\e992";
+$tgico-plus: "\e993";
+$tgico-poll: "\e994";
+$tgico-premium_addone: "\e995";
+$tgico-premium_double: "\e996";
+$tgico-premium_lock: "\e997";
+$tgico-premium_unlock: "\e998";
+$tgico-previous: "\e999";
+$tgico-radiooff: "\e99a";
+$tgico-radioon: "\e99b";
+$tgico-reactions: "\e99c";
+$tgico-readchats: "\e99d";
+$tgico-recent: "\e99e";
+$tgico-replace: "\e99f";
+$tgico-reply: "\e9a0";
+$tgico-reply_filled: "\e9a1";
+$tgico-rightpanel: "\e9a2";
+$tgico-rotate_left: "\e9a3";
+$tgico-rotate_right: "\e9a4";
+$tgico-saved: "\e9a5";
+$tgico-savedmessages: "\e9a6";
+$tgico-schedule: "\e9a7";
+$tgico-scheduled: "\e9a8";
+$tgico-search: "\e9a9";
+$tgico-select: "\e9aa";
+$tgico-send: "\e9ab";
+$tgico-send2: "\e9ac";
+$tgico-sending: "\e9ad";
+$tgico-sendingerror: "\e9ae";
+$tgico-settings: "\e9af";
+$tgico-settings_filled: "\e9b0";
+$tgico-sharescreen_filled: "\e9b1";
+$tgico-shipping: "\e9b2";
+$tgico-shuffle: "\e9b3";
+$tgico-smallscreen: "\e9b4";
+$tgico-smile: "\e9b5";
+$tgico-spoiler: "\e9b6";
+$tgico-sport: "\e9b7";
+$tgico-star: "\e9b8";
+$tgico-stickers: "\e9b9";
+$tgico-stickers_face: "\e9ba";
+$tgico-stop: "\e9bb";
+$tgico-strikethrough: "\e9bc";
+$tgico-textedit: "\e9bd";
+$tgico-tip: "\e9be";
+$tgico-tools: "\e9bf";
+$tgico-topics: "\e9c0";
+$tgico-transcribe: "\e9c1";
+$tgico-unarchive: "\e9c2";
+$tgico-underline: "\e9c3";
+$tgico-unmute: "\e9c4";
+$tgico-unpin: "\e9c5";
+$tgico-unread: "\e9c6";
+$tgico-up: "\e9c7";
+$tgico-user: "\e9c8";
+$tgico-username: "\e9c9";
+$tgico-videocamera: "\e9ca";
+$tgico-videocamera_crossed_filled: "\e9cb";
+$tgico-videocamera_filled: "\e9cc";
+$tgico-videochat: "\e9cd";
+$tgico-volume_down: "\e9ce";
+$tgico-volume_mute: "\e9cf";
+$tgico-volume_off: "\e9d0";
+$tgico-volume_up: "\e9d1";
+$tgico-zoomin: "\e9d2";
+$tgico-zoomout: "\e9d3";