Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

556 lines
16 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type Chat from "../chat/chat";
import InputField from "../inputField";
import PopupElement from ".";
import Scrollable from "../scrollable";
import { toast } from "../toast";
import { prepareAlbum, wrapDocument } from "../wrappers";
import CheckboxField from "../checkboxField";
import SendContextMenu from "../chat/sendContextMenu";
import { createPosterFromMedia, createPosterFromVideo, onMediaLoad } from "../../helpers/files";
import { MyDocument } from "../../lib/appManagers/appDocsManager";
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";
import rootScope from "../../lib/rootScope";
import RichTextProcessor from "../../lib/richtextprocessor";
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";
import createVideo from "../../helpers/dom/createVideo";
type SendFileParams = Partial<{
file: File,
objectURL: string,
thumb: {
blob: Blob,
url: string,
size: MediaSize
},
width: number,
height: number,
duration: number,
noSound: boolean,
itemDiv: HTMLElement
}>;
let currentPopup: PopupNewMedia;
export function getCurrentNewMediaPopup() {
return currentPopup;
}
export default class PopupNewMedia extends PopupElement {
private input: HTMLElement;
private mediaContainer: HTMLElement;
private groupCheckboxField: CheckboxField;
private mediaCheckboxField: CheckboxField;
private wasInputValue: string;
private willAttach: Partial<{
type: 'media' | 'document',
isMedia: true,
group: boolean,
sendFileDetails: SendFileParams[]
}>;
private inputField: InputField;
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,
sendFileDetails: [],
group: false
};
attachClickEvent(this.btnConfirm, () => this.send(), {listenerSetter: this.listenerSetter});
if(this.chat.type !== 'scheduled') {
const sendMenu = new SendContextMenu({
onSilentClick: () => {
this.chat.input.sendSilent = true;
this.send();
},
onScheduleClick: () => {
this.chat.input.scheduleSending(() => {
this.send();
});
},
openSide: 'bottom-left',
onContextElement: this.btnConfirm,
listenerSetter: this.listenerSetter
});
sendMenu.setPeerId(this.chat.peerId);
this.header.append(sendMenu.sendMenu);
}
this.mediaContainer = document.createElement('div');
this.mediaContainer.classList.add('popup-photo');
const scrollable = new Scrollable(null);
scrollable.container.append(this.mediaContainer);
this.inputField = new InputField({
placeholder: 'PreviewSender.CaptionPlaceholder',
label: 'Caption',
name: 'photo-caption',
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.body.append(scrollable.container);
this.container.append(this.inputField.container);
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.mediaCheckboxField?.label, this.inputField.container].filter(Boolean));
this.willAttach.group = true;
this.groupCheckboxField.setValueSilently(this.willAttach.group);
this.listenerSetter.add(this.groupCheckboxField.input)('change', () => {
const checked = this.groupCheckboxField.checked;
this.willAttach.group = checked;
this.attachFiles();
});
} else if(this.groupCheckboxField) {
this.groupCheckboxField.label.classList.toggle('hide', !good);
}
}
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) => {
const target = e.target as HTMLElement;
if(target !== this.input) {
if(target.tagName === 'INPUT' || target.hasAttribute('contenteditable')) {
return;
}
this.input.focus();
placeCaretAtEnd(this.input);
}
};
private send(force = false) {
if(this.chat.type === 'scheduled' && !force) {
this.chat.input.scheduleSending(() => {
this.send(true);
});
return;
}
let caption = this.inputField.value;
if(caption.length > rootScope.config.caption_length_max) {
toast(I18n.format('Error.PreviewSender.CaptionTooLong', true));
return;
}
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, input} = this.chat;
sendFileDetails.forEach(d => {
d.itemDiv = undefined;
});
const {length} = sendFileDetails;
const sendingParams = this.chat.getMessageSendingParams();
this.iterate((sendFileDetails) => {
if(caption && sendFileDetails.length !== length) {
this.chat.appMessagesManager.sendText(peerId, caption, {
...sendingParams,
clearDraft: true
});
caption = undefined;
}
const w = {
...willAttach,
sendFileDetails
};
this.chat.appMessagesManager.sendAlbum(peerId, w.sendFileDetails.map(d => d.file), Object.assign({
...sendingParams,
caption,
isMedia: isMedia,
clearDraft: true as true
}, w));
caption = undefined;
});
input.replyToMsgId = this.chat.threadId;
input.onMessageSent();
}
private attachMedia(file: File, params: SendFileParams, itemDiv: HTMLElement) {
itemDiv.classList.add('popup-item-media');
const isVideo = file.type.startsWith('video/');
let promise: Promise<void>;
if(isVideo) {
const video = createVideo();
const source = document.createElement('source');
source.src = params.objectURL = URL.createObjectURL(file);
video.autoplay = true;
video.controls = false;
video.muted = true;
video.addEventListener('timeupdate', () => {
video.pause();
}, {once: true});
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);
}),
createPosterFromMedia(img).then(thumb => {
params.thumb = {
url: URL.createObjectURL(thumb.blob),
...thumb
};
})
]).then(() => {
resolve();
});
} else {
resolve();
}
};
});
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 || file.size < 20e6) {
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;
if(params.objectURL) {
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();
});
}
}