Allow sending different media types

Added drag&drop in new media popup
Fix rendering thumbnail for outgoing document
Allow pasting media in current popup
This commit is contained in:
morethanwords 2021-11-26 13:58:12 +04:00
parent 24c1a4af23
commit bbb754196e
18 changed files with 622 additions and 499 deletions

View File

@ -2557,13 +2557,15 @@ export default class ChatBubbles {
}
return new Promise<PeerId>((resolve, reject) => {
new PopupForward({
const popup = new PopupForward({
[this.peerId]: []
}, (peerId) => {
resolve(peerId);
}, () => {
reject();
}, true);
popup.addEventListener('close', () => {
reject();
});
});
});

View File

@ -5,7 +5,7 @@
*/
import generatePathData from "../../helpers/generatePathData";
import { i18n, LangPackKey } from "../../lib/langPack";
import { FormatterArguments, i18n, LangPackKey } from "../../lib/langPack";
export default class ChatDragAndDrop {
container: HTMLDivElement;
@ -14,9 +14,10 @@ export default class ChatDragAndDrop {
path: SVGPathElement;
constructor(appendTo: HTMLElement, private options: {
icon: string,
icon?: string,
header: LangPackKey,
subtitle: LangPackKey,
headerArgs?: FormatterArguments,
subtitle?: LangPackKey,
onDrop: (e: DragEvent) => void
}) {
this.container = document.createElement('div');
@ -31,21 +32,27 @@ export default class ChatDragAndDrop {
this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.path.classList.add('drop-outline-path');
const dropIcon = document.createElement('div');
dropIcon.classList.add('drop-icon', 'tgico-' + options.icon);
let dropIcon: HTMLElement;
if(options.icon) {
dropIcon = document.createElement('div');
dropIcon.classList.add('drop-icon', 'tgico-' + options.icon);
}
const dropHeader = document.createElement('div');
dropHeader.classList.add('drop-header');
dropHeader.append(i18n(options.header));
dropHeader.append(i18n(options.header, options.headerArgs));
const dropSubtitle = document.createElement('div');
dropSubtitle.classList.add('drop-subtitle');
dropSubtitle.append(i18n(options.subtitle));
let dropSubtitle: HTMLElement;
if(options.subtitle) {
dropSubtitle = document.createElement('div');
dropSubtitle.classList.add('drop-subtitle');
dropSubtitle.append(i18n(options.subtitle));
}
this.svg.append(this.path);
this.outlineWrapper.append(this.svg);
this.container.append(this.outlineWrapper, dropIcon, dropHeader, dropSubtitle);
this.container.append(...[this.outlineWrapper, dropIcon, dropHeader, dropSubtitle].filter(Boolean));
appendTo.append(this.container);
this.container.addEventListener('dragover', this.onDragOver);

View File

@ -1677,9 +1677,11 @@ export default class ChatInput {
this.clearHelper();
this.updateSendBtn();
let selected = false;
new PopupForward(copy(this.forwarding), () => {
const popup = new PopupForward(copy(this.forwarding), () => {
selected = true;
}, () => {
});
popup.addEventListener('close', () => {
this.helperWaitingForward = false;
if(!selected) {

View File

@ -104,7 +104,7 @@ class InputField {
this.required = options.required;
this.validate = options.validate;
if(options.maxLength) {
if(options.maxLength !== undefined && options.showLengthOn === undefined) {
options.showLengthOn = Math.min(40, Math.round(options.maxLength / 3));
}

View File

@ -81,12 +81,12 @@ export default class PopupAvatar extends PopupElement {
this.container.append(this.cropContainer, this.btnConfirm, this.input);
this.onCloseAfterTimeout = () => {
this.addEventListener('closeAfterTimeout', () => {
this.cropper.removeHandlers();
if(this.image) {
this.image.remove();
}
};
});
}
private resolve() {

View File

@ -11,7 +11,6 @@ export default class PopupForward extends PopupPickUser {
constructor(
peerIdMids: {[fromPeerId: PeerId]: number[]},
onSelect?: (peerId: PeerId) => Promise<void> | void,
onClose?: () => void,
overrideOnSelect = false
) {
super({
@ -27,7 +26,6 @@ export default class PopupForward extends PopupPickUser {
appImManager.setInnerPeer(peerId);
appImManager.chat.input.initMessagesForward(peerIdMids);
},
onClose,
placeholder: 'ShareModal.Search.ForwardPlaceholder',
chatRightsAction: 'send_messages',
selfPresence: 'ChatYourSelf'

View File

@ -16,6 +16,7 @@ import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEve
import isSendShortcutPressed from "../../helpers/dom/isSendShortcutPressed";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
import getKeyFromEvent from "../../helpers/dom/getKeyFromEvent";
import EventListenerBase from "../../helpers/eventListenerBase";
export type PopupButton = {
text?: string,
@ -35,7 +36,10 @@ export type PopupOptions = Partial<{
confirmShortcutIsSendShortcut: boolean
}>;
export default class PopupElement {
export default class PopupElement extends EventListenerBase<{
close: () => void,
closeAfterTimeout: () => void
}> {
protected element = document.createElement('div');
protected container = document.createElement('div');
protected header = document.createElement('div');
@ -45,8 +49,6 @@ export default class PopupElement {
protected body: HTMLElement;
protected buttonsEl: HTMLElement;
protected onClose: () => void;
protected onCloseAfterTimeout: () => void;
protected onEscape: () => boolean = () => true;
protected navigationItem: NavigationItem;
@ -57,6 +59,8 @@ export default class PopupElement {
protected btnConfirmOnEnter: HTMLButtonElement;
constructor(className: string, protected buttons?: Array<PopupButton>, options: PopupOptions = {}) {
super(false);
this.element.classList.add('popup');
this.element.className = 'popup' + (className ? ' ' + className : '');
this.container.classList.add('popup-container', 'z-depth-1');
@ -181,7 +185,7 @@ export default class PopupElement {
};
private destroy = () => {
this.onClose && this.onClose();
this.dispatchEvent('close');
this.element.classList.add('hiding');
this.element.classList.remove('active');
this.listenerSetter.removeAll();
@ -193,7 +197,8 @@ export default class PopupElement {
setTimeout(() => {
this.element.remove();
this.onCloseAfterTimeout && this.onCloseAfterTimeout();
this.dispatchEvent('closeAfterTimeout');
this.cleanup();
animationIntersector.checkAnimations(false);
}, 150);
};

View File

@ -14,7 +14,7 @@ import CheckboxField from "../checkboxField";
import SendContextMenu from "../chat/sendContextMenu";
import { createPosterFromMedia, createPosterFromVideo, onMediaLoad } from "../../helpers/files";
import { MyDocument } from "../../lib/appManagers/appDocsManager";
import I18n, { i18n, LangPackKey } from "../../lib/langPack";
import I18n, { FormatterArguments, i18n, LangPackKey } from "../../lib/langPack";
import appDownloadManager from "../../lib/appManagers/appDownloadManager";
import calcImageInBox from "../../helpers/calcImageInBox";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
@ -24,6 +24,7 @@ import { MediaSize } from "../../helpers/mediaSizes";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport';
import getGifDuration from "../../helpers/getGifDuration";
import replaceContent from "../../helpers/dom/replaceContent";
type SendFileParams = Partial<{
file: File,
@ -36,32 +37,39 @@ type SendFileParams = Partial<{
width: number,
height: number,
duration: number,
noSound: boolean
noSound: boolean,
itemDiv: HTMLElement
}>;
// TODO: .gif upload as video
let currentPopup: PopupNewMedia;
export function getCurrentNewMediaPopup() {
return currentPopup;
}
export default class PopupNewMedia extends PopupElement {
private input: HTMLElement;
private mediaContainer: HTMLElement;
private groupCheckboxField: CheckboxField;
private wasInputValue = '';
private mediaCheckboxField: CheckboxField;
private wasInputValue: string;
private willAttach: Partial<{
type: 'media' | 'document',
isMedia: true,
group: boolean,
sendFileDetails: SendFileParams[]
}> = {
sendFileDetails: [],
group: false
};
}>;
private inputField: InputField;
constructor(private chat: Chat, files: File[], willAttachType: PopupNewMedia['willAttach']['type']) {
super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true});
constructor(private chat: Chat, private files: File[], willAttachType: PopupNewMedia['willAttach']['type']) {
super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true, body: true});
this.willAttach.type = willAttachType;
this.willAttach = {
type: willAttachType,
sendFileDetails: [],
group: false
};
attachClickEvent(this.btnConfirm, () => this.send(), {listenerSetter: this.listenerSetter});
@ -95,41 +103,98 @@ export default class PopupNewMedia extends PopupElement {
placeholder: 'PreviewSender.CaptionPlaceholder',
label: 'Caption',
name: 'photo-caption',
maxLength: rootScope.config.caption_length_max,
showLengthOn: 80
maxLength: rootScope.config.caption_length_max
});
this.input = this.inputField.input;
this.inputField.value = this.wasInputValue = this.chat.input.messageInputField.input.innerHTML;
this.chat.input.messageInputField.value = '';
this.container.append(scrollable.container);
this.body.append(scrollable.container);
this.container.append(this.inputField.container);
if(files.length > 1) {
this.attachFiles();
this.addEventListener('close', () => {
this.files = [];
currentPopup = undefined;
});
currentPopup = this;
}
public appendDrops(element: HTMLElement) {
this.body.append(element);
}
get type() {
return this.willAttach.type;
}
set type(type: PopupNewMedia['willAttach']['type']) {
this.willAttach.type = type;
}
private appendGroupCheckboxField() {
const good = this.files.length > 1;
if(good && !this.groupCheckboxField) {
this.groupCheckboxField = new CheckboxField({
text: 'PreviewSender.GroupItems',
name: 'group-items'
});
this.container.append(this.groupCheckboxField.label, this.inputField.container);
this.container.append(...[this.groupCheckboxField.label, this.mediaCheckboxField?.label, this.inputField.container].filter(Boolean));
this.groupCheckboxField.input.checked = true;
this.willAttach.group = true;
this.groupCheckboxField.setValueSilently(this.willAttach.group);
this.listenerSetter.add(this.groupCheckboxField.input)('change', () => {
const checked = this.groupCheckboxField.input.checked;
const checked = this.groupCheckboxField.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.attachFiles();
});
} else if(this.groupCheckboxField) {
this.groupCheckboxField.label.classList.toggle('hide', !good);
}
this.container.append(this.inputField.container);
}
this.attachFiles(files);
private appendMediaCheckboxField() {
const good = !!this.files.find(file => MEDIA_MIME_TYPES_SUPPORTED.has(file.type));
if(good && !this.mediaCheckboxField) {
this.mediaCheckboxField = new CheckboxField({
text: 'PreviewSender.CompressFile',
name: 'compress-items'
});
this.container.append(...[this.groupCheckboxField?.label, this.mediaCheckboxField.label, this.inputField.container].filter(Boolean));
this.mediaCheckboxField.setValueSilently(this.willAttach.type === 'media');
this.listenerSetter.add(this.mediaCheckboxField.input)('change', () => {
const checked = this.mediaCheckboxField.checked;
this.willAttach.type = checked ? 'media' : 'document';
this.attachFiles();
});
} else if(this.mediaCheckboxField) {
this.mediaCheckboxField.label.classList.toggle('hide', !good);
}
}
public addFiles(files: File[]) {
const toPush = files.filter(file => {
const found = this.files.find(_file => {
return _file.lastModified === file.lastModified && _file.name === file.name && _file.size === file.size;
});
return !found;
});
if(toPush.length) {
this.files.push(...toPush);
this.attachFiles();
}
}
private onKeyDown = (e: KeyboardEvent) => {
@ -144,7 +209,7 @@ export default class PopupNewMedia extends PopupElement {
}
};
public send(force = false) {
private send(force = false) {
if(this.chat.type === 'scheduled' && !force) {
this.chat.input.scheduleSending(() => {
this.send(true);
@ -162,324 +227,335 @@ export default class PopupNewMedia extends PopupElement {
this.hide();
const willAttach = this.willAttach;
willAttach.isMedia = willAttach.type === 'media' ? true : undefined;
const {sendFileDetails, isMedia} = willAttach;
//console.log('will send files with options:', willAttach);
const peerId = this.chat.peerId;
const input = this.chat.input;
const silent = input.sendSilent;
const scheduleDate = input.scheduleDate;
const {peerId, input} = this.chat;
const {sendSilent, scheduleDate} = input;
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;
}
}
sendFileDetails.forEach(d => {
d.itemDiv = undefined;
});
const w = {...willAttach};
w.sendFileDetails = willAttach.sendFileDetails.slice(i - k, i);
this.chat.appMessagesManager.sendAlbum(peerId, w.sendFileDetails.map(d => d.file), Object.assign({
caption,
replyToMsgId: input.replyToMsgId,
const {length} = sendFileDetails;
const replyToMsgId = input.replyToMsgId;
this.iterate((sendFileDetails) => {
if(caption && sendFileDetails.length !== length) {
this.chat.appMessagesManager.sendText(peerId, caption, {
replyToMsgId,
threadId: this.chat.threadId,
isMedia: willAttach.isMedia,
silent,
silent: sendSilent,
scheduleDate,
clearDraft: true as true
}, w));
clearDraft: true
});
caption = undefined;
input.replyToMsgId = this.chat.threadId;
}
} else {
if(caption) {
if(willAttach.sendFileDetails.length > 1) {
this.chat.appMessagesManager.sendText(peerId, caption, {
replyToMsgId: input.replyToMsgId,
threadId: this.chat.threadId,
silent,
scheduleDate,
clearDraft: true
});
caption = '';
//input.replyToMsgId = undefined;
}
}
const promises = willAttach.sendFileDetails.map(params => {
const promise = this.chat.appMessagesManager.sendFile(peerId, params.file, Object.assign({
//isMedia: willAttach.isMedia,
isMedia: willAttach.isMedia,
caption,
replyToMsgId: input.replyToMsgId,
threadId: this.chat.threadId,
silent,
scheduleDate,
clearDraft: true as true
}, params));
caption = '';
return promise;
});
const w = {
...willAttach,
sendFileDetails
};
input.replyToMsgId = this.chat.threadId;
}
this.chat.appMessagesManager.sendAlbum(peerId, w.sendFileDetails.map(d => d.file), Object.assign({
caption,
replyToMsgId,
threadId: this.chat.threadId,
isMedia: isMedia,
silent: sendSilent,
scheduleDate,
clearDraft: true as true
}, w));
//Promise.all(promises);
//appMessagesManager.sendFile(appImManager.peerId, willAttach.file, willAttach);
caption = undefined;
});
input.replyToMsgId = this.chat.threadId;
input.onMessageSent();
}
public attachFile = (file: File) => {
const willAttach = this.willAttach;
return new Promise<HTMLDivElement>((resolve) => {
const params: SendFileParams = {};
params.file = file;
//console.log('selected file:', file, typeof(file), willAttach);
const itemDiv = document.createElement('div');
switch(willAttach.type) {
case 'media': {
const isVideo = file.type.indexOf('video/') === 0;
private attachMedia(file: File, params: SendFileParams, itemDiv: HTMLElement) {
itemDiv.classList.add('popup-item-media');
itemDiv.classList.add('popup-item-media');
const isVideo = file.type.startsWith('video/');
if(isVideo) {
const video = document.createElement('video');
const source = document.createElement('source');
source.src = params.objectURL = URL.createObjectURL(file);
video.autoplay = true;
video.controls = false;
video.muted = true;
video.setAttribute('playsinline', 'true');
let promise: Promise<void>;
if(isVideo) {
const video = document.createElement('video');
const source = document.createElement('source');
source.src = params.objectURL = URL.createObjectURL(file);
video.autoplay = true;
video.controls = false;
video.muted = true;
video.setAttribute('playsinline', 'true');
video.addEventListener('timeupdate', () => {
video.pause();
}, {once: true});
video.addEventListener('timeupdate', () => {
video.pause();
}, {once: true});
onMediaLoad(video).then(() => {
params.width = video.videoWidth;
params.height = video.videoHeight;
params.duration = Math.floor(video.duration);
promise = onMediaLoad(video).then(() => {
params.width = video.videoWidth;
params.height = video.videoHeight;
params.duration = Math.floor(video.duration);
const audioDecodedByteCount = (video as any).webkitAudioDecodedByteCount;
if(audioDecodedByteCount !== undefined) {
params.noSound = !audioDecodedByteCount;
}
itemDiv.append(video);
return createPosterFromVideo(video).then(thumb => {
params.thumb = {
url: URL.createObjectURL(thumb.blob),
...thumb
};
});
});
video.append(source);
} else {
const img = new Image();
promise = new Promise<void>((resolve) => {
img.onload = () => {
params.width = img.naturalWidth;
params.height = img.naturalHeight;
itemDiv.append(img);
if(file.type === 'image/gif') {
params.noSound = true;
Promise.all([
getGifDuration(img).then(duration => {
params.duration = Math.ceil(duration);
}),
const audioDecodedByteCount = (video as any).webkitAudioDecodedByteCount;
if(audioDecodedByteCount !== undefined) {
params.noSound = !audioDecodedByteCount;
}
itemDiv.append(video);
createPosterFromVideo(video).then(thumb => {
createPosterFromMedia(img).then(thumb => {
params.thumb = {
url: URL.createObjectURL(thumb.blob),
...thumb
};
resolve(itemDiv);
});
})
]).then(() => {
resolve();
});
video.append(source);
} else {
const img = new Image();
img.src = params.objectURL = URL.createObjectURL(file);
img.onload = () => {
params.width = img.naturalWidth;
params.height = img.naturalHeight;
itemDiv.append(img);
if(file.type === 'image/gif') {
params.noSound = true;
Promise.all([
getGifDuration(img).then(duration => {
params.duration = Math.ceil(duration);
}),
createPosterFromMedia(img).then(thumb => {
params.thumb = {
url: URL.createObjectURL(thumb.blob),
...thumb
};
})
]).then(() => {
resolve(itemDiv);
});
} else {
resolve(itemDiv);
}
};
}
break;
}
case 'document': {
const isPhoto = file.type.indexOf('image/') !== -1;
const isAudio = file.type.indexOf('audio/') !== -1;
if(isPhoto || isAudio) {
params.objectURL = URL.createObjectURL(file);
}
const doc = {
_: 'document',
file: file,
file_name: file.name || '',
fileName: file.name ? RichTextProcessor.wrapEmojiText(file.name) : '',
size: file.size,
type: isPhoto ? 'photo' : 'doc'
} as MyDocument;
const cacheContext = appDownloadManager.getCacheContext(doc);
cacheContext.url = params.objectURL;
cacheContext.downloaded = file.size;
const docDiv = wrapDocument({
message: {
_: 'message',
pFlags: {
is_outgoing: true
},
mid: 0,
peerId: 0,
media: {
_: 'messageMediaDocument',
document: doc
}
} as any
});
const finish = () => {
itemDiv.append(docDiv);
resolve(itemDiv);
};
if(isPhoto) {
const img = new Image();
img.src = params.objectURL;
img.onload = () => {
params.width = img.naturalWidth;
params.height = img.naturalHeight;
finish();
};
img.onerror = finish;
} else {
finish();
}
break;
}
}
willAttach.sendFileDetails.push(params);
});
};
public attachFiles(files: File[]) {
const container = this.container;
const willAttach = this.willAttach;
/* if(files.length > 10 && willAttach.type === 'media') {
willAttach.type = 'document';
} */
if(willAttach.type === 'media') {
files = files.filter(file => MEDIA_MIME_TYPES_SUPPORTED.has(file.type));
} else {
files = files.slice();
}
Promise.all(files.map(this.attachFile)).then(results => {
this.container.classList.remove('is-media', 'is-document', 'is-album');
this.mediaContainer.innerHTML = '';
if(files.length) {
let key: LangPackKey;
const args: any[] = [];
if(willAttach.type === 'document') {
key = 'PreviewSender.SendFile';
args.push(files.length);
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;
});
const sum = foundPhotos + foundVideos;
if(sum > 1 && willAttach.group) {
key = 'PreviewSender.SendAlbum';
const albumsLength = Math.ceil(sum / 10);
args.push(albumsLength);
} else if(foundPhotos) {
key = 'PreviewSender.SendPhoto';
args.push(foundPhotos);
} else if(foundVideos) {
key = 'PreviewSender.SendVideo';
args.push(foundVideos);
}
}
this.title.textContent = '';
this.title.append(i18n(key, args));
}
if(willAttach.type === 'media') {
if(willAttach.sendFileDetails.length > 1 && willAttach.group) {
container.classList.add('is-album');
for(let i = 0; i < results.length; i += 10) {
const albumContainer = document.createElement('div');
albumContainer.classList.add('popup-album');
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 {
for(let i = 0; i < results.length; ++i) {
const params = willAttach.sendFileDetails[i];
const div = results[i];
const size = calcImageInBox(params.width, params.height, 380, 320);
div.style.width = size.width + 'px';
div.style.height = size.height + 'px';
this.mediaContainer.append(div);
}
}
} else {
this.mediaContainer.append(...results);
}
// show now
if(!this.element.classList.contains('active')) {
this.listenerSetter.add(document.body)('keydown', this.onKeyDown);
this.onClose = () => {
if(this.wasInputValue) {
this.chat.input.messageInputField.value = this.wasInputValue;
resolve();
}
};
this.show();
});
img.src = params.objectURL = URL.createObjectURL(file);
}
return promise;
}
private attachDocument(file: File, params: SendFileParams, itemDiv: HTMLElement): ReturnType<PopupNewMedia['attachMedia']> {
itemDiv.classList.add('popup-item-document');
const isPhoto = file.type.startsWith('image/');
const isAudio = file.type.startsWith('audio/');
if(isPhoto || isAudio) {
params.objectURL = URL.createObjectURL(file);
}
const doc = {
_: 'document',
file: file,
file_name: file.name || '',
fileName: file.name ? RichTextProcessor.wrapEmojiText(file.name) : '',
size: file.size,
type: isPhoto ? 'photo' : 'doc'
} as MyDocument;
const cacheContext = appDownloadManager.getCacheContext(doc);
cacheContext.url = params.objectURL;
cacheContext.downloaded = file.size;
const docDiv = wrapDocument({
message: {
_: 'message',
pFlags: {
is_outgoing: true
},
mid: 0,
peerId: 0,
media: {
_: 'messageMediaDocument',
document: doc
}
} as any
});
const promise = new Promise<void>((resolve) => {
const finish = () => {
itemDiv.append(docDiv);
resolve();
};
if(isPhoto) {
const img = new Image();
img.src = params.objectURL;
img.onload = () => {
params.width = img.naturalWidth;
params.height = img.naturalHeight;
finish();
};
img.onerror = finish;
} else {
finish();
}
});
return promise;
}
private attachFile = (file: File) => {
const willAttach = this.willAttach;
const shouldCompress = this.shouldCompress(file.type);
const params: SendFileParams = {};
params.file = file;
const itemDiv = document.createElement('div');
itemDiv.classList.add('popup-item');
params.itemDiv = itemDiv;
const promise = shouldCompress ? this.attachMedia(file, params, itemDiv) : this.attachDocument(file, params, itemDiv);
willAttach.sendFileDetails.push(params);
return promise;
};
private shouldCompress(mimeType: string) {
return this.willAttach.type === 'media' && MEDIA_MIME_TYPES_SUPPORTED.has(mimeType);
}
private onRender() {
// show now
if(!this.element.classList.contains('active')) {
this.listenerSetter.add(document.body)('keydown', this.onKeyDown);
this.addEventListener('close', () => {
if(this.wasInputValue) {
this.chat.input.messageInputField.value = this.wasInputValue;
}
});
this.show();
}
}
private setTitle() {
const {willAttach, title, files} = this;
let key: LangPackKey;
const args: FormatterArguments = [];
if(willAttach.type === 'document') {
key = 'PreviewSender.SendFile';
args.push(files.length);
} else {
let foundPhotos = 0, foundVideos = 0, foundFiles = 0;
files.forEach(file => {
if(file.type.startsWith('image/')) ++foundPhotos;
else if(file.type.startsWith('video/')) ++foundVideos;
else ++foundFiles;
});
if([foundPhotos, foundVideos, foundFiles].filter(n => n > 0).length > 1) {
key = 'PreviewSender.SendFile';
args.push(files.length);
} else
/* const sum = foundPhotos + foundVideos;
if(sum > 1 && willAttach.group) {
key = 'PreviewSender.SendAlbum';
const albumsLength = Math.ceil(sum / 10);
args.push(albumsLength);
} else */if(foundPhotos) {
key = 'PreviewSender.SendPhoto';
args.push(foundPhotos);
} else if(foundVideos) {
key = 'PreviewSender.SendVideo';
args.push(foundVideos);
}
}
replaceContent(title, i18n(key, args));
}
private appendMediaToContainer(div: HTMLElement, params: SendFileParams) {
if(this.shouldCompress(params.file.type)) {
const size = calcImageInBox(params.width, params.height, 380, 320);
div.style.width = size.width + 'px';
div.style.height = size.height + 'px';
}
this.mediaContainer.append(div);
}
private iterate(cb: (sendFileDetails: SendFileParams[]) => void) {
const {sendFileDetails} = this.willAttach;
if(!this.willAttach.group) {
sendFileDetails.forEach(p => cb([p]));
return;
}
const length = sendFileDetails.length;
for(let i = 0; i < length;) {
const firstType = sendFileDetails[i].file.type;
let k = 0;
for(; k < 10 && i < length; ++i, ++k) {
const type = sendFileDetails[i].file.type;
if(this.shouldCompress(firstType) !== this.shouldCompress(type)) {
break;
}
}
cb(sendFileDetails.slice(i - k, i));
}
}
private attachFiles() {
const {files, willAttach, mediaContainer} = this;
willAttach.sendFileDetails.length = 0;
this.appendGroupCheckboxField();
this.appendMediaCheckboxField();
Promise.all(files.map(this.attachFile)).then(() => {
mediaContainer.innerHTML = '';
if(!files.length) {
return;
}
this.setTitle();
this.iterate((sendFileDetails) => {
if(this.shouldCompress(sendFileDetails[0].file.type) && sendFileDetails.length > 1) {
const albumContainer = document.createElement('div');
albumContainer.classList.add('popup-item-album', 'popup-item');
albumContainer.append(...sendFileDetails.map(s => s.itemDiv));
prepareAlbum({
container: albumContainer,
items: sendFileDetails.map(o => ({w: o.width, h: o.height})),
maxWidth: 380,
minWidth: 100,
spacing: 4
});
mediaContainer.append(albumContainer);
} else {
sendFileDetails.forEach((params) => {
this.appendMediaToContainer(params.itemDiv, params);
});
}
});
}).then(() => {
this.onRender();
});
}
}

View File

@ -15,7 +15,6 @@ export default class PopupPickUser extends PopupElement {
constructor(options: {
peerTypes: AppSelectPeers['peerType'],
onSelect?: (peerId: PeerId) => Promise<void> | void,
onClose?: () => void,
placeholder: LangPackKey,
chatRightsAction?: AppSelectPeers['chatRightsAction'],
peerId?: number,
@ -23,8 +22,6 @@ export default class PopupPickUser extends PopupElement {
}) {
super('popup-forward', null, {closable: true, overlayClosable: true, body: true});
if(options.onClose) this.onClose = options.onClose;
this.selector = new AppSelectPeers({
appendTo: this.body,
onChange: async() => {

View File

@ -38,9 +38,9 @@ export default class PopupStickers extends PopupElement {
this.header.append(this.h6);
this.onClose = () => {
this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup('');
};
});
const div = document.createElement('div');
div.classList.add('sticker-set');

View File

@ -586,7 +586,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
docDiv.classList.add('document-with-thumb');
let imgs: HTMLImageElement[] = [];
if(uploading) {
if(message.pFlags.is_outgoing) {
icoDiv.innerHTML = `<img src="${cacheContext.url}">`;
imgs.push(icoDiv.firstElementChild as HTMLImageElement);
} else {
@ -647,9 +647,9 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
docDiv.prepend(icoDiv);
/* if(!uploading && message.pFlags.is_outgoing) {
if(!uploading && message.pFlags.is_outgoing) {
return docDiv;
} */
}
let downloadDiv: HTMLElement, preloader: ProgressivePreloader = null;
const onLoad = () => {
@ -700,7 +700,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
downloadDiv = docDiv.querySelector('.document-download');
preloader = new ProgressivePreloader();
preloader.attach(downloadDiv, false, appDocsManager.downloading.get(doc.id));
} else if(!(cacheContext.downloaded && !uploading)) {
} else if(!cacheContext.downloaded || uploading) {
downloadDiv = docDiv.querySelector('.document-download');
preloader = message.media.preloader as ProgressivePreloader;

View File

@ -791,7 +791,12 @@ const lang = {
"other_value": "Show More (%d)"
},
//"PeerInfo.Confirm.DeleteGroupConfirmation": "Wait! Deleting this group will remove all members and all messages will be lost. Delete the group anyway?",
"Preview.Dragging.AddItems": {
"one_value": "Add Item",
"other_value": "Add Items"
},
"PreviewSender.CaptionPlaceholder": "Add a caption...",
"PreviewSender.CompressFile": "Send compressed",
"PreviewSender.SendFile": {
"one_value": "Send File",
"other_value": "Send %d Files"

View File

@ -24,7 +24,7 @@ import appPhotosManager from './appPhotosManager';
import appProfileManager from './appProfileManager';
import appStickersManager from './appStickersManager';
import appWebPagesManager from './appWebPagesManager';
import PopupNewMedia from '../../components/popups/newMedia';
import PopupNewMedia, { getCurrentNewMediaPopup } from '../../components/popups/newMedia';
import MarkupTooltip from '../../components/chat/markupTooltip';
import { IS_TOUCH_SUPPORTED } from '../../environment/touchSupport';
import appPollsManager from './appPollsManager';
@ -901,6 +901,7 @@ export class AppImManager {
private attachDragAndDropListeners() {
const drops: ChatDragAndDrop[] = [];
const mediaDrops: ChatDragAndDrop[] = [];
let mounted = false;
const toggle = async(e: DragEvent, mount: boolean) => {
if(mount === mounted) return;
@ -909,63 +910,84 @@ export class AppImManager {
// @ts-ignore
const isFiles = _types.contains ? _types.contains('Files') : _types.indexOf('Files') >= 0;
if(!isFiles || !this.canDrag()) { // * skip dragging text case
const newMediaPopup = getCurrentNewMediaPopup();
if(!isFiles || (!this.canDrag() && !newMediaPopup)) { // * skip dragging text case
counter = 0;
return;
}
if(mount && !drops.length) {
const _dropsContainer = newMediaPopup ? mediaDropsContainer : dropsContainer;
const _drops = newMediaPopup ? mediaDrops : drops;
if(mount && !_drops.length) {
const types: string[] = await getFilesFromEvent(e, true);
const force = isFiles && !types.length; // * can't get file items not from 'drop' on Safari
const foundMedia = types.filter(t => MEDIA_MIME_TYPES_SUPPORTED.has(t)).length;
const foundDocuments = types.length - foundMedia;
// const foundDocuments = types.length - foundMedia;
this.log('drag files', types);
if(types.length || force) {
drops.push(new ChatDragAndDrop(dropsContainer, {
icon: 'dragfiles',
header: 'Chat.DropTitle',
subtitle: 'Chat.DropAsFilesDesc',
onDrop: (e: DragEvent) => {
toggle(e, false);
appImManager.log('drop', e);
appImManager.onDocumentPaste(e, 'document');
}
}));
if(newMediaPopup) {
newMediaPopup.appendDrops(_dropsContainer);
if(types.length || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
header: 'Preview.Dragging.AddItems',
headerArgs: [types.length],
onDrop: (e: DragEvent) => {
toggle(e, false);
appImManager.log('drop', e);
appImManager.onDocumentPaste(e, 'document');
}
}));
}
} else {
if(types.length || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
icon: 'dragfiles',
header: 'Chat.DropTitle',
subtitle: 'Chat.DropAsFilesDesc',
onDrop: (e: DragEvent) => {
toggle(e, false);
appImManager.log('drop', e);
appImManager.onDocumentPaste(e, 'document');
}
}));
}
// if((foundMedia && !foundDocuments) || force) {
if(foundMedia || force) {
_drops.push(new ChatDragAndDrop(_dropsContainer, {
icon: 'dragmedia',
header: 'Chat.DropTitle',
subtitle: 'Chat.DropQuickDesc',
onDrop: (e: DragEvent) => {
toggle(e, false);
appImManager.log('drop', e);
appImManager.onDocumentPaste(e, 'media');
}
}));
}
this.chat.container.append(_dropsContainer);
}
if((foundMedia && !foundDocuments) || force) {
drops.push(new ChatDragAndDrop(dropsContainer, {
icon: 'dragmedia',
header: 'Chat.DropTitle',
subtitle: 'Chat.DropQuickDesc',
onDrop: (e: DragEvent) => {
toggle(e, false);
appImManager.log('drop', e);
appImManager.onDocumentPaste(e, 'media');
}
}));
}
this.chat.container.append(dropsContainer);
}
//if(!mount) return;
SetTransition(dropsContainer, 'is-visible', mount, 200, () => {
SetTransition(_dropsContainer, 'is-visible', mount, 200, () => {
if(!mount) {
drops.forEach(drop => {
_drops.forEach(drop => {
drop.destroy();
});
drops.length = 0;
_drops.length = 0;
}
});
if(mount) {
drops.forEach(drop => {
_drops.forEach(drop => {
drop.setPath();
});
} else {
@ -1003,6 +1025,8 @@ export class AppImManager {
const dropsContainer = document.createElement('div');
dropsContainer.classList.add('drops-container');
const mediaDropsContainer = dropsContainer.cloneNode(true) as HTMLElement;
}
private canDrag() {
@ -1011,7 +1035,8 @@ export class AppImManager {
}
private onDocumentPaste = (e: ClipboardEvent | DragEvent, attachType?: 'media' | 'document') => {
if(!this.canDrag()) return;
const newMediaPopup = getCurrentNewMediaPopup();
if(!this.canDrag() && !newMediaPopup) return;
//console.log('document paste');
//console.log('item', event.clipboardData.getData());
@ -1027,12 +1052,13 @@ export class AppImManager {
getFilesFromEvent(e).then((files: File[]) => {
if(files.length) {
if(/* attachType === 'media' && */files.find(file => !MEDIA_MIME_TYPES_SUPPORTED.has(file.type))) {
attachType = 'document';
if(newMediaPopup) {
newMediaPopup.addFiles(files);
return;
}
const chatInput = this.chat.input;
chatInput.willAttachType = attachType || (MEDIA_MIME_TYPES_SUPPORTED.has(files[0].type) ? 'media' : "document");
chatInput.willAttachType = attachType || (MEDIA_MIME_TYPES_SUPPORTED.has(files[0].type) ? 'media' : 'document');
new PopupNewMedia(this.chat, files, chatInput.willAttachType);
}
});

View File

@ -166,7 +166,7 @@ export class AppMessagesManager {
}
} = {};
private sendSmthLazyLoadQueue = new LazyLoadQueueBase(1);
private sendSmthLazyLoadQueue = new LazyLoadQueueBase(10);
private needSingleMessages: Map<PeerId, Map<number, CancellablePromise<Message>>> = new Map();
private fetchSingleMessagesPromise: Promise<void> = null;
@ -1115,7 +1115,7 @@ export class AppMessagesManager {
};
const inputPeer = appPeersManager.getInputPeerById(peerId);
const invoke = (multiMedia: any[]) => {
const invoke = (multiMedia: InputSingleMedia[]) => {
this.setTyping(peerId, {_: 'sendMessageCancelAction'});
this.sendSmthLazyLoadQueue.push({
@ -1136,15 +1136,15 @@ export class AppMessagesManager {
});
};
const promises: Promise<InputSingleMedia>[] = messages.map((message, idx) => {
return (message.send() as Promise<InputMedia>).then((inputMedia: InputMedia) => {
const promises: Promise<InputSingleMedia>[] = messages.map((message) => {
return (message.send() as Promise<InputMedia>).then((inputMedia) => {
return apiManager.invokeApi('messages.uploadMedia', {
peer: inputPeer,
media: inputMedia
});
})
.then(messageMedia => {
let inputMedia: any;
let inputMedia: InputMedia;
if(messageMedia._ === 'messageMediaPhoto') {
const photo = appPhotosManager.savePhoto(messageMedia.photo);
inputMedia = appPhotosManager.getMediaInput(photo);

View File

@ -663,6 +663,30 @@ $chat-helper-size: 36px;
}
}
}
.drops-container {
--padding: 20px;
--pinned-floating-height: 0px;
top: calc(56px + var(--pinned-floating-height) + var(--padding));
@include respond-to(medium-screens) {
//transition: transform var(--layer-transition);
body.is-right-column-shown & {
//left: calc(var(--right-column-width) / -2);
right: calc(var(--right-column-width));
}
}
@include respond-to(handhelds) {
--padding: 10px;
}
}
.drop {
max-width: 696px;
--wrapper-padding: 15px;
}
}
.chat-input-wrapper {

View File

@ -1039,17 +1039,13 @@ $bubble-beside-button-width: 38px;
&-content {
//max-width: 300px;
position: absolute;
max-width: calc(100% - 1.875rem);
max-width: calc(100% - 1.25rem);
height: auto;
min-height: 32px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&.is-media .reply-content {
max-width: calc(100% - 1.5rem);
}
}
// &:not(.just-media):not(.is-message-empty) .reply {
@ -1068,7 +1064,7 @@ $bubble-beside-button-width: 38px;
margin-bottom: 0;
background-color: var(--message-highlightning-color);
white-space: nowrap;
max-width: 300px;
max-width: 15rem;
@include respond-to(handhelds) {
max-width: calc(100vw - #{$chat-padding-handhelds * 2} - 10px - 100%);
@ -1096,6 +1092,8 @@ $bubble-beside-button-width: 38px;
&-content {
margin-top: 0;
position: relative;
max-width: none !important;
}
&-title,
@ -1620,7 +1618,6 @@ $bubble-beside-button-width: 38px;
color: white;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
height: 1.125rem;
@ -1648,7 +1645,6 @@ $bubble-beside-button-width: 38px;
color: #fff;
text-align: center;
font-size: 2.125rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;

View File

@ -5,11 +5,10 @@
*/
.drops-container {
--padding: 20px;
--pinned-floating-height: 0px;
--padding: 0px;
position: absolute !important;
z-index: 3;
top: calc(56px + var(--pinned-floating-height) + var(--padding));
top: var(--padding);
right: var(--padding);
bottom: var(--padding);
left: var(--padding);
@ -20,19 +19,6 @@
user-select: none;
width: auto !important;
@include respond-to(medium-screens) {
//transition: transform var(--layer-transition);
body.is-right-column-shown & {
//left: calc(var(--right-column-width) / -2);
right: calc(var(--right-column-width));
}
}
@include respond-to(handhelds) {
--padding: 10px;
}
&:not(.is-visible) {
display: none;
}
@ -47,12 +33,13 @@
}
.drop {
--wrapper-padding: -4px;
background-color: var(--surface-color);
position: relative;
//height: 100%;
border-radius: $border-radius-big;
width: 100%;
max-width: 696px;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
@ -65,10 +52,10 @@
&-outline {
&-wrapper {
position: absolute;
top: 15px;
right: 15px;
bottom: 15px;
left: 15px;
top: var(--wrapper-padding);
right: var(--wrapper-padding);
bottom: var(--wrapper-padding);
left: var(--wrapper-padding);
pointer-events: none;
}
@ -101,7 +88,10 @@
&-header {
font-weight: 500;
font-size: 1.25rem;
margin-top: -10px;
&:not(:last-child) {
margin-top: -10px;
}
}
@media only screen and (max-height: 670px) {

View File

@ -16,43 +16,8 @@
//max-height: unquote('min(640px, 100%)');
max-height: 100%;
&.is-media:not(.is-album) {
/* max-height: 425px; */
#{$parent}-photo {
//max-height: 320px;
margin: 0 auto;
img {
object-fit: contain;
}
> div {
display: flex;
justify-content: center;
margin: 0 auto;
}
}
}
&.is-album {
#{$parent}-photo {
margin: 0 auto;
position: relative;
.album-item {
position: absolute;
}
img, video {
object-fit: cover;
width: 100%;
height: 100%;
}
}
}
img, video {
img,
video {
border-radius: inherit;
}
}
@ -94,45 +59,12 @@
&-photo {
max-width: 380px;
//display: flex;
overflow: hidden;
//justify-content: center;
width: fit-content;
// width: fit-content;
width: 100%;
border-radius: $border-radius-medium;
user-select: none;
/* align-items: center; */
.document {
max-width: 100%;
overflow: hidden;
cursor: default;
padding-left: 3.75rem;
height: 4.5rem;
&-name {
font-weight: normal;
width: 100%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
&-ico {
height: 48px;
width: 48px;
font-size: 16px;
font-weight: normal;
line-height: 11px;
letter-spacing: 0;
}
/* &.photo {
.document-ico {
border-radius: $border-radius;
}
} */
}
position: relative;
}
}
@ -175,22 +107,85 @@
padding: 0;
}
.popup-body {
position: relative;
}
.checkbox-field {
margin-bottom: 0;
margin-left: 0;
}
.popup-album,
.popup-container:not(.is-album) .popup-item-media {
.popup-item-album {
position: relative;
.album-item {
position: absolute;
}
img,
video {
object-fit: cover;
width: 100%;
height: 100%;
}
}
.popup-photo > .popup-item-media {
display: flex;
justify-content: center;
margin: 0 auto;
img {
object-fit: contain;
}
}
.popup-photo > .popup-item {
position: relative;
border-radius: inherit;
overflow: hidden;
}
.popup-album + .popup-album,
.popup-container:not(.is-album) .popup-item-media + .popup-item-media {
.popup-photo > .popup-item + .popup-item {
margin-top: .5rem;
}
.drop {
border-radius: 0;
}
.document {
max-width: 100%;
overflow: hidden;
cursor: default;
padding-left: 3.75rem;
height: 4.5rem;
&-name {
font-weight: normal;
width: 100%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
&-ico {
height: 48px;
width: 48px;
font-size: 16px;
font-weight: normal;
line-height: 11px;
letter-spacing: 0;
}
/* &.photo {
.document-ico {
border-radius: $border-radius;
}
} */
}
}
.popup-create-contact {