From 1bb0f31026f77e15c27a43b10ef9c6b8af546ff9 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Fri, 14 May 2021 07:23:17 +0400 Subject: [PATCH] Media aspecter container Fix urls Fix merging message entities Decrease sticker size --- src/components/appMediaViewer.ts | 2 +- src/components/chat/bubbles.ts | 12 ++-- src/components/chat/markupTooltip.ts | 6 +- src/components/wrappers.ts | 44 +++++++++++--- src/helpers/mediaSizes.ts | 15 ++++- src/lib/appManagers/appPhotosManager.ts | 38 ++++++++---- src/lib/richtextprocessor.ts | 80 ++++++++++++++++--------- src/scss/partials/_chatBubble.scss | 19 +++++- 8 files changed, 157 insertions(+), 59 deletions(-) diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 1b7bf697..bf911d55 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -956,7 +956,7 @@ class AppMediaViewerBase = Promise.resolve(); - const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight, mediaSizes.isMobile ? false : true); + const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight, mediaSizes.isMobile ? false : true).photoSize; if(useContainerAsTarget) { const cacheContext = appDownloadManager.getCacheContext(media, size.type); let img: HTMLImageElement; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index d837ab19..44bcd5c2 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -859,9 +859,11 @@ export default class ChatBubbles { str += '.attachment video, .attachment img'; } + const hasAspecter = !!this.bubbles[id].querySelector('.media-container-aspecter'); let elements = this.bubbles[id].querySelectorAll(str) as NodeListOf; const parents: Set = new Set(); Array.from(elements).forEach((element: HTMLElement) => { + if(hasAspecter && !findUpClassName(element, 'media-container-aspecter')) return; let albumItem = findUpClassName(element, 'album-item'); const parent = albumItem || element.parentElement; if(parents.has(parent)) return; @@ -2330,6 +2332,7 @@ export default class ChatBubbles { if(size.w === size.h && quoteTextDiv.childElementCount) { bubble.classList.add('is-square-photo'); isSquare = true; + this.appPhotosManager.setAttachmentSize(webpage.photo, preview, 80, 80, false); } else if(size.h > size.w) { bubble.classList.add('is-vertical-photo'); } @@ -2338,8 +2341,8 @@ export default class ChatBubbles { photo: webpage.photo, message, container: preview, - boxWidth: mediaSizes.active.webpage.width, - boxHeight: mediaSizes.active.webpage.height, + boxWidth: isSquare ? 0 : mediaSizes.active.webpage.width, + boxHeight: isSquare ? 0 : mediaSizes.active.webpage.height, isOut, lazyLoadQueue: this.lazyLoadQueue, middleware: this.getMiddleware(), @@ -2372,8 +2375,9 @@ export default class ChatBubbles { bubble.classList.add('sticker-animated'); } - let size = bubble.classList.contains('emoji-big') ? 140 : 200; - this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size, size); + const sizes = mediaSizes.active; + const size = bubble.classList.contains('emoji-big') ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker); + this.appPhotosManager.setAttachmentSize(doc, attachmentDiv, size.width, size.height); //let preloader = new ProgressivePreloader(attachmentDiv, false); bubbleContainer.style.height = attachmentDiv.style.height; bubbleContainer.style.width = attachmentDiv.style.width; diff --git a/src/components/chat/markupTooltip.ts b/src/components/chat/markupTooltip.ts index 0939bacf..adb3ebfc 100644 --- a/src/components/chat/markupTooltip.ts +++ b/src/components/chat/markupTooltip.ts @@ -170,7 +170,11 @@ export default class MarkupTooltip { private applyLink(e: Event) { cancelEvent(e); this.resetSelection(); - this.appImManager.chat.input.applyMarkdown('link', this.linkInput.value); + let url = this.linkInput.value; + if(url && !RichTextProcessor.matchUrlProtocol(url)) { + url = 'https://' + url; + } + this.appImManager.chat.input.applyMarkdown('link', url); setTimeout(() => { this.hide(); }, 0); diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 0bfb8da8..f261e2e4 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -283,7 +283,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } if(!video.parentElement && container) { - container.append(video); + (photoRes?.aspecter || container).append(video); } const cacheContext = appDownloadManager.getCacheContext(doc); @@ -669,7 +669,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT }) { if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { if(boxWidth && boxHeight && !size && photo._ === 'document') { - size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message && message.message); + appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message); } return { @@ -681,7 +681,8 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT thumb: null, full: null }, - preloader: null + preloader: null, + aspecter: null }; } @@ -690,7 +691,11 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT if(boxHeight === undefined) boxHeight = mediaSizes.active.regular.height; } - let loadThumbPromise: Promise; + container.classList.add('media-container'); + let aspecter = container; + + let isFit = true; + let loadThumbPromise: Promise = Promise.resolve(); let thumbImage: HTMLImageElement; let image: HTMLImageElement; // if(withTail) { @@ -699,15 +704,35 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT image = new Image(); if(boxWidth && boxHeight && !size) { // !album - size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message && message.message); + const set = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message); + size = set.photoSize; + isFit = set.isFit; + + if(!isFit) { + aspecter = document.createElement('div'); + aspecter.classList.add('media-container-aspecter'); + aspecter.style.width = set.size.width + 'px'; + aspecter.style.height = set.size.height + 'px'; + + const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, !noBlur, true); + if(gotThumb) { + loadThumbPromise = gotThumb.loadPromise; + const thumbImage = gotThumb.image; // local scope + thumbImage.classList.add('media-photo'); + container.append(thumbImage); + } + + container.classList.add('media-container-fitted'); + container.append(aspecter); + } } const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, !noBlur); if(gotThumb) { - loadThumbPromise = gotThumb.loadPromise; + loadThumbPromise = Promise.all([loadThumbPromise, gotThumb.loadPromise]); thumbImage = gotThumb.image; thumbImage.classList.add('media-photo'); - container.append(thumbImage); + aspecter.append(thumbImage); } // } @@ -754,7 +779,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT renderImageFromUrl(image, cacheContext.url, () => { sequentialDom.mutateElement(container, () => { - container.append(image); + aspecter.append(image); fastRaf(() => { resolve(); @@ -820,7 +845,8 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT thumb: thumbImage, full: image }, - preloader + preloader, + aspecter }; } diff --git a/src/helpers/mediaSizes.ts b/src/helpers/mediaSizes.ts index 73aef1d0..e51f9a96 100644 --- a/src/helpers/mediaSizes.ts +++ b/src/helpers/mediaSizes.ts @@ -34,7 +34,10 @@ type MediaTypeSizes = { regular: MediaSize, webpage: MediaSize, album: MediaSize, - esgSticker: MediaSize + esgSticker: MediaSize, + animatedSticker: MediaSize, + staticSticker: MediaSize, + emojiSticker: MediaSize }; export enum ScreenSize { @@ -61,13 +64,19 @@ class MediaSizes extends EventListenerBase<{ regular: makeMediaSize(270, 270), webpage: makeMediaSize(270, 200), album: makeMediaSize(270, 0), - esgSticker: makeMediaSize(68, 68) + esgSticker: makeMediaSize(68, 68), + animatedSticker: makeMediaSize(160, 160), + staticSticker: makeMediaSize(160, 160), + emojiSticker: makeMediaSize(112, 112) }, desktop: { regular: makeMediaSize(400, 320), webpage: makeMediaSize(400, 320), album: makeMediaSize(420, 0), - esgSticker: makeMediaSize(80, 80) + esgSticker: makeMediaSize(80, 80), + animatedSticker: makeMediaSize(200, 200), + staticSticker: makeMediaSize(200, 200), + emojiSticker: makeMediaSize(112, 112) } }; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 90f73852..5c47696a 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -216,7 +216,7 @@ export class AppPhotosManager { return {image, loadPromise}; } - public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true, hasText?: boolean) { + public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true, message?: any) { const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight); //console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div); @@ -227,13 +227,29 @@ export class AppPhotosManager { size = makeMediaSize('w' in photoSize ? photoSize.w : 100, 'h' in photoSize ? photoSize.h : 100); } - const boxSize = makeMediaSize(boxWidth, boxHeight); + let boxSize = makeMediaSize(boxWidth, boxHeight); - size = size.aspect(boxSize, noZoom); + boxSize = size = size.aspect(boxSize, noZoom); - // /* if(hasText) { - // w = Math.max(boxWidth, w); - // } */ + let isFit = true; + + if(photo._ === 'photo' || ['video', 'gif'].includes(photo.type)) { + if(boxSize.width < 200 && boxSize.height < 200) { // make at least one side this big + boxSize = size = size.aspectCovered(makeMediaSize(200, 200)); + } + + if(message && (message.message || message.media.webpage || message.replies)) { // make sure that bubble block is human-readable + if(boxSize.width < 320) { + boxSize = makeMediaSize(320, boxSize.height); + isFit = false; + } + } + + if(isFit && boxSize.width < 120) { // if image is too narrow + boxSize = makeMediaSize(120, boxSize.height); + isFit = false; + } + } // if(element instanceof SVGForeignObjectElement) { // element.setAttributeNS(null, 'width', '' + w); @@ -241,16 +257,16 @@ export class AppPhotosManager { // //console.log('set dimensions to svg element:', element, w, h); // } else { - element.style.width = size.width + 'px'; - element.style.height = size.height + 'px'; + element.style.width = boxSize.width + 'px'; + element.style.height = boxSize.height + 'px'; // } - return photoSize; + return {photoSize, size, isFit}; } - public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument, useBlur: boolean): ReturnType { + public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument, useBlur: boolean, ignoreCache = false): ReturnType { const cacheContext = appDownloadManager.getCacheContext(photo); - if(!cacheContext.downloaded || (photo as MyDocument).type === 'video' || (photo as MyDocument).type === 'gif') { + if(!cacheContext.downloaded || (['video', 'gif'] as MyDocument['type'][]).includes((photo as MyDocument).type) || ignoreCache) { if(photo._ === 'document' && cacheContext.downloaded) { return null; } diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index ad6607fc..d3806778 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -54,17 +54,19 @@ const alphaCharsRegExp = 'a-z' + const alphaNumericRegExp = '0-9\_' + alphaCharsRegExp; const domainAddChars = '\u00b7'; // Based on Regular Expression for URL validation by Diego Perini -const urlRegExp = '((?:https?|ftp)://|mailto:)?' + +const urlAlphanumericRegExpPart = '[' + alphaCharsRegExp + '0-9]'; +const urlProtocolRegExpPart = '((?:https?|ftp)://|mailto:)?'; +const urlRegExp = urlProtocolRegExpPart + // user:pass authentication - '(?:\\S{1,64}(?::\\S{0,64})?@)?' + + '(?:' + urlAlphanumericRegExpPart + '{1,64}(?::' + urlAlphanumericRegExpPart + '{0,64})?@)?' + '(?:' + // sindresorhus/ip-regexp '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}' + '|' + // host name - '[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}' + + urlAlphanumericRegExpPart + '[' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}' + // domain name - '(?:\\.[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}){0,10}' + + '(?:\\.' + urlAlphanumericRegExpPart + '[' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}){0,10}' + // TLD identifier '(?:\\.(xn--[0-9a-z]{2,16}|[' + alphaCharsRegExp + ']{2,24}))' + ')' + @@ -72,9 +74,11 @@ const urlRegExp = '((?:https?|ftp)://|mailto:)?' + '(?::\\d{2,5})?' + // resource path '(?:/(?:\\S{0,255}[^\\s.;,(\\[\\]{}<>"\'])?)?'; +const urlProtocolRegExp = new RegExp('^' + urlProtocolRegExpPart.slice(0, -1), 'i'); +const urlAnyProtocolRegExp = /^((?:.+?):\/\/|mailto:)/; const usernameRegExp = '[a-zA-Z\\d_]{5,32}'; const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(\\b|$)'; -const fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i') +const fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i'); const emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; //const markdownTestRegExp = /[`_*@~]/; const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m; @@ -91,7 +95,7 @@ const siteMentions: {[siteName: string]: string} = { Instagram: 'https://instagram.com/{1}/', GitHub: 'https://github.com/{1}' }; -const markdownEntities: {[markdown: string]: any} = { +const markdownEntities: {[markdown: string]: MessageEntity['_']} = { '`': 'messageEntityCode', '``': 'messageEntityPre', '**': 'messageEntityBold', @@ -100,6 +104,11 @@ const markdownEntities: {[markdown: string]: any} = { '_-_': 'messageEntityUnderline' }; +const passConflictingEntities: Set = new Set(); +for(let i in markdownEntities) { + passConflictingEntities.add(markdownEntities[i]); +} + namespace RichTextProcessor { export const emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */; @@ -120,11 +129,11 @@ namespace RichTextProcessor { } export function parseEntities(text: string) { - var match; - var raw = text, url; + let match: any; + let raw = text; const entities: MessageEntity[] = []; let matchIndex; - var rawOffset = 0; + let rawOffset = 0; // var start = tsNow() while((match = raw.match(fullRegExp))) { matchIndex = rawOffset + match.index; @@ -145,19 +154,19 @@ namespace RichTextProcessor { length: match[4].length }); } else { - var url: any = false; - var protocol = match[5]; - var tld = match[6]; - var excluded = ''; + let url: string; + let protocol = match[5]; + const tld = match[6]; + // let excluded = ''; if(tld) { // URL if(!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) { protocol = 'http://'; } if(protocol) { - var balanced = checkBrackets(match[4]); - if (balanced.length !== match[4].length) { - excluded = match[4].substring(balanced.length); + const balanced = checkBrackets(match[4]); + if(balanced.length !== match[4].length) { + // excluded = match[4].substring(balanced.length); match[4] = balanced; } @@ -167,7 +176,7 @@ namespace RichTextProcessor { url = (match[5] ? '' : 'http://') + match[4]; } - if (url) { + if(url) { entities.push({ _: 'messageEntityUrl', offset: matchIndex, @@ -183,7 +192,7 @@ namespace RichTextProcessor { }); } else if(match[8]) { // Emoji //console.log('hit', match[8]); - let emojiCoords = getEmojiSpritesheetCoords(match[8]); + const emojiCoords = getEmojiSpritesheetCoords(match[8]); if(emojiCoords) { entities.push({ _: 'messageEntityEmoji', @@ -233,7 +242,7 @@ namespace RichTextProcessor { const entities: MessageEntity[] = []; let pushedEntity = false; - const pushEntity = (entity: MessageEntity) => !findSameEntity(currentEntities, entity) ? (entities.push(entity), pushedEntity = true) : pushedEntity = false; + const pushEntity = (entity: MessageEntity) => !findConflictingEntity(currentEntities, entity) ? (entities.push(entity), pushedEntity = true) : pushedEntity = false; let raw = text; let match; @@ -273,7 +282,7 @@ namespace RichTextProcessor { const isSOH = match[6] === '\x01'; entity = { - _: markdownEntities[match[7]], + _: markdownEntities[match[7]] as (MessageEntity.messageEntityBold | MessageEntity.messageEntityCode | MessageEntity.messageEntityItalic)['_'], //offset: matchIndex + match[6].length, offset: matchIndex + (isSOH ? 0 : match[6].length), length: text.length @@ -341,17 +350,25 @@ namespace RichTextProcessor { return newText; } - export function findSameEntity(currentEntities: MessageEntity[], newEntity: MessageEntity) { + export function findConflictingEntity(currentEntities: MessageEntity[], newEntity: MessageEntity) { return currentEntities.find(currentEntity => { - return newEntity._ === currentEntity._ && - newEntity.offset >= currentEntity.offset && + const isConflictingTypes = newEntity._ === currentEntity._ || + (!passConflictingEntities.has(newEntity._) && !passConflictingEntities.has(currentEntity._)); + + if(!isConflictingTypes) { + return false; + } + + const isConflictingOffset = newEntity.offset >= currentEntity.offset && (newEntity.length + newEntity.offset) <= (currentEntity.length + currentEntity.offset); + + return isConflictingOffset; }); } export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[]) { const filtered = newEntities.filter(e => { - return !findSameEntity(currentEntities, e); + return !findConflictingEntity(currentEntities, e); }); currentEntities.push(...filtered); @@ -536,7 +553,7 @@ namespace RichTextProcessor { if(!(options.noLinks && !passEntities[entity._])) { const entityText = text.substr(entity.offset, entity.length); - let inner: string; + // let inner: string; let url: string; let masked = false; if(entity._ === 'messageEntityTextUrl') { @@ -548,6 +565,9 @@ namespace RichTextProcessor { nextEntity.length === entity.length && nextEntity.offset === entity.offset) { i++; + } + + if(url !== entityText) { masked = true; } } else { @@ -721,7 +741,7 @@ namespace RichTextProcessor { } export function wrapUrl(url: string, unsafe?: number | boolean): string { - if(!url.match(/^https?:\/\//i)) { + if(!matchUrlProtocol(url)) { url = 'https://' + url; } @@ -729,7 +749,7 @@ namespace RichTextProcessor { let telescoPeMatch; /* if(unsafe === 2) { url = 'tg://unsafe_url?url=' + encodeURIComponent(url); - } else */if((tgMeMatch = url.match(/^https?:\/\/t(?:elegram)?\.me\/(.+)/))) { + } else */if((tgMeMatch = url.match(/^(?:https?:\/\/)?t(?:elegram)?\.me\/(.+)/))) { const fullPath = tgMeMatch[1]; const path = fullPath.split('/'); switch(path[0]) { @@ -771,7 +791,7 @@ namespace RichTextProcessor { break; } - } else if((telescoPeMatch = url.match(/^https?:\/\/telesco\.pe\/([^/?]+)\/(\d+)/))) { + } else if((telescoPeMatch = url.match(/^(?:https?:\/\/)?telesco\.pe\/([^/?]+)\/(\d+)/))) { url = 'tg://resolve?domain=' + telescoPeMatch[1] + '&post=' + telescoPeMatch[2]; }/* else if(unsafe) { url = 'tg://unsafe_url?url=' + encodeURIComponent(url); @@ -779,6 +799,10 @@ namespace RichTextProcessor { return url; } + + export function matchUrlProtocol(text: string) { + return !text ? null : text.match(urlAnyProtocolRegExp); + } export function matchUrl(text: string) { return !text ? null : text.match(urlRegExp); diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 4aa15226..7164be32 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -604,6 +604,21 @@ $bubble-margin: .25rem; } } + .media-container { + &-aspecter { + position: relative; + margin: 0 auto; + } + + &-fitted { + background-color: transparent !important; + + .thumbnail { + opacity: .8; + } + } + } + .preloader-container { z-index: 2; } @@ -798,11 +813,11 @@ $bubble-margin: .25rem; } } - &.is-vertical-photo { + /* &.is-vertical-photo { .bubble-content { width: fit-content; } - } + } */ .reply { padding: 4px;