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.
2031 lines
70 KiB
2031 lines
70 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import { deferredPromise } from "../helpers/cancellablePromise"; |
|
import mediaSizes from "../helpers/mediaSizes"; |
|
import { isTouchSupported } from "../helpers/touchSupport"; |
|
import { isMobileSafari, isSafari } from "../helpers/userAgent"; |
|
import appDocsManager, { MyDocument } from "../lib/appManagers/appDocsManager"; |
|
import appImManager from "../lib/appManagers/appImManager"; |
|
import appMessagesManager from "../lib/appManagers/appMessagesManager"; |
|
import appPhotosManager, { MyPhoto } 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 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 from "./sidebarRight"; |
|
import SwipeHandler from "./swipeHandler"; |
|
import { ONE_DAY } from "../helpers/date"; |
|
import { SearchSuperContext } from "./appSearchSuper."; |
|
import DEBUG from "../config/debug"; |
|
import appNavigationController from "./appNavigationController"; |
|
import { Message } from "../layer"; |
|
import { forEachReverse } from "../helpers/array"; |
|
import AppSharedMediaTab from "./sidebarRight/tabs/sharedMedia"; |
|
import findUpClassName from "../helpers/dom/findUpClassName"; |
|
import renderImageFromUrl, { renderImageFromUrlPromise } from "../helpers/dom/renderImageFromUrl"; |
|
import getVisibleRect from "../helpers/dom/getVisibleRect"; |
|
import appDownloadManager from "../lib/appManagers/appDownloadManager"; |
|
import { cancelEvent } from "../helpers/dom/cancelEvent"; |
|
import fillPropertyValue from "../helpers/fillPropertyValue"; |
|
import generatePathData from "../helpers/generatePathData"; |
|
import replaceContent from "../helpers/dom/replaceContent"; |
|
import PeerTitle from "./peerTitle"; |
|
import appMessagesIdsManager from "../lib/appManagers/appMessagesIdsManager"; |
|
import I18n, { i18n } from "../lib/langPack"; |
|
import { capitalizeFirstLetter } from "../helpers/string"; |
|
import setInnerHTML from "../helpers/dom/setInnerHTML"; |
|
import { doubleRaf, fastRaf } from "../helpers/schedulers"; |
|
import { attachClickEvent } from "../helpers/dom/clickEvent"; |
|
import PopupDeleteMessages from "./popups/deleteMessages"; |
|
import RangeSelector from "./rangeSelector"; |
|
import windowSize from "../helpers/windowSize"; |
|
import { safeAssign } from "../helpers/object"; |
|
|
|
const ZOOM_STEP = 0.5; |
|
const ZOOM_INITIAL_VALUE = 1; |
|
const ZOOM_MIN_VALUE = 0.5; |
|
const ZOOM_MAX_VALUE = 4; |
|
|
|
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию |
|
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) |
|
// TODO: видео в мобильной вёрстке, если показываются элементы управления: если свайпнуть в сторону, то элементы вернутся на место, т.е. прыгнут - это не ок, надо бы замаскировать |
|
|
|
const MEDIA_VIEWER_CLASSNAME = 'media-viewer'; |
|
|
|
type MediaQueueLoaderOptions<Item extends {}> = { |
|
prevTargets?: MediaQueueLoader<Item>['prevTargets'], |
|
nextTargets?: MediaQueueLoader<Item>['nextTargets'], |
|
onLoadedMore?: MediaQueueLoader<Item>['onLoadedMore'], |
|
generateItem?: MediaQueueLoader<Item>['generateItem'], |
|
getLoadPromise?: MediaQueueLoader<Item>['getLoadPromise'], |
|
reverse?: MediaQueueLoader<Item>['reverse'] |
|
}; |
|
|
|
class MediaQueueLoader<Item extends {}> { |
|
public target: Item = false as any; |
|
public prevTargets: Item[] = []; |
|
public nextTargets: Item[] = []; |
|
|
|
public loadMediaPromiseUp: Promise<void> = null; |
|
public loadMediaPromiseDown: Promise<void> = null; |
|
public loadedAllMediaUp = false; |
|
public loadedAllMediaDown = false; |
|
|
|
public reverse = false; // reverse means next = higher msgid |
|
|
|
protected generateItem: (item: Item) => Item = (item) => item; |
|
protected getLoadPromise: (older: boolean, anchor: Item, loadCount: number) => Promise<Item[]>; |
|
protected onLoadedMore: () => void; |
|
|
|
constructor(options: MediaQueueLoaderOptions<Item> = {}) { |
|
safeAssign(this, options); |
|
} |
|
|
|
public setTargets(prevTargets: Item[], nextTargets: Item[], reverse: boolean) { |
|
this.prevTargets = prevTargets; |
|
this.nextTargets = nextTargets; |
|
this.reverse = reverse; |
|
this.loadedAllMediaUp = this.loadedAllMediaDown = false; |
|
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null; |
|
} |
|
|
|
public reset() { |
|
this.prevTargets = []; |
|
this.nextTargets = []; |
|
this.loadedAllMediaUp = this.loadedAllMediaDown = false; |
|
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null; |
|
} |
|
|
|
// нет смысла делать проверку для reverse и loadMediaPromise |
|
public 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; |
|
|
|
let anchor: Item; |
|
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]; |
|
} |
|
|
|
const promise = this.getLoadPromise(older, anchor, loadCount).then(items => { |
|
if((older && this.loadMediaPromiseDown !== promise) || (!older && this.loadMediaPromiseUp !== promise)) { |
|
return; |
|
} |
|
|
|
if(items.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 ? items.forEach.bind(items) : forEachReverse.bind(null, items); |
|
method((item: Item) => { |
|
const t = this.generateItem(item); |
|
if(!t) { |
|
return; |
|
} |
|
|
|
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.onLoadedMore && this.onLoadedMore(); |
|
}, () => {}).then(() => { |
|
if(older) this.loadMediaPromiseDown = null; |
|
else this.loadMediaPromiseUp = null; |
|
}); |
|
|
|
if(older) this.loadMediaPromiseDown = promise; |
|
else this.loadMediaPromiseUp = promise; |
|
|
|
return promise; |
|
}; |
|
} |
|
|
|
class MediaSearchQueueLoader<Item extends {mid: number, peerId: number}> extends MediaQueueLoader<Item> { |
|
public searchContext: SearchSuperContext; |
|
|
|
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> = {}) { |
|
super({ |
|
...options, |
|
getLoadPromise: (older, anchor, loadCount) => { |
|
const backLimit = older ? 0 : loadCount; |
|
let maxId = this.target?.mid; |
|
|
|
if(anchor) maxId = anchor.mid; |
|
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); |
|
|
|
return appMessagesManager.getSearch({ |
|
...this.searchContext, |
|
peerId: anchor?.peerId, |
|
maxId, |
|
limit: backLimit ? 0 : loadCount, |
|
backLimit |
|
}).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; |
|
} |
|
|
|
return value.history as any; |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
public setSearchContext(context: SearchSuperContext) { |
|
this.searchContext = context; |
|
|
|
if(this.searchContext.folderId !== undefined) { |
|
this.loadedAllMediaUp = true; |
|
|
|
if(this.searchContext.nextRate === undefined) { |
|
this.loadedAllMediaDown = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
class MediaAvatarQueueLoader<Item extends {photoId: string}> extends MediaQueueLoader<Item> { |
|
private peerId: number; |
|
|
|
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> & {peerId: number}) { |
|
super({ |
|
...options, |
|
getLoadPromise: (older, anchor, loadCount) => { |
|
if(this.peerId < 0) return Promise.resolve([]); // ! это значит, что открыло аватар чата, но следующих фотографий нет. |
|
|
|
return appPhotosManager.getUserPhotos(this.peerId, anchor?.photoId, loadCount).then(value => { |
|
const idx = value.photos.indexOf(this.target.photoId); |
|
if(idx !== -1) { |
|
value.photos.splice(idx, 1); |
|
} |
|
|
|
return value.photos.map(photoId => { |
|
return {element: null as HTMLElement, photoId} as any; |
|
}); |
|
}); |
|
} |
|
}); |
|
|
|
this.peerId = options.peerId; |
|
} |
|
} |
|
|
|
class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType extends string, TargetType extends {element: HTMLElement}> { |
|
protected 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 buttons: {[k in 'download' | 'close' | 'prev' | 'next' | 'mobile-close' | 'zoom' | ButtonsAdditionType]: HTMLElement} = {} as any; |
|
protected topbar: HTMLElement; |
|
protected moversContainer: HTMLElement; |
|
|
|
protected tempId = 0; |
|
protected preloader: ProgressivePreloader = null; |
|
protected preloaderStreamable: ProgressivePreloader = null; |
|
|
|
protected prevTargets: TargetType[] = []; |
|
protected nextTargets: TargetType[] = []; |
|
//protected targetContainer: HTMLElement = null; |
|
//protected loadMore: () => void = null; |
|
|
|
protected log: ReturnType<typeof logger>; |
|
|
|
protected isFirstOpen = true; |
|
|
|
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 videoPlayer: VideoPlayer; |
|
|
|
protected zoomElements: { |
|
container: HTMLElement, |
|
btnOut: HTMLElement, |
|
btnIn: HTMLElement, |
|
rangeSelector: RangeSelector |
|
} = {} as any; |
|
// protected zoomValue = ZOOM_INITIAL_VALUE; |
|
protected zoomSwipeHandler: SwipeHandler; |
|
protected zoomSwipeStartX = 0; |
|
protected zoomSwipeStartY = 0; |
|
protected zoomSwipeX = 0; |
|
protected zoomSwipeY = 0; |
|
|
|
protected ctrlKeyDown: boolean; |
|
|
|
get target() { |
|
return this.queueLoader.target; |
|
} |
|
|
|
set target(value) { |
|
this.queueLoader.target = value; |
|
} |
|
|
|
constructor(protected queueLoader: MediaQueueLoader<TargetType>, |
|
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); |
|
|
|
const topbar = this.topbar = document.createElement('div'); |
|
topbar.classList.add(MEDIA_VIEWER_CLASSNAME + '-topbar', MEDIA_VIEWER_CLASSNAME + '-appear'); |
|
|
|
const topbarLeft = document.createElement('div'); |
|
topbarLeft.classList.add(MEDIA_VIEWER_CLASSNAME + '-topbar-left'); |
|
|
|
this.buttons['mobile-close'] = ButtonIcon('close', {onlyMobile: true}); |
|
|
|
// * author |
|
this.author.container = document.createElement('div'); |
|
this.author.container.classList.add(MEDIA_VIEWER_CLASSNAME + '-author', 'no-select'); |
|
const authorRight = document.createElement('div'); |
|
|
|
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'); |
|
|
|
authorRight.append(this.author.nameEl, this.author.date); |
|
|
|
this.author.container.append(this.author.avatarEl, authorRight); |
|
|
|
// * buttons |
|
const buttonsDiv = document.createElement('div'); |
|
buttonsDiv.classList.add(MEDIA_VIEWER_CLASSNAME + '-buttons'); |
|
|
|
topButtons.concat(['download', 'zoom', 'close']).forEach(name => { |
|
const button = ButtonIcon(name, {noRipple: true}); |
|
this.buttons[name] = button; |
|
buttonsDiv.append(button); |
|
}); |
|
|
|
this.buttons.zoom.classList.add('zoom-in'); |
|
|
|
// * zoom |
|
this.zoomElements.container = document.createElement('div'); |
|
this.zoomElements.container.classList.add('zoom-container'); |
|
|
|
this.zoomElements.btnOut = ButtonIcon('zoomout', {noRipple: true}); |
|
this.zoomElements.btnOut.addEventListener('click', () => this.changeZoom(false)); |
|
this.zoomElements.btnIn = ButtonIcon('zoomin', {noRipple: true}); |
|
this.zoomElements.btnIn.addEventListener('click', () => this.changeZoom(true)); |
|
|
|
this.zoomElements.rangeSelector = new RangeSelector(ZOOM_STEP, ZOOM_INITIAL_VALUE, ZOOM_MIN_VALUE, ZOOM_MAX_VALUE, true); |
|
this.zoomElements.rangeSelector.setListeners(); |
|
this.zoomElements.rangeSelector.setHandlers({ |
|
onScrub: this.setZoomValue, |
|
onMouseUp: () => this.setZoomValue() |
|
}); |
|
|
|
this.zoomElements.container.append(this.zoomElements.btnOut, this.zoomElements.rangeSelector.container, this.zoomElements.btnIn); |
|
|
|
this.wholeDiv.append(this.zoomElements.container); |
|
|
|
// * 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.content.main); |
|
this.overlaysDiv.append(mainDiv); |
|
// * overlays end |
|
|
|
topbarLeft.append(this.buttons['mobile-close'], this.author.container); |
|
topbar.append(topbarLeft, buttonsDiv); |
|
|
|
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.moversContainer = document.createElement('div'); |
|
this.moversContainer.classList.add(MEDIA_VIEWER_CLASSNAME + '-movers'); |
|
|
|
this.wholeDiv.append(this.overlaysDiv, this.buttons.prev, this.buttons.next, this.topbar, this.moversContainer); |
|
|
|
// * 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.nextTargets.unshift(this.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.prevTargets.push(this.target); |
|
this.onNextClick(target); |
|
} else { |
|
this.buttons.next.style.display = 'none'; |
|
} |
|
}); |
|
|
|
this.buttons.zoom.addEventListener('click', () => { |
|
if(this.isZooming()) this.toggleZoom(false); |
|
else { |
|
this.changeZoom(true); |
|
} |
|
}); |
|
|
|
this.wholeDiv.addEventListener('click', this.onClick); |
|
|
|
if(isTouchSupported) { |
|
const swipeHandler = new SwipeHandler({ |
|
element: this.wholeDiv, |
|
onSwipe: (xDiff, yDiff) => { |
|
if(VideoPlayer.isFullScreen()) { |
|
return; |
|
} |
|
//console.log(xDiff, yDiff); |
|
|
|
const percents = Math.abs(xDiff) / windowSize.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) / windowSize.windowH; |
|
if(percentsY > .2 || yDiff > 125) { |
|
this.buttons.close.click(); |
|
return true; |
|
} |
|
|
|
return false; |
|
}, |
|
verifyTouchTarget: (evt) => { |
|
// * Fix for seek input |
|
if((evt.target as HTMLElement).tagName === 'INPUT' || findUpClassName(evt.target, 'media-viewer-caption')) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
}); |
|
} |
|
} |
|
|
|
protected toggleZoom(enable?: boolean) { |
|
const isVisible = this.isZooming(); |
|
if(this.zoomElements.rangeSelector.mousedown || this.ctrlKeyDown) { |
|
enable = true; |
|
} |
|
|
|
if(isVisible === enable) return; |
|
|
|
if(enable === undefined) { |
|
enable = !isVisible; |
|
} |
|
|
|
this.buttons.zoom.classList.toggle('zoom-in', !enable); |
|
this.zoomElements.container.classList.toggle('is-visible', enable); |
|
const zoomValue = enable ? this.zoomElements.rangeSelector.value : 1; |
|
this.setZoomValue(zoomValue); |
|
this.zoomElements.rangeSelector.setProgress(zoomValue); |
|
|
|
if(this.videoPlayer) { |
|
this.videoPlayer.lockControls(enable ? false : undefined); |
|
} |
|
|
|
if(enable) { |
|
if(!this.zoomSwipeHandler) { |
|
let lastDiffX: number, lastDiffY: number; |
|
const multiplier = -1; |
|
this.zoomSwipeHandler = new SwipeHandler({ |
|
element: this.moversContainer, |
|
onFirstSwipe: () => { |
|
lastDiffX = lastDiffY = 0; |
|
this.moversContainer.classList.add('no-transition'); |
|
}, |
|
onSwipe: (xDiff, yDiff) => { |
|
[xDiff, yDiff] = [xDiff * multiplier, yDiff * multiplier]; |
|
this.zoomSwipeX += xDiff - lastDiffX; |
|
this.zoomSwipeY += yDiff - lastDiffY; |
|
[lastDiffX, lastDiffY] = [xDiff, yDiff]; |
|
|
|
this.setZoomValue(); |
|
}, |
|
onReset: () => { |
|
this.moversContainer.classList.remove('no-transition'); |
|
}, |
|
cursor: 'move' |
|
}); |
|
} else { |
|
this.zoomSwipeHandler.setListeners(); |
|
} |
|
|
|
this.zoomElements.rangeSelector.setProgress(zoomValue); |
|
} else if(!enable) { |
|
this.zoomSwipeHandler.removeListeners(); |
|
} |
|
} |
|
|
|
protected changeZoom(add: boolean) { |
|
this.zoomElements.rangeSelector.addProgress(ZOOM_STEP * (add ? 1 : -1)); |
|
this.setZoomValue(); |
|
} |
|
|
|
protected setZoomValue = (value = this.zoomElements.rangeSelector.value) => { |
|
// this.zoomValue = value; |
|
if(value === ZOOM_INITIAL_VALUE) { |
|
this.zoomSwipeX = 0; |
|
this.zoomSwipeY = 0; |
|
} |
|
|
|
this.moversContainer.style.transform = `matrix(${value}, 0, 0, ${value}, ${this.zoomSwipeX}, ${this.zoomSwipeY})`; |
|
|
|
this.zoomElements.btnOut.classList.toggle('inactive', value === ZOOM_MIN_VALUE); |
|
this.zoomElements.btnIn.classList.toggle('inactive', value === ZOOM_MAX_VALUE); |
|
|
|
this.toggleZoom(value !== ZOOM_INITIAL_VALUE); |
|
}; |
|
|
|
protected isZooming() { |
|
return this.zoomElements.container.classList.contains('is-visible'); |
|
} |
|
|
|
protected setBtnMenuToggle(buttons: ButtonMenuItemOptions[]) { |
|
const btnMenuToggle = ButtonMenuToggle({onlyMobile: true}, 'bottom-left', buttons); |
|
this.topbar.append(btnMenuToggle); |
|
} |
|
|
|
public close(e?: MouseEvent) { |
|
if(e) { |
|
cancelEvent(e); |
|
} |
|
|
|
if(this.setMoverAnimationPromise) return Promise.reject(); |
|
|
|
appNavigationController.removeByType('media'); |
|
|
|
this.lazyLoadQueue.clear(); |
|
|
|
const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd); |
|
|
|
this.target = false as any; |
|
this.prevTargets.length = 0; |
|
this.nextTargets.length = 0; |
|
this.setMoverPromise = null; |
|
this.tempId = -1; |
|
(window as any).appMediaViewer = undefined; |
|
|
|
if(this.zoomSwipeHandler) { |
|
this.zoomSwipeHandler.removeListeners(); |
|
this.zoomSwipeHandler = undefined; |
|
} |
|
|
|
/* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) { |
|
promise.then(() => { |
|
appSidebarRight.forwardTab.closeBtn.click(); |
|
}); |
|
} */ |
|
|
|
window.removeEventListener('keydown', this.onKeyDown); |
|
window.removeEventListener('keyup', this.onKeyUp); |
|
window.removeEventListener('wheel', this.onWheel, {capture: true}); |
|
|
|
promise.finally(() => { |
|
this.wholeDiv.remove(); |
|
rootScope.isOverlayActive = false; |
|
animationIntersector.checkAnimations(false); |
|
}); |
|
|
|
return promise; |
|
} |
|
|
|
onClick = (e: MouseEvent) => { |
|
if(this.setMoverAnimationPromise) return; |
|
|
|
const target = e.target as HTMLElement; |
|
if(target.tagName === 'A') return; |
|
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; |
|
} |
|
|
|
const isZooming = this.isZooming(); |
|
let mover: HTMLElement = null; |
|
const classNames = ['ckin__player', 'media-viewer-buttons', 'media-viewer-author', 'media-viewer-caption', 'zoom-container']; |
|
if(isZooming) { |
|
classNames.push('media-viewer-movers'); |
|
} |
|
|
|
classNames.find(s => { |
|
try { |
|
mover = findUpClassName(target, s); |
|
if(mover) return true; |
|
} catch(err) {return false;} |
|
}); |
|
|
|
if(/* target === this.mediaViewerDiv */!mover || (!isZooming && (target.tagName === 'IMG' || target.tagName === 'image'))) { |
|
this.buttons.close.click(); |
|
} |
|
}; |
|
|
|
private onKeyDown = (e: KeyboardEvent) => { |
|
//this.log('onKeyDown', e); |
|
if(rootScope.overlaysActive > 1) { |
|
return; |
|
} |
|
|
|
let good = true; |
|
if(e.key === 'ArrowRight') { |
|
this.buttons.next.click(); |
|
} else if(e.key === 'ArrowLeft') { |
|
this.buttons.prev.click(); |
|
} else if(e.key === '-' || e.key === '=') { |
|
if(this.ctrlKeyDown) { |
|
this.changeZoom(e.key === '='); |
|
} |
|
} else { |
|
good = false; |
|
} |
|
|
|
if(e.ctrlKey || e.metaKey) { |
|
this.ctrlKeyDown = true; |
|
} |
|
|
|
if(good) { |
|
cancelEvent(e); |
|
} |
|
}; |
|
|
|
private onKeyUp = (e: KeyboardEvent) => { |
|
if(rootScope.overlaysActive > 1) { |
|
return; |
|
} |
|
|
|
if(!(e.ctrlKey || e.metaKey)) { |
|
this.ctrlKeyDown = false; |
|
|
|
if(this.isZooming()) { |
|
this.setZoomValue(); |
|
} |
|
} |
|
}; |
|
|
|
private onWheel = (e: WheelEvent) => { |
|
if(rootScope.overlaysActive > 1 || (findUpClassName(e.target, 'media-viewer-caption') && !this.ctrlKeyDown)) { |
|
return; |
|
} |
|
|
|
cancelEvent(e); |
|
|
|
if(this.ctrlKeyDown) { |
|
const scrollingUp = e.deltaY < 0; |
|
// if(!scrollingUp && !this.isZooming()) return; |
|
this.changeZoom(!!scrollingUp); |
|
} |
|
}; |
|
|
|
protected async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) { |
|
if(this.videoPlayer) { // there could be a better place for it |
|
this.wholeDiv.classList.remove('has-video-controls'); |
|
this.videoPlayer.removeListeners(); |
|
this.videoPlayer = undefined; |
|
} |
|
|
|
const mover = this.content.mover; |
|
|
|
if(!closing) { |
|
mover.innerHTML = ''; |
|
//mover.append(this.buttons.prev, this.buttons.next); |
|
} |
|
|
|
const zoomValue = this.isZooming() && closing /* && false */ ? this.zoomElements.rangeSelector.value : ZOOM_INITIAL_VALUE; |
|
/* if(!(zoomValue > 1 && closing)) */ 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 || target.classList.contains('grid-item')/* || target.classList.contains('document-ico') */) { |
|
realParent = target; |
|
rect = target.getBoundingClientRect(); |
|
} else if(target instanceof SVGImageElement || target.parentElement instanceof SVGForeignObjectElement) { |
|
realParent = findUpClassName(target, 'attachment'); |
|
rect = realParent.getBoundingClientRect(); |
|
} else if(target.classList.contains('profile-avatars-avatar')) { |
|
realParent = findUpClassName(target, 'profile-avatars-container'); |
|
rect = realParent.getBoundingClientRect(); |
|
|
|
// * if not active avatar |
|
if(closing && target.getBoundingClientRect().left !== rect.left) { |
|
target = realParent = rect = undefined; |
|
} |
|
} |
|
} |
|
|
|
if(!target) { |
|
target = this.content.media; |
|
} |
|
|
|
if(!rect) { |
|
realParent = target.parentElement as HTMLElement; |
|
rect = target.getBoundingClientRect(); |
|
} |
|
|
|
let needOpacity = false; |
|
if(target !== this.content.media && !target.classList.contains('profile-avatars-avatar')) { |
|
const overflowElement = findUpClassName(realParent, 'scrollable'); |
|
const visibleRect = getVisibleRect(realParent, overflowElement); |
|
|
|
if(closing && (!visibleRect || visibleRect.overflow.vertical === 2 || visibleRect.overflow.horizontal === 2)) { |
|
target = this.content.media; |
|
realParent = target.parentElement as HTMLElement; |
|
rect = target.getBoundingClientRect(); |
|
} else if(visibleRect && (visibleRect.overflow.vertical === 1 || visibleRect.overflow.horizontal === 1)) { |
|
needOpacity = true; |
|
} |
|
} |
|
|
|
const containerRect = this.content.media.getBoundingClientRect(); |
|
|
|
let transform = ''; |
|
let left: number; |
|
let top: number; |
|
|
|
if(wasActive) { |
|
left = fromRight === 1 ? windowSize.windowW : -containerRect.width; |
|
top = containerRect.top; |
|
} else { |
|
left = rect.left; |
|
top = rect.top; |
|
} |
|
|
|
/* if(zoomValue > 1) { // 33 |
|
// const diffX = (rect.width * zoomValue - rect.width) / 4; |
|
const diffX = (rect.width * zoomValue - rect.width) / 2; |
|
const diffY = (rect.height * zoomValue - rect.height) / 4; |
|
// left -= diffX; |
|
// top += diffY; |
|
} */ |
|
|
|
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) `; |
|
} */ |
|
|
|
let aspecter: HTMLDivElement; |
|
if(target instanceof HTMLImageElement || target instanceof HTMLVideoElement || target.tagName === 'DIV') { |
|
if(mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter')) { |
|
aspecter = mover.firstElementChild as HTMLDivElement; |
|
|
|
const player = aspecter.querySelector('.ckin__player'); |
|
if(player) { |
|
const video = player.firstElementChild as HTMLVideoElement; |
|
aspecter.append(video); |
|
player.remove(); |
|
} |
|
|
|
if(!aspecter.style.cssText) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать |
|
mover.classList.remove('active'); |
|
this.setFullAspect(aspecter, containerRect, rect); |
|
void mover.offsetLeft; // reflow |
|
mover.classList.add('active'); |
|
} |
|
} else { |
|
aspecter = document.createElement('div'); |
|
aspecter.classList.add('media-viewer-aspecter'/* , 'disable-hover' */); |
|
mover.prepend(aspecter); |
|
} |
|
|
|
aspecter.style.cssText = `width: ${rect.width}px; height: ${rect.height}px; transform: scale3d(${containerRect.width / rect.width}, ${containerRect.height / rect.height}, 1);`; |
|
} |
|
|
|
mover.style.width = containerRect.width + 'px'; |
|
mover.style.height = containerRect.height + 'px'; |
|
|
|
// const scaleX = rect.width / (containerRect.width * zoomValue); |
|
// const scaleY = rect.height / (containerRect.height * zoomValue); |
|
const scaleX = rect.width / containerRect.width; |
|
const scaleY = rect.height / containerRect.height; |
|
if(!wasActive) { |
|
transform += `scale3d(${scaleX},${scaleY},1) `; |
|
} |
|
|
|
let borderRadius = window.getComputedStyle(realParent).getPropertyValue('border-radius'); |
|
const brSplitted = fillPropertyValue(borderRadius) as string[]; |
|
borderRadius = brSplitted.map(r => (parseInt(r) / scaleX) + 'px').join(' '); |
|
if(!wasActive) { |
|
mover.style.borderRadius = borderRadius; |
|
} |
|
//let borderRadius = '0px 0px 0px 0px'; |
|
|
|
if(closing && zoomValue !== 1) { |
|
// const width = this.moversContainer.scrollWidth * scaleX; |
|
// const height = this.moversContainer.scrollHeight * scaleY; |
|
const willBeLeft = windowSize.windowW / 2 - rect.width / 2; |
|
const willBeTop = windowSize.windowH / 2 - rect.height / 2; |
|
const left = rect.left - willBeLeft/* + (width - rect.width) / 2 */; |
|
const top = rect.top - willBeTop/* + (height - rect.height) / 2 */; |
|
this.moversContainer.style.transform = `matrix(${scaleX}, 0, 0, ${scaleY}, ${left}, ${top})`; |
|
} else { |
|
mover.style.transform = transform; |
|
} |
|
|
|
needOpacity && (mover.style.opacity = '0'/* !closing ? '0' : '' */); |
|
|
|
/* 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}; |
|
|
|
const timeout = setTimeout(() => { |
|
if(!deferred.isFulfilled && !deferred.isRejected) { |
|
deferred.resolve(); |
|
} |
|
}, 1000); |
|
|
|
this.setMoverAnimationPromise.finally(() => { |
|
this.setMoverAnimationPromise = null; |
|
clearTimeout(timeout); |
|
}); |
|
|
|
if(!closing) { |
|
let mediaElement: HTMLImageElement | HTMLVideoElement; |
|
let src: string; |
|
|
|
if(target instanceof HTMLVideoElement) { |
|
const elements = Array.from(target.parentElement.querySelectorAll('img')) as HTMLImageElement[]; |
|
if(elements.length) { |
|
target = elements.pop(); |
|
} |
|
} |
|
|
|
if(target.tagName === 'DIV' || target.tagName === 'AVATAR-ELEMENT') { // useContainerAsTarget |
|
const images = Array.from(target.querySelectorAll('img')) as HTMLImageElement[]; |
|
const image = images.pop(); |
|
if(image) { |
|
mediaElement = new Image(); |
|
src = image.src; |
|
mover.append(mediaElement); |
|
} |
|
/* mediaElement = new Image(); |
|
src = target.style.backgroundImage.slice(5, -2); */ |
|
|
|
} else if(target instanceof HTMLImageElement) { |
|
mediaElement = new Image(); |
|
src = target.src; |
|
} else if(target instanceof HTMLVideoElement) { |
|
mediaElement = document.createElement('video'); |
|
mediaElement.src = target.src; |
|
} else if(target instanceof SVGSVGElement) { |
|
const clipId = target.dataset.clipId; |
|
const newClipId = clipId + '-mv'; |
|
|
|
const {width, height} = containerRect; |
|
|
|
const newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
|
newSvg.setAttributeNS(null, 'width', '' + width); |
|
newSvg.setAttributeNS(null, 'height', '' + height); |
|
|
|
// нижние два свойства для масштабирования |
|
newSvg.setAttributeNS(null, 'viewBox', `0 0 ${width} ${height}`); |
|
newSvg.setAttributeNS(null, 'preserveAspectRatio', 'xMidYMid meet'); |
|
|
|
newSvg.insertAdjacentHTML('beforeend', target.firstElementChild.outerHTML.replace(clipId, newClipId)); |
|
newSvg.insertAdjacentHTML('beforeend', target.lastElementChild.outerHTML.replace(clipId, newClipId)); |
|
|
|
// теперь надо выставить новую позицию для хвостика |
|
const defs = newSvg.firstElementChild; |
|
const use = defs.firstElementChild.firstElementChild as SVGUseElement; |
|
if(use instanceof SVGUseElement) { |
|
let transform = use.getAttributeNS(null, 'transform'); |
|
transform = transform.replace(/translate\((.+?), (.+?)\) scale\((.+?), (.+?)\)/, (match, x, y, sX, sY) => { |
|
x = +x; |
|
if(x !== 2) { |
|
x = width - (2 / scaleX); |
|
} else { |
|
x = 2 / scaleX; |
|
} |
|
|
|
y = height; |
|
|
|
return `translate(${x}, ${y}) scale(${+sX / scaleX}, ${+sY / scaleY})`; |
|
}); |
|
use.setAttributeNS(null, 'transform', transform); |
|
|
|
// и новый RECT |
|
path = defs.firstElementChild.lastElementChild as SVGPathElement; |
|
|
|
// код ниже нужен только чтобы скрыть моргание до момента как сработает таймаут |
|
let d: string; |
|
const br: [number, number, number, number] = borderRadius.split(' ').map(v => parseInt(v)) as any; |
|
if(isOut) d = generatePathData(0, 0, width - 9 / scaleX, height, ...br); |
|
else d = generatePathData(9 / scaleX, 0, width - 9 / scaleX, height, ...br); |
|
path.setAttributeNS(null, 'd', d); |
|
} |
|
|
|
const foreignObject = newSvg.lastElementChild; |
|
foreignObject.setAttributeNS(null, 'width', '' + containerRect.width); |
|
foreignObject.setAttributeNS(null, 'height', '' + containerRect.height); |
|
|
|
mover.prepend(newSvg); |
|
} |
|
|
|
if(aspecter) { |
|
aspecter.style.borderRadius = borderRadius; |
|
|
|
if(mediaElement) { |
|
aspecter.append(mediaElement); |
|
} |
|
} |
|
|
|
mediaElement = mover.querySelector('video, img'); |
|
if(mediaElement instanceof HTMLImageElement) { |
|
mediaElement.classList.add('thumbnail'); |
|
if(!aspecter) { |
|
mediaElement.style.width = containerRect.width + 'px'; |
|
mediaElement.style.height = containerRect.height + 'px'; |
|
} |
|
|
|
if(src) { |
|
await renderImageFromUrlPromise(mediaElement, src); |
|
} |
|
}/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) { |
|
await new Promise((resolve, reject) => { |
|
mediaElement.addEventListener('loadeddata', resolve); |
|
|
|
if(src) { |
|
(mediaElement.firstElementChild as HTMLSourceElement).src = src; |
|
} |
|
}); |
|
} */ |
|
|
|
mover.style.display = ''; |
|
|
|
fastRaf(() => { |
|
mover.classList.add(wasActive ? 'moving' : 'active'); |
|
}); |
|
} else { |
|
/* if(mover.classList.contains('center')) { |
|
mover.classList.remove('center'); |
|
void mover.offsetLeft; // reflow |
|
} */ |
|
|
|
if(target instanceof SVGSVGElement) { |
|
path = mover.querySelector('path'); |
|
|
|
if(path) { |
|
this.sizeTailPath(path, containerRect, scaleX, delay, false, isOut, borderRadius); |
|
} |
|
} |
|
|
|
if(target.classList.contains('media-viewer-media')) { |
|
mover.classList.add('hiding'); |
|
} |
|
|
|
this.wholeDiv.classList.add('backwards'); |
|
setTimeout(() => { |
|
this.wholeDiv.classList.remove('active'); |
|
}, 0); |
|
|
|
//return ret; |
|
|
|
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); |
|
|
|
mover.classList.remove('opening'); |
|
|
|
return ret; |
|
} |
|
|
|
mover.classList.add('opening'); |
|
|
|
//await new Promise((resolve) => setTimeout(resolve, 0)); |
|
//await new Promise((resolve) => window.requestAnimationFrame(resolve)); |
|
// * одного RAF'а недостаточно, иногда анимация с одним не срабатывает (преимущественно на мобильных) |
|
await doubleRaf(); |
|
|
|
// чтобы проверить установленную позицию - раскомментировать |
|
// throw ''; |
|
|
|
//await new Promise((resolve) => setTimeout(resolve, 5e3)); |
|
|
|
mover.style.transform = `translate3d(${containerRect.left}px,${containerRect.top}px,0) scale3d(1,1,1)`; |
|
//mover.style.transform = `translate(-50%,-50%) scale(1,1)`; |
|
needOpacity && (mover.style.opacity = ''/* closing ? '0' : '' */); |
|
|
|
if(aspecter) { |
|
this.setFullAspect(aspecter, containerRect, rect); |
|
} |
|
|
|
//throw ''; |
|
|
|
setTimeout(() => { |
|
mover.style.borderRadius = ''; |
|
|
|
if(mover.firstElementChild) { |
|
(mover.firstElementChild as HTMLElement).style.borderRadius = ''; |
|
} |
|
}, 0/* delay / 2 */); |
|
|
|
mover.dataset.timeout = '' + setTimeout(() => { |
|
mover.classList.remove('moving', 'opening'); |
|
|
|
if(aspecter) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать |
|
if(mover.querySelector('video') || true) { |
|
mover.classList.remove('active'); |
|
aspecter.style.cssText = ''; |
|
void mover.offsetLeft; // reflow |
|
} |
|
|
|
//aspecter.classList.remove('disable-hover'); |
|
} |
|
|
|
// эти строки нужны для установки центральной позиции, в случае ресайза это будет нужно |
|
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); |
|
|
|
if(path) { |
|
this.sizeTailPath(path, containerRect, scaleX, delay, true, isOut, borderRadius); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
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) { |
|
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) fastRaf(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 = windowSize.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('div'); |
|
newMover.classList.add('media-viewer-mover'); |
|
newMover.style.display = 'none'; |
|
|
|
if(this.content.mover) { |
|
const oldMover = this.content.mover; |
|
oldMover.parentElement.append(newMover); |
|
} else { |
|
this.moversContainer.append(newMover); |
|
} |
|
|
|
return this.content.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.tagName.toLowerCase() === tagName ? target : target.querySelector(tagName) as HTMLElement; |
|
if(el) { |
|
if(!target.classList.contains('document-ico') && findUpClassName(target, 'attachment')) { |
|
// two parentElements because element can be contained in aspecter |
|
const preloader = target.parentElement.parentElement.querySelector('.preloader-container') as HTMLElement; |
|
if(preloader) { |
|
if(tagName === 'video') { |
|
if(preloader.classList.contains('manual')) { |
|
preloader.click(); |
|
// return; |
|
} |
|
|
|
return; |
|
} |
|
|
|
preloader.remove(); |
|
} |
|
} |
|
|
|
renderImageFromUrl(el, url); |
|
|
|
// ! костыль, но он тут даже и не нужен |
|
if(el.classList.contains('thumbnail') && el.parentElement.classList.contains('media-container-aspecter')) { |
|
el.classList.remove('thumbnail'); |
|
} |
|
} |
|
/* } else { |
|
|
|
} */ |
|
} |
|
|
|
protected setAuthorInfo(fromId: number, timestamp: number) { |
|
const date = new Date(); |
|
const time = new Date(timestamp * 1000); |
|
const now = date.getTime() / 1000; |
|
|
|
const timeEl = new I18n.IntlDateElement({ |
|
date: time, |
|
options: { |
|
hour: '2-digit', |
|
minute: '2-digit' |
|
} |
|
}).element; |
|
|
|
let dateEl: Node | string; |
|
if((now - timestamp) < ONE_DAY && date.getDate() === time.getDate()) { // if the same day |
|
dateEl = i18n('Date.Today'); |
|
} else if((now - timestamp) < (ONE_DAY * 2) && (date.getDate() - 1) === time.getDate()) { // yesterday |
|
dateEl = capitalizeFirstLetter(I18n.format('Yesterday', true)); |
|
} else if(date.getFullYear() !== time.getFullYear()) { // different year |
|
dateEl = new I18n.IntlDateElement({ |
|
date: time, |
|
options: { |
|
month: 'short', |
|
day: 'numeric', |
|
year: 'numeric' |
|
} |
|
}).element; |
|
// dateStr = months[time.getMonth()].slice(0, 3) + ' ' + time.getDate() + ', ' + time.getFullYear(); |
|
} else { |
|
dateEl = new I18n.IntlDateElement({ |
|
date: time, |
|
options: { |
|
month: 'short', |
|
day: 'numeric' |
|
} |
|
}).element; |
|
// dateStr = months[time.getMonth()].slice(0, 3) + ' ' + time.getDate(); |
|
} |
|
|
|
this.author.date.innerHTML = ''; |
|
this.author.date.append(dateEl, ' ', i18n('ScheduleController.at'), ' ', timeEl); |
|
|
|
replaceContent(this.author.nameEl, new PeerTitle({ |
|
peerId: fromId, |
|
dialog: false, |
|
onlyFirstName: false, |
|
plainText: false |
|
}).element); |
|
|
|
let oldAvatar = this.author.avatarEl; |
|
this.author.avatarEl = (this.author.avatarEl.cloneNode() as AvatarElement); |
|
this.author.avatarEl.setAttribute('peer', '' + (fromId || rootScope.myId)); |
|
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 isDocument = media._ === 'document'; |
|
const isVideo = media.mime_type && ((['video', 'gif'] as MyDocument['type'][]).includes((media as MyDocument).type) || (media as MyDocument).mime_type.indexOf('video/') === 0); |
|
|
|
if(this.isFirstOpen) { |
|
//this.targetContainer = targetContainer; |
|
this.prevTargets = prevTargets; |
|
this.nextTargets = nextTargets; |
|
this.reverse = reverse; |
|
this.needLoadMore = needLoadMore; |
|
this.isFirstOpen = false; |
|
this.queueLoader.setTargets(this.prevTargets, this.nextTargets, this.reverse); |
|
(window as any).appMediaViewer = this; |
|
//this.loadMore = loadMore; |
|
|
|
/* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) { |
|
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 || target === container; |
|
if(useContainerAsTarget) target = container; |
|
|
|
this.target = {element: target} as any; |
|
const tempId = ++this.tempId; |
|
|
|
if(this.needLoadMore) { |
|
if(this.nextTargets.length < 20) { |
|
this.queueLoader.loadMoreMedia(!this.reverse); |
|
} |
|
|
|
if(this.prevTargets.length < 20) { |
|
this.queueLoader.loadMoreMedia(this.reverse); |
|
} |
|
} |
|
|
|
if(container.firstElementChild) { |
|
container.innerHTML = ''; |
|
} |
|
|
|
// ok set |
|
|
|
const wasActive = fromRight !== 0; |
|
if(wasActive) { |
|
this.moveTheMover(this.content.mover, fromRight === 1); |
|
this.setNewMover(); |
|
} else { |
|
window.addEventListener('keydown', this.onKeyDown); |
|
window.addEventListener('keyup', this.onKeyUp); |
|
if(!isTouchSupported) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true}); |
|
const mainColumns = this.pageEl.querySelector('#main-columns'); |
|
this.pageEl.insertBefore(this.wholeDiv, mainColumns); |
|
void this.wholeDiv.offsetLeft; // reflow |
|
this.wholeDiv.classList.add('active'); |
|
rootScope.isOverlayActive = true; |
|
animationIntersector.checkAnimations(true); |
|
|
|
if(!isMobileSafari) { |
|
appNavigationController.pushItem({ |
|
type: 'media', |
|
onPop: (canAnimate) => { |
|
if(this.setMoverAnimationPromise) { |
|
return false; |
|
} |
|
|
|
this.close(); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
////////this.log('wasActive:', wasActive); |
|
|
|
const mover = this.content.mover; |
|
|
|
const maxWidth = windowSize.windowW; |
|
//const maxWidth = this.pageEl.scrollWidth; |
|
// TODO: const maxHeight = mediaSizes.isMobile ? appPhotosManager.windowH : appPhotosManager.windowH - 100; |
|
let padding = 0; |
|
const windowH = windowSize.windowH; |
|
if(windowH < 1000000 && !mediaSizes.isMobile) { |
|
padding = 120; |
|
} |
|
const maxHeight = windowH - 120 - padding; |
|
let thumbPromise: Promise<any> = Promise.resolve(); |
|
const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight, mediaSizes.isMobile ? false : true, undefined, media._ === 'document' && media.w && media.h).photoSize; |
|
if(useContainerAsTarget) { |
|
const cacheContext = appDownloadManager.getCacheContext(media, size.type); |
|
let img: HTMLImageElement; |
|
if(cacheContext.downloaded) { |
|
img = new Image(); |
|
img.src = cacheContext.url; |
|
} else { |
|
const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(media, cacheContext, true); |
|
if(gotThumb) { |
|
thumbPromise = gotThumb.loadPromise; |
|
img = gotThumb.image; |
|
} |
|
} |
|
|
|
if(img) { |
|
img.classList.add('thumbnail'); |
|
container.append(img); |
|
} |
|
} |
|
|
|
// need after setAttachmentSize |
|
/* if(useContainerAsTarget) { |
|
target = target.querySelector('img, video') || target; |
|
} */ |
|
|
|
const supportsStreaming = media.supportsStreaming; |
|
const preloader = 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'); |
|
|
|
const set = () => 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) { |
|
video.pause(); |
|
} |
|
}); |
|
|
|
video.addEventListener('error', (err) => { |
|
if(video.error.code !== 4) { |
|
this.log.error("Error " + video.error.code + "; details: " + video.error.message); |
|
} |
|
|
|
if(preloader) { |
|
preloader.detach(); |
|
} |
|
}, {once: true}); |
|
|
|
if(isSafari) { |
|
// test stream |
|
// video.controls = true; |
|
video.autoplay = true; |
|
} |
|
|
|
if(media.type === 'gif') { |
|
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') { |
|
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) { |
|
return; |
|
} |
|
|
|
const player = new VideoPlayer(video, true, supportsStreaming); |
|
player.addEventListener('toggleControls', (show) => { |
|
this.wholeDiv.classList.toggle('has-video-controls', show); |
|
}); |
|
this.videoPlayer = player; |
|
if(this.isZooming()) { |
|
this.videoPlayer.lockControls(false); |
|
} |
|
/* div.append(video); |
|
mover.append(player.wrapper); */ |
|
}); |
|
} |
|
}; |
|
|
|
if(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) { |
|
const load = () => { |
|
const cacheContext = appDownloadManager.getCacheContext(media); |
|
const promise: Promise<any> = supportsStreaming ? Promise.resolve() : appDocsManager.downloadDoc(media); |
|
|
|
if(!supportsStreaming) { |
|
onAnimationEnd.then(() => { |
|
if(!cacheContext.url) { |
|
preloader.attach(mover, true, promise); |
|
} |
|
}); |
|
} |
|
|
|
Promise.all([promise, onAnimationEnd]).then(async() => { |
|
if(this.tempId !== tempId) { |
|
this.log.warn('media viewer changed video'); |
|
return; |
|
} |
|
|
|
const url = cacheContext.url; |
|
if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists |
|
//if(!video.parentElement) { |
|
div.firstElementChild.lastElementChild.append(video); |
|
//} |
|
} else { |
|
renderImageFromUrl(video, url); |
|
} |
|
|
|
this.updateMediaSource(target, url, 'video'); |
|
|
|
createPlayer(); |
|
}); |
|
|
|
return promise; |
|
}; |
|
|
|
this.lazyLoadQueue.unshift({load}); |
|
//} else createPlayer(); |
|
}); |
|
|
|
setMoverPromise = thumbPromise.then(set); |
|
} else { |
|
const set = () => this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => { |
|
//return; // set and don't move |
|
//if(wasActive) return; |
|
//return; |
|
|
|
const load = () => { |
|
const cacheContext = appDownloadManager.getCacheContext(media, size.type); |
|
const cancellablePromise = isDocument ? appDocsManager.downloadDoc(media) : appPhotosManager.preloadPhoto(media, size); |
|
|
|
onAnimationEnd.then(() => { |
|
if(!cacheContext.url) { |
|
this.preloader.attachPromise(cancellablePromise); |
|
//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 = cacheContext.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; |
|
const haveImage = div.firstElementChild?.tagName === 'IMG' ? div.firstElementChild as HTMLImageElement : null; |
|
if(!haveImage || haveImage.src !== url) { |
|
let image = new Image(); |
|
image.classList.add('thumbnail'); |
|
|
|
//this.log('will renderImageFromUrl:', image, div, target); |
|
|
|
renderImageFromUrl(image, url, () => { |
|
this.updateMediaSource(target, url, 'img'); |
|
|
|
if(haveImage) { |
|
fastRaf(() => { |
|
haveImage.remove(); |
|
}); |
|
} |
|
|
|
div.append(image); |
|
}); |
|
} |
|
} |
|
|
|
//this.preloader.detach(); |
|
}).catch(err => { |
|
this.log.error(err); |
|
this.preloader.attach(mover); |
|
this.preloader.setManual(); |
|
}); |
|
|
|
return cancellablePromise; |
|
}; |
|
|
|
this.lazyLoadQueue.unshift({load}); |
|
}); |
|
|
|
setMoverPromise = thumbPromise.then(set); |
|
} |
|
|
|
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> { |
|
protected btnMenuDelete: HTMLElement; |
|
|
|
protected queueLoader: MediaSearchQueueLoader<AppMediaViewerTargetType>; |
|
|
|
get searchContext() { |
|
return this.queueLoader.searchContext; |
|
} |
|
|
|
constructor() { |
|
super(new MediaSearchQueueLoader({ |
|
generateItem: (item) => { |
|
const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument'; |
|
const {mid, peerId} = item; |
|
const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item); |
|
|
|
if(!media) return; |
|
|
|
if(isForDocument && !AppMediaViewer.isMediaCompatibleForDocumentViewer(media)) { |
|
return; |
|
} |
|
|
|
return {element: null as HTMLElement, mid, peerId}; |
|
} |
|
}), ['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' */); |
|
|
|
let captionTimeout: number; |
|
const setCaptionTimeout = () => { |
|
if(captionTimeout) { |
|
clearTimeout(captionTimeout); |
|
} |
|
|
|
captionTimeout = window.setTimeout(() => { |
|
captionTimeout = undefined; |
|
this.content.caption.classList.remove('is-focused'); |
|
}, 800); |
|
}; |
|
this.content.caption.addEventListener('touchstart', () => { |
|
if(!mediaSizes.isMobile) return; |
|
|
|
this.content.caption.classList.add('is-focused'); |
|
|
|
if(captionTimeout) { |
|
clearTimeout(captionTimeout); |
|
captionTimeout = undefined; |
|
} |
|
|
|
document.addEventListener('touchend', setCaptionTimeout, {once: true}); |
|
}); |
|
|
|
const captionScrollable = new Scrollable(this.content.caption); |
|
captionScrollable.onAdditionalScroll = setCaptionTimeout; |
|
|
|
//this.content.main.append(this.content.caption); |
|
this.wholeDiv.append(this.content.caption); |
|
|
|
attachClickEvent(this.buttons.delete, this.onDeleteClick); |
|
|
|
const buttons: ButtonMenuItemOptions[] = [{ |
|
icon: 'forward', |
|
text: 'Forward', |
|
onClick: this.onForwardClick |
|
}, { |
|
icon: 'download', |
|
text: 'MediaViewer.Context.Download', |
|
onClick: this.onDownloadClick |
|
}, { |
|
icon: 'delete danger', |
|
text: 'Delete', |
|
onClick: this.onDeleteClick |
|
}]; |
|
|
|
this.setBtnMenuToggle(buttons); |
|
this.btnMenuDelete = buttons[buttons.length - 1].element; |
|
|
|
// * constructing html end |
|
|
|
this.setListeners(); |
|
} |
|
|
|
protected setListeners() { |
|
super.setListeners(); |
|
this.buttons.forward.addEventListener('click', this.onForwardClick); |
|
this.author.container.addEventListener('click', this.onAuthorClick); |
|
|
|
const onCaptionClick = (e: MouseEvent) => { |
|
if(e.target instanceof HTMLAnchorElement) { // close viewer if it's t.me/ redirect |
|
const onclick = (e.target as HTMLElement).getAttribute('onclick'); |
|
if(!onclick || onclick.includes('showMaskedAlert')) { |
|
return; |
|
} |
|
|
|
cancelEvent(e); |
|
|
|
this.close().then(() => { |
|
this.content.caption.removeEventListener('click', onCaptionClick, {capture: true}); |
|
(e.target as HTMLAnchorElement).click(); |
|
}); |
|
|
|
return false; |
|
} |
|
}; |
|
this.content.caption.addEventListener('click', onCaptionClick, {capture: true}); |
|
} |
|
|
|
/* 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.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, -1); |
|
}; |
|
|
|
onNextClick = (target: AppMediaViewerTargetType) => { |
|
this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, 1); |
|
}; |
|
|
|
onDeleteClick = () => { |
|
const target = this.target; |
|
new PopupDeleteMessages(target.peerId, [target.mid], 'chat', () => { |
|
this.target = {element: this.content.media} as any; |
|
this.close(); |
|
}); |
|
}; |
|
|
|
onForwardClick = () => { |
|
const target = this.target; |
|
if(target.mid) { |
|
//appSidebarRight.forwardTab.open([target.mid]); |
|
new PopupForward({ |
|
[target.peerId]: [target.mid] |
|
}, () => { |
|
return this.close(); |
|
}); |
|
} |
|
}; |
|
|
|
onAuthorClick = (e: MouseEvent) => { |
|
const {mid, peerId} = this.target; |
|
if(mid && mid !== Number.MAX_SAFE_INTEGER) { |
|
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(); |
|
} |
|
} |
|
|
|
const message = appMessagesManager.getMessageByPeer(peerId, mid); |
|
appImManager.setInnerPeer(message.peerId, mid, threadId ? 'discussion' : undefined, threadId); |
|
}); |
|
} |
|
}; |
|
|
|
onDownloadClick = () => { |
|
const {peerId, mid} = this.target; |
|
const message = appMessagesManager.getMessageByPeer(peerId, mid); |
|
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); |
|
} |
|
} |
|
}; |
|
|
|
private setCaption(message: Message.message) { |
|
const caption = message.message; |
|
let html = ''; |
|
if(caption) { |
|
html = RichTextProcessor.wrapRichText(caption, { |
|
entities: message.totalEntities |
|
}); |
|
} |
|
|
|
// html = 'Dandelion are a family of flowering plants that grow in many parts of the world.'; |
|
setInnerHTML(this.content.caption.firstElementChild, html); |
|
this.content.caption.classList.toggle('hide', !caption); |
|
// this.content.container.classList.toggle('with-caption', !!caption); |
|
} |
|
|
|
public setSearchContext(context: SearchSuperContext) { |
|
this.queueLoader.setSearchContext(context); |
|
|
|
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 = appMessagesManager.getMediaFromMessage(message); |
|
|
|
this.buttons.forward.classList.toggle('hide', message._ === 'messageService'); |
|
|
|
const canDeleteMessage = appMessagesManager.canDeleteMessage(message); |
|
[this.buttons.delete, this.btnMenuDelete].forEach(button => { |
|
button.classList.toggle('hide', !canDeleteMessage); |
|
}); |
|
|
|
this.setCaption(message); |
|
const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, needLoadMore); |
|
this.target.mid = mid; |
|
this.target.peerId = message.peerId; |
|
|
|
return promise; |
|
} |
|
|
|
public static isMediaCompatibleForDocumentViewer(media: MyPhoto | MyDocument) { |
|
return media._ === 'photo' || (['photo', 'video', 'gif'].includes(media.type) || media.mime_type.indexOf('video/') === 0); |
|
} |
|
} |
|
|
|
type AppMediaViewerAvatarTargetType = {element: HTMLElement, photoId: string}; |
|
export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMediaViewerAvatarTargetType> { |
|
public peerId: number; |
|
|
|
constructor(peerId: number) { |
|
super(new MediaAvatarQueueLoader({peerId}), [/* 'delete' */]); |
|
|
|
this.peerId = peerId; |
|
|
|
this.setBtnMenuToggle([{ |
|
icon: 'download', |
|
text: 'MediaViewer.Context.Download', |
|
onClick: this.onDownloadClick |
|
}/* , { |
|
icon: 'delete danger btn-disabled', |
|
text: 'Delete', |
|
onClick: () => {} |
|
} */]); |
|
|
|
// * constructing html end |
|
|
|
this.setListeners(); |
|
} |
|
|
|
onPrevClick = (target: AppMediaViewerAvatarTargetType) => { |
|
this.openMedia(target.photoId, target.element, -1); |
|
}; |
|
|
|
onNextClick = (target: AppMediaViewerAvatarTargetType) => { |
|
this.openMedia(target.photoId, target.element, 1); |
|
}; |
|
|
|
onDownloadClick = () => { |
|
appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.target.photoId), appImManager.chat.bubbles.lazyLoadQueue.queueId); |
|
}; |
|
|
|
public async openMedia(photoId: string, target?: HTMLElement, fromRight = 0, prevTargets?: AppMediaViewerAvatarTargetType[], nextTargets?: AppMediaViewerAvatarTargetType[]) { |
|
if(this.setMoverPromise) return this.setMoverPromise; |
|
|
|
const photo = appPhotosManager.getPhoto(photoId); |
|
const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets); |
|
this.target.photoId = photo.id; |
|
|
|
return ret; |
|
} |
|
}
|
|
|