diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index 4b9bdd64..9522629d 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -1066,8 +1066,8 @@ export default class AppMediaViewerBase< 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')) { + if(el && !findUpClassName(target, 'document')) { + if(findUpClassName(target, 'attachment')) { // two parentElements because element can be contained in aspecter const preloader = target.parentElement.parentElement.querySelector('.preloader-container') as HTMLElement; if(preloader) { diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 31e8f347..2dbba1f8 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -12,7 +12,7 @@ import { toast } from "../toast"; import { prepareAlbum, wrapDocument } from "../wrappers"; import CheckboxField from "../checkboxField"; import SendContextMenu from "../chat/sendContextMenu"; -import { createPosterFromVideo, onMediaLoad } from "../../helpers/files"; +import { createPosterFromMedia, createPosterFromVideo, onMediaLoad } from "../../helpers/files"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; import I18n, { i18n, LangPackKey } from "../../lib/langPack"; import appDownloadManager from "../../lib/appManagers/appDownloadManager"; @@ -23,6 +23,7 @@ import RichTextProcessor from "../../lib/richtextprocessor"; import { MediaSize } from "../../helpers/mediaSizes"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; +import getGifDuration from "../../helpers/getGifDuration"; type SendFileParams = Partial<{ file: File, @@ -291,7 +292,27 @@ export default class PopupNewMedia extends PopupElement { params.height = img.naturalHeight; itemDiv.append(img); - resolve(itemDiv); + + if(file.type === 'image/gif') { + params.noSound = true; + + Promise.all([ + getGifDuration(img).then(duration => { + params.duration = Math.ceil(duration); + }), + + createPosterFromMedia(img).then(thumb => { + params.thumb = { + url: URL.createObjectURL(thumb.blob), + ...thumb + }; + }) + ]).then(() => { + resolve(itemDiv); + }); + } else { + resolve(itemDiv); + } }; } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 0454423b..6a99af91 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -577,7 +577,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS icoDiv.classList.add('document-ico'); const cacheContext = appDownloadManager.getCacheContext(doc); - if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo')) && doc.mime_type !== 'image/gif') { + if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo'))/* && doc.mime_type !== 'image/gif' */) { docDiv.classList.add('document-with-thumb'); let imgs: HTMLImageElement[] = []; @@ -593,7 +593,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS boxHeight: 54, loadPromises, withoutPreloader: true, - lazyLoadQueue + lazyLoadQueue, + size: appPhotosManager.choosePhotoSize(doc, 54, 54, true) }); icoDiv.style.width = icoDiv.style.height = ''; if(wrapped.images.thumb) imgs.push(wrapped.images.thumb); @@ -832,13 +833,20 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT let thumbImage: HTMLImageElement; let image: HTMLImageElement; let cacheContext: ThumbCache; + const isGif = photo._ === 'document' && photo.mime_type === 'image/gif' && !size; // if(withTail) { // image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut); // } else { image = new Image(); if(boxWidth && boxHeight && !size) { // !album - const set = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message); + const set = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message, undefined, isGif ? { + _: 'photoSize', + w: photo.w, + h: photo.h, + size: photo.size, + type: 'full' + } : undefined); size = set.photoSize; isFit = set.isFit; cacheContext = appDownloadManager.getCacheContext(photo, size.type); @@ -920,7 +928,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT } const getDownloadPromise = () => { - const promise = photo._ === 'document' && photo.mime_type === 'image/gif' ? + const promise = isGif && !size ? appDocsManager.downloadDoc(photo, /* undefined, */lazyLoadQueue?.queueId) : appPhotosManager.preloadPhoto(photo, size, lazyLoadQueue?.queueId, noAutoDownload); diff --git a/src/environment/imageMimeTypesSupport.ts b/src/environment/imageMimeTypesSupport.ts new file mode 100644 index 00000000..bbc25893 --- /dev/null +++ b/src/environment/imageMimeTypesSupport.ts @@ -0,0 +1,13 @@ +import IS_WEBP_SUPPORTED from "./webpSupport"; + +const IMAGE_MIME_TYPES_SUPPORTED = new Set([ + 'image/jpeg', + 'image/png', + 'image/bmp' +]); + +if(IS_WEBP_SUPPORTED) { + IMAGE_MIME_TYPES_SUPPORTED.add('image/webp'); +} + +export default IMAGE_MIME_TYPES_SUPPORTED; diff --git a/src/environment/mediaMimeTypesSupport.ts b/src/environment/mediaMimeTypesSupport.ts index 3125e725..82666b95 100644 --- a/src/environment/mediaMimeTypesSupport.ts +++ b/src/environment/mediaMimeTypesSupport.ts @@ -1,21 +1,8 @@ -import IS_MOV_SUPPORTED from "./movSupport"; -import IS_WEBP_SUPPORTED from "./webpSupport"; +import IMAGE_MIME_TYPES_SUPPORTED from "./imageMimeTypesSupport"; +import VIDEO_MIME_TYPES_SUPPORTED from "./videoMimeTypesSupport"; -const MEDIA_MIME_TYPES_SUPPORTED = new Set([ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/bmp', - 'video/mp4', - 'video/webm' -]); +const arr = [...IMAGE_MIME_TYPES_SUPPORTED].concat([...VIDEO_MIME_TYPES_SUPPORTED]); -if(IS_MOV_SUPPORTED) { - MEDIA_MIME_TYPES_SUPPORTED.add('video/quicktime'); -} - -if(IS_WEBP_SUPPORTED) { - MEDIA_MIME_TYPES_SUPPORTED.add('image/webp'); -} +const MEDIA_MIME_TYPES_SUPPORTED = new Set(arr); export default MEDIA_MIME_TYPES_SUPPORTED; diff --git a/src/environment/videoMimeTypesSupport.ts b/src/environment/videoMimeTypesSupport.ts new file mode 100644 index 00000000..db379b10 --- /dev/null +++ b/src/environment/videoMimeTypesSupport.ts @@ -0,0 +1,13 @@ +import IS_MOV_SUPPORTED from "./movSupport"; + +const VIDEO_MIME_TYPES_SUPPORTED = new Set([ + 'image/gif', // have to display it as video + 'video/mp4', + 'video/webm' +]); + +if(IS_MOV_SUPPORTED) { + VIDEO_MIME_TYPES_SUPPORTED.add('video/quicktime'); +} + +export default VIDEO_MIME_TYPES_SUPPORTED; diff --git a/src/helpers/files.ts b/src/helpers/files.ts index ef5542ff..4a2114d2 100644 --- a/src/helpers/files.ts +++ b/src/helpers/files.ts @@ -38,16 +38,29 @@ export function preloadVideo(url: string): Promise { }); } +export function createPosterFromMedia(media: HTMLVideoElement | HTMLImageElement) { + let width: number, height: number; + if(media instanceof HTMLVideoElement) { + width = media.videoWidth; + height = media.videoHeight; + } else { + width = media.naturalWidth; + height = media.naturalHeight; + } + + return scaleMediaElement({ + media, + mediaSize: makeMediaSize(width, height), + boxSize: makeMediaSize(320, 240), + quality: .9 + }); +} + export function createPosterFromVideo(video: HTMLVideoElement): ReturnType { return new Promise((resolve, reject) => { video.onseeked = () => { video.onseeked = () => { - scaleMediaElement({ - media: video, - mediaSize: makeMediaSize(video.videoWidth, video.videoHeight), - boxSize: makeMediaSize(320, 240), - quality: .9 - }).then(resolve); + createPosterFromMedia(video).then(resolve); video.onseeked = undefined; }; diff --git a/src/helpers/getGifDuration.ts b/src/helpers/getGifDuration.ts new file mode 100644 index 00000000..e6f7b33c --- /dev/null +++ b/src/helpers/getGifDuration.ts @@ -0,0 +1,31 @@ +/** + * @returns duration in ms + */ +export default function getGifDuration(image: HTMLImageElement) { + const src = image.src; + + return fetch(src) + .then(response => response.arrayBuffer()) + .then(arrayBuffer => { + const d = new Uint8Array(arrayBuffer); + // Thanks to http://justinsomnia.org/2006/10/gif-animation-duration-calculation/ + // And http://www.w3.org/Graphics/GIF/spec-gif89a.txt + let duration = 0; + for(let i = 0, length = d.length; i < length; ++i) { + // Find a Graphic Control Extension hex(21F904__ ____ __00) + if(d[i] == 0x21 + && d[i + 1] == 0xF9 + && d[i + 2] == 0x04 + && d[i + 7] == 0x00) { + // Swap 5th and 6th bytes to get the delay per frame + const delay = (d[i + 5] << 8) | (d[i + 4] & 0xFF); + + // Should be aware browsers have a minimum frame delay + // e.g. 6ms for IE, 2ms modern browsers (50fps) + duration += delay < 2 ? 10 : delay; + } + } + + return duration / 1000; + }); +} diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index d1685bd4..962978a6 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -59,6 +59,8 @@ import { getMiddleware } from "../../helpers/middleware"; import assumeType from "../../helpers/assumeType"; import appMessagesIdsManager from "./appMessagesIdsManager"; import type { MediaSize } from "../../helpers/mediaSizes"; +import IMAGE_MIME_TYPES_SUPPORTED from "../../environment/imageMimeTypesSupport"; +import VIDEO_MIME_TYPES_SUPPORTED from "../../environment/videoMimeTypesSupport"; //console.trace('include'); // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках @@ -643,7 +645,7 @@ export class AppMessagesManager { const message = this.generateOutgoingMessage(peerId, options); const replyToMsgId = options.replyToMsgId ? appMessagesIdsManager.getServerMessageId(options.replyToMsgId) : undefined; - let attachType: string, apiFileName: string; + let attachType: 'document' | 'audio' | 'video' | 'voice' | 'photo', apiFileName: string; const fileType = 'mime_type' in file ? file.mime_type : file.type; const fileName = file instanceof File ? file.name : ''; @@ -659,7 +661,7 @@ export class AppMessagesManager { const attributes: DocumentAttribute[] = []; - const isPhoto = ['image/jpeg', 'image/png', 'image/bmp'].indexOf(fileType) >= 0; + const isPhoto = IMAGE_MIME_TYPES_SUPPORTED.has(fileType); let photo: MyPhoto, document: MyDocument; @@ -718,7 +720,7 @@ export class AppMessagesManager { cacheContext.url = options.objectURL || ''; photo = appPhotosManager.savePhoto(photo); - } else if(fileType.indexOf('video/') === 0) { + } else if(VIDEO_MIME_TYPES_SUPPORTED.has(fileType)) { attachType = 'video'; apiFileName = 'video.mp4'; actionName = 'sendMessageUploadVideoAction'; @@ -752,7 +754,7 @@ export class AppMessagesManager { attributes.push({_: 'documentAttributeFilename', file_name: fileName || apiFileName}); - if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1 && !isDocument) { + if((['document', 'video', 'audio', 'voice'] as (typeof attachType)[]).indexOf(attachType) !== -1 && !isDocument) { const thumbs: PhotoSize[] = []; document = { _: 'document', @@ -4972,7 +4974,7 @@ export class AppMessagesManager { } else if(newDoc) { const doc = appDocsManager.getDoc('' + tempId); if(doc) { - if(/* doc._ !== 'documentEmpty' && */doc.type && doc.type !== 'sticker') { + if(/* doc._ !== 'documentEmpty' && */doc.type && doc.type !== 'sticker' && doc.mime_type !== 'image/gif') { const cacheContext = appDownloadManager.getCacheContext(newDoc); const oldCacheContext = appDownloadManager.getCacheContext(doc); Object.assign(cacheContext, oldCacheContext); diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 5cad36f5..b1b4489f 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -219,14 +219,19 @@ export class AppPhotosManager { return {image, loadPromise}; } - public setAttachmentSize(photo: MyPhoto | MyDocument, + public setAttachmentSize( + photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true, message?: any, - pushDocumentSize?: boolean) { - const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight, undefined, pushDocumentSize); + pushDocumentSize?: boolean, + photoSize?: ReturnType + ) { + if(!photoSize) { + photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight, undefined, pushDocumentSize); + } //console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div); let size: MediaSize;