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.

1417 lines
48 KiB

3 years ago
import { deferredPromise } from "../helpers/cancellablePromise";
import mediaSizes from "../helpers/mediaSizes";
import { isTouchSupported } from "../helpers/touchSupport";
import { isSafari } from "../helpers/userAgent";
import appDocsManager, { MyDocument } from "../lib/appManagers/appDocsManager";
import appImManager from "../lib/appManagers/appImManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager";
import { logger } from "../lib/logger";
import VideoPlayer from "../lib/mediaPlayer";
import { RichTextProcessor } from "../lib/richtextprocessor";
import rootScope from "../lib/rootScope";
import { cancelEvent, fillPropertyValue, generatePathData } from "../helpers/dom";
3 years ago
import animationIntersector from "./animationIntersector";
import appMediaPlaybackController from "./appMediaPlaybackController";
import AvatarElement from "./avatar";
import ButtonIcon from "./buttonIcon";
import { ButtonMenuItemOptions } from "./buttonMenu";
import ButtonMenuToggle from "./buttonMenuToggle";
import { LazyLoadQueueBase } from "./lazyLoadQueue";
import PopupForward from "./popups/forward";
import ProgressivePreloader from "./preloader";
import Scrollable from "./scrollable";
import appSidebarRight, { AppSidebarRight } from "./sidebarRight";
import SwipeHandler from "./swipeHandler";
import { months, ONE_DAY } from "../helpers/date";
import { SearchSuperContext } from "./appSearchSuper.";
import { Message, PhotoSize } from "../layer";
import { forEachReverse } from "../helpers/array";
import AppSharedMediaTab from "./sidebarRight/tabs/sharedMedia";
import findUpClassName from "../helpers/dom/findUpClassName";
import renderImageFromUrl from "../helpers/dom/renderImageFromUrl";
3 years ago
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
// TODO: видео в мобильной вёрстке, если показываются элементы управления: если свайпнуть в сторону, то элементы вернутся на место, т.е. прыгнут - это не ок, надо бы замаскировать
const MEDIA_VIEWER_CLASSNAME = 'media-viewer';
function transition(t: number) {
return 1 - ((1 - t) ** 3.5);
}
class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType extends string, TargetType extends object> {
public wholeDiv: HTMLElement;
protected overlaysDiv: HTMLElement;
protected author: {[k in 'container' | 'avatarEl' | 'nameEl' | 'date']: HTMLElement} = {} as any;
protected content: {[k in 'main' | 'container' | 'media' | 'mover' | ContentAdditionType]: HTMLElement} = {} as any;
protected mover: HTMLCanvasElement;
public buttons: {[k in 'download' | 'close' | 'prev' | 'next' | 'mobile-close' | ButtonsAdditionType]: HTMLElement} = {} as any;
protected tempId = 0;
protected preloader: ProgressivePreloader = null;
protected preloaderStreamable: ProgressivePreloader = null;
protected lastTarget: HTMLElement = null;
protected prevTargets: TargetType[] = [];
protected nextTargets: TargetType[] = [];
//protected targetContainer: HTMLElement = null;
//protected loadMore: () => void = null;
public log: ReturnType<typeof logger>;
protected isFirstOpen = true;
protected loadMediaPromiseUp: Promise<void> = null;
protected loadMediaPromiseDown: Promise<void> = null;
protected loadedAllMediaUp = false;
protected loadedAllMediaDown = false;
protected reverse = false; // reverse means next = higher msgid
protected needLoadMore = true;
protected pageEl = document.getElementById('page-chats') as HTMLDivElement;
protected setMoverPromise: Promise<void>;
protected setMoverAnimationPromise: Promise<void>;
protected lazyLoadQueue: LazyLoadQueueBase;
protected highlightSwitchersTimeout: number;
protected onDownloadClick: (e: MouseEvent) => void;
protected onPrevClick: (target: TargetType) => void;
protected onNextClick: (target: TargetType) => void;
protected loadMoreMedia: (older: boolean) => Promise<void>;
protected animatingContext: {
width: number,
height: number,
startWidth: number,
startHeight: number,
media: HTMLImageElement | HTMLVideoElement,
diffX: number,
diffY: number,
startTime: number,
borderRadius: number[]
};
constructor(topButtons: Array<keyof AppMediaViewerBase<ContentAdditionType, ButtonsAdditionType, TargetType>['buttons']>) {
this.log = logger('AMV');
this.preloader = new ProgressivePreloader();
this.preloaderStreamable = new ProgressivePreloader({
cancelable: false,
streamable: true
});
this.preloader.construct();
this.preloaderStreamable.construct();
this.lazyLoadQueue = new LazyLoadQueueBase();
this.wholeDiv = document.createElement('div');
this.wholeDiv.classList.add(MEDIA_VIEWER_CLASSNAME + '-whole');
this.overlaysDiv = document.createElement('div');
this.overlaysDiv.classList.add('overlays');
const mainDiv = document.createElement('div');
mainDiv.classList.add(MEDIA_VIEWER_CLASSNAME);
// * author
this.author.container = document.createElement('div');
this.author.container.classList.add(MEDIA_VIEWER_CLASSNAME + '-author', 'no-select');
this.author.avatarEl = new AvatarElement();
this.author.avatarEl.classList.add(MEDIA_VIEWER_CLASSNAME + '-userpic', 'avatar-44');
this.author.nameEl = document.createElement('div');
this.author.nameEl.classList.add(MEDIA_VIEWER_CLASSNAME + '-name');
this.author.date = document.createElement('div');
this.author.date.classList.add(MEDIA_VIEWER_CLASSNAME + '-date');
this.author.container.append(this.author.avatarEl, this.author.nameEl, this.author.date);
// * buttons
const buttonsDiv = document.createElement('div');
buttonsDiv.classList.add(MEDIA_VIEWER_CLASSNAME + '-buttons');
topButtons.concat(['download', 'close']).forEach(name => {
const button = ButtonIcon(name);
this.buttons[name] = button;
buttonsDiv.append(button);
});
// * content
this.content.main = document.createElement('div');
this.content.main.classList.add(MEDIA_VIEWER_CLASSNAME + '-content');
this.content.container = document.createElement('div');
this.content.container.classList.add(MEDIA_VIEWER_CLASSNAME + '-container');
this.content.media = document.createElement('div');
this.content.media.classList.add(MEDIA_VIEWER_CLASSNAME + '-media');
this.content.container.append(this.content.media);
this.content.main.append(this.content.container);
mainDiv.append(this.author.container, buttonsDiv, this.content.main);
this.overlaysDiv.append(mainDiv);
// * overlays end
this.buttons["mobile-close"] = ButtonIcon('close', {onlyMobile: true});
this.buttons.prev = document.createElement('div');
this.buttons.prev.className = `${MEDIA_VIEWER_CLASSNAME}-switcher ${MEDIA_VIEWER_CLASSNAME}-switcher-left`;
this.buttons.prev.innerHTML = `<span class="tgico-down ${MEDIA_VIEWER_CLASSNAME}-prev-button"></span>`;
this.buttons.next = document.createElement('div');
this.buttons.next.className = `${MEDIA_VIEWER_CLASSNAME}-switcher ${MEDIA_VIEWER_CLASSNAME}-switcher-right`;
this.buttons.next.innerHTML = `<span class="tgico-down ${MEDIA_VIEWER_CLASSNAME}-next-button"></span>`;
this.wholeDiv.append(this.overlaysDiv, this.buttons['mobile-close'], this.buttons.prev, this.buttons.next);
// * constructing html end
this.setNewMover();
}
protected setListeners() {
this.buttons.download.addEventListener('click', this.onDownloadClick);
[this.buttons.close, this.buttons["mobile-close"], this.preloaderStreamable.preloader].forEach(el => {
el.addEventListener('click', this.close.bind(this));
});
this.buttons.prev.addEventListener('click', (e) => {
cancelEvent(e);
if(this.setMoverPromise) return;
const target = this.prevTargets.pop();
if(target) {
this.onPrevClick(target);
} else {
this.buttons.prev.style.display = 'none';
}
});
this.buttons.next.addEventListener('click', (e) => {
cancelEvent(e);
if(this.setMoverPromise) return;
let target = this.nextTargets.shift();
if(target) {
this.onNextClick(target);
} else {
this.buttons.next.style.display = 'none';
}
});
this.wholeDiv.addEventListener('click', this.onClick);
if(isTouchSupported) {
const swipeHandler = new SwipeHandler(this.wholeDiv, (xDiff, yDiff) => {
if(VideoPlayer.isFullScreen()) {
return;
}
//console.log(xDiff, yDiff);
const percents = Math.abs(xDiff) / appPhotosManager.windowW;
if(percents > .2 || xDiff > 125) {
//console.log('will swipe', xDiff);
if(xDiff < 0) {
this.buttons.prev.click();
} else {
this.buttons.next.click();
}
return true;
}
const percentsY = Math.abs(yDiff) / appPhotosManager.windowH;
if(percentsY > .2 || yDiff > 125) {
this.buttons.close.click();
return true;
}
return false;
}, (evt) => {
// * Fix for seek input
if((evt.target as HTMLElement).tagName === 'INPUT' || findUpClassName(evt.target, 'media-viewer-caption')) {
3 years ago
return false;
}
return true;
});
}
}
protected setBtnMenuToggle(buttons: ButtonMenuItemOptions[]) {
const btnMenuToggle = ButtonMenuToggle({onlyMobile: true}, 'bottom-left', buttons);
this.wholeDiv.append(btnMenuToggle);
}
public close(e?: MouseEvent) {
if(e) {
cancelEvent(e);
}
if(this.setMoverAnimationPromise) return;
this.lazyLoadQueue.clear();
const promise = this.setMoverToTarget(this.lastTarget, true).then(({onAnimationEnd}) => onAnimationEnd);
this.lastTarget = null;
this.prevTargets = [];
this.nextTargets = [];
this.loadedAllMediaUp = this.loadedAllMediaDown = false;
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null;
this.setMoverPromise = null;
this.tempId = -1;
/* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) {
3 years ago
promise.then(() => {
appSidebarRight.forwardTab.closeBtn.click();
});
} */
window.removeEventListener('keydown', this.onKeyDown);
promise.finally(() => {
this.wholeDiv.remove();
rootScope.overlayIsActive = false;
animationIntersector.checkAnimations(false);
});
return promise;
}
onClick = (e: MouseEvent) => {
if(this.setMoverAnimationPromise) return;
const target = e.target as HTMLElement;
if(target.tagName === 'A') return;
3 years ago
cancelEvent(e);
if(isTouchSupported) {
if(this.highlightSwitchersTimeout) {
clearTimeout(this.highlightSwitchersTimeout);
} else {
this.wholeDiv.classList.add('highlight-switchers');
}
this.highlightSwitchersTimeout = window.setTimeout(() => {
this.wholeDiv.classList.remove('highlight-switchers');
this.highlightSwitchersTimeout = 0;
}, 3e3);
return;
}
let mover: HTMLElement = null;
['media-viewer-mover', 'media-viewer-buttons', 'media-viewer-author', 'media-viewer-caption'].find(s => {
try {
mover = findUpClassName(target, s);
if(mover) return true;
} catch(err) {return false;}
});
if(/* target === this.mediaViewerDiv */!mover || target.tagName === 'IMG' || target.tagName === 'image') {
3 years ago
this.buttons.close.click();
}
};
onKeyDown = (e: KeyboardEvent) => {
//this.log('onKeyDown', e);
if(e.key === 'Escape') {
3 years ago
this.close();
} else if(e.key === 'ArrowRight') {
3 years ago
this.buttons.next.click();
} else if(e.key === 'ArrowLeft') {
3 years ago
this.buttons.prev.click();
}
};
protected async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) {
const mover = this.mover;
if(!target) {
target = this.content.media;
}
if(!closing) {
mover.innerHTML = '';
//mover.append(this.buttons.prev, this.buttons.next);
}
this.removeCenterFromMover(mover);
const wasActive = fromRight !== 0;
const delay = rootScope.settings.animationsEnabled ? (wasActive ? 350 : 200) : 0;
//let delay = wasActive ? 350 : 10000;
/* if(wasActive) {
this.moveTheMover(mover);
mover = this.setNewMover();
} */
/* if(DEBUG) {
this.log('setMoverToTarget', target, closing, wasActive, fromRight);
} */
let realParent: HTMLElement;
let rect: DOMRect;
if(target) {
if(target instanceof AvatarElement) {
realParent = target;
rect = target.getBoundingClientRect();
} else {
realParent = target.parentElement as HTMLElement;
rect = target.getBoundingClientRect();
}
}
const containerRect = this.content.media.getBoundingClientRect();
let transform = '';
let left: number;
let top: number;
if(wasActive) {
left = fromRight === 1 ? appPhotosManager.windowW : -containerRect.width;
top = containerRect.top;
} else {
left = rect.left;
top = rect.top;
}
transform += `translate3d(${left}px,${top}px,0) `;
/* if(wasActive) {
left = fromRight === 1 ? appPhotosManager.windowW / 2 : -(containerRect.width + appPhotosManager.windowW / 2);
transform += `translate(${left}px,-50%) `;
} else {
left = rect.left - (appPhotosManager.windowW / 2);
top = rect.top - (appPhotosManager.windowH / 2);
transform += `translate(${left}px,${top}px) `;
} */
//mover.style.width = containerRect.width + 'px';
//mover.style.height = containerRect.height + 'px';
const scaleX = rect.width / containerRect.width;
const scaleY = rect.height / containerRect.height;
let borderRadius: any = window.getComputedStyle(realParent).getPropertyValue('border-radius');
const brSplitted = fillPropertyValue(borderRadius) as string[];
borderRadius = brSplitted.map(v => parseInt(v));
//let borderRadius = '0px 0px 0px 0px';
mover.style.transform = transform;
/* if(wasActive) {
this.log('setMoverToTarget', mover.style.transform);
} */
let path: SVGPathElement;
const isOut = target.classList.contains('is-out');
const deferred = this.setMoverAnimationPromise = deferredPromise<void>();
const ret = {onAnimationEnd: deferred};
this.setMoverAnimationPromise.then(() => {
this.setMoverAnimationPromise = null;
});
if(!closing) {
let mediaElement: HTMLImageElement | HTMLVideoElement;
if(target.tagName === 'DIV' || target.tagName === 'AVATAR-ELEMENT') { // useContainerAsTarget
if(target.firstElementChild) {
mediaElement = target.firstElementChild as HTMLImageElement;
}
/* mediaElement = new Image();
src = target.style.backgroundImage.slice(5, -2); */
} else if(target instanceof HTMLImageElement) {
mediaElement = target;
} else if(target instanceof HTMLVideoElement) {
mediaElement = target;
}
const naturalWidth = (mediaElement as HTMLImageElement).naturalWidth || (mediaElement as HTMLVideoElement).videoWidth;
const naturalHeight = (mediaElement as HTMLImageElement).naturalHeight || (mediaElement as HTMLVideoElement).videoHeight;
const context = this.animatingContext = {
width: this.mover.width,
height: this.mover.height,
startWidth: rect.width,
startHeight: rect.height,
diffX: 0,
diffY: 0,
media: mediaElement,
startTime: Date.now(),
borderRadius
};
let left = rect.left, top = rect.top;
if(mediaElement.width !== naturalWidth) {
const diff = context.diffX = (naturalWidth - mediaElement.width) / 2;
left -= diff;
}
if(mediaElement.height !== naturalHeight) {
const diff = context.diffY = (naturalHeight - mediaElement.height) / 2;
top -= diff;
}
transform = `translate3d(${left}px,${top}px,0) `;
mover.style.transform = transform;
mover.style.display = '';
this.beginMagic(context, delay);
//this.renderMedia(context, 0);
window.requestAnimationFrame(() => {
mover.classList.add(wasActive ? 'moving' : 'active');
});
} else {
/* if(mover.classList.contains('center')) {
mover.classList.remove('center');
void mover.offsetLeft; // reflow
} */
if(target.classList.contains('media-viewer-media')) {
mover.classList.add('hiding');
}
setTimeout(() => {
this.wholeDiv.classList.remove('active');
}, 0);
setTimeout(() => {
mover.style.borderRadius = borderRadius;
if(mover.firstElementChild) {
(mover.firstElementChild as HTMLElement).style.borderRadius = borderRadius;
}
}, delay / 2);
setTimeout(() => {
mover.innerHTML = '';
mover.classList.remove('moving', 'active', 'hiding');
mover.style.cssText = 'display: none;';
deferred.resolve();
}, delay);
return ret;
}
//await new Promise((resolve) => setTimeout(resolve, 0));
//await new Promise((resolve) => window.requestAnimationFrame(resolve));
// * одного RAF'а недостаточно, иногда анимация с одним не срабатывает (преимущественно на мобильных)
await new Promise((resolve) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
// чтобы проверить установленную позицию - раскомментировать
throw '';
//await new Promise((resolve) => setTimeout(resolve, 5e3));
mover.style.transform = `translate3d(${containerRect.left}px,${containerRect.top}px,0)`;
//mover.style.transform = `translate(-50%,-50%) scale(1,1)`;
//throw '';
mover.dataset.timeout = '' + setTimeout(() => {
mover.classList.remove('moving');
// эти строки нужны для установки центральной позиции, в случае ресайза это будет нужно
mover.classList.add('center', 'no-transition');
/* mover.style.left = mover.style.top = '50%';
mover.style.transform = 'translate(-50%, -50%)';
void mover.offsetLeft; // reflow */
// это уже нужно для будущих анимаций
mover.classList.add('active');
delete mover.dataset.timeout;
deferred.resolve();
}, delay);
return ret;
}
protected beginMagic(context: AppMediaViewer['animatingContext'], delay: number) {
const start = Date.now();
const step = () => {
const diff = Date.now() - start;
let progress = delay ? Math.min(1, diff / delay) : 1;
this.renderMedia(context, progress);
if(diff < delay) window.requestAnimationFrame(step);
};
window.requestAnimationFrame(step);
//step();
this.renderMedia(context, 0);
}
protected renderMedia(context: AppMediaViewer['animatingContext'], percents: number) {
const ctx = this.mover.getContext('2d');
//percents = transition(percents);
const width = context.startWidth + (context.width - context.startWidth) * percents;
const height = context.startHeight + (context.height - context.startHeight) * percents;
const x = context.diffX - (context.diffX * percents);
const y = context.diffY - (context.diffY * percents);
const borderRadius = context.borderRadius.map(v => v - v * percents);
const [brTl, brTr, brBr, brBl] = borderRadius;
/* ctx.clearRect(0, 0, context.width, context.height);
ctx.closePath(); */
ctx.save();
ctx.beginPath();
ctx.moveTo(x + brTl, y);
ctx.lineTo(x + width - brTr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + brTr);
ctx.lineTo(x + width, y + height - brBr);
ctx.quadraticCurveTo(x + width, y + height, x + width - brBr, y + height);
ctx.lineTo(x + brBl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - brBl);
ctx.lineTo(x, y + brTl);
ctx.quadraticCurveTo(x, y, x + brTl, y);
ctx.closePath();
ctx.clip();
ctx.drawImage(context.media, x, y, width, height);
ctx.restore();
}
protected setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) {
/* let media = aspecter.firstElementChild;
let proportion: number;
if(media instanceof HTMLImageElement) {
proportion = media.naturalWidth / media.naturalHeight;
} else if(media instanceof HTMLVideoElement) {
proportion = media.videoWidth / media.videoHeight;
} */
const proportion = containerRect.width / containerRect.height;
let {width, height} = rect;
/* if(proportion === 1) {
3 years ago
aspecter.style.cssText = '';
} else { */
if(proportion > 0) {
width = height * proportion;
} else {
height = width * proportion;
}
//this.log('will set style aspecter:', `width: ${width}px; height: ${height}px; transform: scale(${containerRect.width / width}, ${containerRect.height / height});`);
aspecter.style.cssText = `width: ${width}px; height: ${height}px; transform: scale3d(${containerRect.width / width}, ${containerRect.height / height}, 1);`;
//}
}
protected sizeTailPath(path: SVGPathElement, rect: DOMRect, scaleX: number, delay: number, upscale: boolean, isOut: boolean, borderRadius: string) {
const start = Date.now();
const {width, height} = rect;
delay = delay / 2;
const br = borderRadius.split(' ').map(v => parseInt(v));
const step = () => {
const diff = Date.now() - start;
let progress = delay ? diff / delay : 1;
if(progress > 1) progress = 1;
if(upscale) progress = 1 - progress;
const _br: [number, number, number, number] = br.map(v => v * progress) as any;
let d: string;
if(isOut) d = generatePathData(0, 0, width - (9 / scaleX * progress), height, ..._br);
else d = generatePathData(9 / scaleX * progress, 0, width/* width - (9 / scaleX * progress) */, height, ..._br);
path.setAttributeNS(null, 'd', d);
if(diff < delay) window.requestAnimationFrame(step);
};
//window.requestAnimationFrame(step);
step();
}
protected removeCenterFromMover(mover: HTMLElement) {
if(mover.classList.contains('center')) {
//const rect = mover.getBoundingClientRect();
const rect = this.content.media.getBoundingClientRect();
mover.style.transform = `translate3d(${rect.left}px,${rect.top}px,0)`;
mover.classList.remove('center');
void mover.offsetLeft; // reflow
mover.classList.remove('no-transition');
}
}
protected moveTheMover(mover: HTMLElement, toLeft = true) {
const windowW = appPhotosManager.windowW;
this.removeCenterFromMover(mover);
//mover.classList.remove('active');
mover.classList.add('moving');
if(mover.dataset.timeout) { // и это тоже всё из-за скейла видео, так бы это не нужно было
clearTimeout(+mover.dataset.timeout);
}
const rect = mover.getBoundingClientRect();
const newTransform = mover.style.transform.replace(/translate3d\((.+?),/, (match, p1) => {
const x = toLeft ? -rect.width : windowW;
//const x = toLeft ? -(rect.right + (rect.width / 2)) : windowW / 2;
return match.replace(p1, x + 'px');
});
////////this.log('set newTransform:', newTransform, mover.style.transform, toLeft);
mover.style.transform = newTransform;
setTimeout(() => {
mover.remove();
}, 350);
}
protected setNewMover() {
const newMover = document.createElement('canvas');
newMover.classList.add('media-viewer-canvas');
if(this.mover) {
const oldMover = this.mover;
oldMover.parentElement.append(newMover);
} else {
this.wholeDiv.append(newMover);
}
return this.mover = newMover;
}
/* public isElementVisible(container: HTMLElement, target: HTMLElement) {
const rect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
return targetRect.bottom > rect.top && targetRect.top < rect.bottom;
} */
protected updateMediaSource(target: HTMLElement, url: string, tagName: 'video' | 'img') {
//if(target instanceof SVGSVGElement) {
const el = target.querySelector(tagName) as HTMLElement;
renderImageFromUrl(el, url);
/* } else {
} */
}
protected setAuthorInfo(fromId: number, timestamp: number) {
const date = new Date();
const time = new Date(timestamp * 1000);
const now = date.getTime() / 1000;
const timeStr = time.getHours() + ':' + ('0' + time.getMinutes()).slice(-2);
let dateStr: string;
if((now - timestamp) < ONE_DAY && date.getDate() === time.getDate()) { // if the same day
3 years ago
dateStr = 'Today';
} else if((now - timestamp) < (ONE_DAY * 2) && (date.getDate() - 1) === time.getDate()) { // yesterday
3 years ago
dateStr = 'Yesterday';
} else if(date.getFullYear() !== time.getFullYear()) { // different year
3 years ago
dateStr = months[time.getMonth()].slice(0, 3) + ' ' + time.getDate() + ', ' + time.getFullYear();
} else {
dateStr = months[time.getMonth()].slice(0, 3) + ' ' + time.getDate();
}
this.author.date.innerText = dateStr + ' at ' + timeStr;
const name = appPeersManager.getPeerTitle(fromId);
this.author.nameEl.innerHTML = name;
let oldAvatar = this.author.avatarEl;
this.author.avatarEl = (this.author.avatarEl.cloneNode() as AvatarElement);
this.author.avatarEl.setAttribute('peer', '' + fromId);
oldAvatar.parentElement.replaceChild(this.author.avatarEl, oldAvatar);
}
protected async _openMedia(media: any, timestamp: number, fromId: number, fromRight: number, target?: HTMLElement, reverse = false,
prevTargets: TargetType[] = [], nextTargets: TargetType[] = [], needLoadMore = true) {
if(this.setMoverPromise) return this.setMoverPromise;
/* if(DEBUG) {
this.log('openMedia:', media, fromId, prevTargets, nextTargets);
} */
this.setAuthorInfo(fromId, timestamp);
const isVideo = (media as MyDocument).type === 'video' || (media as MyDocument).type === 'gif';
3 years ago
if(this.isFirstOpen) {
//this.targetContainer = targetContainer;
this.prevTargets = prevTargets;
this.nextTargets = nextTargets;
this.reverse = reverse;
this.needLoadMore = needLoadMore;
this.isFirstOpen = false;
//this.loadMore = loadMore;
/* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) {
3 years ago
appSidebarRight.forwardTab.closeBtn.click();
await new Promise((resolve) => setTimeout(resolve, 200));
} */
}
/* if(this.nextTargets.length < 10 && this.loadMore) {
this.loadMore();
} */
//if(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null;
//if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null;
this.buttons.prev.classList.toggle('hide', !this.prevTargets.length);
this.buttons.next.classList.toggle('hide', !this.nextTargets.length);
const container = this.content.media;
const useContainerAsTarget = !target;
if(useContainerAsTarget) target = container;
this.lastTarget = target;
const tempId = ++this.tempId;
if(this.needLoadMore) {
if(this.nextTargets.length < 20) {
this.loadMoreMedia(!this.reverse);
}
if(this.prevTargets.length < 20) {
this.loadMoreMedia(this.reverse);
}
}
if(container.firstElementChild) {
container.innerHTML = '';
}
// ok set
const wasActive = fromRight !== 0;
if(wasActive) {
this.moveTheMover(this.mover, fromRight === 1);
this.setNewMover();
} else {
window.addEventListener('keydown', this.onKeyDown);
const mainColumns = this.pageEl.querySelector('#main-columns');
this.pageEl.insertBefore(this.wholeDiv, mainColumns);
void this.wholeDiv.offsetLeft; // reflow
this.wholeDiv.classList.add('active');
rootScope.overlayIsActive = true;
animationIntersector.checkAnimations(true);
}
////////this.log('wasActive:', wasActive);
const mover = this.mover;
//const maxWidth = appPhotosManager.windowW - 16;
const maxWidth = mediaSizes.isMobile ? this.pageEl.scrollWidth : this.pageEl.scrollWidth - 16;
const maxHeight = appPhotosManager.windowH - 100;
const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(media);
if(gotThumb) {
container.append(gotThumb.image);
}
const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight);
this.mover.width = (size as PhotoSize.photoSize).w;
this.mover.height = (size as PhotoSize.photoSize).h;
// need after setAttachmentSize
/* if(useContainerAsTarget) {
target = target.querySelector('img, video') || target;
} */
//return;
const preloader = media.supportsStreaming ? this.preloaderStreamable : this.preloader;
let setMoverPromise: Promise<void>;
if(isVideo) {
////////this.log('will wrap video', media, size);
// потому что для safari нужно создать элемент из event'а
const video = document.createElement('video');
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
//if(wasActive) return;
//return;
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
//const video = mover.querySelector('video') || document.createElement('video');
const moverVideo = mover.querySelector('video');
if(moverVideo) {
moverVideo.remove();
}
//video.src = '';
video.setAttribute('playsinline', 'true');
// * fix for playing video if viewer is closed (https://contest.com/javascript-web-bonus/entry1425#issue11629)
video.addEventListener('timeupdate', () => {
if(this.tempId !== tempId) {
3 years ago
video.pause();
}
});
if(isSafari) {
// test stream
// video.controls = true;
video.autoplay = true;
}
if(media.type === 'gif') {
3 years ago
video.muted = true;
video.autoplay = true;
video.loop = true;
}
if(!video.parentElement) {
div.append(video);
}
const canPlayThrough = new Promise((resolve) => {
video.addEventListener('canplay', resolve, {once: true});
});
const createPlayer = () => {
if(media.type !== 'gif') {
3 years ago
video.dataset.ckin = 'default';
video.dataset.overlay = '1';
// fix for simultaneous play
appMediaPlaybackController.pause();
appMediaPlaybackController.willBePlayedMedia = null;
Promise.all([canPlayThrough, onAnimationEnd]).then(() => {
if(this.tempId !== tempId) {
3 years ago
return;
}
const player = new VideoPlayer(video, true, media.supportsStreaming);
/* div.append(video);
mover.append(player.wrapper); */
});
}
};
if(media.supportsStreaming) {
onAnimationEnd.then(() => {
if(video.readyState < video.HAVE_FUTURE_DATA) {
preloader.attach(mover, true);
}
/* canPlayThrough.then(() => {
preloader.detach();
}); */
});
const attachCanPlay = () => {
video.addEventListener('canplay', () => {
//this.log('video waited and progress loaded');
preloader.detach();
video.parentElement.classList.remove('is-buffering');
}, {once: true});
};
video.addEventListener('waiting', (e) => {
const loading = video.networkState === video.NETWORK_LOADING;
const isntEnoughData = video.readyState < video.HAVE_FUTURE_DATA;
//this.log('video waiting for progress', loading, isntEnoughData);
if(loading && isntEnoughData) {
attachCanPlay();
preloader.attach(mover, true);
// поставлю класс для плеера, чтобы убрать большую иконку пока прелоадер на месте
video.parentElement.classList.add('is-buffering');
}
});
attachCanPlay();
}
//if(!video.src || media.url !== video.src) {
3 years ago
const load = () => {
const promise = media.supportsStreaming ? Promise.resolve() : appDocsManager.downloadDoc(media);
if(!media.supportsStreaming) {
onAnimationEnd.then(() => {
if(!media.url) {
preloader.attach(mover, true, promise);
}
});
}
(promise as Promise<any>).then(async() => {
if(this.tempId !== tempId) {
3 years ago
this.log.warn('media viewer changed video');
return;
}
const url = media.url;
if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists
//if(!video.parentElement) {
div.firstElementChild.lastElementChild.append(video);
//}
this.updateMediaSource(mover, url, 'video');
} else {
renderImageFromUrl(video, url);
}
createPlayer();
});
return promise;
};
this.lazyLoadQueue.unshift({load});
//} else createPlayer();
});
} else {
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
//if(wasActive) return;
//return;
const load = () => {
const cancellablePromise = appPhotosManager.preloadPhoto(media.id, size);
onAnimationEnd.then(() => {
if(!media.url) {
this.preloader.attach(mover, true, cancellablePromise);
}
});
Promise.all([onAnimationEnd, cancellablePromise]).then(() => {
if(this.tempId !== tempId) {
this.log.warn('media viewer changed photo');
return;
}
///////this.log('indochina', blob);
const url = media.url;
if(target instanceof SVGSVGElement) {
this.updateMediaSource(target, url, 'img');
this.updateMediaSource(mover, url, 'img');
if(mediaSizes.isMobile) {
const imgs = mover.querySelectorAll('img');
if(imgs && imgs.length) {
imgs.forEach(img => {
img.classList.remove('thumbnail'); // может здесь это вообще не нужно
});
}
}
} else {
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
let image = new Image();
//this.log('will renderImageFromUrl:', image, div, target);
renderImageFromUrl(image, url, () => {
if(div.firstElementChild?.tagName === 'IMG') {
div.firstElementChild.remove();
}
div.append(image);
});
}
this.preloader.detach();
}).catch(err => {
this.log.error(err);
});
return cancellablePromise;
};
//this.lazyLoadQueue.unshift({load});
});
}
return this.setMoverPromise = setMoverPromise.catch(() => {
this.setMoverAnimationPromise = null;
}).finally(() => {
this.setMoverPromise = null;
});
}
}
type AppMediaViewerTargetType = {
element: HTMLElement,
mid: number,
peerId: number
};
export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> {
public currentMessageId = 0;
public currentPeerId = 0;
public searchContext: SearchSuperContext;
constructor() {
super(['delete', 'forward']);
/* const stub = document.createElement('div');
stub.classList.add(MEDIA_VIEWER_CLASSNAME + '-stub');
this.content.main.prepend(stub); */
this.content.caption = document.createElement('div');
this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption'/* , 'media-viewer-stub' */);
new Scrollable(this.content.caption);
//this.content.main.append(this.content.caption);
this.wholeDiv.append(this.content.caption);
this.setBtnMenuToggle([{
icon: 'forward',
text: 'Forward',
onClick: this.onForwardClick
}, {
icon: 'download',
text: 'MediaViewer.Context.Download',
3 years ago
onClick: this.onDownloadClick
}, {
icon: 'delete danger btn-disabled',
text: 'Delete',
onClick: () => {}
}]);
// * constructing html end
this.setListeners();
}
protected setListeners() {
super.setListeners();
this.buttons.forward.addEventListener('click', this.onForwardClick);
this.author.container.addEventListener('click', this.onAuthorClick);
}
/* public close(e?: MouseEvent) {
const good = !this.setMoverAnimationPromise;
const promise = super.close(e);
if(good) { // clear
this.currentMessageId = 0;
this.peerId = 0;
}
return promise;
} */
onPrevClick = (target: AppMediaViewerTargetType) => {
this.nextTargets.unshift({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId});
this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, -1);
};
onNextClick = (target: AppMediaViewerTargetType) => {
this.prevTargets.push({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId});
this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, 1);
};
onForwardClick = () => {
if(this.currentMessageId) {
//appSidebarRight.forwardTab.open([this.currentMessageId]);
new PopupForward(this.currentPeerId, [this.currentMessageId], () => {
return this.close();
});
}
};
onAuthorClick = (e: MouseEvent) => {
if(this.currentMessageId && this.currentMessageId !== Number.MAX_SAFE_INTEGER) {
const mid = this.currentMessageId;
const peerId = this.currentPeerId;
const threadId = this.searchContext.threadId;
this.close(e)
//.then(() => mediaSizes.isMobile ? appSidebarRight.sharedMediaTab.closeBtn.click() : Promise.resolve())
.then(() => {
if(mediaSizes.isMobile) {
const tab = appSidebarRight.getTab(AppSharedMediaTab);
if(tab) {
tab.close();
}
3 years ago
}
const message = appMessagesManager.getMessageByPeer(peerId, mid);
appImManager.setInnerPeer(message.peerId, mid, threadId ? 'discussion' : undefined, threadId);
});
}
};
onDownloadClick = () => {
const message = appMessagesManager.getMessageByPeer(this.currentPeerId, this.currentMessageId);
if(message.media.photo) {
appPhotosManager.savePhotoFile(message.media.photo, appImManager.chat.bubbles.lazyLoadQueue.queueId);
} else {
let document: MyDocument = null;
if(message.media.webpage) document = message.media.webpage.document;
else document = message.media.document;
if(document) {
//console.log('will save document:', document);
appDocsManager.saveDocFile(document, appImManager.chat.bubbles.lazyLoadQueue.queueId);
}
}
};
// нет смысла делать проверку для reverse и loadMediaPromise
protected loadMoreMedia = (older = true) => {
//if(!older && this.reverse) return;
if(older && this.loadedAllMediaDown) return Promise.resolve();
else if(!older && this.loadedAllMediaUp) return Promise.resolve();
if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown;
else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp;
const loadCount = 50;
const backLimit = older ? 0 : loadCount;
let maxId = this.currentMessageId;
let anchor: AppMediaViewerTargetType;
if(older) {
anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1];
} else {
anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0];
}
if(anchor) maxId = anchor.mid;
if(!older) maxId = appMessagesManager.incrementMessageId(maxId, 1);
const promise = appMessagesManager.getSearch({
peerId: this.searchContext.peerId,
query: this.searchContext.query,
inputFilter: {
_: this.searchContext.inputFilter
},
maxId,
limit: backLimit ? 0 : loadCount,
backLimit,
threadId: this.searchContext.threadId,
folderId: this.searchContext.folderId,
nextRate: this.searchContext.nextRate,
minDate: this.searchContext.minDate,
maxDate: this.searchContext.maxDate
}).then(value => {
/* if(DEBUG) {
this.log('loaded more media by maxId:', maxId, value, older, this.reverse);
} */
if(value.next_rate) {
this.searchContext.nextRate = value.next_rate;
}
if(value.history.length < loadCount) {
/* if(this.reverse) {
if(older) this.loadedAllMediaUp = true;
else this.loadedAllMediaDown = true;
} else { */
if(older) this.loadedAllMediaDown = true;
else this.loadedAllMediaUp = true;
//}
}
const method: any = older ? value.history.forEach.bind(value.history) : forEachReverse.bind(null, value.history);
method((message: Message.message) => {
3 years ago
const {mid, peerId} = message;
const media = this.getMediaFromMessage(message);
if(!media) return;
//if(media._ === 'document' && media.type !== 'video') return;
3 years ago
const t = {element: null as HTMLElement, mid, peerId};
if(older) {
if(this.reverse) this.prevTargets.unshift(t);
else this.nextTargets.push(t);
} else {
if(this.reverse) this.nextTargets.push(t);
else this.prevTargets.unshift(t);
}
});
this.buttons.prev.classList.toggle('hide', !this.prevTargets.length);
this.buttons.next.classList.toggle('hide', !this.nextTargets.length);
}, () => {}).then(() => {
if(older) this.loadMediaPromiseDown = null;
else this.loadMediaPromiseUp = null;
});
if(older) this.loadMediaPromiseDown = promise;
else this.loadMediaPromiseUp = promise;
return promise;
};
private getMediaFromMessage(message: any) {
return message.action ? message.action.photo : message.media.photo
|| message.media.document
|| (message.media.webpage && (message.media.webpage.document || message.media.webpage.photo));
}
private setCaption(message: any) {
const caption = message.message;
this.content.caption.classList.toggle('hide', !caption);
if(caption) {
this.content.caption.firstElementChild.innerHTML = RichTextProcessor.wrapRichText(caption, {
entities: message.totalEntities
});
} else {
this.content.caption.firstElementChild.innerHTML = '';
}
}
public setSearchContext(context: SearchSuperContext) {
this.searchContext = context;
if(this.searchContext.folderId !== undefined) {
this.loadedAllMediaUp = true;
if(this.searchContext.nextRate === undefined) {
this.loadedAllMediaDown = true;
}
}
return this;
}
public async openMedia(message: any, target?: HTMLElement, fromRight = 0, reverse = false,
prevTargets: AppMediaViewer['prevTargets'] = [], nextTargets: AppMediaViewer['prevTargets'] = [], needLoadMore = true) {
if(this.setMoverPromise) return this.setMoverPromise;
const mid = message.mid;
const fromId = message.fromId;
const media = this.getMediaFromMessage(message);
this.currentMessageId = mid;
this.currentPeerId = message.peerId;
const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, needLoadMore);
this.setCaption(message);
return promise;
}
}
type AppMediaViewerAvatarTargetType = {element: HTMLElement, photoId: string};
export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMediaViewerAvatarTargetType> {
public currentPhotoId: string;
public peerId: number;
constructor(peerId: number) {
super(['delete']);
this.peerId = peerId;
this.setBtnMenuToggle([{
icon: 'download',
text: 'MediaViewer.Context.Download',
3 years ago
onClick: this.onDownloadClick
}, {
icon: 'delete danger btn-disabled',
text: 'Delete',
onClick: () => {}
}]);
// * constructing html end
this.setListeners();
}
onPrevClick = (target: AppMediaViewerAvatarTargetType) => {
this.nextTargets.unshift({element: this.lastTarget, photoId: this.currentPhotoId});
this.openMedia(target.photoId, target.element, -1);
};
onNextClick = (target: AppMediaViewerAvatarTargetType) => {
this.prevTargets.push({element: this.lastTarget, photoId: this.currentPhotoId});
this.openMedia(target.photoId, target.element, 1);
};
onDownloadClick = () => {
appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.currentPhotoId), appImManager.chat.bubbles.lazyLoadQueue.queueId);
};
protected loadMoreMedia = (older = true) => {
if(this.peerId < 0) return Promise.resolve(); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
if(this.loadedAllMediaDown) return Promise.resolve();
if(this.loadMediaPromiseDown) return this.loadMediaPromiseDown;
const peerId = this.peerId;
const loadCount = 50;
const maxId = this.nextTargets.length ? this.nextTargets[this.nextTargets.length - 1].photoId : this.currentPhotoId;
const promise = appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => {
if(this.peerId !== peerId) {
3 years ago
this.log.warn('peer changed');
return;
}
// if(DEBUG) {
// this.log('loaded more media by maxId:', /* maxId, */value, older, this.reverse);
// }
if(value.photos.length < loadCount) {
this.loadedAllMediaDown = true;
}
value.photos.forEach(photoId => {
if(this.currentPhotoId === photoId) return;
3 years ago
this.nextTargets.push({element: null as HTMLElement, photoId: photoId});
});
this.buttons.prev.classList.toggle('hide', !this.prevTargets.length);
this.buttons.next.classList.toggle('hide', !this.nextTargets.length);
}, () => {}).then(() => {
this.loadMediaPromiseDown = null;
});
return this.loadMediaPromiseDown = promise;
};
public async openMedia(photoId: string, target?: HTMLElement, fromRight = 0) {
if(this.setMoverPromise) return this.setMoverPromise;
const photo = appPhotosManager.getPhoto(photoId);
this.currentPhotoId = photo.id;
return super._openMedia(photo, photo.date, this.peerId, fromRight, target, false);
}
}