Create group & channel; poll prepare

This commit is contained in:
morethanwords 2020-05-09 15:02:07 +03:00
parent 5f2ba41023
commit f7fcc780fd
36 changed files with 3161 additions and 1946 deletions

View File

@ -109,10 +109,19 @@
<input type="checkbox" id="keepSigned" checked="checked">
<span>Keep me signed in</span>
</label>
<button class="btn-primary" style="visibility: hidden;">NEXT</button>
<button class="btn-primary rp" style="visibility: hidden;">NEXT</button>
</div>
</div>
</div>
<div class="page-signQR">
<div class="container center-align">
<div class="auth-image">
<canvas id="qr-code"></canvas>
</div>
<h4>Scan from mobile Telegram</h4>
<p class="subtitle">Please confirm your country and<br> enter your phone number.</p>
</div>
</div>
<div class="page-authCode">
<div class="container center-align">
<div class="auth-image"></div>
@ -146,15 +155,9 @@
</div>
<div class="page-signUp">
<div class="container center-align">
<div class="auth-image">
<input type="file" id="avatar-input" style="display: none;" />
<canvas id="canvas-avatar"></canvas>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<polygon points="0 0 24 0 24 24 0 24"/>
<path fill="#fff" fill-rule="nonzero" d="M19.8833789,16.0067277 L20,16 C20.5128358,16 20.9355072,16.3860402 20.9932723,16.8833789 L21,17 L21,19 L23,19 C23.5128358,19 23.9355072,19.3860402 23.9932723,19.8833789 L24,20 C24,20.5128358 23.6139598,20.9355072 23.1166211,20.9932723 L23,21 L21,21 L21,23 C21,23.5128358 20.6139598,23.9355072 20.1166211,23.9932723 L20,24 C19.4871642,24 19.0644928,23.6139598 19.0067277,23.1166211 L19,23 L19,21 L17,21 C16.4871642,21 16.0644928,20.6139598 16.0067277,20.1166211 L16,20 C16,19.4871642 16.3860402,19.0644928 16.8833789,19.0067277 L17,19 L19,19 L19,17 C19,16.4871642 19.3860402,16.0644928 19.8833789,16.0067277 L20,16 L19.8833789,16.0067277 Z M8.41421356,2 L13.5857864,2 C14.0572824,2 14.5116128,2.16648982 14.8701798,2.46691315 L15,2.58578644 L16.4142136,4 L18,4 C19.5976809,4 20.9036609,5.24891996 20.9949073,6.82372721 L21,7 L21,12 C21,12.5522847 20.5522847,13 20,13 C19.4871642,13 19.0644928,12.6139598 19.0067277,12.1166211 L19,12 L19,7 C19,6.48716416 18.6139598,6.06449284 18.1166211,6.00672773 L18,6 L16.4142136,6 C15.9427176,6 15.4883872,5.83351018 15.1298202,5.53308685 L15,5.41421356 L13.5857864,4 L8.41421356,4 L7,5.41421356 C6.66660199,5.74761157 6.22761579,5.95114561 5.76163928,5.99225938 L5.58578644,6 L4,6 C3.48716416,6 3.06449284,6.38604019 3.00672773,6.88337887 L3,7 L3,18 C3,18.5128358 3.38604019,18.9355072 3.88337887,18.9932723 L4,19 L13,19 C13.5522847,19 14,19.4477153 14,20 C14,20.5128358 13.6139598,20.9355072 13.1166211,20.9932723 L13,21 L4,21 C2.40231912,21 1.09633912,19.75108 1.00509269,18.1762728 L1,18 L1,7 C1,5.40231912 2.24891996,4.09633912 3.82372721,4.00509269 L4,4 L5.58578644,4 L7,2.58578644 C7.33339801,2.25238843 7.77238421,2.04885439 8.23836072,2.00774062 L8.41421356,2 L13.5857864,2 L8.41421356,2 Z M11,7 C13.7614237,7 16,9.23857625 16,12 C16,14.7614237 13.7614237,17 11,17 C8.23857625,17 6,14.7614237 6,12 C6,9.23857625 8.23857625,7 11,7 Z M11,9 C9.34314575,9 8,10.3431458 8,12 C8,13.6568542 9.34314575,15 11,15 C12.6568542,15 14,13.6568542 14,12 C14,10.3431458 12.6568542,9 11,9 Z"/>
</g>
</svg>
<div class="auth-image avatar-edit">
<canvas class="avatar-edit-canvas" id="canvas-avatar"></canvas>
<span class="tgico tgico-cameraadd"></span>
</div>
<h4 class="fullName">Your Name</h4>
<p class="subtitle">Enter your name and add<br>a profile picture</p>
@ -167,36 +170,21 @@
<input type="text" name="lastName" id="lastName" autocomplete="off" required />
<label for="lastName">Last Name (optional)</label>
</div>
<button class="btn-primary" id="signUp">START MESSAGING</button>
<button class="btn-primary rp" id="signUp">START MESSAGING</button>
</div>
</div>
</div>
<div class="page-signQR">
<div class="container center-align">
<div class="auth-image">
<canvas id="qr-code"></canvas>
</div>
<h4>Scan from mobile Telegram</h4>
<p class="subtitle">Please confirm your country and<br> enter your phone number.</p>
</div>
</div>
</div>
<div class="popup popup-avatar">
<div class="popup-container z-depth-1">
<div class="popup-header">
<span class="popup-close tgico-close"></span>
<h6>Drag to Reposition</h6>
</div>
<div class="crop"></div>
<button class="btn-primary btn-circle btn-crop z-depth-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<polygon points="0 0 24 0 24 24 0 24"/>
<path fill-rule="nonzero" d="M4.70710678,12.2928932 C4.31658249,11.9023689 3.68341751,11.9023689 3.29289322,12.2928932 C2.90236893,12.6834175 2.90236893,13.3165825 3.29289322,13.7071068 L8.29289322,18.7071068 C8.68341751,19.0976311 9.31658249,19.0976311 9.70710678,18.7071068 L20.7071068,7.70710678 C21.0976311,7.31658249 21.0976311,6.68341751 20.7071068,6.29289322 C20.3165825,5.90236893 19.6834175,5.90236893 19.2928932,6.29289322 L9,16.5857864 L4.70710678,12.2928932 Z"/>
</g>
</svg>
</button>
</div>
<div class="popup popup-avatar hide" id="popup-avatar" style="display: none;">
<div class="popup-container z-depth-1">
<div class="popup-header">
<span class="popup-close tgico-close"></span>
<h6>Drag to Reposition</h6>
</div>
<div class="crop"></div>
<button class="btn-primary btn-circle btn-crop btn-icon tgico-check z-depth-1"></button>
<input type="file" style="display: none;" />
</div>
</div>
<div class="whole valign-wrapper page-chats" style="display: none;">
@ -204,6 +192,7 @@
<defs id="svg-defs">
<path id="message-tail" d="M1.00002881,1.03679295e-14 L7,0 L7,17 C6.8069969,14.1607017 6.12380234,11.2332513 4.95041634,8.21764872 C4.04604748,5.89342034 2.50413132,3.73337411 0.324667862,1.73751004 L0.324652538,1.73752677 C-0.0826597201,1.36452676 -0.110475289,0.731958677 0.262524727,0.324646419 C0.451952959,0.117792698 0.719544377,1.0985861e-14 1.00002881,1.04360964e-14 Z"></path>
<path id="logo" fill="#50A2E9" fill-rule="evenodd" d="M80,0 C124.18278,0 160,35.81722 160,80 C160,124.18278 124.18278,160 80,160 C35.81722,160 0,124.18278 0,80 C0,35.81722 35.81722,0 80,0 Z M114.262551,46.4516129 L114.123923,46.4516129 C111.089589,46.5056249 106.482806,48.0771432 85.1289541,56.93769 L81.4133571,58.4849956 C72.8664779,62.0684477 57.2607933,68.7965125 34.5963033,78.66919 C30.6591745,80.2345564 28.5967328,81.765936 28.4089783,83.2633288 C28.0626453,86.0254269 31.8703852,86.959903 36.7890378,88.5302703 L38.2642674,89.0045258 C42.3926354,90.314406 47.5534685,91.7248852 50.3250916,91.7847532 C52.9151948,91.8407003 55.7944784,90.8162976 58.9629426,88.7115451 L70.5121776,80.9327422 C85.6657026,70.7535853 93.6285785,65.5352892 94.4008055,65.277854 L94.6777873,65.216416 C95.1594319,65.1213105 95.7366278,65.0717596 96.1481181,65.4374337 C96.6344248,65.8695939 96.5866185,66.6880224 96.5351057,66.9075859 C96.127514,68.6448691 75.2839361,87.6143392 73.6629144,89.2417998 L73.312196,89.6016896 C68.7645143,94.2254793 63.9030972,97.1721503 71.5637945,102.355193 L73.3593638,103.544598 C79.0660342,107.334968 82.9483395,110.083813 88.8107882,113.958377 L90.3875424,114.996094 C95.0654739,118.061953 98.7330313,121.697601 103.562866,121.253237 C105.740839,121.052855 107.989107,119.042224 109.175465,113.09692 L109.246762,112.727987 C112.002037,98.0012935 117.417883,66.09303 118.669527,52.9443975 C118.779187,51.7924073 118.641237,50.318088 118.530455,49.6708963 L118.474159,49.3781963 C118.341081,48.7651315 118.067967,48.0040758 117.346762,47.4189793 C116.412565,46.6610871 115.002114,46.4638844 114.262551,46.4516129 Z"/>
<path id="poll-line" d="M4.47,2.83v13.6c0,4.97,4.03,9,9,9h458.16"/>
</defs>
</svg>
<div class="overlays">
@ -228,23 +217,9 @@
<div class="media-viewer-switcher-right">
<span class="tgico-down media-viewer-next-button"></span>
</div>
<div class="media-viewer-media">
<!-- <img src="assets/img/camomile.jpg" alt="">
<div class="preloader-container">
<div class="you-spin-me-round">
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50">
<circle class="preloader-path-new" cx="50" cy="50" r="23" fill="none" stroke-miterlimit="10"/>
</svg>
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 20 20">
<line x1="0" y1="20" x2="20" y2="0" stroke-width="1" stroke-linecap="round"></line>
<line x1="0" y1="0" x2="20" y2="20" stroke-width="1" stroke-linecap="round"></line>
</svg>
</div> -->
</div>
<div class="media-viewer-media"></div>
</div>
<div class="media-viewer-caption"></div>
<!-- <div class="media-viewer-loader a-loader"></div> -->
</div>
</div>
</div>
@ -275,7 +250,7 @@
</div>
<div class="chats-container sidebar sidebar-left" id="column-left">
<div class="sidebar-slider tabs-container">
<div class="sidebar-slider-item">
<div class="sidebar-slider-item item-main">
<div class="sidebar-header">
<div class="sidebar-header__btn-container">
<div class="btn-icon tgico-menu btn-menu-toggle rp sidebar-tools-button active">
@ -301,6 +276,13 @@
<ul id="dialogs"></ul>
</div>
<div class="sidebar-search hide" id="search-container"></div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-newchat_filled btn-menu-toggle" id="new-menu">
<div class="btn-menu top-left">
<div class="btn-menu-item menu-channel tgico-newchannel rp">New Channel</div>
<div class="btn-menu-item menu-group tgico-newgroup rp">New Group</div>
<div class="btn-menu-item menu-private-chat tgico-newprivate rp">New Private Chat</div>
</div>
</button>
</div>
</div>
<div class="sidebar-slider-item">
@ -328,10 +310,61 @@
</div>
</div>
</div>
<div class="sidebar-slider-item new-channel-container">
<div class="sidebar-header">
<button class="btn-icon rp tgico-back sidebar-close-button"></button>
<div class="sidebar-header__title">New Channel</div>
</div>
<div class="sidebar-content">
<div class="avatar-edit">
<canvas class="avatar-edit-canvas"></canvas>
<span class="tgico tgico-cameraadd"></span>
</div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="name" class="new-channel-name" autocomplete="xxDDqqOX" required="">
<label for="name">Channel Name</label>
</div>
<div class="input-field">
<input type="text" name="description" class="new-channel-description" autocomplete="aintsofunnow" required="">
<label for="lastName">Description (optional)</label>
</div>
</div>
<div class="caption">You can provide an optional description for your channel.</div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button>
</div>
</div>
<div class="sidebar-slider-item addmembers-container">
<div class="sidebar-header">
<button class="btn-icon rp tgico-back sidebar-close-button"></button>
<div class="sidebar-header__title">Add Members</div>
</div>
<div class="sidebar-content">
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button>
</div>
</div>
<div class="sidebar-slider-item new-group-container">
<div class="sidebar-header">
<button class="btn-icon rp tgico-back sidebar-close-button"></button>
<div class="sidebar-header__title">New Group</div>
</div>
<div class="sidebar-content">
<div class="avatar-edit">
<canvas class="avatar-edit-canvas"></canvas>
<span class="tgico tgico-cameraadd"></span>
</div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="name" class="new-group-name" autocomplete="feellikeamonster2112" required="">
<label for="name">Group Name</label>
</div>
</div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button>
</div>
</div>
</div>
</div>
<div class="chat-container">
<!-- <div class="chat-background"></div> -->
<canvas id="chat-background-canvas"></canvas>
<div id="topbar" style="display: none;">
<div class="chat-info">

