Browse Source

Send multiple files

Refactor sendAlbum
master
Eduard Kuzmenko 4 years ago
parent
commit
eda9f5a306
  1. 113
      src/components/chat/bubbles.ts
  2. 167
      src/components/popupNewMedia.ts
  3. 81
      src/components/wrappers.ts
  4. 507
      src/lib/appManagers/appMessagesManager.ts
  5. 21
      src/scss/partials/popups/_mediaAttacher.scss

113
src/components/chat/bubbles.ts

@ -30,7 +30,7 @@ import AudioElement from "../audio";
import AvatarElement from "../avatar"; import AvatarElement from "../avatar";
import { formatPhoneNumber } from "../misc"; import { formatPhoneNumber } from "../misc";
import { ripple } from "../ripple"; 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 { MessageRender } from "./messageRender";
import LazyLoadQueue from "../lazyLoadQueue"; import LazyLoadQueue from "../lazyLoadQueue";
import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import { AppChatsManager } from "../../lib/appManagers/appChatsManager";
@ -196,20 +196,18 @@ export default class ChatBubbles {
const message = this.appMessagesManager.getMessage(mid); const message = this.appMessagesManager.getMessage(mid);
appSidebarRight.sharedMediaTab.renderNewMessages(message.peerID, [mid]); appSidebarRight.sharedMediaTab.renderNewMessages(message.peerID, [mid]);
const bubble = this.bubbles[tempID]; const mounted = this.getMountedBubble(tempID) || this.getMountedBubble(mid);
if(bubble) { if(mounted) {
this.bubbles[mid] = bubble; const bubble = mounted.bubble;
//this.bubbles[mid] = bubble;
/////this.log('message_sent', bubble); /////this.log('message_sent', bubble);
// set new mids to album items for mediaViewer // set new mids to album items for mediaViewer
if(message.grouped_id) { if(message.grouped_id) {
const items = bubble.querySelectorAll('.grouped-item'); const item = bubble.querySelector(`.grouped-item[data-mid="${tempID}"]`) as HTMLElement;
const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); item.dataset.mid = '' + mid;
(Array.from(items) as HTMLElement[]).forEach((item, idx) => {
item.dataset.mid = '' + groupIDs[idx];
});
} }
if(message.media?.poll) { if(message.media?.poll) {
@ -222,16 +220,16 @@ export default class ChatBubbles {
} }
if(['audio', 'voice'].includes(message.media?.document?.type)) { 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('doc-id', message.media.document.id);
audio.setAttribute('message-id', '' + mid); audio.setAttribute('message-id', '' + mid);
} }
bubble.classList.remove('is-sending'); /* bubble.classList.remove('is-sending');
bubble.classList.add('is-sent'); bubble.classList.add('is-sent');
bubble.dataset.mid = '' + mid; bubble.dataset.mid = '' + mid;
this.bubbleGroups.removeBubble(bubble, tempID); this.bubbleGroups.removeBubble(bubble, tempID); */
if(message.media?.webpage && !bubble.querySelector('.web')) { if(message.media?.webpage && !bubble.querySelector('.web')) {
const mounted = this.getMountedBubble(mid); const mounted = this.getMountedBubble(mid);
@ -239,11 +237,23 @@ export default class ChatBubbles {
this.renderMessage(mounted.message, true, false, mounted.bubble, false); this.renderMessage(mounted.message, true, false, mounted.bubble, false);
} }
delete this.bubbles[tempID]; //delete this.bubbles[tempID];
} else { } else {
this.log.warn('message_sent there is no bubble', e.detail); 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)) { if(this.unreadOut.has(tempID)) {
this.unreadOut.delete(tempID); this.unreadOut.delete(tempID);
this.unreadOut.add(mid); this.unreadOut.add(mid);
@ -1360,7 +1370,7 @@ export default class ChatBubbles {
let messageMedia = message.media; let messageMedia = message.media;
let messageMessage: string, totalEntities: any[]; let messageMessage: string, totalEntities: any[];
if(messageMedia?.document && !messageMedia.document.type) { if(messageMedia?.document && messageMedia.document.type !== 'video') {
// * just filter this case // * just filter this case
} else if(message.grouped_id && albumMustBeRenderedFull) { } else if(message.grouped_id && albumMustBeRenderedFull) {
const t = this.appMessagesManager.getAlbumText(message.grouped_id); const t = this.appMessagesManager.getAlbumText(message.grouped_id);
@ -1575,25 +1585,23 @@ export default class ChatBubbles {
case 'audio': case 'audio':
case 'voice': case 'voice':
case 'document': { case 'document': {
const doc = this.appDocsManager.getDoc(message.id); const newNameContainer = wrapGroupedDocuments({
//if(doc._ == 'documentEmpty') break; albumMustBeRenderedFull,
this.log('will wrap pending doc:', doc); message,
const docDiv = wrapDocument(doc, false, true, message.id); bubble,
messageDiv
if(doc.type == 'audio' || doc.type == 'voice') { });
(docDiv as AudioElement).preloader = preloader;
} else { if(newNameContainer) {
const icoDiv = docDiv.querySelector('.audio-download, .document-ico'); nameContainer = newNameContainer;
preloader.attach(icoDiv, false);
} }
if(pending.type == 'voice') { if(pending.type == 'voice') {
bubble.classList.add('bubble-audio'); bubble.classList.add('bubble-audio');
} }
bubble.classList.remove('is-message-empty'); bubble.classList.remove('is-message-empty');
messageDiv.classList.add((pending.type || 'document') + '-message'); messageDiv.classList.add((pending.type || 'document') + '-message');
messageDiv.append(docDiv);
processingWebPage = true; processingWebPage = true;
break; break;
} }
@ -1805,52 +1813,15 @@ export default class ChatBubbles {
break; break;
} else { } else {
//const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const newNameContainer = wrapGroupedDocuments({
//const isFullAlbum = storage && Object.keys(storage).length != 1; albumMustBeRenderedFull,
const mids = albumMustBeRenderedFull ? this.appMessagesManager.getMidsByMid(message.mid) : [message.mid]; message,
mids.forEach((mid, idx) => { bubble,
const message = this.appMessagesManager.getMessage(mid); messageDiv
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);
}); });
if(mids.length > 1) { if(newNameContainer) {
bubble.classList.add('is-multiple-documents', 'is-grouped'); nameContainer = newNameContainer;
} }
bubble.classList.remove('is-message-empty'); bubble.classList.remove('is-message-empty');

167
src/components/popupNewMedia.ts

@ -8,6 +8,7 @@ import { ripple } from "./ripple";
import Scrollable from "./scrollable"; import Scrollable from "./scrollable";
import { toast } from "./toast"; import { toast } from "./toast";
import { prepareAlbum, wrapDocument } from "./wrappers"; import { prepareAlbum, wrapDocument } from "./wrappers";
import CheckboxField from "./checkbox";
type SendFileParams = Partial<{ type SendFileParams = Partial<{
file: File, file: File,
@ -25,13 +26,16 @@ export default class PopupNewMedia extends PopupElement {
private btnSend: HTMLElement; private btnSend: HTMLElement;
private input: HTMLInputElement; private input: HTMLInputElement;
private mediaContainer: HTMLElement; private mediaContainer: HTMLElement;
private groupCheckboxField: { label: HTMLLabelElement; input: HTMLInputElement; span: HTMLSpanElement; };
private willAttach: Partial<{ private willAttach: Partial<{
type: 'media' | 'document', type: 'media' | 'document',
isMedia: boolean, isMedia: true,
group: boolean,
sendFileDetails: SendFileParams[] sendFileDetails: SendFileParams[]
}> = { }> = {
sendFileDetails: [] sendFileDetails: [],
group: false
}; };
constructor(files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { constructor(files: File[], willAttachType: PopupNewMedia['willAttach']['type']) {
@ -60,7 +64,29 @@ export default class PopupNewMedia extends PopupElement {
showLengthOn: 80 showLengthOn: 80
}); });
this.input = inputField.input; 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); this.attachFiles(files);
} }
@ -85,18 +111,35 @@ export default class PopupNewMedia extends PopupElement {
this.destroy(); this.destroy();
const willAttach = this.willAttach; const willAttach = this.willAttach;
willAttach.isMedia = willAttach.type == 'media'; willAttach.isMedia = willAttach.type == 'media' ? true : undefined;
//console.log('will send files with options:', willAttach); //console.log('will send files with options:', willAttach);
const peerID = appImManager.chat.peerID; const peerID = appImManager.chat.peerID;
const chatInputC = appImManager.chat.input; const chatInputC = appImManager.chat.input;
if(willAttach.sendFileDetails.length > 1 && willAttach.isMedia) { if(willAttach.sendFileDetails.length > 1 && willAttach.group) {
appMessagesManager.sendAlbum(peerID, willAttach.sendFileDetails.map(d => d.file), Object.assign({ for(let i = 0; i < willAttach.sendFileDetails.length;) {
caption, let firstType = willAttach.sendFileDetails[i].file.type.split('/')[0];
replyToMsgID: chatInputC.replyToMsgID for(var k = 0; k < 10 && i < willAttach.sendFileDetails.length; ++i, ++k) {
}, willAttach)); 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 { } else {
if(caption) { if(caption) {
if(willAttach.sendFileDetails.length > 1) { if(willAttach.sendFileDetails.length > 1) {
@ -109,7 +152,7 @@ export default class PopupNewMedia extends PopupElement {
const promises = willAttach.sendFileDetails.map(params => { const promises = willAttach.sendFileDetails.map(params => {
const promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({ const promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({
//isMedia: willAttach.isMedia, //isMedia: willAttach.isMedia,
isMedia: params.file.type.includes('audio/') || willAttach.isMedia, isMedia: willAttach.isMedia,
caption, caption,
replyToMsgID: chatInputC.replyToMsgID replyToMsgID: chatInputC.replyToMsgID
}, params)); }, params));
@ -176,7 +219,8 @@ export default class PopupNewMedia extends PopupElement {
case 'document': { case 'document': {
const isPhoto = file.type.indexOf('image/') !== -1; const isPhoto = file.type.indexOf('image/') !== -1;
if(isPhoto) { const isAudio = file.type.indexOf('audio/') !== -1;
if(isPhoto || isAudio) {
params.objectURL = URL.createObjectURL(file); params.objectURL = URL.createObjectURL(file);
} }
@ -220,9 +264,9 @@ export default class PopupNewMedia extends PopupElement {
const container = this.container; const container = this.container;
const willAttach = this.willAttach; const willAttach = this.willAttach;
if(files.length > 10 && willAttach.type == 'media') { /* if(files.length > 10 && willAttach.type == 'media') {
willAttach.type = 'document'; willAttach.type = 'document';
} } */
files = files.filter(file => { files = files.filter(file => {
if(willAttach.type == 'media') { if(willAttach.type == 'media') {
@ -232,64 +276,77 @@ export default class PopupNewMedia extends PopupElement {
} }
}); });
if(files.length) { Promise.all(files.map(this.attachFile)).then(results => {
if(willAttach.type == 'document') { this.container.classList.remove('is-media', 'is-document', 'is-album');
this.title.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File'); this.mediaContainer.innerHTML = '';
container.classList.add('is-document');
} else { if(files.length) {
container.classList.add('is-media'); if(willAttach.type == 'document') {
this.title.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File');
let foundPhotos = 0; container.classList.add('is-document');
let foundVideos = 0; } else {
files.forEach(file => { container.classList.add('is-media');
if(file.type.indexOf('image/') === 0) ++foundPhotos;
else if(file.type.indexOf('video/') === 0) ++foundVideos; let foundPhotos = 0;
}); let foundVideos = 0;
files.forEach(file => {
if(foundPhotos && foundVideos) { if(file.type.indexOf('image/') === 0) ++foundPhotos;
this.title.innerText = 'Send Album'; else if(file.type.indexOf('video/') === 0) ++foundVideos;
} else if(foundPhotos) { });
this.title.innerText = 'Send ' + (foundPhotos > 1 ? foundPhotos + ' Photos' : 'Photo');
} else if(foundVideos) { if(foundPhotos && foundVideos && willAttach.group) {
this.title.innerText = 'Send ' + (foundVideos > 1 ? foundVideos + ' Videos' : 'Video'); 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.type == 'media') {
if(willAttach.sendFileDetails.length > 1) { if(willAttach.sendFileDetails.length > 1 && willAttach.group) {
container.classList.add('is-album'); 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({ albumContainer.append(...results.slice(i, i + 10));
container: this.mediaContainer, prepareAlbum({
items: willAttach.sendFileDetails.map(o => ({w: o.width, h: o.height})), container: albumContainer,
maxWidth: 380, items: willAttach.sendFileDetails.slice(i, i + 10).map(o => ({w: o.width, h: o.height})),
minWidth: 100, maxWidth: 380,
spacing: 4 minWidth: 100,
}); spacing: 4
});
this.mediaContainer.append(albumContainer);
}
//console.log('chatInput album layout:', layout); //console.log('chatInput album layout:', layout);
} else { } else {
const params = willAttach.sendFileDetails[0]; for(let i = 0; i < results.length; ++i) {
const div = results[0]; const params = willAttach.sendFileDetails[i];
const {w, h} = calcImageInBox(params.width, params.height, 380, 320); const div = results[i];
div.style.width = w + 'px'; const {w, h} = calcImageInBox(params.width, params.height, 380, 320);
div.style.height = h + 'px'; div.style.width = w + 'px';
this.mediaContainer.append(div); div.style.height = h + 'px';
this.mediaContainer.append(div);
}
} }
} else { } else {
this.mediaContainer.append(...results); this.mediaContainer.append(...results);
} }
// show now // show now
document.body.addEventListener('keydown', this.onKeyDown); if(!this.element.classList.contains('active')) {
this.onClose = () => { document.body.addEventListener('keydown', this.onKeyDown);
document.body.removeEventListener('keydown', this.onKeyDown); this.onClose = () => {
}; document.body.removeEventListener('keydown', this.onKeyDown);
this.show(); };
this.show();
}
}); });
} }
} }

81
src/components/wrappers.ts

@ -25,6 +25,7 @@ import PollElement from './poll';
import ProgressivePreloader from './preloader'; import ProgressivePreloader from './preloader';
import './middleEllipsis'; import './middleEllipsis';
import { nextRandomInt } from '../helpers/random'; import { nextRandomInt } from '../helpers/random';
import RichTextProcessor from '../lib/richtextprocessor';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB 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'); const icoDiv = document.createElement('div');
icoDiv.classList.add('document-ico'); 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'); docDiv.classList.add('document-with-thumb');
if(uploading) { if(uploading) {
@ -858,7 +859,7 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo
}) { }) {
const items: {size: PhotoSize.photoSize, media: any, message: any}[] = []; 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); const storage = appMessagesManager.getMidsByAlbum(groupID);
for(const mid of storage) { for(const mid of storage) {
const m = appMessagesManager.getMessage(mid); const m = appMessagesManager.getMessage(mid);
@ -868,6 +869,11 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo
items.push({size, media, message: m}); items.push({size, media, message: m});
} }
// * pending
if(storage[0] < 0) {
items.reverse();
}
prepareAlbum({ prepareAlbum({
container: attachmentDiv, container: attachmentDiv,
items: items.map(i => ({w: i.size.w, h: i.size.h})), 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) { export function wrapPoll(pollID: string, mid: number) {
const elem = new PollElement(); const elem = new PollElement();
elem.setAttribute('poll-id', pollID); elem.setAttribute('poll-id', pollID);

507
src/lib/appManagers/appMessagesManager.ts

@ -4,7 +4,7 @@ import { tsNow } from "../../helpers/date";
import { copy, defineNotNumerableProperties, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object"; import { copy, defineNotNumerableProperties, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random"; import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols } from "../../helpers/string"; 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 { InvokeApiOptions } from "../../types";
import { langPack } from "../langPack"; import { langPack } from "../langPack";
import { logger, LogLevels } from "../logger"; import { logger, LogLevels } from "../logger";
@ -569,31 +569,32 @@ export class AppMessagesManager {
} }
public sendFile(peerID: number, file: File | Blob | MyDocument, options: Partial<{ public sendFile(peerID: number, file: File | Blob | MyDocument, options: Partial<{
isMedia: boolean, isRoundMessage: true,
isVoiceMessage: true,
isGroupedItem: true,
isMedia: true,
replyToMsgID: number, replyToMsgID: number,
caption: string, caption: string,
entities: any[], entities: MessageEntity[],
width: number, width: number,
height: number, height: number,
objectURL: string, objectURL: string,
isRoundMessage: boolean,
duration: number, duration: number,
background: boolean, background: true,
isVoiceMessage: boolean,
waveform: Uint8Array waveform: Uint8Array
}> = {}) { }> = {}) {
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
var messageID = this.tempID--; const messageID = this.tempID--;
var randomIDS = randomLong(); const randomIDS = randomLong();
var historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []}); const historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []});
var flags = 0; const pFlags: any = {};
var pFlags: any = {}; const replyToMsgID = options.replyToMsgID;
var replyToMsgID = options.replyToMsgID; const isChannel = appPeersManager.isChannel(peerID);
var isChannel = appPeersManager.isChannel(peerID); const isMegagroup = isChannel && appPeersManager.isMegagroup(peerID);
var isMegagroup = isChannel && appPeersManager.isMegagroup(peerID); const asChannel = !!(isChannel && !isMegagroup);
var asChannel = isChannel && !isMegagroup ? true : false; let attachType: string, apiFileName: string;
var attachType: string, apiFileName: string;
const fileType = 'mime_type' in file ? file.mime_type : file.type; const fileType = 'mime_type' in file ? file.mime_type : file.type;
const fileName = file instanceof File ? file.name : ''; 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; const isPhoto = ['image/jpeg', 'image/png', 'image/bmp'].indexOf(fileType) >= 0;
let photo: MyPhoto, document: MyDocument;
let actionName = ''; 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'; attachType = 'document';
apiFileName = 'document.' + fileType.split('/')[1]; apiFileName = 'document.' + fileType.split('/')[1];
actionName = 'sendMessageUploadDocumentAction'; actionName = 'sendMessageUploadDocumentAction';
} else if(isDocument) { // maybe it's a sticker or gif
attachType = 'document';
apiFileName = '';
} else if(isPhoto) { } else if(isPhoto) {
attachType = 'photo'; attachType = 'photo';
apiFileName = 'photo.' + fileType.split('/')[1]; apiFileName = 'photo.' + fileType.split('/')[1];
actionName = 'sendMessageUploadPhotoAction'; actionName = 'sendMessageUploadPhotoAction';
let photo: MyPhoto = { photo = {
_: 'photo', _: 'photo',
id: '' + messageID, id: '' + messageID,
sizes: [{ sizes: [{
@ -646,30 +669,6 @@ export class AppMessagesManager {
photo.url = options.objectURL || ''; photo.url = options.objectURL || '';
appPhotosManager.savePhoto(photo); 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) { } else if(fileType.indexOf('video/') === 0) {
attachType = 'video'; attachType = 'video';
apiFileName = 'video.mp4'; apiFileName = 'video.mp4';
@ -677,9 +676,8 @@ export class AppMessagesManager {
let videoAttribute: DocumentAttribute.documentAttributeVideo = { let videoAttribute: DocumentAttribute.documentAttributeVideo = {
_: 'documentAttributeVideo', _: 'documentAttributeVideo',
pFlags: { // that's only for client, not going to telegram pFlags: {
supports_streaming: true, round_message: options.isRoundMessage
round_message: options.isRoundMessage || undefined
}, },
duration: options.duration, duration: options.duration,
w: options.width, w: options.width,
@ -697,7 +695,7 @@ export class AppMessagesManager {
if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1 && !isDocument) { if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1 && !isDocument) {
const thumbs: PhotoSize[] = []; const thumbs: PhotoSize[] = [];
const doc: MyDocument = { document = {
_: 'document', _: 'document',
id: '' + messageID, id: '' + messageID,
duration: options.duration, duration: options.duration,
@ -709,10 +707,10 @@ export class AppMessagesManager {
size: file.size size: file.size
} as any; } as any;
defineNotNumerableProperties(doc, ['downloaded', 'url']); defineNotNumerableProperties(document, ['downloaded', 'url']);
// @ts-ignore // @ts-ignore
doc.downloaded = file.size; document.downloaded = file.size;
doc.url = options.objectURL || ''; document.url = options.objectURL || '';
if(isPhoto) { if(isPhoto) {
attributes.push({ attributes.push({
@ -732,42 +730,36 @@ export class AppMessagesManager {
}); });
} }
appDocsManager.saveDoc(doc); appDocsManager.saveDoc(document);
} }
this.log('AMM: sendFile', attachType, apiFileName, file.type, options); this.log('AMM: sendFile', attachType, apiFileName, file.type, options);
var fromID = appUsersManager.getSelf().id; let fromID = appUsersManager.getSelf().id;
if(peerID != fromID) { if(peerID != fromID) {
flags |= 2;
pFlags.out = true; pFlags.out = true;
if(!isChannel && !appUsersManager.isBot(peerID)) { if(!isChannel && !appUsersManager.isBot(peerID)) {
flags |= 1;
pFlags.unread = true; pFlags.unread = true;
} }
} }
if(replyToMsgID) {
flags |= 8;
}
if(asChannel) { if(asChannel) {
fromID = 0; fromID = 0;
pFlags.post = true; pFlags.post = true;
} else {
flags |= 256;
} }
const preloader = new ProgressivePreloader(null, true, false, 'prepend'); const preloader = new ProgressivePreloader(null, true, false, 'prepend');
const media = { const media = {
_: 'messageMediaPending', _: 'messageMediaPending',
type: attachType, type: options.isGroupedItem && options.isMedia ? 'album' : attachType,
file_name: fileName || apiFileName, file_name: fileName || apiFileName,
size: file.size, size: file.size,
file: file, file,
preloader: preloader, preloader,
photo,
document,
w: options.width, w: options.width,
h: options.height, h: options.height,
url: options.objectURL url: options.objectURL
@ -778,8 +770,8 @@ export class AppMessagesManager {
id: messageID, id: messageID,
from_id: appPeersManager.getOutputPeer(fromID), from_id: appPeersManager.getOutputPeer(fromID),
peer_id: appPeersManager.getOutputPeer(peerID), peer_id: appPeersManager.getOutputPeer(peerID),
pFlags: pFlags, pFlags,
date: date, date,
message: caption, message: caption,
media: isDocument ? { media: isDocument ? {
_: 'messageMediaDocument', _: 'messageMediaDocument',
@ -805,59 +797,22 @@ export class AppMessagesManager {
let uploaded = false, let uploaded = false,
uploadPromise: ReturnType<ApiFileManager['uploadFile']> = null; uploadPromise: ReturnType<ApiFileManager['uploadFile']> = null;
const invoke = (flags: number, inputMedia: any) => { const sentDeferred = deferredPromise<InputMedia>();
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);
});
};
message.send = () => { message.send = () => {
let flags = 0;
if(replyToMsgID) {
flags |= 1;
}
if(options.background) {
flags |= 64;
}
flags |= 128; // clear_draft
if(isDocument) { if(isDocument) {
const {id, access_hash, file_reference} = file as MyDocument; const {id, access_hash, file_reference} = file as MyDocument;
const inputMedia = { const inputMedia: InputMedia = {
_: 'inputMediaDocument', _: 'inputMediaDocument',
id: { id: {
_: 'inputDocument', _: 'inputDocument',
id: id, id,
access_hash: access_hash, access_hash,
file_reference: file_reference file_reference
} }
}; };
invoke(flags, inputMedia); sentDeferred.resolve(inputMedia);
} else if(file instanceof File || file instanceof Blob) { } else if(file instanceof File || file instanceof Blob) {
const deferred = deferredPromise<void>(); const deferred = deferredPromise<void>();
@ -873,7 +828,7 @@ export class AppMessagesManager {
inputFile.name = apiFileName; inputFile.name = apiFileName;
uploaded = true; uploaded = true;
var inputMedia; let inputMedia: InputMedia;
switch(attachType) { switch(attachType) {
case 'photo': case 'photo':
inputMedia = { inputMedia = {
@ -887,11 +842,11 @@ export class AppMessagesManager {
_: 'inputMediaUploadedDocument', _: 'inputMediaUploadedDocument',
file: inputFile, file: inputFile,
mime_type: fileType, mime_type: fileType,
attributes: attributes attributes
}; };
} }
invoke(flags, inputMedia); sentDeferred.resolve(inputMedia);
}, (/* error */) => { }, (/* error */) => {
toggleError(true); toggleError(true);
}); });
@ -907,6 +862,7 @@ export class AppMessagesManager {
this.log('cancelling upload', media); this.log('cancelling upload', media);
deferred.resolve(); deferred.resolve();
sentDeferred.reject(err);
this.cancelPendingMessage(randomIDS); this.cancelPendingMessage(randomIDS);
this.setTyping(peerID, 'sendMessageCancelAction'); this.setTyping(peerID, 'sendMessageCancelAction');
} }
@ -917,19 +873,52 @@ export class AppMessagesManager {
this.sendFilePromise = deferred; this.sendFilePromise = deferred;
} }
return sentDeferred;
}; };
this.saveMessages([message]);
historyStorage.pending.unshift(messageID); 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<{ public async sendAlbum(peerID: number, files: File[], options: Partial<{
entities: any[], isMedia: true,
entities: MessageEntity[],
replyToMsgID: number, replyToMsgID: number,
caption: string, caption: string,
sendFileDetails: Partial<{ sendFileDetails: Partial<{
@ -939,146 +928,50 @@ export class AppMessagesManager {
objectURL: string, objectURL: string,
}>[] }>[]
}> = {}) { }> = {}) {
if(files.length === 1) {
return this.sendFile(peerID, files[0], {...options, ...options.sendFileDetails[0]});
}
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
let groupID: number; const replyToMsgID = options.replyToMsgID;
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;
let caption = options.caption || ''; let caption = options.caption || '';
let entities: MessageEntity[];
let date = tsNow(true) + serverTimeManager.serverTimeOffset;
if(caption) { if(caption) {
let entities = options.entities || []; entities = options.entities || [];
caption = RichTextProcessor.parseMarkdown(caption, entities); caption = RichTextProcessor.parseMarkdown(caption, entities);
} }
this.log('AMM: sendAlbum', files, options); this.log('AMM: sendAlbum', files, options);
let fromID = appUsersManager.getSelf().id; const messages = files.map((file, idx) => {
if(peerID != fromID) { const details = options.sendFileDetails[idx];
pFlags.out = true; const o: any = {
isGroupedItem: true,
if(!isChannel && !appUsersManager.isBot(peerID)) { isMedia: options.isMedia,
pFlags.unread = true; ...details
}
}
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
}; };
if(file.type.indexOf('video/') === 0) { if(idx === 0) {
let videoAttribute: DocumentAttribute.documentAttributeVideo = { o.caption = caption;
_: 'documentAttributeVideo', o.entities = entities;
pFlags: { // that's only for client, not going to telegram o.replyToMsgID = replyToMsgID;
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;
} }
let message = { return this.sendFile(peerID, file, o).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 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) { if(on) {
message.error = true; message.error = true;
} else { } else {
@ -1088,15 +981,11 @@ export class AppMessagesManager {
rootScope.broadcast('messages_pending'); rootScope.broadcast('messages_pending');
}; };
let uploaded = false, const inputPeer = appPeersManager.getInputPeerByID(peerID);
uploadPromise: ReturnType<ApiFileManager['uploadFile']> = null; const invoke = (multiMedia: any[]) => {
let inputPeer = appPeersManager.getInputPeerByID(peerID);
let invoke = (multiMedia: any[]) => {
this.setTyping(peerID, 'sendMessageCancelAction'); this.setTyping(peerID, 'sendMessageCancelAction');
return apiManager.invokeApi('messages.sendMultiMedia', { return apiManager.invokeApi('messages.sendMultiMedia', {
flags: flags,
peer: inputPeer, peer: inputPeer,
multi_media: multiMedia, multi_media: multiMedia,
reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID) reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID)
@ -1107,114 +996,42 @@ export class AppMessagesManager {
}); });
}; };
let inputs: any[] = []; const inputs: InputSingleMedia[] = [];
for(let i = 0, length = files.length; i < length; ++i) { for(const message of messages) {
const file = files[i]; const inputMedia: InputMedia = await message.send();
const message = messages[i]; this.log('sendAlbum uploaded item:', inputMedia);
const media = message.media;
const preloader = media.preloader;
const actionName = file.type.indexOf('video/') === 0 ? 'sendMessageUploadVideoAction' : 'sendMessageUploadPhotoAction';
const deferred = deferredPromise<void>();
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;
await apiManager.invokeApi('messages.uploadMedia', {
peer: inputPeer,
media: inputMedia
}).then(messageMedia => {
let inputMedia: any; let inputMedia: any;
let details = options.sendFileDetails[i]; if(messageMedia._ == 'messageMediaPhoto') {
if(details.duration) { const photo = appPhotosManager.savePhoto(messageMedia.photo);
inputMedia = { inputMedia = appPhotosManager.getInput(photo);
_: 'inputMediaUploadedDocument', } else if(messageMedia._ == 'messageMediaDocument') {
file: inputFile, const doc = appDocsManager.saveDoc(messageMedia.document);
mime_type: file.type, inputMedia = appDocsManager.getMediaInput(doc);
attributes: [{
_: 'documentAttributeVideo',
supports_streaming: true,
duration: details.duration,
w: details.width,
h: details.height
}]
};
} else {
inputMedia = {
_: 'inputMediaUploadedPhoto',
file: inputFile
};
} }
return apiManager.invokeApi('messages.uploadMedia', { inputs.push({
peer: inputPeer, _: 'inputSingleMedia',
media: inputMedia media: inputMedia,
}).then(messageMedia => { random_id: message.random_id,
if(canceled) { message: caption,
return; entities
}
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);
}); });
// * only 1 caption for all inputs
if(caption) {
caption = '';
entities = [];
}
}, () => { }, () => {
toggleError(message, true); toggleError(message, true);
}); });
this.log('appMessagesManager: sendAlbum uploadPromise.finally!');
deferred.resolve();
} }
uploaded = true;
invoke(inputs); invoke(inputs);
} }

21
src/scss/partials/popups/_mediaAttacher.scss

@ -14,7 +14,7 @@
/* max-height: 425px; */ /* max-height: 425px; */
#{$parent}-photo { #{$parent}-photo {
max-height: 320px; //max-height: 320px;
margin: 0 auto; margin: 0 auto;
img { img {
@ -24,6 +24,7 @@
> div { > div {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin: 0 auto;
} }
} }
} }
@ -33,7 +34,7 @@
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
> div { .album-item {
position: absolute; position: absolute;
} }
@ -87,6 +88,7 @@
//justify-content: center; //justify-content: center;
width: fit-content; width: fit-content;
border-radius: $border-radius-medium; border-radius: $border-radius-medium;
user-select: none;
/* align-items: center; */ /* align-items: center; */
.document { .document {
@ -152,4 +154,19 @@
.popup-header { .popup-header {
padding: 0; 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;
}
} }
Loading…
Cancel
Save