Extended media invoices

This commit is contained in:
Eduard Kuzmenko 2022-11-06 17:48:41 +04:00
parent 493897fbd3
commit 057a0638f2
36 changed files with 674 additions and 207 deletions

View File

@ -19,11 +19,12 @@ import {fastRaf} from '../helpers/schedulers';
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' |
`CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS' | 'STICKER-VIEWER' | 'EMOJI' |
'EMOJI-STATUS';
'EMOJI-STATUS' | `chat-${number}`;
export interface AnimationItem {
el: HTMLElement,
group: AnimationItemGroup,
animation: AnimationItemWrapper
animation: AnimationItemWrapper,
controlled?: boolean
};
export interface AnimationItemWrapper {
@ -46,7 +47,7 @@ export class AnimationIntersector {
private onlyOnePlayableGroup: AnimationItemGroup;
private intersectionLockedGroups: {[group in AnimationItemGroup]?: true};
private videosLocked;
private videosLocked: boolean;
constructor() {
this.observer = new IntersectionObserver((entries) => {
@ -107,19 +108,19 @@ export class AnimationIntersector {
appMediaPlaybackController.addEventListener('play', ({doc}) => {
if(doc.type === 'round') {
this.videosLocked = true;
this.checkAnimations();
this.checkAnimations2();
}
});
appMediaPlaybackController.addEventListener('pause', () => {
if(this.videosLocked) {
this.videosLocked = false;
this.checkAnimations();
this.checkAnimations2();
}
});
idleController.addEventListener('change', (idle) => {
this.checkAnimations(idle);
this.checkAnimations2(idle);
});
}
@ -174,40 +175,52 @@ export class AnimationIntersector {
}
}
public addAnimation(_animation: AnimationItem['animation'], group: AnimationItemGroup = '') {
if(group === 'none' || this.byPlayer.has(_animation)) {
public addAnimation(
animation: AnimationItem['animation'],
group: AnimationItemGroup = '',
observeElement?: HTMLElement,
controlled?: boolean
) {
if(group === 'none' || this.byPlayer.has(animation)) {
return;
}
let el: HTMLElement;
if(_animation instanceof RLottiePlayer) {
el = _animation.el[0];
} else if(_animation instanceof CustomEmojiRendererElement) {
el = _animation.canvas;
} else if(_animation instanceof CustomEmojiElement) {
el = _animation.placeholder ?? _animation;
} else if(_animation instanceof HTMLElement) {
el = _animation;
}
const animation: AnimationItem = {
el,
animation: _animation,
group
};
if(_animation instanceof RLottiePlayer) {
if(!rootScope.settings.stickers.loop && _animation.loop) {
_animation.loop = rootScope.settings.stickers.loop;
if(!observeElement) {
if(animation instanceof RLottiePlayer) {
observeElement = animation.el[0];
} else if(animation instanceof CustomEmojiRendererElement) {
observeElement = animation.canvas;
} else if(animation instanceof CustomEmojiElement) {
observeElement = animation.placeholder ?? animation;
} else if(animation instanceof HTMLElement) {
observeElement = animation;
}
}
(this.byGroups[group as AnimationItemGroup] ??= []).push(animation);
this.observer.observe(animation.el);
this.byPlayer.set(_animation, animation);
const item: AnimationItem = {
el: observeElement,
animation: animation,
group,
controlled
};
if(animation instanceof RLottiePlayer) {
if(!rootScope.settings.stickers.loop && animation.loop) {
animation.loop = rootScope.settings.stickers.loop;
}
}
(this.byGroups[group as AnimationItemGroup] ??= []).push(item);
this.observer.observe(item.el);
this.byPlayer.set(animation, item);
}
public checkAnimations(blurred?: boolean, group?: AnimationItemGroup, destroy = false) {
public checkAnimations(
blurred?: boolean,
group?: AnimationItemGroup,
destroy?: boolean,
imitateIntersection?: boolean
) {
// if(rootScope.idle.isIDLE) return;
if(group !== undefined && !this.byGroups[group]) {
@ -218,6 +231,10 @@ export class AnimationIntersector {
const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups) as AnimationItemGroup[];
for(const group of groups) {
if(imitateIntersection && this.intersectionLockedGroups[group]) {
continue;
}
const animations = this.byGroups[group];
forEachReverse(animations, (animation) => {
@ -226,11 +243,18 @@ export class AnimationIntersector {
}
}
public checkAnimation(player: AnimationItem, blurred = false, destroy = false) {
public checkAnimations2(blurred?: boolean) {
this.checkAnimations(blurred, undefined, undefined, true);
}
public checkAnimation(player: AnimationItem, blurred?: boolean, destroy?: boolean) {
const {el, animation, group} = player;
// return;
if(destroy || (!this.lockedGroups[group] && !isInDOM(el))) {
this.removeAnimation(player);
if(!player.controlled || destroy) {
this.removeAnimation(player);
}
return;
}
@ -295,6 +319,11 @@ export class AnimationIntersector {
delete this.intersectionLockedGroups[group];
this.refreshGroup(group);
}
public toggleIntersectionGroup(group: AnimationItemGroup, lock: boolean) {
if(lock) this.lockIntersectionGroup(group);
else this.unlockIntersectionGroup(group);
}
}
const animationIntersector = new AnimationIntersector();

View File

@ -18,6 +18,7 @@ import appDownloadManager from '../lib/appManagers/appDownloadManager';
import appImManager from '../lib/appManagers/appImManager';
import {MyMessage} from '../lib/appManagers/appMessagesManager';
import {MyPhoto} from '../lib/appManagers/appPhotosManager';
import canSaveMessageMedia from '../lib/appManagers/utils/messages/canSaveMessageMedia';
import getMediaFromMessage from '../lib/appManagers/utils/messages/getMediaFromMessage';
import wrapRichText from '../lib/richTextProcessor/wrapRichText';
import {MediaSearchContext} from './appMediaPlaybackController';
@ -260,13 +261,13 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const media = getMediaFromMessage(message);
const cantForwardMessage = message._ === 'messageService' || ! await this.managers.appMessagesManager.canForward(message);
const cantDownloadMessage = cantForwardMessage || !canSaveMessageMedia(message);
[this.buttons.forward, this.btnMenuForward.element].forEach((button) => {
button.classList.toggle('hide', cantForwardMessage);
});
this.wholeDiv.classList.toggle('no-forwards', cantForwardMessage);
this.wholeDiv.classList.toggle('no-forwards', cantDownloadMessage);
const cantDownloadMessage = cantForwardMessage;
[this.buttons.download, this.btnMenuDownload.element].forEach((button) => {
button.classList.toggle('hide', cantDownloadMessage);
});

View File

@ -468,7 +468,7 @@ export default class AppMediaViewerBase<
protected toggleOverlay(active: boolean) {
overlayCounter.isOverlayActive = active;
animationIntersector.checkAnimations(active);
animationIntersector.checkAnimations2(active);
}
protected toggleGlobalListeners(active: boolean) {
@ -1370,6 +1370,10 @@ export default class AppMediaViewerBase<
appMediaPlaybackController.addMedia(message, false, true) as HTMLVideoElement :
*/createVideo({pip: useController});
if(this.wholeDiv.classList.contains('no-forwards')) {
video.addEventListener('contextmenu', cancelEvent);
}
const set = () => this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
// return; // set and don't move
// if(wasActive) return;

View File

@ -8,7 +8,6 @@ import type {AppImManager, ChatSavedPosition} from '../../lib/appManagers/appImM
import type {HistoryResult, MyMessage} from '../../lib/appManagers/appMessagesManager';
import type {MyDocument} from '../../lib/appManagers/appDocsManager';
import type Chat from './chat';
import {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import {logger} from '../../lib/logger';
import rootScope from '../../lib/rootScope';
@ -121,6 +120,9 @@ import wrapPhoto from '../wrappers/photo';
import wrapPoll from '../wrappers/poll';
import wrapVideo from '../wrappers/video';
import isRTL from '../../helpers/string/isRTL';
import NBSP from '../../helpers/string/nbsp';
import DotRenderer from '../dotRenderer';
import toHHMMSS from '../../helpers/string/toHHMMSS';
export const USER_REACTIONS_INLINE = false;
const USE_MEDIA_TAILS = false;
@ -264,7 +266,7 @@ export default class ChatBubbles {
private sliceViewportDebounced: DebounceReturnType<ChatBubbles['sliceViewport']>;
private resizeObserver: ResizeObserver;
private willScrollOnLoad: boolean;
private observer: SuperIntersectionObserver;
public observer: SuperIntersectionObserver;
private renderingMessages: Set<number> = new Set();
private setPeerCached: boolean;
@ -280,6 +282,9 @@ export default class ChatBubbles {
private renderNewPromises: Set<Promise<any>> = new Set();
private updateGradient: boolean;
private extendedMediaMessages: Set<number> = new Set();
private pollExtendedMediaMessagesPromise: Promise<void>;
// private reactions: Map<number, ReactionsElement>;
constructor(
@ -1030,22 +1035,6 @@ export default class ChatBubbles {
this.observer = new SuperIntersectionObserver({root: this.scrollable.container});
this.listenerSetter.add(this.chat.appImManager)('chat_changing', ({to}) => {
const freeze = to !== this.chat;
const cb = () => {
this.observer.toggleObservingNew(freeze);
};
if(!freeze) {
setTimeout(() => {
cb();
}, 400);
} else {
cb();
}
});
this.sendViewCountersDebounced = debounce(() => {
const mids = [...this.viewsMids];
this.viewsMids.clear();
@ -1275,7 +1264,7 @@ export default class ChatBubbles {
height: 18,
needUpscale: true,
middleware,
group: CHAT_ANIMATION_GROUP,
group: this.chat.animationGroup,
withThumb: false,
needFadeIn: false
}).then(({render}) => render).then((player) => {
@ -1646,7 +1635,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);
new PopupPayment(message as Message.message, inputInvoice, undefined, true);
}
} else {
this.chat.appImManager.setInnerPeer({
@ -2206,7 +2195,7 @@ export default class ChatBubbles {
this.chat.selection.deleteSelectedMids(this.peerId, mids);
}
animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP);
animationIntersector.checkAnimations(false, this.chat.animationGroup);
this.deleteEmptyDateGroups();
if(!ignoreOnScroll) {
@ -2215,6 +2204,21 @@ export default class ChatBubbles {
}
}
private pollExtendedMediaMessages() {
const mids = Array.from(this.extendedMediaMessages);
return this.managers.appMessagesManager.getExtendedMedia(this.peerId, mids);
}
private setExtendedMediaMessagesPollInterval() {
if(this.pollExtendedMediaMessagesPromise || !this.extendedMediaMessages.size) {
return;
}
this.pollExtendedMediaMessagesPromise = pause(30000)
.then(() => this.pollExtendedMediaMessages())
.then(() => this.setExtendedMediaMessagesPollInterval());
}
private setTopPadding(middleware = this.getMiddleware()) {
let isPaddingNeeded = false;
let setPaddingTo: HTMLElement;
@ -2875,7 +2879,7 @@ export default class ChatBubbles {
/* this.ladderDeferred && this.ladderDeferred.resolve();
this.ladderDeferred = deferredPromise<void>(); */
animationIntersector.lockGroup(CHAT_ANIMATION_GROUP);
animationIntersector.lockGroup(this.chat.animationGroup);
const setPeerPromise = m(promise).then(async() => {
log.warn('promise fulfilled');
@ -2919,8 +2923,8 @@ export default class ChatBubbles {
log.warn('mounted chat', this.chatInner === chatInner, this.chatInner.parentElement, performance.now() - perf);
animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP);
animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP/* , true */);
animationIntersector.unlockGroup(this.chat.animationGroup);
animationIntersector.checkAnimations(false, this.chat.animationGroup/* , true */);
// fastRaf(() => {
this.lazyLoadQueue.unlock();
@ -3278,9 +3282,13 @@ export default class ChatBubbles {
}
const item = this.bubbleGroups.getItemByBubble(bubble);
item.mounted = false;
if(!groups.includes(item.group)) {
groups.push(item.group);
if(!item) {
this.log.error('NO ITEM BY BUBBLE', bubble);
} else {
item.mounted = false;
if(!groups.includes(item.group)) {
groups.push(item.group);
}
}
this.bubblesToReplace.delete(bubble);
@ -3621,7 +3629,7 @@ export default class ChatBubbles {
lazyLoadQueue: this.lazyLoadQueue,
customEmojiSize,
middleware,
animationGroup: CHAT_ANIMATION_GROUP
animationGroup: this.chat.animationGroup
});
let canHaveTail = true;
@ -3690,14 +3698,16 @@ export default class ChatBubbles {
}
const replyMarkup = isMessage && message.reply_markup;
if(replyMarkup && replyMarkup._ === 'replyInlineMarkup' && replyMarkup.rows && replyMarkup.rows.length) {
const rows = replyMarkup.rows;
let replyMarkupRows = replyMarkup?._ === 'replyInlineMarkup' && replyMarkup.rows;
if(replyMarkupRows) {
replyMarkupRows = replyMarkupRows.filter((row) => row.buttons.length);
}
if(replyMarkupRows) {
const containerDiv = document.createElement('div');
containerDiv.classList.add('reply-markup');
rows.forEach((row) => {
replyMarkupRows.forEach((row) => {
const buttons = row.buttons;
if(!buttons || !buttons.length) return;
const rowDiv = document.createElement('div');
rowDiv.classList.add('reply-markup-row');
@ -3761,13 +3771,16 @@ export default class ChatBubbles {
}
case 'keyboardButtonBuy': {
const mediaInvoice = messageMedia._ === 'messageMediaInvoice' ? messageMedia : undefined;
if(mediaInvoice?.extended_media) {
break;
}
buttonEl = document.createElement('button');
buttonEl.classList.add('is-buy');
if(messageMedia?._ === 'messageMediaInvoice') {
if(messageMedia.receipt_msg_id) {
text = i18n('Message.ReplyActionButtonShowReceipt');
}
if(mediaInvoice?.receipt_msg_id) {
text = i18n('Message.ReplyActionButtonShowReceipt');
}
break;
@ -3779,6 +3792,10 @@ export default class ChatBubbles {
}
}
if(!buttonEl) {
return;
}
buttonEl.classList.add('reply-markup-button', 'rp', 'tgico');
if(typeof(text) === 'string') {
buttonEl.insertAdjacentHTML('beforeend', text);
@ -3791,10 +3808,16 @@ export default class ChatBubbles {
rowDiv.append(buttonEl);
});
if(!rowDiv.childElementCount) {
return;
}
containerDiv.append(rowDiv);
});
attachClickEvent(containerDiv, (e) => {
const haveButtons = !!containerDiv.childElementCount;
haveButtons && attachClickEvent(containerDiv, (e) => {
let target = e.target as HTMLElement;
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
@ -3808,7 +3831,7 @@ export default class ChatBubbles {
cancelEvent(e);
const column = whichChild(target);
const row = rows[whichChild(target.parentElement)];
const row = replyMarkupRows[whichChild(target.parentElement)];
if(!row.buttons || !row.buttons[column]) {
this.log.warn('no such button', row, column, message);
@ -3825,9 +3848,11 @@ export default class ChatBubbles {
});
});
canHaveTail = false;
bubble.classList.add('with-reply-markup');
contentWrapper.append(containerDiv);
if(haveButtons) {
canHaveTail = false;
bubble.classList.add('with-reply-markup');
contentWrapper.append(containerDiv);
}
}
const isOutgoing = message.pFlags.is_outgoing/* && this.peerId !== rootScope.myId */;
@ -3968,7 +3993,7 @@ export default class ChatBubbles {
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
isOut,
group: CHAT_ANIMATION_GROUP,
group: this.chat.animationGroup,
loadPromises,
autoDownload: this.chat.autoDownload,
noInfo: message.mid < 0
@ -4115,7 +4140,7 @@ export default class ChatBubbles {
div: attachmentDiv,
middleware,
lazyLoadQueue: this.lazyLoadQueue,
group: CHAT_ANIMATION_GROUP,
group: this.chat.animationGroup,
// play: !!message.pending || !multipleRender,
play: true,
loop: true,
@ -4172,7 +4197,7 @@ export default class ChatBubbles {
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
group: CHAT_ANIMATION_GROUP,
group: this.chat.animationGroup,
loadPromises,
autoDownload: this.chat.autoDownload,
searchContext: isRound ? {
@ -4329,55 +4354,135 @@ export default class ChatBubbles {
case 'messageMediaInvoice': {
const isTest = messageMedia.pFlags.test;
const photo = messageMedia.photo;
const extendedMedia = messageMedia.extended_media;
const isAlreadyPaid = extendedMedia?._ === 'messageExtendedMedia';
const isNotPaid = extendedMedia?._ === 'messageExtendedMediaPreview';
let innerMedia = isAlreadyPaid ?
(extendedMedia.media as MessageMedia.messageMediaPhoto).photo as Photo.photo ||
(extendedMedia.media as MessageMedia.messageMediaDocument).document as Document.document :
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);
const wrappedPrice = paymentsWrapCurrencyAmount(messageMedia.total_amount, messageMedia.currency);
let priceEl: HTMLElement;
if(!extendedMedia) {
priceEl = document.createElement(innerMedia ? 'span' : 'div');
const f = document.createDocumentFragment();
const l = i18n(messageMedia.receipt_msg_id ? 'PaymentReceipt' : (isTest ? 'PaymentTestInvoice' : 'PaymentInvoice'));
l.classList.add('text-uppercase');
const joiner = ' ' + NBSP;
const p = document.createElement('span');
p.classList.add('text-bold');
p.textContent = wrappedPrice + 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);
} else if(isNotPaid) {
priceEl = document.createElement('span');
priceEl.classList.add('extended-media-buy', 'tgico-premium_lock');
attachmentDiv.classList.add('is-buy');
_i18n(priceEl, 'Checkout.PayPrice', [wrappedPrice]);
if(extendedMedia.video_duration !== undefined) {
const videoTime = document.createElement('span');
videoTime.classList.add('video-time');
videoTime.textContent = toHHMMSS(extendedMedia.video_duration, false);
attachmentDiv.append(videoTime);
}
}
setInnerHTML(priceEl, f);
if(photo) {
const mediaSize = mediaSizes.active.invoice;
wrapPhoto({
photo,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height
});
if(isNotPaid) {
(extendedMedia.thumb as PhotoSize.photoStrippedSize).w = extendedMedia.w;
(extendedMedia.thumb as PhotoSize.photoStrippedSize).h = extendedMedia.h;
innerMedia = {
_: 'photo',
access_hash: '',
pFlags: {},
date: 0,
dc_id: 0,
file_reference: [],
id: 0,
sizes: [extendedMedia.thumb]
};
}
bubble.classList.add('photo');
if(innerMedia) {
const mediaSize = extendedMedia ? mediaSizes.active.extendedInvoice : mediaSizes.active.invoice;
if(innerMedia._ === 'document') {
wrapVideo({
doc: innerMedia,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height,
group: this.chat.animationGroup,
message: message as Message.message
});
bubble.classList.add('video');
} else {
wrapPhoto({
photo: innerMedia,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height,
message: isAlreadyPaid ? message : undefined
});
bubble.classList.add('photo');
}
priceEl.classList.add('video-time');
attachmentDiv.append(priceEl);
if(priceEl) {
if(!extendedMedia) {
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));
if(isNotPaid) {
const {mid} = message;
this.extendedMediaMessages.add(mid);
middleware.onClean(() => {
this.extendedMediaMessages.delete(mid);
animationIntersector.removeAnimationByPlayer(dotRenderer);
});
this.setExtendedMediaMessagesPollInterval();
const richText = wrapEmojiText(messageMedia.description);
messageDiv.prepend(...[titleDiv, !photo && priceEl, richText].filter(Boolean));
const {width, height} = attachmentDiv.style;
const dotRenderer = new DotRenderer(parseInt(width), parseInt(height));
dotRenderer.renderFirstFrame();
attachmentDiv.append(dotRenderer.canvas);
bubble.classList.remove('is-message-empty');
animationIntersector.addAnimation(dotRenderer, this.chat.animationGroup, dotRenderer.canvas, true);
}
let titleDiv: HTMLElement;
if(!extendedMedia) {
titleDiv = document.createElement('div');
titleDiv.classList.add('bubble-primary-color');
setInnerHTML(titleDiv, wrapEmojiText(messageMedia.title));
}
const richText = isAlreadyPaid ? undefined : wrapEmojiText(messageMedia.description);
messageDiv.prepend(...[titleDiv, !innerMedia && priceEl, richText].filter(Boolean));
if(!richText) canHaveTail = false;
else bubble.classList.remove('is-message-empty');
bubble.classList.add('is-invoice');
break;
@ -5028,7 +5133,7 @@ export default class ChatBubbles {
div: stickerDiv,
middleware,
lazyLoadQueue: this.lazyLoadQueue,
group: CHAT_ANIMATION_GROUP,
group: this.chat.animationGroup,
// play: !!message.pending || !multipleRender,
play: true,
loop: true,

View File

@ -35,6 +35,7 @@ import noop from '../../helpers/noop';
import middlewarePromise from '../../helpers/middlewarePromise';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {Message} from '../../layer';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -86,6 +87,8 @@ export default class Chat extends EventListenerBase<{
public isAnyGroup: boolean;
public isMegagroup: boolean;
public animationGroup: AnimationItemGroup;
constructor(
public appImManager: AppImManager,
public managers: AppManagers
@ -93,6 +96,7 @@ export default class Chat extends EventListenerBase<{
super();
this.type = 'chat';
this.animationGroup = `chat-${Math.round(Math.random() * 65535)}`;
this.container = document.createElement('div');
this.container.classList.add('chat', 'tabs-tab');
@ -352,6 +356,26 @@ export default class Chat extends EventListenerBase<{
this.appImManager.setPeer();
}
});
this.bubbles.listenerSetter.add(this.appImManager)('chat_changing', ({to}) => {
const freeze = to !== this;
const cb = () => {
this.bubbles.observer.toggleObservingNew(freeze);
animationIntersector.toggleIntersectionGroup(this.animationGroup, freeze);
if(freeze) {
animationIntersector.checkAnimations(freeze, this.animationGroup);
}
};
if(!freeze) {
setTimeout(() => {
cb();
}, 400);
} else {
cb();
}
});
}
public beforeDestroy() {

View File

@ -45,6 +45,8 @@ import replaceContent from '../../helpers/dom/replaceContent';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import PopupStickers from '../popups/stickers';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
import canSaveMessageMedia from '../../lib/appManagers/utils/messages/canSaveMessageMedia';
export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise<boolean>, notDirect?: () => boolean, withSelection?: true, isSponsored?: true, localName?: 'views' | 'emojis'})[];
@ -432,10 +434,10 @@ export default class ChatContextMenu {
icon: 'download',
text: 'MediaViewer.Context.Download',
onClick: () => {
appDownloadManager.downloadToDisc({media: (this.message as any).media?.document || (this.message as any).media.photo});
appDownloadManager.downloadToDisc({media: getMediaFromMessage(this.message)});
},
verify: () => {
if(this.message.pFlags.is_outgoing || this.noForwards) {
if(!canSaveMessageMedia(this.message) || this.noForwards) {
return false;
}

View File

@ -620,7 +620,7 @@ export default class ChatInput {
this.rowsWrapper.append(this.replyElements.container);
this.autocompleteHelperController = new AutocompleteHelperController();
this.stickersHelper = new StickersHelper(this.rowsWrapper, this.autocompleteHelperController, this.managers);
this.stickersHelper = new StickersHelper(this.rowsWrapper, this.autocompleteHelperController, this.chat, this.managers);
this.emojiHelper = new EmojiHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
this.commandsHelper = new CommandsHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
this.mentionsHelper = new MentionsHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
@ -2786,7 +2786,7 @@ export default class ChatInput {
if(!message) { // load missing replying message
peerTitleEl = i18n('Loading');
this.managers.appMessagesManager.wrapSingleMessage(this.chat.peerId, mid).then((_message) => {
this.managers.appMessagesManager.reloadMessages(this.chat.peerId, mid).then((_message) => {
if(this.replyToMsgId !== mid) {
return;
}
@ -2880,7 +2880,7 @@ export default class ChatInput {
const haveReply = oldReply.classList.contains('reply');
this.replyElements.iconBtn.replaceWith(this.replyElements.iconBtn = ButtonIcon((type === 'webpage' ? 'link' : type) + ' active reply-icon', {noRipple: true}));
const {container} = wrapReply(title, subtitle, message);
const {container} = wrapReply(title, subtitle, this.chat.animationGroup, message);
if(haveReply) {
oldReply.replaceWith(container);
} else {

View File

@ -205,7 +205,7 @@ export namespace MessageRender {
}).element;
}
const {container, fillPromise} = wrapReply(originalPeerTitle, undefined, originalMessage, chat.isAnyGroup ? titlePeerId : undefined);
const {container, fillPromise} = wrapReply(originalPeerTitle, undefined, chat.animationGroup, originalMessage, chat.isAnyGroup ? titlePeerId : undefined);
await fillPromise;
if(currentReplyDiv) {
currentReplyDiv.replaceWith(container);

View File

@ -266,7 +266,7 @@ export default class ChatPinnedMessage {
this.debug = true;
this.isStatic = false;
const dAC = new ReplyContainer('pinned-message');
const dAC = new ReplyContainer('pinned-message', chat.animationGroup);
this.pinnedMessageContainer = new PinnedContainer({
topbar,
chat,
@ -624,7 +624,8 @@ export default class ChatPinnedMessage {
subtitleEl: writeTo,
message,
mediaEl: writeMediaTo,
loadPromises
loadPromises,
animationGroup: this.chat.animationGroup
});
await Promise.all(loadPromises);

View File

@ -8,7 +8,7 @@ import replaceContent from '../../helpers/dom/replaceContent';
import {Middleware} from '../../helpers/middleware';
import limitSymbols from '../../helpers/string/limitSymbols';
import {Document, MessageMedia, Photo, WebPage} from '../../layer';
import appImManager, {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager';
import appImManager from '../../lib/appManagers/appImManager';
import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import DivAndCaption from '../divAndCaption';
@ -16,6 +16,7 @@ import wrapMessageForReply from '../wrappers/messageForReply';
import wrapPhoto from '../wrappers/photo';
import wrapSticker from '../wrappers/sticker';
import wrapVideo from '../wrappers/video';
import {AnimationItemGroup} from '../animationIntersector';
const MEDIA_SIZE = 32;
@ -26,9 +27,10 @@ export async function wrapReplyDivAndCaption(options: {
subtitleEl: HTMLElement,
message: any,
mediaEl: HTMLElement,
loadPromises?: Promise<any>[]
loadPromises?: Promise<any>[],
animationGroup: AnimationItemGroup
}) {
let {title, titleEl, subtitle, subtitleEl, mediaEl, message, loadPromises} = options;
let {title, titleEl, subtitle, subtitleEl, mediaEl, message, loadPromises, animationGroup} = options;
if(title !== undefined) {
if(typeof(title) === 'string') {
title = limitSymbols(title, 140);
@ -62,7 +64,7 @@ export async function wrapReplyDivAndCaption(options: {
doc: document,
div: mediaEl,
lazyLoadQueue,
group: CHAT_ANIMATION_GROUP,
group: animationGroup,
// onlyThumb: document.sticker === 2,
width: MEDIA_SIZE,
height: MEDIA_SIZE,
@ -84,7 +86,7 @@ export async function wrapReplyDivAndCaption(options: {
loadPromises,
withoutPreloader: true,
videoSize: document.video_thumbs[0],
group: CHAT_ANIMATION_GROUP
group: animationGroup
});
} else {
const m = photo || document;
@ -138,7 +140,7 @@ export async function wrapReplyDivAndCaption(options: {
export default class ReplyContainer extends DivAndCaption<(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message?: any) => Promise<void>> {
private mediaEl: HTMLElement;
constructor(protected className: string) {
constructor(protected className: string, protected animationGroup: AnimationItemGroup) {
super(className, async(title, subtitle = '', message?) => {
if(!this.mediaEl) {
this.mediaEl = document.createElement('div');
@ -151,7 +153,8 @@ export default class ReplyContainer extends DivAndCaption<(title: string | HTMLE
subtitle,
subtitleEl: this.subtitle,
mediaEl: this.mediaEl,
message
message,
animationGroup
});
this.container.classList.toggle('is-media', isMediaSet);

View File

@ -4,11 +4,11 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type Chat from './chat';
import ListenerSetter from '../../helpers/listenerSetter';
import mediaSizes from '../../helpers/mediaSizes';
import preloadAnimatedEmojiSticker from '../../helpers/preloadAnimatedEmojiSticker';
import {MyDocument} from '../../lib/appManagers/appDocsManager';
import {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager';
import {AppManagers} from '../../lib/appManagers/managers';
import rootScope from '../../lib/rootScope';
import {EmoticonsDropdown} from '../emoticonsDropdown';
@ -29,6 +29,7 @@ export default class StickersHelper extends AutocompleteHelper {
constructor(
appendTo: HTMLElement,
controller: AutocompleteHelperController,
private chat: Chat,
private managers: AppManagers
) {
super({
@ -132,6 +133,6 @@ export default class StickersHelper extends AutocompleteHelper {
this.scrollable = new Scrollable(this.container);
this.lazyLoadQueue = new LazyLoadQueue();
this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, CHAT_ANIMATION_GROUP, this.managers);
this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, this.chat.animationGroup, this.managers);
}
}

View File

@ -0,0 +1,148 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {IS_MOBILE} from '../environment/userAgent';
import {animate} from '../helpers/animation';
import drawCircle, {drawCircleFromStart} from '../helpers/canvas/drawCircle';
import clamp from '../helpers/number/clamp';
import {AnimationItemWrapper} from './animationIntersector';
type DotRendererDot = {
x: number,
y: number,
opacity: number,
radius: number
mOpacity: number,
adding: boolean,
counter: number,
path: Path2D
};
export default class DotRenderer implements AnimationItemWrapper {
public canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private dots: DotRendererDot[];
public paused: boolean;
public autoplay: boolean;
public tempId: number;
private dpr: number;
constructor(private width: number, private height: number) {
const canvas = this.canvas = document.createElement('canvas');
const dpr = this.dpr = window.devicePixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.cssText = `position: absolute; width: 100%; height: 100%; z-index: 1;`;
this.paused = true;
this.autoplay = true;
this.tempId = 0;
this.context = canvas.getContext('2d');
}
private prepare() {
let count = Math.round(this.width * this.height / (35 * (IS_MOBILE ? 2 : 1)));
count = Math.min(IS_MOBILE ? 1000 : 2200, count);
const dots: DotRendererDot[] = this.dots = new Array(count);
for(let i = 0; i < count; ++i) {
dots[i] = this.generateDot();
}
}
private generateDot(adding?: boolean): DotRendererDot {
const x = Math.floor(Math.random() * this.canvas.width);
const y = Math.floor(Math.random() * this.canvas.height);
const opacity = adding ? 0 : Math.random();
const radius = (Math.random() >= .8 ? 1 : 0.5) * this.dpr;
const path = new Path2D();
path.arc(x, y, radius, 0, 2 * Math.PI, false);
return {
x,
y,
opacity,
radius,
mOpacity: opacity,
adding: adding ?? Math.random() >= .5,
counter: 0,
path
};
}
private draw() {
const {context, canvas, dots} = this;
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#fff';
const add = 0.02;
for(let i = 0, length = dots.length; i < length; ++i) {
const dot = dots[i];
const addOpacity = dot.adding ? add : -add;
dot.mOpacity += addOpacity;
// if(dot.mOpacity <= 0) dot.mOpacity = dot.opacity;
// const easedOpacity = easing(dot.mOpacity);
const easedOpacity = clamp(dot.mOpacity, 0, 1);
context.globalAlpha = easedOpacity;
context.fill(dot.path);
if(dot.mOpacity <= 0) {
dot.adding = true;
if(++dot.counter >= 1) {
dots[i] = this.generateDot(dot.adding);
}
} else if(dot.mOpacity >= 1) {
dot.adding = false;
}
}
}
public remove() {
this.pause();
}
public pause() {
if(this.paused) {
return;
}
this.paused = true;
++this.tempId;
}
public renderFirstFrame() {
if(!this.dots) {
this.prepare();
}
this.draw();
}
public play() {
if(!this.paused) {
return;
}
this.paused = false;
const tempId = ++this.tempId;
if(!this.dots) {
this.prepare();
}
animate(() => {
if(this.tempId !== tempId || this.paused) {
return false;
}
this.draw();
return true;
});
}
}

View File

@ -101,7 +101,7 @@ export class SuperStickerRenderer {
const players = animationIntersector.getAnimations(element);
players.forEach((player) => {
if(!visible) {
animationIntersector.checkAnimation(player, true, true);
animationIntersector.removeAnimation(player);
} else {
animationIntersector.checkAnimation(player, false);
}

View File

@ -116,7 +116,7 @@ export default class GifsMasonry {
video.load();
const animations = animationIntersector.getAnimations(video);
animations.forEach((item) => {
animationIntersector.checkAnimation(item, true, true);
animationIntersector.removeAnimation(item);
});
}, 0);
}
@ -169,7 +169,7 @@ export default class GifsMasonry {
video.load();
const animations = animationIntersector.getAnimations(video);
animations.forEach((item) => {
animationIntersector.checkAnimation(item, true, true);
animationIntersector.removeAnimation(item);
});
}
});

View File

@ -409,7 +409,7 @@ export default class PopupGroupCall extends PopupElement {
this.btnClose.classList.toggle('hide', isFull);
if(isFull !== wasFullScreen) {
animationIntersector.checkAnimations(isFull);
animationIntersector.checkAnimations2(isFull);
themeController.setThemeColor(isFull ? '#000000' : undefined);
}

View File

@ -239,7 +239,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = true;
animationIntersector.checkAnimations(true);
animationIntersector.checkAnimations2(true);
}
// cannot add event instantly because keydown propagation will fire it
@ -292,7 +292,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.cleanup();
if(!this.withoutOverlay) {
animationIntersector.checkAnimations(false);
animationIntersector.checkAnimations2(false);
}
}, 150);
}

View File

@ -105,7 +105,8 @@ export default class PopupPayment extends PopupElement {
constructor(
private message: Message.message,
private inputInvoice: InputInvoice,
private paymentForm?: PaymentsPaymentForm | PaymentsPaymentReceipt
private paymentForm?: PaymentsPaymentForm | PaymentsPaymentReceipt,
private isReceipt?: boolean
) {
super('popup-payment', {
closable: true,
@ -116,7 +117,10 @@ export default class PopupPayment extends PopupElement {
});
this.tipButtonsMap = new Map();
this.d();
this.d().catch((err) => {
console.error('payment popup error', err);
this.hide();
});
}
private async d() {
@ -148,7 +152,12 @@ export default class PopupPayment extends PopupElement {
}
const mediaInvoice = message?.media as MessageMedia.messageMediaInvoice;
const isReceipt = mediaInvoice ? !!mediaInvoice.receipt_msg_id : paymentForm._ === 'payments.paymentReceipt';
const isReceipt = this.isReceipt ??
(
mediaInvoice ?
!!mediaInvoice.receipt_msg_id || mediaInvoice.extended_media?._ === 'messageExtendedMedia' :
paymentForm._ === 'payments.paymentReceipt'
);
const isTest = mediaInvoice ? mediaInvoice.pFlags.test : paymentForm.invoice.pFlags.test;
const photo = mediaInvoice ? mediaInvoice.photo : paymentForm.photo;
@ -215,7 +224,7 @@ export default class PopupPayment extends PopupElement {
const inputInvoice = this.inputInvoice;
if(!paymentForm) {
if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(message.peerId, mediaInvoice.receipt_msg_id);
if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(message.peerId, mediaInvoice.receipt_msg_id || (inputInvoice as InputInvoice.inputInvoiceMessage).msg_id);
else paymentForm = await this.managers.appPaymentsManager.getPaymentForm(inputInvoice);
this.paymentForm = paymentForm;
}
@ -228,7 +237,7 @@ export default class PopupPayment extends PopupElement {
wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()})
]);
console.log(paymentForm, lastRequestedInfo);
// console.log(paymentForm, lastRequestedInfo);
await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()});
preloaderContainer.remove();

View File

@ -169,7 +169,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter,
if(lockGroups) {
animationIntersector.setOnlyOnePlayableGroup(group);
animationIntersector.checkAnimations(true);
animationIntersector.checkAnimations2(true);
}
if(player instanceof RLottiePlayer) {
@ -311,7 +311,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter,
SetTransition(container, 'is-visible', false, openDuration, () => {
container.remove();
animationIntersector.setOnlyOnePlayableGroup(previousGroup);
animationIntersector.checkAnimations(false);
animationIntersector.checkAnimations2(false);
hasViewer = false;
});

View File

@ -159,7 +159,12 @@ export default async function wrapMessageForReply(message: MyMessage | MyDraftMe
}
case 'messageMediaInvoice': {
addPart(undefined, plain ? media.title : wrapEmojiText(media.title));
if(media.extended_media?._ === 'messageExtendedMediaPreview') {
addPart(undefined, plain ? media.description : wrapEmojiText(media.description));
} else {
addPart(undefined, plain ? media.title : wrapEmojiText(media.title));
}
break;
}

View File

@ -161,7 +161,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
}
// }
if(size?._ === 'photoSizeEmpty' && isDocument) {
if((size?._ === 'photoSizeEmpty' && isDocument) || (size as PhotoSize.photoStrippedSize)?.bytes) {
return ret;
}

View File

@ -7,15 +7,17 @@
import {hexToRgb} from '../../helpers/color';
import {Message} from '../../layer';
import getPeerColorById from '../../lib/appManagers/utils/peers/getPeerColorById';
import {AnimationItemGroup} from '../animationIntersector';
import ReplyContainer from '../chat/replyContainer';
export default function wrapReply(
title: Parameters<ReplyContainer['fill']>[0],
subtitle: Parameters<ReplyContainer['fill']>[1],
animationGroup: AnimationItemGroup,
message?: Message.message | Message.messageService,
setColorPeerId?: PeerId
) {
const replyContainer = new ReplyContainer('reply');
const replyContainer = new ReplyContainer('reply', animationGroup);
const fillPromise = replyContainer.fill(title, subtitle, message);
if(setColorPeerId) {

View File

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

3
src/global.d.ts vendored
View File

@ -56,7 +56,8 @@ declare global {
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |
'BOT_PRECHECKOUT_TIMEOUT' | 'TMP_PASSWORD_INVALID' | 'PASSWORD_HASH_INVALID' | 'CHANNEL_PRIVATE' |
'VOICE_MESSAGES_FORBIDDEN' | 'PHOTO_INVALID_DIMENSIONS' | 'PHOTO_SAVE_FILE_INVALID';
'VOICE_MESSAGES_FORBIDDEN' | 'PHOTO_INVALID_DIMENSIONS' | 'PHOTO_SAVE_FILE_INVALID' |
'USER_ALREADY_PARTICIPANT';
type ErrorType = LocalErrorType | ServerErrorType;

View File

@ -20,6 +20,7 @@ type MediaTypeSizes = {
round: MediaSize,
documentName: MediaSize,
invoice: MediaSize,
extendedInvoice: MediaSize,
customEmoji: MediaSize,
esgCustomEmoji: MediaSize,
emojiStatus: MediaSize,
@ -64,7 +65,8 @@ class MediaSizes extends EventListenerBase<{
poll: makeMediaSize(240, 0),
round: makeMediaSize(200, 200),
documentName: makeMediaSize(200, 0),
invoice: makeMediaSize(240, 240),
invoice: makeMediaSize(270, 270),
extendedInvoice: makeMediaSize(270, 270),
customEmoji: CUSTOM_EMOJI_SIZE,
esgCustomEmoji: ESG_CUSTOM_EMOJI_SIZE,
emojiStatus: EMOJI_STATUS_SIZE,
@ -81,7 +83,8 @@ class MediaSizes extends EventListenerBase<{
poll: makeMediaSize(330, 0),
round: makeMediaSize(280, 280),
documentName: makeMediaSize(240, 0),
invoice: makeMediaSize(320, 260),
invoice: makeMediaSize(320, 320),
extendedInvoice: makeMediaSize(420, 340),
customEmoji: CUSTOM_EMOJI_SIZE,
esgCustomEmoji: ESG_CUSTOM_EMOJI_SIZE,
emojiStatus: EMOJI_STATUS_SIZE,

View File

@ -0,0 +1,5 @@
export default function getRandomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@ -949,6 +949,7 @@ const lang = {
'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.PayPrice': 'Pay %@',
'Checkout.WebConfirmation.Title': 'Complete Payment',
'ChatList.Context.Mute': 'Mute',
'ChatList.Context.Unmute': 'Unmute',

4
src/layer.d.ts vendored
View File

@ -1427,7 +1427,9 @@ export namespace PhotoSize {
export type photoStrippedSize = {
_: 'photoStrippedSize',
type: string,
bytes: Uint8Array
bytes: Uint8Array,
w?: number,
h?: number
};
export type photoSizeProgressive = {

View File

@ -92,8 +92,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent';
import PopupPayment from '../../components/popups/payment';
import wrapPeerTitle from '../../components/wrappers/peerTitle';
export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat';
import NBSP from '../../helpers/string/nbsp';
export type ChatSavedPosition = {
mids: number[],
@ -215,10 +214,10 @@ export class AppImManager extends EventListenerBase<{
useHeavyAnimationCheck(() => {
animationIntersector.setOnlyOnePlayableGroup('lock');
animationIntersector.checkAnimations(true);
animationIntersector.checkAnimations2(true);
}, () => {
animationIntersector.setOnlyOnePlayableGroup();
animationIntersector.checkAnimations(false);
animationIntersector.checkAnimations2(false);
});
if(IS_FIREFOX && apiManagerProxy.oldVersion && compareVersion(apiManagerProxy.oldVersion, '1.4.3') === -1) {
@ -1125,7 +1124,7 @@ export class AppImManager extends EventListenerBase<{
* Opens thread when peerId of discussion group is known
*/
public openThread(peerId: PeerId, lastMsgId: number, threadId: number) {
return this.managers.appMessagesManager.wrapSingleMessage(peerId, threadId).then((message) => {
return this.managers.appMessagesManager.reloadMessages(peerId, threadId).then((message) => {
// const message: Message = this.managers.appMessagesManager.getMessageByPeer(peerId, threadId);
if(!message) {
lastMsgId = undefined;
@ -1382,7 +1381,7 @@ export class AppImManager extends EventListenerBase<{
}, rootScope.settings.animationsEnabled ? 250 : 0, false, true);
lottieLoader.setLoop(rootScope.settings.stickers.loop);
animationIntersector.checkAnimations(false);
animationIntersector.checkAnimations2(false);
for(const chat of this.chats) {
chat.setAutoDownloadMedia();
@ -2177,7 +2176,7 @@ export class AppImManager extends EventListenerBase<{
return () => replaceContent(element, subtitle || placeholder);
};
const placeholder = useWhitespace ? '' : ''; // ! HERE U CAN FIND WHITESPACE
const placeholder = useWhitespace ? NBSP : ''; // ! HERE U CAN FIND WHITESPACE
if(!result || result.cached) {
return await set();
} else if(needClear) {

View File

@ -192,6 +192,7 @@ export class AppMessagesManager extends AppManager {
private needSingleMessages: Map<PeerId, Map<number, CancellablePromise<Message.message | Message.messageService>>> = new Map();
private fetchSingleMessagesPromise: Promise<void> = null;
private extendedMedia: Map<PeerId, Map<number, CancellablePromise<void>>> = new Map();
private maxSeenId = 0;
@ -280,7 +281,9 @@ export class AppMessagesManager extends AppManager {
updateNewScheduledMessage: this.onUpdateNewScheduledMessage,
updateDeleteScheduledMessages: this.onUpdateDeleteScheduledMessages
updateDeleteScheduledMessages: this.onUpdateDeleteScheduledMessages,
updateMessageExtendedMedia: this.onUpdateMessageExtendedMedia
});
// ! Invalidate notify settings, can optimize though
@ -2608,9 +2611,7 @@ export class AppMessagesManager extends AppManager {
return;
}
if(message.pFlags === undefined) {
message.pFlags = {};
}
message.pFlags ??= {};
// * exclude from state
// defineNotNumerableProperties(message, ['rReply', 'mid', 'savedFrom', 'fwdFromId', 'fromId', 'peerId', 'reply_to_mid', 'viaBotId']);
@ -2729,21 +2730,22 @@ export class AppMessagesManager extends AppManager {
} */
let unsupported = false;
if(isMessage && message.media) {
switch(message.media._) {
const media = isMessage && message.media;
if(media) {
switch(media._) {
case 'messageMediaEmpty': {
delete message.media;
break;
}
case 'messageMediaPhoto': {
if(message.media.ttl_seconds) {
if(media.ttl_seconds) {
unsupported = true;
} else {
message.media.photo = this.appPhotosManager.savePhoto(message.media.photo, mediaContext);
media.photo = this.appPhotosManager.savePhoto(media.photo, mediaContext);
}
if(!(message.media as MessageMedia.messageMediaPhoto).photo) { // * found this bug on test DC
if(!(media as MessageMedia.messageMediaPhoto).photo) { // * found this bug on test DC
delete message.media;
}
@ -2751,20 +2753,20 @@ export class AppMessagesManager extends AppManager {
}
case 'messageMediaPoll': {
const result = this.appPollsManager.savePoll(message.media.poll, message.media.results, message);
message.media.poll = result.poll;
message.media.results = result.results;
const result = this.appPollsManager.savePoll(media.poll, media.results, message);
media.poll = result.poll;
media.results = result.results;
break;
}
case 'messageMediaDocument': {
if(message.media.ttl_seconds) {
if(media.ttl_seconds) {
unsupported = true;
} else {
const originalDoc = message.media.document;
message.media.document = this.appDocsManager.saveDoc(originalDoc, mediaContext); // 11.04.2020 warning
const originalDoc = media.document;
media.document = this.appDocsManager.saveDoc(originalDoc, mediaContext); // 11.04.2020 warning
if(!message.media.document && originalDoc._ !== 'documentEmpty') {
if(!media.document && originalDoc._ !== 'documentEmpty') {
unsupported = true;
}
}
@ -2774,7 +2776,7 @@ export class AppMessagesManager extends AppManager {
case 'messageMediaWebPage': {
const messageKey = this.appWebPagesManager.getMessageKeyForPendingWebPage(peerId, mid, options.isScheduled);
message.media.webpage = this.appWebPagesManager.saveWebPage(message.media.webpage, messageKey, mediaContext);
media.webpage = this.appWebPagesManager.saveWebPage(media.webpage, messageKey, mediaContext);
break;
}
@ -2784,7 +2786,13 @@ export class AppMessagesManager extends AppManager {
break; */
case 'messageMediaInvoice': {
message.media.photo = this.appWebDocsManager.saveWebDocument(message.media.photo);
media.photo = this.appWebDocsManager.saveWebDocument(media.photo);
const extendedMedia = media.extended_media;
if(extendedMedia?._ === 'messageExtendedMedia') {
const extendedMediaMedia = extendedMedia.media;
(extendedMediaMedia as MessageMedia.messageMediaPhoto).photo = this.appPhotosManager.savePhoto((extendedMediaMedia as MessageMedia.messageMediaPhoto).photo, mediaContext);
(extendedMediaMedia as MessageMedia.messageMediaDocument).document = this.appDocsManager.saveDoc((extendedMediaMedia as MessageMedia.messageMediaDocument).document, mediaContext);
}
break;
}
@ -3046,8 +3054,8 @@ export class AppMessagesManager extends AppManager {
promise = this.appChatsManager.addChatUser(chatId, botId, 0);
}
return promise.catch((error) => {
if(error && error.type == 'USER_ALREADY_PARTICIPANT') {
return promise.catch((error: ApiError) => {
if(error?.type == 'USER_ALREADY_PARTICIPANT') {
error.handled = true;
return;
}
@ -4312,6 +4320,7 @@ export class AppMessagesManager extends AppManager {
this.rootScope.dispatchEvent('dialog_flush', {peerId, dialog});
}
} else {
let dispatchEditEvent = true;
// no sense in dispatching message_edit since only reactions have changed
if(oldMessage?._ === 'message' && !deepEqual(oldMessage.reactions, (newMessage as Message.message).reactions)) {
const newReactions = (newMessage as Message.message).reactions;
@ -4323,10 +4332,10 @@ export class AppMessagesManager extends AppManager {
reactions: newReactions
});
return;
dispatchEditEvent = false;
}
this.rootScope.dispatchEvent('message_edit', {
dispatchEditEvent && this.rootScope.dispatchEvent('message_edit', {
storageKey: storage.key,
peerId,
mid,
@ -4693,7 +4702,7 @@ export class AppMessagesManager extends AppManager {
const storage = this.getHistoryMessagesStorage(peerId);
const missingMessages = messages.filter((mid) => !storage.has(mid));
const getMissingPromise = missingMessages.length ? Promise.all(missingMessages.map((mid) => this.wrapSingleMessage(peerId, mid))) : Promise.resolve();
const getMissingPromise = missingMessages.length ? Promise.all(missingMessages.map((mid) => this.reloadMessages(peerId, mid))) : Promise.resolve();
getMissingPromise.finally(() => {
const werePinned = update.pFlags?.pinned;
if(werePinned) {
@ -4782,6 +4791,30 @@ export class AppMessagesManager extends AppManager {
}
};
private onUpdateMessageExtendedMedia = (update: Update.updateMessageExtendedMedia) => {
const peerId = this.appPeersManager.getPeerId(update.peer);
const mid = generateMessageId(update.msg_id);
const storage = this.getHistoryMessagesStorage(peerId);
if(!storage.has(mid)) {
// this.fixDialogUnreadMentionsIfNoMessage(peerId);
return;
}
const message = this.getMessageFromStorage(storage, mid) as Message.message;
const messageMedia = message.media as MessageMedia.messageMediaInvoice;
if(messageMedia.extended_media?._ === 'messageExtendedMedia') {
return;
}
messageMedia.extended_media = update.extended_media;
this.onUpdateEditMessage({
_: 'updateEditMessage',
message,
pts: 0,
pts_count: 0
});
};
public setDialogToStateIfMessageIsTop(message: MyMessage) {
if(this.isMessageIsTopMessage(message)) {
this.dialogsStorage.setDialogToState(this.getDialogOnly(message.peerId));
@ -4820,7 +4853,7 @@ export class AppMessagesManager extends AppManager {
}
public updateMessage(peerId: PeerId, mid: number, broadcastEventName?: 'replies_updated'): Promise<Message.message> {
const promise: Promise<Message.message> = this.wrapSingleMessage(peerId, mid, true).then(() => {
const promise: Promise<Message.message> = this.reloadMessages(peerId, mid, true).then(() => {
const message = this.getMessageByPeer(peerId, mid) as Message.message;
if(!message) {
return;
@ -5621,7 +5654,15 @@ export class AppMessagesManager extends AppManager {
});
}
public wrapSingleMessage(peerId: PeerId, mid: number, overwrite = false) {
public reloadMessages(peerId: PeerId, mid: number, overwrite?: boolean): Promise<MyMessage>;
public reloadMessages(peerId: PeerId, mid: number[], overwrite?: boolean): Promise<MyMessage[]>;
public reloadMessages(peerId: PeerId, mid: number | number[], overwrite?: boolean): Promise<MyMessage | MyMessage[]> {
if(Array.isArray(mid)) {
return Promise.all(mid.map((mid) => {
return this.reloadMessages(peerId, mid, overwrite);
}));
}
const message = this.getMessageByPeer(peerId, mid);
if(message && !overwrite) {
this.rootScope.dispatchEvent('messages_downloaded', {peerId, mids: [mid]});
@ -5644,10 +5685,50 @@ export class AppMessagesManager extends AppManager {
}
}
public getExtendedMedia(peerId: PeerId, mids: number[]) {
let map = this.extendedMedia.get(peerId);
if(!map) {
this.extendedMedia.set(peerId, map = new Map());
}
const deferred = deferredPromise<void>();
const toRequest: number[] = [];
const promises = mids.map((mid) => {
let promise = map.get(mid);
if(!promise) {
map.set(mid, promise = deferred);
toRequest.push(mid);
promise.then(() => {
map.delete(mid);
if(!map.size && this.extendedMedia.get(peerId) === map) {
this.extendedMedia.delete(peerId);
}
});
}
return promise;
});
if(!toRequest.length) {
deferred.resolve();
} else {
this.apiManager.invokeApi('messages.getExtendedMedia', {
peer: this.appPeersManager.getInputPeerById(peerId),
id: toRequest.map((mid) => getServerMessageId(mid))
}).then((updates) => {
this.apiUpdatesManager.processUpdateMessage(updates);
deferred.resolve();
});
}
return Promise.all(promises);
}
public fetchMessageReplyTo(message: MyMessage) {
if(!message.reply_to_mid) return Promise.resolve(this.generateEmptyMessage(0));
const replyToPeerId = message.reply_to.reply_to_peer_id ? this.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : message.peerId;
return this.wrapSingleMessage(replyToPeerId, message.reply_to_mid).then((originalMessage) => {
return this.reloadMessages(replyToPeerId, message.reply_to_mid).then((originalMessage) => {
if(!originalMessage) { // ! break the infinite loop
message = this.getMessageByPeer(message.peerId, message.mid); // message can come from other thread
delete message.reply_to_mid; // ! WARNING!

View File

@ -116,7 +116,7 @@ export class AppReactionsManager extends AppManager {
}
private unshiftQuickReactionInner(availableReactions: AvailableReaction[], quickReaction: Reaction | AvailableReaction) {
if(quickReaction._ !== 'reactionEmoji' && quickReaction._ !== 'availableReaction') return availableReactions;
if(quickReaction && quickReaction._ !== 'reactionEmoji' && quickReaction._ !== 'availableReaction') return availableReactions;
const emoticon = (quickReaction as Reaction.reactionEmoji).emoticon || (quickReaction as AvailableReaction).reaction;
const availableReaction = findAndSplice(availableReactions, (availableReaction) => availableReaction.reaction === emoticon);
if(availableReaction) {
@ -155,7 +155,7 @@ export class AppReactionsManager extends AppManager {
this.getAvailableReactions()
], ([config, availableReactions]) => {
const reaction = config.reactions_default;
if(reaction._ === 'reactionEmoji') {
if(reaction?._ === 'reactionEmoji') {
return availableReactions.find((availableReaction) => availableReaction.reaction === reaction.emoticon);
}

View File

@ -0,0 +1,8 @@
import {Message, MessageMedia} from '../../../../layer';
export default function canSaveMessageMedia(message: Message.message | Message.messageService) {
return message &&
!message.pFlags.is_outgoing &&
!(message as Message.message).pFlags.noforwards &&
!((message as Message.message).media as MessageMedia.messageMediaInvoice)?.extended_media
}

View File

@ -1,19 +1,22 @@
import {Document, Message, MessageAction, MessageMedia, Photo, WebPage} from '../../../../layer';
import {Document, Message, MessageAction, MessageExtendedMedia, MessageMedia, Photo, WebPage} from '../../../../layer';
export default function getMediaFromMessage(message: Message) {
if(!message) return;
const media = (message as Message.messageService).action ?
((message as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo :
(message as Message.message).media && (
((message as Message.message).media as MessageMedia.messageMediaPhoto).photo ||
((message as Message.message).media as MessageMedia.messageMediaDocument).document || (
((message as Message.message).media as MessageMedia.messageMediaWebPage).webpage && (
(((message as Message.message).media as MessageMedia.messageMediaWebPage).webpage as WebPage.webPage).document ||
(((message as Message.message).media as MessageMedia.messageMediaWebPage).webpage as WebPage.webPage).photo
)
)
);
let media: any;
if((message as Message.messageService).action) {
media = ((message as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo;
} else if((message as Message.message).media) {
let messageMedia = (message as Message.message).media;
if((messageMedia as MessageMedia.messageMediaWebPage).webpage) {
messageMedia = (messageMedia as MessageMedia.messageMediaWebPage).webpage as any as MessageMedia;
} else if((messageMedia as MessageMedia.messageMediaInvoice).extended_media?._ === 'messageExtendedMedia') {
messageMedia = ((messageMedia as MessageMedia.messageMediaInvoice).extended_media as MessageExtendedMedia.messageExtendedMedia).media;
}
media = (messageMedia as MessageMedia.messageMediaPhoto).photo ||
(messageMedia as MessageMedia.messageMediaDocument).document;
}
return media as Photo.photo | Document.document;
}

View File

@ -136,7 +136,7 @@ export class ReferenceDatabase extends AppManager {
let promise: Promise<any>;
switch(context?.type) {
case 'message': {
promise = this.appMessagesManager.wrapSingleMessage(context.peerId, context.messageId, true);
promise = this.appMessagesManager.reloadMessages(context.peerId, context.messageId, true);
break;
// .then(() => {
// console.log('FILE_REFERENCE_EXPIRED: got message', context, appMessagesManager.getMessage((context as ReferenceContext.referenceContextMessage).messageId).media, reference);

View File

@ -211,7 +211,7 @@ export class LottieLoader {
// ! will need refactoring later, this is not the best way to remove the animation
const animations = animationIntersector.getAnimations(player.el[0]);
animations.forEach((animation) => {
animationIntersector.checkAnimation(animation, true, true);
animationIntersector.removeAnimation(animation);
});
};

View File

@ -22,6 +22,12 @@
"params": [
{"name": "size", "type": "number"}
]
}, {
"predicate": "photoStrippedSize",
"params": [
{"name": "w", "type": "number"},
{"name": "h", "type": "number"}
]
}, {
"predicate": "dialog",
"params": [

View File

@ -1902,6 +1902,30 @@ $bubble-beside-button-width: 38px;
}
}
.extended-media-buy {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 2.5rem;
padding: 0 1rem;
background-color: rgba(0, 0, 0, .3);
backdrop-filter: var(--menu-backdrop-filter);
font-weight: var(--font-weight-bold);
color: #fff;
z-index: 2;
font-size: var(--font-size-14);
border-radius: 2rem;
white-space: nowrap;
display: flex;
align-items: center;
&:before {
margin-right: .25rem;
font-size: 1.125rem;
}
}
pre {
display: inline;
margin: 0;