diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index 43e6e482..063f4f92 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -11,6 +11,9 @@ import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromi import { isSafari } from "../helpers/userAgent"; import { MOUNT_CLASS_TO } from "../config/debug"; import appDownloadManager from "../lib/appManagers/appDownloadManager"; +import simulateEvent from "../helpers/dom/dispatchEvent"; +import type { SearchSuperContext } from "./appSearchSuper."; +import { copy, deepEqual } from "../helpers/object"; // TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда @@ -18,9 +21,17 @@ import appDownloadManager from "../lib/appManagers/appDownloadManager"; // TODO: Safari: попробовать замаскировать подгрузку последнего чанка // TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце +type MediaItem = {mid: number, peerId: number}; + type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement; -type MediaType = 'voice' | 'audio' | 'round'; +const SHOULD_USE_SAFARI_FIX = (() => { + try { + return isSafari && +navigator.userAgent.match(/ Version\/(\d+)/)[1] < 14; + } catch(err) { + return false; + } +})(); class AppMediaPlaybackController { private container: HTMLElement; @@ -29,7 +40,7 @@ class AppMediaPlaybackController { [mid: string]: HTMLMediaElement } } = {}; - private playingMedia: HTMLMediaElement; + public playingMedia: HTMLMediaElement; private waitingMediaForLoad: { [peerId: string]: { @@ -38,11 +49,15 @@ class AppMediaPlaybackController { } = {}; public willBePlayedMedia: HTMLMediaElement; + public searchContext: SearchSuperContext; private currentPeerId: number; private prevMid: number; private nextMid: number; + private prev: MediaItem[] = []; + private next: MediaItem[] = []; + constructor() { this.container = document.createElement('div'); //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; @@ -63,6 +78,7 @@ class AppMediaPlaybackController { //media.muted = true; } + media.dataset.peerId = '' + peerId; media.dataset.mid = '' + mid; media.dataset.type = doc.type; @@ -72,30 +88,11 @@ class AppMediaPlaybackController { this.container.append(media); - media.addEventListener('playing', () => { - this.currentPeerId = peerId; - - //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); - - if(this.playingMedia !== media) { - if(this.playingMedia && !this.playingMedia.paused) { - this.playingMedia.pause(); - } - - this.playingMedia = media; - this.loadSiblingsMedia(peerId, doc.type as MediaType, mid); - } - - // audio_pause не успеет сработать без таймаута - setTimeout(() => { - rootScope.dispatchEvent('audio_play', {peerId, doc, mid}); - }, 0); - }); - + media.addEventListener('play', this.onPlay); media.addEventListener('pause', this.onPause); media.addEventListener('ended', this.onEnded); - const onError = (e: Event) => { + /* const onError = (e: Event) => { //console.log('appMediaPlaybackController: video onError', e); if(this.nextMid === mid) { @@ -107,7 +104,7 @@ class AppMediaPlaybackController { } }; - media.addEventListener('error', onError); + media.addEventListener('error', onError); */ const deferred = deferredPromise(); if(autoload) { @@ -122,14 +119,16 @@ class AppMediaPlaybackController { //console.log('will set media url:', media, doc, doc.type, doc.url); ((!doc.supportsStreaming ? appDocsManager.downloadDoc(doc) : Promise.resolve()) as Promise).then(() => { - if(doc.type === 'audio' && doc.supportsStreaming && isSafari) { + if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) { this.handleSafariStreamable(media); } + // setTimeout(() => { const cacheContext = appDownloadManager.getCacheContext(doc); media.src = cacheContext.url; + // }, doc.supportsStreaming ? 500e3 : 0); }); - }, onError); + }/* , onError */); return storage[mid] = media; } @@ -163,7 +162,11 @@ class AppMediaPlaybackController { } public resolveWaitingForLoadMedia(peerId: number, mid: number) { - const storage = this.waitingMediaForLoad[peerId] ?? (this.waitingMediaForLoad[peerId] = {}); + const storage = this.waitingMediaForLoad[peerId]; + if(!storage) { + return; + } + const promise = storage[mid]; if(promise) { promise.resolve(); @@ -184,6 +187,37 @@ class AppMediaPlaybackController { media.safariBuffering = value; } + onPlay = (e?: Event) => { + const media = e.target as HTMLMediaElement; + const peerId = +media.dataset.peerId; + const mid = +media.dataset.mid; + this.currentPeerId = peerId; + + //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); + + const previousMedia = this.playingMedia; + if(previousMedia !== media) { + if(previousMedia) { + if(!previousMedia.paused) { + previousMedia.pause(); + } + + // reset media + previousMedia.currentTime = 0; + simulateEvent(previousMedia, 'ended'); + } + + this.playingMedia = media; + this.loadSiblingsMedia(peerId, mid); + } + + // audio_pause не успеет сработать без таймаута + setTimeout(() => { + const message = appMessagesManager.getMessageByPeer(peerId, mid); + rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid}); + }, 0); + }; + onPause = (e?: Event) => { /* const target = e.target as HTMLMediaElement; if(!isInDOM(target)) { @@ -196,6 +230,10 @@ class AppMediaPlaybackController { }; onEnded = (e?: Event) => { + if(!e.isTrusted) { + return; + } + this.onPause(e); //console.log('on media end'); @@ -215,35 +253,28 @@ class AppMediaPlaybackController { } }; - private loadSiblingsMedia(peerId: number, type: MediaType, mid: number) { - const media = this.playingMedia; - this.prevMid = this.nextMid = 0; + private loadSiblingsMedia(offsetPeerId: number, offsetMid: number) { + const {playingMedia, searchContext} = this; + if(!searchContext) { + return; + } return appMessagesManager.getSearch({ - peerId, - query: '', - inputFilter: { - //_: type === 'audio' ? 'inputMessagesFilterMusic' : (type === 'round' ? 'inputMessagesFilterRoundVideo' : 'inputMessagesFilterVoice') - _: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice' - }, - maxId: mid, + ...searchContext, + maxId: offsetMid, limit: 3, - backLimit: 2 + backLimit: 2, }).then(value => { - if(this.playingMedia !== media) { + if(this.playingMedia !== playingMedia || this.searchContext !== searchContext) { return; } - for(const {mid: m} of value.history) { - if(m > mid) { - this.nextMid = m; - } else if(m < mid) { - this.prevMid = m; - break; - } - } + const idx = Math.max(0, value.history.findIndex(message => message.peerId === offsetPeerId && message.mid === offsetMid)); + const prev = value.history.slice(Math.max(0, idx)); + const next = value.history.slice(0, idx); [this.prevMid, this.nextMid].filter(Boolean).forEach(mid => { + const peerId = searchContext.peerId; const message = appMessagesManager.getMessageByPeer(peerId, mid); this.addMedia(peerId, message.media.document, mid, false); }); @@ -270,6 +301,16 @@ class AppMediaPlaybackController { public willBePlayed(media: HTMLMediaElement) { this.willBePlayedMedia = media; } + + public setSearchContext(context: SearchSuperContext) { + if(deepEqual(this.searchContext, context)) { + return; + } + + this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'} + this.prev.length = 0; + this.next.length = 0; + } } const appMediaPlaybackController = new AppMediaPlaybackController(); diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 8a742df3..b8761064 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -53,6 +53,7 @@ import { attachClickEvent } from "../helpers/dom/clickEvent"; import PopupDeleteMessages from "./popups/deleteMessages"; import RangeSelector from "./rangeSelector"; import windowSize from "../helpers/windowSize"; +import { safeAssign } from "../helpers/object"; const ZOOM_STEP = 0.5; const ZOOM_INITIAL_VALUE = 1; @@ -65,7 +66,148 @@ const ZOOM_MAX_VALUE = 4; const MEDIA_VIEWER_CLASSNAME = 'media-viewer'; -class AppMediaViewerBase { +type MediaQueueLoaderOptions = { + prevTargets?: MediaQueueLoader['prevTargets'], + nextTargets?: MediaQueueLoader['nextTargets'], + onLoadedMore?: MediaQueueLoader['onLoadedMore'], + generateItem?: MediaQueueLoader['generateItem'], + getLoadPromise?: MediaQueueLoader['getLoadPromise'], + reverse?: MediaQueueLoader['reverse'] +}; + +class MediaQueueLoader { + public prevTargets: Item[] = []; + public nextTargets: Item[] = []; + + public loadMediaPromiseUp: Promise = null; + public loadMediaPromiseDown: Promise = null; + public loadedAllMediaUp = false; + public loadedAllMediaDown = false; + + public reverse = false; // reverse means next = higher msgid + + protected generateItem: (item: Item) => Item = (item) => item; + protected getLoadPromise: (older: boolean, anchor: Item, loadCount: number) => Promise; + protected onLoadedMore: () => void; + + constructor(options: MediaQueueLoaderOptions = {}) { + safeAssign(this, options); + } + + public setTargets(prevTargets: Item[], nextTargets: Item[], reverse: boolean) { + this.prevTargets = prevTargets; + this.nextTargets = nextTargets; + this.reverse = reverse; + this.loadedAllMediaUp = this.loadedAllMediaDown = false; + this.loadMediaPromiseUp = this.loadMediaPromiseDown = null; + } + + public reset() { + this.prevTargets = []; + this.nextTargets = []; + this.loadedAllMediaUp = this.loadedAllMediaDown = false; + this.loadMediaPromiseUp = this.loadMediaPromiseDown = null; + } + + // нет смысла делать проверку для reverse и loadMediaPromise + public loadMoreMedia = (older = true) => { + //if(!older && this.reverse) return; + + if(older && this.loadedAllMediaDown) return Promise.resolve(); + else if(!older && this.loadedAllMediaUp) return Promise.resolve(); + + if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown; + else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp; + + const loadCount = 50; + + let anchor: Item; + if(older) { + anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1]; + } else { + anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0]; + } + + const promise = this.getLoadPromise(older, anchor, loadCount).then(items => { + if(items.length < loadCount) { + /* if(this.reverse) { + if(older) this.loadedAllMediaUp = true; + else this.loadedAllMediaDown = true; + } else { */ + if(older) this.loadedAllMediaDown = true; + else this.loadedAllMediaUp = true; + //} + } + + const method: any = older ? items.forEach.bind(items) : forEachReverse.bind(null, items); + method((item: Item) => { + const t = this.generateItem(item); + if(!t) { + return; + } + + if(older) { + if(this.reverse) this.prevTargets.unshift(t); + else this.nextTargets.push(t); + } else { + if(this.reverse) this.nextTargets.push(t); + else this.prevTargets.unshift(t); + } + }); + + this.onLoadedMore && this.onLoadedMore(); + }, () => {}).then(() => { + if(older) this.loadMediaPromiseDown = null; + else this.loadMediaPromiseUp = null; + }); + + if(older) this.loadMediaPromiseDown = promise; + else this.loadMediaPromiseUp = promise; + + return promise; + }; +} + +class MediaSearchQueueLoader extends MediaQueueLoader { + public currentMessageId = 0; + public searchContext: SearchSuperContext; + + constructor(options: Omit, 'getLoadPromise'> = {}) { + super({ + ...options, + getLoadPromise: (older, anchor, loadCount) => { + const backLimit = older ? 0 : loadCount; + let maxId = this.currentMessageId; + + if(anchor) maxId = anchor.mid; + if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); + + return appMessagesManager.getSearch({ + ...this.searchContext, + maxId, + limit: backLimit ? 0 : loadCount, + backLimit + }).then(value => { + /* if(DEBUG) { + this.log('loaded more media by maxId:', maxId, value, older, this.reverse); + } */ + + if(value.next_rate) { + this.searchContext.nextRate = value.next_rate; + } + + return value.history as any; + }); + } + }); + } + + public setSearchContext(context: SearchSuperContext) { + this.searchContext = context; + } +} + +class AppMediaViewerBase { protected wholeDiv: HTMLElement; protected overlaysDiv: HTMLElement; protected author: {[k in 'container' | 'avatarEl' | 'nameEl' | 'date']: HTMLElement} = {} as any; @@ -78,7 +220,7 @@ class AppMediaViewerBase onAnimationEnd); + const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd); - this.lastTarget = null; + this.target = null; this.prevTargets = []; this.nextTargets = []; this.loadedAllMediaUp = this.loadedAllMediaDown = false; @@ -1209,7 +1353,7 @@ class AppMediaViewerBase; + constructor() { super(['delete', 'forward']); @@ -1570,6 +1716,24 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet stub.classList.add(MEDIA_VIEWER_CLASSNAME + '-stub'); this.content.main.prepend(stub); */ + this.queueLoader = new MediaSearchQueueLoader({ + prevTargets: this.prevTargets, + nextTargets: this.nextTargets, + generateItem: (item) => { + const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument'; + const {mid, peerId} = item; + const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item); + + if(!media) return; + + if(isForDocument && !AppMediaViewer.isMediaCompatibleForDocumentViewer(media)) { + return; + } + + return {element: null as HTMLElement, mid, peerId}; + } + }); + this.content.caption = document.createElement('div'); this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption'/* , 'media-viewer-stub' */); @@ -1665,18 +1829,16 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet } */ onPrevClick = (target: AppMediaViewerTargetType) => { - this.nextTargets.unshift({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId}); this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, -1); }; onNextClick = (target: AppMediaViewerTargetType) => { - this.prevTargets.push({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId}); this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, 1); }; onDeleteClick = () => { new PopupDeleteMessages(this.currentPeerId, [this.currentMessageId], 'chat', () => { - this.lastTarget = this.content.media; + this.target = {element: this.content.media} as any; this.close(); }); }; @@ -1732,94 +1894,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet // нет смысла делать проверку для reverse и loadMediaPromise protected loadMoreMedia = (older = true) => { - //if(!older && this.reverse) return; - - if(older && this.loadedAllMediaDown) return Promise.resolve(); - else if(!older && this.loadedAllMediaUp) return Promise.resolve(); - - if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown; - else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp; - - const loadCount = 50; - const backLimit = older ? 0 : loadCount; - let maxId = this.currentMessageId; - - let anchor: AppMediaViewerTargetType; - if(older) { - anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1]; - } else { - anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0]; - } - - if(anchor) maxId = anchor.mid; - if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); - - const promise = appMessagesManager.getSearch({ - peerId: this.searchContext.peerId, - query: this.searchContext.query, - inputFilter: { - _: this.searchContext.inputFilter - }, - maxId, - limit: backLimit ? 0 : loadCount, - backLimit, - threadId: this.searchContext.threadId, - folderId: this.searchContext.folderId, - nextRate: this.searchContext.nextRate, - minDate: this.searchContext.minDate, - maxDate: this.searchContext.maxDate - }).then(value => { - if(DEBUG) { - this.log('loaded more media by maxId:', maxId, value, older, this.reverse); - } - - if(value.next_rate) { - this.searchContext.nextRate = value.next_rate; - } - - if(value.history.length < loadCount) { - /* if(this.reverse) { - if(older) this.loadedAllMediaUp = true; - else this.loadedAllMediaDown = true; - } else { */ - if(older) this.loadedAllMediaDown = true; - else this.loadedAllMediaUp = true; - //} - } - - const method: any = older ? value.history.forEach.bind(value.history) : forEachReverse.bind(null, value.history); - const isForDocument = this.searchContext.inputFilter === 'inputMessagesFilterDocument'; - method((message: Message.message) => { - const {mid, peerId} = message; - const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(message); - - if(!media) return; - - if(isForDocument && !AppMediaViewer.isMediaCompatibleForDocumentViewer(media)) { - return; - } - - const t = {element: null as HTMLElement, mid, peerId}; - if(older) { - if(this.reverse) this.prevTargets.unshift(t); - else this.nextTargets.push(t); - } else { - if(this.reverse) this.nextTargets.push(t); - else this.prevTargets.unshift(t); - } - }); - - this.buttons.prev.classList.toggle('hide', !this.prevTargets.length); - this.buttons.next.classList.toggle('hide', !this.nextTargets.length); - }, () => {}).then(() => { - if(older) this.loadMediaPromiseDown = null; - else this.loadMediaPromiseUp = null; - }); - - if(older) this.loadMediaPromiseDown = promise; - else this.loadMediaPromiseUp = promise; - - return promise; + return this.queueLoader.loadMoreMedia(older); }; private setCaption(message: Message.message) { @@ -1870,6 +1945,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet this.currentPeerId = message.peerId; this.setCaption(message); const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, needLoadMore); + this.target.mid = mid; + this.target.peerId = message.peerId; return promise; } @@ -1905,12 +1982,10 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe } onPrevClick = (target: AppMediaViewerAvatarTargetType) => { - this.nextTargets.unshift({element: this.lastTarget, photoId: this.currentPhotoId}); this.openMedia(target.photoId, target.element, -1); }; onNextClick = (target: AppMediaViewerAvatarTargetType) => { - this.prevTargets.push({element: this.lastTarget, photoId: this.currentPhotoId}); this.openMedia(target.photoId, target.element, 1); }; @@ -1963,6 +2038,9 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe this.currentPhotoId = photo.id; - return super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets); + const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets); + this.target.photoId = photo.id; + + return ret; } } diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 81f42ee2..49fd15d8 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -56,7 +56,7 @@ import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent" export type SearchSuperType = MyInputMessagesFilter/* | 'members' */; export type SearchSuperContext = { peerId: number, - inputFilter: MyInputMessagesFilter, + inputFilter: {_: MyInputMessagesFilter}, query?: string, maxId?: number, folderId?: number, @@ -453,6 +453,7 @@ export default class AppSearchSuper { const onMediaClick = (className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => { const target = findUpClassName(e.target as HTMLDivElement, className); + if(!target) return; const mid = +target.dataset.mid; if(!mid) { @@ -1240,16 +1241,11 @@ export default class AppSearchSuper { //let loadCount = history.length ? 50 : 15; return this.loadPromises[type] = appMessagesManager.getSearch({ - peerId: this.searchContext.peerId, - query: this.searchContext.query, + ...this.searchContext, inputFilter: {_: type}, maxId, limit: loadCount, - nextRate: this.nextRates[type] ?? (this.nextRates[type] = 0), - threadId: this.searchContext.threadId, - folderId: this.searchContext.folderId, - minDate: this.searchContext.minDate, - maxDate: this.searchContext.maxDate + nextRate: this.nextRates[type] ?? (this.nextRates[type] = 0) }).then(value => { history.push(...value.history.map(m => ({mid: m.mid, peerId: m.peerId}))); @@ -1541,7 +1537,7 @@ export default class AppSearchSuper { private copySearchContext(newInputFilter: MyInputMessagesFilter) { const context = copy(this.searchContext); - context.inputFilter = newInputFilter; + context.inputFilter = {_: newInputFilter}; context.nextRate = this.nextRates[newInputFilter]; return context; } @@ -1558,7 +1554,7 @@ export default class AppSearchSuper { this.searchContext = { peerId: peerId || 0, query: query || '', - inputFilter: this.mediaTab.inputFilter, + inputFilter: {_: this.mediaTab.inputFilter}, threadId, folderId, minDate, diff --git a/src/components/audio.ts b/src/components/audio.ts index d2dd5ed5..e7f0e125 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -6,7 +6,7 @@ import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import { RichTextProcessor } from "../lib/richtextprocessor"; -import { formatDate } from "./wrappers"; +import { formatDate, wrapPhoto } from "./wrappers"; import ProgressivePreloader from "./preloader"; import { MediaProgressLine } from "../lib/mediaPlayer"; import appMediaPlaybackController from "./appMediaPlaybackController"; @@ -20,13 +20,14 @@ import { SearchSuperContext } from "./appSearchSuper."; import { formatDateAccordingToToday } from "../helpers/date"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent"; +import LazyLoadQueue from "./lazyLoadQueue"; +import { deferredPromise } from "../helpers/cancellablePromise"; +import ListenerSetter, { Listener } from "../helpers/listenerSetter"; +import noop from "../helpers/noop"; -rootScope.addEventListener('messages_media_read', e => { - const {mids, peerId} = e; - +rootScope.addEventListener('messages_media_read', ({mids, peerId}) => { mids.forEach(mid => { - (Array.from(document.querySelectorAll('audio-element[message-id="' + mid + '"][peer-id="' + peerId + '"]')) as AudioElement[]).forEach(elem => { - //console.log('updating avatar:', elem); + (Array.from(document.querySelectorAll('audio-element[message-id="' + mid + '"][peer-id="' + peerId + '"].is-unread')) as AudioElement[]).forEach(elem => { elem.classList.remove('is-unread'); }); }); @@ -182,7 +183,7 @@ function wrapVoiceMessage(audioEl: AudioElement) { start(); } - audioEl.addAudioListener('playing', () => { + audioEl.addAudioListener('play', () => { if(isUnread && !isOut && audioEl.classList.contains('is-unread')) { audioEl.classList.remove('is-unread'); appMessagesManager.readMessages(audioEl.message.peerId, [audioEl.message.mid]); @@ -319,7 +320,7 @@ function wrapAudio(audioEl: AudioElement) { launched = false; }); - const onPlaying = () => { + const onPlay = () => { if(!launched) { audioEl.classList.add('audio-show-progress'); launched = true; @@ -330,10 +331,10 @@ function wrapAudio(audioEl: AudioElement) { } }; - audioEl.addAudioListener('playing', onPlaying); + audioEl.addAudioListener('play', onPlay); if(!audioEl.audio.paused || audioEl.audio.currentTime > 0) { - onPlaying(); + onPlay(); } return () => { @@ -346,6 +347,15 @@ function wrapAudio(audioEl: AudioElement) { return onLoad; } +function constructDownloadPreloader(tryAgainOnFail = true) { + const preloader = new ProgressivePreloader({cancelable: true, tryAgainOnFail}); + preloader.construct(); + preloader.circle.setAttributeNS(null, 'r', '23'); + preloader.totalLength = 143.58203125; + + return preloader; +} + export default class AudioElement extends HTMLElement { public audio: HTMLAudioElement; public preloader: ProgressivePreloader; @@ -355,20 +365,18 @@ export default class AudioElement extends HTMLElement { public searchContext: SearchSuperContext; public showSender = false; public noAutoDownload: boolean; + public lazyLoadQueue: LazyLoadQueue; + public loadPromises: Promise[]; - private attachedHandlers: {[name: string]: any[]} = {}; + private listenerSetter = new ListenerSetter(); private onTypeDisconnect: () => void; public onLoad: (autoload?: boolean) => void; - - constructor() { - super(); - // элемент создан - } + readyPromise: import("/Users/kuzmenko/Documents/projects/tweb/src/helpers/cancellablePromise").CancellablePromise; public render() { this.classList.add('audio'); - const doc = this.message.media.document || this.message.media.webpage.document; + const doc: MyDocument = this.message.media.document || this.message.media.webpage.document; const isRealVoice = doc.type === 'voice'; const isVoice = !this.voiceAsMusic && isRealVoice; const isOutgoing = this.message.pFlags.is_outgoing; @@ -376,15 +384,21 @@ export default class AudioElement extends HTMLElement { const durationStr = String(doc.duration | 0).toHHMMSS(); - this.innerHTML = `
-
-
-
`; + this.innerHTML = ` +
+
+
+
+
+
`; + + const toggle = this.firstElementChild as HTMLElement; const downloadDiv = document.createElement('div'); downloadDiv.classList.add('audio-download'); if(uploading) { + this.classList.add('is-outgoing'); this.append(downloadDiv); } @@ -400,31 +414,37 @@ export default class AudioElement extends HTMLElement { this.onTypeDisconnect = onTypeLoad(); - const toggle = this.querySelector('.audio-toggle') as HTMLDivElement; - const getTimeStr = () => String(audio.currentTime | 0).toHHMMSS() + (isVoice ? (' / ' + durationStr) : ''); - const onPlaying = () => { + const onPlay = () => { audioTimeDiv.innerText = getTimeStr(); toggle.classList.toggle('playing', !audio.paused); }; if(!audio.paused || (audio.currentTime > 0 && audio.currentTime !== audio.duration)) { - onPlaying(); + onPlay(); } - attachClickEvent(toggle, (e) => { - cancelEvent(e); - if(audio.paused) audio.play().catch(() => {}); - else audio.pause(); - }); + const togglePlay = (e?: Event, paused = audio.paused) => { + e && cancelEvent(e); + + if(paused) { + appMediaPlaybackController.setSearchContext(this.searchContext); + audio.play().catch(() => {}); + } else { + audio.pause(); + } + }; + + attachClickEvent(toggle, (e) => togglePlay(e), {listenerSetter: this.listenerSetter}); this.addAudioListener('ended', () => { toggle.classList.remove('playing'); + audioTimeDiv.innerText = durationStr; }); this.addAudioListener('timeupdate', () => { - if(appMediaPlaybackController.isSafariBuffering(audio)) return; + if(appMediaPlaybackController.playingMedia !== audio || appMediaPlaybackController.isSafariBuffering(audio)) return; audioTimeDiv.innerText = getTimeStr(); }); @@ -432,7 +452,9 @@ export default class AudioElement extends HTMLElement { toggle.classList.remove('playing'); }); - this.addAudioListener('playing', onPlaying); + this.addAudioListener('play', onPlay); + + return togglePlay; }; if(!isOutgoing) { @@ -442,9 +464,7 @@ export default class AudioElement extends HTMLElement { if(isRealVoice) { if(!preloader) { - preloader = new ProgressivePreloader({ - cancelable: true - }); + preloader = constructDownloadPreloader(); } const load = () => { @@ -468,7 +488,7 @@ export default class AudioElement extends HTMLElement { return {download}; }; - preloader.construct(); + // preloader.construct(); preloader.setManual(); preloader.attach(downloadDiv); preloader.setDownloadFunction(load); @@ -487,35 +507,89 @@ export default class AudioElement extends HTMLElement { onLoad(false); } + if(doc.thumbs) { + const imgs: HTMLImageElement[] = []; + const wrapped = wrapPhoto({ + photo: doc, + message: null, + container: toggle, + boxWidth: 48, + boxHeight: 48, + loadPromises: this.loadPromises, + withoutPreloader: true, + lazyLoadQueue: this.lazyLoadQueue + }); + toggle.style.width = toggle.style.height = ''; + if(wrapped.images.thumb) imgs.push(wrapped.images.thumb); + if(wrapped.images.full) imgs.push(wrapped.images.full); + + this.classList.add('audio-with-thumb'); + imgs.forEach(img => img.classList.add('audio-thumb')); + } + //if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано //onLoad(); //} else { const r = (e: Event) => { if(!this.audio) { - onLoad(false); + const togglePlay = onLoad(false); } if(this.audio.src) { return; } - //onLoad(); - //cancelEvent(e); + appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid); - appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio - + + if(isSafari) { + this.audio.autoplay = true; + } + + // togglePlay(undefined, true); + + this.readyPromise = deferredPromise(); + if(this.audio.readyState >= 2) this.readyPromise.resolve(); + else { + this.addAudioListener('canplay', () => this.readyPromise.resolve(), {once: true}); + } + if(!preloader) { if(doc.supportsStreaming) { - preloader = new ProgressivePreloader({ - cancelable: false - }); + this.classList.add('corner-download'); + + let pauseListener: Listener; + const onPlay = () => { + const preloader = constructDownloadPreloader(false); + const deferred = deferredPromise(); + deferred.notifyAll({done: 75, total: 100}); + deferred.catch(() => { + this.audio.pause(); + appMediaPlaybackController.willBePlayed(undefined); + }); + deferred.cancel = () => { + deferred.cancel = noop; + const err = new Error(); + (err as any).type = 'CANCELED'; + deferred.reject(err); + }; + preloader.attach(downloadDiv, false, deferred); + + pauseListener = this.addAudioListener('pause', () => { + deferred.cancel(); + }, {once: true}) as any; + }; - preloader.attach(downloadDiv, false); - } else { - preloader = new ProgressivePreloader({ - cancelable: true - }); + /* if(!this.audio.paused) { + onPlay(); + } */ + const playListener: any = this.addAudioListener('play', onPlay); + this.readyPromise.then(() => { + this.listenerSetter.remove(playListener); + this.listenerSetter.remove(pauseListener); + }); + } else { const load = () => { const download = getDownloadPromise(); preloader.attach(downloadDiv, false, download); @@ -527,17 +601,9 @@ export default class AudioElement extends HTMLElement { } } - if(isSafari) { - this.audio.autoplay = true; - this.audio.play().catch(() => {}); - } - this.append(downloadDiv); - - new Promise((resolve) => { - if(this.audio.readyState >= 2) resolve(); - else this.addAudioListener('canplay', resolve); - }).then(() => { + + this.readyPromise.then(() => { downloadDiv.classList.add('downloaded'); setTimeout(() => { downloadDiv.remove(); @@ -547,14 +613,14 @@ export default class AudioElement extends HTMLElement { // release loaded audio if(appMediaPlaybackController.willBePlayedMedia === this.audio) { this.audio.play(); - appMediaPlaybackController.willBePlayedMedia = null; + appMediaPlaybackController.willBePlayed(undefined); } //}, 10e3); }); }; if(!this.audio?.src) { - attachClickEvent(this, r, {once: true, capture: true, passive: false}); + attachClickEvent(toggle, r, {once: true, capture: true, passive: false}); } //} } @@ -564,15 +630,8 @@ export default class AudioElement extends HTMLElement { } } - /* connectedCallback() { - // браузер вызывает этот метод при добавлении элемента в документ - // (может вызываться много раз, если элемент многократно добавляется/удаляется) - } */ - - public addAudioListener(name: string, callback: any) { - if(!this.attachedHandlers[name]) this.attachedHandlers[name] = []; - this.attachedHandlers[name].push(callback); - this.audio.addEventListener(name, callback); + get addAudioListener() { + return this.listenerSetter.add(this.audio); } disconnectedCallback() { @@ -580,21 +639,18 @@ export default class AudioElement extends HTMLElement { return; } - // браузер вызывает этот метод при удалении элемента из документа - // (может вызываться много раз, если элемент многократно добавляется/удаляется) if(this.onTypeDisconnect) { this.onTypeDisconnect(); this.onTypeDisconnect = null; } - for(let name in this.attachedHandlers) { - for(let callback of this.attachedHandlers[name]) { - this.audio.removeEventListener(name, callback); - } - - delete this.attachedHandlers[name]; + if(this.readyPromise) { + this.readyPromise.reject(); } + this.listenerSetter.removeAll(); + this.listenerSetter = null; + this.preloader = null; } } diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 6a379bb9..4a8c1b66 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -82,7 +82,7 @@ export async function openAvatarViewer(target: HTMLElement, peerId: number, midd new AppMediaViewer() .setSearchContext({ peerId, - inputFilter, + inputFilter: {_: inputFilter}, }) .openMedia(message, getTarget(), undefined, undefined, prevTargets ? f(prevTargets) : undefined, nextTargets ? f(nextTargets) : undefined); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index d1745e4d..babf4784 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -944,7 +944,7 @@ export default class ChatBubbles { } //this.log('chatInner click:', target); - const isVideoComponentElement = target.tagName === 'SPAN'; + const isVideoComponentElement = target.tagName === 'SPAN' && !target.classList.contains('emoji'); /* if(isVideoComponentElement) { const video = target.parentElement.querySelector('video') as HTMLElement; if(video) { @@ -1054,7 +1054,7 @@ export default class ChatBubbles { .setSearchContext({ threadId: this.chat.threadId, peerId: this.peerId, - inputFilter: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo' + inputFilter: {_: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo'} }) .openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1)); @@ -2775,6 +2775,11 @@ export default class ChatBubbles { group: CHAT_ANIMATION_GROUP, loadPromises, noAutoDownload: this.chat.noAutoDownloadMedia, + searchContext: isRound ? { + peerId: this.peerId, + inputFilter: {_: 'inputMessagesFilterRoundVoice'}, + threadId: this.chat.threadId + } : undefined }); } } else { @@ -2786,7 +2791,12 @@ export default class ChatBubbles { chat: this.chat, loadPromises, noAutoDownload: this.chat.noAutoDownloadMedia, - lazyLoadQueue: this.lazyLoadQueue + lazyLoadQueue: this.lazyLoadQueue, + searchContext: doc.type === 'voice' || doc.type === 'audio' ? { + peerId: this.peerId, + inputFilter: {_: doc.type === 'voice' ? 'inputMessagesFilterRoundVoice' : 'inputMessagesFilterMusic'}, + threadId: this.chat.threadId + } : undefined }); if(newNameContainer) { @@ -2879,7 +2889,8 @@ export default class ChatBubbles { let savedFrom = ''; - const needName = ((peerId < 0 && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId; + // const needName = ((peerId < 0 && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId; + const needName = (message.fromId !== rootScope.myId && peerId < 0 && !this.appPeersManager.isBroadcast(peerId)) || message.viaBotId; if(needName || message.fwd_from || message.reply_to_mid) { // chat let title: HTMLElement | DocumentFragment; diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index a74b6752..6de0c077 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -388,7 +388,7 @@ export default class ChatInput { onClick: () => { new PopupCreatePoll(this.chat).show(); }, - verify: (peerId, threadId) => this.appMessagesManager.canSendToPeer(peerId, threadId, 'send_polls') + verify: (peerId, threadId) => this.appMessagesManager.canSendToPeer(peerId, threadId, 'send_polls') && peerId < 0 }]; this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons); @@ -576,6 +576,9 @@ export default class ChatInput { this.recorder.ondataavailable = (typedArray: Uint8Array) => { if(this.recordCanceled) return; + + const {peerId, threadId} = this.chat; + const replyToMsgId = this.replyToMsgId; const duration = (Date.now() - this.recordStartTime) / 1000 | 0; const dataBlob = new Blob([typedArray], {type: 'audio/ogg'}); @@ -588,7 +591,6 @@ export default class ChatInput { opusDecodeController.setKeepAlive(false); - let peerId = this.chat.peerId; // тут objectURL ставится уже с audio/wav this.appMessagesManager.sendFile(peerId, dataBlob, { isVoiceMessage: true, @@ -596,8 +598,8 @@ export default class ChatInput { duration, waveform: result.waveform, objectURL: result.url, - replyToMsgId: this.replyToMsgId, - threadId: this.chat.threadId, + replyToMsgId, + threadId, clearDraft: true }); @@ -1464,7 +1466,7 @@ export default class ChatInput { this.recordTimeEl.innerText = formatted; - window.requestAnimationFrame(r); + fastRaf(r); }; r(); diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 25b131e3..76a8f953 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -516,7 +516,7 @@ export class SearchSelection extends AppSelection { appMessagesManager, listenElement: searchSuper.container, listenerSetter: new ListenerSetter(), - verifyTarget: (e, target) => !!target, + verifyTarget: (e, target) => !!target && this.isSelecting, getElementFromTarget: (target) => findUpClassName(target, 'search-super-item'), targetLookupClassName: 'search-super-item', lookupBetweenParentClassName: 'tabs-tab', @@ -560,6 +560,11 @@ export class SearchSelection extends AppSelection { this.updateElementSelection(element, this.isMidSelected(peerId, mid)); }; + public toggleByMid = (peerId: number, mid: number) => { + const element = this.searchSuper.mediaTab.contentTab.querySelector(`.search-super-item[data-peer-id="${peerId}"][data-mid="${mid}"]`) as HTMLElement; + this.toggleByElement(element); + }; + protected onUpdateContainer = (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => { const length = this.length(); replaceContent(this.selectionCountEl, i18n('messages', [length])); diff --git a/src/components/poll.ts b/src/components/poll.ts index 231dfdb3..5ba5a5d5 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -430,9 +430,12 @@ export default class PollElement extends HTMLElement { // const width = mediaSizes.active.poll.width; // this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left - if(poll.chosenIndexes.length || this.isClosed) { + const canVote = !(poll.chosenIndexes.length || this.isClosed); + if(!canVote || this.isPublic) { this.performResults(results, poll.chosenIndexes, false); - } else if(!this.isClosed) { + } + + if(canVote) { this.setVotersCount(results); attachClickEvent(this, this.clickHandler); } diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 1008881d..8dfc0a0d 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -16,7 +16,7 @@ const TRANSITION_TIME = 200; export default class ProgressivePreloader { public preloader: HTMLDivElement; - private circle: SVGCircleElement; + public circle: SVGCircleElement; private cancelSvg: SVGSVGElement; private downloadSvg: HTMLElement; @@ -33,7 +33,7 @@ export default class ProgressivePreloader { public loadFunc: (e?: Event) => {download: CancellablePromise}; - private totalLength: number; + public totalLength: number; constructor(options?: Partial<{ isUpload: ProgressivePreloader['isUpload'], @@ -85,6 +85,12 @@ export default class ProgressivePreloader { `; + if(this.streamable) { + this.totalLength = 118.61124420166016; + } else { + this.totalLength = 149.82473754882812; + } + if(this.cancelable) { this.preloader.innerHTML += ` @@ -147,7 +153,7 @@ export default class ProgressivePreloader { const startTime = Date.now(); const onEnd = (err: Error) => { - promise.notify = null; + promise.notify = promise.notifyAll = null; if(tempId !== this.tempId) { return; @@ -205,10 +211,6 @@ export default class ProgressivePreloader { } public attach(elem: Element, reset = false, promise?: CancellablePromise) { - if(promise/* && false */) { - this.attachPromise(promise); - } - //return; this.detached = false; @@ -224,19 +226,24 @@ export default class ProgressivePreloader { this.preloader.classList.remove('manual'); } + const useRafs = isInDOM(this.preloader) ? 1 : 2; if(this.preloader.parentElement !== elem) { elem[this.attachMethod](this.preloader); } - fastRaf(() => { + if(promise/* && false */) { + this.attachPromise(promise); + } + + // fastRaf(() => { //console.log('[PP]: attach after rAF', this.detached, performance.now()); - if(this.detached) { - return; - } + // if(this.detached) { + // return; + // } - SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME); - }); + SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME, undefined, useRafs); + // }); if(this.cancelable && reset) { this.setProgress(0); @@ -272,7 +279,7 @@ export default class ProgressivePreloader { } public setProgress(percents: number) { - if(!isInDOM(this.circle)) { + if(!this.totalLength && !isInDOM(this.circle)) { return; } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 0dc73339..3ee05f37 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -70,7 +70,7 @@ mediaSizes.addEventListener('changeScreen', (from, to) => { } }); -export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size}: { +export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size, searchContext}: { doc: MyDocument, container?: HTMLElement, message?: any, @@ -87,7 +87,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai withoutPreloader?: boolean, loadPromises?: Promise[], noAutoDownload?: boolean, - size?: PhotoSize + size?: PhotoSize, + searchContext?: SearchSuperContext }) { const isAlbumItem = !(boxWidth && boxHeight); const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem)) @@ -276,6 +277,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } */ if(globalVideo.paused) { + appMediaPlaybackController.setSearchContext(searchContext); globalVideo.play(); } else { globalVideo.pause(); @@ -525,6 +527,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS audioElement.withTime = withTime; audioElement.message = message; audioElement.noAutoDownload = noAutoDownload; + audioElement.lazyLoadQueue = lazyLoadQueue; + audioElement.loadPromises = loadPromises; if(voiceAsMusic) audioElement.voiceAsMusic = voiceAsMusic; if(searchContext) audioElement.searchContext = searchContext; @@ -1588,7 +1592,7 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo }); } -export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue}: { +export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue, searchContext}: { albumMustBeRenderedFull: boolean, message: any, messageDiv: HTMLElement, @@ -1597,7 +1601,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, chat: Chat, loadPromises?: Promise[], noAutoDownload?: boolean, - lazyLoadQueue?: LazyLoadQueue + lazyLoadQueue?: LazyLoadQueue, + searchContext?: SearchSuperContext }) { let nameContainer: HTMLElement; const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid]; @@ -1611,7 +1616,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, message, loadPromises, noAutoDownload, - lazyLoadQueue + lazyLoadQueue, + searchContext }); const container = document.createElement('div'); diff --git a/src/helpers/cancellablePromise.ts b/src/helpers/cancellablePromise.ts index d67c6da6..eada2a50 100644 --- a/src/helpers/cancellablePromise.ts +++ b/src/helpers/cancellablePromise.ts @@ -30,7 +30,6 @@ export function deferredPromise() { deferredHelper.listeners.forEach((callback: any) => callback(...args)); }, - lastNotify: undefined, listeners: [], addNotifyListener: (callback: (...args: any[]) => void) => { if(deferredHelper.lastNotify) { @@ -43,14 +42,14 @@ export function deferredPromise() { let deferred: CancellablePromise = new Promise((resolve, reject) => { deferredHelper.resolve = (value: T) => { - if(deferred.isFulfilled) return; + if(deferred.isFulfilled || deferred.isRejected) return; deferred.isFulfilled = true; resolve(value); }; deferredHelper.reject = (...args: any[]) => { - if(deferred.isRejected) return; + if(deferred.isRejected || deferred.isFulfilled) return; deferred.isRejected = true; reject(...args); @@ -64,9 +63,8 @@ export function deferredPromise() { }; */ deferred.finally(() => { - deferred.notify = null; + deferred.notify = deferred.notifyAll = deferred.lastNotify = null; deferred.listeners.length = 0; - deferred.lastNotify = null; if(deferred.cancel) { deferred.cancel = () => {}; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 59b98188..99b64219 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -320,6 +320,18 @@ export class AppMessagesManager { this.reloadConversation(peerId); } }); + + rootScope.addEventListener('poll_update', ({poll}) => { + const set = appPollsManager.pollToMessages[poll.id]; + if(set) { + for(const key of set) { + const [peerId, mid] = key.split('_'); + + const message = this.getMessageByPeer(+peerId, +mid); + this.setDialogToStateIfMessageIsTop(message); + } + } + }); appStateManager.getState().then(state => { if(state.maxSeenMsgId) { @@ -2359,7 +2371,9 @@ export class AppMessagesManager { break; case 'messageMediaPoll': - message.media.poll = appPollsManager.savePoll(message.media.poll, message.media.results); + const result = appPollsManager.savePoll(message.media.poll, message.media.results, message); + message.media.poll = result.poll; + message.media.results = result.results; break; case 'messageMediaDocument': if(message.media.ttl_seconds) { @@ -2616,7 +2630,7 @@ export class AppMessagesManager { break; } case 'messageMediaDocument': { - const document = media.document; + const document = media.document as MyDocument; if(document.type === 'video') { addPart('AttachVideo', undefined, message.message); @@ -2630,6 +2644,10 @@ export class AppMessagesManager { addPart(undefined, ((plain ? document.stickerEmojiRaw : document.stickerEmoji) || '')); addPart('AttachSticker'); text = ''; + } else if(document.type === 'audio') { + const attribute = document.attributes.find(attribute => attribute._ === 'documentAttributeAudio' && (attribute.title || attribute.performer)) as DocumentAttribute.documentAttributeAudio; + const f = '🎵' + ' ' + (attribute ? [attribute.title, attribute.performer].filter(Boolean).join(' - ') : document.file_name); + addPart(undefined, plain ? f : RichTextProcessor.wrapEmojiText(f), message.message); } else { addPart(undefined, plain ? document.file_name : RichTextProcessor.wrapEmojiText(document.file_name), message.message); } @@ -5353,6 +5371,10 @@ export class AppMessagesManager { const messageKey = appWebPagesManager.getMessageKeyForPendingWebPage(message.peerId, message.mid, isScheduled); appWebPagesManager.deleteWebPageFromPending(media.webpage, messageKey); } + + if((media as MessageMedia.messageMediaPoll).poll) { + appPollsManager.updatePollToMessage(message as Message.message, false); + } } } diff --git a/src/lib/appManagers/appPollsManager.ts b/src/lib/appManagers/appPollsManager.ts index d063137c..cc51bcd0 100644 --- a/src/lib/appManagers/appPollsManager.ts +++ b/src/lib/appManagers/appPollsManager.ts @@ -6,7 +6,7 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import { copy } from "../../helpers/object"; -import { InputMedia, MessageEntity } from "../../layer"; +import { InputMedia, Message, MessageEntity, MessageMedia } from "../../layer"; import { logger, LogTypes } from "../logger"; import apiManager from "../mtproto/mtprotoworker"; import { RichTextProcessor } from "../richtextprocessor"; @@ -80,6 +80,7 @@ export type Poll = { export class AppPollsManager { public polls: {[id: string]: Poll} = {}; public results: {[id: string]: PollResults} = {}; + public pollToMessages: {[id: string]: Set} = {}; private log = logger('POLLS', LogTypes.Error); @@ -93,27 +94,35 @@ export class AppPollsManager { return; } - poll = this.savePoll(poll, update.results as any); - rootScope.dispatchEvent('poll_update', {poll, results: update.results as any}); + let results = update.results; + const ret = this.savePoll(poll, results as any); + poll = ret.poll; + results = ret.results; + + rootScope.dispatchEvent('poll_update', {poll, results: results as any}); } }); } - public savePoll(poll: Poll, results: PollResults) { + public savePoll(poll: Poll, results: PollResults, message?: Message.message) { + if(message) { + this.updatePollToMessage(message, true); + } + const id = poll.id; if(this.polls[id]) { poll = Object.assign(this.polls[id], poll); - this.saveResults(poll, results); - return poll; - } + results = this.saveResults(poll, results); + } else { + this.polls[id] = poll; - this.polls[id] = poll; + poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question); + poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll'); + poll.chosenIndexes = []; + results = this.saveResults(poll, results); + } - poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question); - poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll'); - poll.chosenIndexes = []; - this.saveResults(poll, results); - return poll; + return {poll, results}; } public saveResults(poll: Poll, results: PollResults) { @@ -133,6 +142,8 @@ export class AppPollsManager { }); } } + + return results; } public getPoll(pollId: string): {poll: Poll, results: PollResults} { @@ -162,6 +173,29 @@ export class AppPollsManager { }; } + public updatePollToMessage(message: Message.message, add: boolean) { + const {id} = (message.media as MessageMedia.messageMediaPoll).poll; + let set = this.pollToMessages[id]; + + if(!add && !set) { + return; + } + + if(!set) { + set = this.pollToMessages[id] = new Set(); + } + + const key = message.peerId + '_' + message.mid; + if(add) set.add(key); + else set.delete(key); + + if(!add && !set.size) { + delete this.polls[id]; + delete this.results[id]; + delete this.pollToMessages[id]; + } + } + public sendVote(message: any, optionIds: number[]): Promise { const poll: Poll = message.media.poll; diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index c5b1d612..35b1a1ac 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -640,6 +640,10 @@ namespace RichTextProcessor { onclick = 'showMaskedAlert'; } + if(options.wrappingDraft) { + onclick = undefined; + } + const href = (currentContext || typeof electronHelpers === 'undefined') ? encodeEntities(url) : `javascript:electronHelpers.openExternal('${encodeEntities(url)}');`; diff --git a/src/scss/partials/_audio.scss b/src/scss/partials/_audio.scss index e27f75b2..e05398b4 100644 --- a/src/scss/partials/_audio.scss +++ b/src/scss/partials/_audio.scss @@ -8,30 +8,65 @@ position: relative; padding-left: 67px; overflow: visible!important; - height: 54px; + height: 3.375rem; user-select: none; /* @include respond-to(handhelds) { padding-left: 45px; } */ - &-toggle, &-download { + &-toggle, + &-download { overflow: hidden; border-radius: 50%; background-color: var(--primary-color); align-items: center; } - &-toggle { + &.corner-download { + .audio-download { + width: 1.375rem; + height: 1.375rem; + margin: 2rem 2rem 0; + background: none; + display: flex !important; + top: 0; + } + + .preloader-container { + border-radius: inherit; + background-color: var(--primary-color); + } + + .preloader-path-new { + stroke-width: .25rem; + } + } + + &-play-icon { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; transform: rotate(-119deg); - + overflow: hidden; + max-width: 100%; + max-height: 100%; + border-radius: inherit; + @include animation-level(2) { transition: transform .25s ease-in-out; } + } + &-toggle { .part { position: absolute; background-color: white; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); @include animation-level(2) { transition: clip-path .25s ease-in-out; @@ -185,160 +220,160 @@ ); } } - } - &-toggle.playing { - transform: rotate(-90deg); - } - - &-toggle:not(.playing) { - .part { - @include respond-to(not-handhelds) { - height: 136px; - width: 136px; - } - - @include respond-to(handhelds) { - height: 92px; - width: 92px; - } - - &.one { - clip-path: polygon( - 43.77666% 55.85251%, - 43.77874% 55.46331%, - 43.7795% 55.09177%, - 43.77934% 54.74844%, - 43.77855% 54.44389%, - 43.77741% 54.18863%, - 43.77625% 53.99325%, - 43.77533% 53.86828%, - 43.77495% 53.82429%, - 43.77518% 53.55329%, - 43.7754% 53.2823%, - 43.77563% 53.01131%, - 43.77585% 52.74031%, - 43.77608% 52.46932%, - 43.7763% 52.19832%, - 43.77653% 51.92733%, - 43.77675% 51.65633%, - 43.77653% 51.38533%, - 43.7763% 51.11434%, - 43.77608% 50.84334%, - 43.77585% 50.57235%, - 43.77563% 50.30136%, - 43.7754% 50.03036%, - 43.77518% 49.75936%, - 43.77495% 49.48837%, - 44.48391% 49.4885%, - 45.19287% 49.48865%, - 45.90183% 49.48878%, - 46.61079% 49.48892%, - 47.31975% 49.48906%, - 48.0287% 49.4892%, - 48.73766% 49.48934%, - 49.44662% 49.48948%, - 50.72252% 49.48934%, - 51.99842% 49.4892%, - 53.27432% 49.48906%, - 54.55022% 49.48892%, - 55.82611% 49.48878%, - 57.10201% 49.48865%, - 58.3779% 49.4885%, - 59.6538% 49.48837%, - 59.57598% 49.89151%, - 59.31883% 50.28598%, - 58.84686% 50.70884%, - 58.12456% 51.19714%, - 57.11643% 51.78793%, - 55.78697% 52.51828%, - 54.10066% 53.42522%, - 52.02202% 54.54581%, - 49.96525% 55.66916%, - 48.3319% 56.57212%, - 47.06745% 57.27347%, - 46.11739% 57.79191%, - 45.42719% 58.14619%, - 44.94235% 58.35507%, - 44.60834% 58.43725%, - 44.37066% 58.41149%, - 44.15383% 58.27711%, - 43.99617% 58.0603%, - 43.88847% 57.77578%, - 43.82151% 57.43825%, - 43.78608% 57.06245%, - 43.77304% 56.66309%, - 43.773% 56.25486% - ); - } + &.playing .audio-play-icon { + transform: rotate(-90deg); + } - &.two { - clip-path: polygon( - 43.77666% 43.83035%, - 43.77874% 44.21955%, - 43.7795% 44.59109%, - 43.77934% 44.93442%, - 43.77855% 45.23898%, - 43.77741% 45.49423%, - 43.77625% 45.68961%, - 43.77533% 45.81458%, - 43.77495% 45.85858%, - 43.77518% 46.12957%, - 43.7754% 46.40056%, - 43.77563% 46.67156%, - 43.77585% 46.94255%, - 43.77608% 47.21355%, - 43.7763% 47.48454%, - 43.77653% 47.75554%, - 43.77675% 48.02654%, - 43.77653% 48.29753%, - 43.7763% 48.56852%, - 43.77608% 48.83952%, - 43.77585% 49.11051%, - 43.77563% 49.38151%, - 43.7754% 49.65251%, - 43.77518% 49.9235%, - 43.77495% 50.1945%, - 44.48391% 50.19436%, - 45.19287% 50.19422%, - 45.90183% 50.19408%, - 46.61079% 50.19394%, - 47.31975% 50.1938%, - 48.0287% 50.19366%, - 48.73766% 50.19353%, - 49.44662% 50.19338%, - 50.72252% 50.19353%, - 51.99842% 50.19366%, - 53.27432% 50.1938%, - 54.55022% 50.19394%, - 55.82611% 50.19408%, - 57.10201% 50.19422%, - 58.3779% 50.19436%, - 59.6538% 50.1945%, - 59.57598% 49.79136%, - 59.31883% 49.39688%, - 58.84686% 48.97402%, - 58.12456% 48.48572%, - 57.11643% 47.89493%, - 55.78697% 47.16458%, - 54.10066% 46.25764%, - 52.02202% 45.13705%, - 49.96525% 44.01371%, - 48.3319% 43.11074%, - 47.06745% 42.4094%, - 46.11739% 41.89096%, - 45.42719% 41.53667%, - 44.94235% 41.3278%, - 44.60834% 41.24561%, - 44.37066% 41.27137%, - 44.15383% 41.40575%, - 43.99617% 41.62256%, - 43.88847% 41.90709%, - 43.82151% 42.24461%, - 43.78608% 42.62041%, - 43.77304% 43.01978%, - 43.773% 43.428% - ); + &:not(.playing) { + .part { + @include respond-to(not-handhelds) { + height: 136px; + width: 136px; + } + + @include respond-to(handhelds) { + height: 92px; + width: 92px; + } + + &.one { + clip-path: polygon( + 43.77666% 55.85251%, + 43.77874% 55.46331%, + 43.7795% 55.09177%, + 43.77934% 54.74844%, + 43.77855% 54.44389%, + 43.77741% 54.18863%, + 43.77625% 53.99325%, + 43.77533% 53.86828%, + 43.77495% 53.82429%, + 43.77518% 53.55329%, + 43.7754% 53.2823%, + 43.77563% 53.01131%, + 43.77585% 52.74031%, + 43.77608% 52.46932%, + 43.7763% 52.19832%, + 43.77653% 51.92733%, + 43.77675% 51.65633%, + 43.77653% 51.38533%, + 43.7763% 51.11434%, + 43.77608% 50.84334%, + 43.77585% 50.57235%, + 43.77563% 50.30136%, + 43.7754% 50.03036%, + 43.77518% 49.75936%, + 43.77495% 49.48837%, + 44.48391% 49.4885%, + 45.19287% 49.48865%, + 45.90183% 49.48878%, + 46.61079% 49.48892%, + 47.31975% 49.48906%, + 48.0287% 49.4892%, + 48.73766% 49.48934%, + 49.44662% 49.48948%, + 50.72252% 49.48934%, + 51.99842% 49.4892%, + 53.27432% 49.48906%, + 54.55022% 49.48892%, + 55.82611% 49.48878%, + 57.10201% 49.48865%, + 58.3779% 49.4885%, + 59.6538% 49.48837%, + 59.57598% 49.89151%, + 59.31883% 50.28598%, + 58.84686% 50.70884%, + 58.12456% 51.19714%, + 57.11643% 51.78793%, + 55.78697% 52.51828%, + 54.10066% 53.42522%, + 52.02202% 54.54581%, + 49.96525% 55.66916%, + 48.3319% 56.57212%, + 47.06745% 57.27347%, + 46.11739% 57.79191%, + 45.42719% 58.14619%, + 44.94235% 58.35507%, + 44.60834% 58.43725%, + 44.37066% 58.41149%, + 44.15383% 58.27711%, + 43.99617% 58.0603%, + 43.88847% 57.77578%, + 43.82151% 57.43825%, + 43.78608% 57.06245%, + 43.77304% 56.66309%, + 43.773% 56.25486% + ); + } + + &.two { + clip-path: polygon( + 43.77666% 43.83035%, + 43.77874% 44.21955%, + 43.7795% 44.59109%, + 43.77934% 44.93442%, + 43.77855% 45.23898%, + 43.77741% 45.49423%, + 43.77625% 45.68961%, + 43.77533% 45.81458%, + 43.77495% 45.85858%, + 43.77518% 46.12957%, + 43.7754% 46.40056%, + 43.77563% 46.67156%, + 43.77585% 46.94255%, + 43.77608% 47.21355%, + 43.7763% 47.48454%, + 43.77653% 47.75554%, + 43.77675% 48.02654%, + 43.77653% 48.29753%, + 43.7763% 48.56852%, + 43.77608% 48.83952%, + 43.77585% 49.11051%, + 43.77563% 49.38151%, + 43.7754% 49.65251%, + 43.77518% 49.9235%, + 43.77495% 50.1945%, + 44.48391% 50.19436%, + 45.19287% 50.19422%, + 45.90183% 50.19408%, + 46.61079% 50.19394%, + 47.31975% 50.1938%, + 48.0287% 50.19366%, + 48.73766% 50.19353%, + 49.44662% 50.19338%, + 50.72252% 50.19353%, + 51.99842% 50.19366%, + 53.27432% 50.1938%, + 54.55022% 50.19394%, + 55.82611% 50.19408%, + 57.10201% 50.19422%, + 58.3779% 50.19436%, + 59.6538% 50.1945%, + 59.57598% 49.79136%, + 59.31883% 49.39688%, + 58.84686% 48.97402%, + 58.12456% 48.48572%, + 57.11643% 47.89493%, + 55.78697% 47.16458%, + 54.10066% 46.25764%, + 52.02202% 45.13705%, + 49.96525% 44.01371%, + 48.3319% 43.11074%, + 47.06745% 42.4094%, + 46.11739% 41.89096%, + 45.42719% 41.53667%, + 44.94235% 41.3278%, + 44.60834% 41.24561%, + 44.37066% 41.27137%, + 44.15383% 41.40575%, + 43.99617% 41.62256%, + 43.88847% 41.90709%, + 43.82151% 42.24461%, + 43.78608% 42.62041%, + 43.77304% 43.01978%, + 43.773% 43.428% + ); + } } } } @@ -366,7 +401,8 @@ opacity: 1; } - &.active, .audio.is-unread:not(.is-out) & { + &.active, + .audio.is-unread:not(.is-out) & { opacity: 1; } } @@ -392,7 +428,8 @@ margin-bottom: -2px; } - &-ico, &-download { + &-ico, + &-download { width: 3rem; height: 3rem; } @@ -400,8 +437,6 @@ .part { height: 112px !important; width: 112px !important; - position: absolute; - background-color: white; @include respond-to(handhelds) { width: 100px !important; @@ -415,8 +450,9 @@ color: var(--primary-text-color); } - &-time, &-subtitle { - font-size: 14px; + &-time, + &-subtitle { + font-size: .875rem; color: var(--secondary-text-color); overflow: hidden; text-overflow: ellipsis; @@ -424,7 +460,7 @@ display: flex; @include respond-to(handhelds) { - font-size: 12px; + font-size: .75rem; } } @@ -434,17 +470,19 @@ .audio-time { flex: 0 0 auto; - margin-right: 4px; + margin-right: .25rem; } } // * for audio - &-title, &-subtitle { + &-title, + &-subtitle { margin-left: -1px; } // * for audio - &-title, &:not(.audio-show-progress) &-subtitle { + &-title, + &:not(.audio-show-progress) &-subtitle { white-space: nowrap; overflow: hidden; max-width: 100%; @@ -490,6 +528,32 @@ --border-radius: 4px; --thumb-size: .75rem; flex: 1 1 auto; - margin-left: 5px; + margin: 0 6px 0 5px; // margin-right due to overflow + } + + &-with-thumb { + .audio-play-icon { + z-index: 1; + background-color: transparent; + + @include animation-level(2) { + transition: transform .25 ease-in-out, background-color .2s ease-in-out; + } + + .part { + background-color: #fff !important; + } + + &:not(:last-child) { + background-color: rgba(0, 0, 0, .3); + } + } + + .media-photo { + border-radius: inherit; + object-fit: cover; + width: inherit; + height: inherit; + } } } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index f97d438a..4c794ac0 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1079,7 +1079,8 @@ $bubble-margin: .25rem; @include respond-to(handhelds) { .document, .audio { - &-ico, &-download { + &-ico, + &-download { height: 2.25rem; width: 2.25rem; } @@ -1087,8 +1088,6 @@ $bubble-margin: .25rem; } .audio { - padding-right: 2px; - $parent: ".audio"; #{$parent} { &-title { @@ -2209,7 +2208,8 @@ $bubble-margin: .25rem; } &-toggle, - &-download { + &-download, + &.corner-download .preloader-container { background-color: var(--message-out-primary-color); } diff --git a/src/scss/partials/_document.scss b/src/scss/partials/_document.scss index 8ebc4931..72930656 100644 --- a/src/scss/partials/_document.scss +++ b/src/scss/partials/_document.scss @@ -56,12 +56,14 @@ } } - &-ico, &-download { + &-ico, + &-download { font-size: 1.125rem; background-size: contain; } - &-ico, &-name { + &-ico, + &-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -129,7 +131,8 @@ overflow: hidden; } - &-name, &-size { + &-name, + &-size { line-height: var(--line-height); } @@ -152,7 +155,8 @@ } } -.document, .audio { +.document, +.audio { display: flex; flex-direction: column; justify-content: center; @@ -160,7 +164,8 @@ position: relative; user-select: none; - &-ico, &-download { + &-ico, + &-download { position: absolute; left: 0; width: 3.375rem; @@ -185,7 +190,7 @@ } } - .preloader-container:not(.preloader-streamable) { + &:not(.corner-download) .preloader-container:not(.preloader-streamable) { transform: scale(1) !important; } } diff --git a/src/scss/partials/_poll.scss b/src/scss/partials/_poll.scss index a9ac7cd8..88c94862 100644 --- a/src/scss/partials/_poll.scss +++ b/src/scss/partials/_poll.scss @@ -37,6 +37,7 @@ poll-element { margin-top: 2px; margin-bottom: 5px; display: flex; + align-items: center; position: relative; // @include respond-to(handhelds) { diff --git a/src/scss/style.scss b/src/scss/style.scss index bb64a163..d5a44090 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -862,11 +862,11 @@ not screen and ( min-resolution: 192dpi), not screen and ( min-resolution: 2dppx) { html:not(.is-safari) { span.emoji { - margin-right: 5px; + margin-right: 5px !important; } avatar-element span.emoji { - margin-right: 0; + margin-right: 0 !important; } } }