View File

@ -1,161 +1,7 @@
import appSidebarRight from "../lib/appManagers/appSidebarRight";
import Scrollable from "./scrollable_new";
import appProfileManager from "../lib/appManagers/appProfileManager";
import { appPeersManager } from "../lib/services";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appDialogsManager from "../lib/appManagers/appDialogsManager";
import appChatsManager from "../lib/appManagers/appChatsManager";
import appUsersManager from "../lib/appManagers/appUsersManager";
import { $rootScope, findUpTag, findUpClassName, cancelEvent } from "../lib/utils";
import { putPreloader } from "./misc";
class AppSelectPeers {
public container = document.createElement('div');
private chatList = document.createElement('ul');
private chatsContainer = document.createElement('div');
private scrollable: Scrollable;
private selectedScrollable: Scrollable;
private selectedContainer = document.createElement('div');
private searchInput = document.createElement('input');
private selected: {[peerID: number]: HTMLDivElement} = {};
public freezed = false;
constructor(private appendTo: HTMLDivElement, private onChange: (length: number) => void) {
this.container.classList.add('selector');
let topContainer = document.createElement('div');
topContainer.classList.add('selector-search-container');
this.selectedContainer.classList.add('selector-search');
this.searchInput.placeholder = 'Select chat';
this.searchInput.type = 'text';
this.selectedContainer.append(this.searchInput);
topContainer.append(this.selectedContainer);
this.selectedScrollable = new Scrollable(topContainer);
let delimiter = document.createElement('hr');
this.chatsContainer.classList.add('chats-container');
this.chatsContainer.append(this.chatList);
this.scrollable = new Scrollable(this.chatsContainer);
// в десктопе - сначала без группы, потом архивные, потом контакты без сообщений
let offsetIndex = 0;
appMessagesManager.getConversations(offsetIndex, 50, 0).then(value => {
let dialogs = value.dialogs;
let myID = $rootScope.myID;
offsetIndex = dialogs[value.dialogs.length - 1].index || 0;
if(dialogs[0].peerID != myID) {
dialogs.findAndSplice(d => d.peerID == myID);
dialogs.unshift({
peerID: myID,
pFlags: {}
} as any);
}
dialogs.forEach(dialog => {
let peerID = dialog.peerID;
let {dom} = appDialogsManager.addDialog(dialog, this.chatList, false, false);
dom.containerEl.insertAdjacentHTML('afterbegin', '<div class="checkbox"><label><input type="checkbox"><span></span></label></div>');
let subtitle = '';
if(peerID < 0) {
subtitle = appChatsManager.getChatMembersString(-peerID);
} else if(peerID == myID) {
subtitle = 'chat with yourself';
} else {
subtitle = appUsersManager.getUserStatusString(peerID);
if(subtitle == 'online') {
subtitle = `<i>${subtitle}</i>`;
}
}
dom.lastMessageSpan.innerHTML = subtitle;
});
});
this.chatList.addEventListener('click', (e) => {
let target = e.target as HTMLElement;
cancelEvent(e);
if(this.freezed) return;
if(target.tagName != 'LI') {
target = findUpTag(target, 'LI');
}
if(!target) return;
let peerID = +target.getAttribute('data-peerID');
target.classList.toggle('active');
if(peerID in this.selected) {
this.remove(peerID);
} else {
this.add(peerID);
}
let checkbox = target.querySelector('input') as HTMLInputElement;
checkbox.checked = !checkbox.checked;
});
this.selectedContainer.addEventListener('click', (e) => {
if(this.freezed) return;
let target = e.target as HTMLElement;
target = findUpClassName(target, 'selector-user');
if(!target) return;
let peerID = target.dataset.peerID;
let li = this.chatList.querySelector('[data-peerid="' + peerID + '"]') as HTMLElement;
li.click();
});
this.container.append(topContainer, delimiter, this.chatsContainer);
appendTo.append(this.container);
}
private add(peerID: number) {
let div = document.createElement('div');
div.classList.add('selector-user', 'scale-in');
div.dataset.peerID = '' + peerID;
this.selected[peerID] = div;
let title = appPeersManager.getPeerTitle(peerID, false, true);
let avatarDiv = document.createElement('div');
avatarDiv.classList.add('user-avatar', 'tgico');
appProfileManager.putPhoto(avatarDiv, peerID);
div.innerHTML = title;
div.insertAdjacentElement('afterbegin', avatarDiv);
this.selectedContainer.insertBefore(div, this.searchInput);
this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight;
this.onChange(Object.keys(this.selected).length);
}
private remove(peerID: number) {
let div = this.selected[peerID];
div.classList.remove('scale-in');
void div.offsetWidth;
div.classList.add('scale-out');
div.addEventListener('animationend', () => {
delete this.selected[peerID];
div.remove();
this.onChange(Object.keys(this.selected).length);
}, {once: true});
}
public getSelected() {
return Object.keys(this.selected).map(p => +p);
}
}
import { AppSelectPeers } from "./appSelectPeers";
class AppForward {
private container = document.getElementById('forward-container') as HTMLDivElement;

View File

@ -13,10 +13,10 @@ export class SearchGroup {
nameEl: HTMLDivElement;
list: HTMLUListElement;
constructor(public name: string, public type: string, private clearable = true, className?: string) {
constructor(public name: string, public type: string, private clearable = true, className?: string, clickable = true) {
this.list = document.createElement('ul');
this.container = document.createElement('div');
if(className) this.container.classList.add(className);
if(className) this.container.className = className;
this.nameEl = document.createElement('div');
this.nameEl.classList.add('search-group__name');
this.nameEl.innerText = name;
@ -25,7 +25,9 @@ export class SearchGroup {
this.container.append(this.nameEl, this.list);
this.container.style.display = 'none';
appDialogsManager.setListClickListener(this.list);
if(clickable) {
appDialogsManager.setListClickListener(this.list);
}
}
clear() {

View File

@ -0,0 +1,214 @@
import Scrollable from "./scrollable_new";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import { $rootScope, cancelEvent, findUpTag, findUpClassName } from "../lib/utils";
import appDialogsManager from "../lib/appManagers/appDialogsManager";
import appChatsManager from "../lib/appManagers/appChatsManager";
import appUsersManager from "../lib/appManagers/appUsersManager";
import { appPeersManager } from "../lib/services";
import appProfileManager from "../lib/appManagers/appProfileManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager";
export class AppSelectPeers {
public container = document.createElement('div');
private list = document.createElement('ul');
private chatsContainer = document.createElement('div');
private scrollable: Scrollable;
private selectedScrollable: Scrollable;
private selectedContainer = document.createElement('div');
private input = document.createElement('input');
private selected: {[peerID: number]: HTMLDivElement} = {};
public freezed = false;
private myID = $rootScope.myID;
private offsetIndex = 0;
private promise: Promise<number[]>;
private query = '';
private cachedContacts: number[];
constructor(private appendTo: HTMLDivElement, private onChange?: (length: number) => void, private peerType: 'contacts' | 'dialogs' = 'dialogs') {
this.container.classList.add('selector');
let topContainer = document.createElement('div');
topContainer.classList.add('selector-search-container');
this.selectedContainer.classList.add('selector-search');
this.input.placeholder = peerType == 'contacts' ? 'Add People...' : 'Select chat';
this.input.type = 'text';
this.selectedContainer.append(this.input);
topContainer.append(this.selectedContainer);
this.selectedScrollable = new Scrollable(topContainer);
let delimiter = document.createElement('hr');
this.chatsContainer.classList.add('chats-container');
this.chatsContainer.append(this.list);
this.scrollable = new Scrollable(this.chatsContainer);
this.list.addEventListener('click', (e) => {
let target = e.target as HTMLElement;
cancelEvent(e);
if(this.freezed) return;
if(target.tagName != 'LI') {
target = findUpTag(target, 'LI');
}
if(!target) return;
let peerID = +target.getAttribute('data-peerID');
target.classList.toggle('active');
if(peerID in this.selected) {
this.remove(peerID);
} else {
this.add(peerID);
}
let checkbox = target.querySelector('input') as HTMLInputElement;
checkbox.checked = !checkbox.checked;
});
this.selectedContainer.addEventListener('click', (e) => {
if(this.freezed) return;
let target = e.target as HTMLElement;
target = findUpClassName(target, 'selector-user');
if(!target) return;
let peerID = target.dataset.peerID;
let li = this.list.querySelector('[data-peerid="' + peerID + '"]') as HTMLElement;
li.click();
});
this.input.addEventListener('input', () => {
let value = this.input.value;
if(this.query != value) {
if(this.peerType == 'contacts') {
this.cachedContacts = null;
this.promise = null;
}
this.list.innerHTML = '';
this.query = value;
console.log('selectPeers input:', this.query);
this.getMoreResults();
}
});
this.scrollable.onScrolledBottom = () => {
this.getMoreResults();
};
this.container.append(topContainer, delimiter, this.chatsContainer);
appendTo.append(this.container);
this.getMoreResults();
}
private getMoreDialogs() {
// в десктопе - сначала без группы, потом архивные, потом контакты без сообщений
appMessagesManager.getConversations(this.offsetIndex, 50, 0).then(value => {
let dialogs = value.dialogs;
this.offsetIndex = dialogs[value.dialogs.length - 1].index || 0;
if(dialogs[0].peerID != this.myID) {
dialogs.findAndSplice(d => d.peerID == this.myID);
dialogs.unshift({
peerID: this.myID,
pFlags: {}
} as any);
}
this.renderResults(dialogs.map(dialog => dialog.peerID));
});
}
private async getMoreContacts() {
if(this.promise) return this.promise;
if(!this.cachedContacts) {
this.promise = appUsersManager.getContacts(this.query);
this.cachedContacts = (await this.promise).slice();
this.cachedContacts.findAndSplice(userID => userID == this.myID); // no my account
this.promise = null;
}
if(this.cachedContacts.length) {
let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
let arr = this.cachedContacts.splice(0, pageCount);
this.renderResults(arr);
}
}
private getMoreResults() {
if(this.peerType == 'dialogs') {
this.getMoreDialogs();
} else {
this.getMoreContacts();
}
}
private renderResults(peerIDs: number[]) {
peerIDs.forEach(peerID => {
let {dom} = appDialogsManager.addDialog(peerID, this.list, false, false);
dom.containerEl.insertAdjacentHTML('afterbegin', '<div class="checkbox"><label><input type="checkbox"><span></span></label></div>');
let subtitle = '';
if(peerID < 0) {
subtitle = appChatsManager.getChatMembersString(-peerID);
} else if(peerID == this.myID) {
subtitle = 'chat with yourself';
} else {
subtitle = appUsersManager.getUserStatusString(peerID);
if(subtitle == 'online') {
subtitle = `<i>${subtitle}</i>`;
}
}
dom.lastMessageSpan.innerHTML = subtitle;
});
}
private add(peerID: number) {
let div = document.createElement('div');
div.classList.add('selector-user', 'scale-in');
div.dataset.peerID = '' + peerID;
this.selected[peerID] = div;
let title = appPeersManager.getPeerTitle(peerID, false, true);
let avatarDiv = document.createElement('div');
avatarDiv.classList.add('user-avatar', 'tgico');
appProfileManager.putPhoto(avatarDiv, peerID);
div.innerHTML = title;
div.insertAdjacentElement('afterbegin', avatarDiv);
this.selectedContainer.insertBefore(div, this.input);
this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight;
this.onChange && this.onChange(Object.keys(this.selected).length);
}
private remove(peerID: number) {
let div = this.selected[peerID];
div.classList.remove('scale-in');
void div.offsetWidth;
div.classList.add('scale-out');
div.addEventListener('animationend', () => {
delete this.selected[peerID];
div.remove();
this.onChange && this.onChange(Object.keys(this.selected).length);
}, {once: true});
}
public getSelected() {
return Object.keys(this.selected).map(p => +p);
}
}

200
src/components/poll.ts Normal file
View File

@ -0,0 +1,200 @@
import appPollsManager, { PollResults } from "../lib/appManagers/appPollsManager";
import { RichTextProcessor } from "../lib/richtextprocessor";
let lineTotalLength = 0;
const tailLength = 9;
const times = 10;
const fullTime = 340;
const oneTime = fullTime / times;
export default class PollElement extends HTMLElement {
private svgLines: SVGSVGElement[];
private numberDivs: HTMLDivElement[];
private maxOffset = -44.8;
private maxLength: number;
private maxLengths: number[];
constructor() {
super();
// элемент создан
}
connectedCallback() {
// браузер вызывает этот метод при добавлении элемента в документ
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
if(!lineTotalLength) {
lineTotalLength = (document.getElementById('poll-line') as any as SVGPathElement).getTotalLength();
console.log('line total length:', lineTotalLength);
}
let pollID = this.getAttribute('poll-id');
let {poll, results} = appPollsManager.getPoll(pollID);
console.log('pollElement poll:', poll, results);
let desc = '';
if(poll.pFlags) {
if(poll.pFlags.closed) {
desc = 'Final results';
} else {
desc = poll.pFlags.public_voters ? 'Public Poll' : 'Anonymous Poll';
}
}
let votes = poll.answers.map(answer => {
return `
<div class="poll-answer">
<div class="circle-hover">
<div class="animation-ring"></div>
<svg class="progress-ring">
<circle class="progress-ring__circle" cx="13" cy="13" r="9"></circle>
</svg>
</div>
<div class="poll-answer-percents"></div>
<svg version="1.1" class="poll-line" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 480 28" xml:space="preserve">
<use href="#poll-line"></use>
</svg>
<div class="poll-answer-text">${RichTextProcessor.wrapEmojiText(answer.text)}</div>
</div>
`;
}).join('');
this.innerHTML = `
<div class="poll-title">${poll.rQuestion}</div>
<div class="poll-desc">${desc}</div>
${votes}
<div class="poll-votes-count">${results.total_voters ? results.total_voters + ' voters' : 'No votes'}</div>
`;
let width = this.getBoundingClientRect().width;
this.maxLength = width + tailLength + this.maxOffset + -9; // 13 - position left
this.performResults(results);
}
disconnectedCallback() {
// браузер вызывает этот метод при удалении элемента из документа
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
}
static get observedAttributes(): string[] {
return [/* массив имён атрибутов для отслеживания их изменений */];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
// вызывается при изменении одного из перечисленных выше атрибутов
}
adoptedCallback() {
// вызывается, когда элемент перемещается в новый документ
// (происходит в document.adoptNode, используется очень редко)
}
performResults(results: PollResults) {
const percents = results.results.map(v => v.voters / results.total_voters * 100);
this.setResults(percents);
}
setResults(percents: number[]) {
if(!this.svgLines) {
this.svgLines = Array.from(this.querySelectorAll('.poll-line')) as SVGSVGElement[];
this.numberDivs = Array.from(this.querySelectorAll('.poll-answer-percents')) as HTMLDivElement[];
}
let maxValue = Math.max(...percents);
this.maxLengths = percents.map(p => p / maxValue * this.maxLength);
/* this.svgLines.forEach((svg, idx) => {
this.setLineProgress(idx, 1);
}); */
/* percents = percents.map(p => {
return Math.round(p);
}); */
console.log('setResults before percents:', percents);
let sum = percents.reduce((acc, p) => acc + Math.round(p), 0);
if(sum > 100) {
let diff = sum - 100;
let length = percents.length;
for(let i = 0; i < diff; ++i) {
let minIndex = -1, minRemainder = 1;
for(let k = 0; k < length; ++k) {
let remainder = percents[k] % 1;
if(remainder >= 0.5 && remainder < minRemainder) {
minRemainder = remainder;
minIndex = k;
}
}
if(minIndex == -1) {
throw new Error('lol chto');
}
percents[minIndex] -= minRemainder;
}
} else {
let diff = 100 - sum;
let length = percents.length;
for(let i = 0; i < diff; ++i) {
let minIndex = -1, maxRemainder = 0;
for(let k = 0; k < length; ++k) {
let remainder = percents[k] % 1;
if(remainder < 0.5 && remainder > maxRemainder) {
maxRemainder = remainder;
minIndex = k;
}
}
if(minIndex == -1) {
throw new Error('lol chto');
}
percents[minIndex] += 1 - maxRemainder;
}
}
console.log('setResults after percents:', percents, sum);
let start = Date.now();
let r = () => {
let diff = Date.now() - start;
let progress = diff / fullTime;
if(progress > 1) progress = 1;
this.svgLines.forEach((svg, idx) => {
this.setLineProgress(idx, progress);
});
if(progress < 1) {
window.requestAnimationFrame(r);
}
};
window.requestAnimationFrame(r);
for(let i = 0; i < times; ++i) {
setTimeout(() => {
percents.forEach((percents, idx) => {
let value = Math.round(percents / times * (i + 1));
let div = this.numberDivs[idx];
//div.style.opacity = ((i + 1) * 0.10).toFixed(1); // опасити в 10 шагов от 0.1 до 1
div.innerText = value + '%';
});
}, oneTime * i);
}
this.classList.add('is-voted');
}
setLineProgress(index: number, percents: number) {
let svg = this.svgLines[index];
svg.style.strokeDasharray = (percents * this.maxLengths[index]) + ', 485.9';
svg.style.strokeDashoffset = '' + percents * this.maxOffset;
}
// у элемента могут быть ещё другие методы и свойства
}
customElements.define("poll-element", PollElement);

View File

@ -0,0 +1,97 @@
import resizeableImage from "../lib/cropper";
import apiFileManager from "../lib/mtproto/apiFileManager";
export class PopupAvatar {
private container = document.getElementById('popup-avatar');
private input = this.container.querySelector('input') as HTMLInputElement;
private cropContainer = this.container.querySelector('.crop') as HTMLDivElement;
private closeBtn = this.container.querySelector('.popup-close') as HTMLButtonElement;
private image = new Image();
private canvas: HTMLCanvasElement;
private blob: Blob;
private cropper = {
crop: () => {},
removeHandlers: () => {}
};
private onCrop: (upload: () => Promise<any>) => void;
constructor() {
this.container.style.display = ''; // need for no blink
this.cropContainer.append(this.image);
this.input.addEventListener('change', (e: any) => {
var file = e.target.files[0];
if(!file) {
return;
}
var reader = new FileReader();
reader.onload = (e) => {
var contents = e.target.result as string;
this.image = new Image();
this.cropContainer.append(this.image);
this.image.src = contents;
this.image.onload = () => {
/* let {w, h} = calcImageInBox(this.image.naturalWidth, this.image.naturalHeight, 460, 554);
cropContainer.style.width = w + 'px';
cropContainer.style.height = h + 'px'; */
this.container.classList.remove('hide');
void this.container.offsetWidth; // reflow
this.container.classList.add('active');
this.cropper = resizeableImage(this.image, this.canvas);
this.input.value = '';
};
};
reader.readAsDataURL(file);
}, false);
// apply
this.container.querySelector('.btn-crop').addEventListener('click', () => {
this.cropper.crop();
this.closeBtn.click();
this.canvas.toBlob(blob => {
this.blob = blob; // save blob to send after reg
// darken
let ctx = this.canvas.getContext('2d');
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.resolve();
}, 'image/jpeg', 1);
});
this.closeBtn.addEventListener('click', () => {
setTimeout(() => {
this.cropper.removeHandlers();
if(this.image) {
this.image.remove();
}
this.container.classList.add('hide');
}, 200);
});
}
private resolve() {
this.onCrop(() => {
return apiFileManager.uploadFile(this.blob);
});
}
public open(postCanvas: HTMLCanvasElement, onCrop: (upload: () => Promise<any>) => void) {
this.canvas = postCanvas;
this.onCrop = onCrop;
this.input.click();
}
}
export default new PopupAvatar();

View File

@ -15,6 +15,8 @@ import { CancellablePromise } from '../lib/polyfill';
import { renderImageFromUrl } from './misc';
import appMessagesManager from '../lib/appManagers/appMessagesManager';
import { Layouter, RectPart } from './groupedLayout';
import { Poll, PollResults } from '../lib/appManagers/appPollsManager';
import PollElement from './poll';
export type MTDocument = {
_: 'document' | 'documentEmpty',
@ -701,11 +703,11 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: (
}
if(!stickerType) {
console.error('wrong doc for wrapSticker!', doc, div);
console.error('wrong doc for wrapSticker!', doc);
return Promise.resolve();
}
console.log('wrap sticker', doc, div, onlyThumb);
//console.log('wrap sticker', doc, div, onlyThumb);
if(doc.thumbs && !div.firstElementChild && (!doc.downloaded || stickerType == 2)) {
let thumb = doc.thumbs[0];
@ -875,6 +877,10 @@ export function wrapReply(title: string, subtitle: string, message?: any) {
if(media) {
if(message.grouped_id) {
replySubtitle.innerHTML = 'Album';
} else if(media._ == 'messageMediaContact') {
replySubtitle.innerHTML = 'Contact';
} else if(media._ == 'messageMediaPoll') {
replySubtitle.innerHTML = media.poll.rReply;
} else if(media.photo) {
replySubtitle.innerHTML = 'Photo';
} else if(media.document && media.document.type) {
@ -946,7 +952,13 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo
let brSplitted = fillPropertyValue(borderRadius); */
for(let {geometry, sides} of layout) {
let {size, media, message} = items.shift();
let item = items.shift();
if(!item) {
console.error('no item for layout!');
continue;
}
let {size, media, message} = item;
let div = document.createElement('div');
div.classList.add('album-item');
div.dataset.mid = message.mid;
@ -1041,3 +1053,9 @@ export function wrapAlbum({groupID, attachmentDiv, middleware, uploading, lazyLo
attachmentDiv.append(div);
}
}
export function wrapPoll(poll: Poll, results: PollResults) {
let elem = new PollElement();
elem.setAttribute('poll-id', poll.id);
return elem;
}

68
src/format_schema.js Normal file
View File

@ -0,0 +1,68 @@
let json = require('./schema');
let top = {};
/* ['MTProto', 'API'].forEach(key => {
let schema = json[key];
let out = {constructors: {}, methods: {}};
['constructors', 'methods'].forEach(key => {
schema[key].forEach(smth => {
let id = smth.id;
if(id < 0) {
id = +id + 4294967296;
}
out[key][id] = smth;
delete smth.id;
});
});
top[key] = out;
//console.log(out);
//process.exit(0);
}); */
['MTProto', 'API'].forEach(key => {
let schema = json[key];
['constructors', 'methods'].forEach(key => {
schema[key].forEach(smth => {
if(+smth.id < 0) {
smth.id = +smth.id + 4294967296;
}
});
});
//console.log(out);
//process.exit(0);
});
top = json;
/* ['API'].forEach(key => {
let schema = json[key];
let out = {constructors: {}, methods: {}};
['constructors', 'methods'].forEach(key => {
schema[key].forEach(smth => {
let id = smth.id;
if(id < 0) {
id = id + 4294967296;
}
out[key][id] = smth;
delete smth.id;
});
});
top[key] = out;
//console.log(out);
//process.exit(0);
}); */
//console.log(out);
require('fs').writeFileSync('./schema_pretty.json', JSON.stringify(top/* , null, '\t' */));

View File

@ -1,9 +1,43 @@
import { $rootScope, isObject, SearchIndexManager, safeReplaceObject, copy, numberWithCommas } from "../utils";
import { RichTextProcessor } from "../richtextprocessor";
import appUsersManager from "./appUsersManager";
import appPeersManager from "./appPeersManager";
import apiManager from '../mtproto/mtprotoworker';
import apiUpdatesManager from "./apiUpdatesManager";
type Channel = {
_: 'channel',
flags: number,
pFlags: Partial<{
creator: true,
left: true,
broadcast: true,
verified: true,
megagroup: true,
restricted: true,
signatures: true,
min: true,
scam: true,
has_link: true,
has_geo: true,
slowmode_enabled: true
}>,
id: number,
access_hash?: string,
title: string,
username?: string,
photo: any,
date: number,
version: number,
restriction_reason?: any,
admin_rights?: any,
banned_rights?: any,
default_banned_rights?: any,
participants_count: number
};
export class AppChatsManager {
public chats: any = {};
public chats: {[id: number]: Channel | any} = {};
public usernames: any = {};
public channelAccess: any = {};
public megagroups: any = {};
@ -143,8 +177,7 @@ export class AppChatsManager {
public isChannel(id: number) {
var chat = this.chats[id];
if(chat && (chat._ == 'channel' || chat._ == 'channelForbidden') ||
this.channelAccess[id]) {
if(chat && (chat._ == 'channel' || chat._ == 'channelForbidden') || this.channelAccess[id]) {
return true;
}
return false;
@ -166,10 +199,6 @@ export class AppChatsManager {
return this.isChannel(id) && !this.isMegagroup(id);
}
public getChatInput(id: number) {
return id || 0;
}
public getChannelInput(id: number) {
if(!id) {
return {_: 'inputChannelEmpty'};
@ -182,6 +211,25 @@ export class AppChatsManager {
};
}
public getChatInputPeer(id: number) {
return {
_: 'inputPeerChat',
chat_id: id
};
}
public getChannelInputPeer(id: number) {
if(!id) {
return {_: 'inputPeerEmpty'};
}
return {
_: 'inputPeerChannel',
channel_id: id,
access_hash: this.getChat(id).access_hash || this.channelAccess[id] || 0
};
}
public hasChat(id: number, allowMin?: any) {
var chat = this.chats[id]
return isObject(chat) && (allowMin || !chat.pFlags.min);
@ -217,7 +265,7 @@ export class AppChatsManager {
var chatFull = copy(fullChat);
var chat = this.getChat(id);
if (!chatFull.participants_count) {
if(!chatFull.participants_count) {
chatFull.participants_count = chat.participants_count;
}
@ -226,7 +274,7 @@ export class AppChatsManager {
chatFull.participants.participants = this.wrapParticipants(id, chatFull.participants.participants);
}
if (chatFull.about) {
if(chatFull.about) {
chatFull.rAbout = RichTextProcessor.wrapRichText(chatFull.about, {noLinebreaks: true});
}
@ -240,7 +288,7 @@ export class AppChatsManager {
var chat = this.getChat(id);
var myID = appUsersManager.getSelf().id;
if(this.isChannel(id)) {
var isAdmin = chat.pFlags.creator || chat.pFlags.editor || chat.pFlags.moderator
var isAdmin = chat.pFlags.creator || chat.pFlags.editor || chat.pFlags.moderator;
participants.forEach((participant) => {
participant.canLeave = myID == participant.user_id;
participant.canKick = isAdmin && participant._ == 'channelParticipant';
@ -249,7 +297,7 @@ export class AppChatsManager {
participant.user = appUsersManager.getUser(participant.user_id);
});
} else {
var isAdmin = chat.pFlags.creator || chat.pFlags.admins_enabled && chat.pFlags.admin
var isAdmin = chat.pFlags.creator || chat.pFlags.admins_enabled && chat.pFlags.admin;
participants.forEach((participant) => {
participant.canLeave = myID == participant.user_id;
participant.canKick = !participant.canLeave && (
@ -261,8 +309,70 @@ export class AppChatsManager {
participant.user = appUsersManager.getUser(participant.user_id);
});
}
return participants;
}
public createChannel(title: string, about: String): Promise<number> {
return apiManager.invokeApi('channels.createChannel', {
flags: 1,
broadcast: true,
title: title,
about: about
}).then((updates: any) => {
apiUpdatesManager.processUpdateMessage(updates);
return updates.chats[0].id;
});
}
public inviteToChannel(id: number, userIDs: number[]) {
let input = this.getChannelInput(id);
let usersInputs = userIDs.map(u => appUsersManager.getUserInput(u));
return apiManager.invokeApi('channels.inviteToChannel', {
channel: input,
users: usersInputs
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
});
}
public createChat(title: string, userIDs: number[]): Promise<number> {
return apiManager.invokeApi('messages.createChat', {
users: userIDs.map(u => appUsersManager.getUserInput(u)),
title: title
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
return updates.chats[0].id;
});
}
public editPhoto(id: number, inputFile: any) {
let isChannel = this.isChannel(id);
let inputChatPhoto = {
_: 'inputChatUploadedPhoto',
file: inputFile
};
if(isChannel) {
return apiManager.invokeApi('channels.editPhoto', {
channel: this.getChannelInputPeer(id),
photo: inputChatPhoto
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
});
} else {
return apiManager.invokeApi('messages.editChatPhoto', {
chat_id: id,
photo: inputChatPhoto
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
});
}
}
}
export default new AppChatsManager();

View File

@ -9,6 +9,7 @@ import { ripple, putPreloader } from "../../components/misc";
import Scrollable from "../../components/scrollable_new";
import appProfileManager from "./appProfileManager";
import { logger } from "../polyfill";
import appChatsManager from "./appChatsManager";
type DialogDom = {
avatarDiv: HTMLDivElement,
@ -474,6 +475,12 @@ export class AppDialogsManager {
case 'messageMediaGeo':
lastMessageText += '<i>Geolocation</i>';
break;
case 'messageMediaPoll':
lastMessageText += '<i>' + lastMessage.media.poll.rReply + '</i>';
break;
case 'messageMediaContact':
lastMessageText += '<i>Contact</i>';
break;
case 'messageMediaDocument':
let document = lastMessage.media.document;
@ -724,6 +731,18 @@ export class AppDialogsManager {
let titleSpan = document.createElement('span');
titleSpan.classList.add('user-title');
if(peerID < 0) {
let chat = appChatsManager.getChat(-peerID);
if(chat && chat.pFlags && chat.pFlags.verified) {
titleSpan.classList.add('is-verified');
}
} else {
let user = appUsersManager.getUser(peerID);
if(user && user.pFlags && user.pFlags.verified) {
titleSpan.classList.add('is-verified');
}
}
if(peerID == $rootScope.myID) {
title = onlyFirstName ? 'Saved' : 'Saved Messages';
}

View File

@ -1,6 +1,6 @@
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
import { $rootScope, isElementInViewport, numberWithCommas, findUpClassName, formatNumber, placeCaretAtEnd, findUpTag, langPack } from "../utils";
import { $rootScope, numberWithCommas, findUpClassName, formatNumber, placeCaretAtEnd, findUpTag, langPack } from "../utils";
import appUsersManager from "./appUsersManager";
import appMessagesManager from "./appMessagesManager";
import appPeersManager from "./appPeersManager";
@ -17,7 +17,7 @@ import appSidebarLeft from "./appSidebarLeft";
import appChatsManager from "./appChatsManager";
import appMessagesIDsManager from "./appMessagesIDsManager";
import apiUpdatesManager from './apiUpdatesManager';
import { wrapDocument, wrapPhoto, wrapVideo, wrapSticker, wrapReply, wrapAlbum } from '../../components/wrappers';
import { wrapDocument, wrapPhoto, wrapVideo, wrapSticker, wrapReply, wrapAlbum, wrapPoll } from '../../components/wrappers';
import ProgressivePreloader from '../../components/preloader';
import { openBtnMenu, formatPhoneNumber } from '../../components/misc';
import { ChatInput } from '../../components/chatInput';
@ -838,8 +838,21 @@ export class AppImManager {
let [chatOnlines, chatInfo] = results;
let onlines = chatOnlines ? chatOnlines.onlines : 1;
if(chatInfo._ == 'chatFull' && chatInfo.participants && chatInfo.participants.participants) {
let participants = chatInfo.participants.participants;
onlines = participants.reduce((acc: number, participant: any) => {
let user = appUsersManager.getUser(participant.user_id);
if(user && user.status && user.status._ == 'userStatusOnline') {
return acc + 1;
}
return acc;
}, 0);
}
///////////this.log('chatInfo res:', chatInfo);
this.log('chatInfo res:', chatInfo);
if(chatInfo.pinned_msg_id) { // request pinned message
this.pinnedMsgID = chatInfo.pinned_msg_id;
@ -882,6 +895,9 @@ export class AppImManager {
}
}
}
} else {
this.subtitleEl.innerText = 'bot';
appSidebarRight.profileElements.subtitle.innerText = 'bot';
}
}
@ -1058,7 +1074,7 @@ export class AppImManager {
this.scroll.scrollTop = this.scroll.scrollHeight;
}
}
/* this.onScroll();
this.scrollable.onScroll();*/
@ -1225,7 +1241,8 @@ export class AppImManager {
// reverse means top
public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) {
//this.log('message to render:', message);
this.log('message to render:', message);
//return;
if(message.deleted) return;
else if(message.grouped_id) { // will render only last album's message
let storage = appMessagesManager.groupedMessagesStorage[message.grouped_id];
@ -1278,8 +1295,14 @@ export class AppImManager {
if(_ == "messageActionPhoneCall") {
_ += '.' + action.type;
}
// @ts-ignore
let str = (name.innerText ? name.outerHTML + ' ' : '') + langPack[_];
let l = langPack[_];
if(!l) {
l = '[' + _ + ']';
}
let str = l[0].toUpperCase() == l[0] ? l : (name.innerText ? name.outerHTML + ' ' : '') + l;
bubbleContainer.innerHTML = `<div class="service-msg">${str}</div>`;
/* if(!updatePosition) {
@ -1401,7 +1424,7 @@ export class AppImManager {
}
// media
if(message.media) {
if(message.media/* && message.media._ == 'messageMediaPhoto' */) {
let attachmentDiv = document.createElement('div');
attachmentDiv.classList.add('attachment');
@ -1712,6 +1735,15 @@ export class AppImManager {
break;
}
case 'messageMediaPoll': {
bubble.classList.remove('is-message-empty');
let pollElement = wrapPoll(message.media.poll, message.media.results);
messageDiv.prepend(pollElement);
break;
}
default:
bubble.classList.remove('is-message-empty');
@ -1877,7 +1909,7 @@ export class AppImManager {
let _history = history.slice();
setTimeout(() => {
this.performHistoryResult(_history, reverse, isBackLimit, 0, resetPromises);
}, (i + 1) * 2500);
}, 0/* (i + 1) * 2500 */);
}
}
@ -2092,8 +2124,8 @@ export class AppImManager {
this.log('getHistory: slice loadedTimes:', reverse, pageCount, this.loadedTopTimes, this.loadedBottomTimes, ids && ids.length);
let removeCount = loadCount / 2;
let safeCount = realLoadCount * 2;
//let removeCount = loadCount / 2;
let safeCount = Math.min(realLoadCount * 2, 35); // cause i've been runningrunningrunning all day
if(ids && ids.length > safeCount) {
if(reverse) {
//ids = ids.slice(-removeCount);
@ -2210,49 +2242,51 @@ export class AppImManager {
public handleUpdate(update: any) {
switch(update._) {
case 'updateUserTyping':
case 'updateChatUserTyping':
if(this.myID == update.user_id) {
return;
}
var peerID = update._ == 'updateUserTyping' ? update.user_id : -update.chat_id;
this.typingUsers[update.user_id] = peerID;
if(!appUsersManager.hasUser(update.user_id)) {
if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) {
appProfileManager.getChatFull(update.chat_id);
case 'updateChatUserTyping': {
if(this.myID == update.user_id) {
return;
}
//return;
}
appUsersManager.forceUserOnline(update.user_id);
let dialog = appMessagesManager.getDialogByPeerID(peerID)[0];
let currentPeer = this.peerID == peerID;
if(this.typingTimeouts[peerID]) clearTimeout(this.typingTimeouts[peerID]);
else if(dialog) {
appDialogsManager.setTyping(dialog, appUsersManager.getUser(update.user_id));
var peerID = update._ == 'updateUserTyping' ? update.user_id : -update.chat_id;
this.typingUsers[update.user_id] = peerID;
if(currentPeer) { // user
if(!appUsersManager.hasUser(update.user_id)) {
if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) {
appProfileManager.getChatFull(update.chat_id);
}
//return;
}
appUsersManager.forceUserOnline(update.user_id);
let dialog = appMessagesManager.getDialogByPeerID(peerID)[0];
let currentPeer = this.peerID == peerID;
if(this.typingTimeouts[peerID]) clearTimeout(this.typingTimeouts[peerID]);
else if(dialog) {
appDialogsManager.setTyping(dialog, appUsersManager.getUser(update.user_id));
if(currentPeer) { // user
this.setPeerStatus();
}
}
this.typingTimeouts[peerID] = setTimeout(() => {
this.typingTimeouts[peerID] = 0;
delete this.typingUsers[update.user_id];
if(dialog) {
appDialogsManager.unsetTyping(dialog);
}
// лень просчитывать случаи
this.setPeerStatus();
}
}, 6000);
break;
}
this.typingTimeouts[peerID] = setTimeout(() => {
this.typingTimeouts[peerID] = 0;
delete this.typingUsers[update.user_id];
if(dialog) {
appDialogsManager.unsetTyping(dialog);
}
// лень просчитывать случаи
this.setPeerStatus();
}, 6000);
break;
case 'updateNotifySettings': {
let {peer, notify_settings} = update;

View File

@ -21,6 +21,7 @@ import serverTimeManager from "../mtproto/serverTimeManager";
import apiManager from '../mtproto/mtprotoworker';
import appWebPagesManager from "./appWebPagesManager";
import { CancellablePromise, deferredPromise } from "../polyfill";
import appPollsManager from "./appPollsManager";
const APITIMEOUT = 0;
@ -1501,7 +1502,10 @@ export class AppMessagesManager {
apiMessage.media.photo = appPhotosManager.savePhoto(apiMessage.media.photo, mediaContext);
//appPhotosManager.savePhoto(apiMessage.media.photo, mediaContext);
}
break
break;
case 'messageMediaPoll':
appPollsManager.savePoll(apiMessage.media.poll, apiMessage.media.results);
break;
case 'messageMediaDocument':
if(apiMessage.media.ttl_seconds) {
apiMessage.media = {_: 'messageMediaUnsupportedWeb'};

View File

@ -30,10 +30,12 @@ const AppPeersManager = {
if(peerID >= 0) {
return false;
}
var chat = appChatsManager.getChat(-peerID);
let chat = appChatsManager.getChat(-peerID);
if(chat && chat.migrated_to && chat.pFlags.deactivated) {
return AppPeersManager.getPeerID(chat.migrated_to);
}
return false;
},
@ -66,10 +68,11 @@ const AppPeersManager = {
return {_: 'peerUser', user_id: peerID};
}
var chatID = -peerID;
let chatID = -peerID;
if(appChatsManager.isChannel(chatID)) {
return {_: 'peerChannel', channel_id: chatID};
}
return {_: 'peerChat', chat_id: chatID};
},
@ -99,8 +102,8 @@ const AppPeersManager = {
? peerString.user_id
: -(peerString.channel_id || peerString.chat_id);
}
var isUser = peerString.charAt(0) == 'u';
var peerParams = peerString.substr(1).split('_');
let isUser = peerString.charAt(0) == 'u';
let peerParams = peerString.substr(1).split('_');
return isUser ? peerParams[0] : -peerParams[0] || 0;
},
@ -125,25 +128,52 @@ const AppPeersManager = {
return (peerID > 0) && appUsersManager.isBot(peerID);
},
getInputPeerByID: (peerID: number) => {
if (!peerID) {
return {_: 'inputPeerEmpty'}
getInputPeer: (peerString: string): any => {
var firstChar = peerString.charAt(0);
var peerParams = peerString.substr(1).split('_');
let id = +peerParams[0];
if(firstChar == 'u') {
appUsersManager.saveUserAccess(id, peerParams[1]);
return {
_: 'inputPeerUser',
user_id: id,
access_hash: peerParams[1]
};
} else if(firstChar == 'c' || firstChar == 's') {
appChatsManager.saveChannelAccess(id, peerParams[1]);
if(firstChar == 's') {
appChatsManager.saveIsMegagroup(id);
}
return {
_: 'inputPeerChannel',
channel_id: id,
access_hash: peerParams[1] || 0
};
} else {
return {
_: 'inputPeerChat',
chat_id: id
};
}
if (peerID < 0) {
var chatID = -peerID
if (!appChatsManager.isChannel(chatID)) {
return {
_: 'inputPeerChat',
chat_id: chatID
};
},
getInputPeerByID: (peerID: number) => {
if(!peerID) {
return {_: 'inputPeerEmpty'};
}
if(peerID < 0) {
let chatID = -peerID;
if(!appChatsManager.isChannel(chatID)) {
return appChatsManager.getChatInputPeer(chatID);
} else {
return {
_: 'inputPeerChannel',
channel_id: chatID,
access_hash: appChatsManager.getChat(chatID).access_hash || 0
};
return appChatsManager.getChannelInputPeer(chatID);
}
}
return {
_: 'inputPeerUser',
user_id: peerID,
@ -158,11 +188,11 @@ const AppPeersManager = {
},
getPeerSearchText: (peerID: number) => {
var text
let text;
if(peerID > 0) {
text = '%pu ' + appUsersManager.getUserSearchText(peerID);
} else if(peerID < 0) {
var chat = appChatsManager.getChat(-peerID);
let chat = appChatsManager.getChat(-peerID);
text = '%pg ' + (chat.title || '');
}
return text;

View File

@ -0,0 +1,89 @@
import { RichTextProcessor } from "../richtextprocessor";
export type PollAnswer = {
_: 'pollAnswer',
text: string,
option: Uint8Array
};
export type PollAnswerVoters = {
_: 'pollAnswerVoters',
flags: number,
option: Uint8Array,
voters: number,
pFlags: Partial<{
chosen: true,
correct: true
}>
};
export type PollResult = {
_: 'pollAnswerVoters',
flags: number,
option: Uint8Array,
voters: number,
pFlags?: Partial<{chosen: true}>
};
export type PollResults = {
_: 'pollResults',
flags: number,
results?: Array<PollResult>,
total_voters?: number,
recent_voters?: number[],
solution?: string,
solution_entities?: any[],
pFlags: Partial<{
min: true
}>,
};
export type Poll = {
_: 'poll',
flags: number,
question: string,
id: string,
answers: Array<PollAnswer>,
close_period?: number,
close_date?: number
pFlags?: Partial<{
closed: true,
public_voters: true,
multiple_choice: true,
quiz: true
}>,
rQuestion?: string,
rReply?: string,
};
class AppPollsManager {
private polls: {[id: string]: Poll} = {};
private results: {[id: string]: PollResults} = {};
public savePoll(poll: Poll, results: PollResults) {
let id = poll.id;
if(this.polls[id]) {
this.results[id] = results;
return;
}
this.polls[id] = poll;
this.results[id] = results;
poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question);
poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll');
}
public getPoll(pollID: string): {poll: Poll, results: PollResults} {
return {
poll: this.polls[pollID],
results: this.results[pollID]
};
}
}
export default new AppPollsManager();

View File

@ -5,17 +5,318 @@ import appImManager from "./appImManager";
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
import AppSearch, { SearchGroup } from "../../components/appSearch";
import { horizontalMenu } from "../../components/misc";
import { horizontalMenu, putPreloader } from "../../components/misc";
import appUsersManager from "./appUsersManager";
import Scrollable from "../../components/scrollable_new";
import appPhotosManager from "./appPhotosManager";
import { appPeersManager } from "../services";
import popupAvatar from "../../components/popupAvatar";
import appChatsManager from "./appChatsManager";
import { AppSelectPeers } from "../../components/appSelectPeers";
const SLIDERITEMSIDS = {
archived: 1,
contacts: 2
contacts: 2,
newChannel: 3,
addMembers: 4,
newGroup: 5,
};
interface SliderTab {
onClose?: () => void,
onCloseAfterTimeout?: () => void
}
class AppAddMembersTab implements SliderTab {
private container = document.querySelector('.addmembers-container') as HTMLDivElement;
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
private nextBtn = this.contentDiv.querySelector('.btn-corner') as HTMLButtonElement;
private selector: AppSelectPeers;
private peerType: 'channel' | 'chat';
private peerID: number; // always positive
private takeOut: (peerIDs: number[]) => void
constructor() {
this.nextBtn.addEventListener('click', () => {
let peerIDs = this.selector.getSelected();
if(peerIDs.length) {
if(this.takeOut) {
this.takeOut(peerIDs);
return;
}
this.nextBtn.classList.remove('tgico-next');
this.nextBtn.disabled = true;
putPreloader(this.nextBtn);
this.selector.freezed = true;
appChatsManager.inviteToChannel(this.peerID, peerIDs).then(() => {
this.backBtn.click();
});
}
});
}
public onCloseAfterTimeout() {
if(this.selector) {
this.selector.container.remove();
this.selector = null;
}
}
public init(id: number, type: 'channel' | 'chat', skipable: boolean, takeOut?: AppAddMembersTab['takeOut']) {
this.peerID = Math.abs(id);
this.peerType = type;
this.takeOut = takeOut;
this.onCloseAfterTimeout();
this.selector = new AppSelectPeers(this.contentDiv, skipable ? null : (length) => {
if(length) {
this.nextBtn.classList.add('is-visible');
} else {
this.nextBtn.classList.remove('is-visible');
}
}, 'contacts');
this.nextBtn.innerHTML = '';
this.nextBtn.disabled = false;
this.nextBtn.classList.add('tgico-next');
if(skipable) {
this.nextBtn.classList.add('is-visible');
} else {
this.nextBtn.classList.remove('is-visible');
}
appSidebarLeft.selectTab(SLIDERITEMSIDS.addMembers);
}
}
class AppNewChannelTab implements SliderTab {
private container = document.querySelector('.new-channel-container') as HTMLDivElement;
private canvas = this.container.querySelector('.avatar-edit-canvas') as HTMLCanvasElement;
private channelNameInput = this.container.querySelector('.new-channel-name') as HTMLInputElement;
private channelDescriptionInput = this.container.querySelector('.new-channel-description') as HTMLInputElement;
private nextBtn = this.container.querySelector('.btn-corner') as HTMLButtonElement;
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
private uploadAvatar: () => Promise<any> = null;
constructor() {
this.container.querySelector('.avatar-edit').addEventListener('click', () => {
popupAvatar.open(this.canvas, (_upload) => {
this.uploadAvatar = _upload;
});
});
this.channelNameInput.addEventListener('input', () => {
let value = this.channelNameInput.value;
if(value.length) {
this.nextBtn.classList.add('is-visible');
} else {
this.nextBtn.classList.remove('is-visible');
}
});
this.nextBtn.addEventListener('click', () => {
let title = this.channelNameInput.value;
let about = this.channelDescriptionInput.value;
this.nextBtn.disabled = true;
appChatsManager.createChannel(title, about).then((channelID) => {
if(this.uploadAvatar) {
this.uploadAvatar().then((inputFile: any) => {
appChatsManager.editPhoto(channelID, inputFile);
});
}
appSidebarLeft.removeTabFromHistory(SLIDERITEMSIDS.newChannel);
appSidebarLeft.addMembersTab.init(channelID, 'channel', true);
});
});
}
public onCloseAfterTimeout() {
let ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.uploadAvatar = null;
this.channelNameInput.value = '';
this.channelDescriptionInput.value = '';
this.nextBtn.disabled = false;
}
}
class AppNewGroupTab implements SliderTab {
private container = document.querySelector('.new-group-container') as HTMLDivElement;
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
private canvas = this.container.querySelector('.avatar-edit-canvas') as HTMLCanvasElement;
private groupNameInput = this.container.querySelector('.new-group-name') as HTMLInputElement;
private nextBtn = this.container.querySelector('.btn-corner') as HTMLButtonElement;
private searchGroup = new SearchGroup('', 'contacts', true, 'new-group-members disable-hover', false);
private uploadAvatar: () => Promise<any> = null;
private userIDs: number[];
constructor() {
this.container.querySelector('.avatar-edit').addEventListener('click', () => {
popupAvatar.open(this.canvas, (_upload) => {
this.uploadAvatar = _upload;
});
});
this.groupNameInput.addEventListener('input', () => {
let value = this.groupNameInput.value;
if(value.length) {
this.nextBtn.classList.add('is-visible');
} else {
this.nextBtn.classList.remove('is-visible');
}
});
this.nextBtn.addEventListener('click', () => {
let title = this.groupNameInput.value;
this.nextBtn.disabled = true;
appChatsManager.createChat(title, this.userIDs).then((chatID) => {
if(this.uploadAvatar) {
this.uploadAvatar().then((inputFile: any) => {
appChatsManager.editPhoto(chatID, inputFile);
});
}
appSidebarLeft.selectTab(0);
});
});
let chatsContainer = document.createElement('div');
chatsContainer.classList.add('chats-container');
chatsContainer.append(this.searchGroup.container);
let scrollable = new Scrollable(chatsContainer);
this.contentDiv.append(chatsContainer);
}
public onClose() {
}
public onCloseAfterTimeout() {
this.searchGroup.clear();
let ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.uploadAvatar = null;
this.groupNameInput.value = '';
this.nextBtn.disabled = false;
}
public init(userIDs: number[]) {
this.userIDs = userIDs;
appSidebarLeft.selectTab(SLIDERITEMSIDS.newGroup);
this.userIDs.forEach(userID => {
let {dom} = appDialogsManager.addDialog(userID, this.searchGroup.list, false, false);
let subtitle = '';
subtitle = appUsersManager.getUserStatusString(userID);
if(subtitle == 'online') {
subtitle = `<i>${subtitle}</i>`;
}
if(subtitle) {
dom.lastMessageSpan.innerHTML = subtitle;
}
});
this.searchGroup.nameEl.innerText = this.userIDs.length + ' members';
this.searchGroup.setActive();
}
}
class AppContactsTab implements SliderTab {
private container = document.getElementById('contacts-container');
private list = this.container.querySelector('#contacts') as HTMLUListElement;
private scrollable: Scrollable;
private promise: Promise<void>;
private input = this.container.querySelector('#contacts-search') as HTMLInputElement;
constructor() {
appDialogsManager.setListClickListener(this.list);
this.scrollable = new Scrollable(this.list.parentElement);
let prevValue = '';
this.input.addEventListener('input', () => {
let value = this.input.value;
if(prevValue != value) {
this.list.innerHTML = '';
this.openContacts(prevValue = value);
}
});
// preload contacts
appUsersManager.getContacts();
}
// need to clear, and left 1 page for smooth slide
public onClose() {
let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
(Array.from(this.list.children) as HTMLElement[]).slice(pageCount).forEach(el => el.remove());
}
public onCloseAfterTimeout() {
this.list.innerHTML = '';
}
public openContacts(query?: string) {
appSidebarLeft.selectTab(SLIDERITEMSIDS.contacts);
if(this.promise) return this.promise;
this.scrollable.onScrolledBottom = null;
this.promise = appUsersManager.getContacts(query).then(contacts => {
this.promise = null;
if(appSidebarLeft.historyTabIDs[appSidebarLeft.historyTabIDs.length - 1] != SLIDERITEMSIDS.contacts) {
console.warn('user closed contacts before it\'s loaded');
return;
}
let sorted = contacts
.map(userID => {
let user = appUsersManager.getUser(userID);
let status = appUsersManager.getUserStatusForSort(user.status);
return {user, status};
})
.sort((a, b) => b.status - a.status);
let renderPage = () => {
let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
let arr = sorted.splice(0, pageCount); // надо splice!
arr.forEach(({user}) => {
let {dialog, dom} = appDialogsManager.addDialog(user.id, this.list, false);
let status = appUsersManager.getUserStatusString(user.id);
dom.lastMessageSpan.innerHTML = status == 'online' ? `<i>${status}</i>` : status;
});
if(!sorted.length) renderPage = undefined;
};
renderPage();
this.scrollable.onScrolledBottom = () => {
if(renderPage) {
renderPage();
} else {
this.scrollable.onScrolledBottom = null;
}
};
});
}
}
class AppSidebarLeft {
private sidebarEl = document.getElementById('column-left') as HTMLDivElement;
private toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement;
@ -24,12 +325,32 @@ class AppSidebarLeft {
private searchInput = document.getElementById('global-search') as HTMLInputElement;
private menuEl = this.toolsBtn.querySelector('.btn-menu');
private savedBtn = this.menuEl.querySelector('.menu-saved');
private archivedBtn = this.menuEl.querySelector('.menu-archive');
private newGroupBtn = this.menuEl.querySelector('.menu-new-group');
private contactsBtn = this.menuEl.querySelector('.menu-contacts');
private archivedBtn = this.menuEl.querySelector('.menu-archive');
private savedBtn = this.menuEl.querySelector('.menu-saved');
private logOutBtn = this.menuEl.querySelector('.menu-logout');
public archivedCount = this.archivedBtn.querySelector('.archived-count') as HTMLSpanElement;
private newBtnMenu = this.sidebarEl.querySelector('#new-menu');
private newButtons = {
channel: this.newBtnMenu.querySelector('.menu-channel'),
group: this.newBtnMenu.querySelector('.menu-group'),
privateChat: this.newBtnMenu.querySelector('.menu-private-chat'),
};
public newChannelTab = new AppNewChannelTab();
public addMembersTab = new AppAddMembersTab();
public contactsTab = new AppContactsTab();
public newGroupTab = new AppNewGroupTab();
private tabs: {[id: number]: SliderTab} = {
[SLIDERITEMSIDS.newChannel]: this.newChannelTab,
[SLIDERITEMSIDS.contacts]: this.contactsTab,
[SLIDERITEMSIDS.addMembers]: this.addMembersTab,
[SLIDERITEMSIDS.newGroup]: this.newGroupTab,
};
//private log = logger('SL');
private searchGroups = {
@ -42,12 +363,7 @@ class AppSidebarLeft {
private globalSearch = new AppSearch(this.searchContainer, this.searchInput, this.searchGroups);
private _selectTab: (id: number) => void;
private historyTabIDs: number[] = [];
private contactsList: HTMLUListElement;
private contactsScrollable: Scrollable;
private contactsPromise: Promise<void>;
private contactsInput: HTMLInputElement;
public historyTabIDs: number[] = [];
constructor() {
let peopleContainer = document.createElement('div');
@ -69,8 +385,7 @@ class AppSidebarLeft {
});
this.contactsBtn.addEventListener('click', (e) => {
this.openContacts();
this.selectTab(SLIDERITEMSIDS.contacts);
this.contactsTab.openContacts();
});
this.logOutBtn.addEventListener('click', (e) => {
@ -83,12 +398,6 @@ class AppSidebarLeft {
this.searchContainer.classList.remove('hide');
void this.searchContainer.offsetWidth; // reflow
this.searchContainer.classList.add('active');
/* if(!this.globalSearch.searchInput.value) {
for(let i in this.globalSearch.searchGroups) {
this.globalSearch.searchGroups[i].clear();
}
} */
false && this.searchInput.addEventListener('blur', (e) => {
if(!this.searchInput.value) {
@ -96,10 +405,6 @@ class AppSidebarLeft {
this.backBtn.classList.remove('active');
this.backBtn.click();
}
/* this.peerID = 0;
this.loadedCount = 0;
this.minMsgID = 0; */
}, {once: true});
});
@ -118,6 +423,18 @@ class AppSidebarLeft {
}, 150);
});
this.newButtons.channel.addEventListener('click', (e) => {
this.selectTab(SLIDERITEMSIDS.newChannel);
});
[this.newButtons.group, this.newGroupBtn].forEach(btn => {
btn.addEventListener('click', (e) => {
this.addMembersTab.init(0, 'chat', false, (peerIDs) => {
this.newGroupTab.init(peerIDs);
});
});
});
$rootScope.$on('dialogs_archived_unread', (e: CustomEvent) => {
this.archivedCount.innerText = '' + e.detail.count;
});
@ -125,22 +442,14 @@ class AppSidebarLeft {
this._selectTab = horizontalMenu(null, this.sidebarEl.querySelector('.sidebar-slider') as HTMLDivElement, null, null, 420);
this._selectTab(0);
let onCloseBtnClick = () => {
console.log('sidebar-close-button click:', this.historyTabIDs);
let closingID = this.historyTabIDs.pop(); // pop current
this.onCloseTab(closingID);
this._selectTab(this.historyTabIDs.pop() || 0);
};
Array.from(this.sidebarEl.querySelectorAll('.sidebar-close-button') as any as HTMLElement[]).forEach(el => {
el.addEventListener('click', () => {
console.log('sidebar-close-button click:', this.historyTabIDs);
let closingID = this.historyTabIDs.pop(); // pop current
// need to clear, and left 1 page for smooth slide
if(closingID == SLIDERITEMSIDS.contacts) {
let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
(Array.from(this.contactsList.children) as HTMLElement[]).slice(pageCount).forEach(el => el.remove());
setTimeout(() => {
this.contactsList.innerHTML = '';
}, 420);
}
this._selectTab(this.historyTabIDs.pop() || 0);
});
el.addEventListener('click', onCloseBtnClick);
});
appUsersManager.getTopPeers().then(categories => {
@ -162,80 +471,34 @@ class AppSidebarLeft {
this.searchGroups.people.setActive();
});
});
let contactsContainer = this.sidebarEl.querySelector('#contacts-container');
this.contactsInput = contactsContainer.querySelector('#contacts-search');
this.contactsList = contactsContainer.querySelector('#contacts') as HTMLUListElement;
appDialogsManager.setListClickListener(this.contactsList);
this.contactsScrollable = new Scrollable(this.contactsList.parentElement);
let prevValue = '';
this.contactsInput.addEventListener('input', () => {
let value = this.contactsInput.value;
if(prevValue != value) {
this.contactsList.innerHTML = '';
this.openContacts(prevValue = value);
}
});
// preload contacts
appUsersManager.getContacts();
}
public openContacts(query?: string) {
if(this.contactsPromise) return this.contactsPromise;
this.contactsScrollable.onScrolledBottom = null;
this.contactsPromise = appUsersManager.getContacts(query).then(contacts => {
this.contactsPromise = null;
if(this.historyTabIDs[this.historyTabIDs.length - 1] != SLIDERITEMSIDS.contacts) {
console.warn('user closed contacts before it\'s loaded');
return;
}
let sorted = contacts
.map(userID => {
let user = appUsersManager.getUser(userID);
let status = appUsersManager.getUserStatusForSort(user.status);
return {user, status};
})
.sort((a, b) => b.status - a.status);
let renderPage = () => {
let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
let arr = sorted.splice(0, pageCount);
arr.forEach(({user}) => {
let {dialog, dom} = appDialogsManager.addDialog(user.id, this.contactsList, false);
let status = appUsersManager.getUserStatusString(user.id);
dom.lastMessageSpan.innerHTML = status == 'online' ? `<i>${status}</i>` : status;
});
if(!sorted.length) renderPage = undefined;
};
renderPage();
this.contactsScrollable.onScrolledBottom = () => {
if(renderPage) {
renderPage();
} else {
this.contactsScrollable.onScrolledBottom = null;
}
};
});
}
public selectTab(id: number) {
this.historyTabIDs.push(id);
this._selectTab(id);
}
public removeTabFromHistory(id: number) {
this.historyTabIDs.findAndSplice(i => i == id);
this.onCloseTab(id);
}
public onCloseTab(id: number) {
let tab = this.tabs[id];
if(tab) {
if('onClose' in tab) {
tab.onClose();
}
if('onCloseAfterTimeout' in tab) {
setTimeout(() => {
tab.onCloseAfterTimeout();
}, 420);
}
}
}
}
const appSidebarLeft = new AppSidebarLeft();
(window as any).appSidebarLeft = appSidebarLeft;
export default appSidebarLeft;

View File

@ -158,12 +158,19 @@ export class AppUsersManager {
contactsList.sort((userID1: number, userID2: number) => {
const sortName1 = (this.users[userID1] || {}).sortName || '';
const sortName2 = (this.users[userID2] || {}).sortName || '';
return sortName1.localeCompare(sortName2);
});
/* contactsList.sort((userID1: number, userID2: number) => {
const sortName1 = (this.users[userID1] || {}).sortName || '';
const sortName2 = (this.users[userID2] || {}).sortName || '';
if(sortName1 == sortName2) {
return 0;
}
}
return sortName1 > sortName2 ? 1 : -1;
});
}); */
return contactsList;
});
@ -215,7 +222,8 @@ export class AppUsersManager {
this.usernames[searchUsername] = userID;
}
apiUser.sortName = apiUser.pFlags.deleted ? '' : SearchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || ''));
//apiUser.sortName = apiUser.pFlags.deleted ? '' : SearchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || ''));
apiUser.sortName = apiUser.pFlags.deleted ? '' : apiUser.first_name + ' ' + (apiUser.last_name || '');
var nameWords = apiUser.sortName.split(' ');
var firstWord = nameWords.shift();

View File

@ -345,17 +345,17 @@ export function longFromInts(high: number, low: number) {
export function intToUint(val: number | string) {
if(typeof(val) === 'string') val = parseInt(val);
if(val < 0) {
/* if(val < 0) {
val = val + 4294967296;
}
} */
return val;
}
export function uintToInt(val: number) {
if(val > 2147483647) {
/* if(val > 2147483647) {
val = val - 4294967296;
}
} */
return val;
}

View File

@ -157,7 +157,5 @@ class LottieLoader {
}
const lottieLoader = new LottieLoader();
//(window as any).LottieLoader = lottieLoader;
(window as any).LottieLoader = lottieLoader;
export default lottieLoader;

View File

@ -227,14 +227,14 @@ class MTPNetworker {
if(!this.connectionInited) { // this will call once for each new session
///////this.log('Wrap api call !this.connectionInited');
let invokeWithLayer = Schema.API.methods.find((m: any) => m.method == 'invokeWithLayer');
let invokeWithLayer = Schema.API.methods.find(m => m.method == 'invokeWithLayer');
if(!invokeWithLayer) throw new Error('no invokeWithLayer!');
serializer.storeInt(+invokeWithLayer.id >>> 0, 'invokeWithLayer');
// @ts-ignore
serializer.storeInt(Schema.layer, 'layer');
let initConnection = Schema.API.methods.find((m: any) => m.method == 'initConnection');
let initConnection = Schema.API.methods.find(m => m.method == 'initConnection');
if(!initConnection) throw new Error('no initConnection!');
serializer.storeInt(+initConnection.id >>> 0, 'initConnection');
@ -260,7 +260,7 @@ class MTPNetworker {
}
if(options.afterMessageID) {
let invokeAfterMsg = Schema.API.methods.find((m: any) => m.method == 'invokeAfterMsg');
let invokeAfterMsg = Schema.API.methods.find(m => m.method == 'invokeAfterMsg');
if(!invokeAfterMsg) throw new Error('no invokeAfterMsg!');
this.log('Api call options.afterMessageID!');

File diff suppressed because one or more lines are too long

View File

@ -16,10 +16,10 @@ import {gzipUncompress} from '../crypto/crypto_utils';
import {gzipUncompress} from '../bin_utils';
/// #endif
const boolFalse = +Schema.API.constructors.find((c: any) => c.predicate == 'boolFalse').id >>> 0;
const boolTrue = +Schema.API.constructors.find((c: any) => c.predicate == 'boolTrue').id >>> 0;
const vector = +Schema.API.constructors.find((c: any) => c.predicate == 'vector').id >>> 0;
const gzipPacked = +Schema.MTProto.constructors.find((c: any) => c.predicate == 'gzip_packed').id >>> 0;
const boolFalse = +Schema.API.constructors.find(c => c.predicate == 'boolFalse').id >>> 0;
const boolTrue = +Schema.API.constructors.find(c => c.predicate == 'boolTrue').id >>> 0;
const vector = +Schema.API.constructors.find(c => c.predicate == 'vector').id >>> 0;
const gzipPacked = +Schema.MTProto.constructors.find(c => c.predicate == 'gzip_packed').id >>> 0;
//console.log('boolFalse', boolFalse == 0xbc799737);
@ -628,7 +628,7 @@ class TLDeserialization {
return result;
}
var schema = (this.mtproto ? Schema.MTProto : Schema.API) as any;
var schema = this.mtproto ? Schema.MTProto : Schema.API;
var predicate = false;
var constructorData: any = false;
@ -675,7 +675,7 @@ class TLDeserialization {
}
}
var i: number = index[constructorCmp];
var i = index[constructorCmp];
if(i) {
constructorData = schema.constructors[i];
}

View File

@ -367,7 +367,7 @@ export const langPack = {
"messageActionChatJoined": "joined the group",
"messageActionChatAddUser": "invited {user}",
"messageActionChatAddUsers": "invited {} users",
"messageActionChatLeave": "left group",
"messageActionChatLeave": "left the group",
"messageActionChatDeleteUser": "removed user",
"messageActionChatJoinedByLink": "joined the group",
"messageActionPinMessage": "pinned message",

View File

@ -1,4 +1,4 @@
import { openBtnMenu, ripple } from "../components/misc";
import { openBtnMenu/* , ripple */ } from "../components/misc";
//import {stackBlurImage} from '../lib/StackBlur';
import Page from "./page";
@ -44,7 +44,7 @@ let onFirstMount = () => import('../lib/appManagers/appImManager').then(() => {/
import('../lib/services');
(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
Array.from(document.getElementsByClassName('btn-menu-toggle')).forEach((el) => {
el.addEventListener('click', (e) => {

View File

@ -1,10 +1,9 @@
import {putPreloader} from '../components/misc';
import resizeableImage from '../lib/cropper';
import pageIm from './pageIm';
//import apiManager from '../lib/mtproto/apiManager';
import apiManager from '../lib/mtproto/mtprotoworker';
import apiFileManager from '../lib/mtproto/apiFileManager';
import Page from './page';
import popupAvatar from '../components/popupAvatar';
let authCode: {
'phone_number': string,
@ -13,84 +12,13 @@ let authCode: {
let onFirstMount = () => {
const pageElement = page.pageEl;
const avatarInput = document.getElementById('avatar-input') as HTMLInputElement;
const avatarPopup = document.getElementsByClassName('popup-avatar')[0];
const avatarPreview = pageElement.querySelector('#canvas-avatar') as HTMLCanvasElement;
const cropContainer = avatarPopup.getElementsByClassName('crop')[0] as HTMLDivElement;
let avatarImage = new Image();
cropContainer.append(avatarImage);
let avatarBlob: Blob;
(avatarPopup.getElementsByClassName('popup-close')[0] as HTMLButtonElement)
.addEventListener('click', function(this, e) {
/* let popup = findUpClassName(this, 'popup');
popup.classList.remove('active'); */
setTimeout(() => {
cropper.removeHandlers();
if(avatarImage) {
avatarImage.remove();
}
}, 200);
/* e.cancelBubble = true;
return false; */
});
let cropper = {
crop: () => {},
removeHandlers: () => {}
};
// apply
avatarPopup.getElementsByClassName('btn-crop')[0].addEventListener('click', () => {
cropper.crop();
avatarPopup.classList.remove('active');
cropper.removeHandlers();
avatarPreview.toBlob(blob => {
avatarBlob = blob; // save blob to send after reg
// darken
let ctx = avatarPreview.getContext('2d');
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.fillRect(0, 0, avatarPreview.width, avatarPreview.height);
}, 'image/jpeg', 1);
avatarImage.remove();
});
avatarInput.addEventListener('change', (e: any) => {
var file = e.target.files[0];
if(!file) {
return;
}
var reader = new FileReader();
reader.onload = (e) => {
var contents = e.target.result as string;
avatarImage = new Image();
cropContainer.append(avatarImage);
avatarImage.src = contents;
avatarImage.onload = () => {
/* let {w, h} = calcImageInBox(avatarImage.naturalWidth, avatarImage.naturalHeight, 460, 554);
cropContainer.style.width = w + 'px';
cropContainer.style.height = h + 'px'; */
avatarPopup.classList.add('active');
cropper = resizeableImage(avatarImage, avatarPreview);
avatarInput.value = '';
};
};
reader.readAsDataURL(file);
}, false);
let uploadAvatar: () => Promise<any>;
pageElement.querySelector('.auth-image').addEventListener('click', () => {
avatarInput.click();
popupAvatar.open(avatarPreview, (_uploadAvatar) => {
uploadAvatar = _uploadAvatar;
});
});
const headerName = pageElement.getElementsByClassName('fullName')[0] as HTMLHeadingElement;
@ -108,13 +36,13 @@ let onFirstMount = () => {
};
let sendAvatar = () => new Promise((resolve, reject) => {
if(!avatarBlob) {
if(!uploadAvatar) {
console.log('User has not selected avatar');
return resolve();
}
console.log('invoking uploadFile...');
apiFileManager.uploadFile(avatarBlob).then((inputFile: any) => {
uploadAvatar().then((inputFile: any) => {
console.log('uploaded smthn', inputFile);
apiManager.invokeApi('photos.uploadProfilePhoto', {

4
src/schema.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
src/schema_pretty.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -176,12 +176,23 @@
//vertical-align: unset;
margin-top: -1.5px;
}
&.is-verified:after {
content: " ";
background: url(/assets/img/icon-verified.svg);
display: inline-block;
width: 20px;
height: 20px;
vertical-align: text-bottom;
margin-left: 2px;
}
}
.user-last-message {
img.emoji {
width: 20px;
height: 20px;
margin-top: -3px;
}
span.emoji {
@ -193,11 +204,11 @@
}
.user-title, .user-last-message {
max-width: 80%;
max-width: 86%;
i {
font-style: normal;
color: $darkblue;
color: $color-blue;
}
}
@ -207,7 +218,7 @@
margin-top: -.3rem;
&[class*=" tgico-"] {
color: $success-color;
color: $color-green;
font-size: 1.25rem;
}
@ -242,7 +253,7 @@
}
.unread {
background: $success-color;
background: $color-green;
}
.unread-muted, .tgico-pinnedchat {
@ -269,6 +280,10 @@
padding-bottom: 17px;
}
}
&:last-child {
border-bottom: none;
}
}
}

View File

@ -124,6 +124,22 @@
}
}
}
.item-main .sidebar-content {
.btn-menu {
bottom: calc(100% + 10px);
}
&:hover {
.btn-corner {
transform: translateY(0px);
&.menu-open:before {
content: $tgico-close;
}
}
}
}
}
#search-container {
@ -138,3 +154,33 @@
opacity: 1;
}
}
.new-channel-container, .new-group-container {
.sidebar-content {
flex-direction: column;
}
.avatar-edit {
width: 120px;
height: 120px;
margin: 1px auto 32px;
flex: 0 0 auto;
}
.input-wrapper {
width: 380px;
margin: 0 auto;
flex: 0 0 auto;
}
.chats-container {
flex: 1 1 auto;
}
.caption {
font-size: 14px;
margin-top: 14px;
margin-left: 23px;
color: #707579;
}
}

View File

@ -5,7 +5,7 @@
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .88);
/* color: $darkgrey; */
/* color: $color-gray; */
display: flex;
align-items: center;
justify-content: center;
@ -122,7 +122,7 @@
.media-viewer-caption {
flex: 1;
text-align: center;
color: $darkgrey;
color: $color-gray;
transition: .2s;
max-width: 50vw;
word-break: break-word;

View File

@ -98,12 +98,12 @@
&-subtitle {
text-align: center;
color: $darkgrey;
color: $color-gray;
font-size: 14px;
margin-bottom: 2px;
&.online {
color: $darkblue;
color: $color-blue;
}
}
@ -123,7 +123,7 @@
left: 24px;
/* top: 0; */
font-size: 24px;
color: $darkgrey;
color: $color-gray;
}
p {
@ -328,7 +328,7 @@
font-size: 2rem;
color: #fff;
text-transform: uppercase;
background-color: $blue;
background-color: $color-blue;
}
}

View File

@ -100,7 +100,7 @@ div.scrollable::-webkit-scrollbar-thumb {
// BROWSER SCROLL
div.scrollable-y::-webkit-scrollbar {
width: .375rem;
height: 200px;
//height: 200px;
}
/* div.scrollable-y::-webkit-scrollbar-thumb {

View File

@ -1,5 +1,5 @@
.menu-horizontal {
color: $darkgrey;
color: $color-gray;
border-bottom: 1px solid $lightgrey;
position: relative;
@ -25,13 +25,13 @@
font-weight: 500;
&.active {
color: $blue;
color: $color-blue;
}
}
&__stripe {
position: absolute;
background: $blue;
background: $color-blue;
//left: 0;
left: -2px;
transition: .3s transform, .3s width;

View File

@ -3,31 +3,20 @@ $border-radius: 8px;
$border-radius-medium: 10px;
$border-radius-big: 12px;
$button-primary-background: #4EA4F6;
$success-color: #4DCD5E;
$color-green: #4DCD5E;
$color-error: #E53935;
$color-gray: #707579;
$color-blue: #50a2e9;
$lightblue: #e6ebee;
$blue: #50a2e9;
$darkblue: #50a2e9;
$lightgreen: #eeffde;
$green: #4dcd5e;
$darkgreen: #50af4f;
$dotgreen: #0ac630;
$color-text-green: $darkgreen;
$lightgrey: #dadce0;
$grey: #c4c9cc;
$darkgrey: #707579;
$light: rgba($darkgrey, 0.08);
$text: #000000;
$bg: #ffffff;
$light: rgba($color-gray, 0.08);
$text-size: 16px;
$time-size: 12px;
$large-screen: 1680px;
//$large-screen: 16800px;
@ -35,6 +24,7 @@ $large-screen: 1680px;
@import "partials/ico";
@import "partials/chatlist";
@import "partials/chat";
@import "partials/chatBubble";
@import "partials/sidebar";
@import "partials/leftSidebar";
@import "partials/rightSidebar";
@ -59,7 +49,7 @@ html {
}
a {
color: $blue;
color: $color-blue;
}
button, input, optgroup, select, textarea, html {
@ -163,7 +153,7 @@ input {
justify-content: center;
&.active {
color: $blue;
color: $color-blue;
}
&:hover {
@ -188,15 +178,6 @@ input {
color: $color-error!important;
}
.btn-menu-toggle {
position: relative;
overflow: visible;
&.menu-open {
background-color: rgba(112, 117, 121, 0.08);
}
}
.btn-menu {
visibility: hidden;
position: absolute;
@ -294,7 +275,7 @@ input {
height: 54px;
line-height: 54px;
border-radius: 50%;
background-color: $blue;
background-color: $color-blue;
text-align: center;
font-size: 1.25em;
/* overflow: hidden; */
@ -412,7 +393,7 @@ input {
height: 70px;
&-ico {
background-color: $blue;
background-color: $color-blue;
border-radius: 5px;
line-height: 10px;
@ -442,7 +423,7 @@ input {
}
&-download {
background-color: $blue;
background-color: $color-blue;
border-radius: 8px;
}
@ -490,7 +471,7 @@ input {
&-size {
white-space: nowrap;
color: $darkgrey;
color: $color-gray;
font-size: 14px;
padding-right: 32px;
line-height: 1.3;
@ -547,7 +528,7 @@ input {
&-toggle, &-download {
border-radius: 50%;
background-color: $blue;
background-color: $color-blue;
font-size: 2.3rem;
align-items: center;
}
@ -566,7 +547,7 @@ input {
fill: #CBCBCB;
&.active {
fill: $blue;
fill: $color-blue;
}
}
}
@ -670,32 +651,36 @@ input {
}
} */
.avatar-edit {
position: relative;
border-radius: 50%;
cursor: pointer;
overflow: hidden;
&-canvas {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
background-color: $color-blue;
}
.tgico-cameraadd {
position: absolute;
font-size: 48px;
line-height: 48px;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
z-index: 2;
color: #fff;
}
}
.page-signUp {
.auth-image {
border-radius: 50%;
cursor: pointer;
position: relative;
overflow: hidden;
margin-top: 10px;
margin-bottom: 14px;
canvas {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
background-color: $blue;
}
svg {
position: absolute;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
z-index: 2;
}
}
}
@ -758,6 +743,10 @@ input {
position: relative;
z-index: 1;
/* font-weight: 500; */
/* &:hover {
border-color: #000;
} */
&:focus {
border-color: $button-primary-background;
@ -951,16 +940,16 @@ input {
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: $placeholder-color;
color: #a2acb4;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: $placeholder-color;
color: #a2acb4;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: $placeholder-color;
color: #a2acb4;
}
input:focus, button:focus {
@ -1019,7 +1008,7 @@ input:focus, button:focus {
} */
.btn-primary {
background: $blue;
background: $color-blue;
color: #fff;
border-radius: $border-radius-medium;
width: 100%;
@ -1033,7 +1022,7 @@ input:focus, button:focus {
padding: 0; // new
&:hover {
background: darken($blue, 8%);
background: darken($color-blue, 8%);
}
svg, use {
@ -1058,6 +1047,16 @@ input:focus, button:focus {
}
}
.btn-menu-toggle {
position: relative;
overflow: visible !important;
font-weight: normal !important;
&:not(.btn-primary).menu-open {
background-color: rgba(112, 117, 121, 0.08);
}
}
.preloader {
&-circular {
animation: rotate 2s linear infinite;
@ -1336,6 +1335,12 @@ img.emoji {
display: flex;
max-height: 100vh;
min-height: 100vh;
.avatar-edit {
.tgico-cameraadd {
top: 52%;
}
}
> div {
height: 100%;