Browse Source

going home

master
morethanwords 4 years ago
parent
commit
4c21835d40
  1. 9
      package-lock.json
  2. 1
      package.json
  3. 10
      src/components/appAudio.ts
  4. 2
      src/components/appForward.ts
  5. 6
      src/components/appSelectPeers.ts
  6. 15
      src/components/audio.ts
  7. 2
      src/components/avatar.ts
  8. 108
      src/components/chatInput.ts
  9. 543
      src/components/emoticonsDropdown.ts
  10. 526
      src/components/emoticonsDropdown_old.ts
  11. 46
      src/components/lazyLoadQueue.ts
  12. 13
      src/components/misc.ts
  13. 403
      src/components/poll.ts
  14. 35
      src/components/popup.ts
  15. 134
      src/components/popupCreatePoll.ts
  16. 39
      src/components/popupStickers.ts
  17. 5
      src/components/preloader.ts
  18. 63
      src/components/scrollable_new.ts
  19. 6
      src/components/slider.ts
  20. 114
      src/components/wrappers.ts
  21. 29
      src/index.hbs
  22. 272
      src/lib/MP4Source.js
  23. 323
      src/lib/MP4Sourcee.ts
  24. 279
      src/lib/MP4Sourceee.ts
  25. 8
      src/lib/appManagers/appDialogsManager.ts
  26. 141
      src/lib/appManagers/appDocsManager.ts
  27. 178
      src/lib/appManagers/appImManager.ts
  28. 148
      src/lib/appManagers/appMediaViewer.ts
  29. 237
      src/lib/appManagers/appMessagesManager.ts
  30. 2
      src/lib/appManagers/appPhotosManager.ts
  31. 104
      src/lib/appManagers/appPollsManager.ts
  32. 5
      src/lib/appManagers/appSidebarLeft.ts
  33. 223
      src/lib/appManagers/appSidebarRight.ts
  34. 8
      src/lib/appManagers/appStickersManager.ts
  35. 5
      src/lib/appManagers/appWebpManager.ts
  36. 5
      src/lib/cacheStorage.ts
  37. 8
      src/lib/filemanager.ts
  38. 15
      src/lib/lottieLoader.ts
  39. 30
      src/lib/mediaPlayer.ts
  40. 174
      src/lib/mtproto/apiFileManager.ts
  41. 2
      src/lib/mtproto/mtproto.worker.js
  42. 5
      src/lib/mtproto/mtprotoworker.ts
  43. 10
      src/lib/mtproto/referenceDatabase.ts
  44. 67
      src/lib/opusDecodeController.ts
  45. 2
      src/lib/polyfill.ts
  46. 5
      src/pages/pagesManager.ts
  47. 55
      src/scss/partials/_chat.scss
  48. 232
      src/scss/partials/_chatBubble.scss
  49. 10
      src/scss/partials/_chatlist.scss
  50. 10
      src/scss/partials/_ckin.scss
  51. 2
      src/scss/partials/_fonts.scss
  52. 1
      src/scss/partials/_ico.scss
  53. 7
      src/scss/partials/_mediaViewer.scss
  54. 96
      src/scss/partials/_rightSidebar.scss
  55. 33
      src/scss/partials/popups/_createPoll.scss
  56. 9
      src/scss/partials/popups/_datePicker.scss
  57. 3
      src/scss/partials/popups/_editAvatar.scss
  58. 5
      src/scss/partials/popups/_mediaAttacher.scss
  59. 14
      src/scss/partials/popups/_popup.scss
  60. 15
      src/scss/partials/popups/_stickers.scss
  61. 2
      src/scss/style.scss
  62. 5
      src/types.d.ts

9
package-lock.json generated

