From c9abc4868b1fbc239c342454d92ef4c12a23ae84 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Sat, 7 Nov 2020 05:48:07 +0200 Subject: [PATCH] Middle ellipsis component Refactor utils --- src/components/animationIntersector.ts | 2 +- src/components/appMediaViewer.ts | 2 +- src/components/appSearch.ts | 2 +- src/components/appSelectPeers.ts | 2 +- src/components/audio.ts | 3 +- src/components/avatar.ts | 2 +- src/components/bubbleGroups.ts | 2 +- src/components/chat/audio.ts | 2 +- src/components/chat/contextMenu.ts | 2 +- src/components/chat/input.ts | 2 +- src/components/chat/messageRender.ts | 2 +- src/components/chat/pinnedContainer.ts | 2 +- src/components/chat/search.ts | 2 +- src/components/chat/selection.ts | 2 +- src/components/dialogsContextMenu.ts | 2 +- src/components/emoticonsDropdown/index.ts | 2 +- src/components/gifsMasonry.ts | 2 +- src/components/groupedLayout.ts | 14 +- src/components/horizontalMenu.ts | 2 +- src/components/middleEllipsis.ts | 142 +++++ src/components/poll.ts | 2 +- src/components/popup.ts | 2 +- src/components/popupCreatePoll.ts | 2 +- src/components/popupNewMedia.ts | 2 +- src/components/popupStickers.ts | 2 +- src/components/preloader.ts | 2 +- src/components/ripple.ts | 2 +- src/components/sidebarLeft/index.ts | 3 +- .../sidebarLeft/tabs/chatFolders.ts | 2 +- src/components/sidebarLeft/tabs/editFolder.ts | 2 +- .../sidebarLeft/tabs/includedChats.ts | 2 +- src/components/sidebarRight/tabs/forward.ts | 2 +- src/components/sidebarRight/tabs/gifs.ts | 2 +- .../sidebarRight/tabs/sharedMedia.ts | 18 +- src/components/sidebarRight/tabs/stickers.ts | 2 +- src/components/wrappers.ts | 14 +- src/emoji/index.ts | 5 + src/helpers/array.ts | 16 + src/helpers/date.ts | 7 +- src/helpers/dom.ts | 332 ++++++++++ src/helpers/fileName.ts | 13 +- src/helpers/number.ts | 34 ++ src/helpers/object.ts | 94 +++ src/helpers/string.ts | 77 +++ src/lib/appManagers/apiUpdatesManager.ts | 5 +- src/lib/appManagers/appChatsManager.ts | 3 +- src/lib/appManagers/appDialogsManager.ts | 5 +- src/lib/appManagers/appDocsManager.ts | 5 +- src/lib/appManagers/appImManager.ts | 50 +- src/lib/appManagers/appMessagesManager.ts | 5 +- src/lib/appManagers/appPeersManager.ts | 2 +- src/lib/appManagers/appPhotosManager.ts | 3 +- src/lib/appManagers/appPollsManager.ts | 2 +- src/lib/appManagers/appUsersManager.ts | 3 +- src/lib/appManagers/appWebPagesManager.ts | 3 +- src/lib/mediaPlayer.ts | 2 +- src/lib/mtproto/mtprotoworker.ts | 2 +- src/lib/mtproto/referenceDatabase.ts | 2 +- src/lib/mtproto/serverTimeManager.ts | 2 +- src/lib/richtextprocessor.ts | 3 +- src/lib/utils.ts | 569 ------------------ src/pages/pagePassword.ts | 2 +- src/pages/pageSignIn.ts | 2 +- src/pages/pagesManager.ts | 2 +- src/scss/partials/_chatlist.scss | 2 - src/scss/style.scss | 6 + 66 files changed, 834 insertions(+), 678 deletions(-) create mode 100644 src/components/middleEllipsis.ts create mode 100644 src/helpers/array.ts create mode 100644 src/helpers/dom.ts create mode 100644 src/helpers/number.ts create mode 100644 src/helpers/object.ts create mode 100644 src/helpers/string.ts delete mode 100644 src/lib/utils.ts diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index f50b27e3..982dde83 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -1,4 +1,4 @@ -import { isInDOM } from "../lib/utils"; +import { isInDOM } from "../helpers/dom"; import { RLottiePlayer } from "../lib/lottieLoader"; import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; import $rootScope from "../lib/rootScope"; diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 4bb09467..d44ba09f 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -11,7 +11,7 @@ import { logger } from "../lib/logger"; import VideoPlayer from "../lib/mediaPlayer"; import { RichTextProcessor } from "../lib/richtextprocessor"; import $rootScope from "../lib/rootScope"; -import { cancelEvent, fillPropertyValue, findUpClassName, generatePathData } from "../lib/utils"; +import { cancelEvent, fillPropertyValue, findUpClassName, generatePathData } from "../helpers/dom"; import animationIntersector from "./animationIntersector"; import appMediaPlaybackController from "./appMediaPlaybackController"; import AvatarElement from "./avatar"; diff --git a/src/components/appSearch.ts b/src/components/appSearch.ts index da8e0ddd..a1c8b4b5 100644 --- a/src/components/appSearch.ts +++ b/src/components/appSearch.ts @@ -4,12 +4,12 @@ import appMessagesIDsManager from "../lib/appManagers/appMessagesIDsManager"; import appUsersManager from "../lib/appManagers/appUsersManager"; import appPeersManager from '../lib/appManagers/appPeersManager'; import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { escapeRegExp } from "../lib/utils"; import { formatPhoneNumber } from "./misc"; import appChatsManager from "../lib/appManagers/appChatsManager"; import SearchInput from "./searchInput"; import { Peer } from "../layer"; import $rootScope from "../lib/rootScope"; +import { escapeRegExp } from "../helpers/string"; export class SearchGroup { container: HTMLDivElement; diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 151bbd67..fa032218 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -5,7 +5,7 @@ import appPeersManager from "../lib/appManagers/appPeersManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager"; import appUsersManager from "../lib/appManagers/appUsersManager"; import $rootScope from "../lib/rootScope"; -import { cancelEvent, findUpAttribute, findUpClassName } from "../lib/utils"; +import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom"; import Scrollable from "./scrollable"; type PeerType = 'contacts' | 'dialogs'; diff --git a/src/components/audio.ts b/src/components/audio.ts index 05628922..ddb5cd45 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -10,6 +10,7 @@ import mediaSizes from "../helpers/mediaSizes"; import { isSafari } from "../helpers/userAgent"; import appMessagesManager from "../lib/appManagers/appMessagesManager"; import $rootScope from "../lib/rootScope"; +import './middleEllipsis'; $rootScope.$on('messages_media_read', e => { const mids = e.detail; @@ -252,7 +253,7 @@ function wrapAudio(doc: MyDocument, audioEl: AudioElement) { const html = `
-
${title}
+
${title}
${subtitle}
`; diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 332b7ea3..610063f6 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -1,7 +1,7 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appProfileManager from "../lib/appManagers/appProfileManager"; import $rootScope from "../lib/rootScope"; -import { cancelEvent } from "../lib/utils"; +import { cancelEvent } from "../helpers/dom"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; $rootScope.$on('avatar_update', (e) => { diff --git a/src/components/bubbleGroups.ts b/src/components/bubbleGroups.ts index e5c98d72..f7a6742f 100644 --- a/src/components/bubbleGroups.ts +++ b/src/components/bubbleGroups.ts @@ -1,5 +1,5 @@ import $rootScope from "../lib/rootScope"; -import { generatePathData } from "../lib/utils"; +import { generatePathData } from "../helpers/dom"; export default class BubbleGroups { bubblesByGroups: Array<{timestamp: number, fromID: number, mid: number, group: HTMLDivElement[]}> = []; // map to group diff --git a/src/components/chat/audio.ts b/src/components/chat/audio.ts index a27f6084..162f6bae 100644 --- a/src/components/chat/audio.ts +++ b/src/components/chat/audio.ts @@ -2,7 +2,7 @@ import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../lib/appManagers/appPeersManager"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import $rootScope from "../../lib/rootScope"; -import { cancelEvent } from "../../lib/utils"; +import { cancelEvent } from "../../helpers/dom"; import appMediaPlaybackController from "../appMediaPlaybackController"; import DivAndCaption from "../divAndCaption"; import { formatDate } from "../wrappers"; diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 0d8bf795..f59f3b3e 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -5,7 +5,7 @@ import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../lib/appManagers/appPeersManager"; import appPollsManager, { Poll } from "../../lib/appManagers/appPollsManager"; import $rootScope from "../../lib/rootScope"; -import { cancelEvent, cancelSelection, findUpClassName } from "../../lib/utils"; +import { cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc"; import PopupDeleteMessages from "../popupDeleteMessages"; diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index baafa896..7d722bee 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import $rootScope from '../../lib/rootScope'; -import { cancelEvent, findUpClassName, getRichValue } from "../../lib/utils"; +import { cancelEvent, findUpClassName, getRichValue } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popupCreatePoll"; diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index ce2e1235..89ae47be 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -1,6 +1,6 @@ import { getFullDate } from "../../helpers/date"; +import { formatNumber } from "../../helpers/number"; import RichTextProcessor from "../../lib/richtextprocessor"; -import { formatNumber } from "../../lib/utils"; type Message = any; diff --git a/src/components/chat/pinnedContainer.ts b/src/components/chat/pinnedContainer.ts index b0409711..47715b4f 100644 --- a/src/components/chat/pinnedContainer.ts +++ b/src/components/chat/pinnedContainer.ts @@ -1,6 +1,6 @@ import mediaSizes from "../../helpers/mediaSizes"; import appImManager from "../../lib/appManagers/appImManager"; -import { cancelEvent } from "../../lib/utils"; +import { cancelEvent } from "../../helpers/dom"; import DivAndCaption from "../divAndCaption"; import { ripple } from "../ripple"; diff --git a/src/components/chat/search.ts b/src/components/chat/search.ts index 8411d7e7..23e421c6 100644 --- a/src/components/chat/search.ts +++ b/src/components/chat/search.ts @@ -1,6 +1,6 @@ import appImManager from "../../lib/appManagers/appImManager"; import $rootScope from "../../lib/rootScope"; -import { cancelEvent, whichChild, findUpTag } from "../../lib/utils"; +import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom"; import AppSearch, { SearchGroup } from "../appSearch"; import PopupDatePicker from "../popupDatepicker"; import { ripple } from "../ripple"; diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index b85463c1..7cf39957 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -1,7 +1,7 @@ import { isTouchSupported } from "../../helpers/touchSupport"; import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; -import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../lib/utils"; +import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; import CheckboxField from "../checkbox"; diff --git a/src/components/dialogsContextMenu.ts b/src/components/dialogsContextMenu.ts index f6569b88..6dd05170 100644 --- a/src/components/dialogsContextMenu.ts +++ b/src/components/dialogsContextMenu.ts @@ -4,7 +4,7 @@ import appImManager from "../lib/appManagers/appImManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appPeersManager from "../lib/appManagers/appPeersManager"; import $rootScope from "../lib/rootScope"; -import { findUpTag } from "../lib/utils"; +import { findUpTag } from "../helpers/dom"; import { parseMenuButtonsTo, positionMenu, openBtnMenu } from "./misc"; import { PopupButton } from "./popup"; import PopupPeer from "./popupPeer"; diff --git a/src/components/emoticonsDropdown/index.ts b/src/components/emoticonsDropdown/index.ts index 5f370a35..3abcf635 100644 --- a/src/components/emoticonsDropdown/index.ts +++ b/src/components/emoticonsDropdown/index.ts @@ -3,7 +3,7 @@ import appChatsManager from "../../lib/appManagers/appChatsManager"; import appImManager from "../../lib/appManagers/appImManager"; import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; import $rootScope from "../../lib/rootScope"; -import { findUpClassName, findUpTag, whichChild } from "../../lib/utils"; +import { findUpClassName, findUpTag, whichChild } from "../../helpers/dom"; import animationIntersector from "../animationIntersector"; import { horizontalMenu } from "../horizontalMenu"; import LazyLoadQueue, { LazyLoadQueueIntersector } from "../lazyLoadQueue"; diff --git a/src/components/gifsMasonry.ts b/src/components/gifsMasonry.ts index 2524fdf4..c8b5d9ec 100644 --- a/src/components/gifsMasonry.ts +++ b/src/components/gifsMasonry.ts @@ -1,4 +1,4 @@ -import { calcImageInBox } from "../lib/utils"; +import { calcImageInBox } from "../helpers/dom"; import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import { wrapVideo } from "./wrappers"; import { renderImageFromUrl } from "./misc"; diff --git a/src/components/groupedLayout.ts b/src/components/groupedLayout.ts index 192be05d..29988835 100644 --- a/src/components/groupedLayout.ts +++ b/src/components/groupedLayout.ts @@ -5,6 +5,9 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ +import { accumulate } from "../helpers/array"; +import { clamp } from "../helpers/number"; + type Size = {w: number, h: number}; export type GroupMediaLayout = { geometry: { @@ -27,13 +30,6 @@ export const RectPart = { Left: 8 }; -let accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); - -// https://github.com/telegramdesktop/tdesktop/blob/74d848311b31ef0eb6d2c43a4d30ade8f1d2d9fb/Telegram/SourceFiles/core/utils.h#L128 -function snap(v: T, _min: T, _max: T): T { - return (v < _min) ? _min : ((v > _max) ? _max : v); -} - // https://github.com/telegramdesktop/tdesktop/blob/4669c07dc5335cbf4795bbbe5b0ab7c007b9aee2/Telegram/SourceFiles/ui/grouped_layout.cpp export class Layouter { private count: number; @@ -324,8 +320,8 @@ class ComplexLayouter { const kMinRatio = 0.6667; return ratios.map(ratio => { return averageRatio > 1.1 - ? snap(ratio, 1., kMaxRatio) - : snap(ratio, kMinRatio, 1.); + ? clamp(ratio, 1., kMaxRatio) + : clamp(ratio, kMinRatio, 1.); }); } diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index e9845c1c..983dd018 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -1,4 +1,4 @@ -import { findUpTag, whichChild } from "../lib/utils"; +import { findUpTag, whichChild } from "../helpers/dom"; import Transition from "./transition"; export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement) => void, onTransitionEnd?: () => void, transitionTime = 250) { diff --git a/src/components/middleEllipsis.ts b/src/components/middleEllipsis.ts new file mode 100644 index 00000000..2a6afc9c --- /dev/null +++ b/src/components/middleEllipsis.ts @@ -0,0 +1,142 @@ +// Thanks to https://stackoverflow.com/a/49349813 +import { clamp } from "../helpers/number"; + +/** + * Attibute modifier to create middle ellipsis + * When the attribute value is left blank the ellipsis will be in the middle + * When positive the attribute value will be used as a percentage + * When negative the attribute value will be used as character index counted from the end + * @example + *
A Javascript solution to middle ellipsis
+ *
A Javascript solution to middle ellipsis
+ *
A Javascript solution to middle ellipsis
+ */ +const attributeName = 'data-middle-ellipsis'; +const ellipsis = '…'; +const map: Map = new Map(); + +const testQueue: Set = new Set(); +const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif'; +const fontSize = '16px'; +let timeoutId: number; + +const setTestQueue = () => { + cancelAnimationFrame(timeoutId); + timeoutId = window.requestAnimationFrame(testQueueElements); +}; + +const testQueueElements = () => { + testQueue.forEach(testElement); + testQueue.clear(); +}; + +window.addEventListener('resize', () => { + (Array.from(document.querySelectorAll(`[${attributeName}]`)) as HTMLElement[]).forEach(el => testQueue.add(el)); + setTestQueue(); +}, {capture: true, passive: true}); + +const testElement = (elm: HTMLElement) => { + //const perf = performance.now(); + // do not recalculate variables a second time + const mapped = map.get(elm); + let {text, textLength, from, multiplier, font, textWidth, elementWidth} = mapped || {}; + // first time + if(!mapped) { + text = elm.textContent; + textLength = text.length; + from = parseFloat(elm.getAttribute(attributeName)) || 50; + multiplier = from > 0 && from / 100; + + //const perf = performance.now(); + font = `${elm.dataset.fontWeight || 400} ${fontSize} ${fontFamily}`; + /* const computedStyle = window.getComputedStyle(elm, null); + font = `${computedStyle.getPropertyValue('font-weight')} ${computedStyle.getPropertyValue('font-size')} ${computedStyle.getPropertyValue('font-family')}`; */ + //console.log('testMiddleEllipsis get computed style:', performance.now() - perf, font); + + textWidth = getTextWidth(text, font); + //const perf = performance.now(); + elementWidth = elm.offsetWidth; + //console.log('testMiddleEllipsis get offsetWidth:', performance.now() - perf, font); + map.set(elm, {text, textLength, from, multiplier, font, textWidth, elementWidth}); + } + + const {offsetWidth} = elm; + const widthChanged = !mapped || elementWidth !== offsetWidth; + mapped && widthChanged && (mapped.elementWidth = elementWidth = offsetWidth); + + if(widthChanged) { + if(textWidth > elementWidth) { + elm.setAttribute('title', text); + let smallerText = text; + let smallerWidth = elementWidth; + while(smallerText.length > 3) { + let smallerTextLength = smallerText.length; + const half = multiplier && + clamp(multiplier * smallerTextLength << 0, 1, smallerTextLength - 2) || + Math.max(smallerTextLength + from - 1, 1); + const half1 = smallerText.substr(0, half).replace(/\s*$/,''); + const half2 = smallerText.substr(half + 1).replace(/^\s*/,''); + smallerText = half1 + half2; + smallerWidth = getTextWidth(smallerText + ellipsis, font); + if(smallerWidth < elementWidth) { + elm.textContent = half1 + ellipsis + half2; + break; + } + } + } else { + elm.removeAttribute('title'); + } + } + + //console.log('testMiddleEllipsis for element:', elm, performance.now() - perf); +}; + +let context: CanvasRenderingContext2D; +/** + * Get the text width + * @param {string} text + * @param {string} font + */ +function getTextWidth(text: string, font: string) { + //const perf = performance.now(); + if(!context) { + const canvas = document.createElement('canvas'); + context = canvas.getContext('2d'); + context.font = font; + } + + //context.font = font; + const metrics = context.measureText(text); + //console.log('getTextWidth perf:', performance.now() - perf); + return metrics.width; +} + +export class MiddleEllipsisElement extends HTMLElement { + constructor() { + super(); + + if(this.getAttribute('data-middle-ellipsis') === null) { + this.setAttribute('data-middle-ellipsis', ''); + } + } + + connectedCallback() { + testQueue.add(this); + setTestQueue(); + //testElement(this); + } + + disconnectedCallback() { + map.delete(this); + } +} + +customElements.define("middle-ellipsis-element", MiddleEllipsisElement); diff --git a/src/components/poll.ts b/src/components/poll.ts index 8ca788cd..023daf80 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -5,7 +5,7 @@ import appPollsManager, { Poll, PollResults } from "../lib/appManagers/appPollsM import serverTimeManager from "../lib/mtproto/serverTimeManager"; import { RichTextProcessor } from "../lib/richtextprocessor"; import $rootScope from "../lib/rootScope"; -import { cancelEvent, findUpClassName } from "../lib/utils"; +import { cancelEvent, findUpClassName } from "../helpers/dom"; import { ripple } from "./ripple"; import appSidebarRight from "./sidebarRight"; diff --git a/src/components/popup.ts b/src/components/popup.ts index 1ccd6cc0..683ae366 100644 --- a/src/components/popup.ts +++ b/src/components/popup.ts @@ -1,5 +1,5 @@ import $rootScope from "../lib/rootScope"; -import { cancelEvent, findUpClassName } from "../lib/utils"; +import { cancelEvent, findUpClassName } from "../helpers/dom"; import { ripple } from "./ripple"; export class PopupElement { diff --git a/src/components/popupCreatePoll.ts b/src/components/popupCreatePoll.ts index 7176b730..0afefbcc 100644 --- a/src/components/popupCreatePoll.ts +++ b/src/components/popupCreatePoll.ts @@ -2,7 +2,7 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appPeersManager from "../lib/appManagers/appPeersManager"; import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; import $rootScope from "../lib/rootScope"; -import { findUpTag, whichChild } from "../lib/utils"; +import { findUpTag, whichChild } from "../helpers/dom"; import CheckboxField from "./checkbox"; import InputField from "./inputField"; import { PopupElement } from "./popup"; diff --git a/src/components/popupNewMedia.ts b/src/components/popupNewMedia.ts index 3185607a..4282a782 100644 --- a/src/components/popupNewMedia.ts +++ b/src/components/popupNewMedia.ts @@ -1,7 +1,7 @@ import { isTouchSupported } from "../helpers/touchSupport"; import appImManager from "../lib/appManagers/appImManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { calcImageInBox } from "../lib/utils"; +import { calcImageInBox } from "../helpers/dom"; import { Layouter, RectPart } from "./groupedLayout"; import InputField from "./inputField"; import { PopupElement } from "./popup"; diff --git a/src/components/popupStickers.ts b/src/components/popupStickers.ts index 4de24c4f..ba98c529 100644 --- a/src/components/popupStickers.ts +++ b/src/components/popupStickers.ts @@ -6,7 +6,7 @@ import { wrapSticker } from "./wrappers"; import LazyLoadQueue from "./lazyLoadQueue"; import { putPreloader } from "./misc"; import animationIntersector from "./animationIntersector"; -import { findUpClassName } from "../lib/utils"; +import { findUpClassName } from "../helpers/dom"; import appImManager from "../lib/appManagers/appImManager"; import { StickerSet } from "../layer"; import mediaSizes from "../helpers/mediaSizes"; diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 6bc8e3b8..c2e3a26f 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -1,4 +1,4 @@ -import { isInDOM, cancelEvent } from "../lib/utils"; +import { isInDOM, cancelEvent } from "../helpers/dom"; import { CancellablePromise } from "../helpers/cancellablePromise"; export default class ProgressivePreloader { diff --git a/src/components/ripple.ts b/src/components/ripple.ts index 58e9cc8a..72529451 100644 --- a/src/components/ripple.ts +++ b/src/components/ripple.ts @@ -1,5 +1,5 @@ import {isTouchSupported} from "../helpers/touchSupport"; -import { findUpClassName } from "../lib/utils"; +import { findUpClassName } from "../helpers/dom"; let rippleClickID = 0; export function ripple(elem: HTMLElement, callback: (id: number) => Promise = () => Promise.resolve(), onEnd: (id: number) => void = null) { diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 731e2bbc..5ab26055 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -1,4 +1,5 @@ //import { logger } from "../polyfill"; +import { formatNumber } from "../../helpers/number"; import appChatsManager from "../../lib/appManagers/appChatsManager"; import appDialogsManager from "../../lib/appManagers/appDialogsManager"; import appImManager from "../../lib/appManagers/appImManager"; @@ -7,7 +8,7 @@ import appStateManager from "../../lib/appManagers/appStateManager"; import appUsersManager from "../../lib/appManagers/appUsersManager"; import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; import $rootScope from "../../lib/rootScope"; -import { findUpClassName, findUpTag, formatNumber } from "../../lib/utils"; +import { findUpClassName, findUpTag } from "../../helpers/dom"; import AppSearch, { SearchGroup } from "../appSearch"; import AvatarElement from "../avatar"; import { parseMenuButtonsTo } from "../misc"; diff --git a/src/components/sidebarLeft/tabs/chatFolders.ts b/src/components/sidebarLeft/tabs/chatFolders.ts index 905b32e5..d50ce129 100644 --- a/src/components/sidebarLeft/tabs/chatFolders.ts +++ b/src/components/sidebarLeft/tabs/chatFolders.ts @@ -4,7 +4,7 @@ import apiManager from "../../../lib/mtproto/mtprotoworker"; import appMessagesManager, { MyDialogFilter } from "../../../lib/appManagers/appMessagesManager"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import appPeersManager from "../../../lib/appManagers/appPeersManager"; -import { cancelEvent } from "../../../lib/utils"; +import { cancelEvent } from "../../../helpers/dom"; import appSidebarLeft from ".."; import { ripple } from "../../ripple"; import { toast } from "../../toast"; diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index 6a5135c7..15dc7212 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -1,8 +1,8 @@ import appSidebarLeft, { AppSidebarLeft } from ".."; +import { deepEqual, copy } from "../../../helpers/object"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appMessagesManager, { MyDialogFilter as DialogFilter } from "../../../lib/appManagers/appMessagesManager"; import lottieLoader, { RLottiePlayer } from "../../../lib/lottieLoader"; -import { copy, deepEqual } from "../../../lib/utils"; import { parseMenuButtonsTo } from "../../misc"; import { ripple } from "../../ripple"; import { SliderTab } from "../../slider"; diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index 4ad92098..928ab643 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -4,9 +4,9 @@ import appSidebarLeft, { AppSidebarLeft } from ".."; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appPeersManager from "../../../lib/appManagers/appPeersManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; -import { copy } from "../../../lib/utils"; import { MyDialogFilter as DialogFilter } from "../../../lib/appManagers/appMessagesManager"; import $rootScope from "../../../lib/rootScope"; +import { copy } from "../../../helpers/object"; export default class AppIncludedChatsTab implements SliderTab { public container: HTMLElement; diff --git a/src/components/sidebarRight/tabs/forward.ts b/src/components/sidebarRight/tabs/forward.ts index 2bbbe58a..e67413a6 100644 --- a/src/components/sidebarRight/tabs/forward.ts +++ b/src/components/sidebarRight/tabs/forward.ts @@ -1,4 +1,4 @@ -import appSidebarRight, { AppSidebarRight } from ".."; +import appSidebarRight from ".."; import appMessagesManager from "../../../lib/appManagers/appMessagesManager"; import AppSelectPeers from "../../appSelectPeers"; import { putPreloader } from "../../misc"; diff --git a/src/components/sidebarRight/tabs/gifs.ts b/src/components/sidebarRight/tabs/gifs.ts index 0e98cead..194c8830 100644 --- a/src/components/sidebarRight/tabs/gifs.ts +++ b/src/components/sidebarRight/tabs/gifs.ts @@ -6,7 +6,7 @@ import appSidebarRight, { AppSidebarRight } from ".."; import appUsersManager from "../../../lib/appManagers/appUsersManager"; import appInlineBotsManager, { AppInlineBotsManager } from "../../../lib/appManagers/AppInlineBotsManager"; import GifsMasonry from "../../gifsMasonry"; -import { findUpClassName } from "../../../lib/utils"; +import { findUpClassName } from "../../../helpers/dom"; import appImManager from "../../../lib/appManagers/appImManager"; import type { MyDocument } from "../../../lib/appManagers/appDocsManager"; import mediaSizes from "../../../helpers/mediaSizes"; diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 49e4645a..f84ea172 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -1,3 +1,4 @@ +import { limitSymbols } from "../../../helpers/string"; import appImManager from "../../../lib/appManagers/appImManager"; import appMessagesManager from "../../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../../lib/appManagers/appPeersManager"; @@ -7,7 +8,6 @@ import appUsersManager from "../../../lib/appManagers/appUsersManager"; import { logger } from "../../../lib/logger"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import $rootScope from "../../../lib/rootScope"; -import { limitSymbols } from "../../../lib/utils"; import AppMediaViewer from "../../appMediaViewer"; import AvatarElement from "../../avatar"; import { horizontalMenu } from "../../horizontalMenu"; @@ -478,9 +478,10 @@ export default class AppSharedMediaTab implements SliderTab { break; } + case 'inputMessagesFilterMusic': case 'inputMessagesFilterDocument': { - for(let message of messages) { - let div = wrapDocument(message.media.document, true, false, message.mid); + for(const message of messages) { + const div = wrapDocument(message.media.document, true, false, message.mid, 400); div.dataset.mid = '' + message.mid; elemsToAppend.push(div); } @@ -592,16 +593,7 @@ export default class AppSharedMediaTab implements SliderTab { break; } - - case 'inputMessagesFilterMusic': { - for(let message of messages) { - let div = wrapAudio(message.media.document, true, message.mid); - div.dataset.mid = '' + message.mid; - elemsToAppend.push(div); - } - break; - } - + default: //console.warn('death is my friend', messages); break; diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index 0fd3b184..e3393dce 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -2,7 +2,7 @@ import { SliderTab } from "../../slider"; import SearchInput from "../../searchInput"; import Scrollable from "../../scrollable"; import LazyLoadQueue from "../../lazyLoadQueue"; -import { findUpClassName } from "../../../lib/utils"; +import { findUpClassName } from "../../../helpers/dom"; import appImManager from "../../../lib/appManagers/appImManager"; import appStickersManager from "../../../lib/appManagers/appStickersManager"; import PopupStickers from "../../popupStickers"; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index b4b9c1a3..81da62d1 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1,7 +1,9 @@ +import { getEmojiToneIndex } from '../emoji'; import { readBlobAsText } from '../helpers/blob'; import { deferredPromise } from '../helpers/cancellablePromise'; import { months } from '../helpers/date'; import mediaSizes from '../helpers/mediaSizes'; +import { formatBytes } from '../helpers/number'; import { isAppleMobile, isSafari } from '../helpers/userAgent'; import { PhotoSize } from '../layer'; import appDocsManager, { MyDocument } from "../lib/appManagers/appDocsManager"; @@ -10,7 +12,7 @@ import appMessagesManager from '../lib/appManagers/appMessagesManager'; import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; import LottieLoader from '../lib/lottieLoader'; import VideoPlayer from '../lib/mediaPlayer'; -import { cancelEvent, formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils"; +import { isInDOM } from "../helpers/dom"; import webpWorkerController from '../lib/webp/webpWorkerController'; import animationIntersector from './animationIntersector'; import appMediaPlaybackController from './appMediaPlaybackController'; @@ -21,6 +23,7 @@ import LazyLoadQueue from './lazyLoadQueue'; import { renderImageFromUrl } from './misc'; import PollElement from './poll'; import ProgressivePreloader from './preloader'; +import './middleEllipsis'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB @@ -316,9 +319,11 @@ export const formatDate = (timestamp: number, monthShort = false, withYear = tru return str + ' at ' + date.getHours() + ':' + ('0' + date.getMinutes()).slice(-2); }; -export function wrapDocument(doc: MyDocument, withTime = false, uploading = false, mid?: number): HTMLElement { +export function wrapDocument(doc: MyDocument, withTime = false, uploading = false, mid?: number, fontWeight = 500): HTMLElement { if(doc.type == 'audio' || doc.type == 'voice') { - return wrapAudio(doc, withTime, mid); + const audioElement = wrapAudio(doc, withTime, mid); + audioElement.dataset.fontWeight = '' + fontWeight; + return audioElement; } let extSplitted = doc.file_name ? doc.file_name.split('.') : ''; @@ -347,6 +352,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals icoDiv.innerText = ext; } + //let fileName = stringMiddleOverflow(doc.file_name || 'Unknown.file', 26); let fileName = doc.file_name || 'Unknown.file'; let size = formatBytes(doc.size); @@ -356,7 +362,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals docDiv.innerHTML = ` ${!uploading ? `
` : ''} -
${fileName}
+
${fileName}
${size}
`; diff --git a/src/emoji/index.ts b/src/emoji/index.ts index f480ce3c..5844a502 100644 --- a/src/emoji/index.ts +++ b/src/emoji/index.ts @@ -33,4 +33,9 @@ export function toCodePoints(unicodeSurrogates: string): Array { } return points; +} + +export function getEmojiToneIndex(input: string) { + let match = input.match(/[\uDFFB-\uDFFF]/); + return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0; } \ No newline at end of file diff --git a/src/helpers/array.ts b/src/helpers/array.ts new file mode 100644 index 00000000..def55103 --- /dev/null +++ b/src/helpers/array.ts @@ -0,0 +1,16 @@ +import { copy } from "./object"; + +export function listMergeSorted(list1: any[] = [], list2: any[] = []) { + const result = copy(list1); + + const minID = list1.length ? list1[list1.length - 1] : 0xFFFFFFFF; + for(let i = 0; i < list2.length; i++) { + if(list2[i] < minID) { + result.push(list2[i]); + } + } + + return result; +} + +export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); \ No newline at end of file diff --git a/src/helpers/date.ts b/src/helpers/date.ts index 2b78b9f9..379d3369 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -34,4 +34,9 @@ export const formatDateAccordingToToday = (time: Date) => { export const getFullDate = (date: Date) => { return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear() + ', ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2); -}; \ No newline at end of file +}; + +export function tsNow(seconds?: true) { + const t = Date.now(); + return seconds ? Math.floor(t / 1000) : t; +} \ No newline at end of file diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts new file mode 100644 index 00000000..de86a929 --- /dev/null +++ b/src/helpers/dom.ts @@ -0,0 +1,332 @@ +/* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { + if(!element) { + return false; + } + + parentNode = parentNode || document.body; + if(element == parentNode) { + return true; + } + return isInDOM(element.parentNode as HTMLElement, parentNode); +} */ +export function isInDOM(element: Element): boolean { + return element?.isConnected; +} + +/* export function checkDragEvent(e: any) { + if(!e || e.target && (e.target.tagName == 'IMG' || e.target.tagName == 'A')) return false + if(e.dataTransfer && e.dataTransfer.types) { + for(var i = 0; i < e.dataTransfer.types.length; i++) { + if(e.dataTransfer.types[i] == 'Files') { + return true; + } + } + } else { + return true; + } + + return false; +} */ + +export function cancelEvent(event: Event) { + event = event || window.event; + if(event) { + // @ts-ignore + event = event.originalEvent || event; + + try { + if(event.stopPropagation) event.stopPropagation(); + if(event.preventDefault) event.preventDefault(); + event.returnValue = false; + event.cancelBubble = true; + } catch(err) {} + } + + return false; +} + +export function getRichValue(field: any) { + if(!field) { + return ''; + } + var lines: string[] = []; + var line: string[] = []; + + getRichElementValue(field, lines, line); + if (line.length) { + lines.push(line.join('')); + } + + var value = lines.join('\n'); + value = value.replace(/\u00A0/g, ' '); + + return value; +} + +export function placeCaretAtEnd(el: HTMLElement) { + el.focus(); + if(typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { + var range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + // @ts-ignore + } else if(typeof document.body.createTextRange != "undefined") { + // @ts-ignore + var textRange = document.body.createTextRange(); + textRange.moveToElementText(el); + textRange.collapse(false); + textRange.select(); + } +} + +export function getRichElementValue(node: any, lines: string[], line: string[], selNode?: Node, selOffset?: number) { + if(node.nodeType == 3) { // TEXT + if(selNode === node) { + var value = node.nodeValue + line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)) + } else { + line.push(node.nodeValue) + } + return + } + if (node.nodeType != 1) { // NON-ELEMENT + return + } + var isSelected = (selNode === node) + var isBlock = node.tagName == 'DIV' || node.tagName == 'P' + var curChild + if(isBlock && line.length || node.tagName == 'BR') { + lines.push(line.join('')) + line.splice(0, line.length) + } else if(node.tagName == 'IMG') { + if(node.alt) { + line.push(node.alt); + } + } + + if(isSelected && !selOffset) { + line.push('\x01'); + } + + var curChild = node.firstChild; + while(curChild) { + getRichElementValue(curChild, lines, line, selNode, selOffset); + curChild = curChild.nextSibling; + } + + if(isSelected && selOffset) { + line.push('\x01'); + } + + if(isBlock && line.length) { + lines.push(line.join('')); + line.splice(0, line.length); + } +} + +/* if (Config.Modes.animations && + typeof window.requestAnimationFrame == 'function') { + window.onAnimationFrameCallback = function (cb) { + return (function () { + window.requestAnimationFrame(cb) + }) + } +} else { + window.onAnimationFrameCallback = function (cb) { + return cb + } +} */ + +// generate a path's arc data parameter +// http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands +var arcParameter = function(rx: number, ry: number, xAxisRotation: number, largeArcFlag: number, sweepFlag: number, x: number, y: number) { + return [rx, ',', ry, ' ', + xAxisRotation, ' ', + largeArcFlag, ',', + sweepFlag, ' ', + x, ',', y ].join(''); +}; + +export function generatePathData(x: number, y: number, width: number, height: number, tl: number, tr: number, br: number, bl: number) { + const data: string[] = []; + + // start point in top-middle of the rectangle + data.push('M' + (x + width / 2) + ',' + y); + + // next we go to the right + data.push('H' + (x + width - tr)); + + if(tr > 0) { + // now we draw the arc in the top-right corner + data.push('A' + arcParameter(tr, tr, 0, 0, 1, (x + width), (y + tr))); + } + + // next we go down + data.push('V' + (y + height - br)); + + if(br > 0) { + // now we draw the arc in the lower-right corner + data.push('A' + arcParameter(br, br, 0, 0, 1, (x + width - br), (y + height))); + } + + // now we go to the left + data.push('H' + (x + bl)); + + if(bl > 0) { + // now we draw the arc in the lower-left corner + data.push('A' + arcParameter(bl, bl, 0, 0, 1, (x + 0), (y + height - bl))); + } + + // next we go up + data.push('V' + (y + tl)); + + if(tl > 0) { + // now we draw the arc in the top-left corner + data.push('A' + arcParameter(tl, tl, 0, 0, 1, (x + tl), (y + 0))); + } + + // and we close the path + data.push('Z'); + + return data.join(' '); +}; + +//export function findUpClassName(el: any, className: string): T; +export function findUpClassName(el: any, className: string): HTMLElement { + return el.closest('.' + className); + /* if(el.classList.contains(className)) return el; // 03.02.2020 + + while(el.parentElement) { + el = el.parentElement; + if(el.classList.contains(className)) + return el; + } + return null; */ +} + +export function findUpTag(el: any, tag: string): HTMLElement { + return el.closest(tag); + /* if(el.tagName == tag) return el; // 03.02.2020 + + while(el.parentElement) { + el = el.parentElement; + if(el.tagName === tag) + return el; + } + return null; */ +} + +export function findUpAttribute(el: any, attribute: string): HTMLElement { + return el.closest(`[${attribute}]`); + /* if(el.getAttribute(attribute) != null) return el; // 03.02.2020 + + while(el.parentElement) { + el = el.parentElement; + if(el.getAttribute(attribute) != null) + return el; + } + return null; */ +} + +export function whichChild(elem: Node) { + if(!elem.parentNode) { + return -1; + } + + let i = 0; + // @ts-ignore + while((elem = elem.previousElementSibling) != null) ++i; + return i; +}; + +export function fillPropertyValue(str: string) { + let splitted = str.split(' '); + if(splitted.length != 4) { + if(!splitted[0]) splitted[0] = '0px'; + for(let i = splitted.length; i < 4; ++i) { + splitted[i] = splitted[i % 2] || splitted[0] || '0px'; + } + } + + return splitted; +} + +export function calcImageInBox(imageW: number, imageH: number, boxW: number, boxH: number, noZoom?: boolean) { + if(imageW < boxW && imageH < boxH) { + return {w: imageW, h: imageH}; + } + + var boxedImageW = boxW; + var boxedImageH = boxH; + + if((imageW / imageH) > (boxW / boxH)) { + boxedImageH = (imageH * boxW / imageW) | 0; + } else { + boxedImageW = (imageW * boxH / imageH) | 0; + if(boxedImageW > boxW) { + boxedImageH = (boxedImageH * boxW / boxedImageW) | 0; + boxedImageW = boxW; + } + } + + // if (Config.Navigator.retina) { + // imageW = Math.floor(imageW / 2) + // imageH = Math.floor(imageH / 2) + // } + + if(noZoom && boxedImageW >= imageW && boxedImageH >= imageH) { + boxedImageW = imageW; + boxedImageH = imageH; + } + + return {w: boxedImageW, h: boxedImageH}; +} + +export function positionElementByIndex(element: HTMLElement, container: HTMLElement, pos: number) { + const prevPos = whichChild(element); + + if(prevPos == pos) { + return false; + } else if(prevPos != -1 && prevPos < pos) { // was higher + pos += 1; + } + + if(container.childElementCount > pos) { + container.insertBefore(element, container.children[pos]); + } else { + container.append(element); + } + + return true; +} + +export function cancelSelection() { + if(window.getSelection) { + if(window.getSelection().empty) { // Chrome + window.getSelection().empty(); + } else if(window.getSelection().removeAllRanges) { // Firefox + window.getSelection().removeAllRanges(); + } + // @ts-ignore + } else if(document.selection) { // IE? + // @ts-ignore + document.selection.empty(); + } +} + +//(window as any).splitStringByLength = splitStringByLength; + +export function getSelectedText(): string { + if(window.getSelection) { + return window.getSelection().toString(); + // @ts-ignore + } else if(document.selection) { + // @ts-ignore + return document.selection.createRange().text; + } + + return ''; +} diff --git a/src/helpers/fileName.ts b/src/helpers/fileName.ts index 3c1851c7..db29e57e 100644 --- a/src/helpers/fileName.ts +++ b/src/helpers/fileName.ts @@ -1,4 +1,5 @@ -import { InputFileLocation, FileLocation } from "../layer"; +import type { InputFileLocation, FileLocation } from "../layer"; +import type { DownloadOptions } from "../lib/mtproto/apiFileManager"; export function getFileNameByLocation(location: InputFileLocation | FileLocation, options?: Partial<{ fileName: string @@ -25,4 +26,14 @@ export function getFileNameByLocation(location: InputFileLocation | FileLocation return ''; } } +} + +export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download'; +export function getFileURL(type: FileURLType, options: DownloadOptions) { + //console.log('getFileURL', location); + //const perf = performance.now(); + const encoded = encodeURIComponent(JSON.stringify(options)); + //console.log('getFileURL encode:', performance.now() - perf, encoded); + + return '/' + type + '/' + encoded; } \ No newline at end of file diff --git a/src/helpers/number.ts b/src/helpers/number.ts new file mode 100644 index 00000000..a6419e19 --- /dev/null +++ b/src/helpers/number.ts @@ -0,0 +1,34 @@ +export function numberWithCommas(x: number) { + const parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); +} + +export function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +export function formatNumber(bytes: number, decimals = 2) { + if(bytes === 0) return '0'; + + const k = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['', 'K', 'M', 'B', 'T']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]; +} + +export function clamp(v: number, min: number, max: number): number { + return v < min ? min : ((v > max) ? max : v); +} + diff --git a/src/helpers/object.ts b/src/helpers/object.ts new file mode 100644 index 00000000..87ec0c19 --- /dev/null +++ b/src/helpers/object.ts @@ -0,0 +1,94 @@ +export function copy(obj: T): T { + //in case of premitives + if(obj === null || typeof(obj) !== "object") { + return obj; + } + + //date objects should be + if(obj instanceof Date) { + return new Date(obj.getTime()) as any; + } + + //handle Array + if(Array.isArray(obj)) { + const clonedArr: T = obj.map(el => copy(el)) as any as T; + return clonedArr; + } + + //lastly, handle objects + // @ts-ignore + let clonedObj = new obj.constructor(); + for(var prop in obj){ + if(obj.hasOwnProperty(prop)) { + clonedObj[prop] = copy(obj[prop]); + } + } + return clonedObj; +} + +export function deepEqual(x: any, y: any): boolean { + const ok = Object.keys, tx = typeof x, ty = typeof y; + return x && y && tx === 'object' && tx === ty ? ( + ok(x).length === ok(y).length && + ok(x).every(key => deepEqual(x[key], y[key])) + ) : (x === y); +} + +export function defineNotNumerableProperties(obj: {[key: string]: any}, names: string[]) { + //const perf = performance.now(); + const props = {writable: true, configurable: true}; + const out: {[name: string]: typeof props} = {}; + names.forEach(name => { + if(obj[name] === undefined) { + out[name] = props; + } + }); + Object.defineProperties(obj, out); + //console.log('defineNotNumerableProperties time:', performance.now() - perf); +} + +export function getObjectKeysAndSort(object: any, sort: 'asc' | 'desc' = 'asc') { + const ids = Object.keys(object).map(i => +i); + if(sort == 'asc') return ids.sort((a, b) => a - b); + else return ids.sort((a, b) => b - a); +} + +export function safeReplaceObject(wasObject: any, newObject: any) { + for(var key in wasObject) { + if(!newObject.hasOwnProperty(key) && key.charAt(0) != '$') { + delete wasObject[key]; + } + } + + for(var key in newObject) { + //if (newObject.hasOwnProperty(key)) { // useless + wasObject[key] = newObject[key]; + //} + } +} + +/** + * Will be used for FILE_REFERENCE_EXPIRED + * @param key + * @param wasObject + * @param newObject + */ +export function safeReplaceArrayInObject(key: K, wasObject: any, newObject: any) { + if('byteLength' in newObject[key]) { // Uint8Array + newObject[key] = [...newObject[key]]; + } + + if(wasObject && wasObject[key] != newObject[key]) { + wasObject[key].length = newObject[key].length; + (newObject[key] as any[]).forEach((v, i) => { + wasObject[key][i] = v; + }); + + /* wasObject[key].set(newObject[key]); */ + newObject[key] = wasObject[key]; + } +} + +export function isObject(object: any) { + return typeof(object) === 'object' && object !== null; +} \ No newline at end of file diff --git a/src/helpers/string.ts b/src/helpers/string.ts new file mode 100644 index 00000000..0a3dc926 --- /dev/null +++ b/src/helpers/string.ts @@ -0,0 +1,77 @@ +/* export function stringMiddleOverflow(str: string, maxLength: number) { + return str.length > maxLength ? str.slice(0, maxLength / 2 | 0) + '...' + str.slice(-Math.round(maxLength / 2)) : str; +} */ + +export function limitSymbols(str: string, length: number, limitFrom = length + 10) { + if(str.length > limitFrom) { + str = str.slice(0, length).replace(/(\n|\s)+$/, '') + '...'; + } + + return str; +} + +// credits to https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js +export function escapeRegExp(str: string) { + return str + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d'); +} + +export function encodeEntities(value: string) { + return value.replace(/&/g, '&').replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, (value) => { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }).replace(/([^\#-~| |!])/g, (value) => { // non-alphanumeric + return '&#' + value.charCodeAt(0) + ';'; + }).replace(//g, '>'); +} + +export function splitStringByLength(str: string, maxLength: number) { + if(str.length < maxLength) return [str]; + let length = 0, lastSliceStartIndex = 0, arrayIndex = 0; + const delimiter = ' ';//'\n'; + const out: string[] = []; + + const cut = (end?: number) => { + let part = str.slice(lastSliceStartIndex, end); + const _arrayIndex = arrayIndex++; + if(part.length > maxLength) { + let overflowPart = part.slice(maxLength); + const splitted = splitStringByLength(overflowPart, maxLength); + splitted.forEach(part => { + out[arrayIndex++] = part; + }); + + part = part.slice(0, maxLength); + } + + lastSliceStartIndex = end; + length = 0; + out[_arrayIndex] = (out[_arrayIndex] || '') + part; + }; + + let lastIndex = 0; + do { + let index = str.indexOf(delimiter, lastIndex); + if(index === -1) { + if(lastIndex != (str.length - 1)) { + cut(); + } + + break; + } + + index += delimiter.length; + + const partLength = index - lastIndex; + if((length + partLength) > maxLength) { + cut(length); + } + + lastIndex = index; + length += partLength; + } while(true); + + return out; +} \ No newline at end of file diff --git a/src/lib/appManagers/apiUpdatesManager.ts b/src/lib/appManagers/apiUpdatesManager.ts index 4b9458a1..5604812d 100644 --- a/src/lib/appManagers/apiUpdatesManager.ts +++ b/src/lib/appManagers/apiUpdatesManager.ts @@ -3,7 +3,6 @@ import { logger, LogLevels } from '../logger'; import apiManager from '../mtproto/mtprotoworker'; import $rootScope from '../rootScope'; //import networkerFactory from '../mtproto/networkerFactory'; -import { tsNow } from "../utils"; import appChatsManager from "./appChatsManager"; import appPeersManager from "./appPeersManager"; import appStateManager from './appStateManager'; @@ -400,7 +399,7 @@ export class ApiUpdatesManager { if(update._ == 'updateChannelTooLong') { if(!curState.lastPtsUpdateTime || - curState.lastPtsUpdateTime < tsNow() - 10000) { + curState.lastPtsUpdateTime < Date.now() - 10000) { // this.log.trace('channel too long, get diff', channelID, update) this.getChannelDifference(channelID); } @@ -461,7 +460,7 @@ export class ApiUpdatesManager { curState.pts = update.pts; popPts = true; - curState.lastPtsUpdateTime = tsNow(); + curState.lastPtsUpdateTime = Date.now(); } else if(update.pts_count) { // this.log.warn('Duplicate update', update) return false; diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index 56fea42d..1e02c19d 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -1,8 +1,9 @@ +import { numberWithCommas } from "../../helpers/number"; +import { isObject, safeReplaceObject, copy } from "../../helpers/object"; import { ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, Updates } from "../../layer"; import apiManager from '../mtproto/mtprotoworker'; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; -import { copy, isObject, numberWithCommas, safeReplaceObject } from "../utils"; import apiUpdatesManager from "./apiUpdatesManager"; import appMessagesManager from "./appMessagesManager"; import appProfileManager from "./appProfileManager"; diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 6e1a3a4b..373d1617 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1,4 +1,3 @@ -import { ResolutionUnitSpecifier } from "fast-png"; import AvatarElement from "../../components/avatar"; import DialogsContextMenu from "../../components/dialogsContextMenu"; import { horizontalMenu } from "../../components/horizontalMenu"; @@ -8,13 +7,13 @@ import { ripple } from "../../components/ripple"; import Scrollable, { ScrollableX } from "../../components/scrollable"; import appSidebarLeft from "../../components/sidebarLeft"; import { formatDateAccordingToToday } from "../../helpers/date"; +import { escapeRegExp } from "../../helpers/string"; import { isTouchSupported } from "../../helpers/touchSupport"; import { isSafari } from "../../helpers/userAgent"; import { logger, LogLevels } from "../logger"; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; -import { cancelEvent, escapeRegExp, findUpClassName, positionElementByIndex } from "../utils"; -import appChatsManager from "./appChatsManager"; +import { findUpClassName, positionElementByIndex } from "../../helpers/dom"; import appImManager, { AppImManager } from "./appImManager"; import appMessagesManager, { Dialog, MyDialogFilter as DialogFilter } from "./appMessagesManager"; import appPeersManager from './appPeersManager'; diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index f854c321..9dcf5896 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -1,10 +1,11 @@ -import { getFileNameByLocation } from '../../helpers/fileName'; +import { FileURLType, getFileNameByLocation, getFileURL } from '../../helpers/fileName'; +import { safeReplaceArrayInObject, defineNotNumerableProperties } from '../../helpers/object'; import { Document, InputFileLocation, PhotoSize } from '../../layer'; +import { isObject } from '../mtproto/bin_utils'; import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config'; import referenceDatabase, { ReferenceContext } from '../mtproto/referenceDatabase'; import opusDecodeController from '../opusDecodeController'; import { RichTextProcessor } from '../richtextprocessor'; -import { defineNotNumerableProperties, FileURLType, getFileURL, isObject, safeReplaceArrayInObject } from '../utils'; import appDownloadManager, { DownloadBlob } from './appDownloadManager'; import appPhotosManager from './appPhotosManager'; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index cf7aef0c..30e61ca7 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -29,6 +29,8 @@ import appSidebarRight, { AppSidebarRight, RIGHT_COLUMN_ACTIVE_CLASSNAME } from import StickyIntersector from '../../components/stickyIntersector'; import { wrapAlbum, wrapDocument, wrapPhoto, wrapPoll, wrapReply, wrapSticker, wrapVideo } from '../../components/wrappers'; import mediaSizes, { ScreenSize } from '../../helpers/mediaSizes'; +import { numberWithCommas } from '../../helpers/number'; +import { defineNotNumerableProperties, getObjectKeysAndSort } from '../../helpers/object'; import { isTouchSupported } from '../../helpers/touchSupport'; import { isAndroid, isApple, isSafari } from '../../helpers/userAgent'; import { InputNotifyPeer, InputPeerNotifySettings, NotifyPeer, Update } from '../../layer'; @@ -38,7 +40,7 @@ import apiManager from '../mtproto/mtprotoworker'; import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config'; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from '../rootScope'; -import { cancelEvent, defineNotNumerableProperties, findUpClassName, findUpTag, getObjectKeysAndSort, numberWithCommas, placeCaretAtEnd, whichChild } from "../utils"; +import { cancelEvent, findUpClassName, findUpTag, placeCaretAtEnd, whichChild } from "../../helpers/dom"; import apiUpdatesManager from './apiUpdatesManager'; import appChatsManager, { Channel, Chat } from "./appChatsManager"; import appDialogsManager from "./appDialogsManager"; @@ -327,7 +329,7 @@ export class AppImManager { appSidebarRight.sharedMediaTab.renderNewMessages(message.peerID, [mid]); - let bubble = this.bubbles[tempID]; + const bubble = this.bubbles[tempID]; if(bubble) { this.bubbles[mid] = bubble; @@ -335,8 +337,8 @@ export class AppImManager { // set new mids to album items for mediaViewer if(message.grouped_id) { - let items = bubble.querySelectorAll('.album-item'); - let groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); + const items = bubble.querySelectorAll('.album-item'); + const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); (Array.from(items) as HTMLElement[]).forEach((item, idx) => { item.dataset.mid = '' + groupIDs[idx]; }); @@ -412,15 +414,15 @@ export class AppImManager { }); */ this.needUpdate.forEachReverse((obj, idx) => { if(obj.replyMid == mid) { - let {mid, replyMid} = this.needUpdate.splice(idx, 1)[0]; + const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0]; //this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]); - let bubble = this.bubbles[mid]; + const bubble = this.bubbles[mid]; if(!bubble) return; - let message = appMessagesManager.getMessage(mid); + const message = appMessagesManager.getMessage(mid); - let repliedMessage = appMessagesManager.getMessage(replyMid); + const repliedMessage = appMessagesManager.getMessage(replyMid); if(repliedMessage.deleted) { // чтобы не пыталось бесконечно загрузить удалённое сообщение delete message.reply_to_mid; // WARNING! } @@ -439,7 +441,7 @@ export class AppImManager { }); $rootScope.$on('apiUpdate', (e) => { - let update = e.detail; + const update = e.detail; this.handleUpdate(update); }); @@ -489,8 +491,8 @@ export class AppImManager { return; } - for(let timestamp in this.dateMessages) { - let d = this.dateMessages[timestamp]; + for(const timestamp in this.dateMessages) { + const d = this.dateMessages[timestamp]; if(d.div == bubble) { new PopupDatePicker(new Date(+timestamp), this.onDatePick).show(); break; @@ -518,7 +520,7 @@ export class AppImManager { return; } - let contactDiv: HTMLElement = findUpClassName(target, 'contact'); + const contactDiv: HTMLElement = findUpClassName(target, 'contact'); if(contactDiv) { this.setPeer(+contactDiv.dataset.peerID); return; @@ -707,10 +709,10 @@ export class AppImManager { new ChatSearch(); }); - let onKeyDown = (e: KeyboardEvent) => { + const onKeyDown = (e: KeyboardEvent) => { if($rootScope.overlayIsActive) return; - let target = e.target as HTMLElement; + const target = e.target as HTMLElement; //if(target.tagName == 'INPUT') return; @@ -743,7 +745,7 @@ export class AppImManager { document.body.addEventListener('keydown', onKeyDown); this.goDownBtn.addEventListener('click', () => { - let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; + const dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; if(dialog) { this.setPeer(this.peerID/* , dialog.top_message */); @@ -761,8 +763,8 @@ export class AppImManager { //apiUpdatesManager.attach(); this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { - for(let timestamp in this.dateMessages) { - let dateMessage = this.dateMessages[timestamp]; + for(const timestamp in this.dateMessages) { + const dateMessage = this.dateMessages[timestamp]; if(dateMessage.container == target) { dateMessage.div.classList.toggle('is-sticky', stuck); break; @@ -775,12 +777,12 @@ export class AppImManager { return; } - let readed: number[] = []; + const readed: number[] = []; entries.forEach(entry => { if(entry.isIntersecting) { - let target = entry.target as HTMLElement; - let mid = +target.dataset.mid; + const target = entry.target as HTMLElement; + const mid = +target.dataset.mid; readed.push(mid); this.unreadedObserver.unobserve(target); this.unreaded.findAndSplice(id => id == mid); @@ -788,11 +790,11 @@ export class AppImManager { }); if(readed.length) { - let max = Math.max(...readed); + const max = Math.max(...readed); let length = readed.length; for(let i = this.unreaded.length - 1; i >= 0; --i) { - let mid = this.unreaded[i]; + const mid = this.unreaded[i]; if(mid < max) { length++; this.unreaded.splice(i, 1); @@ -1106,9 +1108,9 @@ export class AppImManager { } public getMountedBubble(mid: number) { - let message = appMessagesManager.getMessage(mid); + const message = appMessagesManager.getMessage(mid); - let bubble = this.bubbles[mid]; + const bubble = this.bubbles[mid]; if(!bubble && message.grouped_id) { const a = this.getAlbumBubble(message.grouped_id); if(a) return a; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index b74131cd..9f078c37 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -1,6 +1,10 @@ import ProgressivePreloader from "../../components/preloader"; +import { listMergeSorted } from "../../helpers/array"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; +import { tsNow } from "../../helpers/date"; +import { copy, defineNotNumerableProperties, deepEqual, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; +import { splitStringByLength, limitSymbols } from "../../helpers/string"; import { Dialog as MTDialog, DialogFilter, DialogPeer, DocumentAttribute, InputMessage, Message, MessageAction, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, PhotoSize, SendMessageAction, Update } from "../../layer"; import { InvokeApiOptions, Modify } from "../../types"; import { langPack } from "../langPack"; @@ -14,7 +18,6 @@ import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; import searchIndexManager from '../searchIndexManager'; import AppStorage from '../storage'; -import { copy, deepEqual, defineNotNumerableProperties, getObjectKeysAndSort, limitSymbols, listMergeSorted, safeReplaceObject, splitStringByLength, tsNow } from "../utils"; //import { telegramMeWebService } from "../mtproto/mtproto"; import apiUpdatesManager from "./apiUpdatesManager"; import appChatsManager from "./appChatsManager"; diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index 7b9352d3..947af072 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -1,7 +1,7 @@ +import { isObject } from "../../helpers/object"; import { DialogPeer, InputDialogPeer, InputPeer, Peer } from "../../layer"; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; -import { isObject } from "../utils"; import appChatsManager from "./appChatsManager"; import appUsersManager from "./appUsersManager"; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index f63c1c72..7c07d63b 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -1,12 +1,13 @@ import { bytesFromHex } from "../../helpers/bytes"; import { CancellablePromise } from "../../helpers/cancellablePromise"; import { getFileNameByLocation } from "../../helpers/fileName"; +import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from "../../helpers/object"; import { isSafari } from "../../helpers/userAgent"; import { FileLocation, InputFileLocation, Photo, PhotoSize, PhotosPhotos } from "../../layer"; import { DownloadOptions } from "../mtproto/apiFileManager"; import apiManager from "../mtproto/mtprotoworker"; import referenceDatabase, { ReferenceContext } from "../mtproto/referenceDatabase"; -import { calcImageInBox, defineNotNumerableProperties, isObject, safeReplaceArrayInObject } from "../utils"; +import { calcImageInBox } from "../../helpers/dom"; import { MyDocument } from "./appDocsManager"; import appDownloadManager from "./appDownloadManager"; import appUsersManager from "./appUsersManager"; diff --git a/src/lib/appManagers/appPollsManager.ts b/src/lib/appManagers/appPollsManager.ts index eab54926..73dca2c7 100644 --- a/src/lib/appManagers/appPollsManager.ts +++ b/src/lib/appManagers/appPollsManager.ts @@ -1,10 +1,10 @@ +import { copy } from "../../helpers/object"; import { InputMedia } from "../../layer"; import { logger, LogLevels } from "../logger"; import apiManager from "../mtproto/mtprotoworker"; import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; -import { copy } from "../utils"; import apiUpdatesManager from "./apiUpdatesManager"; import appMessagesManager from './appMessagesManager'; import appPeersManager from './appPeersManager'; diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 4c04eec0..c5aa46da 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -1,4 +1,6 @@ import { formatPhoneNumber } from "../../components/misc"; +import { tsNow } from "../../helpers/date"; +import { safeReplaceObject, isObject } from "../../helpers/object"; import { InputUser, Update, User as MTUser, UserStatus } from "../../layer"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; @@ -6,7 +8,6 @@ import serverTimeManager from "../mtproto/serverTimeManager"; import { RichTextProcessor } from "../richtextprocessor"; import $rootScope from "../rootScope"; import searchIndexManager from "../searchIndexManager"; -import { isObject, safeReplaceObject, tsNow } from "../utils"; import appChatsManager from "./appChatsManager"; import appPeersManager from "./appPeersManager"; import appStateManager from "./appStateManager"; diff --git a/src/lib/appManagers/appWebPagesManager.ts b/src/lib/appManagers/appWebPagesManager.ts index 3a30ad82..0a12f375 100644 --- a/src/lib/appManagers/appWebPagesManager.ts +++ b/src/lib/appManagers/appWebPagesManager.ts @@ -1,9 +1,10 @@ -import { limitSymbols, safeReplaceObject } from "../utils"; import appPhotosManager from "./appPhotosManager"; import appDocsManager from "./appDocsManager"; import { RichTextProcessor } from "../richtextprocessor"; import { ReferenceContext } from "../mtproto/referenceDatabase"; import $rootScope from "../rootScope"; +import { safeReplaceObject } from "../../helpers/object"; +import { limitSymbols } from "../../helpers/string"; class AppWebPagesManager { webpages: any = {}; diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 007e9698..361cce12 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -1,4 +1,4 @@ -import { cancelEvent } from "./utils"; +import { cancelEvent } from "../helpers/dom"; import appMediaPlaybackController from "../components/appMediaPlaybackController"; import { isAppleMobile } from "../helpers/userAgent"; import { isTouchSupported } from "../helpers/touchSupport"; diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 7eb2fefc..7a412907 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -1,11 +1,11 @@ import MTProtoWorker from 'worker-loader!./mtproto.worker'; +import { isObject } from '../../helpers/object'; import type { MethodDeclMap } from '../../layer'; import type { InvokeApiOptions } from '../../types'; import CryptoWorkerMethods from '../crypto/crypto_methods'; import { logger } from '../logger'; import $rootScope from '../rootScope'; import AppStorage from '../storage'; -import { isObject } from '../utils'; import webpWorkerController from '../webp/webpWorkerController'; import type { DownloadOptions } from './apiFileManager'; import { ApiError } from './apiManager'; diff --git a/src/lib/mtproto/referenceDatabase.ts b/src/lib/mtproto/referenceDatabase.ts index 60c9bf13..dfd5d8eb 100644 --- a/src/lib/mtproto/referenceDatabase.ts +++ b/src/lib/mtproto/referenceDatabase.ts @@ -1,8 +1,8 @@ import appMessagesManager from "../appManagers/appMessagesManager"; import { Photo } from "../../layer"; -import { deepEqual } from "../utils"; import { MOUNT_CLASS_TO } from "./mtproto_config"; import { bytesToHex } from "../../helpers/bytes"; +import { deepEqual } from "../../helpers/object"; export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage; export namespace ReferenceContext { diff --git a/src/lib/mtproto/serverTimeManager.ts b/src/lib/mtproto/serverTimeManager.ts index 5f4dcd67..37dec425 100644 --- a/src/lib/mtproto/serverTimeManager.ts +++ b/src/lib/mtproto/serverTimeManager.ts @@ -1,5 +1,5 @@ +import { tsNow } from '../../helpers/date'; import AppStorage from '../storage'; -import { tsNow } from '../utils'; export class ServerTimeManager { public timestampNow = tsNow(true); diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 1fb38f0a..e3dcbd2d 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -1,10 +1,11 @@ -import {encodeEntities, copy} from './utils'; import Config from './config'; import emojiRegExp from '../emoji/regex'; import { encodeEmoji } from '../emoji'; import { MOUNT_CLASS_TO } from './mtproto/mtproto_config'; import { MessageEntity } from '../layer'; +import { copy } from '../helpers/object'; +import { encodeEntities } from '../helpers/string'; const EmojiHelper = { emojiMap: (code: string) => { return code; }, diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index 6de19153..00000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,569 +0,0 @@ -import type { DownloadOptions } from "./mtproto/apiFileManager"; - -/* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { - if(!element) { - return false; - } - - parentNode = parentNode || document.body; - if(element == parentNode) { - return true; - } - return isInDOM(element.parentNode as HTMLElement, parentNode); -} */ -export function isInDOM(element: Element): boolean { - return element?.isConnected; -} - -/* export function checkDragEvent(e: any) { - if(!e || e.target && (e.target.tagName == 'IMG' || e.target.tagName == 'A')) return false - if(e.dataTransfer && e.dataTransfer.types) { - for(var i = 0; i < e.dataTransfer.types.length; i++) { - if(e.dataTransfer.types[i] == 'Files') { - return true; - } - } - } else { - return true; - } - - return false; -} */ - -export function cancelEvent (event: Event) { - event = event || window.event; - if(event) { - // @ts-ignore - event = event.originalEvent || event; - - try { - if(event.stopPropagation) event.stopPropagation(); - if(event.preventDefault) event.preventDefault(); - event.returnValue = false; - event.cancelBubble = true; - } catch(err) {} - } - - return false; -} - -export function getRichValue(field: any) { - if(!field) { - return ''; - } - var lines: string[] = []; - var line: string[] = []; - - getRichElementValue(field, lines, line); - if (line.length) { - lines.push(line.join('')); - } - - var value = lines.join('\n'); - value = value.replace(/\u00A0/g, ' '); - - return value; -} - -export function placeCaretAtEnd(el: HTMLElement) { - el.focus(); - if(typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { - var range = document.createRange(); - range.selectNodeContents(el); - range.collapse(false); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - // @ts-ignore - } else if(typeof document.body.createTextRange != "undefined") { - // @ts-ignore - var textRange = document.body.createTextRange(); - textRange.moveToElementText(el); - textRange.collapse(false); - textRange.select(); - } -} - -export function getRichElementValue(node: any, lines: string[], line: string[], selNode?: Node, selOffset?: number) { - if(node.nodeType == 3) { // TEXT - if(selNode === node) { - var value = node.nodeValue - line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)) - } else { - line.push(node.nodeValue) - } - return - } - if (node.nodeType != 1) { // NON-ELEMENT - return - } - var isSelected = (selNode === node) - var isBlock = node.tagName == 'DIV' || node.tagName == 'P' - var curChild - if(isBlock && line.length || node.tagName == 'BR') { - lines.push(line.join('')) - line.splice(0, line.length) - } else if(node.tagName == 'IMG') { - if(node.alt) { - line.push(node.alt); - } - } - - if(isSelected && !selOffset) { - line.push('\x01'); - } - - var curChild = node.firstChild; - while(curChild) { - getRichElementValue(curChild, lines, line, selNode, selOffset); - curChild = curChild.nextSibling; - } - - if(isSelected && selOffset) { - line.push('\x01'); - } - - if(isBlock && line.length) { - lines.push(line.join('')); - line.splice(0, line.length); - } -} - -/* if (Config.Modes.animations && - typeof window.requestAnimationFrame == 'function') { - window.onAnimationFrameCallback = function (cb) { - return (function () { - window.requestAnimationFrame(cb) - }) - } -} else { - window.onAnimationFrameCallback = function (cb) { - return cb - } -} */ - -// generate a path's arc data parameter -// http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands -var arcParameter = function(rx: number, ry: number, xAxisRotation: number, largeArcFlag: number, sweepFlag: number, x: number, y: number) { - return [rx, ',', ry, ' ', - xAxisRotation, ' ', - largeArcFlag, ',', - sweepFlag, ' ', - x, ',', y ].join(''); -}; - -export function generatePathData(x: number, y: number, width: number, height: number, tl: number, tr: number, br: number, bl: number) { - var data = []; - - // start point in top-middle of the rectangle - data.push('M' + (x + width / 2) + ',' + y); - - // next we go to the right - data.push('H' + (x + width - tr)); - - if (tr > 0) { - // now we draw the arc in the top-right corner - data.push('A' + arcParameter(tr, tr, 0, 0, 1, (x + width), (y + tr))); - } - - // next we go down - data.push('V' + (y + height - br)); - - if (br > 0) { - // now we draw the arc in the lower-right corner - data.push('A' + arcParameter(br, br, 0, 0, 1, (x + width - br), (y + height))); - } - - // now we go to the left - data.push('H' + (x + bl)); - - if (bl > 0) { - // now we draw the arc in the lower-left corner - data.push('A' + arcParameter(bl, bl, 0, 0, 1, (x + 0), (y + height - bl))); - } - - // next we go up - data.push('V' + (y + tl)); - - if (tl > 0) { - // now we draw the arc in the top-left corner - data.push('A' + arcParameter(tl, tl, 0, 0, 1, (x + tl), (y + 0))); - } - - // and we close the path - data.push('Z'); - - return data.join(' '); -}; - -export function isObject(object: any) { - return typeof(object) === 'object' && object !== null; -} - -export function tsNow(seconds?: boolean) { - var t = +new Date(); - return seconds ? Math.floor(t / 1000) : t; -} - -export function safeReplaceObject(wasObject: any, newObject: any) { - for(var key in wasObject) { - if(!newObject.hasOwnProperty(key) && key.charAt(0) != '$') { - delete wasObject[key]; - } - } - - for(var key in newObject) { - //if (newObject.hasOwnProperty(key)) { // useless - wasObject[key] = newObject[key]; - //} - } -} - -/** - * Will be used for FILE_REFERENCE_EXPIRED - * @param key - * @param wasObject - * @param newObject - */ -export function safeReplaceArrayInObject(key: K, wasObject: any, newObject: any) { - if('byteLength' in newObject[key]) { // Uint8Array - newObject[key] = [...newObject[key]]; - } - - if(wasObject && wasObject[key] != newObject[key]) { - wasObject[key].length = newObject[key].length; - (newObject[key] as any[]).forEach((v, i) => { - wasObject[key][i] = v; - }); - - /* wasObject[key].set(newObject[key]); */ - newObject[key] = wasObject[key]; - } -} - -export function limitSymbols(str: string, length: number, limitFrom = length + 10) { - if(str.length > limitFrom) { - str = str.slice(0, length).replace(/(\n|\s)+$/, '') + '...'; - } - - return str; -} - -export function numberWithCommas(x: number) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); -} - -//export function findUpClassName(el: any, className: string): T; -export function findUpClassName(el: any, className: string): HTMLElement { - if(el.classList.contains(className)) return el; // 03.02.2020 - - while(el.parentElement) { - el = el.parentElement; - if(el.classList.contains(className)) - return el; - } - return null; -} - -export function findUpTag(el: any, tag: string): HTMLElement { - if(el.tagName == tag) return el; // 03.02.2020 - - while(el.parentElement) { - el = el.parentElement; - if(el.tagName === tag) - return el; - } - return null; -} - -export function findUpAttribute(el: any, attribute: string): HTMLElement { - if(el.getAttribute(attribute) != null) return el; // 03.02.2020 - - while(el.parentElement) { - el = el.parentElement; - if(el.getAttribute(attribute) != null) - return el; - } - return null; -} - -export function getObjectKeysAndSort(object: any, sort: 'asc' | 'desc' = 'asc') { - const ids = Object.keys(object).map(i => +i); - if(sort == 'asc') return ids.sort((a, b) => a - b); - else return ids.sort((a, b) => b - a); -} - -export function whichChild(elem: Node) { - if(!elem.parentNode) { - return -1; - } - - let i = 0; - // @ts-ignore - while((elem = elem.previousElementSibling) != null) ++i; - return i; -}; - -export function copy(obj: T): T { - //in case of premitives - if(obj === null || typeof(obj) !== "object") { - return obj; - } - - //date objects should be - if(obj instanceof Date) { - return new Date(obj.getTime()) as any; - } - - //handle Array - if(Array.isArray(obj)){ - var clonedArr: any = []; - obj.forEach(function(element){ - clonedArr.push(copy(element)) - }); - return clonedArr; - } - - //lastly, handle objects - // @ts-ignore - let clonedObj = new obj.constructor(); - for(var prop in obj){ - if(obj.hasOwnProperty(prop)){ - clonedObj[prop] = copy(obj[prop]); - } - } - return clonedObj; -} - -export function formatBytes(bytes: number, decimals = 2) { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -} - -export function formatNumber(bytes: number, decimals = 2) { - if(bytes === 0) return '0'; - - const k = 1000; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['', 'K', 'M', 'B', 'T']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]; -} - -export function deepEqual(x: any, y: any): boolean { - const ok = Object.keys, tx = typeof x, ty = typeof y; - return x && y && tx === 'object' && tx === ty ? ( - ok(x).length === ok(y).length && - ok(x).every(key => deepEqual(x[key], y[key])) - ) : (x === y); -} - -export function listMergeSorted(list1: any, list2: any) { - list1 = list1 || []; - list2 = list2 || []; - - var result = copy(list1); - - var minID = list1.length ? list1[list1.length - 1] : 0xFFFFFFFF; - for (var i = 0; i < list2.length; i++) { - if (list2[i] < minID) { - result.push(list2[i]); - } - } - - return result; -} - -// credits to https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js -export function escapeRegExp(str: string) { - return str - .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') - .replace(/-/g, '\\x2d'); -} - -export function encodeEntities(value: string) { - return value.replace(/&/g, '&').replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, (value) => { - var hi = value.charCodeAt(0); - var low = value.charCodeAt(1); - return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; - }).replace(/([^\#-~| |!])/g, (value) => { // non-alphanumeric - return '&#' + value.charCodeAt(0) + ';'; - }).replace(//g, '>'); -} - -export function fillPropertyValue(str: string) { - let splitted = str.split(' '); - if(splitted.length != 4) { - if(!splitted[0]) splitted[0] = '0px'; - for(let i = splitted.length; i < 4; ++i) { - splitted[i] = splitted[i % 2] || splitted[0] || '0px'; - } - } - - return splitted; -} - -export function calcImageInBox (imageW: number, imageH: number, boxW: number, boxH: number, noZoom?: boolean) { - if(imageW < boxW && imageH < boxH) { - return {w: imageW, h: imageH}; - } - - var boxedImageW = boxW; - var boxedImageH = boxH; - - if((imageW / imageH) > (boxW / boxH)) { - boxedImageH = (imageH * boxW / imageW) | 0; - } else { - boxedImageW = (imageW * boxH / imageH) | 0; - if(boxedImageW > boxW) { - boxedImageH = (boxedImageH * boxW / boxedImageW) | 0; - boxedImageW = boxW; - } - } - - // if (Config.Navigator.retina) { - // imageW = Math.floor(imageW / 2) - // imageH = Math.floor(imageH / 2) - // } - - if(noZoom && boxedImageW >= imageW && boxedImageH >= imageH) { - boxedImageW = imageW; - boxedImageH = imageH; - } - - return {w: boxedImageW, h: boxedImageH}; -} - -export function getEmojiToneIndex(input: string) { - let match = input.match(/[\uDFFB-\uDFFF]/); - return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0; -} - -export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download'; -export function getFileURL(type: FileURLType, options: DownloadOptions) { - //console.log('getFileURL', location); - //const perf = performance.now(); - const encoded = encodeURIComponent(JSON.stringify(options)); - //console.log('getFileURL encode:', performance.now() - perf, encoded); - - return '/' + type + '/' + encoded; -} - -export function positionElementByIndex(element: HTMLElement, container: HTMLElement, pos: number) { - const prevPos = whichChild(element); - - if(prevPos == pos) { - return false; - } else if(prevPos != -1 && prevPos < pos) { // was higher - pos += 1; - } - - if(container.childElementCount > pos) { - container.insertBefore(element, container.children[pos]); - } else { - container.append(element); - } - - return true; -} - -export function splitStringByLength(str: string, maxLength: number) { - if(str.length < maxLength) return [str]; - let length = 0, lastSliceStartIndex = 0, arrayIndex = 0; - const delimiter = ' ';//'\n'; - const out: string[] = []; - - const cut = (end?: number) => { - let part = str.slice(lastSliceStartIndex, end); - const _arrayIndex = arrayIndex++; - if(part.length > maxLength) { - let overflowPart = part.slice(maxLength); - const splitted = splitStringByLength(overflowPart, maxLength); - splitted.forEach(part => { - out[arrayIndex++] = part; - }); - - part = part.slice(0, maxLength); - } - - lastSliceStartIndex = end; - length = 0; - out[_arrayIndex] = (out[_arrayIndex] || '') + part; - }; - - let lastIndex = 0; - do { - let index = str.indexOf(delimiter, lastIndex); - if(index === -1) { - if(lastIndex != (str.length - 1)) { - cut(); - } - - break; - } - - index += delimiter.length; - - const partLength = index - lastIndex; - if((length + partLength) > maxLength) { - cut(length); - } - - lastIndex = index; - length += partLength; - } while(true); - - return out; -} - -export function defineNotNumerableProperties(obj: {[key: string]: any}, names: string[]) { - //const perf = performance.now(); - const props = {writable: true, configurable: true}; - const out: {[name: string]: typeof props} = {}; - names.forEach(name => { - if(obj[name] === undefined) { - out[name] = props; - } - }); - Object.defineProperties(obj, out); - //console.log('defineNotNumerableProperties time:', performance.now() - perf); -} - -export function cancelSelection() { - if(window.getSelection) { - if(window.getSelection().empty) { // Chrome - window.getSelection().empty(); - } else if(window.getSelection().removeAllRanges) { // Firefox - window.getSelection().removeAllRanges(); - } - // @ts-ignore - } else if(document.selection) { // IE? - // @ts-ignore - document.selection.empty(); - } -} - -//(window as any).splitStringByLength = splitStringByLength; - -export function getSelectedText(): string { - if(window.getSelection) { - return window.getSelection().toString(); - // @ts-ignore - } else if(document.selection) { - // @ts-ignore - return document.selection.createRange().text; - } - - return ''; -} \ No newline at end of file diff --git a/src/pages/pagePassword.ts b/src/pages/pagePassword.ts index 4ba9d742..74233a94 100644 --- a/src/pages/pagePassword.ts +++ b/src/pages/pagePassword.ts @@ -9,7 +9,7 @@ import LottieLoader, { RLottiePlayer } from '../lib/lottieLoader'; //import passwordManager from '../lib/mtproto/passwordManager'; import apiManager from '../lib/mtproto/mtprotoworker'; import passwordManager from '../lib/mtproto/passwordManager'; -import { cancelEvent } from '../lib/utils'; +import { cancelEvent } from '../helpers/dom'; import Page from './page'; import pageIm from './pageIm'; diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index b0a809fe..d83133ee 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -5,7 +5,7 @@ import appStateManager from "../lib/appManagers/appStateManager"; import apiManager from "../lib/mtproto/mtprotoworker"; import { App, Modes } from "../lib/mtproto/mtproto_config"; import { RichTextProcessor } from '../lib/richtextprocessor'; -import { findUpTag } from "../lib/utils"; +import { findUpTag } from "../helpers/dom"; import Page from "./page"; import pageAuthCode from "./pageAuthCode"; import pageSignQR from './pageSignQR'; diff --git a/src/pages/pagesManager.ts b/src/pages/pagesManager.ts index 91843e82..232c4c6e 100644 --- a/src/pages/pagesManager.ts +++ b/src/pages/pagesManager.ts @@ -1,5 +1,5 @@ import Page from "./page"; -import { whichChild } from "../lib/utils"; +import { whichChild } from "../helpers/dom"; import lottieLoader from "../lib/lottieLoader"; import { horizontalMenu } from "../components/horizontalMenu"; import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 2a40b52c..38c5233b 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -116,7 +116,6 @@ color: #a3a3a3; font-size: 1.125rem; margin-left: .125rem; - display: inline; animation: fade-in-opacity .2s ease forwards; } } @@ -274,7 +273,6 @@ flex: 0 0 auto; content: " "; background: url(assets/img/icon-verified.svg); - display: inline-block; width: 20px; height: 20px; margin-left: .125rem; diff --git a/src/scss/style.scss b/src/scss/style.scss index 869b745d..efffe251 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1017,3 +1017,9 @@ img.emoji { // -webkit-user-select: none; /* disable selection/Copy of UIWebView */ // -webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */ // } + +middle-ellipsis-element { + width: 100%; + overflow: hidden; + display: block; +}