diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 767dddde..d6f66608 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -880,6 +880,10 @@ class AppMediaViewerBase 0; + let wrapped: ReturnType; + if(media._ !== 'photo') { + wrapped = wrapVideo({ + doc: media, + message, + container: div, + boxWidth: 0, + boxHeight: 0, + lazyLoadQueue: this.lazyLoadQueue, + middleware, + onlyPreview: true, + withoutPreloader: true + }).thumb; } else { - const cachedThumb = appPhotosManager.getDocumentCachedThumb(media.id); - isDownloaded = cachedThumb?.downloaded > 0; - } - - //this.log('inputMessagesFilterPhotoVideo', message, media); - - if(!isPhoto) { - const span = document.createElement('span'); - span.classList.add('video-time'); - div.append(span); - - if(media.type !== 'gif') { - span.innerText = (media.duration + '').toHHMMSS(false); - - /* const spanPlay = document.createElement('span'); - spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center'); - div.append(spanPlay); */ - } else { - span.innerText = 'GIF'; - } - } - - const load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200)) - .then(() => { - if(!middleware()) { - //this.log.warn('peer changed'); - return; - } - - const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url; - if(url) { - //if(needBlur) return; - - const needBlurCallback = needBlur ? () => { - //void img.offsetLeft; // reflow - img.style.opacity = ''; - - if(thumb) { - window.setTimeout(() => { - thumb.remove(); - }, 200); - } - } : undefined; - renderImageFromUrl(img, url, needBlurCallback); - } - }); - - let thumb: HTMLImageElement; - const sizes = media.sizes || media.thumbs; - - const willHaveThumb = !isDownloaded && sizes && sizes[0].bytes; - if(willHaveThumb) { - thumb = new Image(); - thumb.classList.add('grid-item-media', 'thumbnail'); - thumb.dataset.mid = '' + message.mid; - appPhotosManager.setAttachmentPreview(sizes[0].bytes, thumb, false, false); - div.append(thumb); - } - - const needBlur = (!isDownloaded || !willHaveThumb) && rootScope.settings.animationsEnabled; - const img = new Image(); - img.dataset.mid = '' + message.mid; - img.classList.add('grid-item-media'); - if(needBlur) img.style.opacity = '0'; - div.append(img); - - if(isDownloaded || willHaveThumb) { - const promise = new Promise((resolve, reject) => { - (thumb || img).addEventListener('load', () => { - clearTimeout(timeout); - resolve(); - }); - - const timeout = setTimeout(() => { - //this.log('didn\'t load', thumb, media, isDownloaded, sizes); - reject(); - }, 1e3); + wrapped = wrapPhoto({ + photo: media, + message, + container: div, + boxWidth: 0, + boxHeight: 0, + lazyLoadQueue: this.lazyLoadQueue, + middleware, + withoutPreloader: true }); - - promises.push(promise); } - if(sizes?.length) { - if(isDownloaded) load(); - else this.lazyLoadQueue.push({div, load}); - } + wrapped.images.thumb && wrapped.images.thumb.classList.add('grid-item-media'); + wrapped.images.full && wrapped.images.full.classList.add('grid-item-media'); + + promises.push(wrapped.loadPromises.thumb); elemsToAppend.push({element: div, message}); } diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 44141a60..ea28411a 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -44,6 +44,7 @@ import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimatio import { fastRaf } from "../../helpers/schedulers"; import { deferredPromise, CancellablePromise } from "../../helpers/cancellablePromise"; +const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS = ['messageActionHistoryClear']; const TEST_SCROLL_TIMES: number = undefined; @@ -1392,7 +1393,7 @@ export default class ChatBubbles { this.chatInner.classList.toggle('is-channel', isChannel); } - public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) { + public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise[]) { /* let dateMessage = this.getDateContainerByMessage(message, reverse); if(reverse) dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling); else dateMessage.container.append(bubble); @@ -1400,47 +1401,6 @@ export default class ChatBubbles { //this.log('renderMessagesQueue'); - let promises: Promise[] = []; - (Array.from(bubble.querySelectorAll('img, video')) as HTMLImageElement[]).forEach(el => { - if(el instanceof HTMLVideoElement) { - if(!el.src) { - //this.log.warn('no source', el, source, 'src', source.src); - return; - } else if(el.readyState >= 4) return; - } else if(el.complete || !el.src) return; - - let promise = new Promise((resolve, reject) => { - let r: () => boolean; - let onLoad = () => { - clearTimeout(timeout); - resolve(); - - // lol - el.removeEventListener(el instanceof HTMLVideoElement ? 'canplay' : 'load', onLoad); - }; - - if(el instanceof HTMLVideoElement) { - el.addEventListener('canplay', onLoad); - r = () => el.readyState >= 1; - } else { - el.addEventListener('load', onLoad); - r = () => el.complete; - } - - // for safari - let c = () => r() ? onLoad() : window.requestAnimationFrame(c); - window.requestAnimationFrame(c); - - let timeout = setTimeout(() => { - // @ts-ignore - //this.log.error('did not called', el, el.parentElement, el.complete, el.readyState, src); - resolve(); - }, 1500); - }); - - promises.push(promise); - }); - this.messagesQueue.push({message, bubble, reverse, promises}); this.setMessagesQueuePromise(); @@ -1624,6 +1584,8 @@ export default class ChatBubbles { bubble.dataset.mid = message.mid; bubble.dataset.timestamp = message.date; + const loadPromises: Promise[] = []; + if(message._ === 'messageService') { let action = message.action; let _ = action._; @@ -1636,7 +1598,7 @@ export default class ChatBubbles { bubbleContainer.innerHTML = `
${message.rReply}
`; if(updatePosition) { - this.renderMessagesQueue(message, bubble, reverse); + this.renderMessagesQueue(message, bubble, reverse, loadPromises); } return bubble; @@ -1832,13 +1794,14 @@ export default class ChatBubbles { middleware: this.getMiddleware(), isOut: our, lazyLoadQueue: this.lazyLoadQueue, - chat: this.chat + chat: this.chat, + loadPromises }); break; } - const withTail = !isAndroid && !message.message && !withReplies; + const withTail = !isAndroid && !message.message && !withReplies && USE_MEDIA_TAILS; if(withTail) bubble.classList.add('with-media-tail'); wrapPhoto({ photo, @@ -1847,7 +1810,8 @@ export default class ChatBubbles { withTail, isOut, lazyLoadQueue: this.lazyLoadQueue, - middleware: this.getMiddleware() + middleware: this.getMiddleware(), + loadPromises }); break; @@ -1858,7 +1822,7 @@ export default class ChatBubbles { let webpage = messageMedia.webpage; ////////this.log('messageMediaWebPage', webpage); - if(webpage._ == 'webPageEmpty') { + if(webpage._ === 'webPageEmpty') { break; } @@ -1895,7 +1859,8 @@ export default class ChatBubbles { lazyLoadQueue: this.lazyLoadQueue, middleware: this.getMiddleware(), isOut, - group: CHAT_ANIMATION_GROUP + group: CHAT_ANIMATION_GROUP, + loadPromises }); //} } else { @@ -1945,7 +1910,7 @@ export default class ChatBubbles { bubble.classList.add('photo'); const size = webpage.photo.sizes[webpage.photo.sizes.length - 1]; - if(size.w == size.h && quoteTextDiv.childElementCount) { + if(size.w === size.h && quoteTextDiv.childElementCount) { bubble.classList.add('is-square-photo'); } else if(size.h > size.w) { bubble.classList.add('is-vertical-photo'); @@ -1959,7 +1924,8 @@ export default class ChatBubbles { boxHeight: mediaSizes.active.webpage.height, isOut, lazyLoadQueue: this.lazyLoadQueue, - middleware: this.getMiddleware() + middleware: this.getMiddleware(), + loadPromises }); } @@ -1986,7 +1952,7 @@ export default class ChatBubbles { } let size = bubble.classList.contains('emoji-big') ? 140 : 200; - this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size, size, true); + this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size, size); //let preloader = new ProgressivePreloader(attachmentDiv, false); bubbleContainer.style.height = attachmentDiv.style.height; bubbleContainer.style.width = attachmentDiv.style.width; @@ -2019,10 +1985,11 @@ export default class ChatBubbles { middleware: this.getMiddleware(), isOut: our, lazyLoadQueue: this.lazyLoadQueue, - chat: this.chat + chat: this.chat, + loadPromises }); } else { - const withTail = !isAndroid && !isApple && doc.type != 'round' && !message.message && !withReplies; + const withTail = !isAndroid && !isApple && doc.type != 'round' && !message.message && !withReplies && USE_MEDIA_TAILS; if(withTail) bubble.classList.add('with-media-tail'); wrapVideo({ doc, @@ -2034,7 +2001,8 @@ export default class ChatBubbles { isOut, lazyLoadQueue: this.lazyLoadQueue, middleware: this.getMiddleware(), - group: CHAT_ANIMATION_GROUP + group: CHAT_ANIMATION_GROUP, + loadPromises }); } @@ -2045,7 +2013,8 @@ export default class ChatBubbles { message, bubble, messageDiv, - chat: this.chat + chat: this.chat, + loadPromises }); if(newNameContainer) { @@ -2260,7 +2229,7 @@ export default class ChatBubbles { if(updatePosition) { this.bubbleGroups.addBubble(bubble, message, reverse); - this.renderMessagesQueue(message, bubble, reverse); + this.renderMessagesQueue(message, bubble, reverse, loadPromises); } else { this.bubbleGroups.updateGroupByMessageId(message.mid); } diff --git a/src/components/gifsMasonry.ts b/src/components/gifsMasonry.ts index 20063962..181d1696 100644 --- a/src/components/gifsMasonry.ts +++ b/src/components/gifsMasonry.ts @@ -64,7 +64,7 @@ export default class GifsMasonry { const doc = appDocsManager.getDoc(docId); const promise = this.scrollPromise.then(() => { - const promise = wrapVideo({ + const res = wrapVideo({ doc, container: div as HTMLDivElement, lazyLoadQueue: null, @@ -73,6 +73,7 @@ export default class GifsMasonry { noInfo: true, }); + const promise = res.loadPromise; promise.finally(() => { const video = div.querySelector('video'); diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index a57f7718..c56b8070 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -1,6 +1,8 @@ import { debounce } from "../helpers/schedulers"; import { logger, LogLevels } from "../lib/logger"; import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector"; +import { DEBUG } from "../lib/mtproto/mtproto_config"; +import { findAndSpliceAll } from "../helpers/array"; type LazyLoadElementBase = { load: () => Promise @@ -42,14 +44,16 @@ export class LazyLoadQueueBase { public lock() { if(this.lockPromise) return; - const perf = performance.now(); + //const perf = performance.now(); this.lockPromise = new Promise((resolve, reject) => { this.unlockResolve = resolve; }); - this.lockPromise.then(() => { - this.log('was locked for:', performance.now() - perf); - }); + /* if(DEBUG) { + this.lockPromise.then(() => { + this.log('was locked for:', performance.now() - perf); + }); + } */ } public unlock() { @@ -68,7 +72,9 @@ export class LazyLoadQueueBase { this.inProcess.add(item); - this.log('will load media', this.lockPromise, item); + /* if(DEBUG) { + this.log('will load media', this.lockPromise, item); + } */ try { //await new Promise((resolve) => setTimeout(resolve, 2e3)); @@ -81,7 +87,9 @@ export class LazyLoadQueueBase { this.inProcess.delete(item); - this.log('loaded media', item); + /* if(DEBUG) { + this.log('loaded media', item); + } */ this.processQueue(); } @@ -104,7 +112,7 @@ export class LazyLoadQueueBase { do { if(item) { - this.queue.findAndSplice(i => i == item); + this.queue.findAndSplice(i => i === item); } else { item = this.getItem(); } @@ -168,12 +176,12 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase { } protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) { - const item = this.queue.find(i => i.div == el.div); + const item = this.queue.find(i => i.div === el.div && i.load === el.load); if(item) { return false; } else { for(const item of this.inProcess) { - if(item.div == el.div) { + if(item.div === el.div && item.load === el.load) { return false; } } @@ -201,7 +209,8 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase { } public unobserve(el: HTMLElement) { - this.queue.findAndSplice(i => i.div === el); + findAndSpliceAll(this.queue, (i) => i.div === el); + this.intersector.unobserve(el); } } @@ -218,12 +227,11 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector { this.log('isIntersecting', target); // need for set element first if scrolled - const item = this.queue.findAndSplice(i => i.div == target); - if(item) { + findAndSpliceAll(this.queue, (i) => i.div === target).forEach(item => { item.wasSeen = true; this.queue.unshift(item); //this.processQueue(item); - } + }); this.setProcessQueueTimeout(); } @@ -261,11 +269,11 @@ export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { super(parallelLimit); this.intersector = new VisibilityIntersector((target, visible) => { + const spliced = findAndSpliceAll(this.queue, (i) => i.div === target); if(visible) { - const item = this.queue.findAndSplice(i => i.div == target); - this.queue.unshift(item || this._queue.get(target)); - } else { - this.queue.findAndSplice(i => i.div == target); + spliced.forEach(item => { + this.queue.unshift(item || this._queue.get(target)); + }); } this.onVisibilityChange && this.onVisibilityChange(target, visible); @@ -298,9 +306,11 @@ export class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector { super(parallelLimit); this.intersector = new VisibilityIntersector((target, visible) => { - const item = this.queue.findAndSplice(i => i.div == target); - if(visible && item) { - this.queue.unshift(item); + const spliced = findAndSpliceAll(this.queue, (i) => i.div === target); + if(visible && spliced.length) { + spliced.forEach(item => { + this.queue.unshift(item); + }); } this.onVisibilityChange && this.onVisibilityChange(target, visible); diff --git a/src/components/preloader.ts b/src/components/preloader.ts index c2fa73c2..d860ee8e 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -1,5 +1,8 @@ import { isInDOM, cancelEvent, attachClickEvent } from "../helpers/dom"; import { CancellablePromise } from "../helpers/cancellablePromise"; +import SetTransition from "./singleTransition"; + +const TRANSITION_TIME = 200; export default class ProgressivePreloader { public preloader: HTMLDivElement; @@ -20,8 +23,8 @@ export default class ProgressivePreloader { this.preloader.innerHTML = `
- - + +
`; @@ -95,16 +98,20 @@ export default class ProgressivePreloader { } this.detached = false; - window.requestAnimationFrame(() => { + /* window.requestAnimationFrame(() => { if(this.detached) return; - this.detached = false; + this.detached = false; */ elem[this.attachMethod](this.preloader); + window.requestAnimationFrame(() => { + SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME); + }); + if(this.cancelable && reset) { this.setProgress(0); } - }); + //}); } public detach() { @@ -113,14 +120,18 @@ export default class ProgressivePreloader { //return; if(this.preloader.parentElement) { - /* setTimeout(() => */window.requestAnimationFrame(() => { - if(!this.detached) return; - this.detached = true; + /* setTimeout(() => *///window.requestAnimationFrame(() => { + /* if(!this.detached) return; + this.detached = true; */ - if(this.preloader.parentElement) { - this.preloader.remove(); - } - })/* , 5e3) */; + //if(this.preloader.parentElement) { + window.requestAnimationFrame(() => { + SetTransition(this.preloader, 'is-visible', false, TRANSITION_TIME, () => { + this.preloader.remove(); + }); + }); + //} + //})/* , 5e3) */; } } @@ -137,7 +148,7 @@ export default class ProgressivePreloader { try { const totalLength = this.circle.getTotalLength(); //console.log('setProgress', (percents / 100 * totalLength)); - this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200'; + this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', ' + totalLength; } catch(err) {} } } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index abbdcea1..459f2935 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -29,10 +29,11 @@ import RichTextProcessor from '../lib/richtextprocessor'; import appImManager from '../lib/appManagers/appImManager'; import Chat from './chat/chat'; import { SearchSuperContext } from './appSearchSuper.'; +import rootScope from '../lib/rootScope'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB -export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: { +export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises}: { doc: MyDocument, container?: HTMLElement, message?: any, @@ -43,7 +44,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, noInfo?: true, - group?: string + group?: string, + onlyPreview?: boolean, + withoutPreloader?: boolean, + loadPromises?: Promise[] }) { const isAlbumItem = !(boxWidth && boxHeight); const canAutoplay = doc.type != 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem); @@ -71,8 +75,13 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } } + let res: { + thumb?: typeof photoRes, + loadPromise: Promise + } = {} as any; + if(doc.mime_type == 'image/gif') { - return wrapPhoto({ + const photoRes = wrapPhoto({ photo: doc, message, container, @@ -81,8 +90,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai withTail, isOut, lazyLoadQueue, - middleware + middleware, + withoutPreloader, + loadPromises }); + + res.thumb = photoRes; + res.loadPromise = photoRes.loadPromises.full; + return res; } /* const video = doc.type == 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video'); @@ -91,6 +106,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } */ const video = document.createElement('video'); + video.classList.add('media-video'); video.muted = true; video.setAttribute('playsinline', 'true'); if(doc.type == 'round') { @@ -155,53 +171,37 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } else { video.autoplay = true; // для safari } - - let img: HTMLImageElement; + + let photoRes: ReturnType; if(message) { - if(!canAutoplay) { - return wrapPhoto({ - photo: doc, - message, - container, - boxWidth, - boxHeight, - withTail, - isOut, - lazyLoadQueue, - middleware - }); + photoRes = wrapPhoto({ + photo: doc, + message, + container, + boxWidth, + boxHeight, + withTail, + isOut, + lazyLoadQueue, + middleware, + withoutPreloader: true, + loadPromises + }); + + res.thumb = photoRes; + + if(!canAutoplay || onlyPreview) { + res.loadPromise = photoRes.loadPromises.full; + return res; } if(withTail) { - img = wrapMediaWithTail(doc, message, container, boxWidth, boxHeight, isOut); - } else { - if(boxWidth && boxHeight) { // !album - appPhotosManager.setAttachmentSize(doc, container, boxWidth, boxHeight, false, true); - } - - if(doc.thumbs?.length && 'bytes' in doc.thumbs[0]) { - appPhotosManager.setAttachmentPreview(doc.thumbs[0].bytes, container, false); - } - - img = container.lastElementChild as HTMLImageElement; - if(img?.tagName != 'IMG') { - container.append(img = new Image()); - } - } - - if(img) { - img.classList.add('thumbnail'); - } - - if(withTail) { - const foreignObject = img.parentElement; + const foreignObject = (photoRes.images.thumb || photoRes.images.full).parentElement; video.width = +foreignObject.getAttributeNS(null, 'width'); video.height = +foreignObject.getAttributeNS(null, 'height'); foreignObject.append(video); } - } - - if(!img?.parentElement) { + } else { // * gifs masonry const gotThumb = appDocsManager.getThumb(doc, false); if(gotThumb) { gotThumb.promise.then(() => { @@ -253,14 +253,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai //if(doc.type == 'gif'/* || true */) { video.addEventListener(isAppleMobile ? 'loadeddata' : 'canplay', () => { - if(img?.parentElement) { - img.remove(); - } - /* if(!video.paused) { video.pause(); } */ - if(doc.type != 'round' && group) { + if(doc.type !== 'round' && group) { animationIntersector.addAnimation(video, group); } @@ -327,7 +323,9 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai return; } */ - return /* doc.downloaded || */!lazyLoadQueue/* && false */ ? loadVideo() : (lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */}), Promise.resolve()); + res.loadPromise = !lazyLoadQueue ? loadVideo() : (lazyLoadQueue.push({div: container, load: loadVideo}), Promise.resolve()); + + return res; } export const formatDate = (timestamp: number, monthShort = false, withYear = true) => { @@ -344,13 +342,14 @@ export const formatDate = (timestamp: number, monthShort = false, withYear = tru return str + ' at ' + date.getHours() + ':' + ('0' + date.getMinutes()).slice(-2); }; -export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showSender, searchContext}: { +export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showSender, searchContext, loadPromises}: { message: any, withTime?: boolean, fontWeight?: number, voiceAsMusic?: boolean, showSender?: boolean, - searchContext?: SearchSuperContext + searchContext?: SearchSuperContext, + loadPromises?: Promise[] }): HTMLElement { if(!fontWeight) fontWeight = 500; @@ -394,7 +393,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS message: null, container: icoDiv, boxWidth: 54, - boxHeight: 54 + boxHeight: 54, + loadPromises }); icoDiv.style.width = icoDiv.style.height = ''; } @@ -475,8 +475,12 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m svg.classList.add('bubble__media-container', isOut ? 'is-out' : 'is-in'); const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); - - appPhotosManager.setAttachmentSize(photo, foreignObject, boxWidth, boxHeight/* , false, true */); + + const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo); + if(gotThumb) { + foreignObject.append(gotThumb.image); + } + appPhotosManager.setAttachmentSize(photo, foreignObject, boxWidth, boxHeight); const width = +foreignObject.getAttributeNS(null, 'width'); const height = +foreignObject.getAttributeNS(null, 'height'); @@ -525,7 +529,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m return img; } -export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size}: { +export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises}: { photo: MyPhoto | MyDocument, message: any, container: HTMLElement, @@ -535,8 +539,23 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT isOut?: boolean, lazyLoadQueue?: LazyLoadQueue, middleware?: () => boolean, - size?: PhotoSize + size?: PhotoSize, + withoutPreloader?: boolean, + loadPromises?: Promise[] }) { + if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { + return { + loadPromises: { + thumb: Promise.resolve(), + full: Promise.resolve() + }, + images: { + thumb: null, + full: null + } + }; + } + if(boxWidth === undefined) { boxWidth = mediaSizes.active.regular.width; } @@ -545,44 +564,50 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT boxHeight = mediaSizes.active.regular.height; } + let loadThumbPromise: Promise; + let thumbImage: HTMLImageElement; let image: HTMLImageElement; if(withTail) { image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut); } else { + image = new Image(); + if(boxWidth && boxHeight) { // !album - size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, false, true); + size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight); } - if(photo._ == 'document' || !photo.downloaded) { - const thumbs = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs; - if(thumbs?.length && 'bytes' in thumbs[0]) { - appPhotosManager.setAttachmentPreview(thumbs[0].bytes, container, false); - } - } - - image = container.lastElementChild as HTMLImageElement; - if(!image || image.tagName != 'IMG') { - container.append(image = new Image()); + const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo); + if(gotThumb) { + loadThumbPromise = gotThumb.loadPromise; + thumbImage = gotThumb.image; + thumbImage.classList.add('media-photo'); + container.append(thumbImage); } } - if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { - return Promise.resolve(); - } - + image.classList.add('media-photo'); + //console.log('wrapPhoto downloaded:', photo, photo.downloaded, container); - + const cacheContext = appPhotosManager.getCacheContext(photo); + const needFadeIn = (thumbImage || !cacheContext.downloaded) && rootScope.settings.animationsEnabled; + if(needFadeIn) { + image.classList.add('fade-in'); + } + let preloader: ProgressivePreloader; if(message?.media?.preloader) { // means upload message.media.preloader.attach(container, false); - } else if(!cacheContext.downloaded) { - preloader = new ProgressivePreloader(null, false, false, photo._ == 'document' ? 'prepend' : 'append'); + } else if(!cacheContext.downloaded && !withoutPreloader) { + preloader = new ProgressivePreloader(null, false, false, 'prepend'); } + let loadPromise: Promise; const load = () => { - const promise = photo._ == 'document' && photo.animated ? + if(loadPromise) return loadPromise; + + const promise = photo._ === 'document' && photo.mime_type === 'image/gif' ? appDocsManager.downloadDoc(photo, undefined, lazyLoadQueue?.queueId) : appPhotosManager.preloadPhoto(photo, size, lazyLoadQueue?.queueId); @@ -590,14 +615,56 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT preloader.attach(container, true, promise); } - return promise.then(() => { + return loadPromise = promise.then(() => { if(middleware && !middleware()) return; - renderImageFromUrl(image || container, cacheContext.url || photo.url); + return new Promise((resolve) => { + renderImageFromUrl(image, cacheContext.url || photo.url, () => { + container.append(image); + + window.requestAnimationFrame(() => { + resolve(); + }); + //resolve(); + + if(needFadeIn) { + setTimeout(() => { + image.classList.remove('fade-in'); + + if(thumbImage) { + thumbImage.remove(); + } + }, 200); + } + }); + }); }); }; + + if(cacheContext.downloaded) { + loadThumbPromise = load(); + } - return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load/* : load, wasSeen: true */}), Promise.resolve()); + if(!lazyLoadQueue) { + loadPromise = load(); + } else { + lazyLoadQueue.push({div: container, load}); + } + + if(loadPromises) { + loadPromises.push(loadThumbPromise); + } + + return { + loadPromises: { + thumb: loadThumbPromise, + full: loadPromise || Promise.resolve() + }, + images: { + thumb: thumbImage, + full: image + } + }; } export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: { @@ -923,14 +990,15 @@ export function prepareAlbum(options: { } */ } -export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat}: { +export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat, loadPromises}: { groupId: string, attachmentDiv: HTMLElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, uploading?: boolean, isOut: boolean, - chat: Chat + chat: Chat, + loadPromises?: Promise[] }) { const items: {size: PhotoSize.photoSize, media: any, message: any}[] = []; @@ -974,7 +1042,8 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo isOut, lazyLoadQueue, middleware, - size + size, + loadPromises }); } else { wrapVideo({ @@ -986,19 +1055,21 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo withTail: false, isOut, lazyLoadQueue, - middleware + middleware, + loadPromises }); } }); } -export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat}: { +export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises}: { albumMustBeRenderedFull: boolean, message: any, messageDiv: HTMLElement, bubble: HTMLElement, uploading?: boolean, - chat: Chat + chat: Chat, + loadPromises?: Promise[] }) { let nameContainer: HTMLDivElement; const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid]; @@ -1011,7 +1082,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, const message = chat.getMessage(mid); const doc = message.media.document; const div = wrapDocument({ - message + message, + loadPromises }); const container = document.createElement('div'); diff --git a/src/helpers/array.ts b/src/helpers/array.ts index 0fe1ee3c..e1aa7d0b 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -13,4 +13,14 @@ export function listMergeSorted(list1: any[] = [], list2: any[] = []) { return result; } -export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); \ No newline at end of file +export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); + +export function findAndSpliceAll(array: Array, verify: (value: T, index: number, arr: typeof array) => boolean) { + const out: typeof array = []; + let idx = -1; + while((idx = array.findIndex(verify)) !== -1) { + out.push(array.splice(idx, 1)[0]); + } + + return out; +} \ No newline at end of file diff --git a/src/helpers/blur.ts b/src/helpers/blur.ts new file mode 100644 index 00000000..b2c888bc --- /dev/null +++ b/src/helpers/blur.ts @@ -0,0 +1,34 @@ +import fastBlur from '../vendor/fastBlur'; +import { fastRaf } from './schedulers'; + +const RADIUS = 2; +const ITERATIONS = 2; + +export default function blur(dataUri: string, delay?: number) { + return new Promise((resolve) => { + fastRaf(() => { + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + const ctx = canvas.getContext('2d')!; + + ctx.drawImage(img, 0, 0); + fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS); + + resolve(canvas.toDataURL()); + }; + + if(delay) { + setTimeout(() => { + img.src = dataUri; + }, delay); + } else { + img.src = dataUri; + } + }); + }); +} diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index ea1ffa5e..fc54002a 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -8,6 +8,7 @@ import { RichTextProcessor } from '../richtextprocessor'; import webpWorkerController from '../webp/webpWorkerController'; import appDownloadManager, { DownloadBlob } from './appDownloadManager'; import appPhotosManager from './appPhotosManager'; +import blur from '../../helpers/blur'; export type MyDocument = Document.document; @@ -246,7 +247,7 @@ export class AppDocsManager { if('bytes' in thumb) { // * exclude from state defineNotNumerableProperties(thumb, ['url']); - thumb.url = appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker); + promise = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker)).then(url => thumb.url = url); } else { //return this.getFileURL(doc, false, thumb); promise = this.downloadDoc(doc, thumb); diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index 2dfb22dc..d047009e 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -19,7 +19,7 @@ import appUsersManager from "./appUsersManager"; #ce671b 5 orange */ const DialogColorsFg = ['#c03d33', '#4fad2d', '#d09306', '#168acd', '#8544d6', '#cd4073', '#2996ad', '#ce671b']; -const DialogColors = ['#e17076', '#7bc862', '#e5ca77', '#65AADD', '#a695e7', '#ee7aae', '#6ec9cb', '#faa774']; +const DialogColors = ['red', 'green', 'yellow', 'blue', 'violet', 'pink', 'cyan', 'orange']; const DialogColorsMap = [0, 7, 4, 1, 6, 3, 5]; export type PeerType = 'channel' | 'chat' | 'megagroup' | 'group' | 'saved'; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 7faf0285..c68b7ca1 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -12,6 +12,8 @@ import { MyDocument } from "./appDocsManager"; import appDownloadManager from "./appDownloadManager"; import appUsersManager from "./appUsersManager"; import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; +import blur from "../../helpers/blur"; +import { renderImageFromUrl } from "../../components/misc"; export type MyPhoto = Photo.photo; @@ -106,7 +108,7 @@ export class AppPhotosManager { bestPhotoSize = photoSize; const {w, h} = calcImageInBox(photoSize.w, photoSize.h, width, height); - if(w == width || h == height) { + if(w === width || h === height) { break; } } @@ -189,47 +191,28 @@ export class AppPhotosManager { return thumb.url ?? (defineNotNumerableProperties(thumb, ['url']), thumb.url = this.getPreviewURLFromBytes(thumb.bytes, isSticker)); } - public setAttachmentPreview(bytes: Uint8Array | number[], element: HTMLElement | SVGForeignObjectElement, isSticker = false, background = false) { - let url = this.getPreviewURLFromBytes(bytes, isSticker); + public getImageFromStrippedThumb(thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize) { + const url = this.getPreviewURLFromThumb(thumb, false); - if(background) { - let img = new Image(); - img.src = url; - img.addEventListener('load', () => { - element.style.backgroundImage = 'url(' + url + ')'; + const image = new Image(); + image.classList.add('thumbnail'); + + const loadPromise = blur(url).then(url => { + return new Promise((resolve) => { + renderImageFromUrl(image, url, resolve); }); - - return element; - } else { - if(element instanceof HTMLImageElement) { - element.src = url; - return element; - } else { - let img = new Image(); - - img.src = url; - element.append(img); - return img; - } - } + }); + + return {image, loadPromise}; } - public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, isSticker = false, dontRenderPreview = false) { + public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number) { const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight); //console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div); - const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs; - const thumb = sizes?.length ? sizes[0] : null; - if(thumb && ('bytes' in thumb)) { - if((!photo.downloaded || (photo as MyDocument).type == 'video' || (photo as MyDocument).type == 'gif') && !isSticker && !dontRenderPreview) { - this.setAttachmentPreview(thumb.bytes, element, isSticker); - } - } - - let width: number; let height: number; - if(photo._ == 'document') { + if(photo._ === 'document') { width = photo.w || 512; height = photo.h || 512; } else { @@ -250,17 +233,36 @@ export class AppPhotosManager { return photoSize; } + + public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument): ReturnType { + if(!photo.downloaded || (photo as MyDocument).type === 'video' || (photo as MyDocument).type === 'gif') { + if(photo._ === 'document') { + const cacheContext = this.getCacheContext(photo); + if(cacheContext.downloaded) { + return null; + } + } + + const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs; + const thumb = sizes?.length ? sizes[0] : null; + if(thumb && ('bytes' in thumb)) { + return appPhotosManager.getImageFromStrippedThumb(thumb as any); + } + } + + return null; + } public getPhotoDownloadOptions(photo: MyPhoto | MyDocument, photoSize: PhotoSize, queueId?: number) { - const isMyDocument = photo._ == 'document'; + const isMyDocument = photo._ === 'document'; - if(!photoSize || photoSize._ == 'photoSizeEmpty') { + if(!photoSize || photoSize._ === 'photoSizeEmpty') { //console.error('no photoSize by photo:', photo); throw new Error('photoSizeEmpty!'); } // maybe it's a thumb - const isPhoto = (photoSize._ == 'photoSize' || photoSize._ == 'photoSizeProgressive') && photo.access_hash && photo.file_reference; + const isPhoto = (photoSize._ === 'photoSize' || photoSize._ === 'photoSizeProgressive') && photo.access_hash && photo.file_reference; const location: InputFileLocation.inputPhotoFileLocation | InputFileLocation.inputDocumentFileLocation | FileLocation = isPhoto ? { _: isMyDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation', id: photo.id, @@ -277,12 +279,26 @@ export class AppPhotosManager { return {url: getFileURL('photo', downloadOptions), location: downloadOptions.location}; } */ + + public isDownloaded(media: any) { + const isPhoto = media._ === 'photo'; + const photo = isPhoto ? this.getPhoto(media.id) : null; + let isDownloaded: boolean; + if(photo) { + isDownloaded = photo.downloaded > 0; + } else { + const cachedThumb = this.getDocumentCachedThumb(media.id); + isDownloaded = cachedThumb?.downloaded > 0; + } + + return isDownloaded; + } public preloadPhoto(photoId: any, photoSize?: PhotoSize, queueId?: number): CancellablePromise { const photo = this.getPhoto(photoId); // @ts-ignore - if(!photo || photo._ == 'photoEmpty') { + if(!photo || photo._ === 'photoEmpty') { throw new Error('preloadPhoto photoEmpty!'); } @@ -325,7 +341,7 @@ export class AppPhotosManager { } public getCacheContext(photo: any): DocumentCacheThumb { - return photo._ == 'document' ? this.getDocumentCachedThumb(photo.id) : photo; + return photo._ === 'document' ? this.getDocumentCachedThumb(photo.id) : photo; } public getDocumentCachedThumb(docId: string) { @@ -351,7 +367,7 @@ export class AppPhotosManager { public savePhotoFile(photo: MyPhoto | MyDocument, queueId?: number) { const fullPhotoSize = this.choosePhotoSize(photo, 0xFFFF, 0xFFFF); - if(!(fullPhotoSize._ == 'photoSize' || fullPhotoSize._ == 'photoSizeProgressive')) { + if(!(fullPhotoSize._ === 'photoSize' || fullPhotoSize._ === 'photoSizeProgressive')) { return; } diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 0987250a..9b688d3d 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -1,18 +1,51 @@ avatar-element { --size: 54px; --multiplier: 1; + --color-top: var(--peer-avatar-blue-top); + --color-bottom: var(--peer-avatar-blue-bottom); color: #fff; width: var(--size); height: var(--size); line-height: var(--size); border-radius: 50%; - background-color: $color-blue; + background: linear-gradient(var(--color-top), var(--color-bottom)); text-align: center; font-size: calc(1.25rem / var(--multiplier)); /* overflow: hidden; */ position: relative; user-select: none; text-transform: uppercase; + font-weight: 700; + + &[data-color="red"] { + --color-top: var(--peer-avatar-red-top); + --color-bottom: var(--peer-avatar-red-bottom); + } + + &[data-color="orange"] { + --color-top: var(--peer-avatar-orange-top); + --color-bottom: var(--peer-avatar-orange-bottom); + } + + &[data-color="violet"] { + --color-top: var(--peer-avatar-violet-top); + --color-bottom: var(--peer-avatar-violet-bottom); + } + + &[data-color="green"] { + --color-top: var(--peer-avatar-green-top); + --color-bottom: var(--peer-avatar-green-bottom); + } + + &[data-color="cyan"] { + --color-top: var(--peer-avatar-cyan-top); + --color-bottom: var(--peer-avatar-cyan-bottom); + } + + &[data-color="pink"] { + --color-top: var(--peer-avatar-pink-top); + --color-bottom: var(--peer-avatar-pink-bottom); + } &.tgico-savedmessages:before { font-size: calc(25px / var(--multiplier)); diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 02e1b452..bd9f313a 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -1039,7 +1039,7 @@ $chat-helper-size: 39px; } &-container .preloader-circular { - background-color: var(--message-time-background); + background-color: rgba(0, 0, 0, .3); } } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 0f5b553c..b48e384a 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -597,16 +597,10 @@ $bubble-margin: .25rem; display: flex; // lol justify-content: center; position: relative; + cursor: pointer; img, video { max-width: 100%; - cursor: pointer; - opacity: 1; - transition: opacity .3s ease; - - body.animation-level-0 & { - transition: none; - } } .download { @@ -633,10 +627,10 @@ $bubble-margin: .25rem; display: none; } } + } - .preloader-container { - z-index: 1; - } + .preloader-container { + z-index: 1; } &:not(.sticker) { diff --git a/src/scss/partials/_preloader.scss b/src/scss/partials/_preloader.scss index dcda2f9b..3514a06b 100644 --- a/src/scss/partials/_preloader.scss +++ b/src/scss/partials/_preloader.scss @@ -28,10 +28,24 @@ left: 0; right: 0; margin: auto; - width: 50px; - height: 50px; + width: 54px; + height: 54px; display: flex; /* cursor: pointer; */ + + opacity: 0; + transform: scale(0); + + body:not(.animation-level-0) & { + transition: opacity .2s ease-in-out, transform .2s ease-in-out; + } + + &.is-visible { + &:not(.backwards) { + opacity: 1; + transform: scale(1); + } + } } } @@ -52,12 +66,12 @@ } .preloader-path-new { - stroke-dasharray: 5, 200; + stroke-dasharray: 5, 149.82; stroke-dashoffset: 0; transition: stroke-dasharray 400ms ease-in-out; stroke-linecap: round; stroke: white; - stroke-width: 1.5; + stroke-width: 2; } &.preloader-swing { @@ -68,9 +82,7 @@ } .preloader-path-new { - stroke-dasharray: 1, 200; - stroke-dashoffset: 0; - animation: dashNew 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */; + animation: dashNew 1.5s ease-in-out infinite; } } @@ -111,7 +123,7 @@ background-color: #fff; left: 50%; top: 50%; - transform: translate3d(-50%,-50%,0); + transform: translate3d(-50%, -50%, 0); } } } @@ -139,16 +151,16 @@ @keyframes dashNew { 0% { - stroke-dasharray: 1, 200; + stroke-dasharray: 1, 149.82; // 149.82 = getTotalLength stroke-dashoffset: 0; } 50% { - stroke-dasharray: 89, 200; - stroke-dashoffset: -35px; + stroke-dasharray: 112.36, 149.82; // 112.36 = 149.82 * .75 + stroke-dashoffset: -38; // bruted } 100% { - stroke-dasharray: 89, 200; - stroke-dashoffset: -286%; + stroke-dasharray: 112.36, 149.82; + stroke-dashoffset: -149.82; // totalLength } } diff --git a/src/scss/partials/_rightSidebar.scss b/src/scss/partials/_rightSidebar.scss index 1c5e86f0..3949e89d 100644 --- a/src/scss/partials/_rightSidebar.scss +++ b/src/scss/partials/_rightSidebar.scss @@ -361,15 +361,6 @@ .grid-item { overflow: hidden; - - &-media { - opacity: 1; - transition: opacity .2s ease; - - html:not(.is-mac) &.thumbnail { - filter: blur(7px); - } - } } /* span.video-play { diff --git a/src/scss/style.scss b/src/scss/style.scss index facae31f..278c36ce 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -96,6 +96,22 @@ $chat-padding-handhelds: .5rem; --messages-secondary-text-size: calc(var(--messages-text-size) - 1px); --esg-sticker-size: 80px; + // https://github.com/overtake/TelegramSwift/blob/5cc7d2475fe4738a6aa0486c23eaf80a89d33b97/submodules/TGUIKit/TGUIKit/PresentationTheme.swift#L2054 + --peer-avatar-red-top: #ff885e; + --peer-avatar-red-bottom: #ff516a; + --peer-avatar-orange-top: #ffcd6a; + --peer-avatar-orange-bottom: #ffa85c; + --peer-avatar-violet-top: #82b1ff; + --peer-avatar-violet-bottom: #665fff; + --peer-avatar-green-top: #a0de7e; + --peer-avatar-green-bottom: #54cb68; + --peer-avatar-cyan-top: #53edd6; + --peer-avatar-cyan-bottom: #28c9b7; + --peer-avatar-blue-top: #72d5fd; + --peer-avatar-blue-bottom: #2a9ef1; + --peer-avatar-pink-top: #e0a2f3; + --peer-avatar-pink-bottom: #d669ed; + @include respond-to(handhelds) { --right-column-width: 100vw; --esg-sticker-size: 68px; @@ -859,7 +875,6 @@ img.emoji { top: 0; width: 100%; height: 100%; - -o-object-fit: cover; object-fit: cover; } } @@ -1161,3 +1176,19 @@ middle-ellipsis-element { background-color: var(--color-gray-hover); } } + +.media-photo, .media-video { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + &.fade-in { + animation: fade-in-opacity .2s ease-in-out forwards; + } +} + +.media-video { + z-index: 1; // * overflow media-photo +} diff --git a/src/vendor/fastBlur.js b/src/vendor/fastBlur.js new file mode 100644 index 00000000..972f9178 --- /dev/null +++ b/src/vendor/fastBlur.js @@ -0,0 +1,160 @@ +/* +Superfast Blur - a fast Box Blur For Canvas + +Version: 0.5 +Author: Mario Klingemann +Contact: mario@quasimondo.com +Website: http://www.quasimondo.com/BoxBlurForCanvas +Twitter: @quasimondo + +In case you find this class useful - especially in commercial projects - +I am not totally unhappy for a small donation to my PayPal account +mario@quasimondo.de + +Or support me on flattr: +https://flattr.com/thing/140066/Superfast-Blur-a-pretty-fast-Box-Blur-Effect-for-CanvasJavascript + +Copyright (c) 2011 Mario Klingemann + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +// eslint-disable-next-line max-len +const mul_table = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1]; +// eslint-disable-next-line max-len +const shg_table = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18]; + +export default function boxBlurCanvasRGB(context, top_x, top_y, width, height, radius, iterations) { + if (Number.isNaN(radius) || radius < 1) return; + + radius |= 0; + + if (Number.isNaN(iterations)) iterations = 1; + iterations |= 0; + if (iterations > 3) iterations = 3; + if (iterations < 1) iterations = 1; + + const imageData = context.getImageData(top_x, top_y, width, height); + + const pixels = imageData.data; + + let rsum; + let gsum; + let bsum; + let x; + let y; + let i; + let p; + let p1; + let p2; + let yp; + let yi; + let yw; + let wm = width - 1; + let hm = height - 1; + let rad1 = radius + 1; + + let r = []; + let g = []; + let b = []; + + let mul_sum = mul_table[radius]; + let shg_sum = shg_table[radius]; + + let vmin = []; + let vmax = []; + + while (iterations-- > 0) { + yw = yi = 0; + + for (y = 0; y < height; y++) { + rsum = pixels[yw] * rad1; + gsum = pixels[yw + 1] * rad1; + bsum = pixels[yw + 2] * rad1; + + for (i = 1; i <= radius; i++) { + p = yw + (((i > wm ? wm : i)) << 2); + rsum += pixels[p++]; + gsum += pixels[p++]; + bsum += pixels[p++]; + } + + for (x = 0; x < width; x++) { + r[yi] = rsum; + g[yi] = gsum; + b[yi] = bsum; + + if (y == 0) { + vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2; + vmax[x] = ((p = x - radius) > 0 ? p << 2 : 0); + } + + p1 = yw + vmin[x]; + p2 = yw + vmax[x]; + + rsum += pixels[p1++] - pixels[p2++]; + gsum += pixels[p1++] - pixels[p2++]; + bsum += pixels[p1++] - pixels[p2++]; + + yi++; + } + yw += (width << 2); + } + + for (x = 0; x < width; x++) { + yp = x; + rsum = r[yp] * rad1; + gsum = g[yp] * rad1; + bsum = b[yp] * rad1; + + for (i = 1; i <= radius; i++) { + yp += (i > hm ? 0 : width); + rsum += r[yp]; + gsum += g[yp]; + bsum += b[yp]; + } + + yi = x << 2; + for (y = 0; y < height; y++) { + pixels[yi] = (rsum * mul_sum) >>> shg_sum; + pixels[yi + 1] = (gsum * mul_sum) >>> shg_sum; + pixels[yi + 2] = (bsum * mul_sum) >>> shg_sum; + + if (x == 0) { + vmin[y] = ((p = y + rad1) < hm ? p : hm) * width; + vmax[y] = ((p = y - radius) > 0 ? p * width : 0); + } + + p1 = x + vmin[y]; + p2 = x + vmax[y]; + + rsum += r[p1] - r[p2]; + gsum += g[p1] - g[p2]; + bsum += b[p1] - b[p2]; + + yi += width << 2; + } + } + } + + context.putImageData(imageData, top_x, top_y); +}