From 987653c905cc9ff8218b025c8763eac8782ec7f8 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 24 Dec 2020 07:26:29 +0200 Subject: [PATCH] Upload video thumb Cancel album item uploading --- src/components/audio.ts | 2 +- src/components/chat/bubbles.ts | 100 ------------- src/components/popups/newMedia.ts | 9 +- src/components/preloader.ts | 8 +- src/components/wrappers.ts | 4 +- src/helpers/files.ts | 38 +++++ src/lib/appManagers/appImManager.ts | 63 +++----- src/lib/appManagers/appMessagesManager.ts | 169 ++++++++++++++-------- src/lib/mtproto/apiFileManager.ts | 2 +- 9 files changed, 187 insertions(+), 208 deletions(-) create mode 100644 src/helpers/files.ts diff --git a/src/components/audio.ts b/src/components/audio.ts index b0d3b84c..666bcf8e 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -469,7 +469,7 @@ export default class AudioElement extends HTMLElement { this.audio.play().catch(() => {}); } - preloader.attach(downloadDiv); + preloader.attach(downloadDiv, false); this.append(downloadDiv); new Promise((resolve) => { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 4d9c09a6..70cf5547 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1743,106 +1743,6 @@ export default class ChatBubbles { let processingWebPage = false; switch(messageMedia._) { - case 'messageMediaPending': { - let pending = messageMedia; - let preloader = pending.preloader as ProgressivePreloader; - - switch(pending.type) { - case 'album': { - this.log('will wrap pending album'); - - bubble.classList.add('hide-name', 'photo', 'is-album', 'is-grouped'); - wrapAlbum({ - groupId: '' + message.id, - attachmentDiv, - uploading: true, - isOut: true, - chat: this.chat - }); - - break; - } - - case 'photo': { - //if(pending.size < 5e6) { - const photo = this.appPhotosManager.getPhoto(message.id); - //if(photo._ == 'photoEmpty') break; - this.log('will wrap pending photo:', pending, message, photo); - const withTail = !isAndroid && !message.message && !withReplies; - if(withTail) bubble.classList.add('with-media-tail'); - wrapPhoto({ - photo, message, - container: attachmentDiv, - withTail, - isOut: true, - lazyLoadQueue: this.lazyLoadQueue - }); - - bubble.classList.add('hide-name', 'photo'); - //} - - break; - } - - case 'video': { - //if(pending.size < 5e6) { - let doc = this.appDocsManager.getDoc(message.id); - //if(doc._ == 'documentEmpty') break; - this.log('will wrap pending video:', pending, message, doc); - const withTail = !isAndroid && !isApple && doc.type != 'round' && !message.message && !withReplies; - if(withTail) bubble.classList.add('with-media-tail'); - wrapVideo({ - doc, - container: attachmentDiv, - message, - boxWidth: mediaSizes.active.regular.width, - boxHeight: mediaSizes.active.regular.height, - withTail, - isOut: isOut, - lazyLoadQueue: this.lazyLoadQueue, - middleware: null, - group: CHAT_ANIMATION_GROUP - }); - - preloader.attach(attachmentDiv, false); - bubble.classList.add('hide-name', 'video'); - //} - break; - } - - case 'audio': - case 'voice': - case 'document': { - const newNameContainer = wrapGroupedDocuments({ - albumMustBeRenderedFull, - message, - bubble, - messageDiv, - chat: this.chat - }); - - if(newNameContainer) { - nameContainer = newNameContainer; - } - - const lastContainer = messageDiv.lastElementChild.querySelector('.document-size'); - lastContainer && lastContainer.append(timeSpan.cloneNode(true)); - - if(pending.type == 'voice') { - bubble.classList.add('bubble-audio'); - } - - bubble.classList.remove('is-message-empty'); - messageDiv.classList.add((pending.type || 'document') + '-message'); - processingWebPage = true; - break; - } - - } - - break; - } - case 'messageMediaPhoto': { const photo = messageMedia.photo; ////////this.log('messageMediaPhoto', photo); diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 33cf1210..89479a6c 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -8,10 +8,13 @@ import { toast } from "../toast"; import { prepareAlbum, wrapDocument } from "../wrappers"; import CheckboxField from "../checkbox"; import SendContextMenu from "../chat/sendContextMenu"; +import { createPosterForVideo, createPosterFromVideo } from "../../helpers/files"; type SendFileParams = Partial<{ file: File, objectURL: string, + thumbBlob: Blob, + thumbURL: string, width: number, height: number, duration: number @@ -238,7 +241,11 @@ export default class PopupNewMedia extends PopupElement { params.duration = Math.floor(video.duration); itemDiv.append(video); - resolve(itemDiv); + createPosterFromVideo(video).then(blob => { + params.thumbBlob = blob; + params.thumbURL = URL.createObjectURL(blob); + resolve(itemDiv); + }); }; video.append(source); diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 209ea75c..c2fa73c2 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -65,7 +65,7 @@ export default class ProgressivePreloader { const onEnd = () => { promise.notify = null; - if(tempId == this.tempId) { + if(tempId === this.tempId) { this.detach(); this.promise = promise = null; } @@ -80,7 +80,7 @@ export default class ProgressivePreloader { onEnd(); } */ - if(tempId != this.tempId) return; + if(tempId !== this.tempId) return; //console.log('preloader download', promise, details); const percents = details.done / details.total * 100; @@ -89,7 +89,7 @@ export default class ProgressivePreloader { } } - public attach(elem: Element, reset = true, promise?: CancellablePromise) { + public attach(elem: Element, reset = false, promise?: CancellablePromise) { if(promise/* && false */) { this.attachPromise(promise); } @@ -129,7 +129,7 @@ export default class ProgressivePreloader { return; } - if(percents == 0) { + if(percents === 0) { this.circle.style.strokeDasharray = ''; return; } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 90769b14..d8ade332 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -221,7 +221,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai let preloader: ProgressivePreloader; if(message?.media?.preloader) { // means upload preloader = message.media.preloader as ProgressivePreloader; - preloader.attach(container, undefined, undefined); + preloader.attach(container, false); } else if(!doc.downloaded && !doc.supportsStreaming) { const promise = appDocsManager.downloadDoc(doc, undefined, lazyLoadQueue?.queueId); preloader = new ProgressivePreloader(null, true, false, 'prepend'); @@ -557,7 +557,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT let preloader: ProgressivePreloader; if(message?.media?.preloader) { // means upload - message.media.preloader.attach(container); + message.media.preloader.attach(container, false); } else if(!cacheContext.downloaded) { preloader = new ProgressivePreloader(null, false, false, photo._ == 'document' ? 'prepend' : 'append'); } diff --git a/src/helpers/files.ts b/src/helpers/files.ts new file mode 100644 index 00000000..b86929c5 --- /dev/null +++ b/src/helpers/files.ts @@ -0,0 +1,38 @@ +import { pause } from "./schedulers"; + +export function preloadVideo(url: string): Promise { + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + video.volume = 0; + video.onloadedmetadata = () => resolve(video); + video.onerror = reject; + video.src = url; + }); +} + +export function createPosterFromVideo(video: HTMLVideoElement): Promise { + return new Promise((resolve, reject) => { + video.onseeked = () => { + const canvas = document.createElement('canvas'); + canvas.width = Math.min(1280, video.videoWidth); + canvas.height = Math.min(720, video.videoHeight); + const ctx = canvas.getContext('2d')!; + ctx.drawImage(video, 0, 0); + canvas.toBlob(blob => { + resolve(blob); + }, 'image/jpeg', 1); + }; + + video.onerror = reject; + video.currentTime = Math.min(video.duration, 1); + }); +} + +export async function createPosterForVideo(url: string): Promise { + const video = await preloadVideo(url); + + return Promise.race([ + pause(2000) as Promise, + createPosterFromVideo(video), + ]); +} \ No newline at end of file diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 778bdc9b..932c4a06 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -36,13 +36,12 @@ appSidebarLeft; // just to include const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; export const CHAT_ANIMATION_GROUP = 'chat'; +const FOCUS_EVENT_NAME = isTouchSupported ? 'touchstart' : 'mousemove'; export class AppImManager { public columnEl = document.getElementById('column-center') as HTMLDivElement; public chatsContainer: HTMLElement; - //public chatsSelectTab: ReturnType; - public offline = false; public updateStatusInterval = 0; @@ -50,10 +49,7 @@ export class AppImManager { public setPeerPromise: Promise = null; - //private mainColumns: HTMLElement; - //public _selectTab: ReturnType; public tabId = -1; - //private closeBtn: HTMLButtonElement;// = this.topbar.querySelector('.sidebar-close-button') as HTMLButtonElement; public hideRightSidebar = false; private chats: Chat[] = []; @@ -75,8 +71,6 @@ export class AppImManager { this.log = logger('IM', LogLevels.log | LogLevels.warn | LogLevels.debug | LogLevels.error); - //this.mainColumns = this.columnEl.parentElement; - //this._selectTab = horizontalMenu(null, this.mainColumns); this.selectTab(0); window.addEventListener('blur', () => { @@ -95,27 +89,12 @@ export class AppImManager { animationIntersector.checkAnimations(false); }, {once: true}); }); - - /* this.closeBtn.addEventListener('click', (e) => { - cancelEvent(e); - if(mediaSizes.isMobile) { - //this.setPeer(0); - this.selectTab(0); - } else { - const isNowOpen = document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME); - - if(isNowOpen && document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) { - appSidebarRight.toggleSidebar(false, false); - this.hideRightSidebar = isNowOpen; - } else if(this.hideRightSidebar) { - appSidebarRight.toggleSidebar(true); - } - } - }); */ - - this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); - this.updateStatus(); + // * Prevent setting online after reloading page + window.addEventListener(FOCUS_EVENT_NAME, () => { + this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); + this.updateStatus(); + }, {once: true, passive: true}); this.chatsContainer = document.createElement('div'); this.chatsContainer.classList.add('chats-container', 'tabs-container'); @@ -166,8 +145,6 @@ export class AppImManager { location.hash = ''; }); - - //apiUpdatesManager.attach(); } private chatsSelectTab(tab: HTMLElement) { @@ -198,7 +175,7 @@ export class AppImManager { const chat = this.chat; - if(e.key == 'Escape') { + if(e.key === 'Escape') { let cancel = true; if(this.markupTooltip?.container?.classList.contains('is-visible')) { this.markupTooltip.hide(); @@ -206,7 +183,7 @@ export class AppImManager { chat.selection.cancelSelection(); } else if(chat.container.classList.contains('is-helper-active')) { chat.input.replyElements.cancelBtn.click(); - } else if(chat.peerId != 0) { // hide current dialog + } else if(chat.peerId !== 0) { // hide current dialog this.setPeer(0); } else { cancel = false; @@ -216,25 +193,27 @@ export class AppImManager { if(cancel) { cancelEvent(e); } - } else if(e.key == 'Meta' || e.key == 'Control') { + } else if(e.key === 'Meta' || e.key === 'Control') { return; - } else if(e.code == "KeyC" && (e.ctrlKey || e.metaKey) && target.tagName != 'INPUT') { + } else if(e.code === "KeyC" && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT') { return; - } else if(e.code == 'ArrowUp') { + } else if(e.code === 'ArrowUp') { if(!chat.input.editMsgId) { const history = appMessagesManager.getHistoryStorage(chat.peerId); if(history.history.length) { let goodMid: number; for(const mid of history.history) { const message = appMessagesManager.getMessageByPeer(chat.peerId, mid); - const good = this.myId == chat.peerId ? message.fromId == this.myId : message.pFlags.out; + const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out; if(good) { - if(appMessagesManager.canEditMessage(this.chat.getMessage(mid), 'text')) { + if(appMessagesManager.canEditMessage(chat.getMessage(mid), 'text')) { goodMid = mid; + break; } - break; + // * this check will allow editing only last message + //break; } } @@ -246,7 +225,7 @@ export class AppImManager { } } - if(chat.input.messageInput && e.target != chat.input.messageInput && target.tagName != 'INPUT' && !target.hasAttribute('contenteditable')) { + if(chat.input.messageInput && e.target !== chat.input.messageInput && target.tagName !== 'INPUT' && !target.hasAttribute('contenteditable')) { chat.input.messageInput.focus(); placeCaretAtEnd(chat.input.messageInput); } @@ -426,11 +405,11 @@ export class AppImManager { }; public selectTab(id: number) { - document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id == 0); + document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id === 0); const prevTabId = this.tabId; this.tabId = id; - if(mediaSizes.isMobile && prevTabId == 2 && id == 1) { + if(mediaSizes.isMobile && prevTabId === 2 && id === 1) { //appSidebarRight.toggleSidebar(false); document.body.classList.remove(RIGHT_COLUMN_ACTIVE_CLASSNAME); } @@ -469,7 +448,7 @@ export class AppImManager { if(justReturn) { rootScope.broadcast('peer_changed', this.chat.peerId); - if(appSidebarRight.historyTabIds[appSidebarRight.historyTabIds.length - 1] == AppSidebarRight.SLIDERITEMSIDS.search) { + if(appSidebarRight.historyTabIds[appSidebarRight.historyTabIds.length - 1] === AppSidebarRight.SLIDERITEMSIDS.search) { appSidebarRight.searchTab.closeBtn?.click(); } @@ -516,7 +495,7 @@ export class AppImManager { return; } - } else if(chatIndex > 0 && chat.peerId && chat.peerId != peerId) { + } else if(chatIndex > 0 && chat.peerId && chat.peerId !== peerId) { this.spliceChats(1, false); return this.setPeer(peerId, lastMsgId); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 920a5f36..705620c3 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2,6 +2,7 @@ import { LazyLoadQueueBase } from "../../components/lazyLoadQueue"; import ProgressivePreloader from "../../components/preloader"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { tsNow } from "../../helpers/date"; +import { createPosterForVideo } from "../../helpers/files"; import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols } from "../../helpers/string"; @@ -361,7 +362,7 @@ export class AppMessagesManager { }); } - public getInputEntities(entities: any) { + public getInputEntities(entities: MessageEntity[]) { var sendEntites = copy(entities); sendEntites.forEach((entity: any) => { if(entity._ == 'messageEntityMentionName') { @@ -615,6 +616,8 @@ export class AppMessagesManager { width: number, height: number, objectURL: string, + thumbBlob: Blob, + thumbURL: string, duration: number, background: true, silent: true, @@ -765,7 +768,31 @@ export class AppMessagesManager { size: file.size, url: options.objectURL }); + } else if(attachType === 'video') { + if(options.thumbURL) { + thumbs.push({ + _: 'photoSize', + w: options.width, + h: options.height, + type: 'full', + location: null, + size: options.thumbBlob.size, + url: options.thumbURL + }); + } + + const thumb = thumbs[0] as PhotoSize.photoSize; + const docThumb = appPhotosManager.getDocumentCachedThumb(document.id); + docThumb.downloaded = thumb.size; + docThumb.url = thumb.url; } + + /* if(thumbs.length) { + const thumb = thumbs[0] as PhotoSize.photoSize; + const docThumb = appPhotosManager.getDocumentCachedThumb(document.id); + docThumb.downloaded = thumb.size; + docThumb.url = thumb.url; + } */ appDocsManager.saveDoc(document); } @@ -775,17 +802,11 @@ export class AppMessagesManager { const preloader = new ProgressivePreloader(null, true, false, 'prepend'); const media = { - _: 'messageMediaPending', - type: options.isGroupedItem && options.isMedia ? 'album' : attachType, - file_name: fileName || apiFileName, - size: file.size, - file, + _: photo ? 'messageMediaPhoto' : 'messageMediaDocument', + pFlags: {}, preloader, photo, - document, - w: options.width, - h: options.height, - url: options.objectURL + document }; const message: any = { @@ -844,9 +865,25 @@ export class AppMessagesManager { uploadPromise = appDownloadManager.upload(file); preloader.attachPromise(uploadPromise); } + + let thumbUploadPromise: typeof uploadPromise; + if(attachType === 'video' && options.objectURL) { + thumbUploadPromise = new Promise((resolve, reject) => { + const blobPromise = options.thumbBlob ? Promise.resolve(options.thumbBlob) : createPosterForVideo(options.objectURL); + blobPromise.then(blob => { + if(!blob) { + resolve(null); + } else { + appDownloadManager.upload(blob).then(resolve, reject); + } + }, reject); + }); + } - uploadPromise && uploadPromise.then((inputFile) => { + uploadPromise && uploadPromise.then(async(inputFile) => { this.log('appMessagesManager: sendFile uploaded:', inputFile); + + delete message.media.preloader; inputFile.name = apiFileName; uploaded = true; @@ -867,7 +904,16 @@ export class AppMessagesManager { attributes }; } - + + if(thumbUploadPromise) { + try { + const inputFile = await thumbUploadPromise; + (inputMedia as InputMedia.inputMediaUploadedDocument).thumb = inputFile; + } catch(err) { + this.log.error('sendFile thumb upload error:', err); + } + } + sentDeferred.resolve(inputMedia); }, (/* error */) => { toggleError(true); @@ -952,6 +998,8 @@ export class AppMessagesManager { width: number, height: number, objectURL: string, + thumbBlob: Blob, + thumbURL: string }>[], silent: true, scheduleDate: number @@ -1045,7 +1093,7 @@ export class AppMessagesManager { }); }; - const promises: Promise[] = messages.map(message => { + const promises: Promise[] = messages.map((message, idx) => { return (message.send() as Promise).then((inputMedia: InputMedia) => { return apiManager.invokeApi('messages.uploadMedia', { peer: inputPeer, @@ -1078,13 +1126,18 @@ export class AppMessagesManager { return inputSingleMedia; }).catch((err: any) => { + if(err.name === 'AbortError') { + return null; + } + + this.log.error('sendAlbum upload item error:', err, message); toggleError(message, true); throw err; }); }); Promise.all(promises).then(inputs => { - invoke(inputs); + invoke(inputs.filter(Boolean)); }); } @@ -2251,47 +2304,50 @@ export class AppMessagesManager { if(message.grouped_id) { text = this.getAlbumText(message.grouped_id).message; messageText += 'Album' + (text ? ', ' : '') + ''; - } else switch(message.media._) { - case 'messageMediaPhoto': - messageText += 'Photo' + (message.message ? ', ' : '') + ''; - break; - case 'messageMediaDice': - messageText += RichTextProcessor.wrapEmojiText(message.media.emoticon); - break; - case 'messageMediaGeo': - messageText += 'Geolocation'; - break; - case 'messageMediaPoll': - messageText += '' + message.media.poll.rReply + ''; - break; - case 'messageMediaContact': - messageText += 'Contact'; - break; - case 'messageMediaDocument': - let document = message.media.document; - - if(document.type == 'video') { - messageText = 'Video' + (message.message ? ', ' : '') + ''; - } else if(document.type == 'voice') { - messageText = 'Voice message'; - } else if(document.type == 'gif') { - messageText = 'GIF' + (message.message ? ', ' : '') + ''; - } else if(document.type == 'round') { - messageText = 'Video message' + (message.message ? ', ' : '') + ''; - } else if(document.type == 'sticker') { - messageText = (document.stickerEmoji || '') + 'Sticker'; - text = ''; - } else { - messageText = '' + document.file_name + (message.message ? ', ' : '') + ''; - } - - break; - - default: - //messageText += message.media._; - ///////this.log.warn('Got unknown message.media type!', message); - break; - } + } else { + const media = message.media; + switch(media._) { + case 'messageMediaPhoto': + messageText += 'Photo' + (message.message ? ', ' : '') + ''; + break; + case 'messageMediaDice': + messageText += RichTextProcessor.wrapEmojiText(media.emoticon); + break; + case 'messageMediaGeo': + messageText += 'Geolocation'; + break; + case 'messageMediaPoll': + messageText += '' + media.poll.rReply + ''; + break; + case 'messageMediaContact': + messageText += 'Contact'; + break; + case 'messageMediaDocument': + let document = media.document; + + if(document.type == 'video') { + messageText = 'Video' + (message.message ? ', ' : '') + ''; + } else if(document.type == 'voice') { + messageText = 'Voice message'; + } else if(document.type == 'gif') { + messageText = 'GIF' + (message.message ? ', ' : '') + ''; + } else if(document.type == 'round') { + messageText = 'Video message' + (message.message ? ', ' : '') + ''; + } else if(document.type == 'sticker') { + messageText = (document.stickerEmoji || '') + 'Sticker'; + text = ''; + } else { + messageText = '' + document.file_name + (message.message ? ', ' : '') + ''; + } + + break; + + default: + //messageText += media._; + ///////this.log.warn('Got unknown media type!', message); + break; + } + } } if(message.action) { @@ -2482,8 +2538,7 @@ export class AppMessagesManager { const goodMedias = [ 'messageMediaPhoto', 'messageMediaDocument', - 'messageMediaWebPage', - 'messageMediaPending' + 'messageMediaWebPage' ]; if(kind == 'poll') { diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 9713e605..7593b15a 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -110,7 +110,7 @@ export class ApiFileManager { data.deferred.resolve(result); }, (error: Error) => { // @ts-ignore - if(!error.type || (error.type !== 'DOWNLOAD_CANCELED' && error.type !== 'UPLOAD_CANCELED')) { + if(!error || !error.type || (error.type !== 'DOWNLOAD_CANCELED' && error.type !== 'UPLOAD_CANCELED')) { this.log.error('downloadCheck error:', error); }