Eduard Kuzmenko
3 years ago
174 changed files with 4273 additions and 1967 deletions
@ -0,0 +1,424 @@
@@ -0,0 +1,424 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import IS_SCREEN_SHARING_SUPPORTED from "../../environment/screenSharingSupport"; |
||||
import { attachClickEvent } from "../../helpers/dom/clickEvent"; |
||||
import ControlsHover from "../../helpers/dom/controlsHover"; |
||||
import findUpClassName from "../../helpers/dom/findUpClassName"; |
||||
import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../../helpers/dom/fullScreen"; |
||||
import { MediaSize } from "../../helpers/mediaSizes"; |
||||
import MovablePanel from "../../helpers/movablePanel"; |
||||
import safeAssign from "../../helpers/object/safeAssign"; |
||||
import toggleClassName from "../../helpers/toggleClassName"; |
||||
import type { AppAvatarsManager } from "../../lib/appManagers/appAvatarsManager"; |
||||
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; |
||||
import CallInstance from "../../lib/calls/callInstance"; |
||||
import CALL_STATE from "../../lib/calls/callState"; |
||||
import I18n, { i18n } from "../../lib/langPack"; |
||||
import RichTextProcessor from "../../lib/richtextprocessor"; |
||||
import rootScope from "../../lib/rootScope"; |
||||
import animationIntersector from "../animationIntersector"; |
||||
import ButtonIcon from "../buttonIcon"; |
||||
import GroupCallMicrophoneIconMini from "../groupCall/microphoneIconMini"; |
||||
import { MovableState } from "../movableElement"; |
||||
import PeerTitle from "../peerTitle"; |
||||
import PopupElement from "../popups"; |
||||
import SetTransition from "../singleTransition"; |
||||
import makeButton from "./button"; |
||||
import CallDescriptionElement from "./description"; |
||||
import callVideoCanvasBlur from "./videoCanvasBlur"; |
||||
|
||||
const className = 'call'; |
||||
|
||||
let previousState: MovableState = { |
||||
width: 400, |
||||
height: 580 |
||||
}; |
||||
|
||||
export default class PopupCall extends PopupElement { |
||||
private instance: CallInstance; |
||||
private appAvatarsManager: AppAvatarsManager; |
||||
private appPeersManager: AppPeersManager; |
||||
private peerId: PeerId; |
||||
|
||||
private description: CallDescriptionElement; |
||||
private emojisSubtitle: HTMLElement; |
||||
|
||||
private partyStates: HTMLElement; |
||||
private partyMutedState: HTMLElement; |
||||
|
||||
private firstButtonsRow: HTMLElement; |
||||
private secondButtonsRow: HTMLElement; |
||||
|
||||
private declineI18nElement: I18n.IntlElement; |
||||
|
||||
private makeButton: (options: Parameters<typeof makeButton>[2]) => HTMLElement; |
||||
private btnAccept: HTMLElement; |
||||
private btnDecline: HTMLElement; |
||||
private btnVideo: HTMLElement; |
||||
private btnScreen: HTMLElement; |
||||
private btnMute: HTMLElement; |
||||
private btnFullScreen: HTMLButtonElement; |
||||
private btnExitFullScreen: HTMLButtonElement; |
||||
|
||||
private movablePanel: MovablePanel; |
||||
private microphoneIcon: GroupCallMicrophoneIconMini; |
||||
private muteI18nElement: I18n.IntlElement; |
||||
|
||||
private videoContainers: { |
||||
input?: HTMLElement, |
||||
output?: HTMLElement |
||||
}; |
||||
|
||||
private controlsHover: ControlsHover; |
||||
|
||||
constructor(options: { |
||||
appAvatarsManager: AppAvatarsManager, |
||||
appPeersManager: AppPeersManager, |
||||
instance: CallInstance |
||||
}) { |
||||
super('popup-call', undefined, { |
||||
withoutOverlay: true, |
||||
closable: true |
||||
}); |
||||
|
||||
safeAssign(this, options); |
||||
|
||||
this.videoContainers = {}; |
||||
|
||||
const {container, listenerSetter, instance} = this; |
||||
container.classList.add(className, 'night'); |
||||
|
||||
const avatarContainer = document.createElement('div'); |
||||
avatarContainer.classList.add(className + '-avatar'); |
||||
|
||||
const peerId = this.peerId = this.instance.interlocutorUserId.toPeerId(); |
||||
const photo = this.appPeersManager.getPeerPhoto(peerId); |
||||
this.appAvatarsManager.putAvatar(avatarContainer, peerId, photo, 'photo_big'); |
||||
|
||||
const title = new PeerTitle({ |
||||
peerId |
||||
}).element; |
||||
|
||||
title.classList.add(className + '-title'); |
||||
|
||||
const subtitle = document.createElement('div'); |
||||
subtitle.classList.add(className + '-subtitle'); |
||||
|
||||
const description = this.description = new CallDescriptionElement(subtitle); |
||||
|
||||
const emojisSubtitle = this.emojisSubtitle = document.createElement('div'); |
||||
emojisSubtitle.classList.add(className + '-emojis'); |
||||
|
||||
container.append(avatarContainer, title, subtitle, emojisSubtitle); |
||||
|
||||
this.btnFullScreen = ButtonIcon('fullscreen'); |
||||
this.btnExitFullScreen = ButtonIcon('smallscreen hide'); |
||||
attachClickEvent(this.btnFullScreen, this.onFullScreenClick, {listenerSetter}); |
||||
attachClickEvent(this.btnExitFullScreen, () => cancelFullScreen(), {listenerSetter}); |
||||
addFullScreenListener(this.container, this.onFullScreenChange, listenerSetter); |
||||
this.header.prepend(this.btnExitFullScreen); |
||||
this.header.append(this.btnFullScreen); |
||||
|
||||
this.partyStates = document.createElement('div'); |
||||
this.partyStates.classList.add(className + '-party-states'); |
||||
|
||||
this.partyMutedState = document.createElement('div'); |
||||
this.partyMutedState.classList.add(className + '-party-state'); |
||||
const stateText = i18n('VoipUserMicrophoneIsOff', [new PeerTitle({peerId, onlyFirstName: true}).element]); |
||||
stateText.classList.add(className + '-party-state-text'); |
||||
const mutedIcon = new GroupCallMicrophoneIconMini(false, true); |
||||
mutedIcon.setState(false, false); |
||||
this.partyMutedState.append( |
||||
mutedIcon.container, |
||||
stateText |
||||
); |
||||
|
||||
this.partyStates.append(this.partyMutedState); |
||||
this.container.append(this.partyStates); |
||||
|
||||
this.makeButton = makeButton.bind(null, className, this.listenerSetter); |
||||
this.constructFirstButtons(); |
||||
this.constructSecondButtons(); |
||||
|
||||
listenerSetter.add(instance)('state', () => { |
||||
this.updateInstance(); |
||||
}); |
||||
|
||||
listenerSetter.add(instance)('mediaState', () => { |
||||
this.updateInstance(); |
||||
}); |
||||
|
||||
this.movablePanel = new MovablePanel({ |
||||
listenerSetter, |
||||
movableOptions: { |
||||
minWidth: 400, |
||||
minHeight: 580, |
||||
element: this.element, |
||||
verifyTouchTarget: (e) => { |
||||
const target = e.target; |
||||
if(findUpClassName(target, 'call-button') || |
||||
findUpClassName(target, 'btn-icon') || |
||||
isFullScreen()) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
}, |
||||
// onResize: () => this.toggleBigLayout(),
|
||||
previousState |
||||
}); |
||||
|
||||
const controlsHover = this.controlsHover = new ControlsHover(); |
||||
controlsHover.setup({ |
||||
element: this.container, |
||||
listenerSetter: this.listenerSetter, |
||||
showOnLeaveToClassName: 'call-buttons' |
||||
}); |
||||
controlsHover.showControls(false); |
||||
|
||||
this.addEventListener('close', () => { |
||||
const {movablePanel} = this; |
||||
previousState = movablePanel.state; |
||||
|
||||
this.microphoneIcon.destroy(); |
||||
|
||||
movablePanel.destroy(); |
||||
}); |
||||
|
||||
this.updateInstance(); |
||||
} |
||||
|
||||
private constructFirstButtons() { |
||||
const buttons = this.firstButtonsRow = document.createElement('div'); |
||||
buttons.classList.add(className + '-buttons', 'is-first'); |
||||
|
||||
const toggleDisability = toggleClassName.bind(null, 'btn-disabled'); |
||||
|
||||
const btnVideo = this.btnVideo = this.makeButton({ |
||||
text: 'Call.Camera', |
||||
icon: 'videocamera_filled', |
||||
callback: () => { |
||||
const toggle = toggleDisability([btnVideo, btnScreen], true); |
||||
this.instance.toggleVideoSharing().finally(toggle); |
||||
} |
||||
}); |
||||
|
||||
const btnScreen = this.btnScreen = this.makeButton({ |
||||
text: 'Call.Screen', |
||||
icon: 'sharescreen_filled', |
||||
callback: () => { |
||||
const toggle = toggleDisability([btnVideo, btnScreen], true); |
||||
this.instance.toggleScreenSharing().finally(toggle); |
||||
} |
||||
}); |
||||
|
||||
if(!IS_SCREEN_SHARING_SUPPORTED) { |
||||
btnScreen.classList.add('hide'); |
||||
this.container.classList.add('no-screen'); |
||||
} |
||||
|
||||
this.muteI18nElement = new I18n.IntlElement({ |
||||
key: 'Call.Mute' |
||||
}); |
||||
const btnMute = this.btnMute = this.makeButton({ |
||||
text: this.muteI18nElement.element, |
||||
callback: () => { |
||||
this.instance.toggleMuted(); |
||||
} |
||||
}); |
||||
|
||||
const microphoneIcon = this.microphoneIcon = new GroupCallMicrophoneIconMini(true, true); |
||||
btnMute.firstElementChild.append(microphoneIcon.container); |
||||
|
||||
// btnVideo.classList.add('disabled');
|
||||
// btnScreen.classList.add('disabled');
|
||||
|
||||
buttons.append(btnVideo, btnScreen, btnMute); |
||||
this.container.append(buttons); |
||||
} |
||||
|
||||
private constructSecondButtons() { |
||||
const buttons = this.secondButtonsRow = document.createElement('div'); |
||||
buttons.classList.add(className + '-buttons', 'is-second'); |
||||
|
||||
this.declineI18nElement = new I18n.IntlElement({ |
||||
key: 'Call.Decline' |
||||
}); |
||||
const btnDecline = this.btnDecline = this.makeButton({ |
||||
text: this.declineI18nElement.element, |
||||
icon: 'endcall_filled', |
||||
callback: () => { |
||||
this.instance.hangUp('phoneCallDiscardReasonHangup'); |
||||
}, |
||||
isDanger: true |
||||
}); |
||||
|
||||
const btnAccept = this.btnAccept = this.makeButton({ |
||||
text: 'Call.Accept', |
||||
icon: 'phone', |
||||
callback: () => { |
||||
this.instance.acceptCall(); |
||||
}, |
||||
isConfirm: true, |
||||
}); |
||||
|
||||
buttons.append(btnDecline, btnAccept); |
||||
this.container.append(buttons); |
||||
} |
||||
|
||||
private onFullScreenClick = () => { |
||||
requestFullScreen(this.container); |
||||
}; |
||||
|
||||
private onFullScreenChange = () => { |
||||
const isFull = isFullScreen(); |
||||
|
||||
const {btnFullScreen, btnExitFullScreen} = this; |
||||
|
||||
const wasFullScreen = this.container.classList.contains('is-full-screen'); |
||||
this.container.classList.toggle('is-full-screen', isFull); |
||||
btnFullScreen && btnFullScreen.classList.toggle('hide', isFull); |
||||
btnExitFullScreen && btnExitFullScreen.classList.toggle('hide', !isFull); |
||||
this.btnClose.classList.toggle('hide', isFull); |
||||
|
||||
if(isFull !== wasFullScreen) { |
||||
animationIntersector.checkAnimations(isFull); |
||||
|
||||
rootScope.setThemeColor(isFull ? '#000000' : undefined); |
||||
} |
||||
}; |
||||
|
||||
private createVideoContainer(video: HTMLVideoElement) { |
||||
const _className = className + '-video'; |
||||
const container = document.createElement('div'); |
||||
container.classList.add(_className + '-container'); |
||||
|
||||
video.classList.add(_className); |
||||
if(video.paused) { |
||||
video.play(); |
||||
} |
||||
|
||||
attachClickEvent(container, () => { |
||||
if(!container.classList.contains('small')) { |
||||
return; |
||||
} |
||||
|
||||
const big = Object.values(this.videoContainers).find(container => !container.classList.contains('small')); |
||||
big.classList.add('small'); |
||||
big.style.cssText = container.style.cssText; |
||||
container.classList.remove('small'); |
||||
container.style.cssText = ''; |
||||
}); |
||||
|
||||
const canvas = callVideoCanvasBlur(video); |
||||
canvas.classList.add(_className + '-blur'); |
||||
|
||||
container.append(canvas, video); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
private updateInstance() { |
||||
const {instance} = this; |
||||
const {connectionState} = instance; |
||||
if(connectionState === CALL_STATE.CLOSED) { |
||||
if(this.container.classList.contains('is-full-screen')) { |
||||
cancelFullScreen(); |
||||
} |
||||
|
||||
this.btnVideo.classList.add('disabled'); |
||||
|
||||
this.hide(); |
||||
return; |
||||
} |
||||
|
||||
const isPendingIncoming = !instance.isOutgoing && connectionState === CALL_STATE.PENDING; |
||||
this.declineI18nElement.compareAndUpdate({ |
||||
key: connectionState === CALL_STATE.PENDING ? 'Call.Decline' : 'Call.End' |
||||
}); |
||||
this.btnAccept.classList.toggle('disable', !isPendingIncoming); |
||||
this.btnAccept.classList.toggle('hide-me', !isPendingIncoming); |
||||
this.container.classList.toggle('two-button-rows', isPendingIncoming); |
||||
|
||||
const isMuted = instance.isMuted; |
||||
const onFrame = () => { |
||||
this.btnMute.firstElementChild.classList.toggle('active', isMuted); |
||||
}; |
||||
|
||||
const player = this.microphoneIcon.getItem().player; |
||||
this.microphoneIcon.setState(!isMuted, !isMuted, onFrame); |
||||
if(!player) { |
||||
onFrame(); |
||||
} |
||||
|
||||
this.muteI18nElement.compareAndUpdate({ |
||||
key: isMuted ? 'VoipUnmute' : 'Call.Mute' |
||||
}); |
||||
|
||||
const isSharingVideo = instance.isSharingVideo; |
||||
this.btnVideo.firstElementChild.classList.toggle('active', isSharingVideo); |
||||
|
||||
const isSharingScreen = instance.isSharingScreen; |
||||
this.btnScreen.firstElementChild.classList.toggle('active', isSharingScreen); |
||||
|
||||
const outputState = instance.getMediaState('output'); |
||||
|
||||
SetTransition(this.partyMutedState, 'is-visible', !!outputState?.muted, 300); |
||||
|
||||
const containers = this.videoContainers; |
||||
['input' as const, 'output' as const].forEach(type => { |
||||
const mediaState = instance.getMediaState(type); |
||||
const video = instance.getVideoElement(type) as HTMLVideoElement; |
||||
const isActive = !!video && !!(mediaState && (mediaState.videoState === 'active' || mediaState.screencastState === 'active')); |
||||
let videoContainer = containers[type]; |
||||
|
||||
if(isActive && video && !videoContainer) { |
||||
videoContainer = containers[type] = this.createVideoContainer(video); |
||||
this.container.append(videoContainer); |
||||
} |
||||
|
||||
if(!isActive && videoContainer) { |
||||
videoContainer.remove(); |
||||
delete containers[type]; |
||||
} |
||||
}); |
||||
|
||||
const inputVideoContainer = containers.input; |
||||
if(inputVideoContainer) { |
||||
const isSmall = !!containers.output; |
||||
inputVideoContainer.classList.toggle('small', isSmall); |
||||
|
||||
const video = instance.getVideoElement('input') as HTMLVideoElement; |
||||
if(isSmall) { |
||||
const mediaSize = new MediaSize(120, 80); |
||||
const aspected = mediaSize.aspectFitted(new MediaSize(video.videoWidth, video.videoHeight)); |
||||
// inputVideoContainer.style.width = aspected.width + 'px';
|
||||
// inputVideoContainer.style.height = aspected.height + 'px';
|
||||
// const ratio = 120 / 80;
|
||||
inputVideoContainer.style.width = '120px'; |
||||
inputVideoContainer.style.height = '80px'; |
||||
} else { |
||||
inputVideoContainer.style.cssText = ''; |
||||
} |
||||
} |
||||
|
||||
this.container.classList.toggle('no-video', !Object.keys(containers).length); |
||||
|
||||
if(!this.emojisSubtitle.textContent && connectionState < CALL_STATE.EXCHANGING_KEYS) { |
||||
Promise.resolve(instance.getEmojisFingerprint()).then(emojis => { |
||||
this.emojisSubtitle.innerHTML = RichTextProcessor.wrapEmojiText(emojis.join('')); |
||||
}); |
||||
} |
||||
|
||||
this.setDescription(); |
||||
} |
||||
|
||||
private setDescription() { |
||||
this.description.update(this.instance); |
||||
} |
||||
} |
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
import IS_WEBRTC_SUPPORTED from "./webrtcSupport"; |
||||
|
||||
const IS_CALL_SUPPORTED = IS_WEBRTC_SUPPORTED && false; |
||||
const IS_CALL_SUPPORTED = IS_WEBRTC_SUPPORTED; |
||||
|
||||
export default IS_CALL_SUPPORTED; |
||||
|
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export default function accumulate(arr: number[], initialValue: number) { |
||||
return arr.reduce((acc, value) => acc + value, initialValue); |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export default function filterUnique<T extends Array<any>>(arr: T): T { |
||||
return [...new Set(arr)] as T; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
export default function findAndSpliceAll<T>(array: Array<T>, verify: (value: T, index: number, arr: typeof array) => boolean) { |
||||
const out: typeof array = []; |
||||
let idx = -1; |
||||
while((idx = array.findIndex(verify)) !== -1) { |
||||
out.push(array.splice(idx, 1)[0]); |
||||
} |
||||
|
||||
return out; |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export default function flatten<T>(arr: T[][]): T[] { |
||||
return arr.reduce((acc, val) => (acc.push(...val), acc), []); |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
export default function forEachReverse<T>(array: Array<T>, callback: (value: T, index?: number, array?: Array<T>) => void) { |
||||
for(let length = array.length, i = length - 1; i >= 0; --i) { |
||||
callback(array[i], i, array); |
||||
} |
||||
}; |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
export default function indexOfAndSplice<T>(array: Array<T>, item: T) { |
||||
const idx = array.indexOf(item); |
||||
const spliced = idx !== -1 && array.splice(idx, 1); |
||||
return spliced && spliced[0]; |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
export default function insertInDescendSortedArray<T extends {[smth in K]?: number}, K extends keyof T>(array: Array<T>, element: T, property: K, pos?: number) { |
||||
const sortProperty: number = element[property]; |
||||
|
||||
if(pos === undefined) { |
||||
pos = array.indexOf(element); |
||||
if(pos !== -1) { |
||||
const prev = array[pos - 1]; |
||||
const next = array[pos + 1]; |
||||
if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) { |
||||
// console.warn('same pos', pos, sortProperty, prev, next);
|
||||
return pos; |
||||
} |
||||
|
||||
array.splice(pos, 1); |
||||
} |
||||
} |
||||
|
||||
const len = array.length; |
||||
if(!len || sortProperty <= array[len - 1][property]) { |
||||
return array.push(element) - 1; |
||||
} else if(sortProperty >= array[0][property]) { |
||||
array.unshift(element); |
||||
return 0; |
||||
} else { |
||||
for(let i = 0; i < len; i++) { |
||||
if(sortProperty > array[i][property]) { |
||||
array.splice(i, 0, element); |
||||
return i; |
||||
} |
||||
} |
||||
} |
||||
|
||||
console.error('wtf', array, element); |
||||
return array.indexOf(element); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import bigInt from 'big-integer'; |
||||
|
||||
export function bigIntFromBytes(bytes: Uint8Array | number[], base = 256) { |
||||
return bigInt.fromArray(bytes instanceof Uint8Array ? [...bytes] : bytes, base); |
||||
} |
||||
|
||||
export function bigIntToBytes(bigInt: bigInt.BigInteger) { |
||||
return new Uint8Array(bigInt.toArray(256).value); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
import bigInt from "big-integer"; |
||||
import { nextRandomUint } from "../random"; |
||||
|
||||
export default function bigIntRandom(min: bigInt.BigNumber, max: bigInt.BigNumber) { |
||||
return bigInt.randBetween(min, max, () => { |
||||
return nextRandomUint(32) / 0xFFFFFFFF; |
||||
/* const bits = 32; |
||||
const randomBytes = new Uint8Array(bits / 8); |
||||
crypto.getRandomValues(randomBytes); |
||||
const r = bigIntFromBytes(randomBytes).mod(bigInt(2).pow(bits)); |
||||
return r.toJSNumber(); */ |
||||
}); |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
import bufferConcats from "./bufferConcats"; |
||||
|
||||
export default function addPadding<T extends number[] | ArrayBuffer | Uint8Array>( |
||||
bytes: T, |
||||
blockSize: number = 16, |
||||
zeroes?: boolean, |
||||
blockSizeAsTotalLength = false, |
||||
prepend = false |
||||
): T { |
||||
const len = (bytes as ArrayBuffer).byteLength || (bytes as Uint8Array).length; |
||||
const needPadding = blockSizeAsTotalLength ? blockSize - len : blockSize - (len % blockSize); |
||||
if(needPadding > 0 && needPadding < blockSize) { |
||||
////console.log('addPadding()', len, blockSize, needPadding);
|
||||
const padding = new Uint8Array(needPadding); |
||||
if(zeroes) { |
||||
for(let i = 0; i < needPadding; ++i) { |
||||
padding[i] = 0; |
||||
} |
||||
} else { |
||||
padding.randomize(); |
||||
} |
||||
|
||||
if(bytes instanceof ArrayBuffer) { |
||||
return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)).buffer as T; |
||||
} else if(bytes instanceof Uint8Array) { |
||||
return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)) as T; |
||||
} else { |
||||
// @ts-ignore
|
||||
return (prepend ? [...padding].concat(bytes) : bytes.concat([...padding])) as T; |
||||
} |
||||
} |
||||
|
||||
return bytes; |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
export default function bufferConcats(...args: (ArrayBuffer | Uint8Array | number[])[]) { |
||||
const length = args.reduce((acc, v) => acc + ((v as ArrayBuffer).byteLength || (v as Uint8Array).length), 0); |
||||
|
||||
const tmp = new Uint8Array(length); |
||||
|
||||
let lastLength = 0; |
||||
args.forEach(b => { |
||||
tmp.set(b instanceof ArrayBuffer ? new Uint8Array(b) : b, lastLength); |
||||
lastLength += (b as ArrayBuffer).byteLength || (b as Uint8Array).length; |
||||
}); |
||||
|
||||
return tmp/* .buffer */; |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
export default function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8Array) { |
||||
const len = bytes1.length; |
||||
if(len !== bytes2.length) { |
||||
return false; |
||||
} |
||||
|
||||
for(let i = 0; i < len; ++i) { |
||||
if(bytes1[i] !== bytes2[i]) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
export default function bytesFromHex(hexString: string) { |
||||
const len = hexString.length; |
||||
const bytes = new Uint8Array(Math.ceil(len / 2)); |
||||
let start = 0; |
||||
|
||||
if(len % 2) { // read 0x581 as 0x0581
|
||||
bytes[start++] = parseInt(hexString.charAt(0), 16); |
||||
} |
||||
|
||||
for(let i = start; i < len; i += 2) { |
||||
bytes[start++] = parseInt(hexString.substr(i, 2), 16); |
||||
} |
||||
|
||||
return bytes; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
export default function bytesFromWordss(input: Uint32Array) { |
||||
const o = new Uint8Array(input.byteLength); |
||||
for(let i = 0, length = input.length * 4; i < length; ++i) { |
||||
o[i] = ((input[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); |
||||
} |
||||
|
||||
return o; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { bigIntFromBytes, bigIntToBytes } from '../bigInt/bigIntConversion'; |
||||
|
||||
export default function bytesModPow(bytes: number[] | Uint8Array, exp: number[] | Uint8Array, mod: number[] | Uint8Array) { |
||||
const bytesBigInt = bigIntFromBytes(bytes); |
||||
const expBigInt = bigIntFromBytes(exp); |
||||
const modBigInt = bigIntFromBytes(mod); |
||||
const resBigInt = bytesBigInt.modPow(expBigInt, modBigInt); |
||||
return bigIntToBytes(resBigInt); |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
export default function bytesToBase64(bytes: number[] | Uint8Array) { |
||||
let mod3: number; |
||||
let result = ''; |
||||
|
||||
for(let nLen = bytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; ++nIdx) { |
||||
mod3 = nIdx % 3; |
||||
nUint24 |= bytes[nIdx] << (16 >>> mod3 & 24); |
||||
if(mod3 === 2 || nLen - nIdx === 1) { |
||||
result += String.fromCharCode( |
||||
uint6ToBase64(nUint24 >>> 18 & 63), |
||||
uint6ToBase64(nUint24 >>> 12 & 63), |
||||
uint6ToBase64(nUint24 >>> 6 & 63), |
||||
uint6ToBase64(nUint24 & 63) |
||||
); |
||||
nUint24 = 0; |
||||
} |
||||
} |
||||
|
||||
return result.replace(/A(?=A$|$)/g, '='); |
||||
} |
||||
|
||||
export function uint6ToBase64(nUint6: number) { |
||||
return nUint6 < 26 |
||||
? nUint6 + 65 |
||||
: nUint6 < 52 |
||||
? nUint6 + 71 |
||||
: nUint6 < 62 |
||||
? nUint6 - 4 |
||||
: nUint6 === 62 |
||||
? 43 |
||||
: nUint6 === 63 |
||||
? 47 |
||||
: 65; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
export default function bytesToHex(bytes: ArrayLike<number>) { |
||||
const length = bytes.length; |
||||
const arr: string[] = new Array(length); |
||||
for(let i = 0; i < length; ++i) { |
||||
arr[i] = (bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16); |
||||
} |
||||
return arr.join(''); |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
import convertToUint8Array from "./convertToUint8Array"; |
||||
|
||||
export default function bytesToWordss(input: Parameters<typeof convertToUint8Array>[0]) { |
||||
const bytes = convertToUint8Array(input); |
||||
|
||||
const words: number[] = []; |
||||
for(let i = 0, len = bytes.length; i < len; ++i) { |
||||
words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); |
||||
} |
||||
|
||||
return new Uint32Array(words); |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
export default function bytesXor(bytes1: Uint8Array, bytes2: Uint8Array) { |
||||
const len = bytes1.length; |
||||
const bytes = new Uint8Array(len); |
||||
|
||||
for(let i = 0; i < len; ++i) { |
||||
bytes[i] = bytes1[i] ^ bytes2[i]; |
||||
} |
||||
|
||||
return bytes; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
export default function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] | string): Uint8Array { |
||||
if(bytes instanceof Uint8Array) { |
||||
return bytes; |
||||
} else if(typeof(bytes) === 'string') { |
||||
return new TextEncoder().encode(bytes); |
||||
} |
||||
|
||||
return new Uint8Array(bytes); |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
//export function gzipUncompress(bytes: ArrayBuffer, toString: true): string;
|
||||
|
||||
// @ts-ignore
|
||||
import pako from 'pako/dist/pako_inflate.min.js'; |
||||
|
||||
//export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array;
|
||||
export default function gzipUncompress(bytes: ArrayBuffer, toString?: boolean): string | Uint8Array { |
||||
//console.log(dT(), 'Gzip uncompress start');
|
||||
const result = pako.inflate(bytes, toString ? {to: 'string'} : undefined); |
||||
//console.log(dT(), 'Gzip uncompress finish'/* , result */);
|
||||
return result; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import bigInt from "big-integer"; |
||||
import intToUint from "../number/intToUint"; |
||||
|
||||
export default function longFromInts(high: number, low: number): string { |
||||
high = intToUint(high), low = intToUint(low); |
||||
return bigInt(high).shiftLeft(32).add(bigInt(low)).toString(10); |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import addPadding from '../bytes/addPadding'; |
||||
import bigInt from 'big-integer'; |
||||
import { bigIntToBytes } from '../bigInt/bigIntConversion'; |
||||
|
||||
export default function longToBytes(sLong: string) { |
||||
const bigIntBytes = bigIntToBytes(bigInt(sLong)).reverse(); |
||||
const bytes = addPadding(bigIntBytes, 8, true, false, false); |
||||
// console.log('longToBytes', bytes, bigIntBytes);
|
||||
|
||||
return bytes; |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import bigInt from "big-integer"; |
||||
|
||||
export default function sortLongsArray(arr: string[]) { |
||||
return arr.map(long => { |
||||
return bigInt(long); |
||||
}).sort((a, b) => { |
||||
return a.compare(b); |
||||
}).map(bigInt => { |
||||
return bigInt.toString(10); |
||||
}); |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
export default function intToUint(val: number) { |
||||
// return val < 0 ? val + 4294967296 : val; // 0 <= val <= Infinity
|
||||
return val >>> 0; // (4294967296 >>> 0) === 0; 0 <= val <= 4294967295
|
||||
} |
@ -1,157 +0,0 @@
@@ -1,157 +0,0 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
* |
||||
* Originally from: |
||||
* https://github.com/zhukov/webogram
|
||||
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com> |
||||
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
export function copy<T>(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)) { |
||||
// @ts-ignore
|
||||
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<T extends any>(obj: T, names: (keyof T)[]) { |
||||
//const perf = performance.now();
|
||||
const props = {writable: true, configurable: true}; |
||||
const out: {[name in keyof T]?: typeof props} = {}; |
||||
names.forEach(name => { |
||||
if(!obj.hasOwnProperty(name)) { |
||||
out[name] = props; |
||||
} |
||||
}); |
||||
Object.defineProperties(obj, out); |
||||
//console.log('defineNotNumerableProperties time:', performance.now() - perf);
|
||||
} |
||||
|
||||
export function getObjectKeysAndSort(object: {[key: string]: any}, sort: 'asc' | 'desc' = 'asc') { |
||||
if(!object) return []; |
||||
const ids = object instanceof Map ? [...object.keys()] : 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) { |
||||
if(!wasObject) { |
||||
return newObject; |
||||
} |
||||
|
||||
for(var key in wasObject) { |
||||
if(!newObject.hasOwnProperty(key)) { |
||||
delete wasObject[key]; |
||||
} |
||||
} |
||||
|
||||
for(var key in newObject) { |
||||
//if (newObject.hasOwnProperty(key)) { // useless
|
||||
wasObject[key] = newObject[key]; |
||||
//}
|
||||
} |
||||
|
||||
return wasObject; |
||||
} |
||||
|
||||
/** |
||||
* Will be used for FILE_REFERENCE_EXPIRED |
||||
* @param key |
||||
* @param wasObject |
||||
* @param newObject |
||||
*/ |
||||
export function safeReplaceArrayInObject<K>(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<T extends Record<any, any>>(object: any): object is T { |
||||
return typeof(object) === 'object' && object !== null; |
||||
} |
||||
|
||||
export function getDeepProperty(object: any, key: string) { |
||||
const splitted = key.split('.'); |
||||
let o: any = object; |
||||
splitted.forEach(key => { |
||||
if(!key) { |
||||
return; |
||||
} |
||||
|
||||
// @ts-ignore
|
||||
o = o[key]; |
||||
}); |
||||
|
||||
return o; |
||||
} |
||||
|
||||
export function setDeepProperty(object: any, key: string, value: any) { |
||||
const splitted = key.split('.'); |
||||
getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value; |
||||
} |
||||
|
||||
export function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { |
||||
for(const key in initObject) { |
||||
if(typeof(currentObject[key]) !== typeof(initObject[key])) { |
||||
currentObject[key] = copy(initObject[key]); |
||||
onReplace && onReplace(previousKey || key); |
||||
} else if(isObject(initObject[key])) { |
||||
validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export function safeAssign<T>(object: T, fromObject: any) { |
||||
if(fromObject) { |
||||
for(let i in fromObject) { |
||||
if(fromObject[i] !== undefined) { |
||||
// @ts-ignore
|
||||
object[i] = fromObject[i]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return object; |
||||
} |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
export default function copy<T>(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)) { |
||||
// @ts-ignore
|
||||
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; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
export default 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); |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
export default function defineNotNumerableProperties<T extends any>(obj: T, names: (keyof T)[]) { |
||||
//const perf = performance.now();
|
||||
const props = {writable: true, configurable: true}; |
||||
const out: {[name in keyof T]?: typeof props} = {}; |
||||
names.forEach(name => { |
||||
if(!obj.hasOwnProperty(name)) { |
||||
out[name] = props; |
||||
} |
||||
}); |
||||
Object.defineProperties(obj, out); |
||||
//console.log('defineNotNumerableProperties time:', performance.now() - perf);
|
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
export default function getDeepProperty(object: any, key: string) { |
||||
const splitted = key.split('.'); |
||||
let o: any = object; |
||||
splitted.forEach(key => { |
||||
if(!key) { |
||||
return; |
||||
} |
||||
|
||||
// @ts-ignore
|
||||
o = o[key]; |
||||
}); |
||||
|
||||
return o; |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export default function getObjectKeysAndSort(object: {[key: string]: any}, sort: 'asc' | 'desc' = 'asc') { |
||||
if(!object) return []; |
||||
const ids = object instanceof Map ? [...object.keys()] : Object.keys(object).map(i => +i); |
||||
if(sort === 'asc') return ids.sort((a, b) => a - b); |
||||
else return ids.sort((a, b) => b - a); |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export default function isObject<T extends Record<any, any>>(object: any): object is T { |
||||
return typeof(object) === 'object' && object !== null; |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
export default function safeAssign<T>(object: T, fromObject: any) { |
||||
if(fromObject) { |
||||
for(let i in fromObject) { |
||||
if(fromObject[i] !== undefined) { |
||||
// @ts-ignore
|
||||
object[i] = fromObject[i]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return object; |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
/** |
||||
* Will be used for FILE_REFERENCE_EXPIRED |
||||
* @param key |
||||
* @param wasObject |
||||
* @param newObject |
||||
*/ |
||||
export default function safeReplaceArrayInObject<K>(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]; |
||||
} |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
export default function safeReplaceObject(wasObject: any, newObject: any) { |
||||
if(!wasObject) { |
||||
return newObject; |
||||
} |
||||
|
||||
for(var key in wasObject) { |
||||
if(!newObject.hasOwnProperty(key)) { |
||||
delete wasObject[key]; |
||||
} |
||||
} |
||||
|
||||
for(var key in newObject) { |
||||
//if (newObject.hasOwnProperty(key)) { // useless
|
||||
wasObject[key] = newObject[key]; |
||||
//}
|
||||
} |
||||
|
||||
return wasObject; |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
import getDeepProperty from "./getDeepProperty"; |
||||
|
||||
export default function setDeepProperty(object: any, key: string, value: any) { |
||||
const splitted = key.split('.'); |
||||
getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value; |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
import copy from "./copy"; |
||||
import isObject from "./isObject"; |
||||
|
||||
export default function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { |
||||
for(const key in initObject) { |
||||
if(typeof(currentObject[key]) !== typeof(initObject[key])) { |
||||
currentObject[key] = copy(initObject[key]); |
||||
onReplace && onReplace(previousKey || key); |
||||
} else if(isObject(initObject[key])) { |
||||
validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
export default function toggleClassName(className: string, elements: HTMLElement[], disable: boolean) { |
||||
elements.forEach((element) => { |
||||
element.classList.toggle(className, disable); |
||||
}); |
||||
|
||||
return () => toggleClassName(className, elements, !disable); |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue