From 40a2e087d6d4b8106e4856d67239130a9a5e1ffc Mon Sep 17 00:00:00 2001 From: morethanwords Date: Thu, 23 Sep 2021 17:41:02 +0400 Subject: [PATCH] Fix saving poll results to state Release poll if it's deleted Added thumbs for audio Fix sending voice message to wrong chat Fix missed avatars in messages from group Fix clickable links in input --- src/components/appMediaPlaybackController.ts | 135 ++++--- src/components/appMediaViewer.ts | 276 ++++++++----- src/components/appSearchSuper..ts | 16 +- src/components/audio.ts | 212 ++++++---- src/components/avatar.ts | 2 +- src/components/chat/bubbles.ts | 19 +- src/components/chat/input.ts | 12 +- src/components/chat/selection.ts | 7 +- src/components/poll.ts | 7 +- src/components/preloader.ts | 35 +- src/components/wrappers.ts | 16 +- src/helpers/cancellablePromise.ts | 8 +- src/lib/appManagers/appMessagesManager.ts | 26 +- src/lib/appManagers/appPollsManager.ts | 60 ++- src/lib/richtextprocessor.ts | 4 + src/scss/partials/_audio.scss | 398 +++++++++++-------- src/scss/partials/_chatBubble.scss | 8 +- src/scss/partials/_document.scss | 17 +- src/scss/partials/_poll.scss | 1 + src/scss/style.scss | 4 +- 20 files changed, 798 insertions(+), 465 deletions(-) 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; } } }