diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index 86cc6c142..2675f98b7 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -1168,7 +1168,9 @@ export default class AppMediaViewerBase< } } - renderImageFromUrl(el, url); + if((el as HTMLImageElement).src !== url) { + renderImageFromUrl(el, url); + } // ! костыль, но он тут даже и не нужен if(el.classList.contains('thumbnail') && el.parentElement.classList.contains('media-container-aspecter')) { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index c4bc52f94..37b978f7f 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -2328,7 +2328,7 @@ export default class ChatBubbles { // * if it's a start, then scroll to start of the group if(bubble && position !== 'end') { const item = this.bubbleGroups.getItemByBubble(bubble); - if(item.group.firstItem === item && whichChild(item.group.container) === (this.stickyIntersector ? STICKY_OFFSET : 1)) { + if(item && item.group.firstItem === item && whichChild(item.group.container) === (this.stickyIntersector ? STICKY_OFFSET : 1)) { const dateGroup = item.group.container.parentElement; // if(whichChild(dateGroup) === 0) { fallbackToElementStartWhenCentering = dateGroup; @@ -3600,7 +3600,8 @@ export default class ChatBubbles { loadPromises, lazyLoadQueue: this.lazyLoadQueue, customEmojiSize, - middleware + middleware, + animationGroup: CHAT_ANIMATION_GROUP }); let canHaveTail = true; @@ -4521,6 +4522,8 @@ export default class ChatBubbles { if(isFooter) { canHaveTail = true; + } else { + bubble.classList.add('with-beside-replies'); } } diff --git a/src/components/chat/reaction.ts b/src/components/chat/reaction.ts index e47906a4b..2b3ebe177 100644 --- a/src/components/chat/reaction.ts +++ b/src/components/chat/reaction.ts @@ -16,6 +16,7 @@ import SetTransition from '../singleTransition'; import StackedAvatars from '../stackedAvatars'; import {wrapSticker, wrapStickerAnimation} from '../wrappers'; import {Awaited} from '../../types'; +import noop from '../../helpers/noop'; const CLASS_NAME = 'reaction'; const TAG_NAME = CLASS_NAME + '-element'; @@ -186,26 +187,33 @@ export default class ReactionElement extends HTMLElement { skipRatio: 1, play: false, managers: this.managers - }).stickerPromise + }).stickerPromise.catch(noop) ]).then(([iconPlayer, aroundPlayer]) => { const remove = () => { // if(!isInDOM(div)) return; - fastRaf(() => { - // if(!isInDOM(div)) return; - iconPlayer.remove(); - div.remove(); - this.stickerContainer.classList.remove('has-animation'); - }); + iconPlayer.remove(); + div.remove(); + this.stickerContainer.classList.remove('has-animation'); + }; + + if(!aroundPlayer) { + remove(); + return; + } + + const removeOnFrame = () => { + // if(!isInDOM(div)) return; + fastRaf(remove); }; iconPlayer.addEventListener('enterFrame', (frameNo) => { if(frameNo === iconPlayer.maxFrame) { if(this.wrapStickerPromise) { // wait for fade in animation this.wrapStickerPromise.then(() => { - setTimeout(remove, 1e3); + setTimeout(removeOnFrame, 1e3); }); } else { - remove(); + removeOnFrame(); } } }); diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 5ea9c0b5f..17dd370ab 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -14,7 +14,6 @@ import ButtonIcon from '../buttonIcon'; import CheckboxField from '../checkboxField'; import PopupDeleteMessages from '../popups/deleteMessages'; import PopupForward from '../popups/forward'; -import {toast} from '../toast'; import SetTransition from '../singleTransition'; import ListenerSetter from '../../helpers/listenerSetter'; import PopupSendNow from '../popups/sendNow'; @@ -26,7 +25,6 @@ import blurActiveElement from '../../helpers/dom/blurActiveElement'; import cancelEvent from '../../helpers/dom/cancelEvent'; import cancelSelection from '../../helpers/dom/cancelSelection'; import getSelectedText from '../../helpers/dom/getSelectedText'; -import rootScope from '../../lib/rootScope'; import replaceContent from '../../helpers/dom/replaceContent'; import AppSearchSuper from '../appSearchSuper.'; import isInDOM from '../../helpers/dom/isInDOM'; @@ -58,7 +56,7 @@ class AppSelection extends EventListenerBase<{ protected isScheduled: boolean; protected listenElement: HTMLElement; - protected onToggleSelection: (forwards: boolean, animate: boolean) => void; + protected onToggleSelection: (forwards: boolean, animate: boolean) => void | Promise; protected onUpdateContainer: (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => void; protected onCancelSelection: () => void; protected toggleByMid: (peerId: PeerId, mid: number) => void; @@ -361,7 +359,7 @@ class AppSelection extends EventListenerBase<{ if(cantForward && cantDelete) break; } - this.onUpdateContainer && this.onUpdateContainer(cantForward, cantDelete, cantSend); + this.onUpdateContainer?.(cantForward, cantDelete, cantSend); } public toggleSelection(toggleCheckboxes = true, forceSelection = false) { @@ -404,7 +402,7 @@ class AppSelection extends EventListenerBase<{ blurActiveElement(); const forwards = !!size || forceSelection; - this.onToggleSelection && this.onToggleSelection(forwards, !this.doNotAnimate); + const toggleResult = this.onToggleSelection?.(forwards, !this.doNotAnimate); if(!IS_MOBILE_SAFARI) { if(forwards) { @@ -420,7 +418,7 @@ class AppSelection extends EventListenerBase<{ } if(forceSelection) { - this.updateContainer(forceSelection); + (toggleResult || Promise.resolve()).then(() => this.updateContainer(forceSelection)); } return true; @@ -972,7 +970,7 @@ export default class ChatSelection extends AppSelection { replaceContent(this.selectionCountEl, i18n('messages', [this.length()])); this.selectionSendNowBtn && this.selectionSendNowBtn.toggleAttribute('disabled', cantSend); this.selectionForwardBtn && this.selectionForwardBtn.toggleAttribute('disabled', cantForward); - this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); + this.selectionDeleteBtn && this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); }; protected onCancelSelection = async() => { diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index afc719e33..2a56ad0a6 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -708,7 +708,7 @@ export default class ChatTopbar { return () => { this.btnMute && this.btnMute.classList.toggle('hide', !isBroadcast); if(this.btnJoin) { - if(isAnyChat) { + if(isAnyChat && !this.chat.isRestricted) { replaceContent(this.btnJoin, i18n(isBroadcast ? 'Chat.Subscribe' : 'ChannelJoin')); this.btnJoin.classList.toggle('hide', !chat?.pFlags?.left); } else { @@ -783,10 +783,15 @@ export default class ChatTopbar { } else if(this.chat.type === 'discussion') { if(count === undefined) { const result = await this.managers.acknowledged.appMessagesManager.getHistory(peerId, 0, 1, 0, this.chat.threadId); + if(!middleware()) return; if(result.cached) { const historyResult = await result.result; + if(!middleware()) return; count = historyResult.count; - } else result.result.then((historyResult) => this.setTitle(historyResult.count)); + } else result.result.then((historyResult) => { + if(!middleware()) return; + this.setTitle(historyResult.count); + }); } if(count === undefined) titleEl = i18n('Loading'); diff --git a/src/components/sidebarLeft/tabs/archivedTab.ts b/src/components/sidebarLeft/tabs/archivedTab.ts index 0546307e2..f0b84476f 100644 --- a/src/components/sidebarLeft/tabs/archivedTab.ts +++ b/src/components/sidebarLeft/tabs/archivedTab.ts @@ -32,8 +32,8 @@ export default class AppArchivedTab extends SliderSuperTab { const scrollable = appDialogsManager.scrollables[AppArchivedTab.filterId]; this.scrollable.container.replaceWith(scrollable.container); - this.scrollable = scrollable; - + // ! DO NOT UNCOMMENT NEXT LINE - chats will stop loading on scroll after closing the tab + // this.scrollable = scrollable; return appDialogsManager.setFilterIdAndChangeTab(AppArchivedTab.filterId).then(({cached, renderPromise}) => { if(cached) { return renderPromise; diff --git a/src/components/stickerViewer.ts b/src/components/stickerViewer.ts index 391d01d48..a7d428734 100644 --- a/src/components/stickerViewer.ts +++ b/src/components/stickerViewer.ts @@ -180,6 +180,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter, player.addEventListener('enterFrame', c); }); + if(!middleware()) return; player.pause(); } else if(player instanceof HTMLVideoElement) { player.currentTime = (mediaContainer.querySelector('video') as HTMLVideoElement).currentTime; @@ -283,7 +284,6 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter, const onMousePreMove = (e: MouseEvent) => { if(!findUpAsChild(e.target as HTMLElement, mediaContainer)) { - document.removeEventListener('mousemove', onMousePreMove); onMouseUp(); } }; @@ -303,6 +303,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter, attachClickEvent(document.body, cancelEvent, {capture: true, once: true}); } + document.removeEventListener('mousemove', onMousePreMove); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp, {capture: true}); }; diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index e96df797d..f44e746f7 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -700,8 +700,8 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m data.a.length = 0; }, 1000, false); - const animation = lottieLoader.getAnimation(container); - if(animation.paused) { + const animation = !container.classList.contains('custom-emoji') ? lottieLoader.getAnimation(container) : undefined; + if(animation?.paused) { const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji); if(doc) { const audio = document.createElement('audio'); diff --git a/src/components/wrappers/stickerAnimation.ts b/src/components/wrappers/stickerAnimation.ts index 9f6c3c275..a17b31a40 100644 --- a/src/components/wrappers/stickerAnimation.ts +++ b/src/components/wrappers/stickerAnimation.ts @@ -7,7 +7,8 @@ import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport'; import assumeType from '../../helpers/assumeType'; import isInDOM from '../../helpers/dom/isInDOM'; -import {Middleware} from '../../helpers/middleware'; +import makeError from '../../helpers/makeError'; +import {getMiddleware, Middleware} from '../../helpers/middleware'; import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf'; import windowSize from '../../helpers/windowSize'; import {PhotoSize, VideoSize} from '../../layer'; @@ -53,11 +54,15 @@ export default function wrapStickerAnimation({ let animation: RLottiePlayer; const unmountAnimation = () => { + middlewareHelper.clean(); animation?.remove(); animationDiv.remove(); appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll); }; + const middlewareHelper = middleware?.create() ?? getMiddleware(); + middleware = middlewareHelper.get(); + const stickerPromise = wrapSticker({ div: animationDiv, doc, @@ -74,6 +79,11 @@ export default function wrapStickerAnimation({ fullThumb }).then(({render}) => render).then((_animation) => { assumeType(_animation); + if(!middleware()) { + _animation.remove(); + throw makeError('MIDDLEWARE'); + } + animation = _animation; animation.addEventListener('enterFrame', (frameNo) => { if((!loopEffect && frameNo === animation.maxFrame) || !isInDOM(target)) { diff --git a/src/helpers/eventListenerBase.ts b/src/helpers/eventListenerBase.ts index 49dfcf606..79c63f73b 100644 --- a/src/helpers/eventListenerBase.ts +++ b/src/helpers/eventListenerBase.ts @@ -84,7 +84,7 @@ export default class EventListenerBase } public addEventListener(name: T, callback: Listeners[T], options?: boolean | AddEventListenerOptions) { - (this.listeners[name] ?? (this.listeners[name] = [])).push({callback, options}); // ! add before because if you don't, you won't be able to delete it from callback + (this.listeners[name] ??= []).push({callback, options}); // ! add before because if you don't, you won't be able to delete it from callback if(this.listenerResults.hasOwnProperty(name)) { callback(...this.listenerResults[name]); @@ -108,7 +108,7 @@ export default class EventListenerBase public removeEventListener(name: T, callback: Listeners[T], options?: boolean | AddEventListenerOptions) { if(this.listeners[name]) { - findAndSplice(this.listeners[name], l => l.callback === callback); + findAndSplice(this.listeners[name], (l) => l.callback === callback); } // e.remove(this, name, callback); } diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 7d69f8679..2f5cb6af5 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1003,6 +1003,7 @@ export class AppDialogsManager { let loadCount = windowSize.height / 72 * 1.25 | 0; let offsetIndex = 0; + const doNotRenderChatList = this.doNotRenderChatList; // cache before awaits const {index: currentOffsetIndex} = this.getOffsetIndex(side); if(currentOffsetIndex) { if(side === 'top') { @@ -1045,7 +1046,7 @@ export class AppDialogsManager { const a = await getConversationsResult; const result = await a.result; - if(this.loadDialogsRenderPromise !== renderPromise || this.doNotRenderChatList) { + if(this.loadDialogsRenderPromise !== renderPromise || doNotRenderChatList) { reject(); cachedInfoPromise.reject(); return; diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index fa740885e..da270ccc0 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -437,7 +437,6 @@ export default function wrapRichText(text: string, options: Partial<{ }, voodoo?: boolean, customEmojis?: {[docId: DocId]: CustomEmojiElement[]}, - wrappingSpoiler?: boolean, loadPromises?: Promise[], middleware?: Middleware, @@ -478,7 +477,8 @@ export default function wrapRichText(text: string, options: Partial<{ } } else if((entity.offset + entity.length) > textLength) { entity = copy(entity); - entity.length = entity.offset + entity.length - textLength; + // entity.length = entity.offset + entity.length - textLength; + entity.length = textLength - entity.offset; } if(entity.length) { @@ -621,7 +621,7 @@ export default function wrapRichText(text: string, options: Partial<{ break; } - if(nextEntity?._ === 'messageEntityEmoji') { + while(nextEntity?._ === 'messageEntityEmoji' && nextEntity.offset < endOffset) { ++nasty.i; nasty.lastEntity = nextEntity; nasty.usedLength += nextEntity.length; @@ -808,7 +808,9 @@ export default function wrapRichText(text: string, options: Partial<{ const encoded = encodeSpoiler(nasty.text, entity); nasty.text = encoded.text; partText = encoded.entityText; - nasty.usedLength += partText.length; + if(endPartOffset !== endOffset) { + nasty.usedLength += endOffset - endPartOffset; + } let n: MessageEntity; for(; n = entities[nasty.i + 1], n && n.offset < endOffset;) { // nasty.usedLength += n.length; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 041ee328e..a2d44c2b9 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1753,7 +1753,7 @@ $bubble-beside-button-width: 38px; } } - &.with-replies:not(.sticker) .message { + &.with-replies:not(.sticker):not(.with-beside-replies) .message { bottom: 55px; } @@ -1786,6 +1786,10 @@ $bubble-beside-button-width: 38px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } + + &.with-beside-replies .bubble-content { + min-height: 5.5rem; + } .time { visibility: hidden; // * can't use color transparent here, because in name can be emoji