@ -9512,6 +9512,15 @@ @@ -9512,6 +9512,15 @@
}
}
},
"mp4box": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.3.20.tgz",
"integrity": "sha512-9I1wOBql0c9BsIPDGHY97dcH5kT7hG0Tx6SAaJvXf+A6Z0zBfGy7L1vEfjMKgjXSjtdXWL7gO+8a5euikaFTEA==",
"dev": true,
"requires": {
"npm": "^6.9.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",

1
package.json

@ -47,6 +47,7 @@ @@ -47,6 +47,7 @@
"lottie-web": "^5.6.10",
"media-query-plugin": "^1.3.1",
"mini-css-extract-plugin": "^0.9.0",
"mp4box": "^0.3.20",
"node-sass": "^4.14.1",
"npm": "^6.14.5",
"on-build-webpack": "^0.1.0",

10
src/components/appAudio.ts

@ -52,6 +52,14 @@ class AppAudio { @@ -52,6 +52,14 @@ class AppAudio {
appDocsManager.downloadDoc(doc.id).then(() => {
this.container.append(audio);
source.src = doc.url;
}, () => {
if(this.nextMid == mid) {
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
if(this.nextMid && this.audios[this.nextMid]) {
this.audios[this.nextMid].play();
}
})
}
});
return this.audios[mid] = audio;
@ -74,7 +82,7 @@ class AppAudio { @@ -74,7 +82,7 @@ class AppAudio {
const message = appMessagesManager.getMessage(mid);
this.prevMid = this.nextMid = 0;
appMessagesManager.getSearch(message.peerID, '', {
return appMessagesManager.getSearch(message.peerID, '', {
_: type == 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterVoice'
}, mid, 3, 0, 2).then(value => {
if(this.playingAudio != audio) {

2
src/components/appForward.ts

@ -74,7 +74,7 @@ class AppForward { @@ -74,7 +74,7 @@ class AppForward {
this.sendBtn.classList.remove('is-visible');
}
}, 'dialogs', () => {
console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount);
//console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount);
this.sidebarWasActive = appSidebarRight.sidebarEl.classList.contains('active');
appSidebarRight.toggleSidebar(true);
});

6
src/components/appSelectPeers.ts

@ -98,7 +98,7 @@ export class AppSelectPeers { @@ -98,7 +98,7 @@ export class AppSelectPeers {
this.list.innerHTML = '';
this.query = value;
console.log('selectPeers input:', this.query);
//console.log('selectPeers input:', this.query);
this.getMoreResults();
}
});
@ -135,7 +135,7 @@ export class AppSelectPeers { @@ -135,7 +135,7 @@ export class AppSelectPeers {
const newOffsetIndex = dialogs[dialogs.length - 1].index || 0;
dialogs = dialogs.filter(d => d.peerID != this.myID);
if(!this.offsetIndex) {
if(!this.offsetIndex && !this.query) {
dialogs.unshift({
peerID: this.myID,
pFlags: {}
@ -175,7 +175,7 @@ export class AppSelectPeers { @@ -175,7 +175,7 @@ export class AppSelectPeers {
}
private renderResults(peerIDs: number[]) {
console.log('will renderResults:', peerIDs);
//console.log('will renderResults:', peerIDs);
peerIDs.forEach(peerID => {
const {dom} = appDialogsManager.addDialog(peerID, this.scrollable, false, false);
dom.containerEl.insertAdjacentHTML('afterbegin', '<div class="checkbox"><label><input type="checkbox"><span></span></label></div>');

15
src/components/audio.ts

@ -118,12 +118,10 @@ function wrapVoiceMessage(doc: MTDocument, audioEl: AudioElement) { @@ -118,12 +118,10 @@ function wrapVoiceMessage(doc: MTDocument, audioEl: AudioElement) {
rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active'));
}
audioEl.addAudioListener('playing', () => {
//rects.forEach(node => node.classList.remove('active'));
let start = () => {
clearInterval(interval);
interval = setInterval(() => {
if(lastIndex > svg.childElementCount || isNaN(audio.duration)) {
if(lastIndex > svg.childElementCount || isNaN(audio.duration) || audio.paused) {
clearInterval(interval);
return;
}
@ -137,6 +135,15 @@ function wrapVoiceMessage(doc: MTDocument, audioEl: AudioElement) { @@ -137,6 +135,15 @@ function wrapVoiceMessage(doc: MTDocument, audioEl: AudioElement) {
//console.log('lastIndex:', lastIndex, audio.currentTime);
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
}, 20);
};
if(!audio.paused) {
start();
}
audioEl.addAudioListener('playing', () => {
//rects.forEach(node => node.classList.remove('active'));
start();
});
audioEl.addAudioListener('pause', () => {

2
src/components/avatar.ts

@ -45,7 +45,7 @@ export default class AvatarElement extends HTMLElement { @@ -45,7 +45,7 @@ export default class AvatarElement extends HTMLElement {
} else if(name == 'peer-title') {
this.peerTitle = newValue;
} else if(name == 'dialog') {
this.isDialog = !!newValue;
this.isDialog = !!+newValue;
}
}

108
src/components/chatInput.ts

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
import Scrollable from "./scrollable_new";
import LazyLoadQueue from "./lazyLoadQueue";
import { RichTextProcessor } from "../lib/richtextprocessor";
//import apiManager from "../lib/mtproto/apiManager";
import apiManager from "../lib/mtproto/mtprotoworker";
@ -8,14 +7,14 @@ import appImManager from "../lib/appManagers/appImManager"; @@ -8,14 +7,14 @@ import appImManager from "../lib/appManagers/appImManager";
import { getRichValue, calcImageInBox } from "../lib/utils";
import { wrapDocument, wrapReply } from "./wrappers";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import initEmoticonsDropdown, { EMOTICONSSTICKERGROUP } from "./emoticonsDropdown";
import { Layouter, RectPart } from "./groupedLayout";
import Recorder from '../../public/recorder.min';
//import Recorder from '../opus-recorder/dist/recorder.min';
import opusDecodeController from "../lib/opusDecodeController";
import { touchSupport } from "../lib/config";
import animationIntersector from "./animationIntersector";
import appDocsManager from "../lib/appManagers/appDocsManager";
import emoticonsDropdown from "./emoticonsDropdown";
import PopupCreatePoll from "./popupCreatePoll";
export class ChatInput {
public pageEl = document.getElementById('page-chats') as HTMLDivElement;
@ -25,10 +24,6 @@ export class ChatInput { @@ -25,10 +24,6 @@ export class ChatInput {
public inputScroll = new Scrollable(this.inputMessageContainer);
public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
public btnCancelRecord = this.btnSend.parentElement.previousElementSibling as HTMLButtonElement;
public emoticonsDropdown: HTMLDivElement = null;
public emoticonsTimeout: number = 0;
public toggleEmoticons: HTMLButtonElement;
public emoticonsLazyLoadQueue: LazyLoadQueue = null;
public lastUrl = '';
public lastTimeType = 0;
@ -74,8 +69,6 @@ export class ChatInput { @@ -74,8 +69,6 @@ export class ChatInput {
private scrollDiff = 0;
constructor() {
this.toggleEmoticons = this.pageEl.querySelector('.toggle-emoticons') as HTMLButtonElement;
this.attachMenu.container = document.getElementById('attach-file') as HTMLButtonElement;
this.attachMenu.media = this.attachMenu.container.querySelector('.menu-media') as HTMLDivElement;
this.attachMenu.document = this.attachMenu.container.querySelector('.menu-document') as HTMLDivElement;
@ -109,7 +102,7 @@ export class ChatInput { @@ -109,7 +102,7 @@ export class ChatInput {
}
this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {
if(e.key == 'Enter') {
if(e.key == 'Enter' && !touchSupport) {
/* if(e.ctrlKey || e.metaKey) {
this.messageInput.innerHTML += '<br>';
placeCaretAtEnd(this.message)
@ -127,7 +120,7 @@ export class ChatInput { @@ -127,7 +120,7 @@ export class ChatInput {
if(touchSupport) {
this.messageInput.addEventListener('touchend', (e) => {
this.saveScroll();
toggleEmoticons(false);
emoticonsDropdown.toggle(false);
});
window.addEventListener('resize', () => {
@ -423,6 +416,10 @@ export class ChatInput { @@ -423,6 +416,10 @@ export class ChatInput {
this.fileInput.click();
});
this.attachMenu.poll.addEventListener('click', () => {
new PopupCreatePoll().show();
});
document.addEventListener('paste', (event) => {
if(!appImManager.peerID || this.attachMediaPopUp.container.classList.contains('active')) {
return;
@ -632,95 +629,6 @@ export class ChatInput { @@ -632,95 +629,6 @@ export class ChatInput {
};
}
let emoticonsDisplayTimeout = 0;
const toggleEmoticons = async(enable?: boolean) => {
if(!this.emoticonsDropdown) return;
if(touchSupport) {
const willBeActive = (!!this.emoticonsDropdown.style.display && enable === undefined) || enable;
this.toggleEmoticons.classList.toggle('flip-icon', willBeActive);
if(willBeActive) {
this.saveScroll();
// @ts-ignore
document.activeElement.blur();
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
} else {
this.toggleEmoticons.classList.toggle('active', enable);
}
if((this.emoticonsDropdown.style.display && enable === undefined) || enable) {
this.emoticonsDropdown.style.display = '';
void this.emoticonsDropdown.offsetLeft; // reflow
this.emoticonsDropdown.classList.add('active');
this.emoticonsLazyLoadQueue.unlock();
clearTimeout(emoticonsDisplayTimeout);
/* if(touchSupport) {
this.restoreScroll();
} */
} else {
this.emoticonsDropdown.classList.remove('active');
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
this.emoticonsLazyLoadQueue.lock();
clearTimeout(emoticonsDisplayTimeout);
emoticonsDisplayTimeout = setTimeout(() => {
this.emoticonsDropdown.style.display = 'none';
}, touchSupport ? 0 : 200);
/* if(touchSupport) {
this.restoreScroll();
} */
}
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
};
if(touchSupport) {
this.toggleEmoticons.addEventListener('click', () => {
if(!this.emoticonsDropdown) {
let res = initEmoticonsDropdown(this.pageEl, appImManager,
appMessagesManager, this.messageInput, this.toggleEmoticons, this.btnSend);
this.emoticonsDropdown = res.dropdown;
this.emoticonsLazyLoadQueue = res.lazyLoadQueue;
toggleEmoticons(true);
} else {
toggleEmoticons();
}
});
} else {
this.toggleEmoticons.onmouseover = (e) => {
clearTimeout(this.emoticonsTimeout);
//this.emoticonsTimeout = setTimeout(() => {
if(!this.emoticonsDropdown) {
let res = initEmoticonsDropdown(this.pageEl, appImManager,
appMessagesManager, this.messageInput, this.toggleEmoticons, this.btnSend);
this.emoticonsDropdown = res.dropdown;
this.emoticonsLazyLoadQueue = res.lazyLoadQueue;
this.toggleEmoticons.onmouseout = this.emoticonsDropdown.onmouseout = (e) => {
clearTimeout(this.emoticonsTimeout);
this.emoticonsTimeout = setTimeout(() => {
toggleEmoticons();
}, 200);
};
this.emoticonsDropdown.onmouseover = (e) => {
clearTimeout(this.emoticonsTimeout);
};
}
toggleEmoticons(true);
//}, 0/* 200 */);
};
}
this.replyElements.cancelBtn.addEventListener('click', () => {
this.replyElements.container.classList.remove('active');
this.replyToMsgID = 0;

543
src/components/emoticonsDropdown.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { AppImManager } from "../lib/appManagers/appImManager";
import { AppMessagesManager } from "../lib/appManagers/appMessagesManager";
import { horizontalMenu, renderImageFromUrl } from "./misc";
import appImManager from "../lib/appManagers/appImManager";
import { horizontalMenu, renderImageFromUrl, putPreloader } from "./misc";
import lottieLoader from "../lib/lottieLoader";
//import Scrollable from "./scrollable";
import Scrollable from "./scrollable_new";
@ -9,125 +8,41 @@ import { RichTextProcessor } from "../lib/richtextprocessor"; @@ -9,125 +8,41 @@ import { RichTextProcessor } from "../lib/richtextprocessor";
import appStickersManager, { MTStickerSet } from "../lib/appManagers/appStickersManager";
//import apiManager from '../lib/mtproto/apiManager';
import apiManager from '../lib/mtproto/mtprotoworker';
//import CryptoWorker from '../lib/crypto/cryptoworker';
import LazyLoadQueue from "./lazyLoadQueue";
import { wrapSticker } from "./wrappers";
import appDocsManager from "../lib/appManagers/appDocsManager";
import ProgressivePreloader from "./preloader";
import Config from "../lib/config";
import Config, { touchSupport } from "../lib/config";
import { MTDocument } from "../types";
import animationIntersector from "./animationIntersector";
import appSidebarRight from "../lib/appManagers/appSidebarRight";
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
const initEmoticonsDropdown = (pageEl: HTMLDivElement,
appImManager: AppImManager, appMessagesManager: AppMessagesManager,
messageInput: HTMLDivElement, toggleEl: HTMLButtonElement, btnSend: HTMLButtonElement) => {
let dropdown = pageEl.querySelector('.emoji-dropdown') as HTMLDivElement;
dropdown.style.display = '';
void dropdown.offsetLeft; // reflow
dropdown.classList.add('active'); // need
let lazyLoadQueue = new LazyLoadQueue(5);
const searchButton = dropdown.querySelector('.emoji-tabs-search');
searchButton.addEventListener('click', () => {
appSidebarRight.stickersTab.init();
});
let container = pageEl.querySelector('.emoji-container .tabs-container') as HTMLDivElement;
let tabs = pageEl.querySelector('.emoji-dropdown .emoji-tabs') as HTMLUListElement;
let tabID = -1;
horizontalMenu(tabs, container, (id) => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
tabID = id;
}, () => {
if(tabID == 1 && stickersInit) {
stickersInit();
} else if(tabID == 2 && gifsInit) {
gifsInit();
}
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
});
interface EmoticonsTab {
init: () => void,
onCloseAfterTimeout?: () => void
}
(tabs.firstElementChild.children[1] as HTMLLIElement).click(); // set emoji tab
let emoticonsMenuOnClick = (menu: HTMLUListElement, heights: number[], scroll: Scrollable, menuScroll?: Scrollable) => {
menu.addEventListener('click', function(e) {
let target = e.target as HTMLLIElement;
target = findUpTag(target, 'LI');
class EmojiTab implements EmoticonsTab {
public content: HTMLElement;
let index = whichChild(target);
let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable
init() {
this.content = document.getElementById('content-emoji') as HTMLDivElement;
/* if(menuScroll) {
menuScroll.container.scrollLeft = target.scrollWidth * index;
}
console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect());
*/
/* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись!
scroll.container.scrollTop = y;
scroll.onAddedBottom = () => {};
}; */
scroll.container.scrollTop = y;
setTimeout(() => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
}, 100);
/* window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP);
});
}); */
});
};
let emoticonsContentOnScroll = (menu: HTMLUListElement, heights: number[], prevCategoryIndex: number, scroll: HTMLDivElement, menuScroll?: Scrollable) => {
let y = scroll.scrollTop;
//console.log(heights, y);
for(let i = 0; i < heights.length; ++i) {
let height = heights[i];
if(y < height) {
menu.children[prevCategoryIndex].classList.remove('active');
prevCategoryIndex = i/* + 1 */;
menu.children[prevCategoryIndex].classList.add('active');
if(menuScroll) {
if(i < heights.length - 4) {
menuScroll.container.scrollLeft = (i - 3) * 47;
} else {
menuScroll.container.scrollLeft = i * 47;
}
}
break;
}
}
return prevCategoryIndex;
};
{
const categories = ["Smileys & Emotion", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", /* "Symbols", */"Flags", "Skin Tones"];
let divs: {
const divs: {
[category: string]: HTMLDivElement
} = {};
let sorted: {
const sorted: {
[category: string]: string[]
} = {};
for(let emoji in Config.Emoji) {
let details = Config.Emoji[emoji];
let i = '' + details;
let category = categories[+i[0] - 1];
for(const emoji in Config.Emoji) {
const details = Config.Emoji[emoji];
const i = '' + details;
const category = categories[+i[0] - 1];
if(!category) continue; // maybe it's skin tones
if(!sorted[category]) sorted[category] = [];
@ -142,27 +57,27 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -142,27 +57,27 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
delete sorted["Skin Tones"];
//console.time('emojiParse');
for(let category in sorted) {
let div = document.createElement('div');
for(const category in sorted) {
const div = document.createElement('div');
div.classList.add('emoji-category');
let titleDiv = document.createElement('div');
const titleDiv = document.createElement('div');
titleDiv.classList.add('category-title');
titleDiv.innerText = category;
let itemsDiv = document.createElement('div');
const itemsDiv = document.createElement('div');
itemsDiv.classList.add('category-items');
div.append(titleDiv, itemsDiv);
let emojis = sorted[category];
const emojis = sorted[category];
emojis.forEach(emoji => {
//let emoji = details.unified;
//let emoji = (details.unified as string).split('-')
//const emoji = details.unified;
//const emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
let spanEmoji = document.createElement('span');
let kek = RichTextProcessor.wrapRichText(emoji);
const spanEmoji = document.createElement('span');
const kek = RichTextProcessor.wrapRichText(emoji);
if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji));
@ -182,84 +97,77 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -182,84 +97,77 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
}
//console.timeEnd('emojiParse');
let contentEmojiDiv = document.getElementById('content-emoji') as HTMLDivElement;
let heights: number[] = [0];
const heights: number[] = [0];
let prevCategoryIndex = 1;
let menu = contentEmojiDiv.previousElementSibling.firstElementChild as HTMLUListElement;
let emojiScroll = new Scrollable(contentEmojiDiv, 'y', 'EMOJI', null);
const menu = this.content.previousElementSibling.firstElementChild as HTMLUListElement;
const emojiScroll = new Scrollable(this.content, 'y', 'EMOJI', null);
emojiScroll.container.addEventListener('scroll', (e) => {
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container);
prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container);
});
//emojiScroll.setVirtualContainer(emojiScroll.container);
categories.map(category => {
let div = divs[category];
const preloader = putPreloader(this.content, true);
if(!div) {
console.error('no div by category:', category);
}
setTimeout(() => {
preloader.remove();
emojiScroll.append(div);
return div;
}).forEach(div => {
//console.log('emoji heights push: ', (heights[heights.length - 1] || 0) + div.scrollHeight, div, div.scrollHeight);
heights.push((heights[heights.length - 1] || 0) + div.scrollHeight);
});
categories.map(category => {
const div = divs[category];
if(!div) {
console.error('no div by category:', category);
}
emojiScroll.append(div);
return div;
}).forEach(div => {
//console.log('emoji heights push: ', (heights[heights.length - 1] || 0) + div.scrollHeight, div, div.scrollHeight);
heights.push((heights[heights.length - 1] || 0) + div.scrollHeight);
});
}, 200);
contentEmojiDiv.addEventListener('click', function(e) {
let target = e.target as any;
//if(target.tagName != 'SPAN') return;
this.content.addEventListener('click', this.onContentClick);
EmoticonsDropdown.menuOnClick(menu, heights, emojiScroll);
this.init = null;
}
if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) {
target = target.firstElementChild;
} else if(target.tagName == 'DIV') return;
onContentClick = (e: MouseEvent) => {
let target = e.target as any;
//if(target.tagName != 'SPAN') return;
//console.log('contentEmoji div', target);
if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) {
target = target.firstElementChild;
} else if(target.tagName == 'DIV') return;
/* if(!target.classList.contains('emoji')) {
target = target.parentElement as HTMLSpanElement;
//console.log('contentEmoji div', target);
if(!target.classList.contains('emoji')) {
return;
}
} */
appImManager.chatInputC.messageInput.innerHTML += target.outerHTML;
//messageInput.innerHTML += target.innerHTML;
messageInput.innerHTML += target.outerHTML;
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chatInputC.messageInput.dispatchEvent(event);
};
btnSend.classList.add('tgico-send');
btnSend.classList.remove('tgico-microphone2');
});
onClose() {
emoticonsMenuOnClick(menu, heights, emojiScroll);
}
}
let onMediaClick = (e: MouseEvent) => {
let target = e.target as HTMLDivElement;
target = findUpTag(target, 'DIV');
let fileID = target.dataset.docID;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
dropdown.classList.remove('active');
toggleEl.classList.remove('active');
} else {
console.warn('got no doc by id:', fileID);
}
};
class StickersTab implements EmoticonsTab {
public content: HTMLElement;
let stickersInit = () => {
let contentStickersDiv = document.getElementById('content-stickers') as HTMLDivElement;
init() {
this.content = document.getElementById('content-stickers');
//let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement;
let menuWrapper = contentStickersDiv.previousElementSibling as HTMLDivElement;
let menuWrapper = this.content.previousElementSibling as HTMLDivElement;
let menu = menuWrapper.firstElementChild.firstElementChild as HTMLUListElement;
let menuScroll = new Scrollable(menuWrapper, 'x');
let stickersDiv = document.createElement('div');
stickersDiv.classList.add('stickers-categories');
contentStickersDiv.append(stickersDiv);
this.content.append(stickersDiv);
/* stickersDiv.addEventListener('mouseover', (e) => {
let target = e.target as HTMLElement;
@ -278,7 +186,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -278,7 +186,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
}
}); */
stickersDiv.addEventListener('click', onMediaClick);
stickersDiv.addEventListener('click', EmoticonsDropdown.onMediaClick);
let heights: number[] = [];
@ -300,7 +208,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -300,7 +208,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
wrapSticker({
doc,
div,
lazyLoadQueue,
lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue,
group: EMOTICONSSTICKERGROUP,
onlyThumb: true
});
@ -349,17 +257,17 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -349,17 +257,17 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
};
let prevCategoryIndex = 0;
let stickersScroll = new Scrollable(contentStickersDiv, 'y', 'STICKERS', undefined, undefined, 2);
let stickersScroll = new Scrollable(this.content, 'y', 'STICKERS', undefined, undefined, 2);
stickersScroll.container.addEventListener('scroll', (e) => {
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll);
prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll);
});
stickersScroll.setVirtualContainer(stickersDiv);
emoticonsMenuOnClick(menu, heights, stickersScroll, menuScroll);
EmoticonsDropdown.menuOnClick(menu, heights, stickersScroll, menuScroll);
stickersInit = null;
const preloader = putPreloader(this.content, true);
Promise.all([
appStickersManager.getRecentStickers().then(stickers => {
@ -368,6 +276,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -368,6 +276,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
//stickersScroll.prepend(categoryDiv);
preloader.remove();
categoryPush(categoryDiv, 'Recent', stickers.stickers, true);
}),
@ -378,6 +287,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -378,6 +287,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
sets: Array<MTStickerSet>
} = res as any;
preloader.remove();
for(let set of stickers.sets) {
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
@ -434,19 +345,31 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -434,19 +345,31 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
}
})
]);
};
let gifsInit = () => {
let contentDiv = document.getElementById('content-gifs') as HTMLDivElement;
let masonry = contentDiv.firstElementChild as HTMLDivElement;
this.init = null;
}
onClose() {
masonry.addEventListener('click', onMediaClick);
}
}
let scroll = new Scrollable(contentDiv, 'y', 'GIFS', null);
class GifsTab implements EmoticonsTab {
public content: HTMLElement;
let width = 400;
let maxSingleWidth = width - 100;
let height = 100;
init() {
this.content = document.getElementById('content-gifs');
const masonry = this.content.firstElementChild as HTMLDivElement;
masonry.addEventListener('click', EmoticonsDropdown.onMediaClick);
const scroll = new Scrollable(this.content, 'y', 'GIFS', null);
const preloader = putPreloader(this.content, true);
const width = 400;
const maxSingleWidth = width - 100;
const height = 100;
apiManager.invokeApi('messages.getSavedGifs', {hash: 0}).then((_res) => {
let res = _res as {
@ -454,9 +377,9 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -454,9 +377,9 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
gifs: MTDocument[],
hash: number
};
console.log('getSavedGifs res:', res);
//console.log('getSavedGifs res:', res);
let line: MTDocument[] = [];
//let line: MTDocument[] = [];
let wastedWidth = 0;
@ -464,6 +387,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -464,6 +387,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
res.gifs[idx] = appDocsManager.saveDoc(gif);
});
preloader.remove();
for(let i = 0, length = res.gifs.length; i < length;) {
let gif = res.gifs[i];
@ -489,7 +414,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -489,7 +414,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
line.push(gif); */
++i;
console.log('gif:', gif, w, h);
//console.log('gif:', gif, w, h);
let div = document.createElement('div');
div.style.width = w + 'px';
@ -499,7 +424,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -499,7 +424,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
masonry.append(div);
let preloader = new ProgressivePreloader(div);
lazyLoadQueue.push({
EmoticonsDropdown.lazyLoadQueue.push({
div,
load: () => {
let promise = appDocsManager.downloadDoc(gif);
@ -517,10 +442,258 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -517,10 +442,258 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
}
});
gifsInit = undefined;
this.init = null;
}
onClose() {
}
}
class EmoticonsDropdown {
public static lazyLoadQueue = new LazyLoadQueue();
private element: HTMLElement;
private emojiTab: EmojiTab;
private stickersTab: StickersTab;
private gifsTab: GifsTab;
private container: HTMLElement;
private tabsEl: HTMLElement;
private tabID = -1;
private tabs: {[id: number]: EmoticonsTab};
public searchButton: HTMLElement;
public deleteBtn: HTMLElement;
public toggleEl: HTMLElement;
private displayTimeout: number;
constructor() {
this.element = document.getElementById('emoji-dropdown') as HTMLDivElement;
let firstTime = true;
this.toggleEl = document.getElementById('toggle-emoticons');
if(touchSupport) {
this.toggleEl.addEventListener('click', () => {
if(firstTime) {
firstTime = false;
this.toggle(true);
} else {
this.toggle();
}
});
} else {
this.toggleEl.onmouseover = (e) => {
clearTimeout(this.displayTimeout);
//this.displayTimeout = setTimeout(() => {
if(firstTime) {
this.toggleEl.onmouseout = this.element.onmouseout = (e) => {
clearTimeout(this.displayTimeout);
this.displayTimeout = setTimeout(() => {
this.toggle();
}, 200);
};
this.element.onmouseover = (e) => {
clearTimeout(this.displayTimeout);
};
firstTime = false;
}
this.toggle(true);
//}, 0/* 200 */);
};
}
}
private init() {
this.emojiTab = new EmojiTab();
this.stickersTab = new StickersTab();
this.gifsTab = new GifsTab();
this.tabs = {
0: this.emojiTab,
1: this.stickersTab,
2: this.gifsTab
};
this.container = this.element.querySelector('.emoji-container .tabs-container') as HTMLDivElement;
this.tabsEl = this.element.querySelector('.emoji-tabs') as HTMLUListElement;
horizontalMenu(this.tabsEl, this.container, (id) => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
this.tabID = id;
this.searchButton.classList.toggle('hide', this.tabID != 1);
this.deleteBtn.classList.toggle('hide', this.tabID != 0);
}, () => {
const tab = this.tabs[this.tabID];
if(tab.init) {
tab.init();
}
tab.onCloseAfterTimeout && tab.onCloseAfterTimeout();
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
});
this.searchButton = this.element.querySelector('.emoji-tabs-search');
this.searchButton.addEventListener('click', () => {
appSidebarRight.stickersTab.init();
});
this.deleteBtn = this.element.querySelector('.emoji-tabs-delete');
this.deleteBtn.addEventListener('click', () => {
const input = appImManager.chatInputC.messageInput;
if((input.lastChild as any)?.tagName) {
input.lastElementChild.remove();
} else if(input.lastChild) {
if(!input.lastChild.textContent.length) {
input.lastChild.remove();
} else {
input.lastChild.textContent = input.lastChild.textContent.slice(0, -1);
}
}
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chatInputC.messageInput.dispatchEvent(event);
//appSidebarRight.stickersTab.init();
});
(this.tabsEl.firstElementChild.children[1] as HTMLLIElement).click(); // set emoji tab
this.tabs[0].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка
}
public toggle = async(enable?: boolean) => {
//if(!this.element) return;
const willBeActive = (!!this.element.style.display && enable === undefined) || enable;
if(this.init) {
if(willBeActive) {
this.init();
this.init = null;
} else {
return;
}
}
if(touchSupport) {
this.toggleEl.classList.toggle('flip-icon', willBeActive);
if(willBeActive) {
appImManager.chatInputC.saveScroll();
// @ts-ignore
document.activeElement.blur();
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
} else {
this.toggleEl.classList.toggle('active', enable);
}
if((this.element.style.display && enable === undefined) || enable) {
this.element.style.display = '';
void this.element.offsetLeft; // reflow
this.element.classList.add('active');
EmoticonsDropdown.lazyLoadQueue.unlock();
clearTimeout(this.displayTimeout);
/* if(touchSupport) {
this.restoreScroll();
} */
} else {
this.element.classList.remove('active');
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
EmoticonsDropdown.lazyLoadQueue.lock();
clearTimeout(this.displayTimeout);
this.displayTimeout = setTimeout(() => {
this.element.style.display = 'none';
}, touchSupport ? 0 : 200);
/* if(touchSupport) {
this.restoreScroll();
} */
}
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
};
public static menuOnClick = (menu: HTMLUListElement, heights: number[], scroll: Scrollable, menuScroll?: Scrollable) => {
menu.addEventListener('click', function(e) {
let target = e.target as HTMLLIElement;
target = findUpTag(target, 'LI');
let index = whichChild(target);
let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable
/* if(menuScroll) {
menuScroll.container.scrollLeft = target.scrollWidth * index;
}
console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect());
*/
/* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись!
scroll.container.scrollTop = y;
scroll.onAddedBottom = () => {};
}; */
scroll.container.scrollTop = y;
setTimeout(() => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
}, 100);
/* window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP);
});
}); */
});
};
return {dropdown, lazyLoadQueue};
};
public static contentOnScroll = (menu: HTMLUListElement, heights: number[], prevCategoryIndex: number, scroll: HTMLDivElement, menuScroll?: Scrollable) => {
let y = scroll.scrollTop;
//console.log(heights, y);
for(let i = 0; i < heights.length; ++i) {
let height = heights[i];
if(y < height) {
menu.children[prevCategoryIndex].classList.remove('active');
prevCategoryIndex = i/* + 1 */;
menu.children[prevCategoryIndex].classList.add('active');
if(menuScroll) {
if(i < heights.length - 4) {
menuScroll.container.scrollLeft = (i - 3) * 47;
} else {
menuScroll.container.scrollLeft = i * 47;
}
}
break;
}
}
return prevCategoryIndex;
};
export default initEmoticonsDropdown;
public static onMediaClick = (e: MouseEvent) => {
let target = e.target as HTMLDivElement;
target = findUpTag(target, 'DIV');
let fileID = target.dataset.docID;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
/* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */
} else {
console.warn('got no doc by id:', fileID);
}
};
}
const emoticonsDropdown = new EmoticonsDropdown();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).emoticonsDropdown = emoticonsDropdown;
}
export default emoticonsDropdown;

526
src/components/emoticonsDropdown_old.ts

@ -0,0 +1,526 @@ @@ -0,0 +1,526 @@
import { AppImManager } from "../lib/appManagers/appImManager";
import { AppMessagesManager } from "../lib/appManagers/appMessagesManager";
import { horizontalMenu, renderImageFromUrl } from "./misc";
import lottieLoader from "../lib/lottieLoader";
//import Scrollable from "./scrollable";
import Scrollable from "./scrollable_new";
import { findUpTag, whichChild, calcImageInBox } from "../lib/utils";
import { RichTextProcessor } from "../lib/richtextprocessor";
import appStickersManager, { MTStickerSet } from "../lib/appManagers/appStickersManager";
//import apiManager from '../lib/mtproto/apiManager';
import apiManager from '../lib/mtproto/mtprotoworker';
//import CryptoWorker from '../lib/crypto/cryptoworker';
import LazyLoadQueue from "./lazyLoadQueue";
import { wrapSticker } from "./wrappers";
import appDocsManager from "../lib/appManagers/appDocsManager";
import ProgressivePreloader from "./preloader";
import Config from "../lib/config";
import { MTDocument } from "../types";
import animationIntersector from "./animationIntersector";
import appSidebarRight from "../lib/appManagers/appSidebarRight";
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
const initEmoticonsDropdown = (pageEl: HTMLDivElement,
appImManager: AppImManager, appMessagesManager: AppMessagesManager,
messageInput: HTMLDivElement, toggleEl: HTMLButtonElement, btnSend: HTMLButtonElement) => {
let dropdown = pageEl.querySelector('.emoji-dropdown') as HTMLDivElement;
dropdown.style.display = '';
void dropdown.offsetLeft; // reflow
dropdown.classList.add('active'); // need
let lazyLoadQueue = new LazyLoadQueue();
const searchButton = dropdown.querySelector('.emoji-tabs-search');
searchButton.addEventListener('click', () => {
appSidebarRight.stickersTab.init();
});
let container = pageEl.querySelector('.emoji-container .tabs-container') as HTMLDivElement;
let tabs = pageEl.querySelector('.emoji-dropdown .emoji-tabs') as HTMLUListElement;
let tabID = -1;
horizontalMenu(tabs, container, (id) => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
tabID = id;
}, () => {
if(tabID == 1 && stickersInit) {
stickersInit();
} else if(tabID == 2 && gifsInit) {
gifsInit();
}
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
});
(tabs.firstElementChild.children[1] as HTMLLIElement).click(); // set emoji tab
let emoticonsMenuOnClick = (menu: HTMLUListElement, heights: number[], scroll: Scrollable, menuScroll?: Scrollable) => {
menu.addEventListener('click', function(e) {
let target = e.target as HTMLLIElement;
target = findUpTag(target, 'LI');
let index = whichChild(target);
let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable
/* if(menuScroll) {
menuScroll.container.scrollLeft = target.scrollWidth * index;
}
console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect());
*/
/* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись!
scroll.container.scrollTop = y;
scroll.onAddedBottom = () => {};
}; */
scroll.container.scrollTop = y;
setTimeout(() => {
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
}, 100);
/* window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP);
});
}); */
});
};
let emoticonsContentOnScroll = (menu: HTMLUListElement, heights: number[], prevCategoryIndex: number, scroll: HTMLDivElement, menuScroll?: Scrollable) => {
let y = scroll.scrollTop;
//console.log(heights, y);
for(let i = 0; i < heights.length; ++i) {
let height = heights[i];
if(y < height) {
menu.children[prevCategoryIndex].classList.remove('active');
prevCategoryIndex = i/* + 1 */;
menu.children[prevCategoryIndex].classList.add('active');
if(menuScroll) {
if(i < heights.length - 4) {
menuScroll.container.scrollLeft = (i - 3) * 47;
} else {
menuScroll.container.scrollLeft = i * 47;
}
}
break;
}
}
return prevCategoryIndex;
};
{
const categories = ["Smileys & Emotion", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", /* "Symbols", */"Flags", "Skin Tones"];
let divs: {
[category: string]: HTMLDivElement
} = {};
let sorted: {
[category: string]: string[]
} = {};
for(let emoji in Config.Emoji) {
let details = Config.Emoji[emoji];
let i = '' + details;
let category = categories[+i[0] - 1];
if(!category) continue; // maybe it's skin tones
if(!sorted[category]) sorted[category] = [];
sorted[category][+i.slice(1) || 0] = emoji;
}
console.log('emoticons sorted:', sorted);
//Object.keys(sorted).forEach(c => sorted[c].sort((a, b) => a - b));
categories.pop();
delete sorted["Skin Tones"];
//console.time('emojiParse');
for(let category in sorted) {
let div = document.createElement('div');
div.classList.add('emoji-category');
let titleDiv = document.createElement('div');
titleDiv.classList.add('category-title');
titleDiv.innerText = category;
let itemsDiv = document.createElement('div');
itemsDiv.classList.add('category-items');
div.append(titleDiv, itemsDiv);
let emojis = sorted[category];
emojis.forEach(emoji => {
//let emoji = details.unified;
//let emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
let spanEmoji = document.createElement('span');
let kek = RichTextProcessor.wrapRichText(emoji);
if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji));
return;
}
//console.log(kek);
spanEmoji.innerHTML = kek;
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
//spanEmoji.setAttribute('emoji', emoji);
itemsDiv.appendChild(spanEmoji);
});
divs[category] = div;
}
//console.timeEnd('emojiParse');
let contentEmojiDiv = document.getElementById('content-emoji') as HTMLDivElement;
let heights: number[] = [0];
let prevCategoryIndex = 1;
let menu = contentEmojiDiv.previousElementSibling.firstElementChild as HTMLUListElement;
let emojiScroll = new Scrollable(contentEmojiDiv, 'y', 'EMOJI', null);
emojiScroll.container.addEventListener('scroll', (e) => {
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container);
});
//emojiScroll.setVirtualContainer(emojiScroll.container);
categories.map(category => {
let div = divs[category];
if(!div) {
console.error('no div by category:', category);
}
emojiScroll.append(div);
return div;
}).forEach(div => {
//console.log('emoji heights push: ', (heights[heights.length - 1] || 0) + div.scrollHeight, div, div.scrollHeight);
heights.push((heights[heights.length - 1] || 0) + div.scrollHeight);
});
contentEmojiDiv.addEventListener('click', function(e) {
let target = e.target as any;
//if(target.tagName != 'SPAN') return;
if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) {
target = target.firstElementChild;
} else if(target.tagName == 'DIV') return;
//console.log('contentEmoji div', target);
/* if(!target.classList.contains('emoji')) {
target = target.parentElement as HTMLSpanElement;
if(!target.classList.contains('emoji')) {
return;
}
} */
//messageInput.innerHTML += target.innerHTML;
messageInput.innerHTML += target.outerHTML;
btnSend.classList.add('tgico-send');
btnSend.classList.remove('tgico-microphone2');
});
emoticonsMenuOnClick(menu, heights, emojiScroll);
}
let onMediaClick = (e: MouseEvent) => {
let target = e.target as HTMLDivElement;
target = findUpTag(target, 'DIV');
let fileID = target.dataset.docID;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
dropdown.classList.remove('active');
toggleEl.classList.remove('active');
} else {
console.warn('got no doc by id:', fileID);
}
};
let stickersInit = () => {
let contentStickersDiv = document.getElementById('content-stickers') as HTMLDivElement;
//let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement;
let menuWrapper = contentStickersDiv.previousElementSibling as HTMLDivElement;
let menu = menuWrapper.firstElementChild.firstElementChild as HTMLUListElement;
let menuScroll = new Scrollable(menuWrapper, 'x');
let stickersDiv = document.createElement('div');
stickersDiv.classList.add('stickers-categories');
contentStickersDiv.append(stickersDiv);
/* stickersDiv.addEventListener('mouseover', (e) => {
let target = e.target as HTMLElement;
if(target.tagName == 'CANVAS') { // turn on sticker
let animation = lottieLoader.getAnimation(target.parentElement, EMOTICONSSTICKERGROUP);
if(animation) {
// @ts-ignore
if(animation.currentFrame == animation.totalFrames - 1) {
animation.goToAndPlay(0, true);
} else {
animation.play();
}
}
}
}); */
stickersDiv.addEventListener('click', onMediaClick);
let heights: number[] = [];
let heightRAF = 0;
let categoryPush = (categoryDiv: HTMLDivElement, categoryTitle: string, docs: MTDocument[], prepend?: boolean) => {
//if((docs.length % 5) != 0) categoryDiv.classList.add('not-full');
let itemsDiv = document.createElement('div');
itemsDiv.classList.add('category-items');
let titleDiv = document.createElement('div');
titleDiv.classList.add('category-title');
titleDiv.innerText = categoryTitle;
categoryDiv.append(titleDiv, itemsDiv);
docs.forEach(doc => {
let div = document.createElement('div');
wrapSticker({
doc,
div,
lazyLoadQueue,
group: EMOTICONSSTICKERGROUP,
onlyThumb: true
});
itemsDiv.append(div);
});
if(prepend) stickersScroll.prepend(categoryDiv);
else stickersScroll.append(categoryDiv);
/* let scrollHeight = categoryDiv.scrollHeight;
let prevHeight = heights[heights.length - 1] || 0;
//console.log('scrollHeight', scrollHeight, categoryDiv, stickersDiv.childElementCount);
if(prepend && heights.length) {// all stickers loaded faster than recent
heights.forEach((h, i) => heights[i] += scrollHeight);
return heights.unshift(scrollHeight) - 1;
} */
if(heightRAF) window.cancelAnimationFrame(heightRAF);
heightRAF = window.requestAnimationFrame(() => {
heightRAF = 0;
let paddingTop = parseInt(window.getComputedStyle(stickersScroll.container).getPropertyValue('padding-top')) || 0;
heights.length = 0;
/* let concated = stickersScroll.hiddenElements.up.concat(stickersScroll.visibleElements, stickersScroll.hiddenElements.down);
concated.forEach((el, i) => {
heights[i] = (heights[i - 1] || 0) + el.height + (i == 0 ? paddingTop : 0);
}); */
let concated = Array.from(stickersScroll.splitUp.children) as HTMLElement[];
concated.forEach((el, i) => {
heights[i] = (heights[i - 1] || 0) + el.scrollHeight + (i == 0 ? paddingTop : 0);
});
//console.log('stickers concated', concated, heights);
});
/* Array.from(stickersDiv.children).forEach((div, i) => {
heights[i] = (heights[i - 1] || 0) + div.scrollHeight;
}); */
//stickersScroll.onScroll();
//return heights.push(prevHeight + scrollHeight) - 1;
};
let prevCategoryIndex = 0;
let stickersScroll = new Scrollable(contentStickersDiv, 'y', 'STICKERS', undefined, undefined, 2);
stickersScroll.container.addEventListener('scroll', (e) => {
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll);
});
stickersScroll.setVirtualContainer(stickersDiv);
emoticonsMenuOnClick(menu, heights, stickersScroll, menuScroll);
stickersInit = null;
Promise.all([
appStickersManager.getRecentStickers().then(stickers => {
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
//stickersScroll.prepend(categoryDiv);
categoryPush(categoryDiv, 'Recent', stickers.stickers, true);
}),
apiManager.invokeApi('messages.getAllStickers', {hash: 0}).then(async(res) => {
let stickers: {
_: 'messages.allStickers',
hash: number,
sets: Array<MTStickerSet>
} = res as any;
for(let set of stickers.sets) {
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
let li = document.createElement('li');
li.classList.add('btn-icon');
menu.append(li);
//stickersScroll.append(categoryDiv);
let stickerSet = await appStickersManager.getStickerSet(set);
//console.log('got stickerSet', stickerSet, li);
if(stickerSet.set.thumb) {
appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => {
//console.log('setting thumb', stickerSet, blob);
if(stickerSet.set.pFlags.animated) { // means animated
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
// @ts-ignore
const text = e.srcElement.result;
let json = await apiManager.gzipUncompress<string>(text, true);
let animation = await lottieLoader.loadAnimationWorker({
container: li,
loop: true,
autoplay: false,
animationData: JSON.parse(json),
width: 40,
height: 40
}, EMOTICONSSTICKERGROUP);
});
reader.readAsArrayBuffer(blob);
} else {
let image = new Image();
renderImageFromUrl(image, URL.createObjectURL(blob));
li.append(image);
}
});
} else { // as thumb will be used first sticker
wrapSticker({
doc: stickerSet.documents[0],
div: li as any,
group: EMOTICONSSTICKERGROUP
}); // kostil
}
categoryPush(categoryDiv, stickerSet.set.title, stickerSet.documents, false);
}
})
]);
};
let gifsInit = () => {
let contentDiv = document.getElementById('content-gifs') as HTMLDivElement;
let masonry = contentDiv.firstElementChild as HTMLDivElement;
masonry.addEventListener('click', onMediaClick);
let scroll = new Scrollable(contentDiv, 'y', 'GIFS', null);
let width = 400;
let maxSingleWidth = width - 100;
let height = 100;
apiManager.invokeApi('messages.getSavedGifs', {hash: 0}).then((_res) => {
let res = _res as {
_: 'messages.savedGifs',
gifs: MTDocument[],
hash: number
};
console.log('getSavedGifs res:', res);
let line: MTDocument[] = [];
let wastedWidth = 0;
res.gifs.forEach((gif, idx) => {
res.gifs[idx] = appDocsManager.saveDoc(gif);
});
for(let i = 0, length = res.gifs.length; i < length;) {
let gif = res.gifs[i];
let gifWidth = gif.w;
let gifHeight = gif.h;
if(gifHeight < height) {
gifWidth = height / gifHeight * gifWidth;
gifHeight = height;
}
let willUseWidth = Math.min(maxSingleWidth, width - wastedWidth, gifWidth);
let {w, h} = calcImageInBox(gifWidth, gifHeight, willUseWidth, height);
/* wastedWidth += w;
if(wastedWidth == width || h < height) {
wastedWidth = 0;
console.log('completed line', i, line);
line = [];
continue;
}
line.push(gif); */
++i;
console.log('gif:', gif, w, h);
let div = document.createElement('div');
div.style.width = w + 'px';
//div.style.height = h + 'px';
div.dataset.docID = gif.id;
masonry.append(div);
let preloader = new ProgressivePreloader(div);
lazyLoadQueue.push({
div,
load: () => {
let promise = appDocsManager.downloadDoc(gif);
preloader.attach(div, true, promise);
promise.then(blob => {
preloader.detach();
div.innerHTML = `<video autoplay="true" muted="true" loop="true" src="${gif.url}" type="video/mp4"></video>`;
});
return promise;
}
});
}
});
gifsInit = undefined;
};
return {dropdown, lazyLoadQueue};
};
export default initEmoticonsDropdown;

46
src/components/lazyLoadQueue.ts

@ -8,8 +8,7 @@ type LazyLoadElement = { @@ -8,8 +8,7 @@ type LazyLoadElement = {
export default class LazyLoadQueue {
private lazyLoadMedia: Array<LazyLoadElement> = [];
private loadingMedia = 0;
private tempID = 0;
private inProcess: Array<LazyLoadElement> = [];
private lockPromise: Promise<void> = null;
private unlockResolve: () => void = null;
@ -25,28 +24,32 @@ export default class LazyLoadQueue { @@ -25,28 +24,32 @@ export default class LazyLoadQueue {
this.observer = new IntersectionObserver(entries => {
if(this.lockPromise) return;
for(const entry of entries) {
if(entry.isIntersecting) {
const target = entry.target as HTMLElement;
const intersecting = entries.filter(entry => entry.isIntersecting);
intersecting.forEachReverse(entry => {
const target = entry.target as HTMLElement;
this.log('isIntersecting', target);
this.log('isIntersecting', target);
// need for set element first if scrolled
const item = this.lazyLoadMedia.findAndSplice(i => i.div == target);
if(item) {
item.wasSeen = true;
this.lazyLoadMedia.unshift(item);
this.processQueue(item);
}
// need for set element first if scrolled
const item = this.lazyLoadMedia.findAndSplice(i => i.div == target);
if(item) {
item.wasSeen = true;
this.lazyLoadMedia.unshift(item);
//this.processQueue(item);
}
});
if(intersecting.length) {
this.processQueue();
}
});
}
public clear() {
this.tempID--;
this.lazyLoadMedia.length = 0;
this.loadingMedia = 0;
for(let item of this.inProcess) {
this.lazyLoadMedia.push(item);
}
if(this.observer) {
this.observer.disconnect();
@ -54,7 +57,7 @@ export default class LazyLoadQueue { @@ -54,7 +57,7 @@ export default class LazyLoadQueue {
}
public length() {
return this.lazyLoadMedia.length + this.loadingMedia;
return this.lazyLoadMedia.length + this.inProcess.length;
}
public lock() {
@ -72,7 +75,7 @@ export default class LazyLoadQueue { @@ -72,7 +75,7 @@ export default class LazyLoadQueue {
}
public async processQueue(item?: LazyLoadElement) {
if(this.parallelLimit > 0 && this.loadingMedia >= this.parallelLimit) return;
if(this.parallelLimit > 0 && this.inProcess.length >= this.parallelLimit) return;
if(item) {
this.lazyLoadMedia.findAndSplice(i => i == item);
@ -81,9 +84,7 @@ export default class LazyLoadQueue { @@ -81,9 +84,7 @@ export default class LazyLoadQueue {
}
if(item) {
this.loadingMedia++;
let tempID = this.tempID;
this.inProcess.push(item);
this.log('will load media', this.lockPromise, item);
@ -95,15 +96,14 @@ export default class LazyLoadQueue { @@ -95,15 +96,14 @@ export default class LazyLoadQueue {
this.log('waited lock:', performance.now() - perf);
}
//await new Promise((resolve) => setTimeout(resolve, 2e3));
//await new Promise((resolve, reject) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
await item.load();
} catch(err) {
this.log.error('loadMediaQueue error:', err, item);
}
if(tempID == this.tempID) {
this.loadingMedia--;
}
this.inProcess.findAndSplice(i => i == item);
this.log('loaded media', item);

13
src/components/misc.ts

@ -18,7 +18,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool @@ -18,7 +18,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
let clickID = rippleClickID++;
console.log('ripple drawRipple');
//console.log('ripple drawRipple');
handler = () => {
let elapsedTime = Date.now() - startTime;
@ -328,8 +328,11 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? @@ -328,8 +328,11 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
if(activeStripe) {
const tabsRect = tabs.getBoundingClientRect();
const textRect = target.firstElementChild.getBoundingClientRect();
activeStripe.style.cssText = `width: ${textRect.width + (2 * 2)}px; transform: translateX(${textRect.left - tabsRect.left}px);`;
const targetRect = target.getBoundingClientRect();
const width = 50;
activeStripe.style.cssText = `width: ${width}px; transform: translateX(${targetRect.left - tabsRect.left + ((targetRect.width - width) / 2)}px);`;
/* const textRect = target.firstElementChild.getBoundingClientRect();
activeStripe.style.cssText = `width: ${textRect.width + (2 * 2)}px; transform: translateX(${textRect.left - tabsRect.left}px);`; */
//activeStripe.style.transform = `scaleX(${textRect.width}) translateX(${(textRect.left - tabsRect.left) / textRect.width + 0.5}px)`;
//console.log('tabs click:', tabsRect, textRect);
}
@ -362,9 +365,9 @@ export function formatPhoneNumber(str: string) { @@ -362,9 +365,9 @@ export function formatPhoneNumber(str: string) {
}
});
if(country.pattern) {
/* if(country.pattern) {
str = str.slice(0, country.pattern.length);
}
} */
}
return {formatted: str, country};

403
src/components/poll.ts

@ -1,7 +1,11 @@ @@ -1,7 +1,11 @@
import appPollsManager, { PollResults, Poll } from "../lib/appManagers/appPollsManager";
import { RichTextProcessor } from "../lib/richtextprocessor";
import { findUpClassName, $rootScope } from "../lib/utils";
import { mediaSizes } from "../lib/config";
import { findUpClassName, $rootScope, cancelEvent } from "../lib/utils";
import { mediaSizes, touchSupport } from "../lib/config";
import { ripple } from "./misc";
import appSidebarRight from "../lib/appManagers/appSidebarRight";
import appImManager from "../lib/appManagers/appImManager";
import serverTimeManager from "../lib/mtproto/serverTimeManager";
let lineTotalLength = 0;
const tailLength = 9;
@ -9,13 +13,13 @@ const times = 10; @@ -9,13 +13,13 @@ const times = 10;
const fullTime = 340;
const oneTime = fullTime / times;
let roundPercents = (percents: number[]) => {
export const roundPercents = (percents: number[]) => {
//console.log('roundPercents before percents:', percents);
let sum = percents.reduce((acc, p) => acc + Math.round(p), 0);
const sum = percents.reduce((acc, p) => acc + Math.round(p), 0);
if(sum > 100) {
let diff = sum - 100;
let length = percents.length;
const diff = sum - 100;
const length = percents.length;
for(let i = 0; i < diff; ++i) {
let minIndex = -1, minRemainder = 1;
for(let k = 0; k < length; ++k) {
@ -27,14 +31,15 @@ let roundPercents = (percents: number[]) => { @@ -27,14 +31,15 @@ let roundPercents = (percents: number[]) => {
}
if(minIndex == -1) {
throw new Error('lol chto');
//throw new Error('lol chto');
return;
}
percents[minIndex] -= minRemainder;
}
} else if(sum < 100) {
let diff = 100 - sum;
let length = percents.length;
const diff = 100 - sum;
const length = percents.length;
for(let i = 0; i < diff; ++i) {
let minIndex = -1, maxRemainder = 0;
for(let k = 0; k < length; ++k) {
@ -46,7 +51,8 @@ let roundPercents = (percents: number[]) => { @@ -46,7 +51,8 @@ let roundPercents = (percents: number[]) => {
}
if(minIndex == -1) {
throw new Error('lol chto');
//throw new Error('lol chto');
return;
}
percents[minIndex] += 1 - maxRemainder;
@ -58,32 +64,103 @@ let roundPercents = (percents: number[]) => { @@ -58,32 +64,103 @@ let roundPercents = (percents: number[]) => {
const connectedPolls: {id: string, element: PollElement}[] = [];
$rootScope.$on('poll_update', (e: CustomEvent) => {
let {poll, results} = e.detail as {poll: Poll, results: PollResults};
const {poll, results} = e.detail as {poll: Poll, results: PollResults};
for(let connected of connectedPolls) {
//console.log('poll_update', poll, results);
for(const connected of connectedPolls) {
if(connected.id == poll.id) {
let pollElement = connected.element;
pollElement.performResults(results, poll.chosenIndex);
const pollElement = connected.element;
pollElement.isClosed = !!poll.pFlags.closed;
pollElement.performResults(results, poll.chosenIndexes);
}
}
});
$rootScope.$on('peer_changed', () => {
if(prevQuizHint) {
hideQuizHint(prevQuizHint, prevQuizHintOnHide, prevQuizHintTimeout);
}
});
const hideQuizHint = (element: HTMLElement, onHide: () => void, timeout: number) => {
element.classList.remove('active');
clearTimeout(timeout);
setTimeout(() => {
onHide();
element.remove();
if(prevQuizHint == element && prevQuizHintOnHide == onHide && prevQuizHintTimeout == timeout) {
prevQuizHint = prevQuizHintOnHide = null;
prevQuizHintTimeout = 0;
}
}, 200);
};
let prevQuizHint: HTMLElement, prevQuizHintOnHide: () => void, prevQuizHintTimeout: number;
const setQuizHint = (solution: string, solution_entities: any[], onHide: () => void) => {
if(prevQuizHint) {
hideQuizHint(prevQuizHint, prevQuizHintOnHide, prevQuizHintTimeout);
}
const element = document.createElement('div');
element.classList.add('quiz-hint');
const container = document.createElement('div');
container.classList.add('container', 'tgico');
const textEl = document.createElement('div');
textEl.classList.add('text');
container.append(textEl);
element.append(container);
textEl.innerHTML = RichTextProcessor.wrapRichText(solution, {entities: solution_entities});
appImManager.bubblesContainer.append(element);
void element.offsetLeft; // reflow
element.classList.add('active');
prevQuizHint = element;
prevQuizHintOnHide = onHide;
prevQuizHintTimeout = setTimeout(() => {
hideQuizHint(element, onHide, prevQuizHintTimeout);
}, touchSupport ? 5000 : 7000);
};
export default class PollElement extends HTMLElement {
private svgLines: SVGSVGElement[];
private numberDivs: HTMLDivElement[];
private selectedSpan: HTMLSpanElement;
private answerDivs: HTMLDivElement[];
private descDiv: HTMLElement;
private typeDiv: HTMLElement;
private avatarsDiv: HTMLElement;
private viewResults: HTMLElement;
private votersCountDiv: HTMLDivElement;
private maxOffset = -46.5;
private maxLength: number;
private maxLengths: number[];
public isClosed = false;
private isQuiz = false;
private isRetracted = false;
private chosenIndex = -1;
private isPublic = false;
private isMultiple = false;
private chosenIndexes: number[] = [];
private percents: number[];
private pollID: string;
private mid: number;
private quizInterval: number;
private quizTimer: SVGSVGElement;
private sendVoteBtn: HTMLElement;
private chosingIndexes: number[] = [];
private sendVotePromise: Promise<void>;
constructor() {
super();
// элемент создан
@ -98,28 +175,32 @@ export default class PollElement extends HTMLElement { @@ -98,28 +175,32 @@ export default class PollElement extends HTMLElement {
console.log('line total length:', lineTotalLength);
}
let pollID = this.getAttribute('poll-id');
let {poll, results} = appPollsManager.getPoll(pollID);
this.pollID = this.getAttribute('poll-id');
this.mid = +this.getAttribute('message-id');
const {poll, results} = appPollsManager.getPoll(this.pollID);
connectedPolls.push({id: pollID, element: this});
connectedPolls.push({id: this.pollID, element: this});
console.log('pollElement poll:', poll, results);
let desc = '';
if(poll.pFlags) {
if(poll.pFlags.closed) {
this.isPublic = !!poll.pFlags.public_voters;
this.isQuiz = !!poll.pFlags.quiz;
this.isClosed = !!poll.pFlags.closed;
this.isMultiple = !!poll.pFlags.multiple_choice;
if(this.isClosed) {
desc = 'Final results';
this.classList.add('is-closed');
} else {
if(poll.pFlags.quiz) {
this.isQuiz = true;
}
let type = this.isQuiz ? 'Quiz' : 'Poll';
desc = (poll.pFlags.public_voters ? 'Public' : 'Anonymous') + ' ' + type;
desc = (this.isPublic ? '' : 'Anonymous ') + type;
}
}
let votes = poll.answers.map((answer, idx) => {
const multipleSelect = this.isMultiple ? '<span class="poll-answer-selected tgico-check"></span>' : '';
const votes = poll.answers.map((answer, idx) => {
return `
<div class="poll-answer" data-index="${idx}">
<div class="circle-hover">
@ -127,34 +208,127 @@ export default class PollElement extends HTMLElement { @@ -127,34 +208,127 @@ export default class PollElement extends HTMLElement {
<svg class="progress-ring">
<circle class="progress-ring__circle" cx="13" cy="13" r="9"></circle>
</svg>
${multipleSelect}
</div>
<div class="poll-answer-percents"></div>
<div class="poll-answer-text">${RichTextProcessor.wrapEmojiText(answer.text)}</div>
<svg version="1.1" class="poll-line" style="display: none;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${mediaSizes.active.regular.width} 35" xml:space="preserve">
<use href="#poll-line"></use>
</svg>
<span class="poll-answer-selected tgico"></span>
</div>
`;
}).join('');
this.innerHTML = `
<div class="poll-title">${poll.rQuestion}</div>
<div class="poll-desc">${desc}</div>
<div class="poll-desc">
<div class="poll-type">${desc}</div>
<div class="poll-avatars"></div>
</div>
${votes}
<div class="poll-votes-count"></div>
<div class="poll-footer">
<div class="poll-footer-button poll-view-results hide">View Results</div>
<div class="poll-votes-count"></div>
</div>
`;
this.descDiv = this.firstElementChild.nextElementSibling as HTMLElement;
this.typeDiv = this.descDiv.firstElementChild as HTMLElement;
this.avatarsDiv = this.descDiv.lastElementChild as HTMLElement;
if(this.isQuiz) {
this.classList.add('is-quiz');
if(poll.close_period && poll.close_date) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
//svg.setAttributeNS(null, 'viewBox', '0 0 15 15');
svg.classList.add('poll-quiz-timer');
this.quizTimer = svg;
const strokeWidth = 2;
const radius = (15 / 2) - (strokeWidth * 2);
const circumference = 2 * Math.PI * radius;
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.classList.add('poll-quiz-timer-circle');
circle.setAttributeNS(null, 'cx', '15');
circle.setAttributeNS(null, 'cy', '15');
circle.setAttributeNS(null, 'r', '' + radius);
svg.append(circle);
this.descDiv.append(svg);
const period = poll.close_period * 1000;
const closeTime = (poll.close_date - serverTimeManager.serverTimeOffset) * 1000;
this.quizInterval = setInterval(() => {
const time = Date.now();
const totalLength = circle.getTotalLength();
const percents = (closeTime - time) / period;
circle.style.strokeDasharray = '' + (percents * totalLength) + ', ' + circumference;
if(time >= closeTime) {
clearInterval(this.quizInterval);
this.quizInterval = 0;
// нужно запросить апдейт чтобы опрос обновился
appPollsManager.getResults(this.mid);
}
}, 1e3);
}
}
this.answerDivs = Array.from(this.querySelectorAll('.poll-answer')) as HTMLDivElement[];
this.votersCountDiv = this.querySelector('.poll-votes-count') as HTMLDivElement;
this.svgLines = Array.from(this.querySelectorAll('.poll-line')) as SVGSVGElement[];
this.numberDivs = Array.from(this.querySelectorAll('.poll-answer-percents')) as HTMLDivElement[];
let width = this.getBoundingClientRect().width;
const footerDiv = this.lastElementChild;
this.viewResults = footerDiv.firstElementChild as HTMLElement;
this.votersCountDiv = footerDiv.lastElementChild as HTMLDivElement;
this.viewResults.addEventListener('click', (e) => {
cancelEvent(e);
appSidebarRight.pollResultsTab.init(this.pollID, this.mid);
});
ripple(this.viewResults);
if(this.isMultiple) {
this.sendVoteBtn = document.createElement('div');
this.sendVoteBtn.classList.add('poll-footer-button', 'poll-send-vote');
this.sendVoteBtn.innerText = 'Vote';
ripple(this.sendVoteBtn);
if(!poll.chosenIndexes.length) {
this.votersCountDiv.classList.add('hide');
}
this.sendVoteBtn.addEventListener('click', () => {
/* const indexes = this.answerDivs.filter(el => el.classList.contains('is-chosing')).map(el => +el.dataset.index);
if(indexes.length) {
} */
if(this.chosingIndexes.length) {
this.sendVotes(this.chosingIndexes).then(() => {
this.chosingIndexes.length = 0;
this.answerDivs.forEach(el => {
el.classList.remove('is-chosing');
});
});
}
});
footerDiv.append(this.sendVoteBtn);
}
const width = this.getBoundingClientRect().width;
this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left
if(poll.chosenIndex !== -1) {
this.performResults(results, poll.chosenIndex);
} else {
if(poll.chosenIndexes.length || this.isClosed) {
this.performResults(results, poll.chosenIndexes);
} else if(!this.isClosed) {
this.setVotersCount(results);
this.addEventListener('click', this.clickHandler);
}
@ -168,11 +342,16 @@ export default class PollElement extends HTMLElement { @@ -168,11 +342,16 @@ export default class PollElement extends HTMLElement {
}
static get observedAttributes(): string[] {
return [/* массив имён атрибутов для отслеживания их изменений */];
return ['poll-id', 'message-id'/* массив имён атрибутов для отслеживания их изменений */];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
// вызывается при изменении одного из перечисленных выше атрибутов
if(name == 'poll-id') {
this.pollID = newValue;
} else if(name == 'message-id') {
this.mid = +newValue;
}
}
adoptedCallback() {
@ -180,14 +359,45 @@ export default class PollElement extends HTMLElement { @@ -180,14 +359,45 @@ export default class PollElement extends HTMLElement {
// (происходит в document.adoptNode, используется очень редко)
}
initQuizHint(results: PollResults) {
if(results.solution && results.solution_entities) {
const toggleHint = document.createElement('div');
toggleHint.classList.add('tgico-tip', 'poll-hint');
this.descDiv.append(toggleHint);
//let active = false;
toggleHint.addEventListener('click', (e) => {
cancelEvent(e);
//active = true;
toggleHint.classList.add('active');
setQuizHint(results.solution, results.solution_entities, () => {
//active = false;
toggleHint.classList.remove('active');
});
});
}
}
clickHandler(e: MouseEvent) {
let target = findUpClassName(e.target, 'poll-answer') as HTMLElement;
const target = findUpClassName(e.target, 'poll-answer') as HTMLElement;
if(!target) {
return;
}
let answerIndex = +target.dataset.index;
this.sendVote(answerIndex);
const answerIndex = +target.dataset.index;
if(this.isMultiple) {
target.classList.toggle('is-chosing');
const foundIndex = this.chosingIndexes.indexOf(answerIndex);
if(foundIndex !== -1) {
this.chosingIndexes.splice(foundIndex, 1);
} else {
this.chosingIndexes.push(answerIndex);
}
} else {
this.sendVotes([answerIndex]);
}
/* target.classList.add('is-voting');
setTimeout(() => { // simulate
@ -196,23 +406,57 @@ export default class PollElement extends HTMLElement { @@ -196,23 +406,57 @@ export default class PollElement extends HTMLElement {
}, 1000); */
}
sendVote(index: number) {
let target = this.answerDivs[index];
target.classList.add('is-voting');
let mid = +this.getAttribute('message-id');
sendVotes(indexes: number[]) {
if(this.sendVotePromise) return this.sendVotePromise;
const targets = this.answerDivs.filter((_, idx) => indexes.includes(idx));
targets.forEach(target => {
target.classList.add('is-voting');
});
this.classList.add('disable-hover');
appPollsManager.sendVote(mid, [index]).then(() => {
target.classList.remove('is-voting');
return this.sendVotePromise = appPollsManager.sendVote(this.mid, indexes).then(() => {
targets.forEach(target => {
target.classList.remove('is-voting');
});
this.classList.remove('disable-hover');
}).finally(() => {
this.sendVotePromise = null;
});
}
performResults(results: PollResults, chosenIndex: number) {
if(this.chosenIndex != chosenIndex) { // if we voted
this.isRetracted = this.chosenIndex != -1 && chosenIndex == -1;
this.chosenIndex = chosenIndex;
performResults(results: PollResults, chosenIndexes: number[]) {
if(this.isQuiz && (results.results?.length || this.isClosed)) {
this.answerDivs.forEach((el, idx) => {
el.classList.toggle('is-correct', !!results.results[idx].pFlags.correct);
});
if(this.initQuizHint) {
this.initQuizHint(results);
delete this.initQuizHint;
}
if(this.quizInterval) {
clearInterval(this.quizInterval);
this.quizInterval = 0;
}
if(this.quizTimer?.parentElement) {
this.quizTimer.remove();
}
}
if(this.isClosed) {
this.classList.add('is-closed');
this.typeDiv.innerText = 'Final results';
}
// set chosen
if(this.chosenIndexes.length != chosenIndexes.length || this.isClosed) { // if we voted
this.isRetracted = this.chosenIndexes.length && !chosenIndexes.length;
this.chosenIndexes = chosenIndexes.slice();
if(this.isRetracted) {
this.addEventListener('click', this.clickHandler);
} else {
@ -221,29 +465,56 @@ export default class PollElement extends HTMLElement { @@ -221,29 +465,56 @@ export default class PollElement extends HTMLElement {
}
// is need update
if(this.chosenIndex != -1 || this.isRetracted) {
const percents = results.results.map(v => v.voters / results.total_voters * 100);
this.setResults(this.isRetracted ? this.percents : percents, chosenIndex);
if(this.chosenIndexes.length || this.isRetracted || this.isClosed) {
const percents = results.results.map(v => results.total_voters ? v.voters / results.total_voters * 100 : 0);
this.setResults(this.isRetracted ? this.percents : percents, this.chosenIndexes);
this.percents = percents;
this.isRetracted = false;
}
this.setVotersCount(results);
}
setResults(percents: number[], chosenIndex: number) {
this.svgLines.forEach(svg => svg.style.display = '');
if(this.isPublic) {
if(!this.isMultiple) {
this.viewResults.classList.toggle('hide', !results.total_voters || !this.chosenIndexes.length);
this.votersCountDiv.classList.toggle('hide', !!this.chosenIndexes.length);
}
let html = '';
/**
* MACOS, ANDROID - без реверса
* WINDOWS DESKTOP - реверс
* все приложения накладывают аватарку первую на вторую, а в макете зато вторая на первую, ЛОЛ!
*/
results.recent_voters/* .slice().reverse() */.forEach((userID, idx) => {
const style = idx == 0 ? '' : `style="transform: translateX(-${idx * 5}px);"`;
html += `<avatar-element dialog="0" peer="${userID}" ${style}></avatar-element>`;
});
this.avatarsDiv.innerHTML = html;
}
if(chosenIndex !== -1) {
let answerDiv = this.answerDivs[chosenIndex];
if(!this.selectedSpan) {
this.selectedSpan = document.createElement('span');
this.selectedSpan.classList.add('poll-answer-selected', 'tgico-check');
if(this.isMultiple) {
this.sendVoteBtn.classList.toggle('hide', !!this.chosenIndexes.length);
if(!this.chosenIndexes.length) {
this.votersCountDiv.classList.add('hide');
this.viewResults.classList.add('hide');
} else if(this.isPublic) {
this.viewResults.classList.toggle('hide', !results.total_voters || !this.chosenIndexes.length);
this.votersCountDiv.classList.toggle('hide', !!this.chosenIndexes.length);
} else {
this.votersCountDiv.classList.toggle('hide', !this.chosenIndexes.length);
}
answerDiv.append(this.selectedSpan);
}
}
setResults(percents: number[], chosenIndexes: number[]) {
this.svgLines.forEach(svg => svg.style.display = '');
this.answerDivs.forEach((el, idx) => {
el.classList.toggle('is-chosen', chosenIndexes.includes(idx));
});
let maxValue = Math.max(...percents);
const maxValue = Math.max(...percents);
this.maxLengths = percents.map(p => p / maxValue * this.maxLength);
// line
@ -265,7 +536,7 @@ export default class PollElement extends HTMLElement { @@ -265,7 +536,7 @@ export default class PollElement extends HTMLElement {
for(let i = (times - 1), k = 0; i >= 0; --i, ++k) {
setTimeout(() => {
percents.forEach((percents, idx) => {
let value = Math.round(percents / times * i);
const value = Math.round(percents / times * i);
this.numberDivs[idx].innerText = value + '%';
});
}, oneTime * k);
@ -274,7 +545,7 @@ export default class PollElement extends HTMLElement { @@ -274,7 +545,7 @@ export default class PollElement extends HTMLElement {
for(let i = 0; i < times; ++i) {
setTimeout(() => {
percents.forEach((percents, idx) => {
let value = Math.round(percents / times * (i + 1));
const value = Math.round(percents / times * (i + 1));
this.numberDivs[idx].innerText = value + '%';
});
}, oneTime * i);
@ -294,14 +565,14 @@ export default class PollElement extends HTMLElement { @@ -294,14 +565,14 @@ export default class PollElement extends HTMLElement {
}
setVotersCount(results: PollResults) {
let votersCount = results.total_voters || 0;
let votersOrAnswers = this.isQuiz ? (votersCount > 1 || !votersCount ? 'answers' : 'answer') : (votersCount > 1 || !votersCount ? 'votes' : 'vote');
const votersCount = results.total_voters || 0;
const votersOrAnswers = this.isQuiz ? (votersCount > 1 || !votersCount ? 'answers' : 'answer') : (votersCount > 1 || !votersCount ? 'votes' : 'vote');
this.votersCountDiv.innerText = `${results.total_voters ? results.total_voters + ' ' + votersOrAnswers : 'No ' + votersOrAnswers}`;
}
setLineProgress(index: number, percents: number) {
let svg = this.svgLines[index];
const svg = this.svgLines[index];
if(percents == -1) {
svg.style.strokeDasharray = '';

35
src/components/popup.ts

@ -6,8 +6,14 @@ export class PopupElement { @@ -6,8 +6,14 @@ export class PopupElement {
protected container = document.createElement('div');
protected header = document.createElement('div');
protected title = document.createElement('div');
protected closeBtn: HTMLElement;
protected confirmBtn: HTMLElement;
protected body: HTMLElement;
constructor(className: string, buttons?: Array<PopupButton>) {
protected onClose: () => void;
protected onCloseAfterTimeout: () => void;
constructor(className: string, buttons?: Array<PopupButton>, options: Partial<{closable: boolean, withConfirm: string, body: boolean}> = {}) {
this.element.classList.add('popup');
this.element.className = 'popup' + (className ? ' ' + className : '');
this.container.classList.add('popup-container', 'z-depth-1');
@ -16,7 +22,32 @@ export class PopupElement { @@ -16,7 +22,32 @@ export class PopupElement {
this.title.classList.add('popup-title');
this.header.append(this.title);
if(options.closable) {
this.closeBtn = document.createElement('span');
this.closeBtn.classList.add('btn-icon', 'popup-close', 'tgico-close');
ripple(this.closeBtn);
this.header.prepend(this.closeBtn);
this.closeBtn.addEventListener('click', () => {
this.destroy();
}, {once: true});
}
if(options.withConfirm) {
this.confirmBtn = document.createElement('button');
this.confirmBtn.classList.add('btn-primary');
this.confirmBtn.innerText = options.withConfirm;
this.header.append(this.confirmBtn);
ripple(this.confirmBtn);
}
this.container.append(this.header);
if(options.body) {
this.body = document.createElement('div');
this.body.classList.add('popup-body');
this.container.append(this.body);
}
if(buttons && buttons.length) {
const buttonsDiv = document.createElement('div');
@ -56,9 +87,11 @@ export class PopupElement { @@ -56,9 +87,11 @@ export class PopupElement {
}
public destroy() {
this.onClose && this.onClose();
this.element.classList.remove('active');
setTimeout(() => {
this.element.remove();
this.onCloseAfterTimeout && this.onCloseAfterTimeout();
}, 1000);
}
}

134
src/components/popupCreatePoll.ts

@ -0,0 +1,134 @@ @@ -0,0 +1,134 @@
import { PopupElement } from "./popup";
import Scrollable from "./scrollable_new";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import { $rootScope } from "../lib/utils";
import { Poll } from "../lib/appManagers/appPollsManager";
import { nextRandomInt, bigint } from "../lib/bin_utils";
import { toast } from "./misc";
const InputField = (placeholder: string, label: string, name: string) => {
const div = document.createElement('div');
div.classList.add('input-field');
div.innerHTML = `
<input type="text" name="${name}" id="input-${name}" placeholder="${placeholder}" autocomplete="off" required="">
<label for="input-${name}">${label}</label>
`;
return div;
};
export default class PopupCreatePoll extends PopupElement {
private questionInput: HTMLInputElement;
private questions: HTMLElement;
private scrollable: Scrollable;
private tempID = 0;
constructor() {
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true});
this.title.innerText = 'New Poll';
const questionField = InputField('Ask a question', 'Ask a question', 'question');
this.questionInput = questionField.firstElementChild as HTMLInputElement;
this.header.append(questionField);
const d = document.createElement('div');
d.innerText = 'Options';
this.questions = document.createElement('div');
this.questions.classList.add('poll-create-questions');
this.body.append(d, this.questions);
this.confirmBtn.addEventListener('click', this.onSubmitClick);
this.scrollable = new Scrollable(this.body, 'y', undefined);
this.appendMoreField();
}
onSubmitClick = (e: MouseEvent) => {
const question = this.questionInput.value;
if(!question.trim()) {
toast('Please enter a question');
return;
}
const answers = Array.from(this.questions.children).map((el, idx) => {
const input = (el.firstElementChild as HTMLInputElement);
return input.value;
}).filter(v => !!v.trim());
if(answers.length < 2) {
toast('Please enter at least two options');
return;
}
this.closeBtn.click();
this.confirmBtn.removeEventListener('click', this.onSubmitClick);
//const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
//const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString();
const poll: Partial<Poll> = {};
poll._ = 'poll';
//poll.id = randomIDS;
poll.flags = 0;
poll.question = question;
poll.answers = answers.map((value, idx) => {
return {
_: 'pollAnswer',
text: value,
option: new Uint8Array([idx])
};
});
appMessagesManager.sendOther($rootScope.selectedPeerID, {
_: 'inputMediaPoll',
flags: 0,
poll
});
};
onInput = (e: Event) => {
const target = e.target as HTMLInputElement;
if(target.value.length) {
target.parentElement.classList.add('is-filled');
}
const isLast = !target.parentElement.nextElementSibling;
if(isLast && target.value.length && this.questions.childElementCount < 10) {
this.appendMoreField();
}
};
onDeleteClick = (e: MouseEvent) => {
const target = e.target as HTMLSpanElement;
target.parentElement.remove();
Array.from(this.questions.children).forEach((el, idx) => {
const label = el.firstElementChild.nextElementSibling as HTMLLabelElement;
label.innerText = 'Option ' + (idx + 1);
});
};
private appendMoreField() {
const idx = this.questions.childElementCount + 1;
const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + this.tempID++);
(questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput);
const deleteBtn = document.createElement('span');
deleteBtn.classList.add('btn-icon', 'tgico-close');
questionField.append(deleteBtn);
deleteBtn.addEventListener('click', this.onDeleteClick, {once: true});
this.questions.append(questionField);
this.scrollable.scrollTo(this.scrollable.scrollHeight, true, true);
}
}

39
src/components/popupStickers.ts

@ -4,17 +4,15 @@ import { RichTextProcessor } from "../lib/richtextprocessor"; @@ -4,17 +4,15 @@ import { RichTextProcessor } from "../lib/richtextprocessor";
import Scrollable from "./scrollable_new";
import { wrapSticker } from "./wrappers";
import LazyLoadQueue from "./lazyLoadQueue";
import { putPreloader, ripple } from "./misc";
import { putPreloader } from "./misc";
import animationIntersector from "./animationIntersector";
import { findUpClassName } from "../lib/utils";
import appDocsManager from "../lib/appManagers/appDocsManager";
import appImManager from "../lib/appManagers/appImManager";
export default class PopupStickers extends PopupElement {
private stickersFooter: HTMLElement;
private stickersDiv: HTMLElement;
private h6: HTMLElement;
private closeBtn: HTMLElement;
private set: MTStickerSet;
@ -23,30 +21,23 @@ export default class PopupStickers extends PopupElement { @@ -23,30 +21,23 @@ export default class PopupStickers extends PopupElement {
id: string,
access_hash: string
}) {
super('popup-stickers');
super('popup-stickers', null, {closable: true, body: true});
this.h6 = document.createElement('h6');
this.h6.innerText = 'Loading...';
const popupBody = document.createElement('div');
popupBody.classList.add('popup-body');
this.header.append(this.h6);
this.closeBtn = document.createElement('span');
this.closeBtn.classList.add('btn-icon', 'popup-close', 'tgico-close');
this.header.append(this.closeBtn, this.h6);
this.closeBtn.addEventListener('click', () => {
this.destroy();
this.onClose = () => {
animationIntersector.checkAnimations(false);
this.stickersFooter.removeEventListener('click', this.onFooterClick);
this.stickersDiv.removeEventListener('click', this.onStickersClick);
this.element.removeEventListener('click', onOverlayClick);
};
setTimeout(() => {
animationIntersector.checkAnimations(undefined, 'STICKERS-POPUP');
}, 1001);
}, {once: true});
ripple(this.closeBtn);
this.onCloseAfterTimeout = () => {
animationIntersector.checkAnimations(undefined, 'STICKERS-POPUP');
};
const onOverlayClick = (e: MouseEvent) => {
if(!findUpClassName(e.target, 'popup-container')) {
@ -71,12 +62,9 @@ export default class PopupStickers extends PopupElement { @@ -71,12 +62,9 @@ export default class PopupStickers extends PopupElement {
this.stickersFooter.innerText = 'Loading...';
popupBody.append(div);
this.container.append(popupBody);
const scrollable = new Scrollable(popupBody, 'y', undefined);
popupBody.append(this.stickersFooter);
this.body.append(div);
const scrollable = new Scrollable(this.body, 'y', undefined);
this.body.append(this.stickersFooter);
// const editButton = document.createElement('button');
// editButton.classList.add('btn-primary');
@ -111,11 +99,6 @@ export default class PopupStickers extends PopupElement { @@ -111,11 +99,6 @@ export default class PopupStickers extends PopupElement {
private loadStickerSet() {
return appStickersManager.getStickerSet(this.stickerSetInput).then(set => {
//console.log('PopupStickers loadStickerSet got set:', set);
//JOPA SGORELA SUKA, kak zdes mojet bit false esli u menya etot nabor dobavlen i otobrajaetsya v telege ???
//koroche sut v tom, chto esli ti dobavil nabor a potom ctrl + f5 , i najimaesh na popup stikera v chate, to ono dumaet chto nabora net, potomu chto installed_date kakogoto huya false, i ego pravda tam net.
//razberis brat.. a tak vrode vse rabotaet namana. ya gavnokoder i gavnoverstker
//testiroval na stikere v dialoge viti (zeleniy dinozavr) http://i.piccy.info/i9/71cbc718bedb6d8a33da9bff775d8316/1591669743/204119/1382638/Snymok_ekrana_2020_06_09_v_05_28_25.jpg
//console.log('hasOwnProperty got set installed_date ????', set.set.hasOwnProperty('installed_date'));
this.set = set.set;

5
src/components/preloader.ts

@ -58,9 +58,14 @@ export default class ProgressivePreloader { @@ -58,9 +58,14 @@ export default class ProgressivePreloader {
this.promise = promise = null;
}
};
//promise.catch(onEnd);
promise.finally(onEnd);
promise.notify = (details: {done: number, total: number}) => {
/* if(details.done >= details.total) {
onEnd();
} */
if(tempID != this.tempID) return;
//console.log('preloader download', promise, details);

63
src/components/scrollable_new.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { logger, LogLevels } from "../lib/polyfill";
import smoothscroll from '../lib/smoothscroll';
import { touchSupport, isSafari } from "../lib/config";
//import { isInDOM } from "../lib/utils";
(window as any).__forceSmoothScrollPolyfill__ = true;
smoothscroll.polyfill();
@ -103,22 +104,22 @@ export default class Scrollable { @@ -103,22 +104,22 @@ export default class Scrollable {
this.visible = new Set();
this.observer = new IntersectionObserver(entries => {
let filtered = entries.filter(entry => entry.isIntersecting);
const filtered = entries.filter(entry => entry.isIntersecting);
//return;
//this.log('entries:', entries);
entries.forEach(entry => {
let target = entry.target as HTMLElement;
const target = entry.target as HTMLElement;
if(entry.isIntersecting) {
this.setVisible(target);
this.log.debug('intersection entry:', entry, this.lastTopID, this.lastBottomID);
} else {
let id = +target.dataset.virtual;
let isTop = entry.boundingClientRect.top < 0;
const id = +target.dataset.virtual;
const isTop = entry.boundingClientRect.top < 0;
if(isTop) {
this.lastTopID = id + 1;
@ -159,10 +160,10 @@ export default class Scrollable { @@ -159,10 +160,10 @@ export default class Scrollable {
this.log.debug('entries:', entries, filtered, this.lastScrollDirection, this.lastTopID, this.lastBottomID);
let minVisibleID = this.lastTopID - this.splitCount;
let maxVisibleID = this.lastBottomID + this.splitCount;
for(let target of this.visible) {
let id = +target.dataset.virtual;
const minVisibleID = this.lastTopID - this.splitCount;
const maxVisibleID = this.lastBottomID + this.splitCount;
for(const target of this.visible) {
const id = +target.dataset.virtual;
if(id < minVisibleID || id > maxVisibleID) {
this.setHidden(target);
}
@ -178,9 +179,9 @@ export default class Scrollable { @@ -178,9 +179,9 @@ export default class Scrollable {
if(axis == 'x') {
this.container.classList.add('scrollable-x');
let scrollHorizontally = (e: any) => {
const scrollHorizontally = (e: any) => {
e = window.event || e;
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
const delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
this.container.scrollLeft -= (delta * 20);
e.preventDefault();
};
@ -217,6 +218,27 @@ export default class Scrollable { @@ -217,6 +218,27 @@ export default class Scrollable {
this.overflowContainer = window.innerWidth <= 720 && false ? document.documentElement : this.container;
if(touchSupport && isSafari) {
let allowUp: boolean, allowDown: boolean, slideBeginY: number;
this.container.addEventListener('touchstart', (event) => {
allowUp = this.container.scrollTop > 0;
allowDown = (this.container.scrollTop < this.container.scrollHeight - this.container.clientHeight);
// @ts-ignore
slideBeginY = event.pageY;
});
this.container.addEventListener('touchmove', (event: any) => {
var up = (event.pageY > slideBeginY);
var down = (event.pageY < slideBeginY);
slideBeginY = event.pageY;
if((up && allowUp) || (down && allowDown)) {
event.stopPropagation();
} else {
event.preventDefault();
}
});
}
/* scrollables.set(this.container, this);
scrollsIntersector.observe(this.container); */
}
@ -323,7 +345,7 @@ export default class Scrollable { @@ -323,7 +345,7 @@ export default class Scrollable {
this.onScrollMeasure = 0;
if(!this.splitUp) return;
let scrollTop = this.overflowContainer.scrollTop;
const scrollTop = this.overflowContainer.scrollTop;
if(this.lastScrollTop != scrollTop) {
this.lastScrollDirection = this.lastScrollTop < scrollTop ? 1 : -1;
this.lastScrollTop = scrollTop;
@ -336,8 +358,8 @@ export default class Scrollable { @@ -336,8 +358,8 @@ export default class Scrollable {
public checkForTriggers(container: HTMLElement) {
if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return;
let scrollTop = container.scrollTop;
let maxScrollTop = container.scrollHeight - container.clientHeight;
const scrollTop = container.scrollTop;
const maxScrollTop = container.scrollHeight - container.clientHeight;
//this.log('checkForTriggers:', scrollTop, maxScrollTop);
@ -359,7 +381,7 @@ export default class Scrollable { @@ -359,7 +381,7 @@ export default class Scrollable {
public updateElement(element: HTMLElement) {
element.style.minHeight = '';
window.requestAnimationFrame(() => {
let height = element.scrollHeight;
const height = element.scrollHeight;
window.requestAnimationFrame(() => {
element.style.minHeight = height + 'px';
@ -368,12 +390,13 @@ export default class Scrollable { @@ -368,12 +390,13 @@ export default class Scrollable {
}
public prepareElement(element: HTMLElement, append = true) {
//return;
element.dataset.virtual = '' + (append ? this.virtualTempIDBottom++ : this.virtualTempIDTop--);
this.log.debug('prepareElement: prepared');
window.requestAnimationFrame(() => {
let {scrollHeight/* , scrollWidth */} = element;
const {scrollHeight/* , scrollWidth */} = element;
this.log.debug('prepareElement: first rAF');
@ -413,7 +436,7 @@ export default class Scrollable { @@ -413,7 +436,7 @@ export default class Scrollable {
public scrollIntoView(element: HTMLElement, smooth = true) {
if(element.parentElement && !this.scrollLocked) {
let isFirstUnread = element.classList.contains('is-first-unread');
const isFirstUnread = element.classList.contains('is-first-unread');
let offsetTop = element.getBoundingClientRect().top - this.container.getBoundingClientRect().top;
offsetTop = this.container.scrollTop + offsetTop;
@ -423,10 +446,10 @@ export default class Scrollable { @@ -423,10 +446,10 @@ export default class Scrollable {
return;
}
let clientHeight = this.container.clientHeight;
let height = element.scrollHeight;
const clientHeight = this.container.clientHeight;
const height = element.scrollHeight;
let d = (clientHeight - height) / 2;
const d = (clientHeight - height) / 2;
offsetTop -= d;
this.scrollTo(offsetTop, smooth);
@ -436,7 +459,7 @@ export default class Scrollable { @@ -436,7 +459,7 @@ export default class Scrollable {
public scrollTo(top: number, smooth = true, important = false) {
if(this.scrollLocked && !important) return;
let scrollTop = this.scrollTop;
const scrollTop = this.scrollTop;
if(scrollTop == Math.floor(top)) {
return;
}

6
src/components/slider.ts

@ -14,7 +14,7 @@ export default class SidebarSlider { @@ -14,7 +14,7 @@ export default class SidebarSlider {
this._selectTab(0);
let onCloseBtnClick = () => {
console.log('sidebar-close-button click:', this.historyTabIDs);
//console.log('sidebar-close-button click:', this.historyTabIDs);
let closingID = this.historyTabIDs.pop(); // pop current
this.onCloseTab(closingID);
this._selectTab(this.historyTabIDs[this.historyTabIDs.length - 1] || 0);
@ -25,6 +25,10 @@ export default class SidebarSlider { @@ -25,6 +25,10 @@ export default class SidebarSlider {
}
public selectTab(id: number) {
if(this.historyTabIDs[this.historyTabIDs.length - 1] == id) {
return;
}
this.historyTabIDs.push(id);
this._selectTab(id);
}

114
src/components/wrappers.ts

@ -18,6 +18,7 @@ import { mediaSizes } from '../lib/config'; @@ -18,6 +18,7 @@ import { mediaSizes } from '../lib/config';
import { MTDocument, MTPhotoSize } from '../types';
import animationIntersector from './animationIntersector';
import AudioElement from './audio';
import MP4Source from '../lib/MP4Sourcee';
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: {
doc: MTDocument,
@ -30,6 +31,23 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -30,6 +31,23 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
middleware: () => boolean,
lazyLoadQueue: LazyLoadQueue
}) {
let span: HTMLSpanElement, spanPlay: HTMLSpanElement;
if(doc.type != 'round') {
span = document.createElement('span');
span.classList.add('video-time');
container.append(span);
if(doc.type != 'gif') {
span.innerText = (doc.duration + '').toHHMMSS(false);
spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
container.append(spanPlay);
} else {
span.innerText = 'GIF';
}
}
if(doc.type == 'video') {
return wrapPhoto(doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware);
}
@ -55,19 +73,16 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -55,19 +73,16 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
}
let video = document.createElement('video');
let source = document.createElement('source');
video.append(source);
if(doc.type == 'gif') {
video.addEventListener('loadeddata', () => {
video.pause();
animationIntersector.addAnimation(video, 'chat');
}, {once: true});
if(img) {
img.classList.add('thumbnail');
}
const video = document.createElement('video');
const source = document.createElement('source');
video.append(source);
if(withTail) {
let foreignObject = img.parentElement;
const foreignObject = img.parentElement;
video.width = +foreignObject.getAttributeNS(null, 'width');
video.height = +foreignObject.getAttributeNS(null, 'height');
foreignObject.append(video);
@ -75,31 +90,26 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -75,31 +90,26 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
container.append(video);
}
let span: HTMLSpanElement, spanPlay: HTMLSpanElement;
if(doc.type != 'round') {
span = document.createElement('span');
span.classList.add('video-time');
container.append(span);
if(doc.type != 'gif') {
span.innerText = (doc.duration + '').toHHMMSS(false);
spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
container.append(spanPlay);
} else {
span.innerText = 'GIF';
}
}
let loadVideo = async() => {
let url: string;
if(message.media.preloader) { // means upload
(message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false);
} else if(!doc.downloaded) {
let preloader = new ProgressivePreloader(container, true);
let promise = appDocsManager.downloadDoc(doc);
preloader.attach(container, true, promise, false);
await promise;
const promise = appDocsManager.downloadVideo(doc.id);
//if(!doc.supportsStreaming) {
const preloader = new ProgressivePreloader(container, true);
preloader.attach(container, true, promise, false);
//}
const mp4Source: MP4Source = await (promise as Promise<MP4Source>);
if(mp4Source instanceof MP4Source) {
url = mp4Source.getURL();
}
}
if(!url) {
url = doc.url;
}
if(middleware && !middleware()) {
@ -107,17 +117,30 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -107,17 +117,30 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
//console.log('loaded doc:', doc, doc.url, container);
if(doc.type == 'gif'/* || true */) {
video.addEventListener('canplay', () => {
if(img && img.parentElement) {
img.remove();
}
/* if(!video.paused) {
video.pause();
} */
animationIntersector.addAnimation(video, 'chat');
}, {once: true});
}
renderImageFromUrl(source, doc.url);
renderImageFromUrl(source, url);
source.type = doc.mime_type;
video.append(source);
video.setAttribute('playsinline', '');
if(img && img.parentElement) {
img.remove();
}
/* if(!container.parentElement) {
container.append(video);
} */
if(doc.type == 'gif') {
if(doc.type == 'gif'/* || true */) {
video.muted = true;
video.loop = true;
//video.play();
@ -130,7 +153,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -130,7 +153,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
};
if(doc.size >= 20e6 && !doc.downloaded) {
/* if(doc.size >= 20e6 && !doc.downloaded) {
let downloadDiv = document.createElement('div');
downloadDiv.classList.add('download');
@ -146,9 +169,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -146,9 +169,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
container.prepend(downloadDiv);
return;
}
} */
return doc.downloaded ? loadVideo() : lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */});
//return;
return doc.downloaded/* && false */ ? loadVideo() : lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */});
}
export const formatDate = (timestamp: number, monthShort = false, withYear = true) => {
@ -402,7 +426,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -402,7 +426,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
if(thumb.bytes) {
let img = new Image();
if(appWebpManager.isSupported() || doc.stickerThumbConverted) {
if((appWebpManager.isSupported() || doc.stickerThumbConverted)/* && false */) {
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true));
div.append(img);
@ -415,7 +439,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -415,7 +439,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
if(!div.childElementCount) {
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(() => {
div.append(img);
if(!div.childElementCount) {
div.append(img);
}
});
}
});
@ -427,12 +453,14 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -427,12 +453,14 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
} else if(!onlyThumb && stickerType == 2 && withThumb && toneIndex <= 0) {
let img = new Image();
let load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => {
if(!img.parentElement || (middleware && !middleware())) return;
if(div.childElementCount || (middleware && !middleware())) return;
let promise = renderImageFromUrl(img, url);
if(!downloaded) {
promise.then(() => {
div.append(img);
if(!div.childElementCount) {
div.append(img);
}
});
}
});

29
src/index.hbs

@ -176,7 +176,7 @@ @@ -176,7 +176,7 @@
<div class="media-viewer-switcher media-viewer-switcher-right menu-next"><span class="tgico-down media-viewer-next-button"></span></div>
</div>
</div>
<div class="popup popup-send-photo">
<div class="popup popup-send-photo popup-new-media">
<div class="popup-container z-depth-1">
<div class="popup-header">
<span class="btn-icon popup-close tgico-close"></span>
@ -332,16 +332,16 @@ @@ -332,16 +332,16 @@
</div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="name" class="firstname" autocomplete="xxDDqqOXJXC" required="">
<label for="name">Name</label>
<input type="text" name="name" class="firstname" autocomplete="xxDDqqOXJXC" id="settings-name" required="">
<label for="settings-name">Name</label>
</div>
<div class="input-field">
<input type="text" name="lastname" class="lastname" autocomplete="aintsofunnowzXCFF" required="">
<label for="lastname">Last Name</label>
<input type="text" name="lastname" class="lastname" autocomplete="aintsofunnowzXCFF" id="settings-lastname" required="">
<label for="settings-lastname">Last Name</label>
</div>
<div class="input-field">
<input type="text" name="bio" class="bio" autocomplete="aintsofunnowhHQ" required="">
<label for="bio">Bio (optional)</label>
<input type="text" name="bio" class="bio" autocomplete="aintsofunnowhHQ" id="settings-bio" required="">
<label for="settings-bio">Bio (optional)</label>
</div>
</div>
<div class="caption">Any details such as age, occupation or city. Example:<br>23 y.o. designer from San Francisco.</div>
@ -349,8 +349,8 @@ @@ -349,8 +349,8 @@
<div class="sidebar-left-h2">Username</div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="username" class="username" autocomplete="xxDDqqOXffEER" required="">
<label for="username">Username (optional)</label>
<input type="text" name="username" class="username" autocomplete="xxDDqqOXffEER" id="settings-username" required="">
<label for="settings-username">Username (optional)</label>
</div>
</div>
<div class="caption">You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.<br><br>You can use a-z, 0-9 and underscores. Minimum length is 5 characters.<br><br><div class="profile-url-container">This link opens a chat with you:
@ -422,7 +422,7 @@ @@ -422,7 +422,7 @@
</div>
</div>
<div class="new-message-wrapper">
<button class="btn-icon rp tgico toggle-emoticons"></button>
<button class="btn-icon rp tgico toggle-emoticons" id="toggle-emoticons"></button>
<!-- <textarea type="text" id="input-message" placeholder="Message" contenteditable="true"></textarea> -->
<div class="input-message-container">
<div id="input-message" contenteditable="true" data-placeholder="Message"></div>
@ -444,7 +444,7 @@ @@ -444,7 +444,7 @@
<button class="btn-circle z-depth-1 btn-icon tgico-microphone2" id="btn-send"></button>
</div>
</div>
<div class="emoji-dropdown" style="display: none;">
<div class="emoji-dropdown" id="emoji-dropdown" style="display: none;">
<div class="emoji-container">
<div class="tabs-container">
<div class="emoji-padding">
@ -574,6 +574,13 @@ @@ -574,6 +574,13 @@
</div>
<div class="sidebar-content"><div class="sticker-sets"></div></div>
</div>
<div class="sidebar-slider-item chats-container" id="poll-results-container">
<div class="sidebar-header">
<button class="btn-icon rp tgico-close sidebar-close-button"></button>
<div class="sidebar-header__title">Results</div>
</div>
<div class="sidebar-content"><div class="poll-results"></div></div>
</div>
</div>
</div>
</div>

272
src/lib/MP4Source.js

@ -0,0 +1,272 @@ @@ -0,0 +1,272 @@
/*
* Copyright (c) 2018-present, Evgeny Nadymov
*
* This source code is licensed under the GPL v.3.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import MP4Box from 'mp4box/dist/mp4box.all.min';
//import { LOG, logSourceBufferRanges } from '../Utils/Common';
let LOG = () => console.log(...arguments);
export default class MP4Source {
constructor(video, getBufferAsync) {
this.mp4file = null;
this.nextBufferStart = 0;
this.mediaSource = null;
this.ready = false;
this.bufferedTime = 40;
this.beforeMoovBufferSize = 32 * 1024;
this.moovBufferSize = 512 * 1024;
this.bufferSize = 1024 * 1024;
this.seekBufferSize = 1024 * 1024;
this.currentBufferSize = this.beforeMoovBufferSize;
this.nbSamples = 10;
this.video = video;
this.getBufferAsync = getBufferAsync;
this.expectedSize = this.video.video.expected_size;
this.seeking = false;
this.loading = false;
this.url = null;
this.init(video.duration);
}
init(videoDuration) {
const mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', async () => {
LOG('[MediaSource] sourceopen start', this.mediaSource, this);
if (this.mediaSource.sourceBuffers.length > 0) return;
const mp4File = MP4Box.createFile();
mp4File.onMoovStart = () => {
LOG('[MP4Box] onMoovStart');
this.currentBufferSize = this.moovBufferSize;
};
mp4File.onError = error => {
LOG('[MP4Box] onError', error);
};
mp4File.onReady = info => {
LOG('[MP4Box] onReady', info);
this.ready = true;
this.currentBufferSize = this.bufferSize;
const { isFragmented, timescale, fragment_duration, duration } = info;
if (!fragment_duration && !duration) {
this.mediaSource.duration = videoDuration;
this.bufferedTime = videoDuration;
} else {
this.mediaSource.duration = isFragmented
? fragment_duration / timescale
: duration / timescale;
}
for (let i = 0; i < info.tracks.length; i++) {
this.addSourceBuffer(mp4File, this.mediaSource, info.tracks[i]);
}
const initSegs = mp4File.initializeSegmentation();
LOG('[MP4Box] initializeSegmentation', initSegs);
for (let i = 0; i < initSegs.length; i++) {
const { user: sourceBuffer } = initSegs[i];
sourceBuffer.onupdateend = () => {
sourceBuffer.initSegs = true;
sourceBuffer.onupdateend = this.handleSourceBufferUpdateEnd;
};
sourceBuffer.appendBuffer(initSegs[i].buffer);
}
LOG('[MP4Box] start fragmentation');
mp4File.start();
};
mp4File.onSegment = (id, sourceBuffer, buffer, sampleNum, is_last) => {
const isLast = (sampleNum + this.nbSamples) > sourceBuffer.nb_samples;
LOG('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sourceBuffer.nb_samples}`, isLast, sourceBuffer.timestampOffset);
if (mediaSource.readyState !== 'open') {
return;
}
sourceBuffer.pendingUpdates.push({ id, buffer, sampleNum, isLast });
if (sourceBuffer.initSegs && !sourceBuffer.updating) {
this.handleSourceBufferUpdateEnd({ target: sourceBuffer, mediaSource: this.mediaSource });
}
};
this.nextBufferStart = 0;
this.mp4file = mp4File;
LOG('[MediaSource] sourceopen end', this, this.mp4file);
this.loadNextBuffer();
});
mediaSource.addEventListener('sourceended', () => {
LOG('[MediaSource] sourceended', mediaSource.readyState);
});
mediaSource.addEventListener('sourceclose', () => {
LOG('[MediaSource] sourceclose', mediaSource.readyState);
});
this.mediaSource = mediaSource;
}
addSourceBuffer(file, source, track) {
if (!track) return null;
const { id, codec, type: trackType, nb_samples } = track;
const type = `video/mp4; codecs="${codec}"`;
if (!MediaSource.isTypeSupported(type)) {
LOG('[addSourceBuffer] not supported', type);
return null;
}
// if (trackType !== 'video') {
// LOG('[addSourceBuffer] skip', trackType);
// return null;
// }
const sourceBuffer = source.addSourceBuffer(type);
sourceBuffer.id = id;
sourceBuffer.pendingUpdates = [];
sourceBuffer.nb_samples = nb_samples;
file.setSegmentOptions(id, sourceBuffer, { nbSamples: this.nbSamples });
LOG('[addSourceBuffer] add', id, codec, trackType);
return sourceBuffer;
}
handleSourceBufferUpdateEnd = event => {
const { target: sourceBuffer } = event;
const { mediaSource, mp4file } = this;
if (!sourceBuffer) return;
if (sourceBuffer.updating) return;
//logSourceBufferRanges(sourceBuffer, 0, 0);
const { pendingUpdates } = sourceBuffer;
if (!pendingUpdates) return;
if (!pendingUpdates.length) {
if (sourceBuffer.isLast && mediaSource.readyState === 'open') {
LOG('[SourceBuffer] updateend endOfStream start', sourceBuffer.id);
if (Array.from(mediaSource.sourceBuffers).every(x => !x.pendingUpdates.length && !x.updating)) {
mediaSource.endOfStream();
LOG('[SourceBuffer] updateend endOfStream stop', sourceBuffer.id);
}
}
return;
}
const update = pendingUpdates.shift();
if (!update) return;
const { id, buffer, sampleNum, isLast } = update;
if (sampleNum) {
LOG('[SourceBuffer] updateend releaseUsedSamples', id, sampleNum);
mp4file.releaseUsedSamples(id, sampleNum);
}
LOG('[SourceBuffer] updateend end', sourceBuffer.id, sourceBuffer.pendingUpdates.length);
sourceBuffer.isLast = isLast;
sourceBuffer.appendBuffer(buffer);
};
getURL() {
this.url = this.url || URL.createObjectURL(this.mediaSource);
return this.url;
}
seek(currentTime, buffered) {
const seekInfo = this.mp4file.seek(currentTime, true);
this.nextBufferStart = seekInfo.offset;
let loadNextBuffer = buffered.length === 0;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
LOG('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart);
if (loadNextBuffer) {
this.loadNextBuffer(true);
}
}
timeUpdate(currentTime, duration, buffered) {
const ranges = [];
for (let i = 0; i < buffered.length; i++) {
ranges.push({ start: buffered.start(i), end: buffered.end(i)})
}
let loadNextBuffer = buffered.length === 0;
let hasRange = false;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime <= end) {
hasRange = true;
if (end < duration && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
}
if (!hasRange) {
loadNextBuffer = true;
}
LOG('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges));
if (loadNextBuffer) {
this.loadNextBuffer();
}
}
async loadNextBuffer(seek = false) {
const { nextBufferStart, loading, currentBufferSize, mp4file } = this;
LOG('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file);
if (!mp4file) return;
if (nextBufferStart === undefined) return;
if (loading) return;
this.loading = true;
let bufferSize = seek ? this.seekBufferSize : this.bufferSize;
if (nextBufferStart + bufferSize > this.expectedSize) {
bufferSize = this.expectedSize - nextBufferStart;
}
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize);
nextBuffer.fileStart = nextBufferStart;
LOG('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart);
if (nextBuffer.byteLength) {
this.nextBufferStart = mp4file.appendBuffer(nextBuffer);
} else {
this.nextBufferStart = undefined;
}
LOG('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart);
if (nextBuffer.byteLength < currentBufferSize) {
LOG('[player] loadNextBuffer flush');
this.mp4file.flush();
}
this.loading = false;
if (!this.ready) {
LOG('[player] loadNextBuffer next');
this.loadNextBuffer();
}
}
}

323
src/lib/MP4Sourcee.ts

@ -0,0 +1,323 @@ @@ -0,0 +1,323 @@
/*
* Copyright (c) 2018-present, Evgeny Nadymov
*
* This source code is licensed under the GPL v.3.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
// @ts-ignore
import MP4Box from 'mp4box/dist/mp4box.all.min';
import { logger, LogLevels } from './polyfill';
export default class MP4Source {
private mp4file: any;
private nextBufferStart = 0;
private mediaSource: MediaSource = null;
private ready = false;
private bufferedTime = 40;
private beforeMoovBufferSize = 32 * 1024;
private moovBufferSize = 512 * 1024;
private bufferSize = 512 * 1024;
private seekBufferSize = 256 * 1024;
private currentBufferSize = this.beforeMoovBufferSize;
private nbSamples = 10;
private expectedSize: number;
private seeking = false;
private loading = false;
private url: string;
private log = logger('MP4', LogLevels.error);
//public onLoadBuffer: (offset: number)
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) {
this.expectedSize = this.video.video.expected_size;
this.init(video.duration);
}
init(videoDuration: number) {
const mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', () => {
this.log('[MediaSource] sourceopen start', this.mediaSource, this);
if(this.mediaSource.sourceBuffers.length > 0) return;
const mp4File = MP4Box.createFile();
mp4File.onMoovStart = () => {
this.log('[MP4Box] onMoovStart');
this.currentBufferSize = this.moovBufferSize;
};
mp4File.onError = (error: Error) => {
this.log('[MP4Box] onError', error);
};
mp4File.onReady = (info: any) => {
this.log('[MP4Box] onReady', info);
this.ready = true;
this.currentBufferSize = this.bufferSize;
const { isFragmented, timescale, fragment_duration, duration } = info;
if(!fragment_duration && !duration) {
this.mediaSource.duration = videoDuration;
this.bufferedTime = videoDuration;
} else {
this.mediaSource.duration = isFragmented
? fragment_duration / timescale
: duration / timescale;
}
this.initializeAllSourceBuffers(info);
};
mp4File.onSegment = (id: number, sb: any, buffer: ArrayBuffer, sampleNum: number, is_last: boolean) => {
const isLast = (sampleNum + this.nbSamples) > sb.nb_samples;
this.log('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sb.nb_samples}`, isLast, sb.timestampOffset, mediaSource, is_last);
sb.segmentIndex++;
sb.pendingAppends.push({ id, buffer, sampleNum, is_last: isLast });
this.onUpdateEnd(sb, true, false);
};
this.mp4file = mp4File;
this.log('[MediaSource] sourceopen end', this, this.mp4file);
this.loadNextBuffer();
});
mediaSource.addEventListener('sourceended', () => {
this.log('[MediaSource] sourceended', mediaSource.readyState);
//this.getBufferAsync = null;
});
mediaSource.addEventListener('sourceclose', () => {
this.log('[MediaSource] sourceclose', mediaSource.readyState);
//this.getBufferAsync = null;
});
this.mediaSource = mediaSource;
}
private onInitAppended(sb: any) {
sb.sampleNum = 0;
sb.addEventListener('updateend', () => this.onUpdateEnd(sb, true, true));
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/
this.onUpdateEnd(sb, false, true);
// @ts-ignore
this.mediaSource.pendingInits--;
// @ts-ignore
if(this.mediaSource.pendingInits === 0) {
this.log('onInitAppended start!');
this.mp4file.start();
if(this.expectedSize > this.bufferSize) {
this.nextBufferStart = this.bufferSize;
} else {
return;
}
/* setInterval(() => {
this.loadNextBuffer();
}, 1e3); */
this.loadNextBuffer();
}
};
private onUpdateEnd(sb: any, isNotInit: boolean, isEndOfAppend: boolean) {
//console.this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
if(isEndOfAppend === true) {
if(sb.sampleNum) {
this.mp4file.releaseUsedSamples(sb.id, sb.sampleNum);
delete sb.sampleNum;
}
if(sb.is_last) {
this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
this.mediaSource.endOfStream();
}
}
if(this.mediaSource.readyState === "open" && sb.updating === false && sb.pendingAppends.length > 0) {
const obj = sb.pendingAppends.shift();
this.log("MSE - SourceBuffer #"+sb.id, "Appending new buffer, pending: "+sb.pendingAppends.length);
sb.sampleNum = obj.sampleNum;
sb.is_last = obj.is_last;
sb.appendBuffer(obj.buffer);
}
}
private initializeAllSourceBuffers(info: any) {
for(let i = 0; i < info.tracks.length; i++) {
this.addSourceBuffer(info.tracks[i]);
}
this.initializeSourceBuffers();
}
private initializeSourceBuffers() {
const initSegs = this.mp4file.initializeSegmentation();
this.log('[MP4Box] initializeSegmentation', initSegs);
for(let i = 0; i < initSegs.length; i++) {
const sb: any = initSegs[i].user;
if(i === 0) {
// @ts-ignore
this.mediaSource.pendingInits = 0;
}
let onInitAppended = () => {
if(this.mediaSource.readyState === "open") {
sb.removeEventListener('updateend', onInitAppended);
this.onInitAppended(sb);
}
};
sb.addEventListener('updateend', onInitAppended);
sb.appendBuffer(initSegs[i].buffer);
sb.segmentIndex = 0;
// @ts-ignore
this.mediaSource.pendingInits++;
}
}
private addSourceBuffer(track: {id: number, codec: string, type: 'video', nb_samples: number}) {
const file = this.mp4file;
const ms = this.mediaSource;
if(!track) return;
const { id, codec, type: trackType, nb_samples } = track;
const mime = `video/mp4; codecs="${codec}"`;
this.log('mimetype:', mime);
if(!MediaSource.isTypeSupported(mime)) {
this.log('[addSourceBuffer] not supported', mime);
return;
}
const sb: any = ms.addSourceBuffer(mime);
sb.id = id;
sb.pendingAppends = [];
sb.nb_samples = nb_samples;
file.setSegmentOptions(id, sb, { nbSamples: this.nbSamples });
this.log('[addSourceBuffer] add', id, codec, trackType, sb);
sb.addEventListener("error", (e: Event) => {
this.log("MSE SourceBuffer #" + id, e);
});
}
stop() {
this.mp4file.stop();
this.mp4file = null;
this.getBufferAsync = null;
}
getURL() {
return this.url ?? (this.url = URL.createObjectURL(this.mediaSource));
}
seek(currentTime: number/* , buffered: any */) {
const seekInfo: {offset: number, time: number} = this.mp4file.seek(currentTime, true);
this.nextBufferStart = seekInfo.offset;
const loadNextBuffer = true;
/* let loadNextBuffer = buffered.length === 0;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if(start <= currentTime && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
} */
this.log('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart);
if(loadNextBuffer) {
this.loadNextBuffer(true);
}
return seekInfo.offset;
}
timeUpdate(currentTime: number, duration: number, buffered: any) {
//return;
const ranges = [];
for(let i = 0; i < buffered.length; i++) {
ranges.push({ start: buffered.start(i), end: buffered.end(i)})
}
let loadNextBuffer = buffered.length === 0;
let hasRange = false;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime <= end) {
hasRange = true;
if (end < duration && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
}
if(!hasRange) {
loadNextBuffer = true;
}
this.log('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges));
if(loadNextBuffer) {
this.loadNextBuffer();
}
}
async loadNextBuffer(seek = false) {
const { nextBufferStart, loading, currentBufferSize, mp4file } = this;
this.log('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file);
if(!mp4file) return;
if(nextBufferStart === undefined) return;
if(loading) return;
//return;
this.loading = true;
let bufferSize = seek ? this.seekBufferSize : this.bufferSize;
if(nextBufferStart + bufferSize > this.expectedSize) {
bufferSize = this.expectedSize - nextBufferStart;
}
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize);
// @ts-ignore
nextBuffer.fileStart = nextBufferStart;
const end = (nextBuffer.byteLength !== bufferSize)/* || (nextBuffer.byteLength === this.expectedSize) */;
this.log('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart, end);
if(nextBuffer.byteLength) {
this.nextBufferStart = mp4file.appendBuffer(nextBuffer/* , end */);
} else {
this.nextBufferStart = undefined;
}
if(end) {
this.log('[player] loadNextBuffer flush');
this.mp4file.flush();
}
this.log('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart);
this.loading = false;
if(!this.ready || !end) {
this.log('[player] loadNextBuffer next');
this.loadNextBuffer();
}
}
}

279
src/lib/MP4Sourceee.ts

@ -0,0 +1,279 @@ @@ -0,0 +1,279 @@
/*
* Copyright (c) 2018-present, Evgeny Nadymov
*
* This source code is licensed under the GPL v.3.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
// @ts-ignore
import MP4Box from 'mp4box/dist/mp4box.all.min';
let LOG = (...args: any[]) => {
console.log(...args);
};
export default class MP4Source {
private mp4file: any;
private nextBufferStart = 0;
private mediaSource: MediaSource = null;
private ready = false;
private bufferedTime = 40;
private beforeMoovBufferSize = 32 * 1024;
private moovBufferSize = 512 * 1024;
private bufferSize = 512 * 1024;
private seekBufferSize = 512 * 1024;
private currentBufferSize = this.beforeMoovBufferSize;
private nbSamples = 12;
private expectedSize: number;
private seeking = false;
private loading = false;
private url: string = null;
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) {
this.expectedSize = this.video.video.expected_size;
this.init(video.duration);
}
init(videoDuration: any) {
const mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', async () => {
LOG('[MediaSource] sourceopen start', this.mediaSource, this);
if (this.mediaSource.sourceBuffers.length > 0) return;
const mp4File = MP4Box.createFile();
mp4File.onMoovStart = () => {
LOG('[MP4Box] onMoovStart');
this.currentBufferSize = this.moovBufferSize;
};
mp4File.onError = (error: any) => {
LOG('[MP4Box] onError', error);
};
mp4File.onReady = (info: any) => {
LOG('[MP4Box] onReady', info);
this.ready = true;
this.currentBufferSize = this.bufferSize;
const { isFragmented, timescale, fragment_duration, duration } = info;
if (!fragment_duration && !duration) {
this.mediaSource.duration = videoDuration;
this.bufferedTime = videoDuration;
} else {
this.mediaSource.duration = isFragmented
? fragment_duration / timescale
: duration / timescale;
}
for (let i = 0; i < info.tracks.length; i++) {
this.addSourceBuffer(mp4File, this.mediaSource, info.tracks[i]);
}
const initSegs = mp4File.initializeSegmentation();
LOG('[MP4Box] initializeSegmentation', initSegs);
for (let i = 0; i < initSegs.length; i++) {
const { user: sourceBuffer } = initSegs[i];
sourceBuffer.onupdateend = () => {
sourceBuffer.initSegs = true;
sourceBuffer.onupdateend = this.handleSourceBufferUpdateEnd;
};
sourceBuffer.appendBuffer(initSegs[i].buffer);
}
LOG('[MP4Box] start fragmentation');
mp4File.start();
setInterval(() => {
this.loadNextBuffer();
}, 1e3);
};
mp4File.onSegment = (id: any, sourceBuffer: any, buffer: any, sampleNum: any, is_last: boolean) => {
const isLast = (sampleNum + this.nbSamples) > sourceBuffer.nb_samples;
LOG('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sourceBuffer.nb_samples}`, isLast, sourceBuffer.timestampOffset);
if (mediaSource.readyState !== 'open') {
return;
}
sourceBuffer.pendingUpdates.push({ id, buffer, sampleNum, isLast });
if (sourceBuffer.initSegs && !sourceBuffer.updating) {
this.handleSourceBufferUpdateEnd({ target: sourceBuffer, mediaSource: this.mediaSource });
}
};
this.nextBufferStart = 0;
this.mp4file = mp4File;
LOG('[MediaSource] sourceopen end', this, this.mp4file);
this.loadNextBuffer();
});
mediaSource.addEventListener('sourceended', () => {
LOG('[MediaSource] sourceended', mediaSource.readyState);
});
mediaSource.addEventListener('sourceclose', () => {
LOG('[MediaSource] sourceclose', mediaSource.readyState);
});
this.mediaSource = mediaSource;
}
addSourceBuffer(file: any, source: any, track: any) {
if (!track) return null;
const { id, codec, type: trackType, nb_samples } = track;
const type = `video/mp4; codecs="${codec}"`;
if (!MediaSource.isTypeSupported(type)) {
LOG('[addSourceBuffer] not supported', type);
return null;
}
// if (trackType !== 'video') {
// LOG('[addSourceBuffer] skip', trackType);
// return null;
// }
const sourceBuffer = source.addSourceBuffer(type);
sourceBuffer.id = id;
sourceBuffer.pendingUpdates = [];
sourceBuffer.nb_samples = nb_samples;
file.setSegmentOptions(id, sourceBuffer, { nbSamples: this.nbSamples });
LOG('[addSourceBuffer] add', id, codec, trackType);
return sourceBuffer;
}
handleSourceBufferUpdateEnd = (event: any) => {
const { target: sourceBuffer } = event;
const { mediaSource, mp4file } = this;
if (!sourceBuffer) return;
if (sourceBuffer.updating) return;
//logSourceBufferRanges(sourceBuffer, 0, 0);
const { pendingUpdates } = sourceBuffer;
if (!pendingUpdates) return;
if (!pendingUpdates.length) {
if (sourceBuffer.isLast && mediaSource.readyState === 'open') {
LOG('[SourceBuffer] updateend endOfStream start', sourceBuffer.id);
if (Array.from(mediaSource.sourceBuffers).every((x: any) => !x.pendingUpdates.length && !x.updating)) {
mediaSource.endOfStream();
LOG('[SourceBuffer] updateend endOfStream stop', sourceBuffer.id);
}
}
return;
}
const update = pendingUpdates.shift();
if (!update) return;
const { id, buffer, sampleNum, isLast } = update;
if (sampleNum) {
LOG('[SourceBuffer] updateend releaseUsedSamples', id, sampleNum);
mp4file.releaseUsedSamples(id, sampleNum);
}
LOG('[SourceBuffer] updateend end', sourceBuffer.id, sourceBuffer.pendingUpdates.length);
sourceBuffer.isLast = isLast;
sourceBuffer.appendBuffer(buffer);
};
getURL() {
this.url = this.url || URL.createObjectURL(this.mediaSource);
return this.url;
}
seek(currentTime: number, buffered: any) {
const seekInfo = this.mp4file.seek(currentTime, true);
this.nextBufferStart = seekInfo.offset;
let loadNextBuffer = buffered.length === 0;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
LOG('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart);
if (loadNextBuffer) {
this.loadNextBuffer(true);
}
}
timeUpdate(currentTime: number, duration: number, buffered: any) {
const ranges = [];
for (let i = 0; i < buffered.length; i++) {
ranges.push({ start: buffered.start(i), end: buffered.end(i)})
}
let loadNextBuffer = buffered.length === 0;
let hasRange = false;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime <= end) {
hasRange = true;
if (end < duration && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
}
if (!hasRange) {
loadNextBuffer = true;
}
LOG('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges));
if (loadNextBuffer) {
this.loadNextBuffer();
}
}
async loadNextBuffer(seek = false) {
const { nextBufferStart, loading, currentBufferSize, mp4file } = this;
LOG('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file);
if (!mp4file) return;
if (nextBufferStart === undefined) return;
if (loading) return;
this.loading = true;
let bufferSize = seek ? this.seekBufferSize : this.bufferSize;
if (nextBufferStart + bufferSize > this.expectedSize) {
bufferSize = this.expectedSize - nextBufferStart;
}
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize);
// @ts-ignore
nextBuffer.fileStart = nextBufferStart;
LOG('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart);
if (nextBuffer.byteLength) {
this.nextBufferStart = mp4file.appendBuffer(nextBuffer);
} else {
this.nextBufferStart = undefined;
}
LOG('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart);
if (nextBuffer.byteLength < currentBufferSize) {
LOG('[player] loadNextBuffer flush');
this.mp4file.flush();
}
this.loading = false;
if (!this.ready) {
LOG('[player] loadNextBuffer next');
this.loadNextBuffer();
}
}
}

8
src/lib/appManagers/appDialogsManager.ts

@ -770,7 +770,7 @@ export class AppDialogsManager { @@ -770,7 +770,7 @@ export class AppDialogsManager {
//dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count ? formatNumber(dialog.unread_count, 1) : ' ');
dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count || ' ');
//dom.unreadMessagesSpan.classList.remove('tgico-pinnedchat');
dom.unreadMessagesSpan.classList.add(new Date(dialog.notify_settings.mute_until * 1000) > new Date() ?
dom.unreadMessagesSpan.classList.add(new Date(dialog.notify_settings?.mute_until * 1000) > new Date() ?
'unread-muted' : 'unread');
} else if(dialog.pFlags.pinned && dialog.folder_id == 0) {
dom.unreadMessagesSpan.classList.remove('unread', 'unread-muted');
@ -796,7 +796,7 @@ export class AppDialogsManager { @@ -796,7 +796,7 @@ export class AppDialogsManager {
return this.doms[peerID] || this.domsArchived[peerID];
}
public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable, drawStatus = true, rippleEnabled = true, onlyFirstName = false) {
public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true) {
let dialog: Dialog;
if(typeof(_dialog) === 'number') {
@ -820,7 +820,7 @@ export class AppDialogsManager { @@ -820,7 +820,7 @@ export class AppDialogsManager {
let title = appPeersManager.getPeerTitle(peerID, false, onlyFirstName);
let avatarEl = new AvatarElement();
avatarEl.setAttribute('dialog', '1');
avatarEl.setAttribute('dialog', meAsSaved ? '1' : '0');
avatarEl.setAttribute('peer', '' + peerID);
avatarEl.classList.add('dialog-avatar');
@ -860,7 +860,7 @@ export class AppDialogsManager { @@ -860,7 +860,7 @@ export class AppDialogsManager {
}
}
if(peerID == $rootScope.myID) {
if(peerID == $rootScope.myID && meAsSaved) {
title = onlyFirstName ? 'Saved' : 'Saved Messages';
}

141
src/lib/appManagers/appDocsManager.ts

@ -5,12 +5,17 @@ import { CancellablePromise, deferredPromise } from '../polyfill'; @@ -5,12 +5,17 @@ import { CancellablePromise, deferredPromise } from '../polyfill';
import { isObject } from '../utils';
import opusDecodeController from '../opusDecodeController';
import { MTDocument } from '../../types';
import MP4Source from '../MP4Sourcee';
import { bufferConcat } from '../bin_utils';
class AppDocsManager {
private docs: {[docID: string]: MTDocument} = {};
private thumbs: {[docIDAndSize: string]: Promise<string>} = {};
private downloadPromises: {[docID: string]: CancellablePromise<Blob>} = {};
private videoChunks: {[docID: string]: CancellablePromise<ArrayBuffer>[]} = {};
private videoChunksQueue: {[docID: string]: {offset: number}[]} = {};
public saveDoc(apiDoc: MTDocument, context?: any) {
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
if(this.docs[apiDoc.id]) {
@ -23,7 +28,8 @@ class AppDocsManager { @@ -23,7 +28,8 @@ class AppDocsManager {
}
}
return context ? Object.assign(d, context) : d;
return Object.assign(d, apiDoc, context);
//return context ? Object.assign(d, context) : d;
}
if(context) {
@ -56,13 +62,14 @@ class AppDocsManager { @@ -56,13 +62,14 @@ class AppDocsManager {
apiDoc.duration = attribute.duration;
apiDoc.audioTitle = attribute.title;
apiDoc.audioPerformer = attribute.performer;
apiDoc.type = attribute.pFlags.voice ? 'voice' : 'audio';
apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio';
break;
case 'documentAttributeVideo':
apiDoc.duration = attribute.duration;
apiDoc.w = attribute.w;
apiDoc.h = attribute.h;
apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming && apiDoc.size > 524288 && typeof(MediaSource) !== 'undefined';
if(apiDoc.thumbs && attribute.pFlags.round_message) {
apiDoc.type = 'round';
} else /* if(apiDoc.thumbs) */ {
@ -189,7 +196,110 @@ class AppDocsManager { @@ -189,7 +196,110 @@ class AppDocsManager {
return 't_' + (doc.type || 'file') + doc.id + fileExt;
}
private createMP4Stream(doc: MTDocument) {
const limitPart = 524288;
const chunks = this.videoChunks[doc.id];
const queue = this.videoChunksQueue[doc.id];
let mp4Source = new MP4Source({duration: doc.duration, video: {expected_size: doc.size}}, (offset: number, end: number) => {
const chunkStart = offset - (offset % limitPart);
const sorted: typeof queue = [];
const lower: typeof queue = [];
for(let i = 0; i < queue.length; ++i) {
if(queue[i].offset >= chunkStart) {
sorted.push(queue[i]);
} else {
lower.push(queue[i]);
}
}
sorted.sort((a, b) => a.offset - b.offset).concat(lower).forEach((q, i) => {
queue[i] = q;
});
const index1 = offset / limitPart | 0;
const index2 = end / limitPart | 0;
const p = chunks.slice(index1, index2 + 1);
//console.log('MP4Source getBuffer:', offset, end, index1, index2, doc.size, JSON.stringify(queue));
if(offset % limitPart == 0) {
return p[0];
} else {
return Promise.all(p).then(buffers => {
const buffer = buffers.length > 1 ? bufferConcat(buffers[0], buffers[1]) : buffers[0];
const start = (offset % limitPart);
const _end = start + (end - offset);
const sliced = buffer.slice(start, _end);
//console.log('slice buffer:', sliced);
return sliced;
});
}
});
return mp4Source;
}
private mp4Stream(doc: MTDocument, deferred: CancellablePromise<Blob>) {
const limitPart = 524288;
const promises = this.videoChunks[doc.id] ?? (this.videoChunks[doc.id] = []);
if(!promises.length) {
for(let offset = 0; offset < doc.size; offset += limitPart) {
const deferred = deferredPromise<ArrayBuffer>();
promises.push(deferred);
}
}
let good = false;
return async(bytes: Uint8Array, offset: number, queue: {offset: number}[]) => {
if(!deferred.isFulfilled && !deferred.isRejected/* && offset == 0 */) {
this.videoChunksQueue[doc.id] = queue;
console.log('stream:', doc, doc.url, deferred);
//doc.url = mp4Source.getURL();
//deferred.resolve(mp4Source);
deferred.resolve();
good = true;
} else if(!good) {
//mp4Source.stop();
//mp4Source = null;
promises.length = 0;
return;
}
const index = offset % limitPart == 0 ? offset / limitPart : promises.length - 1;
promises[index].resolve(bytes.slice().buffer);
//console.log('i wont believe in you', doc, bytes, offset, promises, bytes.length, bytes.buffer.byteLength, bytes.slice().buffer);
//console.log('i wont believe in you', bytes, doc, bytes.length, offset);
};
}
public downloadVideo(docID: string): CancellablePromise<MP4Source | Blob> {
const promise = this.downloadDoc(docID);
const doc = this.getDoc(docID);
if(!doc.supportsStreaming || doc.url) {
return promise;
}
const deferred = deferredPromise<Blob>();
deferred.cancel = () => {
promise.cancel();
};
promise.notify = (...args) => {
deferred.notify && deferred.notify(...args);
};
promise.then(() => {
deferred.resolve(this.createMP4Stream(doc));
});
return deferred;
}
public downloadDoc(docID: any, toFileEntry?: any): CancellablePromise<Blob> {
const doc = this.getDoc(docID);
@ -218,18 +328,21 @@ class AppDocsManager { @@ -218,18 +328,21 @@ class AppDocsManager {
downloadPromise.cancel();
};
const processPart = doc.supportsStreaming ? this.mp4Stream(doc, deferred) : undefined;
// нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся
const downloadPromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, {
mimeType: doc.mime_type || 'application/octet-stream',
toFileEntry: toFileEntry,
stickerType: doc.sticker
stickerType: doc.sticker,
processPart
});
downloadPromise.notify = (...args) => {
deferred.notify && deferred.notify(...args);
};
deferred.notify = downloadPromise.notify;
//deferred.notify = downloadPromise.notify;
downloadPromise.then((blob) => {
if(blob) {
@ -244,13 +357,20 @@ class AppDocsManager { @@ -244,13 +357,20 @@ class AppDocsManager {
opusDecodeController.decode(uint8).then(result => {
doc.url = result.url;
deferred.resolve(blob);
}, deferred.reject);
}, (err) => {
delete doc.downloaded;
deferred.reject(err);
});
};
reader.readAsArrayBuffer(blob);
return;
} else if(doc.type && doc.sticker != 2) {
/* if(processPart) {
console.log('stream after:', doc, doc.url, deferred);
} */
doc.url = URL.createObjectURL(blob);
}
}
@ -260,6 +380,8 @@ class AppDocsManager { @@ -260,6 +380,8 @@ class AppDocsManager {
console.log('document download failed', e);
deferred.reject(e);
//historyDoc.progress.enabled = false;
}).finally(() => {
deferred.notify = downloadPromise.notify = deferred.cancel = downloadPromise.cancel = null;
});
/* downloadPromise.notify = (progress) => {
@ -336,4 +458,9 @@ class AppDocsManager { @@ -336,4 +458,9 @@ class AppDocsManager {
}
}
export default new AppDocsManager();
const appDocsManager = new AppDocsManager();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appDocsManager = appDocsManager;
}
export default appDocsManager;

178
src/lib/appManagers/appImManager.ts

@ -36,6 +36,7 @@ import SearchInput from '../../components/searchInput'; @@ -36,6 +36,7 @@ import SearchInput from '../../components/searchInput';
import AppSearch, { SearchGroup } from '../../components/appSearch';
import PopupDatePicker from '../../components/popupDatepicker';
import appAudio from '../../components/appAudio';
import appPollsManager from './appPollsManager';
console.log('appImManager included33!');
@ -368,8 +369,10 @@ class ChatSearch { @@ -368,8 +369,10 @@ class ChatSearch {
};
onFooterClick = (e: MouseEvent) => {
appImManager.bubblesContainer.classList.toggle('search-results-active');
this.results.classList.toggle('active');
if(this.foundCount) {
appImManager.bubblesContainer.classList.toggle('search-results-active');
this.results.classList.toggle('active');
}
};
onUpClick = (e: MouseEvent) => {
@ -625,24 +628,24 @@ export class AppImManager { @@ -625,24 +628,24 @@ export class AppImManager {
// Calls when message successfully sent and we have an ID
$rootScope.$on('message_sent', (e: CustomEvent) => {
let {tempID, mid} = e.detail;
const {tempID, mid} = e.detail;
this.log('message_sent', e.detail);
// set cached url to media
let message = appMessagesManager.getMessage(mid);
const message = appMessagesManager.getMessage(mid);
if(message.media) {
if(message.media.photo) {
let photo = appPhotosManager.getPhoto(tempID);
const photo = appPhotosManager.getPhoto(tempID);
if(photo) {
let newPhoto = message.media.photo;
const newPhoto = message.media.photo;
newPhoto.downloaded = photo.downloaded;
newPhoto.url = photo.url;
}
} else if(message.media.document) {
let doc = appDocsManager.getDoc(tempID);
const doc = appDocsManager.getDoc(tempID);
if(doc && doc.type && doc.type != 'sticker') {
let newDoc = message.media.document;
const newDoc = message.media.document;
newDoc.downloaded = doc.downloaded;
newDoc.url = doc.url;
}
@ -664,6 +667,17 @@ export class AppImManager { @@ -664,6 +667,17 @@ export class AppImManager {
});
}
if(message.media?.poll) {
const newPoll = message.media.poll;
const pollElement = bubble.querySelector('poll-element');
if(pollElement) {
pollElement.setAttribute('poll-id', newPoll.id);
pollElement.setAttribute('message-id', mid);
delete appPollsManager.polls[tempID];
delete appPollsManager.results[tempID];
}
}
bubble.classList.remove('is-sending');
bubble.classList.add('is-sent');
bubble.dataset.mid = mid;
@ -960,7 +974,7 @@ export class AppImManager { @@ -960,7 +974,7 @@ export class AppImManager {
this.chatInputC.attachMediaPopUp.captionInput.focus();
}
if(e.key == 'Enter') {
if(e.key == 'Enter' && !touchSupport) {
this.chatInputC.attachMediaPopUp.sendBtn.click();
} else if(e.key == 'Escape') {
this.chatInputC.attachMediaPopUp.container.classList.remove('active');
@ -1079,6 +1093,7 @@ export class AppImManager { @@ -1079,6 +1093,7 @@ export class AppImManager {
public setPinnedMessage(message: any) {
/////this.log('setting pinned message', message);
//return;
const height = 52;
const scrollTop = this.scrollable.container.scrollTop;
const newPinned = wrapReply('Pinned Message', message.message, message, true);
newPinned.dataset.mid = '' + message.mid;
@ -1086,6 +1101,15 @@ export class AppImManager { @@ -1086,6 +1101,15 @@ export class AppImManager {
const close = document.createElement('button');
close.classList.add('pinned-message-close', 'btn-icon', 'tgico-close');
close.addEventListener('click', (e) => {
cancelEvent(e);
const scrollTop = this.scrollable.scrollTop;
newPinned.remove();
this.topbar.classList.remove('is-pinned-shown');
this.pinnedMessageContainer = null;
this.scrollable.scrollTop = scrollTop - height;
}, {once: true});
newPinned.append(close);
this.topbar.insertBefore(newPinned, this.btnMute);
@ -1097,7 +1121,7 @@ export class AppImManager { @@ -1097,7 +1121,7 @@ export class AppImManager {
this.pinnedMessageContainer = newPinned;
//this.pinnedMessageContent.innerHTML = message.rReply;
this.scrollable.scrollTop = scrollTop + 60;
this.scrollable.scrollTop = scrollTop + height;
}
public updateStatus() {
@ -1120,7 +1144,7 @@ export class AppImManager { @@ -1120,7 +1144,7 @@ export class AppImManager {
return null;
}
public loadMoreHistory(top: boolean) {
public loadMoreHistory(top: boolean, justLoad = false) {
//this.log('loadMoreHistory', top);
if(!this.peerID || testScroll || this.setPeerPromise || (top && this.getHistoryTopPromise) || (!top && this.getHistoryBottomPromise)) return;
@ -1134,7 +1158,7 @@ export class AppImManager { @@ -1134,7 +1158,7 @@ export class AppImManager {
this.log('load more', this.scrollable.scrollHeight, this.scrollable.scrollTop, this.scrollable);
return;
} */
/* false && */this.getHistory(history[0], true);
/* false && */this.getHistory(history[0], true, undefined, undefined, justLoad);
}
if(this.scrolledAllDown) return;
@ -1144,7 +1168,7 @@ export class AppImManager { @@ -1144,7 +1168,7 @@ export class AppImManager {
// if scroll down after search
if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) {
this.log('Will load more (down) history by maxID:', history[history.length - 1], history);
/* false && */this.getHistory(history[history.length - 1], false, true);
/* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad);
}
}
@ -1183,6 +1207,17 @@ export class AppImManager { @@ -1183,6 +1207,17 @@ export class AppImManager {
public setScroll() {
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, 300);
/* const getScrollOffset = () => {
//return Math.round(Math.max(300, appPhotosManager.windowH / 1.5));
return 300;
};
window.addEventListener('resize', () => {
this.scrollable.onScrollOffset = getScrollOffset();
});
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */
this.scroll = this.scrollable.container;
this.bubblesContainer.append(this.goDownBtn);
@ -1201,17 +1236,21 @@ export class AppImManager { @@ -1201,17 +1236,21 @@ export class AppImManager {
} else if(!this.chatInner.classList.contains('is-scrolling')) {
this.chatInner.classList.add('is-scrolling');
}
}, {passive: true});
this.scroll.addEventListener('touchend', () => {
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
}
this.scroll.addEventListener('touchend', () => {
if(!this.chatInner.classList.contains('is-scrolling')) {
return;
}
this.isScrollingTimeout = setTimeout(() => {
this.chatInner.classList.remove('is-scrolling');
this.isScrollingTimeout = 0;
}, 1350);
}, {passive: true, once: true})
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
}
this.isScrollingTimeout = setTimeout(() => {
this.chatInner.classList.remove('is-scrolling');
this.isScrollingTimeout = 0;
}, 1350);
}, {passive: true});
}
}
@ -1293,7 +1332,7 @@ export class AppImManager { @@ -1293,7 +1332,7 @@ export class AppImManager {
this.bubbleGroups.cleanup();
this.unreadOut.clear();
this.needUpdate.length = 0;
this.lazyLoadQueue.clear();
//this.lazyLoadQueue.clear();
// clear input
this.chatInputC.messageInput.innerHTML = '';
@ -1346,13 +1385,20 @@ export class AppImManager { @@ -1346,13 +1385,20 @@ export class AppImManager {
if(this.setPeerPromise && samePeer) return this.setPeerPromise;
const dialog = appMessagesManager.getDialogByPeerID(peerID)[0] || null;
const topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0;
let topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0; // убрать + 1 после создания базы референсов
const isTarget = lastMsgID !== undefined;
// @ts-ignore
/* if(topMessage && dialog && dialog.top_message == topMessage && dialog.refetchTopMessage) {
// @ts-ignore
dialog.refetchTopMessage = false;
topMessage += 1;
} */
if(!isTarget && dialog) {
if(dialog.unread_count && !samePeer) {
lastMsgID = dialog.read_inbox_max_id;
} else {
lastMsgID = dialog.top_message;
//lastMsgID = topMessage;
}
}
@ -1397,6 +1443,10 @@ export class AppImManager { @@ -1397,6 +1443,10 @@ export class AppImManager {
this.chatInner.className = oldChatInner.className;
this.chatInner.classList.add('disable-hover', 'is-scrolling');
if(!samePeer) {
this.lazyLoadQueue.clear();
}
this.lazyLoadQueue.lock();
const {promise, cached} = this.getHistory(lastMsgID, true, isJump, additionMsgID);
@ -1447,8 +1497,7 @@ export class AppImManager { @@ -1447,8 +1497,7 @@ export class AppImManager {
this.scrollable.container.append(this.chatInner);
animationIntersector.unlockGroup('chat');
animationIntersector.checkAnimations(false, 'chat', true);
animationIntersector.checkAnimations(false, 'chat'/* , true */);
//this.scrollable.attachSentinels();
//this.scrollable.container.insertBefore(this.chatInner, this.scrollable.container.lastElementChild);
@ -1488,7 +1537,7 @@ export class AppImManager { @@ -1488,7 +1537,7 @@ export class AppImManager {
appMessagesManager.readHistory(peerID, dialog.top_message);
}
if(dialog.pFlags?.unread_mark) {
if(dialog?.pFlags?.unread_mark) {
appMessagesManager.markDialogUnread(peerID, true);
}
@ -1705,6 +1754,11 @@ export class AppImManager { @@ -1705,6 +1754,11 @@ export class AppImManager {
}
public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) {
/* let dateMessage = this.getDateContainerByMessage(message, reverse);
if(reverse) dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling);
else dateMessage.container.append(bubble);
return; */
let promises: Promise<any>[] = [];
(Array.from(bubble.querySelectorAll('img, video')) as HTMLImageElement[]).forEach(el => {
if(el instanceof HTMLVideoElement) {
@ -1722,6 +1776,10 @@ export class AppImManager { @@ -1722,6 +1776,10 @@ export class AppImManager {
let onLoad = () => {
clearTimeout(timeout);
resolve();
// lol
el.removeEventListener('canplay', onLoad);
el.removeEventListener('load', onLoad);
};
if(el instanceof HTMLVideoElement) {
@ -1829,6 +1887,7 @@ export class AppImManager { @@ -1829,6 +1887,7 @@ export class AppImManager {
bubble.className = 'bubble';
bubbleContainer = bubble.firstElementChild as HTMLDivElement;
bubbleContainer.innerHTML = '';
bubbleContainer.style.marginBottom = '';
if(bubble == this.firstUnreadBubble) {
bubble.classList.add('is-first-unread');
@ -1986,27 +2045,27 @@ export class AppImManager { @@ -1986,27 +2045,27 @@ export class AppImManager {
//bubble.prepend(timeSpan, messageDiv); // that's bad
if(message.reply_markup && message.reply_markup._ == 'replyInlineMarkup' && message.reply_markup.rows && message.reply_markup.rows.length) {
let rows = message.reply_markup.rows;
const rows = message.reply_markup.rows;
let containerDiv = document.createElement('div');
const containerDiv = document.createElement('div');
containerDiv.classList.add('reply-markup');
rows.forEach((row: any) => {
let buttons = row.buttons;
const buttons = row.buttons;
if(!buttons || !buttons.length) return;
let rowDiv = document.createElement('div');
const rowDiv = document.createElement('div');
rowDiv.classList.add('reply-markup-row');
buttons.forEach((button: any) => {
let text = RichTextProcessor.wrapRichText(button.text, {noLinks: true, noLinebreaks: true});
const text = RichTextProcessor.wrapRichText(button.text, {noLinks: true, noLinebreaks: true});
let buttonEl: HTMLButtonElement | HTMLAnchorElement;
switch(button._) {
case 'keyboardButtonUrl': {
let from = appUsersManager.getUser(message.fromID);
let unsafe = !(from && from.pFlags && from.pFlags.verified);
let url = RichTextProcessor.wrapUrl(button.url, unsafe);
const from = appUsersManager.getUser(message.fromID);
const unsafe = !(from && from.pFlags && from.pFlags.verified);
const url = RichTextProcessor.wrapUrl(button.url, unsafe);
buttonEl = document.createElement('a');
buttonEl.href = url;
buttonEl.rel = 'noopener noreferrer';
@ -2039,19 +2098,19 @@ export class AppImManager { @@ -2039,19 +2098,19 @@ export class AppImManager {
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
if(!target) return;
let column = whichChild(target);
let row = rows[whichChild(target.parentElement)];
const column = whichChild(target);
const row = rows[whichChild(target.parentElement)];
if(!row.buttons || !row.buttons[column]) {
this.log.warn('no such button', row, column, message);
return;
}
let button = row.buttons[column];
const button = row.buttons[column];
appInlineBotsManager.callbackButtonClick(message.mid, button);
});
let offset = rows.length * 45 + 'px';
const offset = rows.length * 45 + 'px';
bubbleContainer.style.marginBottom = offset;
containerDiv.style.bottom = '-' + offset;
@ -2639,15 +2698,15 @@ export class AppImManager { @@ -2639,15 +2698,15 @@ export class AppImManager {
return true;
});
}
// reverse means scroll up
public getHistory(maxID = 0, reverse = false, isBackLimit = false, additionMsgID = 0): {cached: boolean, promise: Promise<boolean>} {
let peerID = this.peerID;
public getHistory(maxID = 0, reverse = false, isBackLimit = false, additionMsgID = 0, justLoad = false): {cached: boolean, promise: Promise<boolean>} {
const peerID = this.peerID;
//console.time('appImManager call getHistory');
let pageCount = appPhotosManager.windowH / 38/* * 1.25 */ | 0;
//let loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount;
let realLoadCount = Object.keys(this.bubbles).length > 0 ? Math.max(40, pageCount) : pageCount;//let realLoadCount = 50;
const pageCount = appPhotosManager.windowH / 38/* * 1.25 */ | 0;
//const loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount;
const realLoadCount = Object.keys(this.bubbles).length > 0 ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50;
let loadCount = realLoadCount;
if(testScroll) {
@ -2668,7 +2727,7 @@ export class AppImManager { @@ -2668,7 +2727,7 @@ export class AppImManager {
}
}
let result = appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit);
const result = appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit);
let promise: Promise<boolean>, cached: boolean;
if(result instanceof Promise) {
@ -2676,6 +2735,7 @@ export class AppImManager { @@ -2676,6 +2735,7 @@ export class AppImManager {
promise = result.then((result) => {
this.log('getHistory not cached result by maxID:', maxID, reverse, isBackLimit, result, peerID);
if(justLoad) return true;
//console.timeEnd('appImManager call getHistory');
if(this.peerID != peerID) {
@ -2689,9 +2749,10 @@ export class AppImManager { @@ -2689,9 +2749,10 @@ export class AppImManager {
return this.performHistoryResult(result.history || [], reverse, isBackLimit, additionMsgID);
}, (err) => {
this.log.error('getHistory error:', err);
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
return false;
});
} else if(justLoad) {
return null;
} else {
cached = true;
this.log('getHistory cached result by maxID:', maxID, reverse, isBackLimit, result, peerID);
@ -2702,6 +2763,14 @@ export class AppImManager { @@ -2702,6 +2763,14 @@ export class AppImManager {
(reverse ? this.getHistoryTopPromise = promise : this.getHistoryBottomPromise = promise);
promise.finally(() => {
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
});
if(justLoad) {
return null;
}
/* false && */promise.then(() => {
if(reverse) {
this.loadedTopTimes++;
@ -2717,7 +2786,7 @@ export class AppImManager { @@ -2717,7 +2786,7 @@ export class AppImManager {
}
//let removeCount = loadCount / 2;
let safeCount = realLoadCount * 2; // cause i've been runningrunningrunning all day
const safeCount = realLoadCount * 2; // cause i've been runningrunningrunning all day
this.log('getHistory: slice loadedTimes:', reverse, pageCount, this.loadedTopTimes, this.loadedBottomTimes, ids && ids.length, safeCount);
if(ids && ids.length > safeCount) {
if(reverse) {
@ -2738,9 +2807,13 @@ export class AppImManager { @@ -2738,9 +2807,13 @@ export class AppImManager {
this.deleteMessagesByIDs(ids);
}
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
this.setUnreadDelimiter(); // не нашёл места лучше
// preload more
setTimeout(() => {
this.loadMoreHistory(true, true);
this.loadMoreHistory(false, true);
}, 0);
});
return {cached, promise};
@ -2930,5 +3003,8 @@ export class AppImManager { @@ -2930,5 +3003,8 @@ export class AppImManager {
}
const appImManager = new AppImManager();
(window as any).appImManager = appImManager;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appImManager = appImManager;
}
export default appImManager;

148
src/lib/appManagers/appMediaViewer.ts

@ -11,6 +11,8 @@ import { renderImageFromUrl, parseMenuButtonsTo } from "../../components/misc"; @@ -11,6 +11,8 @@ import { renderImageFromUrl, parseMenuButtonsTo } from "../../components/misc";
import AvatarElement from "../../components/avatar";
import LazyLoadQueue from "../../components/lazyLoadQueue";
import appForward from "../../components/appForward";
import { isSafari } from "../config";
import MP4Source from "../MP4Sourcee";
export class AppMediaViewer {
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
@ -62,7 +64,7 @@ export class AppMediaViewer { @@ -62,7 +64,7 @@ export class AppMediaViewer {
constructor() {
this.log = logger('AMV');
this.preloader = new ProgressivePreloader();
this.lazyLoadQueue = new LazyLoadQueue(5, false);
this.lazyLoadQueue = new LazyLoadQueue(undefined, true);
parseMenuButtonsTo(this.buttons, this.wholeDiv.querySelectorAll(`[class*='menu']`) as NodeListOf<HTMLElement>);
@ -325,6 +327,12 @@ export class AppMediaViewer { @@ -325,6 +327,12 @@ export class AppMediaViewer {
newSvg.insertAdjacentHTML('beforeend', target.firstElementChild.outerHTML.replace(clipID, newClipID));
newSvg.insertAdjacentHTML('beforeend', target.lastElementChild.outerHTML.replace(clipID, newClipID));
// FIX STREAM
let source = newSvg.querySelector('source');
if(source) {
source.removeAttribute('src');
}
// теперь надо выставить новую позицию для хвостика
let defs = newSvg.firstElementChild;
let use = defs.firstElementChild.firstElementChild as SVGUseElement;
@ -376,7 +384,7 @@ export class AppMediaViewer { @@ -376,7 +384,7 @@ export class AppMediaViewer {
mediaElement.src = src;
}
});
} else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) {
}/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) {
await new Promise((resolve, reject) => {
mediaElement.addEventListener('loadeddata', resolve);
@ -384,7 +392,7 @@ export class AppMediaViewer { @@ -384,7 +392,7 @@ export class AppMediaViewer {
(mediaElement.firstElementChild as HTMLSourceElement).src = src;
}
});
}
} */
mover.style.display = '';
@ -769,13 +777,21 @@ export class AppMediaViewer { @@ -769,13 +777,21 @@ export class AppMediaViewer {
if(isVideo) {
////////this.log('will wrap video', media, size);
/* let source = target.querySelector('source');
if(source && source.src) {
source.src = '';
} */
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(() => {
//return; // set and don't move
//if(wasActive) return;
//return;
let video = mover.querySelector('video') || document.createElement('video');
let source = video.firstElementChild as HTMLSourceElement || document.createElement('source');
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
const video = mover.querySelector('video') || document.createElement('video');
const source = video.firstElementChild as HTMLSourceElement || document.createElement('source');
source.removeAttribute('src');
video.setAttribute('playsinline', '');
if(media.type == 'gif') {
@ -784,54 +800,128 @@ export class AppMediaViewer { @@ -784,54 +800,128 @@ export class AppMediaViewer {
video.loop = true;
}
let createPlayer = () => {
if(!source.parentElement) {
video.append(source);
}
const createPlayer = (streamable = false) => {
if(media.type != 'gif') {
video.dataset.ckin = 'default';
video.dataset.overlay = '1';
let player = new VideoPlayer(video, true);
if(!video.parentElement) {
div.append(video);
}
const player = new VideoPlayer(video, true, streamable);
return player;
/* player.wrapper.parentElement.append(video);
mover.append(player.wrapper); */
} else {
}/* else {
video.play();
}
} */
};
if(!source || !source.src) {
let load = () => {
let promise = appDocsManager.downloadDoc(media);
this.preloader.attach(mover, true, promise);
promise.then(() => {
if(!source.src || (media.url && media.url != source.src)) {
const load = () => {
const promise = appDocsManager.downloadVideo(media.id);
const streamable = media.supportsStreaming && !media.url;
//if(!streamable) {
this.preloader.attach(mover, true, promise);
//}
let player: VideoPlayer;
let offset = 0;
let loadedParts: {[offset: number]: true} = {};
let preloaderNotify = promise.notify;
let promiseNotify = (details: {offset: number, total: number, done: number}) => {
if(player) {
//player.progress.setLoadProgress(details.done / details.total);
setLoadProgress();
}
loadedParts[details.offset] = true;
preloaderNotify(details);
};
if(streamable) {
promise.notify = promiseNotify;
}
let setLoadProgress = () => {
let rounded = offset - (offset % 524288);
let downloadedAfter = 0;
for(let i in loadedParts) {
let o = +i;
if(o >= rounded) {
downloadedAfter += 524288;
}
}
if(offset > rounded) {
downloadedAfter -= offset % 524288;
}
player.progress.setLoadProgress(Math.min(1, downloadedAfter / media.size + rounded / media.size));
};
promise.then(async(mp4Source: any) => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
}
const isStream = mp4Source instanceof MP4Source;
if(isStream) {
promise.notify = promiseNotify;
}
let url = media.url;
if(target instanceof SVGSVGElement) {
const url = isStream ? mp4Source.getURL() : media.url;
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
if(!video.parentElement) {
div.firstElementChild.lastElementChild.append(video);
}
this.updateMediaSource(mover, url, 'source');
this.updateMediaSource(target, url, 'source');
//this.updateMediaSource(target, url, 'source');
} else {
let div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
let image = div.firstElementChild as HTMLImageElement;
if(image instanceof HTMLImageElement) {
image.remove();
}
//const promise = new Promise((resolve) => video.addEventListener('loadeddata', resolve, {once: true}));
renderImageFromUrl(source, url);
source.type = media.mime_type;
if(!source.parentElement) {
video.append(source);
//await promise;
const first = div.firstElementChild as HTMLImageElement;
if(!(first instanceof HTMLVideoElement)) {
first.remove();
}
if(!video.parentElement) {
div.prepend(video);
}
}
// я хз что это такое, видео появляются просто чёрными и не включаются без этого кода снизу
source.remove();
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
//parent.append(video);
video.append(source);
});
});
createPlayer();
player = createPlayer(streamable);
if(player && mp4Source instanceof MP4Source) {
player.progress.onSeek = (time) => {
//this.log('seek', time);
offset = mp4Source.seek(time);
setLoadProgress();
};
this.log('lol');
}
});
return promise;

237
src/lib/appManagers/appMessagesManager.ts

@ -225,6 +225,13 @@ export class AppMessagesManager { @@ -225,6 +225,13 @@ export class AppMessagesManager {
} */
this.saveMessages(messages);
// FIX FILE_REFERENCE_EXPIRED KOSTIL'1999
for(let message of messages) {
if(message.media) {
this.wrapSingleMessage(message.mid, true);
}
}
}
if(allDialogsLoaded) {
@ -233,6 +240,8 @@ export class AppMessagesManager { @@ -233,6 +240,8 @@ export class AppMessagesManager {
if(dialogs) {
dialogs.forEachReverse(dialog => {
// @ts-ignore
//dialog.refetchTopMessage = true;
this.saveConversation(dialog);
});
}
@ -1241,6 +1250,230 @@ export class AppMessagesManager { @@ -1241,6 +1250,230 @@ export class AppMessagesManager {
invoke(inputs);
}
public sendOther(peerID: number, inputMedia: any, options: Partial<{
replyToMsgID: number,
viaBotID: number,
reply_markup: any,
clearDraft: boolean,
queryID: number
resultID: number
}> = {}) {
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
const messageID = this.tempID--;
const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString();
const historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {count: null, history: [], pending: []});
const replyToMsgID = options.replyToMsgID;
const isChannel = appPeersManager.isChannel(peerID);
const isMegagroup = isChannel && appPeersManager.isMegagroup(peerID);
const asChannel = isChannel && !isMegagroup ? true : false;
let fromID = appUsersManager.getSelf().id;
let media;
switch(inputMedia._) {
case 'inputMediaPoll': {
inputMedia.poll.id = messageID;
appPollsManager.savePoll(inputMedia.poll, {
_: 'pollResults',
flags: 4,
total_voters: 0,
pFlags: {},
});
const {poll, results} = appPollsManager.getPoll('' + messageID);
media = {
_: 'messageMediaPoll',
poll,
results
};
break;
}
/* case 'inputMediaPhoto':
media = {
_: 'messageMediaPhoto',
photo: appPhotosManager.getPhoto(inputMedia.id.id),
caption: inputMedia.caption || ''
};
break;
case 'inputMediaDocument':
var doc = appDocsManager.getDoc(inputMedia.id.id);
if(doc.sticker && doc.stickerSetInput) {
appStickersManager.pushPopularSticker(doc.id);
}
media = {
_: 'messageMediaDocument',
'document': doc,
caption: inputMedia.caption || ''
};
break;
case 'inputMediaContact':
media = {
_: 'messageMediaContact',
phone_number: inputMedia.phone_number,
first_name: inputMedia.first_name,
last_name: inputMedia.last_name,
user_id: 0
};
break;
case 'inputMediaGeoPoint':
media = {
_: 'messageMediaGeo',
geo: {
_: 'geoPoint',
'lat': inputMedia.geo_point['lat'],
'long': inputMedia.geo_point['long']
}
};
break;
case 'inputMediaVenue':
media = {
_: 'messageMediaVenue',
geo: {
_: 'geoPoint',
'lat': inputMedia.geo_point['lat'],
'long': inputMedia.geo_point['long']
},
title: inputMedia.title,
address: inputMedia.address,
provider: inputMedia.provider,
venue_id: inputMedia.venue_id
};
break;
case 'messageMediaPending':
media = inputMedia;
break; */
}
let flags = 0;
let pFlags: any = {};
if(peerID != fromID) {
flags |= 2;
pFlags.out = true;
if(!appUsersManager.isBot(peerID)) {
flags |= 1;
pFlags.unread = true;
}
}
if(replyToMsgID) {
flags |= 8;
}
if(asChannel) {
fromID = 0;
pFlags.post = true;
} else {
flags |= 256;
}
const message: any = {
_: 'message',
id: messageID,
from_id: fromID,
to_id: appPeersManager.getOutputPeer(peerID),
flags: flags,
pFlags: pFlags,
date: tsNow(true) + ServerTimeManager.serverTimeOffset,
message: '',
media: media,
random_id: randomIDS,
reply_to_msg_id: replyToMsgID,
via_bot_id: options.viaBotID,
reply_markup: options.reply_markup,
views: asChannel && 1,
pending: true,
};
let toggleError = (on: boolean) => {
/* const historyMessage = this.messagesForHistory[messageID];
if (on) {
message.error = true
if (historyMessage) {
historyMessage.error = true
}
} else {
delete message.error
if (historyMessage) {
delete historyMessage.error
}
} */
$rootScope.$broadcast('messages_pending');
};
message.send = () => {
let flags = 0;
if(replyToMsgID) {
flags |= 1;
}
if(asChannel) {
flags |= 16;
}
if(options.clearDraft) {
flags |= 128;
}
const sentRequestOptions: any = {};
if(this.pendingAfterMsgs[peerID]) {
sentRequestOptions.afterMessageID = this.pendingAfterMsgs[peerID].messageID;
}
let apiPromise: Promise<any>;
if(options.viaBotID) {
apiPromise = apiManager.invokeApi('messages.sendInlineBotResult', {
flags: flags,
peer: appPeersManager.getInputPeerByID(peerID),
random_id: randomID,
reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID),
query_id: options.queryID,
id: options.resultID
}, sentRequestOptions);
} else {
apiPromise = apiManager.invokeApi('messages.sendMedia', {
flags: flags,
peer: appPeersManager.getInputPeerByID(peerID),
media: inputMedia,
random_id: randomID,
reply_to_msg_id: appMessagesIDsManager.getMessageLocalID(replyToMsgID)
}, sentRequestOptions);
}
apiPromise.then((updates) => {
if(updates.updates) {
updates.updates.forEach((update: any) => {
if(update._ == 'updateDraftMessage') {
update.local = true
}
});
}
apiUpdatesManager.processUpdateMessage(updates);
}, (error) => {
toggleError(true);
}).finally(() => {
if(this.pendingAfterMsgs[peerID] === sentRequestOptions) {
delete this.pendingAfterMsgs[peerID];
}
});
this.pendingAfterMsgs[peerID] = sentRequestOptions;
}
this.saveMessages([message]);
historyStorage.pending.unshift(messageID);
$rootScope.$broadcast('history_append', {peerID: peerID, messageID: messageID, my: true});
setTimeout(message.send, 0);
/* if(options.clearDraft) {
DraftsManager.clearDraft(peerID)
} */
this.pendingByRandomID[randomIDS] = [peerID, messageID];
}
public cancelPendingMessage(randomID: string) {
var pendingData = this.pendingByRandomID[randomID];
@ -3849,8 +4082,8 @@ export class AppMessagesManager { @@ -3849,8 +4082,8 @@ export class AppMessagesManager {
});
}
public wrapSingleMessage(msgID: number) {
if(this.messagesStorage[msgID]) {
public wrapSingleMessage(msgID: number, overwrite = false) {
if(this.messagesStorage[msgID] && !overwrite) {
$rootScope.$broadcast('messages_downloaded', [msgID]);
} else if(this.needSingleMessages.indexOf(msgID) == -1) {
this.needSingleMessages.push(msgID);

2
src/lib/appManagers/appPhotosManager.ts

@ -50,7 +50,7 @@ export class AppPhotosManager { @@ -50,7 +50,7 @@ export class AppPhotosManager {
}
public savePhoto(photo: MTPhoto, context?: any) {
if(this.photos[photo.id]) return this.photos[photo.id];
if(this.photos[photo.id]) return Object.assign(this.photos[photo.id], photo);
/* if(context) {
Object.assign(photo, context);

104
src/lib/appManagers/appPollsManager.ts

@ -4,6 +4,8 @@ import appPeersManager from './appPeersManager'; @@ -4,6 +4,8 @@ import appPeersManager from './appPeersManager';
import apiManager from "../mtproto/mtprotoworker";
import apiUpdatesManager from "./apiUpdatesManager";
import { $rootScope } from "../utils";
import { logger, LogLevels } from "../polyfill";
import appUsersManager, { User } from "./appUsersManager";
export type PollAnswer = {
_: 'pollAnswer',
@ -29,7 +31,7 @@ export type PollResult = { @@ -29,7 +31,7 @@ export type PollResult = {
option: Uint8Array,
voters: number,
pFlags?: Partial<{chosen: true}>
pFlags?: Partial<{chosen: true, correct: true}>
};
export type PollResults = {
@ -63,12 +65,14 @@ export type Poll = { @@ -63,12 +65,14 @@ export type Poll = {
}>,
rQuestion?: string,
rReply?: string,
chosenIndex?: number
chosenIndexes?: number[]
};
class AppPollsManager {
private polls: {[id: string]: Poll} = {};
private results: {[id: string]: PollResults} = {};
public polls: {[id: string]: Poll} = {};
public results: {[id: string]: PollResults} = {};
private log = logger('POLLS', LogLevels.error);
constructor() {
$rootScope.$on('apiUpdate', (e: CustomEvent) => {
@ -81,9 +85,9 @@ class AppPollsManager { @@ -81,9 +85,9 @@ class AppPollsManager {
public handleUpdate(update: any) {
switch(update._) {
case 'updateMessagePoll': { // when someone voted, we too
console.log('updateMessagePoll:', update);
this.log('updateMessagePoll:', update);
let poll: Poll = this.polls[update.poll_id] || update.poll;
let poll: Poll = /* this.polls[update.poll_id] || */update.poll;
if(!poll) {
break;
}
@ -99,9 +103,9 @@ class AppPollsManager { @@ -99,9 +103,9 @@ class AppPollsManager {
}
public savePoll(poll: Poll, results: PollResults) {
let id = poll.id;
const id = poll.id;
if(this.polls[id]) {
poll = this.polls[id];
poll = Object.assign(this.polls[id], poll);
this.saveResults(poll, results);
return poll;
}
@ -110,13 +114,22 @@ class AppPollsManager { @@ -110,13 +114,22 @@ class AppPollsManager {
poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question);
poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll');
poll.chosenIndexes = [];
this.saveResults(poll, results);
return poll;
}
public saveResults(poll: Poll, results: PollResults) {
this.results[poll.id] = results;
poll.chosenIndex = (results && results.results && results.results.findIndex(answer => answer.pFlags?.chosen)) ?? -1;
poll.chosenIndexes.length = 0;
if(results?.results?.length) {
results.results.forEach((answer, idx) => {
if(answer.pFlags?.chosen) {
poll.chosenIndexes.push(idx);
}
});
}
}
public getPoll(pollID: string): {poll: Poll, results: PollResults} {
@ -127,27 +140,86 @@ class AppPollsManager { @@ -127,27 +140,86 @@ class AppPollsManager {
}
public sendVote(mid: number, optionIDs: number[]) {
let message = appMessagesManager.getMessage(mid);
let poll: Poll = message.media.poll;
const message = appMessagesManager.getMessage(mid);
const poll: Poll = message.media.poll;
let options: Uint8Array[] = optionIDs.map(index => {
const options: Uint8Array[] = optionIDs.map(index => {
return poll.answers[index].option;
});
let inputPeer = appPeersManager.getInputPeerByID(message.peerID);
let messageID = message.id;
const inputPeer = appPeersManager.getInputPeerByID(message.peerID);
const messageID = message.id;
return apiManager.invokeApi('messages.sendVote', {
peer: inputPeer,
msg_id: messageID,
options
}).then(updates => {
console.log('appPollsManager sendVote updates:', updates);
this.log('sendVote updates:', updates);
apiUpdatesManager.processUpdateMessage(updates);
});
}
public getResults(mid: number) {
const message = appMessagesManager.getMessage(mid);
const inputPeer = appPeersManager.getInputPeerByID(message.peerID);
const messageID = message.id;
return apiManager.invokeApi('messages.getPollResults', {
peer: inputPeer,
msg_id: messageID
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
this.log('getResults updates:', updates);
});
}
public getVotes(mid: number, option?: Uint8Array, offset?: string, limit = 20) {
const message = appMessagesManager.getMessage(mid);
const inputPeer = appPeersManager.getInputPeerByID(message.peerID);
const messageID = message.id;
let flags = 0;
if(option) {
flags |= 1 << 0;
}
if(offset) {
flags |= 1 << 1;
}
return apiManager.invokeApi('messages.getPollVotes', {
flags,
peer: inputPeer,
id: messageID,
option,
offset,
limit
}).then((votesList: {
_: 'messages.votesList',
flags: number,
count: number,
next_offset: string,
pFlags: {},
users: User[],
votes: {
_: 'messageUserVoteInputOption',
date: number,
user_id: number
}[]
}) => {
this.log('getPollVotes messages:', votesList);
appUsersManager.saveApiUsers(votesList.users);
return votesList;
});
}
}
const appPollsManager = new AppPollsManager();
(window as any).appPollsManager = appPollsManager;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appPollsManager = appPollsManager;
}
export default appPollsManager;

5
src/lib/appManagers/appSidebarLeft.ts

@ -210,5 +210,8 @@ export class AppSidebarLeft extends SidebarSlider { @@ -210,5 +210,8 @@ export class AppSidebarLeft extends SidebarSlider {
}
const appSidebarLeft = new AppSidebarLeft();
(window as any).appSidebarLeft = appSidebarLeft;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appSidebarLeft = appSidebarLeft;
}
export default appSidebarLeft;

223
src/lib/appManagers/appSidebarRight.ts

@ -19,9 +19,12 @@ import appForward from "../../components/appForward"; @@ -19,9 +19,12 @@ import appForward from "../../components/appForward";
import { mediaSizes } from "../config";
import SidebarSlider, { SliderTab } from "../../components/slider";
import appStickersManager, { MTStickerSet, MTStickerSetCovered, MTStickerSetMultiCovered } from "./appStickersManager";
import animationIntersector, { AnimationItem } from "../../components/animationIntersector";
import animationIntersector from "../../components/animationIntersector";
import PopupStickers from "../../components/popupStickers";
import SearchInput from "../../components/searchInput";
import appPollsManager from "./appPollsManager";
import { roundPercents } from "../../components/poll";
import appDialogsManager from "./appDialogsManager";
const testScroll = false;
@ -53,7 +56,7 @@ class AppStickersTab implements SliderTab { @@ -53,7 +56,7 @@ class AppStickersTab implements SliderTab {
this.scrollable = new Scrollable(this.contentDiv, 'y', 'STICKERS-SEARCH', undefined, undefined, 2);
this.scrollable.setVirtualContainer(this.setsDiv);
this.lazyLoadQueue = new LazyLoadQueue(5);
this.lazyLoadQueue = new LazyLoadQueue();
this.searchInput = new SearchInput('Search Stickers', (value) => {
this.search(value);
@ -262,13 +265,137 @@ class AppStickersTab implements SliderTab { @@ -262,13 +265,137 @@ class AppStickersTab implements SliderTab {
}
}
class AppPollResultsTab implements SliderTab {
private container = document.getElementById('poll-results-container') as HTMLDivElement;
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
private resultsDiv = this.contentDiv.firstElementChild as HTMLDivElement;
private scrollable: Scrollable;
private pollID: string;
private mid: number;
constructor() {
this.scrollable = new Scrollable(this.contentDiv, 'y', 'POLL-RESULTS', undefined, undefined, 2);
}
public cleanup() {
this.resultsDiv.innerHTML = '';
this.pollID = '';
this.mid = 0;
}
public onCloseAfterTimeout() {
this.cleanup();
}
public init(pollID: string, mid: number) {
if(this.pollID == pollID && this.mid == mid) return;
this.cleanup();
this.pollID = pollID;
this.mid = mid;
appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.pollResults);
const poll = appPollsManager.getPoll(pollID);
const title = document.createElement('h3');
title.innerHTML = poll.poll.rQuestion;
const percents = poll.results.results.map(v => v.voters / poll.results.total_voters * 100);
roundPercents(percents);
const fragment = document.createDocumentFragment();
poll.results.results.forEach((result, idx) => {
if(!result.voters) return;
const hr = document.createElement('hr');
const answer = poll.poll.answers[idx];
// Head
const answerEl = document.createElement('div');
answerEl.classList.add('poll-results-answer');
const answerTitle = document.createElement('div');
answerTitle.innerHTML = RichTextProcessor.wrapEmojiText(answer.text);
const answerPercents = document.createElement('div');
answerPercents.innerText = Math.round(percents[idx]) + '%';
answerEl.append(answerTitle, answerPercents);
// Humans
const list = document.createElement('ul');
list.classList.add('poll-results-voters');
appDialogsManager.setListClickListener(list);
list.style.minHeight = Math.min(result.voters, 4) * 50 + 'px';
fragment.append(hr, answerEl, list);
let offset: string, limit = 4, loading = false, left = result.voters - 4;
const load = () => {
if(loading) return;
loading = true;
appPollsManager.getVotes(mid, answer.option, offset, limit).then(votesList => {
votesList.votes.forEach(vote => {
const {dom} = appDialogsManager.addDialog(vote.user_id, list, false, false, undefined, false);
dom.lastMessageSpan.parentElement.remove();
});
if(offset) {
left -= votesList.votes.length;
(showMore.lastElementChild as HTMLElement).innerText = `Show ${Math.min(20, left)} more voter${left > 1 ? 's' : ''}`;
}
offset = votesList.next_offset;
limit = 20;
if(!left || !votesList.votes.length) {
showMore.remove();
}
}).finally(() => {
loading = false;
});
};
load();
if(left <= 0) return;
const showMore = document.createElement('div');
showMore.classList.add('poll-results-more');
showMore.addEventListener('click', load);
showMore.innerHTML = `<div class="tgico-down"></div><div>Show ${Math.min(20, left)} more voter${left > 1 ? 's' : ''}</div>`;
ripple(showMore);
fragment.append(showMore);
});
this.resultsDiv.append(title, fragment);
appSidebarRight.toggleSidebar(true).then(() => {
/* appPollsManager.getVotes(mid).then(votes => {
console.log('gOt VotEs', votes);
}); */
});
}
}
const stickersTab = new AppStickersTab();
const pollResultsTab = new AppPollResultsTab();
export class AppSidebarRight extends SidebarSlider {
public static SLIDERITEMSIDS = {
search: 1,
forward: 2,
stickers: 3
stickers: 3,
pollResults: 4,
};
public profileContainer: HTMLDivElement;
@ -303,7 +430,7 @@ export class AppSidebarRight extends SidebarSlider { @@ -303,7 +430,7 @@ export class AppSidebarRight extends SidebarSlider {
public sharedMediaType: AppSidebarRight['sharedMediaTypes'][number] = '';
private sharedMediaSelected: HTMLDivElement = null;
private lazyLoadQueue = new LazyLoadQueue(5);
private lazyLoadQueue = new LazyLoadQueue();
public historiesStorage: {
[peerID: number]: {
@ -337,15 +464,18 @@ export class AppSidebarRight extends SidebarSlider { @@ -337,15 +464,18 @@ export class AppSidebarRight extends SidebarSlider {
private loadMutex: Promise<any> = Promise.resolve();
public stickersTab: AppStickersTab;
public pollResultsTab: AppPollResultsTab;
constructor() {
super(document.getElementById('column-right') as HTMLElement, {
[AppSidebarRight.SLIDERITEMSIDS.stickers]: stickersTab,
[AppSidebarRight.SLIDERITEMSIDS.pollResults]: pollResultsTab,
});
this._selectTab(3);
//this._selectTab(3);
this.stickersTab = stickersTab;
this.pollResultsTab = pollResultsTab;
this.profileContainer = this.sidebarEl.querySelector('.profile-container');
this.profileContentEl = this.sidebarEl.querySelector('.profile-content');
@ -609,49 +739,45 @@ export class AppSidebarRight extends SidebarSlider { @@ -609,49 +739,45 @@ export class AppSidebarRight extends SidebarSlider {
}
public async performSearchResult(messages: any[], type: string) {
let peerID = this.peerID;
const peerID = this.peerID;
const elemsToAppend: HTMLElement[] = [];
const promises: Promise<any>[] = [];
let sharedMediaDiv: HTMLDivElement;
let elemsToAppend: HTMLElement[] = [];
let promises: Promise<any>[] = [];
// https://core.telegram.org/type/MessagesFilter
switch(type) {
case 'inputMessagesFilterPhotoVideo': {
sharedMediaDiv = this.sharedMedia.contentMedia;
for(let message of messages) {
let media = message.media.photo || message.media.document || (message.media.webpage && message.media.webpage.document);
for(const message of messages) {
const media = message.media.photo || message.media.document || (message.media.webpage && message.media.webpage.document);
let div = document.createElement('div');
const div = document.createElement('div');
div.classList.add('media-item');
//console.log(message, photo);
let isPhoto = media._ == 'photo';
const isPhoto = media._ == 'photo';
let photo = isPhoto ? appPhotosManager.getPhoto(media.id) : null;
const photo = isPhoto ? appPhotosManager.getPhoto(media.id) : null;
let isDownloaded: boolean;
if(photo) {
isDownloaded = photo.downloaded > 0;
} else {
let cachedThumb = appPhotosManager.getDocumentCachedThumb(media.id);
const cachedThumb = appPhotosManager.getDocumentCachedThumb(media.id);
isDownloaded = cachedThumb?.downloaded > 0;
}
let img = new Image();
img.classList.add('media-image');
div.append(img);
//this.log('inputMessagesFilterPhotoVideo', message, media);
if(!isPhoto) {
let span = document.createElement('span');
const span = document.createElement('span');
span.classList.add('video-time');
div.append(span);
if(media.type != 'gif') {
span.innerText = (media.duration + '').toHHMMSS(false);
/* let spanPlay = document.createElement('span');
/* const spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
div.append(spanPlay); */
} else {
@ -659,31 +785,54 @@ export class AppSidebarRight extends SidebarSlider { @@ -659,31 +785,54 @@ export class AppSidebarRight extends SidebarSlider {
}
}
let load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200))
const load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200))
.then(() => {
if($rootScope.selectedPeerID != peerID) {
this.log.warn('peer changed');
return;
}
let url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url;
const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url;
if(url) {
renderImageFromUrl(img, url);
//if(needBlur) return;
const p = renderImageFromUrl(img, url);
if(needBlur) {
p.then(() => {
//void img.offsetLeft; // reflow
img.style.opacity = '';
});
}
}
});
let thumb: HTMLImageElement;
const sizes = media.sizes || media.thumbs;
const willHaveThumb = !isDownloaded && sizes && sizes[0].bytes;
if(willHaveThumb) {
thumb = new Image();
thumb.classList.add('media-image', 'thumbnail');
thumb.dataset.mid = '' + message.mid;
appPhotosManager.setAttachmentPreview(sizes[0].bytes, thumb, false, false);
div.append(thumb);
}
const needBlur = !isDownloaded || !willHaveThumb;
const img = new Image();
img.dataset.mid = '' + message.mid;
img.classList.add('media-image');
if(needBlur) img.style.opacity = '0';
div.append(img);
let sizes = media.sizes || media.thumbs;
if(isDownloaded || (sizes && sizes[0].bytes)) {
let promise = new Promise((resolve, reject) => {
img.addEventListener('load', () => {
if(isDownloaded || willHaveThumb) {
const promise = new Promise((resolve, reject) => {
(thumb || img).addEventListener('load', () => {
clearTimeout(timeout);
resolve();
});
let timeout = setTimeout(() => {
this.log('did not loaded', img, media, isDownloaded, sizes);
const timeout = setTimeout(() => {
this.log('did not loaded', thumb, media, isDownloaded, sizes);
reject();
}, 1e3);
});
@ -692,19 +841,10 @@ export class AppSidebarRight extends SidebarSlider { @@ -692,19 +841,10 @@ export class AppSidebarRight extends SidebarSlider {
}
if(isDownloaded) load();
else {
if(sizes && sizes[0].bytes) {
appPhotosManager.setAttachmentPreview(sizes[0].bytes, img, false, false);
}
this.lazyLoadQueue.push({div, load});
}
else this.lazyLoadQueue.push({div, load});
elemsToAppend.push(div);
this.mediaDivsByIDs[message.mid] = div;
//sharedMediaDiv.append(div);
}
break;
@ -1047,5 +1187,8 @@ export class AppSidebarRight extends SidebarSlider { @@ -1047,5 +1187,8 @@ export class AppSidebarRight extends SidebarSlider {
}
const appSidebarRight = new AppSidebarRight();
(window as any).appSidebarRight = appSidebarRight;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appSidebarRight = appSidebarRight;
}
export default appSidebarRight;

8
src/lib/appManagers/appStickersManager.ts

@ -158,6 +158,7 @@ class AppStickersManager { @@ -158,6 +158,7 @@ class AppStickersManager {
public getAnimatedEmojiSticker(emoji: string) {
let stickerSet = this.stickerSets.emoji;
if(!stickerSet || !stickerSet.documents) return undefined;
emoji = emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, '');
return stickerSet.documents.find(doc => doc.stickerEmojiRaw == emoji);
@ -191,7 +192,7 @@ class AppStickersManager { @@ -191,7 +192,7 @@ class AppStickersManager {
const savedSets: {[id: string]: MTStickerSetFull} = {};
for(const id in this.stickerSets) {
const set = this.stickerSets[id];
if(set.set.installed_date) {
if(set.set.installed_date || id == 'emoji') {
savedSets[id] = set;
}
}
@ -328,5 +329,8 @@ class AppStickersManager { @@ -328,5 +329,8 @@ class AppStickersManager {
}
const appStickersManager = new AppStickersManager();
(window as any).appStickersManager = appStickersManager;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appStickersManager = appStickersManager;
}
export default appStickersManager;

5
src/lib/appManagers/appWebpManager.ts

@ -95,5 +95,8 @@ class AppWebpManager { @@ -95,5 +95,8 @@ class AppWebpManager {
}
const appWebpManager = new AppWebpManager();
(window as any).appWebpManager = appWebpManager;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appWebpManager = appWebpManager;
}
export default appWebpManager;

5
src/lib/cacheStorage.ts

@ -60,5 +60,8 @@ class CacheStorageController { @@ -60,5 +60,8 @@ class CacheStorageController {
}
const cacheStorage = new CacheStorageController();
(window as any).cacheStorage = cacheStorage;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).cacheStorage = cacheStorage;
}
export default cacheStorage;

8
src/lib/filemanager.ts

@ -78,8 +78,8 @@ class FileManager { @@ -78,8 +78,8 @@ class FileManager {
});
} */
public write(fileWriter: any, bytes: any): Promise<void> {
if(bytes.file) {
public write(fileWriter: any, bytes: Uint8Array | Blob | {file: any}): Promise<void> {
if('file' in bytes) {
return bytes.file((file: any) => {
return fileWriter.write(file);
});
@ -115,9 +115,9 @@ class FileManager { @@ -115,9 +115,9 @@ class FileManager {
}
public getFakeFileWriter(mimeType: string, saveFileCallback: any) {
var blobParts: Array<Blob> = [];
var blobParts: Array<Uint8Array> = [];
var fakeFileWriter = {
write: async(blob: Blob) => {
write: async(blob: Uint8Array) => {
if(!this.blobSupported) {
throw false;
}

15
src/lib/lottieLoader.ts

@ -482,6 +482,7 @@ class LottieLoader { @@ -482,6 +482,7 @@ class LottieLoader {
public loadPromise: Promise<void>;
public loaded = false;
// https://github.com/telegramdesktop/tdesktop/blob/35e575c2d7b56446be95561e4565628859fb53d3/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp#L65
private static COLORREPLACEMENTS = [
[
[0xf77e41, 0xca907a],
@ -509,6 +510,13 @@ class LottieLoader { @@ -509,6 +510,13 @@ class LottieLoader {
[0xffb139, 0x925a34],
[0xffd140, 0xa16e46],
[0xffdf79, 0xac7a52],
],
[
[0xf77e41, 0x291c12],
[0xffb139, 0x472a22],
[0xffd140, 0x573b30],
[0xffdf79, 0x68493c],
]
];
@ -559,7 +567,7 @@ class LottieLoader { @@ -559,7 +567,7 @@ class LottieLoader {
}
private applyReplacements(object: any, toneIndex: number) {
const replacements = LottieLoader.COLORREPLACEMENTS[toneIndex - 2];
const replacements = LottieLoader.COLORREPLACEMENTS[Math.max(toneIndex - 1, 0)];
const iterateIt = (it: any) => {
for(let smth of it) {
@ -680,5 +688,8 @@ class LottieLoader { @@ -680,5 +688,8 @@ class LottieLoader {
}
const lottieLoader = new LottieLoader();
(window as any).LottieLoader = lottieLoader;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).lottieLoader = lottieLoader;
}
export default lottieLoader;

30
src/lib/mediaPlayer.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
export class MediaProgressLine {
public container: HTMLDivElement;
private filled: HTMLDivElement;
private filledLoad: HTMLDivElement;
private seek: HTMLInputElement;
private duration = 0;
@ -8,13 +9,21 @@ export class MediaProgressLine { @@ -8,13 +9,21 @@ export class MediaProgressLine {
private stopAndScrubTimeout = 0;
private progressRAF = 0;
constructor(private media: HTMLAudioElement | HTMLVideoElement) {
public onSeek: (time: number) => void;
constructor(private media: HTMLAudioElement | HTMLVideoElement, streamable = false) {
this.container = document.createElement('div');
this.container.classList.add('media-progress');
this.filled = document.createElement('div');
this.filled.classList.add('media-progress__filled');
if(streamable) {
this.filledLoad = document.createElement('div');
this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded');
this.container.append(this.filledLoad);
}
let seek = this.seek = document.createElement('input');
seek.classList.add('media-progress__seek');
seek.value = '0';
@ -87,6 +96,10 @@ export class MediaProgressLine { @@ -87,6 +96,10 @@ export class MediaProgressLine {
this.mousedown = false;
};
public setLoadProgress(percents: number) {
this.filledLoad.style.transform = 'scaleX(' + percents + ')';
}
private setSeekMax() {
this.duration = this.media.duration;
if(this.duration > 0) {
@ -116,10 +129,13 @@ export class MediaProgressLine { @@ -116,10 +129,13 @@ export class MediaProgressLine {
private scrub(e: MouseEvent) {
const scrubTime = e.offsetX / this.container.offsetWidth * this.duration;
this.media.currentTime = scrubTime;
if(this.onSeek) {
this.onSeek(scrubTime);
}
let scaleX = scrubTime / this.duration;
scaleX = Math.max(0, Math.min(1, scaleX));
this.filled.style.transform = 'scaleX(' + scaleX + ')';
}
@ -132,6 +148,8 @@ export class MediaProgressLine { @@ -132,6 +148,8 @@ export class MediaProgressLine {
this.container.removeEventListener('mousedown', this.onMouseDown);
this.container.removeEventListener('mouseup', this.onMouseUp);
this.onSeek = null;
if(this.stopAndScrubTimeout) {
clearTimeout(this.stopAndScrubTimeout);
}
@ -144,10 +162,10 @@ export class MediaProgressLine { @@ -144,10 +162,10 @@ export class MediaProgressLine {
export default class VideoPlayer {
public wrapper: HTMLDivElement;
public progress: MediaProgressLine;
private skin: string;
private progress: MediaProgressLine;
constructor(public video: HTMLVideoElement, play = false) {
constructor(public video: HTMLVideoElement, play = false, streamable = false) {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('ckin__player');
@ -160,7 +178,7 @@ export default class VideoPlayer { @@ -160,7 +178,7 @@ export default class VideoPlayer {
if(this.skin == 'default') {
let controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement;
this.progress = new MediaProgressLine(video);
this.progress = new MediaProgressLine(video, streamable);
controls.prepend(this.progress.container);
}

174
src/lib/mtproto/apiFileManager.ts

@ -8,6 +8,12 @@ import apiManager from "./mtprotoworker"; @@ -8,6 +8,12 @@ import apiManager from "./mtprotoworker";
import { logger, deferredPromise, CancellablePromise } from "../polyfill";
import appWebpManager from "../appManagers/appWebpManager";
type Delayed = {
offset: number,
writeFilePromise: CancellablePromise<unknown>,
writeFileDeferred: CancellablePromise<unknown>
};
export class ApiFileManager {
public cachedSavePromises: {
[fileName: string]: Promise<Blob>
@ -265,7 +271,8 @@ export class ApiFileManager { @@ -265,7 +271,8 @@ export class ApiFileManager {
mimeType: string,
toFileEntry: any,
limitPart: number,
stickerType: number
stickerType: number,
processPart: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise<any>
}> = {}): CancellablePromise<Blob> {
if(!FileManager.isAvailable()) {
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
@ -286,10 +293,10 @@ export class ApiFileManager { @@ -286,10 +293,10 @@ export class ApiFileManager {
}
// this.log('Dload file', dcID, location, size)
let fileName = this.getFileName(location, options);
let toFileEntry = options.toFileEntry || null;
let cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName];
let fileStorage = this.getFileStorage();
const fileName = this.getFileName(location, options);
const toFileEntry = options.toFileEntry || null;
const cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName];
const fileStorage = this.getFileStorage();
//this.log('downloadFile', fileStorage.name, fileName, fileName.length, location, arguments);
@ -321,13 +328,13 @@ export class ApiFileManager { @@ -321,13 +328,13 @@ export class ApiFileManager {
}
}
let deferred = deferredPromise<Blob>();
const deferred = deferredPromise<Blob>();
const mimeType = options.mimeType || 'image/jpeg';
var canceled = false;
var resolved = false;
var mimeType = options.mimeType || 'image/jpeg',
cacheFileWriter: any;
var errorHandler = (error: any) => {
let canceled = false;
let resolved = false;
let cacheFileWriter: any;
let errorHandler = (error: any) => {
deferred.reject(error);
errorHandler = () => {};
@ -347,32 +354,23 @@ export class ApiFileManager { @@ -347,32 +354,23 @@ export class ApiFileManager {
}
if(toFileEntry) {
FileManager.copy(blob, toFileEntry).then(() => {
deferred.resolve();
}, errorHandler);
FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
} else {
deferred.resolve(this.cachedDownloads[fileName] = blob);
}
}).catch(() => {
//this.log('not i wanted');
//var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
var fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
var processDownloaded = (bytes: Uint8Array) => {
if(processSticker) {
return appWebpManager.convertToPng(bytes);
}
return Promise.resolve(bytes);
};
const fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
fileWriterPromise.then((fileWriter: any) => {
cacheFileWriter = fileWriter;
var limit = options.limitPart || 524288,
offset;
var startOffset = 0;
var writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
const limit = options.limitPart || 524288;
let offset: number;
let startOffset = 0;
let writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
writeFileDeferred: CancellablePromise<unknown>;
const maxRequests = options.processPart ? 5 : 5;
if(fileWriter.length) {
startOffset = fileWriter.length;
@ -393,62 +391,98 @@ export class ApiFileManager { @@ -393,62 +391,98 @@ export class ApiFileManager {
/////this.log('deferred notify 1:', {done: startOffset, total: size});
}
for(offset = startOffset; offset < size; offset += limit) {
//writeFileDeferred = $q.defer();
let writeFileDeferredHelper: any = {};
writeFileDeferred = new Promise((resolve, reject) => {
writeFileDeferredHelper.resolve = resolve;
writeFileDeferredHelper.reject = reject;
});
Object.assign(writeFileDeferred, writeFileDeferredHelper);
const processDownloaded = async(bytes: Uint8Array, offset: number) => {
if(options.processPart) {
await options.processPart(bytes, offset, delayed);
}
if(processSticker) {
return appWebpManager.convertToPng(bytes);
}
return bytes;
};
const delayed: Delayed[] = [];
for(offset = startOffset; offset < size; offset += limit) {
writeFileDeferred = deferredPromise<void>();
delayed.push({offset, writeFilePromise, writeFileDeferred});
writeFilePromise = writeFileDeferred;
////this.log('offset:', startOffset);
}
;((isFinal, offset, writeFileDeferred, writeFilePromise) => {
return this.downloadRequest(dcID, () => {
// для потокового видео нужно скачать первый и последний чанки
if(options.processPart && delayed.length > 2) {
const last = delayed.splice(delayed.length - 1, 1)[0];
delayed.splice(1, 0, last);
}
// @ts-ignore
//deferred.queue = delayed;
let done = 0;
const superpuper = async() => {
//if(!delayed.length) return;
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
try {
const result: any = await this.downloadRequest(dcID, () => {
if(canceled) {
return Promise.resolve();
}
return apiManager.invokeApi('upload.getFile', {
flags: 0,
location: location,
offset: offset,
limit: limit
location,
offset,
limit
}, {
dcID: dcID,
dcID,
fileDownload: true/* ,
singleInRequest: 'safari' in window */
});
}, 2).then((result: any) => {
writeFilePromise.then(() => {
if(canceled) {
return Promise.resolve();
}
return processDownloaded(result.bytes).then((processedResult) => {
return FileManager.write(fileWriter, processedResult).then(() => {
writeFileDeferred.resolve();
}, errorHandler).then(() => {
if(isFinal) {
resolved = true;
if(toFileEntry) {
deferred.resolve();
} else {
deferred.resolve(this.cachedDownloads[fileName] = fileWriter.finalize());
}
} else {
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
deferred.notify({done: offset + limit, total: size});
}
});
});
});
}, errorHandler);
})(offset + limit >= size, offset, writeFileDeferred, writeFilePromise);
}, 2);
writeFilePromise = writeFileDeferred;
if(delayed.length) {
superpuper();
}
//////////////////////////////////////////
const processedResult = await processDownloaded(result.bytes, offset);
if(canceled) {
return Promise.resolve();
}
done += limit;
const isFinal = offset + limit >= size;
//if(!isFinal) {
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
deferred.notify({done, offset, total: size});
//}
await writeFilePromise;
if(canceled) {
return Promise.resolve();
}
await FileManager.write(fileWriter, processedResult);
writeFileDeferred.resolve();
if(isFinal) {
resolved = true;
if(toFileEntry) {
deferred.resolve();
} else {
deferred.resolve(this.cachedDownloads[fileName] = fileWriter.finalize());
}
}
} catch(err) {
errorHandler(err);
}
};
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
superpuper();
}
});
});

2
src/lib/mtproto/mtproto.worker.js

@ -54,7 +54,7 @@ function fillTransfer(transfer, obj) { @@ -54,7 +54,7 @@ function fillTransfer(transfer, obj) {
}
function reply() {
if(isSafari(self)) {
if(isSafari(self)/* || true */) {
ctx.postMessage(...arguments);
} else {
var transfer = new Set();

5
src/lib/mtproto/mtprotoworker.ts

@ -152,5 +152,8 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -152,5 +152,8 @@ class ApiManagerProxy extends CryptoWorkerMethods {
}
const apiManagerProxy = new ApiManagerProxy();
(window as any).apiManagerProxy = apiManagerProxy;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).apiManagerProxy = apiManagerProxy;
}
export default apiManagerProxy;

10
src/lib/mtproto/referenceDatabase.ts

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
class ReferenceDatabase {
}
const referenceDatabase = new ReferenceDatabase();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).referenceDatabase = referenceDatabase;
}
export default referenceDatabase;

67
src/lib/opusDecodeController.ts

@ -1,3 +1,6 @@ @@ -1,3 +1,6 @@
import { isSafari } from "./config";
import { logger, LogLevels } from "./polyfill";
type Result = {
bytes: Uint8Array,
waveform?: Uint8Array
@ -7,7 +10,8 @@ type Task = { @@ -7,7 +10,8 @@ type Task = {
pages: Uint8Array,
withWaveform: boolean,
waveform?: Uint8Array,
callback: {resolve: (result: Result) => void, reject: (err: Error) => void}
callback: {resolve: (result: Result) => void, reject: (err: any) => void},
timeout: number
};
export class OpusDecodeController {
@ -17,12 +21,13 @@ export class OpusDecodeController { @@ -17,12 +21,13 @@ export class OpusDecodeController {
private tasks: Array<Task> = [];
private keepAlive = false;
private isPlaySupportedResult: boolean;
private log = logger('OPUS', LogLevels.error);
public isPlaySupported() {
if(this.isPlaySupportedResult !== undefined) return this.isPlaySupportedResult;
const audio = document.createElement('audio');
return this.isPlaySupportedResult = !!(audio.canPlayType && audio.canPlayType('audio/ogg;').replace(/no/, ''));
return this.isPlaySupportedResult = !!(audio.canPlayType && audio.canPlayType('audio/ogg;').replace(/no/, ''))/* && false */;
}
public loadWavWorker() {
@ -32,6 +37,7 @@ export class OpusDecodeController { @@ -32,6 +37,7 @@ export class OpusDecodeController {
this.wavWorker.addEventListener('message', (e) => {
const data = e.data;
this.log('[WAV] got message:', data);
if(data && data.page) {
const bytes = data.page;
this.onTaskEnd(this.tasks.shift(), bytes);
@ -46,17 +52,20 @@ export class OpusDecodeController { @@ -46,17 +52,20 @@ export class OpusDecodeController {
this.worker.addEventListener('message', (e) => {
const data = e.data;
this.log('[DECODER] got message', data);
if(data.type == 'done') {
//this.log('[DECODER] send done to wav');
this.wavWorker.postMessage({command: 'done'});
if(data.waveform) {
this.tasks[0].waveform = data.waveform;
}
} else { // e.data contains decoded buffers as float32 values
//this.log('[DECODER] send encode to wav');
this.wavWorker.postMessage({
command: 'encode',
buffers: e.data
}, data.map((typedArray: Uint8Array) => typedArray.buffer));
}, isSafari ? undefined : data.map((typedArray: Uint8Array) => typedArray.buffer));
}
});
}
@ -71,8 +80,13 @@ export class OpusDecodeController { @@ -71,8 +80,13 @@ export class OpusDecodeController {
}
}
public onTaskEnd(task: Task, result: Uint8Array) {
task.callback.resolve({bytes: result, waveform: task.waveform});
public onTaskEnd(task: Task, result?: Uint8Array) {
if(!result) {
task.callback.reject('timeout');
} else {
clearTimeout(task.timeout);
task.callback.resolve({bytes: result, waveform: task.waveform});
}
if(this.tasks.length) {
this.executeNewTask(this.tasks[0]);
@ -81,14 +95,18 @@ export class OpusDecodeController { @@ -81,14 +95,18 @@ export class OpusDecodeController {
this.terminateWorkers();
}
public terminateWorkers() {
if(this.keepAlive || this.tasks.length) return;
this.worker.terminate();
this.worker = null;
public terminateWorkers(kill = false) {
if((this.keepAlive || this.tasks.length) && !kill) return;
this.wavWorker.terminate();
this.wavWorker = null;
if(this.worker) {
this.worker.terminate();
this.worker = null;
}
if(this.wavWorker) {
this.wavWorker.terminate();
this.wavWorker = null;
}
}
public executeNewTask(task: Task) {
@ -106,12 +124,25 @@ export class OpusDecodeController { @@ -106,12 +124,25 @@ export class OpusDecodeController {
//console.log('sending command to worker:', task);
//setTimeout(() => {
this.log('[DECODER] send decode');
this.worker.postMessage({
command: 'decode',
pages: task.pages,
waveform: task.withWaveform
}, [task.pages.buffer]);
}, isSafari ? undefined : [task.pages.buffer]);
//}, 1e3);
task.timeout = setTimeout(() => {
this.log.error('decode timeout'/* , task */);
this.terminateWorkers(true);
if(this.tasks.length) {
this.loadWorker();
this.loadWavWorker();
}
this.onTaskEnd(this.tasks.shift());
}, 2e3);
}
public pushDecodeTask(pages: Uint8Array, withWaveform: boolean) {
@ -119,7 +150,8 @@ export class OpusDecodeController { @@ -119,7 +150,8 @@ export class OpusDecodeController {
const task = {
pages,
withWaveform,
callback: {resolve, reject}
callback: {resolve, reject},
timeout: 0
};
this.loadWorker();
@ -139,4 +171,9 @@ export class OpusDecodeController { @@ -139,4 +171,9 @@ export class OpusDecodeController {
}
}
export default new OpusDecodeController();
const opusDecodeController = new OpusDecodeController();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).opusDecodeController = opusDecodeController;
}
export default opusDecodeController;

2
src/lib/polyfill.ts

@ -12,7 +12,7 @@ export enum LogLevels { @@ -12,7 +12,7 @@ export enum LogLevels {
};
export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | LogLevels.error) {
// @ts-ignore
if(process.env.NODE_ENV == 'production') {
if(process.env.NODE_ENV == 'production'/* || true */) {
level = LogLevels.error;
}

5
src/pages/pagesManager.ts

@ -38,5 +38,8 @@ class PagesManager { @@ -38,5 +38,8 @@ class PagesManager {
}
const pagesManager = new PagesManager();
(window as any).pagesManager = pagesManager;
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).pagesManager = pagesManager;
}
export default pagesManager;

55
src/scss/partials/_chat.scss

@ -16,7 +16,7 @@ $time-background: rgba(0, 0, 0, .35); @@ -16,7 +16,7 @@ $time-background: rgba(0, 0, 0, .35);
// border-bottom: 1px solid #DADCE0;
@include respond-to(handhelds) {
&.is-audio-shown, &.is-pinned.shown {
&.is-audio-shown, &.is-pinned-shown {
& + #bubbles {
margin-top: 52px;
}
@ -948,3 +948,56 @@ $time-background: rgba(0, 0, 0, .35); @@ -948,3 +948,56 @@ $time-background: rgba(0, 0, 0, .35);
margin-right: 2px;
}
}
.quiz-hint {
position: absolute;
left: 0;
display: flex;
width: 100%;
justify-content: center;
z-index: 5;
top: 10px;
align-items: center;
transform: translateY(calc(-100% - 10px));
transition: transform .2s ease;
&.active {
transform: translateY(0);
}
.container {
background: rgba(0, 0, 0, .7);
text-align: center;
width: auto;
padding: 12px 18px 12px 48px;
min-height: 48px;
border-radius: 12px;
line-height: 1.5;
color: white;
font-size: 15px;
max-width: 400px;
overflow: hidden;
text-align: left;
position: relative;
display: flex;
align-items: center;
flex-wrap: wrap;
.text {
word-break: break-word;
}
&:before {
content: $tgico-info2;
position: absolute;
left: 12px;
font-size: 1.5rem;
top: 12px;
}
a {
color: white;
border-bottom: 1px solid white;
}
}
}

232
src/scss/partials/_chatBubble.scss

@ -265,6 +265,10 @@ $bubble-margin: .25rem; @@ -265,6 +265,10 @@ $bubble-margin: .25rem;
overflow: visible;
vertical-align: unset;
}
.thumbnail {
position: absolute;
}
&.emoji-big {
font-size: 0;
@ -913,6 +917,7 @@ $bubble-margin: .25rem; @@ -913,6 +917,7 @@ $bubble-margin: .25rem;
.name {
cursor: pointer;
user-select: none;
}
&__container > .name {
@ -1030,25 +1035,34 @@ $bubble-margin: .25rem; @@ -1030,25 +1035,34 @@ $bubble-margin: .25rem;
.bubble__container {
margin-right: auto;
background-color: #ffffff;
border-radius: 6px 12px 12px 6px;
&, .poll-footer-button {
border-radius: 6px 12px 12px 6px;
}
}
&.is-group-first .bubble__container {
border-radius: 12px 12px 12px 6px;
&.is-group-first {
.bubble__container, .poll-footer-button {
border-radius: 12px 12px 12px 6px;
}
}
&.is-group-last .bubble__container {
border-radius: 6px 12px 12px 0px;
//border-radius: 12px 12px 12px 0px;
&:after {
&.is-group-last {
.bubble__container, .poll-footer-button {
border-radius: 6px 12px 12px 0px;
//border-radius: 12px 12px 12px 0px;
}
.bubble__container:after {
left: -8.4px;
background-image: url('assets/img/msg-tail-left.svg');
}
}
&.is-group-first.is-group-last .bubble__container {
border-radius: 12px 12px 12px 0px;
&.is-group-first.is-group-last {
.bubble__container, .poll-footer-button {
border-radius: 12px 12px 12px 0px;
}
}
&.forwarded .attachment,
@ -1164,29 +1178,38 @@ $bubble-margin: .25rem; @@ -1164,29 +1178,38 @@ $bubble-margin: .25rem;
.bubble__container {
margin-left: auto;
background-color: #eeffde;
border-radius: 12px 6px 6px 12px;
> .user-avatar {
left: auto;
right: -2.5rem;
}
&, .poll-footer-button {
border-radius: 12px 6px 6px 12px;
}
}
&.is-group-first .bubble__container {
border-radius: 12px 12px 6px 12px;
&.is-group-first {
.bubble__container, .poll-footer-button {
border-radius: 12px 12px 6px 12px;
}
}
&.is-group-last .bubble__container {
border-radius: 12px 6px 0px 12px;
&:after {
&.is-group-last {
.bubble__container, .poll-footer-button {
border-radius: 12px 6px 0px 12px;
}
.bubble__container:after {
right: -8.4px;
background-image: url('assets/img/msg-tail-right.svg');
}
}
&.is-group-first.is-group-last .bubble__container {
border-radius: 12px 12px 0px 12px;
&.is-group-first.is-group-last {
.bubble__container, .poll-footer-button {
border-radius: 12px 12px 0px 12px;
}
}
&.forwarded .attachment,
@ -1344,12 +1367,20 @@ $bubble-margin: .25rem; @@ -1344,12 +1367,20 @@ $bubble-margin: .25rem;
background-color: rgba(79, 174, 78, 0.08);
}
}
&-footer-button {
color: #4fae4e;
}
}
.progress-ring__circle {
stroke: #4fae4e;
}
}
&.is-sending poll-element {
pointer-events: none;
}
}
.reply-markup {
@ -1409,20 +1440,52 @@ $bubble-margin: .25rem; @@ -1409,20 +1440,52 @@ $bubble-margin: .25rem;
}
}
poll-element {
poll-element {
margin-top: -1px;
display: block;
min-width: 280px;
&:not(.is-closed):not(.is-voted) .poll-answer {
cursor: pointer;
}
.poll {
&-title {
font-weight: 500;
user-select: none;
}
&-desc {
font-size: 14px;
color: #707579;
margin-bottom: 7px;
user-select: none;
display: flex;
position: relative;
}
&-hint {
position: absolute;
right: 0;
font-size: 1.5rem;
color: #50a2e9;
cursor: pointer;
transform: scale(1);
transition: transform .2s ease;
&.active {
transform: scale(0);
pointer-events: none;
}
}
&-send-vote {
cursor: default;
}
&-avatars {
display: flex;
margin-left: 1rem;
}
&-answer {
@ -1430,11 +1493,11 @@ poll-element { @@ -1430,11 +1493,11 @@ poll-element {
position: relative;
padding-bottom: 20px;
padding-left: 34px;
cursor: pointer;
&-text {
margin-top: 7px;
margin-left: 14px;
user-select: none;
}
&-percents {
@ -1448,23 +1511,32 @@ poll-element { @@ -1448,23 +1511,32 @@ poll-element {
margin-left: -3px;
text-align: right;
width: 40px;
user-select: none;
}
&-selected {
position: absolute;
bottom: 3px;
left: 26px;
bottom: 1px;
left: 22px;
color: #fff;
background: #50a2e9;
border-radius: 50%;
height: 12px;
width: 12px;
font-size: 11px;
line-height: 15px;
height: 16px;
width: 16px;
font-weight: bold;
font-size: 14px;
line-height: 1.4;
opacity: 0;
animation: fadeIn .1s ease forwards;
animation-direction: reverse;
animation-delay: .24s;
text-align: center;
&:before {
content: $tgico-check;
//margin-left: 1px;
font-weight: bold;
}
}
html.no-touch &:hover {
@ -1480,12 +1552,31 @@ poll-element { @@ -1480,12 +1552,31 @@ poll-element {
animation: pollAnswerRotate 0.65s linear infinite;
}
}
&:not(.is-correct):not(.is-chosen) {
.poll-answer-selected {
display: none;
}
}
// Multiple answers
&.is-chosing {
.poll-answer-selected {
opacity: 1;
}
& ~ .poll-footer {
.poll-send-vote {
cursor: pointer;
}
}
}
}
&-votes-count {
color: #707579;
font-size: 14px;
margin-top: 7px;
user-select: none;
}
&-line {
@ -1504,6 +1595,77 @@ poll-element { @@ -1504,6 +1595,77 @@ poll-element {
fill: none;
}
}
&-footer {
text-align: center;
margin-top: 7px;
height: 21px;
}
&-footer-button {
cursor: pointer;
position: absolute;
left: 0;
margin-top: -7px;
width: 100%;
height: 41px;
color: #50a2e9;
//text-transform: uppercase;
font-weight: 500;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
//border-bottom-left-radius: 6px;
//border-bottom-right-radius: 12px;
font-size: 1rem;
line-height: 37px;
user-select: none;
overflow: hidden;
}
&-quiz-timer {
width: 30px;
height: 30px;
stroke: #DF3F40;
fill: none;
position: absolute;
right: 0;
}
}
&.is-quiz .poll {
&-answer {
&.is-chosen:not(.is-correct) {
use {
stroke: #DF3F40;
}
.poll-answer-selected {
background: #DF3F40;
line-height: 16px;
&:before {
content: $tgico-close;
font-size: 12px;
//margin-left: 2.5px;
}
}
}
}
&-line {
use {
}
}
}
avatar-element {
width: 20px;
height: 20px;
border: 1px solid #fff;
line-height: 20px;
font-size: 10px;
cursor: pointer;
}
& + .time {
@ -1524,6 +1686,20 @@ poll-element { @@ -1524,6 +1686,20 @@ poll-element {
top: 0;
transform: scale(1);
transition: .1s transform;
.poll-answer-selected {
display: flex!important;
opacity: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
font-size: 16px;
line-height: 16px;
animation: none;
transition: opacity .2s ease;
}
}
.animation-ring {

10
src/scss/partials/_chatlist.scss

@ -1,6 +1,12 @@ @@ -1,6 +1,12 @@
.chats-container {
position: relative;
/* .scrollable {
html.is-mac & {
overflow-y: scroll;
}
} */
.input-search {
position: relative;
width: 100%;
@ -70,6 +76,10 @@ @@ -70,6 +76,10 @@
flex-direction: column;
/* grid-gap: 4px; */
width: 100%;
html.is-mac & {
transform: translateZ(0);
}
}
li {

10
src/scss/partials/_ckin.scss

@ -42,7 +42,7 @@ @@ -42,7 +42,7 @@
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
position: relative;
font-size: 0;
overflow: hidden;
//overflow: hidden;
//border-radius: 5px;
cursor: pointer;
@ -181,6 +181,14 @@ html.no-touch .default.is-playing:hover .default__controls { @@ -181,6 +181,14 @@ html.no-touch .default.is-playing:hover .default__controls {
height: 5px;
transform: scaleX(0);
}
&__loaded {
background: rgba(255, 255, 255, 0.38);
position: absolute;
width: calc(100% - 16px);
left: 0;
top: 0;
}
}
}

2
src/scss/partials/_fonts.scss

@ -189,7 +189,7 @@ @@ -189,7 +189,7 @@
content: "\e933";
}
.tgico-info2:before {
content: "\e934";
content: $tgico-info2;
}
.tgico-keyboard:before {
content: $tgico-keyboard;

1
src/scss/partials/_ico.scss

@ -18,4 +18,5 @@ $tgico-unread: "\e96c"; @@ -18,4 +18,5 @@ $tgico-unread: "\e96c";
$tgico-unpin: "\e96b";
$tgico-unarchive: "\e968";
$tgico-smile: "\e963";
$tgico-info2: "\e934";
$tgico-keyboard: "\e935";

7
src/scss/partials/_mediaViewer.scss

@ -195,6 +195,11 @@ $move-duration: .35s; @@ -195,6 +195,11 @@ $move-duration: .35s;
user-select: none;
object-fit: cover;
opacity: 1;
//&.thumbnail {
position: absolute;
//z-index: -1;
//}
}
&.active {
@ -218,7 +223,7 @@ $move-duration: .35s; @@ -218,7 +223,7 @@ $move-duration: .35s;
width: 100%;
height: 100%;
transform: scale(1);
overflow: hidden;
//overflow: hidden; // WARNING
position: absolute;
}

96
src/scss/partials/_rightSidebar.scss

@ -264,7 +264,7 @@ @@ -264,7 +264,7 @@
overflow: hidden;
position: relative;
cursor: pointer;
background-color: #000;
//background-color: #000;
}
.video-time {
@ -286,8 +286,13 @@ @@ -286,8 +286,13 @@
top: 0;
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
opacity: 1;
transition: opacity .2s ease;
&.thumbnail {
filter: blur(7px);
}
}
/* span.video-play {
@ -553,3 +558,90 @@ @@ -553,3 +558,90 @@
}
}
}
#poll-results-container {
.poll-results {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
&-answer {
color: #707579;
padding: 0 16px 8px 16px;
margin: 0;
padding-bottom: 8px;
font-weight: 500;
justify-content: space-between;
display: flex;
font-size: 15px;
user-select: none;
@include respond-to(not-handhelds) {
padding: 0 24px 8px 24px;
}
}
&-more {
padding-top: 13px;
padding-bottom: 13px;
cursor: pointer;
user-select: none;
position: relative;
@include respond-to(not-handhelds) {
padding-left: 8px;
}
.tgico-down {
float: left;
padding-right: 32px;
padding-left: 16.5px;
font-size: 24px;
color: #707579;
}
}
h3 {
padding: 0 16px;
margin-top: 15px;
font-size: 20px;
margin-bottom: 16px;
@include respond-to(not-handhelds) {
padding: 0 24px;
}
}
hr {
margin-bottom: 15px;
margin-top: 7px;
}
avatar-element {
width: 32px;
height: 32px;
}
.user-caption {
padding: 6px 28px;
}
.user-title {
font-weight: normal;
}
li {
padding-bottom: 2px;
> .rp {
padding: 8px 5px;
height: 48px;
@include respond-to(not-handhelds) {
padding: 8px 13px;
}
}
}
}
}

33
src/scss/partials/popups/_createPoll.scss

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
.popup-create-poll {
$parent: ".popup";
#{$parent} {
&-container {
max-height: 468px;
}
}
.input-field {
.btn-icon {
position: absolute;
right: .5rem;
top: 50%;
z-index: 1;
transform: translateY(-50%);
opacity: 1;
transition: opacity .2s ease;
}
&:not(.is-filled), &:first-child:last-child {
.btn-icon {
pointer-events: none;
opacity: 0;
}
}
/* &:last-child:not(:nth-child(10)) {
.btn-icon {
display: none;
}
} */
}
}

9
src/scss/partials/popups/_datePicker.scss

@ -6,9 +6,6 @@ @@ -6,9 +6,6 @@
// max-width: 300px;
// max-height: 424px;
min-width: 300px;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 12px 14px;
}
@ -24,12 +21,6 @@ @@ -24,12 +21,6 @@
font-size: 20px;
}
&-body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
&-buttons {
flex-direction: row;
order: 2;

3
src/scss/partials/popups/_editAvatar.scss

@ -6,9 +6,6 @@ @@ -6,9 +6,6 @@
max-width: 600px;
//max-height: 600px;
padding: 15px 16px 16px 24px;
overflow: hidden;
display: flex;
flex-direction: column;
> button {
position: absolute;

5
src/scss/partials/popups/_mediaAttacher.scss

@ -1,11 +1,10 @@ @@ -1,11 +1,10 @@
.popup-send-photo {
.popup-new-media {
$parent: ".popup";
#{$parent} {
&-container {
width: 420px;
max-width: 420px;
overflow: hidden;
/* padding: 12px 20px 50px; */
padding: 12px 20px 32.5px;
@ -50,6 +49,7 @@ @@ -50,6 +49,7 @@
}
&-header {
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 9px;
@ -124,6 +124,7 @@ @@ -124,6 +124,7 @@
}
.input-field {
width: 100%;
margin-top: 1rem;
&::placeholder {

14
src/scss/partials/popups/_popup.scss

@ -45,6 +45,9 @@ @@ -45,6 +45,9 @@
backface-visibility: hidden;
transition-property: transform;
transition-duration: 0.3s;
display: flex;
flex-direction: column;
overflow: hidden;
}
&-close {
@ -67,6 +70,17 @@ @@ -67,6 +70,17 @@
align-items: center;
}
&-body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
.scrollable {
position: relative;
}
}
&-buttons {
display: flex;
flex-direction: column;

15
src/scss/partials/popups/_stickers.scss

@ -30,9 +30,6 @@ @@ -30,9 +30,6 @@
max-width: 420px;
max-height: 420px;
width: 420px;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
}
@ -47,21 +44,9 @@ @@ -47,21 +44,9 @@
flex: 0 0 auto;
margin-top: 5px;
}
&-body {
flex: 1 1 auto;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
.scrollable {
position: relative;
}
.sticker-set {
margin-bottom: 8px;
&-stickers {

2
src/scss/style.scss

@ -55,6 +55,7 @@ $large-screen: 1680px; @@ -55,6 +55,7 @@ $large-screen: 1680px;
@import "partials/popups/peer";
@import "partials/popups/stickers";
@import "partials/popups/datePicker";
@import "partials/popups/createPoll";
@import "partials/pages/pages";
@import "partials/pages/authCode";
@ -396,6 +397,7 @@ input, textarea { @@ -396,6 +397,7 @@ input, textarea {
font-size: 1rem;
border-radius: $border-radius-medium;
animation: fadeInFadeOut 3s linear forwards;
z-index: 5;
}
hr {

5
src/types.d.ts vendored

@ -13,7 +13,7 @@ export type MTDocument = { @@ -13,7 +13,7 @@ export type MTDocument = {
attributes: any[],
thumb?: MTPhotoSize,
type?: string,
type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo',
h?: number,
w?: number,
file_name?: string,
@ -31,7 +31,8 @@ export type MTDocument = { @@ -31,7 +31,8 @@ export type MTDocument = {
stickerSetInput?: any,
stickerThumbConverted?: true,
animated?: boolean
animated?: boolean,
supportsStreaming?: boolean
};
export type MTPhotoSize = {

Loading…
Cancel
Save