From eda9f5a306f4e598b693b73db782dff6653f08ca Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 10 Dec 2020 01:58:20 +0200 Subject: [PATCH] Send multiple files Refactor sendAlbum --- src/components/chat/bubbles.ts | 113 ++--- src/components/popupNewMedia.ts | 167 ++++-- src/components/wrappers.ts | 81 ++- src/lib/appManagers/appMessagesManager.ts | 507 ++++++------------- src/scss/partials/popups/_mediaAttacher.scss | 21 +- 5 files changed, 414 insertions(+), 475 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 0450a12d..d7217698 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -30,7 +30,7 @@ import AudioElement from "../audio"; import AvatarElement from "../avatar"; import { formatPhoneNumber } from "../misc"; import { ripple } from "../ripple"; -import { wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, wrapReply } from "../wrappers"; +import { wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, wrapReply, wrapGroupedDocuments } from "../wrappers"; import { MessageRender } from "./messageRender"; import LazyLoadQueue from "../lazyLoadQueue"; import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; @@ -196,20 +196,18 @@ export default class ChatBubbles { const message = this.appMessagesManager.getMessage(mid); appSidebarRight.sharedMediaTab.renderNewMessages(message.peerID, [mid]); - - const bubble = this.bubbles[tempID]; - if(bubble) { - this.bubbles[mid] = bubble; + + const mounted = this.getMountedBubble(tempID) || this.getMountedBubble(mid); + if(mounted) { + const bubble = mounted.bubble; + //this.bubbles[mid] = bubble; /////this.log('message_sent', bubble); // set new mids to album items for mediaViewer if(message.grouped_id) { - const items = bubble.querySelectorAll('.grouped-item'); - const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); - (Array.from(items) as HTMLElement[]).forEach((item, idx) => { - item.dataset.mid = '' + groupIDs[idx]; - }); + const item = bubble.querySelector(`.grouped-item[data-mid="${tempID}"]`) as HTMLElement; + item.dataset.mid = '' + mid; } if(message.media?.poll) { @@ -222,16 +220,16 @@ export default class ChatBubbles { } if(['audio', 'voice'].includes(message.media?.document?.type)) { - const audio = bubble.querySelector('audio-element'); + const audio = bubble.querySelector(`audio-element[message-id="${tempID}"]`); audio.setAttribute('doc-id', message.media.document.id); audio.setAttribute('message-id', '' + mid); } - bubble.classList.remove('is-sending'); + /* bubble.classList.remove('is-sending'); bubble.classList.add('is-sent'); bubble.dataset.mid = '' + mid; - this.bubbleGroups.removeBubble(bubble, tempID); + this.bubbleGroups.removeBubble(bubble, tempID); */ if(message.media?.webpage && !bubble.querySelector('.web')) { const mounted = this.getMountedBubble(mid); @@ -239,11 +237,23 @@ export default class ChatBubbles { this.renderMessage(mounted.message, true, false, mounted.bubble, false); } - delete this.bubbles[tempID]; + //delete this.bubbles[tempID]; } else { this.log.warn('message_sent there is no bubble', e.detail); } + if(this.bubbles[tempID]) { + const bubble = this.bubbles[tempID]; + this.bubbles[mid] = bubble; + delete this.bubbles[tempID]; + + bubble.classList.remove('is-sending'); + bubble.classList.add('is-sent'); + bubble.dataset.mid = '' + mid; + + this.bubbleGroups.removeBubble(bubble, tempID); + } + if(this.unreadOut.has(tempID)) { this.unreadOut.delete(tempID); this.unreadOut.add(mid); @@ -1360,7 +1370,7 @@ export default class ChatBubbles { let messageMedia = message.media; let messageMessage: string, totalEntities: any[]; - if(messageMedia?.document && !messageMedia.document.type) { + if(messageMedia?.document && messageMedia.document.type !== 'video') { // * just filter this case } else if(message.grouped_id && albumMustBeRenderedFull) { const t = this.appMessagesManager.getAlbumText(message.grouped_id); @@ -1575,25 +1585,23 @@ export default class ChatBubbles { case 'audio': case 'voice': case 'document': { - const doc = this.appDocsManager.getDoc(message.id); - //if(doc._ == 'documentEmpty') break; - this.log('will wrap pending doc:', doc); - const docDiv = wrapDocument(doc, false, true, message.id); - - if(doc.type == 'audio' || doc.type == 'voice') { - (docDiv as AudioElement).preloader = preloader; - } else { - const icoDiv = docDiv.querySelector('.audio-download, .document-ico'); - preloader.attach(icoDiv, false); + const newNameContainer = wrapGroupedDocuments({ + albumMustBeRenderedFull, + message, + bubble, + messageDiv + }); + + if(newNameContainer) { + nameContainer = newNameContainer; } - + if(pending.type == 'voice') { bubble.classList.add('bubble-audio'); } bubble.classList.remove('is-message-empty'); messageDiv.classList.add((pending.type || 'document') + '-message'); - messageDiv.append(docDiv); processingWebPage = true; break; } @@ -1805,52 +1813,15 @@ export default class ChatBubbles { break; } else { - //const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; - //const isFullAlbum = storage && Object.keys(storage).length != 1; - const mids = albumMustBeRenderedFull ? this.appMessagesManager.getMidsByMid(message.mid) : [message.mid]; - mids.forEach((mid, idx) => { - const message = this.appMessagesManager.getMessage(mid); - const doc = message.media.document; - const div = wrapDocument(doc, false, false, mid); - - const container = document.createElement('div'); - container.classList.add('document-container'); - container.dataset.mid = '' + mid; - - const wrapper = document.createElement('div'); - wrapper.classList.add('document-wrapper'); - - if(message.message) { - const messageDiv = document.createElement('div'); - messageDiv.classList.add('document-message'); - - const richText = RichTextProcessor.wrapRichText(message.message, { - entities: message.totalEntities - }); - - messageDiv.innerHTML = richText; - wrapper.append(messageDiv); - } - - if(mids.length > 1) { - const selection = document.createElement('div'); - selection.classList.add('document-selection'); - container.append(selection); - - container.classList.add('grouped-item'); - - if(idx === 0) { - nameContainer = wrapper; - } - } - - wrapper.append(div); - container.append(wrapper); - messageDiv.append(container); + const newNameContainer = wrapGroupedDocuments({ + albumMustBeRenderedFull, + message, + bubble, + messageDiv }); - if(mids.length > 1) { - bubble.classList.add('is-multiple-documents', 'is-grouped'); + if(newNameContainer) { + nameContainer = newNameContainer; } bubble.classList.remove('is-message-empty'); diff --git a/src/components/popupNewMedia.ts b/src/components/popupNewMedia.ts index 673814ec..25ed01dd 100644 --- a/src/components/popupNewMedia.ts +++ b/src/components/popupNewMedia.ts @@ -8,6 +8,7 @@ import { ripple } from "./ripple"; import Scrollable from "./scrollable"; import { toast } from "./toast"; import { prepareAlbum, wrapDocument } from "./wrappers"; +import CheckboxField from "./checkbox"; type SendFileParams = Partial<{ file: File, @@ -25,13 +26,16 @@ export default class PopupNewMedia extends PopupElement { private btnSend: HTMLElement; private input: HTMLInputElement; private mediaContainer: HTMLElement; + private groupCheckboxField: { label: HTMLLabelElement; input: HTMLInputElement; span: HTMLSpanElement; }; private willAttach: Partial<{ type: 'media' | 'document', - isMedia: boolean, + isMedia: true, + group: boolean, sendFileDetails: SendFileParams[] }> = { - sendFileDetails: [] + sendFileDetails: [], + group: false }; constructor(files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { @@ -60,7 +64,29 @@ export default class PopupNewMedia extends PopupElement { showLengthOn: 80 }); this.input = inputField.input; - this.container.append(scrollable.container, inputField.container); + + this.container.append(scrollable.container); + + if(files.length > 1) { + this.groupCheckboxField = CheckboxField('Group items', 'group-items'); + this.container.append(this.groupCheckboxField.label, inputField.container); + + this.groupCheckboxField.input.checked = true; + this.willAttach.group = true; + + this.groupCheckboxField.input.addEventListener('change', () => { + const checked = this.groupCheckboxField.input.checked; + + this.willAttach.group = checked; + this.willAttach.sendFileDetails.length = 0; + + //this.mediaContainer.innerHTML = ''; + //this.container.classList.remove('is-media', 'is-document', 'is-album'); + this.attachFiles(files); + }); + } + + this.container.append(inputField.container); this.attachFiles(files); } @@ -85,18 +111,35 @@ export default class PopupNewMedia extends PopupElement { this.destroy(); const willAttach = this.willAttach; - willAttach.isMedia = willAttach.type == 'media'; + willAttach.isMedia = willAttach.type == 'media' ? true : undefined; //console.log('will send files with options:', willAttach); const peerID = appImManager.chat.peerID; const chatInputC = appImManager.chat.input; - if(willAttach.sendFileDetails.length > 1 && willAttach.isMedia) { - appMessagesManager.sendAlbum(peerID, willAttach.sendFileDetails.map(d => d.file), Object.assign({ - caption, - replyToMsgID: chatInputC.replyToMsgID - }, willAttach)); + if(willAttach.sendFileDetails.length > 1 && willAttach.group) { + for(let i = 0; i < willAttach.sendFileDetails.length;) { + let firstType = willAttach.sendFileDetails[i].file.type.split('/')[0]; + for(var k = 0; k < 10 && i < willAttach.sendFileDetails.length; ++i, ++k) { + const type = willAttach.sendFileDetails[i].file.type.split('/')[0]; + if(firstType != type) { + break; + } + } + + const w = {...willAttach}; + w.sendFileDetails = willAttach.sendFileDetails.slice(i - k, i); + + appMessagesManager.sendAlbum(peerID, w.sendFileDetails.map(d => d.file), Object.assign({ + caption, + replyToMsgID: chatInputC.replyToMsgID, + isMedia: willAttach.isMedia + }, w)); + + caption = undefined; + chatInputC.replyToMsgID = 0; + } } else { if(caption) { if(willAttach.sendFileDetails.length > 1) { @@ -109,7 +152,7 @@ export default class PopupNewMedia extends PopupElement { const promises = willAttach.sendFileDetails.map(params => { const promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({ //isMedia: willAttach.isMedia, - isMedia: params.file.type.includes('audio/') || willAttach.isMedia, + isMedia: willAttach.isMedia, caption, replyToMsgID: chatInputC.replyToMsgID }, params)); @@ -176,7 +219,8 @@ export default class PopupNewMedia extends PopupElement { case 'document': { const isPhoto = file.type.indexOf('image/') !== -1; - if(isPhoto) { + const isAudio = file.type.indexOf('audio/') !== -1; + if(isPhoto || isAudio) { params.objectURL = URL.createObjectURL(file); } @@ -220,9 +264,9 @@ export default class PopupNewMedia extends PopupElement { const container = this.container; const willAttach = this.willAttach; - if(files.length > 10 && willAttach.type == 'media') { + /* if(files.length > 10 && willAttach.type == 'media') { willAttach.type = 'document'; - } + } */ files = files.filter(file => { if(willAttach.type == 'media') { @@ -232,64 +276,77 @@ export default class PopupNewMedia extends PopupElement { } }); - if(files.length) { - if(willAttach.type == 'document') { - this.title.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File'); - container.classList.add('is-document'); - } else { - container.classList.add('is-media'); - - let foundPhotos = 0; - let foundVideos = 0; - files.forEach(file => { - if(file.type.indexOf('image/') === 0) ++foundPhotos; - else if(file.type.indexOf('video/') === 0) ++foundVideos; - }); - - if(foundPhotos && foundVideos) { - this.title.innerText = 'Send Album'; - } else if(foundPhotos) { - this.title.innerText = 'Send ' + (foundPhotos > 1 ? foundPhotos + ' Photos' : 'Photo'); - } else if(foundVideos) { - this.title.innerText = 'Send ' + (foundVideos > 1 ? foundVideos + ' Videos' : 'Video'); + Promise.all(files.map(this.attachFile)).then(results => { + this.container.classList.remove('is-media', 'is-document', 'is-album'); + this.mediaContainer.innerHTML = ''; + + if(files.length) { + if(willAttach.type == 'document') { + this.title.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File'); + container.classList.add('is-document'); + } else { + container.classList.add('is-media'); + + let foundPhotos = 0; + let foundVideos = 0; + files.forEach(file => { + if(file.type.indexOf('image/') === 0) ++foundPhotos; + else if(file.type.indexOf('video/') === 0) ++foundVideos; + }); + + if(foundPhotos && foundVideos && willAttach.group) { + this.title.innerText = 'Send Album'; + } else if(foundPhotos) { + this.title.innerText = 'Send ' + (foundPhotos > 1 ? foundPhotos + ' Photos' : 'Photo'); + } else if(foundVideos) { + this.title.innerText = 'Send ' + (foundVideos > 1 ? foundVideos + ' Videos' : 'Video'); + } } } - } - Promise.all(files.map(this.attachFile)).then(results => { if(willAttach.type == 'media') { - if(willAttach.sendFileDetails.length > 1) { + if(willAttach.sendFileDetails.length > 1 && willAttach.group) { container.classList.add('is-album'); - this.mediaContainer.append(...results); + for(let i = 0; i < results.length; i += 10) { + const albumContainer = document.createElement('div'); + albumContainer.classList.add('popup-album'); - prepareAlbum({ - container: this.mediaContainer, - items: willAttach.sendFileDetails.map(o => ({w: o.width, h: o.height})), - maxWidth: 380, - minWidth: 100, - spacing: 4 - }); + albumContainer.append(...results.slice(i, i + 10)); + prepareAlbum({ + container: albumContainer, + items: willAttach.sendFileDetails.slice(i, i + 10).map(o => ({w: o.width, h: o.height})), + maxWidth: 380, + minWidth: 100, + spacing: 4 + }); + + this.mediaContainer.append(albumContainer); + } //console.log('chatInput album layout:', layout); } else { - const params = willAttach.sendFileDetails[0]; - const div = results[0]; - const {w, h} = calcImageInBox(params.width, params.height, 380, 320); - div.style.width = w + 'px'; - div.style.height = h + 'px'; - this.mediaContainer.append(div); + for(let i = 0; i < results.length; ++i) { + const params = willAttach.sendFileDetails[i]; + const div = results[i]; + const {w, h} = calcImageInBox(params.width, params.height, 380, 320); + div.style.width = w + 'px'; + div.style.height = h + 'px'; + this.mediaContainer.append(div); + } } } else { this.mediaContainer.append(...results); } // show now - document.body.addEventListener('keydown', this.onKeyDown); - this.onClose = () => { - document.body.removeEventListener('keydown', this.onKeyDown); - }; - this.show(); + if(!this.element.classList.contains('active')) { + document.body.addEventListener('keydown', this.onKeyDown); + this.onClose = () => { + document.body.removeEventListener('keydown', this.onKeyDown); + }; + this.show(); + } }); } } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 6ab15dd4..ada7b306 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -25,6 +25,7 @@ import PollElement from './poll'; import ProgressivePreloader from './preloader'; import './middleEllipsis'; import { nextRandomInt } from '../helpers/random'; +import RichTextProcessor from '../lib/richtextprocessor'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB @@ -337,7 +338,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals const icoDiv = document.createElement('div'); icoDiv.classList.add('document-ico'); - if(doc.thumbs?.length || (uploading && doc.url)) { + if(doc.thumbs?.length || (uploading && doc.url && doc.type == 'photo')) { docDiv.classList.add('document-with-thumb'); if(uploading) { @@ -858,7 +859,7 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo }) { const items: {size: PhotoSize.photoSize, media: any, message: any}[] = []; - // !higher msgID will be the FIRST in album + // !lowest msgID will be the FIRST in album const storage = appMessagesManager.getMidsByAlbum(groupID); for(const mid of storage) { const m = appMessagesManager.getMessage(mid); @@ -868,6 +869,11 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo items.push({size, media, message: m}); } + // * pending + if(storage[0] < 0) { + items.reverse(); + } + prepareAlbum({ container: attachmentDiv, items: items.map(i => ({w: i.size.w, h: i.size.h})), @@ -912,6 +918,77 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo }); } +export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv}: { + albumMustBeRenderedFull: boolean, + message: any, + messageDiv: HTMLElement, + bubble: HTMLElement, + uploading?: boolean +}) { + let nameContainer: HTMLDivElement; + const mids = albumMustBeRenderedFull ? appMessagesManager.getMidsByMid(message.mid) : [message.mid]; + const isPending = message.mid < 0; + if(isPending) { + mids.reverse(); + } + + mids.forEach((mid, idx) => { + const message = appMessagesManager.getMessage(mid); + const doc = message.media.document; + const div = wrapDocument(doc, false, isPending, mid); + + const container = document.createElement('div'); + container.classList.add('document-container'); + container.dataset.mid = '' + mid; + + const wrapper = document.createElement('div'); + wrapper.classList.add('document-wrapper'); + + if(message.message) { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('document-message'); + + const richText = RichTextProcessor.wrapRichText(message.message, { + entities: message.totalEntities + }); + + messageDiv.innerHTML = richText; + wrapper.append(messageDiv); + } + + if(mids.length > 1) { + const selection = document.createElement('div'); + selection.classList.add('document-selection'); + container.append(selection); + + container.classList.add('grouped-item'); + + if(idx === 0) { + nameContainer = wrapper; + } + } + + if(isPending) { + if(doc.type == 'audio' || doc.type == 'voice') { + (div as AudioElement).preloader = message.media.preloader; + } else { + const icoDiv = div.querySelector('.audio-download, .document-ico'); + message.media.preloader.attach(icoDiv, false); + } + } + + wrapper.append(div); + container.append(wrapper); + messageDiv.append(container); + }); + + if(mids.length > 1) { + bubble.classList.add('is-multiple-documents', 'is-grouped'); + } + + return nameContainer; +} + export function wrapPoll(pollID: string, mid: number) { const elem = new PollElement(); elem.setAttribute('poll-id', pollID); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 5575d7b1..495486ee 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -4,7 +4,7 @@ import { tsNow } from "../../helpers/date"; import { copy, defineNotNumerableProperties, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols } from "../../helpers/string"; -import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMessage, InputNotifyPeer, InputPeerNotifySettings, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; +import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputNotifyPeer, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; import { InvokeApiOptions } from "../../types"; import { langPack } from "../langPack"; import { logger, LogLevels } from "../logger"; @@ -569,31 +569,32 @@ export class AppMessagesManager { } public sendFile(peerID: number, file: File | Blob | MyDocument, options: Partial<{ - isMedia: boolean, + isRoundMessage: true, + isVoiceMessage: true, + isGroupedItem: true, + isMedia: true, + replyToMsgID: number, caption: string, - entities: any[], + entities: MessageEntity[], width: number, height: number, objectURL: string, - isRoundMessage: boolean, duration: number, - background: boolean, + background: true, - isVoiceMessage: boolean, waveform: Uint8Array }> = {}) { peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; - var messageID = this.tempID--; - var randomIDS = randomLong(); - var historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []}); - var flags = 0; - var pFlags: any = {}; - var replyToMsgID = options.replyToMsgID; - var isChannel = appPeersManager.isChannel(peerID); - var isMegagroup = isChannel && appPeersManager.isMegagroup(peerID); - var asChannel = isChannel && !isMegagroup ? true : false; - var attachType: string, apiFileName: string; + const messageID = this.tempID--; + const randomIDS = randomLong(); + const historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []}); + const pFlags: any = {}; + const replyToMsgID = options.replyToMsgID; + const isChannel = appPeersManager.isChannel(peerID); + const isMegagroup = isChannel && appPeersManager.isMegagroup(peerID); + const asChannel = !!(isChannel && !isMegagroup); + let attachType: string, apiFileName: string; const fileType = 'mime_type' in file ? file.mime_type : file.type; const fileName = file instanceof File ? file.name : ''; @@ -613,20 +614,42 @@ export class AppMessagesManager { const isPhoto = ['image/jpeg', 'image/png', 'image/bmp'].indexOf(fileType) >= 0; + let photo: MyPhoto, document: MyDocument; + let actionName = ''; - if(!options.isMedia) { + if(isDocument) { // maybe it's a sticker or gif + attachType = 'document'; + apiFileName = ''; + } else if(fileType.indexOf('audio/') === 0 || ['video/ogg'].indexOf(fileType) >= 0) { + attachType = 'audio'; + apiFileName = 'audio.' + (fileType.split('/')[1] == 'ogg' ? 'ogg' : 'mp3'); + actionName = 'sendMessageUploadAudioAction'; + + if(options.isVoiceMessage) { + attachType = 'voice'; + pFlags.media_unread = true; + } + + let attribute: DocumentAttribute.documentAttributeAudio = { + _: 'documentAttributeAudio', + pFlags: { + voice: options.isVoiceMessage + }, + waveform: options.waveform, + duration: options.duration || 0 + }; + + attributes.push(attribute); + } else if(!options.isMedia) { attachType = 'document'; apiFileName = 'document.' + fileType.split('/')[1]; actionName = 'sendMessageUploadDocumentAction'; - } else if(isDocument) { // maybe it's a sticker or gif - attachType = 'document'; - apiFileName = ''; } else if(isPhoto) { attachType = 'photo'; apiFileName = 'photo.' + fileType.split('/')[1]; actionName = 'sendMessageUploadPhotoAction'; - let photo: MyPhoto = { + photo = { _: 'photo', id: '' + messageID, sizes: [{ @@ -646,30 +669,6 @@ export class AppMessagesManager { photo.url = options.objectURL || ''; appPhotosManager.savePhoto(photo); - } else if(fileType.indexOf('audio/') === 0 || ['video/ogg'].indexOf(fileType) >= 0) { - attachType = 'audio'; - apiFileName = 'audio.' + (fileType.split('/')[1] == 'ogg' ? 'ogg' : 'mp3'); - actionName = 'sendMessageUploadAudioAction'; - - let flags = 0; - if(options.isVoiceMessage) { - flags |= 1 << 10; - flags |= 1 << 2; - attachType = 'voice'; - pFlags.media_unread = true; - } - - let attribute: DocumentAttribute.documentAttributeAudio = { - _: 'documentAttributeAudio', - flags: flags, - pFlags: { // that's only for client, not going to telegram - voice: options.isVoiceMessage || undefined - }, - waveform: options.waveform, - duration: options.duration || 0 - }; - - attributes.push(attribute); } else if(fileType.indexOf('video/') === 0) { attachType = 'video'; apiFileName = 'video.mp4'; @@ -677,9 +676,8 @@ export class AppMessagesManager { let videoAttribute: DocumentAttribute.documentAttributeVideo = { _: 'documentAttributeVideo', - pFlags: { // that's only for client, not going to telegram - supports_streaming: true, - round_message: options.isRoundMessage || undefined + pFlags: { + round_message: options.isRoundMessage }, duration: options.duration, w: options.width, @@ -697,7 +695,7 @@ export class AppMessagesManager { if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1 && !isDocument) { const thumbs: PhotoSize[] = []; - const doc: MyDocument = { + document = { _: 'document', id: '' + messageID, duration: options.duration, @@ -709,10 +707,10 @@ export class AppMessagesManager { size: file.size } as any; - defineNotNumerableProperties(doc, ['downloaded', 'url']); + defineNotNumerableProperties(document, ['downloaded', 'url']); // @ts-ignore - doc.downloaded = file.size; - doc.url = options.objectURL || ''; + document.downloaded = file.size; + document.url = options.objectURL || ''; if(isPhoto) { attributes.push({ @@ -732,42 +730,36 @@ export class AppMessagesManager { }); } - appDocsManager.saveDoc(doc); + appDocsManager.saveDoc(document); } this.log('AMM: sendFile', attachType, apiFileName, file.type, options); - var fromID = appUsersManager.getSelf().id; + let fromID = appUsersManager.getSelf().id; if(peerID != fromID) { - flags |= 2; pFlags.out = true; if(!isChannel && !appUsersManager.isBot(peerID)) { - flags |= 1; pFlags.unread = true; } } - if(replyToMsgID) { - flags |= 8; - } - if(asChannel) { fromID = 0; pFlags.post = true; - } else { - flags |= 256; } const preloader = new ProgressivePreloader(null, true, false, 'prepend'); const media = { _: 'messageMediaPending', - type: attachType, + type: options.isGroupedItem && options.isMedia ? 'album' : attachType, file_name: fileName || apiFileName, size: file.size, - file: file, - preloader: preloader, + file, + preloader, + photo, + document, w: options.width, h: options.height, url: options.objectURL @@ -778,8 +770,8 @@ export class AppMessagesManager { id: messageID, from_id: appPeersManager.getOutputPeer(fromID), peer_id: appPeersManager.getOutputPeer(peerID), - pFlags: pFlags, - date: date, + pFlags, + date, message: caption, media: isDocument ? { _: 'messageMediaDocument', @@ -805,59 +797,22 @@ export class AppMessagesManager { let uploaded = false, uploadPromise: ReturnType = null; - const invoke = (flags: number, inputMedia: any) => { - this.setTyping(peerID, 'sendMessageCancelAction'); - - return apiManager.invokeApi('messages.sendMedia', { - flags: flags, - background: options.background || undefined, - clear_draft: true, - peer: appPeersManager.getInputPeerByID(peerID), - media: inputMedia, - message: caption, - random_id: randomIDS, - reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID) - }).then((updates) => { - apiUpdatesManager.processUpdateMessage(updates); - }, (error) => { - if(attachType == 'photo' && - error.code == 400 && - (error.type == 'PHOTO_INVALID_DIMENSIONS' || - error.type == 'PHOTO_SAVE_FILE_INVALID')) { - error.handled = true - attachType = 'document' - message.send(); - return; - } - - toggleError(true); - }); - }; - + const sentDeferred = deferredPromise(); message.send = () => { - let flags = 0; - if(replyToMsgID) { - flags |= 1; - } - if(options.background) { - flags |= 64; - } - flags |= 128; // clear_draft - if(isDocument) { const {id, access_hash, file_reference} = file as MyDocument; - const inputMedia = { + const inputMedia: InputMedia = { _: 'inputMediaDocument', id: { _: 'inputDocument', - id: id, - access_hash: access_hash, - file_reference: file_reference + id, + access_hash, + file_reference } }; - invoke(flags, inputMedia); + sentDeferred.resolve(inputMedia); } else if(file instanceof File || file instanceof Blob) { const deferred = deferredPromise(); @@ -873,7 +828,7 @@ export class AppMessagesManager { inputFile.name = apiFileName; uploaded = true; - var inputMedia; + let inputMedia: InputMedia; switch(attachType) { case 'photo': inputMedia = { @@ -887,11 +842,11 @@ export class AppMessagesManager { _: 'inputMediaUploadedDocument', file: inputFile, mime_type: fileType, - attributes: attributes + attributes }; } - invoke(flags, inputMedia); + sentDeferred.resolve(inputMedia); }, (/* error */) => { toggleError(true); }); @@ -907,6 +862,7 @@ export class AppMessagesManager { this.log('cancelling upload', media); deferred.resolve(); + sentDeferred.reject(err); this.cancelPendingMessage(randomIDS); this.setTyping(peerID, 'sendMessageCancelAction'); } @@ -917,19 +873,52 @@ export class AppMessagesManager { this.sendFilePromise = deferred; } + + return sentDeferred; }; - this.saveMessages([message]); historyStorage.pending.unshift(messageID); - rootScope.broadcast('history_append', {peerID, messageID, my: true}); + this.pendingByRandomID[randomIDS] = [peerID, messageID]; - setTimeout(message.send.bind(this), 0); + if(!options.isGroupedItem) { + this.saveMessages([message]); + rootScope.broadcast('history_append', {peerID, messageID, my: true}); + setTimeout(message.send, 0); + sentDeferred.then(inputMedia => { + this.setTyping(peerID, 'sendMessageCancelAction'); + + return apiManager.invokeApi('messages.sendMedia', { + background: options.background, + clear_draft: true, + peer: appPeersManager.getInputPeerByID(peerID), + media: inputMedia, + message: caption, + random_id: randomIDS, + reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID) + }).then((updates) => { + apiUpdatesManager.processUpdateMessage(updates); + }, (error) => { + if(attachType == 'photo' && + error.code == 400 && + (error.type == 'PHOTO_INVALID_DIMENSIONS' || + error.type == 'PHOTO_SAVE_FILE_INVALID')) { + error.handled = true; + attachType = 'document'; + message.send(); + return; + } - this.pendingByRandomID[randomIDS] = [peerID, messageID]; + toggleError(true); + }); + }); + } + + return {message, promise: sentDeferred}; } public async sendAlbum(peerID: number, files: File[], options: Partial<{ - entities: any[], + isMedia: true, + entities: MessageEntity[], replyToMsgID: number, caption: string, sendFileDetails: Partial<{ @@ -939,146 +928,50 @@ export class AppMessagesManager { objectURL: string, }>[] }> = {}) { + if(files.length === 1) { + return this.sendFile(peerID, files[0], {...options, ...options.sendFileDetails[0]}); + } + peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; - let groupID: number; - let historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []}); - let flags = 0; - let pFlags: any = {}; - let replyToMsgID = options.replyToMsgID; - let isChannel = appPeersManager.isChannel(peerID); - let isMegagroup = isChannel && appPeersManager.isMegagroup(peerID); - let asChannel = isChannel && !isMegagroup ? true : false; + const replyToMsgID = options.replyToMsgID; let caption = options.caption || ''; - - let date = tsNow(true) + serverTimeManager.serverTimeOffset; - + let entities: MessageEntity[]; if(caption) { - let entities = options.entities || []; + entities = options.entities || []; caption = RichTextProcessor.parseMarkdown(caption, entities); } this.log('AMM: sendAlbum', files, options); - let fromID = appUsersManager.getSelf().id; - if(peerID != fromID) { - pFlags.out = true; - - if(!isChannel && !appUsersManager.isBot(peerID)) { - pFlags.unread = true; - } - } - - if(replyToMsgID) { - flags |= 1; - } - - if(asChannel) { - fromID = 0; - pFlags.post = true; - } else { - flags |= 128; // clear_draft - } - - let ids = files.map(() => this.tempID--).reverse(); - groupID = ids[ids.length - 1]; - let messages = files.map((file, idx) => { - //let messageID = this.tempID--; - //if(!groupID) groupID = messageID; - let messageID = ids[idx]; - let randomIDS = randomLong(); - let preloader = new ProgressivePreloader(null, true, false, 'prepend'); - - let details = options.sendFileDetails[idx]; - - let media = { - _: 'messageMediaPending', - type: 'album', - preloader: preloader, - document: undefined as any, - photo: undefined as any + const messages = files.map((file, idx) => { + const details = options.sendFileDetails[idx]; + const o: any = { + isGroupedItem: true, + isMedia: options.isMedia, + ...details }; - if(file.type.indexOf('video/') === 0) { - let videoAttribute: DocumentAttribute.documentAttributeVideo = { - _: 'documentAttributeVideo', - pFlags: { // that's only for client, not going to telegram - supports_streaming: true - }, - duration: details.duration, - w: details.width, - h: details.height - }; - - let doc: MyDocument = { - _: 'document', - id: '' + messageID, - attributes: [videoAttribute], - thumbs: [], - mime_type: file.type, - size: file.size - } as any; - - defineNotNumerableProperties(doc, ['downloaded', 'url']); - // @ts-ignore - doc.downloaded = file.size; - doc.url = details.objectURL || ''; - - appDocsManager.saveDoc(doc); - media.document = doc; - } else { - let photo: any = { - _: 'photo', - id: '' + messageID, - sizes: [{ - _: 'photoSize', - w: details.width, - h: details.height, - type: 'm', - size: file.size - } as PhotoSize], - w: details.width, - h: details.height - }; - - defineNotNumerableProperties(photo, ['downloaded', 'url']); - // @ts-ignore - photo.downloaded = file.size; - photo.url = details.objectURL || ''; - - appPhotosManager.savePhoto(photo); - media.photo = photo; + if(idx === 0) { + o.caption = caption; + o.entities = entities; + o.replyToMsgID = replyToMsgID; } - let message = { - _: 'message', - id: messageID, - from_id: appPeersManager.getOutputPeer(fromID), - grouped_id: groupID, - peer_id: appPeersManager.getOutputPeer(peerID), - pFlags: pFlags, - date: date, - message: caption, - media: media, - random_id: randomIDS, - reply_to: {reply_to_msg_id: replyToMsgID}, - views: asChannel && 1, - pending: true, - error: false - }; - - this.saveMessages([message]); - historyStorage.pending.unshift(messageID); - //rootScope.$broadcast('history_append', {peerID: peerID, messageID: messageID, my: true}); - - this.pendingByRandomID[randomIDS] = [peerID, messageID]; + return this.sendFile(peerID, file, o).message; + }); - return message; + const groupID = messages[0].id; + messages.forEach(message => { + message.grouped_id = groupID; }); + this.saveMessages(messages); - rootScope.broadcast('history_append', {peerID, messageID: messages[messages.length - 1].id, my: true}); + rootScope.broadcast('history_append', {peerID, messageID: groupID, my: true}); + + //return; - let toggleError = (message: any, on: boolean) => { + const toggleError = (message: any, on: boolean) => { if(on) { message.error = true; } else { @@ -1088,15 +981,11 @@ export class AppMessagesManager { rootScope.broadcast('messages_pending'); }; - let uploaded = false, - uploadPromise: ReturnType = null; - - let inputPeer = appPeersManager.getInputPeerByID(peerID); - let invoke = (multiMedia: any[]) => { + const inputPeer = appPeersManager.getInputPeerByID(peerID); + const invoke = (multiMedia: any[]) => { this.setTyping(peerID, 'sendMessageCancelAction'); return apiManager.invokeApi('messages.sendMultiMedia', { - flags: flags, peer: inputPeer, multi_media: multiMedia, reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID) @@ -1107,114 +996,42 @@ export class AppMessagesManager { }); }; - let inputs: any[] = []; - for(let i = 0, length = files.length; i < length; ++i) { - const file = files[i]; - const message = messages[i]; - const media = message.media; - const preloader = media.preloader; - const actionName = file.type.indexOf('video/') === 0 ? 'sendMessageUploadVideoAction' : 'sendMessageUploadPhotoAction'; - const deferred = deferredPromise(); - let canceled = false; - - let apiFileName: string; - if(file.type.indexOf('video/') === 0) { - apiFileName = 'video.mp4'; - } else { - apiFileName = 'photo.' + file.type.split('/')[1]; - } - - await this.sendFilePromise; - this.sendFilePromise = deferred; - - if(!uploaded || message.error) { - uploaded = false; - uploadPromise = appDownloadManager.upload(file); - preloader.attachPromise(uploadPromise); - } - - uploadPromise.addNotifyListener((progress: {done: number, total: number}) => { - this.log('upload progress', progress); - const percents = Math.max(1, Math.floor(100 * progress.done / progress.total)); - this.setTyping(peerID, {_: actionName, progress: percents | 0}); - }); - - uploadPromise.catch(err => { - if(err.name === 'AbortError' && !uploaded) { - this.log('cancelling upload item', media); - canceled = true; - } - }); - - await uploadPromise.then((inputFile) => { - this.log('appMessagesManager: sendAlbum file uploaded:', inputFile); - - if(canceled) { - return; - } - - inputFile.name = apiFileName; + const inputs: InputSingleMedia[] = []; + for(const message of messages) { + const inputMedia: InputMedia = await message.send(); + this.log('sendAlbum uploaded item:', inputMedia); + await apiManager.invokeApi('messages.uploadMedia', { + peer: inputPeer, + media: inputMedia + }).then(messageMedia => { let inputMedia: any; - let details = options.sendFileDetails[i]; - if(details.duration) { - inputMedia = { - _: 'inputMediaUploadedDocument', - file: inputFile, - mime_type: file.type, - attributes: [{ - _: 'documentAttributeVideo', - supports_streaming: true, - duration: details.duration, - w: details.width, - h: details.height - }] - }; - } else { - inputMedia = { - _: 'inputMediaUploadedPhoto', - file: inputFile - }; + if(messageMedia._ == 'messageMediaPhoto') { + const photo = appPhotosManager.savePhoto(messageMedia.photo); + inputMedia = appPhotosManager.getInput(photo); + } else if(messageMedia._ == 'messageMediaDocument') { + const doc = appDocsManager.saveDoc(messageMedia.document); + inputMedia = appDocsManager.getMediaInput(doc); } - return apiManager.invokeApi('messages.uploadMedia', { - peer: inputPeer, - media: inputMedia - }).then(messageMedia => { - if(canceled) { - return; - } - - let inputMedia: any; - if(messageMedia._ == 'messageMediaPhoto') { - const photo = appPhotosManager.savePhoto(messageMedia.photo); - inputMedia = appPhotosManager.getInput(photo); - } else if(messageMedia._ == 'messageMediaDocument') { - const doc = appDocsManager.saveDoc(messageMedia.document); - inputMedia = appDocsManager.getMediaInput(doc); - } - - inputs.push({ - _: 'inputSingleMedia', - media: inputMedia, - random_id: message.random_id, - message: caption, - entities: [] - }); - - caption = ''; // only 1 caption for all inputs - }, () => { - toggleError(message, true); + inputs.push({ + _: 'inputSingleMedia', + media: inputMedia, + random_id: message.random_id, + message: caption, + entities }); + + // * only 1 caption for all inputs + if(caption) { + caption = ''; + entities = []; + } }, () => { toggleError(message, true); }); - - this.log('appMessagesManager: sendAlbum uploadPromise.finally!'); - deferred.resolve(); } - uploaded = true; invoke(inputs); } diff --git a/src/scss/partials/popups/_mediaAttacher.scss b/src/scss/partials/popups/_mediaAttacher.scss index 4ec0cc3a..45803ccd 100644 --- a/src/scss/partials/popups/_mediaAttacher.scss +++ b/src/scss/partials/popups/_mediaAttacher.scss @@ -14,7 +14,7 @@ /* max-height: 425px; */ #{$parent}-photo { - max-height: 320px; + //max-height: 320px; margin: 0 auto; img { @@ -24,6 +24,7 @@ > div { display: flex; justify-content: center; + margin: 0 auto; } } } @@ -33,7 +34,7 @@ margin: 0 auto; position: relative; - > div { + .album-item { position: absolute; } @@ -87,6 +88,7 @@ //justify-content: center; width: fit-content; border-radius: $border-radius-medium; + user-select: none; /* align-items: center; */ .document { @@ -152,4 +154,19 @@ .popup-header { padding: 0; } + + .checkbox-field { + margin-bottom: 0; + padding-left: 0; + } + + .popup-album, .popup-container:not(.is-album) .popup-item-media { + position: relative; + border-radius: inherit; + overflow: hidden; + } + + .popup-album + .popup-album, .popup-container:not(.is-album) .popup-item-media + .popup-item-media { + margin-top: .5rem; + } } \ No newline at end of file