fixes and fixes
This commit is contained in:
parent
47ce25fce2
commit
d3836e71f8
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,4 +4,5 @@ __pycache__
|
||||
dist
|
||||
.DS_Store
|
||||
stats.json
|
||||
certs
|
||||
certs
|
||||
src/rlottie.github.io
|
@ -50,9 +50,10 @@ export class SearchGroup {
|
||||
|
||||
export default class AppSearch {
|
||||
private minMsgID = 0;
|
||||
private loadedCount = 0;
|
||||
private foundCount = 0;
|
||||
private loadedCount = -1;
|
||||
private foundCount = -1;
|
||||
private offsetRate = 0;
|
||||
private loadedContacts = false;
|
||||
|
||||
private searchPromise: Promise<void> = null;
|
||||
private searchTimeout: number = 0;
|
||||
@ -107,9 +108,10 @@ export default class AppSearch {
|
||||
}
|
||||
|
||||
this.minMsgID = 0;
|
||||
this.loadedCount = 0;
|
||||
this.foundCount = 0;
|
||||
this.loadedCount = -1;
|
||||
this.foundCount = -1;
|
||||
this.offsetRate = 0;
|
||||
this.loadedContacts = false;
|
||||
|
||||
for(let i in this.searchGroups) {
|
||||
this.searchGroups[i].clear();
|
||||
@ -129,25 +131,27 @@ export default class AppSearch {
|
||||
public searchMore() {
|
||||
if(this.searchPromise) return this.searchPromise;
|
||||
|
||||
let query = this.query;
|
||||
const query = this.query;
|
||||
|
||||
if(!query.trim()) {
|
||||
this.onSearch && this.onSearch(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.loadedCount != 0 && this.loadedCount >= this.foundCount) {
|
||||
if(this.foundCount != -1 && this.loadedCount >= this.foundCount) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let maxID = appMessagesIDsManager.getMessageIDInfo(this.minMsgID)[0];
|
||||
const maxID = appMessagesIDsManager.getMessageIDInfo(this.minMsgID)[0] || 0;
|
||||
|
||||
if(!this.peerID && !maxID) {
|
||||
if(!this.peerID && !maxID && !this.loadedContacts) {
|
||||
appUsersManager.searchContacts(query, 20).then((contacts: any) => {
|
||||
if(this.searchInput.value != query) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedContacts = true;
|
||||
|
||||
///////this.log('input search contacts result:', contacts);
|
||||
|
||||
let setResults = (results: any, group: SearchGroup, showMembersCount = false) => {
|
||||
@ -205,19 +209,19 @@ export default class AppSearch {
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('input search result:', this.peerID, query, null, maxID, 20, res);
|
||||
console.log('input search result:', this.peerID, query, null, maxID, 20, res);
|
||||
|
||||
let {count, history, next_rate} = res;
|
||||
const {count, history, next_rate} = res;
|
||||
|
||||
if(history[0] == this.minMsgID) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
let searchGroup = this.searchGroups['messages'];
|
||||
const searchGroup = this.searchGroups['messages'];
|
||||
searchGroup.setActive();
|
||||
|
||||
history.forEach((msgID: number) => {
|
||||
let message = appMessagesManager.getMessage(msgID);
|
||||
const message = appMessagesManager.getMessage(msgID);
|
||||
let originalDialog = appMessagesManager.getDialogByPeerID(message.peerID)[0];
|
||||
|
||||
if(!originalDialog) {
|
||||
@ -230,15 +234,18 @@ export default class AppSearch {
|
||||
} as any;
|
||||
}
|
||||
|
||||
let {dialog, dom} = appDialogsManager.addDialog(originalDialog, this.scrollable/* searchGroup.list */, false);
|
||||
const {dialog, dom} = appDialogsManager.addDialog(originalDialog, this.scrollable/* searchGroup.list */, false);
|
||||
appDialogsManager.setLastMessage(dialog, message, dom, query);
|
||||
});
|
||||
|
||||
this.minMsgID = history[history.length - 1];
|
||||
this.offsetRate = next_rate;
|
||||
this.loadedCount += history.length;
|
||||
if(this.loadedCount == -1) {
|
||||
this.loadedCount = 0;
|
||||
}
|
||||
|
||||
if(!this.foundCount) {
|
||||
if(this.foundCount == -1) {
|
||||
this.foundCount = count;
|
||||
this.onSearch && this.onSearch(this.foundCount);
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ export class AppSelectPeers {
|
||||
}
|
||||
|
||||
public add(peerID: any, title?: string) {
|
||||
console.trace('add');
|
||||
//console.trace('add');
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('selector-user', 'scale-in');
|
||||
|
||||
|
@ -271,6 +271,7 @@ function wrapAudio(doc: MTDocument, audioEl: AudioElement) {
|
||||
|
||||
export default class AudioElement extends HTMLElement {
|
||||
public audio: HTMLAudioElement;
|
||||
public preloader: ProgressivePreloader;
|
||||
|
||||
private attachedHandlers: {[name: string]: any[]} = {};
|
||||
private onTypeDisconnect: () => void;
|
||||
@ -303,7 +304,7 @@ export default class AudioElement extends HTMLElement {
|
||||
const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement;
|
||||
audioTimeDiv.innerHTML = durationStr;
|
||||
|
||||
let preloader: ProgressivePreloader;
|
||||
let preloader: ProgressivePreloader = this.preloader;
|
||||
let promise: CancellablePromise<Blob>;
|
||||
|
||||
const onLoad = () => {
|
||||
@ -377,7 +378,8 @@ export default class AudioElement extends HTMLElement {
|
||||
this.addEventListener('click', onClick);
|
||||
this.click();
|
||||
} else {
|
||||
onLoad();
|
||||
this.preloader.attach(this.querySelector('.audio-download'), false);
|
||||
//onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,6 +404,8 @@ export default class AudioElement extends HTMLElement {
|
||||
|
||||
delete this.attachedHandlers[name];
|
||||
}
|
||||
|
||||
this.preloader = null;
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
|
@ -151,7 +151,7 @@ export class ChatInput {
|
||||
}).then((webpage: any) => {
|
||||
appWebPagesManager.saveWebPage(webpage);
|
||||
if(this.lastUrl != url) return;
|
||||
console.log('got webpage: ', webpage);
|
||||
//console.log('got webpage: ', webpage);
|
||||
|
||||
this.setTopInfo(webpage.site_name || webpage.title, webpage.description || webpage.url);
|
||||
|
||||
@ -235,7 +235,7 @@ export class ChatInput {
|
||||
return new Promise<HTMLDivElement>((resolve, reject) => {
|
||||
let params: SendFileParams = {};
|
||||
params.file = file;
|
||||
console.log('selected file:', file, typeof(file), willAttach);
|
||||
//console.log('selected file:', file, typeof(file), willAttach);
|
||||
let itemDiv = document.createElement('div');
|
||||
switch(willAttach.type) {
|
||||
case 'media': {
|
||||
@ -249,6 +249,8 @@ export class ChatInput {
|
||||
source.src = params.objectURL = URL.createObjectURL(file);
|
||||
video.autoplay = false;
|
||||
video.controls = false;
|
||||
video.muted = true;
|
||||
video.setAttribute('playsinline', '');
|
||||
|
||||
video.onloadeddata = () => {
|
||||
params.width = video.videoWidth;
|
||||
@ -283,6 +285,8 @@ export class ChatInput {
|
||||
type: file.type.indexOf('image/') !== -1 ? 'photo' : 'doc'
|
||||
} as any, false, true);
|
||||
|
||||
params.objectURL = URL.createObjectURL(file);
|
||||
|
||||
itemDiv.append(docDiv);
|
||||
resolve(itemDiv);
|
||||
break;
|
||||
@ -364,7 +368,7 @@ export class ChatInput {
|
||||
this.attachMediaPopUp.mediaContainer.append(div);
|
||||
}
|
||||
|
||||
console.log('chatInput album layout:', layout);
|
||||
//console.log('chatInput album layout:', layout);
|
||||
} else {
|
||||
let params = willAttach.sendFileDetails[0];
|
||||
let div = results[0];
|
||||
@ -407,11 +411,13 @@ export class ChatInput {
|
||||
}, false);
|
||||
|
||||
this.attachMenu.media.addEventListener('click', () => {
|
||||
this.fileInput.setAttribute('accept', 'image/*, video/*');
|
||||
willAttach.type = 'media';
|
||||
this.fileInput.click();
|
||||
});
|
||||
|
||||
this.attachMenu.document.addEventListener('click', () => {
|
||||
this.fileInput.removeAttribute('accept');
|
||||
willAttach.type = 'document';
|
||||
this.fileInput.click();
|
||||
});
|
||||
@ -451,7 +457,7 @@ export class ChatInput {
|
||||
let caption = this.attachMediaPopUp.captionInput.value;
|
||||
willAttach.isMedia = willAttach.type == 'media';
|
||||
|
||||
console.log('will send files with options:', willAttach);
|
||||
//console.log('will send files with options:', willAttach);
|
||||
|
||||
let peerID = appImManager.peerID;
|
||||
|
||||
@ -471,7 +477,8 @@ export class ChatInput {
|
||||
|
||||
let promises = willAttach.sendFileDetails.map(params => {
|
||||
let promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({
|
||||
isMedia: willAttach.isMedia,
|
||||
//isMedia: willAttach.isMedia,
|
||||
isMedia: true,
|
||||
caption,
|
||||
replyToMsgID: this.replyToMsgID
|
||||
}, params));
|
||||
|
@ -3,7 +3,7 @@ import { horizontalMenu, renderImageFromUrl, putPreloader } from "./misc";
|
||||
import lottieLoader from "../lib/lottieLoader";
|
||||
//import Scrollable from "./scrollable";
|
||||
import Scrollable from "./scrollable_new";
|
||||
import { findUpTag, whichChild, calcImageInBox } from "../lib/utils";
|
||||
import { findUpTag, whichChild, calcImageInBox, emojiUnicode } from "../lib/utils";
|
||||
import { RichTextProcessor } from "../lib/richtextprocessor";
|
||||
import appStickersManager, { MTStickerSet } from "../lib/appManagers/appStickersManager";
|
||||
//import apiManager from '../lib/mtproto/apiManager';
|
||||
@ -16,6 +16,7 @@ import Config, { touchSupport } from "../lib/config";
|
||||
import { MTDocument } from "../types";
|
||||
import animationIntersector from "./animationIntersector";
|
||||
import appSidebarRight from "../lib/appManagers/appSidebarRight";
|
||||
import appStateManager from "../lib/appManagers/appStateManager";
|
||||
|
||||
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
|
||||
|
||||
@ -27,6 +28,12 @@ interface EmoticonsTab {
|
||||
class EmojiTab implements EmoticonsTab {
|
||||
public content: HTMLElement;
|
||||
|
||||
private recent: string[] = [];
|
||||
private recentItemsDiv: HTMLElement;
|
||||
|
||||
private heights: number[] = [];
|
||||
private scroll: Scrollable;
|
||||
|
||||
init() {
|
||||
this.content = document.getElementById('content-emoji') as HTMLDivElement;
|
||||
|
||||
@ -37,7 +44,9 @@ class EmojiTab implements EmoticonsTab {
|
||||
|
||||
const sorted: {
|
||||
[category: string]: string[]
|
||||
} = {};
|
||||
} = {
|
||||
'Recent': []
|
||||
};
|
||||
|
||||
for(const emoji in Config.Emoji) {
|
||||
const details = Config.Emoji[emoji];
|
||||
@ -72,46 +81,48 @@ class EmojiTab implements EmoticonsTab {
|
||||
|
||||
const emojis = sorted[category];
|
||||
emojis.forEach(emoji => {
|
||||
//const emoji = details.unified;
|
||||
//const emoji = (details.unified as string).split('-')
|
||||
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
|
||||
/* if(emojiUnicode(emoji) == '1f481-200d-2642') {
|
||||
console.log('append emoji', emoji, emojiUnicode(emoji));
|
||||
} */
|
||||
|
||||
const spanEmoji = document.createElement('span');
|
||||
const kek = RichTextProcessor.wrapRichText(emoji);
|
||||
this.appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv);
|
||||
|
||||
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);
|
||||
/* if(category == 'Smileys & Emotion') {
|
||||
console.log('appended emoji', emoji, itemsDiv.children[itemsDiv.childElementCount - 1].innerHTML, emojiUnicode(emoji));
|
||||
} */
|
||||
});
|
||||
|
||||
divs[category] = div;
|
||||
}
|
||||
//console.timeEnd('emojiParse');
|
||||
|
||||
const heights: number[] = [0];
|
||||
|
||||
let prevCategoryIndex = 1;
|
||||
let prevCategoryIndex = 0;
|
||||
const menu = this.content.previousElementSibling.firstElementChild as HTMLUListElement;
|
||||
const emojiScroll = new Scrollable(this.content, 'y', 'EMOJI', null);
|
||||
const emojiScroll = this.scroll = new Scrollable(this.content, 'y', 'EMOJI', null);
|
||||
emojiScroll.container.addEventListener('scroll', (e) => {
|
||||
prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container);
|
||||
prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, this.heights, prevCategoryIndex, emojiScroll.container);
|
||||
});
|
||||
//emojiScroll.setVirtualContainer(emojiScroll.container);
|
||||
|
||||
const preloader = putPreloader(this.content, true);
|
||||
|
||||
setTimeout(() => {
|
||||
Promise.all([
|
||||
new Promise((resolve) => setTimeout(resolve, 200)),
|
||||
|
||||
appStateManager.getState().then(state => {
|
||||
if(Array.isArray(state.recentEmoji)) {
|
||||
this.recent = state.recentEmoji;
|
||||
}
|
||||
})
|
||||
]).then(() => {
|
||||
preloader.remove();
|
||||
|
||||
this.recentItemsDiv = divs['Recent'].querySelector('.category-items');
|
||||
for(const emoji of this.recent) {
|
||||
this.appendEmoji(emoji, this.recentItemsDiv);
|
||||
}
|
||||
|
||||
categories.unshift('Recent');
|
||||
categories.map(category => {
|
||||
const div = divs[category];
|
||||
|
||||
@ -123,27 +134,86 @@ class EmojiTab implements EmoticonsTab {
|
||||
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);
|
||||
this.heights.push((this.heights[this.heights.length - 1] || 0) + div.scrollHeight);
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
this.content.addEventListener('click', this.onContentClick);
|
||||
EmoticonsDropdown.menuOnClick(menu, heights, emojiScroll);
|
||||
EmoticonsDropdown.menuOnClick(menu, this.heights, emojiScroll);
|
||||
this.init = null;
|
||||
}
|
||||
|
||||
private appendEmoji(emoji: string, container: HTMLElement, prepend = false) {
|
||||
//const emoji = details.unified;
|
||||
//const emoji = (details.unified as string).split('-')
|
||||
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
|
||||
|
||||
const spanEmoji = document.createElement('span');
|
||||
const kek = RichTextProcessor.wrapEmojiText(emoji);
|
||||
|
||||
/* if(!kek.includes('emoji')) {
|
||||
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
|
||||
return;
|
||||
} */
|
||||
|
||||
//console.log(kek);
|
||||
|
||||
spanEmoji.innerHTML = kek;
|
||||
|
||||
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
|
||||
//spanEmoji.setAttribute('emoji', emoji);
|
||||
if(prepend) container.prepend(spanEmoji);
|
||||
else container.appendChild(spanEmoji);
|
||||
}
|
||||
|
||||
private getEmojiFromElement(element: HTMLElement) {
|
||||
if(element.tagName == 'SPAN' && !element.classList.contains('emoji')) {
|
||||
element = element.firstElementChild as HTMLElement;
|
||||
}
|
||||
|
||||
return element.getAttribute('alt') || element.innerText;
|
||||
}
|
||||
|
||||
onContentClick = (e: MouseEvent) => {
|
||||
let target = e.target as any;
|
||||
let target = e.target as HTMLElement;
|
||||
//if(target.tagName != 'SPAN') return;
|
||||
|
||||
if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) {
|
||||
target = target.firstElementChild;
|
||||
target = target.firstElementChild as HTMLElement;
|
||||
} else if(target.tagName == 'DIV') return;
|
||||
|
||||
//console.log('contentEmoji div', target);
|
||||
|
||||
appImManager.chatInputC.messageInput.innerHTML += target.outerHTML;
|
||||
|
||||
// Recent
|
||||
const emoji = this.getEmojiFromElement(target);
|
||||
(Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => {
|
||||
const _emoji = this.getEmojiFromElement(el);
|
||||
if(emoji == _emoji) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
const scrollHeight = this.recentItemsDiv.scrollHeight;
|
||||
this.appendEmoji(emoji, this.recentItemsDiv, true);
|
||||
|
||||
// нужно поставить новые размеры для скролла
|
||||
if(this.recentItemsDiv.scrollHeight != scrollHeight) {
|
||||
this.heights.length = 0;
|
||||
(Array.from(this.scroll.container.children) as HTMLElement[]).forEach(div => {
|
||||
this.heights.push((this.heights[this.heights.length - 1] || 0) + div.scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
this.recent.findAndSplice(e => e == emoji);
|
||||
this.recent.unshift(emoji);
|
||||
if(this.recent.length > 36) {
|
||||
this.recent.length = 36;
|
||||
}
|
||||
|
||||
appStateManager.pushToState('recentEmoji', this.recent);
|
||||
|
||||
// Append to input
|
||||
const event = new Event('input', {bubbles: true, cancelable: true});
|
||||
appImManager.chatInputC.messageInput.dispatchEvent(event);
|
||||
};
|
||||
@ -685,6 +755,7 @@ class EmoticonsDropdown {
|
||||
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
|
||||
/* dropdown.classList.remove('active');
|
||||
toggleEl.classList.remove('active'); */
|
||||
emoticonsDropdown.toggle(false);
|
||||
} else {
|
||||
console.warn('got no doc by id:', fileID);
|
||||
}
|
||||
|
@ -111,8 +111,10 @@ export default class ProgressivePreloader {
|
||||
return;
|
||||
}
|
||||
|
||||
let totalLength = this.circle.getTotalLength();
|
||||
//console.log('setProgress', (percents / 100 * totalLength));
|
||||
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200';
|
||||
try {
|
||||
let totalLength = this.circle.getTotalLength();
|
||||
//console.log('setProgress', (percents / 100 * totalLength));
|
||||
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200';
|
||||
} catch(err) {}
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +143,8 @@ export default class AppChatFoldersTab implements SliderTab {
|
||||
delete this.filtersRendered[filter.id]
|
||||
}
|
||||
});
|
||||
|
||||
this.getSuggestedFilters();
|
||||
}
|
||||
|
||||
private getSuggestedFilters() {
|
||||
|
@ -31,7 +31,10 @@ export default class AppSettingsTab implements SliderTab {
|
||||
});
|
||||
|
||||
this.logOutBtn.addEventListener('click', (e) => {
|
||||
apiManager.logOut();
|
||||
apiManager.logOut().finally(() => {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
this.buttons.edit.addEventListener('click', () => {
|
||||
|
File diff suppressed because one or more lines are too long
@ -90,7 +90,7 @@ if(false) {
|
||||
formatted.forEach(e => {
|
||||
let {unified, name, short_names, category, sheet_x, sheet_y, sort_order} = e;
|
||||
|
||||
let emoji = unified.replace(/-FE0F/gi, '').split('-')
|
||||
let emoji = unified/* .replace(/-FE0F/gi, '') */.split('-')
|
||||
.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
|
||||
|
||||
let c = categories[category] === undefined ? 9 : categories[category];
|
||||
|
@ -77,7 +77,7 @@
|
||||
<p class="subtitle sent-type"></p>
|
||||
<div class="input-wrapper">
|
||||
<div class="input-field">
|
||||
<input type="number" name="code" id="code" autocomplete="off" required />
|
||||
<input type="tel" name="code" id="code" autocomplete="off" required />
|
||||
<label for="code">Code</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -214,7 +214,7 @@
|
||||
<div class="folders-tabs-scrollable">
|
||||
<nav class="menu-horizontal" style="display: none;" id="folders-tabs">
|
||||
<ul>
|
||||
<li class="rp"><span>All</span></li>
|
||||
<li class="rp"><span>All</span><span class="unread-count"></span></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@ -268,12 +268,12 @@
|
||||
</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>
|
||||
<input type="text" name="name" class="new-channel-name" id="new-channel-name" autocomplete="xxDDqqOX" required="">
|
||||
<label for="new-channel-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>
|
||||
<input type="text" name="description" class="new-channel-description" id="new-channel-description" autocomplete="aintsofunnow" required="">
|
||||
<label for="new-channel-description">Description (optional)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">You can provide an optional description for your channel.</div>
|
||||
@ -301,8 +301,8 @@
|
||||
</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>
|
||||
<input type="text" name="name" class="new-group-name" id="new-group-name" autocomplete="feellikeamonster2112" required="">
|
||||
<label for="new-group-name">Group Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button>
|
||||
|
@ -1,272 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ export default class MP4Source {
|
||||
private loading = false;
|
||||
private url: string;
|
||||
|
||||
private log = logger('MP4', LogLevels.error);
|
||||
private log = logger('MP4'/* , LogLevels.error */);
|
||||
|
||||
//public onLoadBuffer: (offset: number)
|
||||
|
||||
|
@ -1,279 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import appChatsManager from "./appChatsManager";
|
||||
import AvatarElement from "../../components/avatar";
|
||||
import { PopupButton, PopupPeer } from "../../components/popup";
|
||||
import { SliderTab } from "../../components/slider";
|
||||
import appStateManager from "./appStateManager";
|
||||
|
||||
type DialogDom = {
|
||||
avatarEl: AvatarElement,
|
||||
@ -330,7 +331,7 @@ export class AppDialogsManager {
|
||||
public chatsContainer = document.getElementById('chats-container') as HTMLDivElement;
|
||||
private chatsPreloader: HTMLDivElement;
|
||||
|
||||
public loadDialogsPromise: ReturnType<AppMessagesManager["getConversations"]>;
|
||||
public loadDialogsPromise: Promise<any>;
|
||||
public loadedAll = false;
|
||||
|
||||
public scroll: Scrollable = null;
|
||||
@ -351,15 +352,21 @@ export class AppDialogsManager {
|
||||
};
|
||||
private filtersRendered: {
|
||||
[filterID: string]: {
|
||||
menu: HTMLElement, container: HTMLElement
|
||||
menu: HTMLElement,
|
||||
container: HTMLElement,
|
||||
unread: HTMLElement
|
||||
}
|
||||
} = {};
|
||||
private showFiltersTimeout: number;
|
||||
private allUnreadCount: HTMLElement;
|
||||
|
||||
private accumulateArchivedTimeout: number;
|
||||
|
||||
constructor() {
|
||||
this.chatList.addEventListener('contextmenu', this.contextMenu.onContextMenu);
|
||||
this.chatsPreloader = putPreloader(null, true);
|
||||
|
||||
this.allUnreadCount = this.folders.menu.querySelector('.unread-count');
|
||||
|
||||
if(USEPINNEDDELIMITER) {
|
||||
this.pinnedDelimiter = document.createElement('div');
|
||||
@ -423,6 +430,7 @@ export class AppDialogsManager {
|
||||
this.setDialogPosition(dialog);
|
||||
|
||||
this.setPinnedDelimiter();
|
||||
this.setFiltersUnreadCount();
|
||||
});
|
||||
|
||||
$rootScope.$on('dialog_flush', (e: CustomEvent) => {
|
||||
@ -431,6 +439,7 @@ export class AppDialogsManager {
|
||||
if(dialog) {
|
||||
this.setLastMessage(dialog);
|
||||
this.validateForFilter();
|
||||
this.setFiltersUnreadCount();
|
||||
}
|
||||
});
|
||||
|
||||
@ -444,6 +453,7 @@ export class AppDialogsManager {
|
||||
|
||||
this.setPinnedDelimiter();
|
||||
this.validateForFilter();
|
||||
this.setFiltersUnreadCount();
|
||||
});
|
||||
|
||||
$rootScope.$on('dialog_drop', (e: CustomEvent) => {
|
||||
@ -455,6 +465,8 @@ export class AppDialogsManager {
|
||||
delete this.doms[peerID];
|
||||
this.scroll.reorder();
|
||||
}
|
||||
|
||||
this.setFiltersUnreadCount();
|
||||
});
|
||||
|
||||
$rootScope.$on('dialog_unread', (e: CustomEvent) => {
|
||||
@ -472,6 +484,7 @@ export class AppDialogsManager {
|
||||
}
|
||||
|
||||
this.validateForFilter();
|
||||
this.setFiltersUnreadCount();
|
||||
}
|
||||
});
|
||||
|
||||
@ -505,6 +518,7 @@ export class AppDialogsManager {
|
||||
const dialog = folder[i];
|
||||
this.updateDialog(dialog);
|
||||
}
|
||||
this.setFiltersUnreadCount();
|
||||
}
|
||||
});
|
||||
|
||||
@ -549,6 +563,7 @@ export class AppDialogsManager {
|
||||
|
||||
if(this.filterID == id) return;
|
||||
|
||||
this.chatLists[id].innerHTML = '';
|
||||
this.scroll.setVirtualContainer(this.chatLists[id]);
|
||||
this.filterID = id;
|
||||
this.onTabChange();
|
||||
@ -563,7 +578,7 @@ export class AppDialogsManager {
|
||||
//selectTab(0);
|
||||
(this.folders.menu.firstElementChild.firstElementChild as HTMLElement).click();
|
||||
|
||||
/* false && */appMessagesManager.loadSavedState().then(() => {
|
||||
/* false && */appStateManager.loadSavedState().then(() => {
|
||||
return appMessagesManager.filtersStorage.getDialogFilters();
|
||||
}).then(filters => {
|
||||
for(const filterID in filters) {
|
||||
@ -601,6 +616,26 @@ export class AppDialogsManager {
|
||||
this.loadDialogs(this.filterID);
|
||||
};
|
||||
|
||||
public setFilterUnreadCount(filterID: number, folder?: Dialog[]) {
|
||||
const unreadSpan = filterID == 0 ? this.allUnreadCount : this.filtersRendered[filterID]?.unread;
|
||||
if(!unreadSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder = folder || appMessagesManager.dialogsStorage.getFolder(filterID);
|
||||
const sum = folder.reduce((acc, dialog) => acc + +!!dialog.unread_count, 0);
|
||||
|
||||
unreadSpan.innerText = sum ? '' + sum : '';
|
||||
}
|
||||
|
||||
public setFiltersUnreadCount() {
|
||||
for(const filterID in this.filtersRendered) {
|
||||
this.setFilterUnreadCount(+filterID);
|
||||
}
|
||||
|
||||
this.setFilterUnreadCount(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалит неподходящие чаты из списка, но не добавит их(!)
|
||||
*/
|
||||
@ -635,7 +670,9 @@ export class AppDialogsManager {
|
||||
const li = document.createElement('li');
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = RichTextProcessor.wrapEmojiText(filter.title);
|
||||
li.append(span);
|
||||
const unreadSpan = document.createElement('span');
|
||||
unreadSpan.classList.add('unread-count');
|
||||
li.append(span, unreadSpan);
|
||||
ripple(li);
|
||||
|
||||
this.folders.menu.firstElementChild.append(li);
|
||||
@ -650,13 +687,18 @@ export class AppDialogsManager {
|
||||
this.setListClickListener(ul);
|
||||
ul.addEventListener('contextmenu', this.contextMenu.onContextMenu);
|
||||
|
||||
setTimeout(() => {
|
||||
this.folders.menu.style.display = '';
|
||||
}, 0);
|
||||
if(!this.showFiltersTimeout) {
|
||||
this.showFiltersTimeout = setTimeout(() => {
|
||||
this.showFiltersTimeout = 0;
|
||||
this.folders.menu.style.display = '';
|
||||
this.setFiltersUnreadCount();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.filtersRendered[filter.id] = {
|
||||
menu: li,
|
||||
container: div
|
||||
container: div,
|
||||
unread: unreadSpan
|
||||
};
|
||||
}
|
||||
|
||||
@ -688,9 +730,14 @@ export class AppDialogsManager {
|
||||
//console.time('getDialogs time');
|
||||
|
||||
const loadCount = 50/*this.chatsLoadCount */;
|
||||
this.loadDialogsPromise = appMessagesManager.getConversations('', offsetIndex, loadCount, folderID);
|
||||
|
||||
const getConversationPromise = (this.filterID > 1 ? appUsersManager.getContacts() as Promise<any> : Promise.resolve()).then(() => {
|
||||
return appMessagesManager.getConversations('', offsetIndex, loadCount, folderID);
|
||||
});
|
||||
|
||||
this.loadDialogsPromise = getConversationPromise;
|
||||
|
||||
const result = await this.loadDialogsPromise;
|
||||
const result = await getConversationPromise;
|
||||
|
||||
//console.timeEnd('getDialogs time');
|
||||
|
||||
@ -1170,6 +1217,11 @@ export class AppDialogsManager {
|
||||
|
||||
this.doms[dialog.peerID] = dom;
|
||||
|
||||
if($rootScope.selectedPeerID == peerID) {
|
||||
li.classList.add('active');
|
||||
this.lastActiveListElement = li;
|
||||
}
|
||||
|
||||
/* if(container) {
|
||||
container.append(li);
|
||||
} */
|
||||
|
@ -294,7 +294,11 @@ class AppDocsManager {
|
||||
};
|
||||
|
||||
promise.then(() => {
|
||||
deferred.resolve(this.createMP4Stream(doc));
|
||||
if(doc.url) { // может быть уже загружен из кэша
|
||||
deferred.resolve();
|
||||
} else {
|
||||
deferred.resolve(this.createMP4Stream(doc));
|
||||
}
|
||||
});
|
||||
|
||||
return deferred;
|
||||
|
@ -678,6 +678,12 @@ export class AppImManager {
|
||||
}
|
||||
}
|
||||
|
||||
if(['audio', 'voice'].includes(message.media?.document?.type)) {
|
||||
const audio = bubble.querySelector('audio-element');
|
||||
audio.setAttribute('doc-id', message.media.document.id);
|
||||
audio.setAttribute('message-id', '' + mid);
|
||||
}
|
||||
|
||||
bubble.classList.remove('is-sending');
|
||||
bubble.classList.add('is-sent');
|
||||
bubble.dataset.mid = mid;
|
||||
@ -1404,12 +1410,12 @@ export class AppImManager {
|
||||
|
||||
if(samePeer) {
|
||||
if(this.bubbles[lastMsgID]) {
|
||||
if(dialog && lastMsgID == topMessage) {
|
||||
this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight);
|
||||
this.scroll.scrollTop = this.scroll.scrollHeight;
|
||||
} else if(isTarget) {
|
||||
if(isTarget) {
|
||||
this.scrollable.scrollIntoView(this.bubbles[lastMsgID]);
|
||||
this.highlightBubble(this.bubbles[lastMsgID]);
|
||||
} else if(dialog && lastMsgID == topMessage) {
|
||||
this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight);
|
||||
this.scroll.scrollTop = this.scroll.scrollHeight;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1503,14 +1509,15 @@ export class AppImManager {
|
||||
|
||||
this.lazyLoadQueue.unlock();
|
||||
|
||||
if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
|
||||
//if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
|
||||
if(dialog && (isTarget || (lastMsgID != topMessage)) && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
|
||||
if(this.scrollable.scrollLocked) {
|
||||
clearTimeout(this.scrollable.scrollLocked);
|
||||
this.scrollable.scrollLocked = 0;
|
||||
}
|
||||
|
||||
const fromUp = maxBubbleID > 0 && (maxBubbleID < lastMsgID || lastMsgID < 0);
|
||||
const forwardingUnread = dialog.read_inbox_max_id == lastMsgID;
|
||||
const forwardingUnread = dialog.read_inbox_max_id == lastMsgID && !isTarget;
|
||||
if(!fromUp && (samePeer || forwardingUnread)) {
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||
}
|
||||
@ -2209,9 +2216,14 @@ export class AppImManager {
|
||||
let doc = appDocsManager.getDoc(message.id);
|
||||
this.log('will wrap pending doc:', doc);
|
||||
let docDiv = wrapDocument(doc, false, true, message.id);
|
||||
|
||||
let icoDiv = docDiv.querySelector('.audio-download, .document-ico');
|
||||
preloader.attach(icoDiv, false);
|
||||
|
||||
if(doc.type == 'audio' || doc.type == 'voice') {
|
||||
// @ts-ignore
|
||||
docDiv.preloader = preloader;
|
||||
} else {
|
||||
let icoDiv = docDiv.querySelector('.audio-download, .document-ico');
|
||||
preloader.attach(icoDiv, false);
|
||||
}
|
||||
|
||||
if(pending.type == 'voice') {
|
||||
bubble.classList.add('bubble-audio');
|
||||
@ -2735,7 +2747,10 @@ export class AppImManager {
|
||||
promise = result.then((result) => {
|
||||
this.log('getHistory not cached result by maxID:', maxID, reverse, isBackLimit, result, peerID);
|
||||
|
||||
if(justLoad) return true;
|
||||
if(justLoad) {
|
||||
this.scrollable.onScroll(); // нужно делать из-за ранней прогрузки
|
||||
return true;
|
||||
}
|
||||
//console.timeEnd('appImManager call getHistory');
|
||||
|
||||
if(this.peerID != peerID) {
|
||||
|
@ -475,13 +475,14 @@ export class AppMediaViewer {
|
||||
}
|
||||
|
||||
private setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) {
|
||||
let media = aspecter.firstElementChild;
|
||||
/* let media = aspecter.firstElementChild;
|
||||
let proportion: number;
|
||||
if(media instanceof HTMLImageElement) {
|
||||
proportion = media.naturalWidth / media.naturalHeight;
|
||||
} else if(media instanceof HTMLVideoElement) {
|
||||
proportion = media.videoWidth / media.videoHeight;
|
||||
}
|
||||
} */
|
||||
const proportion = containerRect.width / containerRect.height;
|
||||
|
||||
let {width, height} = rect;
|
||||
/* if(proportion == 1) {
|
||||
|
@ -59,7 +59,6 @@ export type Dialog = {
|
||||
|
||||
index: number,
|
||||
peerID: number,
|
||||
pinnedIndex: number,
|
||||
pFlags: Partial<{
|
||||
pinned: true,
|
||||
unread_mark: true
|
||||
@ -67,15 +66,15 @@ export type Dialog = {
|
||||
pts: number
|
||||
}
|
||||
|
||||
class DialogsStorage {
|
||||
export class DialogsStorage {
|
||||
public dialogs: {[peerID: string]: Dialog} = {};
|
||||
public byFolders: {[folderID: number]: Dialog[]} = {};
|
||||
|
||||
public allDialogsLoaded: {[folder_id: number]: boolean} = {};
|
||||
public dialogsOffsetDate: {[folder_id: number]: number} = {};
|
||||
public pinnedIndexes: {[folder_id: number]: number} = {
|
||||
0: 0,
|
||||
1: 0
|
||||
public pinnedOrders: {[folder_id: number]: number[]} = {
|
||||
0: [],
|
||||
1: []
|
||||
};
|
||||
public dialogsNum = 0;
|
||||
|
||||
@ -176,26 +175,14 @@ class DialogsStorage {
|
||||
}
|
||||
|
||||
public generateDialogPinnedDateByIndex(pinnedIndex: number) {
|
||||
return 0x7fffff00 + (pinnedIndex & 0xff);
|
||||
return 0x7fff0000 + (pinnedIndex & 0xFFFF); // 0xFFFF - потому что в папках может быть бесконечное число пиннедов
|
||||
}
|
||||
|
||||
public generateDialogPinnedDate(dialog?: Dialog) {
|
||||
const folderID = dialog.folder_id;
|
||||
let pinnedIndex: number;
|
||||
|
||||
if(dialog) {
|
||||
if(dialog.hasOwnProperty('pinnedIndex')) {
|
||||
pinnedIndex = dialog.pinnedIndex;
|
||||
} else {
|
||||
dialog.pinnedIndex = pinnedIndex = this.pinnedIndexes[folderID]++;
|
||||
}
|
||||
} else {
|
||||
pinnedIndex = this.pinnedIndexes[folderID]++;
|
||||
}
|
||||
public generateDialogPinnedDate(dialog: Dialog) {
|
||||
const order = this.pinnedOrders[dialog.folder_id];
|
||||
|
||||
if(pinnedIndex > this.pinnedIndexes[folderID]) {
|
||||
this.pinnedIndexes[folderID] = pinnedIndex;
|
||||
}
|
||||
const foundIndex = order.indexOf(dialog.peerID);
|
||||
const pinnedIndex = foundIndex === -1 ? order.push(dialog.peerID) - 1 : foundIndex;
|
||||
|
||||
return this.generateDialogPinnedDateByIndex(pinnedIndex);
|
||||
}
|
||||
@ -268,7 +255,7 @@ export type DialogFilter = {
|
||||
include_peers: number[],
|
||||
exclude_peers: number[]
|
||||
};
|
||||
class FiltersStorage {
|
||||
export class FiltersStorage {
|
||||
public filters: {[filterID: string]: DialogFilter} = {};
|
||||
|
||||
constructor() {
|
||||
@ -465,11 +452,18 @@ class FiltersStorage {
|
||||
}
|
||||
|
||||
public getOutputDialogFilter(filter: DialogFilter) {
|
||||
const c = copy(filter);
|
||||
const c: DialogFilter = copy(filter);
|
||||
['pinned_peers', 'exclude_peers', 'include_peers'].forEach(key => {
|
||||
// @ts-ignore
|
||||
c[key] = c[key].map((peerID: number) => appPeersManager.getInputPeerByID(peerID));
|
||||
});
|
||||
|
||||
c.include_peers.forEachReverse((peerID, idx) => {
|
||||
if(c.pinned_peers.includes(peerID)) {
|
||||
c.include_peers.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
@ -500,6 +494,14 @@ class FiltersStorage {
|
||||
filter[key] = filter[key].map((peer: any) => appPeersManager.getPeerID(peer));
|
||||
});
|
||||
|
||||
filter.include_peers.forEachReverse((peerID, idx) => {
|
||||
if(filter.pinned_peers.includes(peerID)) {
|
||||
filter.include_peers.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
filter.include_peers = filter.pinned_peers.concat(filter.include_peers);
|
||||
|
||||
/* if(this.filters[filter.id]) {
|
||||
// ну давай же найдём различия теперь, раз они сами не хотят приходить
|
||||
const oldFilter = this.filters[filter.id];
|
||||
@ -578,7 +580,8 @@ export class AppMessagesManager {
|
||||
public newDialogsToHandle: {[peerID: string]: {reload: true} | Dialog} = {};
|
||||
public newUpdatesAfterReloadToHandle: any = {};
|
||||
|
||||
public loaded: Promise<any> = null;
|
||||
private reloadConversationsPromise: Promise<void>;
|
||||
private reloadConversationsPeers: number[] = [];
|
||||
|
||||
private dialogsIndex = searchIndexManager.createIndex();
|
||||
private cachedResults: {
|
||||
@ -652,156 +655,7 @@ export class AppMessagesManager {
|
||||
});
|
||||
}
|
||||
|
||||
public loadSavedState() {
|
||||
if(this.loaded) return this.loaded;
|
||||
return this.loaded = new Promise((resolve, reject) => {
|
||||
AppStorage.get<{
|
||||
dialogs: Dialog[],
|
||||
allDialogsLoaded: DialogsStorage['allDialogsLoaded'],
|
||||
peers: any[],
|
||||
messages: any[],
|
||||
contactsList: number[],
|
||||
updates: any,
|
||||
filters: FiltersStorage['filters'],
|
||||
maxSeenMsgID: number
|
||||
}>('state').then(({dialogs, allDialogsLoaded, peers, messages, contactsList, maxSeenMsgID, updates, filters}) => {
|
||||
this.log('state res', dialogs, messages);
|
||||
|
||||
if(maxSeenMsgID && !appMessagesIDsManager.getMessageIDInfo(maxSeenMsgID)[1]) {
|
||||
this.maxSeenID = maxSeenMsgID;
|
||||
}
|
||||
|
||||
//return resolve();
|
||||
|
||||
if(peers) {
|
||||
for(let peerID in peers) {
|
||||
let peer = peers[peerID];
|
||||
if(+peerID < 0) appChatsManager.saveApiChat(peer);
|
||||
else appUsersManager.saveApiUser(peer);
|
||||
}
|
||||
}
|
||||
|
||||
if(contactsList && Array.isArray(contactsList) && contactsList.length) {
|
||||
contactsList.forEach(userID => {
|
||||
appUsersManager.pushContact(userID);
|
||||
});
|
||||
appUsersManager.contactsFillPromise = Promise.resolve(appUsersManager.contactsList);
|
||||
}
|
||||
|
||||
if(messages) {
|
||||
/* let tempID = this.tempID;
|
||||
|
||||
for(let message of messages) {
|
||||
if(message.id < tempID) {
|
||||
tempID = message.id;
|
||||
}
|
||||
}
|
||||
|
||||
if(tempID != this.tempID) {
|
||||
this.log('Set tempID to:', tempID);
|
||||
this.tempID = tempID;
|
||||
} */
|
||||
|
||||
this.saveMessages(messages);
|
||||
|
||||
// FIX FILE_REFERENCE_EXPIRED KOSTIL'1999
|
||||
for(let message of messages) {
|
||||
if(message.media) {
|
||||
this.wrapSingleMessage(message.mid, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(allDialogsLoaded) {
|
||||
this.dialogsStorage.allDialogsLoaded = allDialogsLoaded;
|
||||
}
|
||||
|
||||
if(filters) {
|
||||
this.filtersStorage.filters = filters;
|
||||
}
|
||||
|
||||
if(dialogs) {
|
||||
dialogs.forEachReverse(dialog => {
|
||||
// @ts-ignore
|
||||
//dialog.refetchTopMessage = true;
|
||||
this.saveConversation(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
apiUpdatesManager.attach(updates ?? null);
|
||||
|
||||
resolve();
|
||||
}).catch(resolve).finally(() => {
|
||||
setInterval(() => this.saveState(), 10000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public saveState() {
|
||||
const messages: any[] = [];
|
||||
const dialogs: Dialog[] = [];
|
||||
const peers: {[peerID: number]: any} = {};
|
||||
|
||||
for(const peerID in this.dialogsStorage.dialogs) {
|
||||
let dialog = this.dialogsStorage.dialogs[peerID];
|
||||
const historyStorage = this.historiesStorage[dialog.peerID];
|
||||
const history = [].concat(historyStorage?.pending ?? [], historyStorage?.history ?? []);
|
||||
|
||||
dialog = copy(dialog);
|
||||
let removeUnread = 0;
|
||||
for(const mid of history) {
|
||||
const message = this.getMessage(mid);
|
||||
if(/* message._ != 'messageEmpty' && */message.id > 0) {
|
||||
messages.push(message);
|
||||
|
||||
if(message.fromID != dialog.peerID) {
|
||||
peers[message.fromID] = appPeersManager.getPeer(message.fromID);
|
||||
}
|
||||
|
||||
dialog.top_message = message.mid;
|
||||
|
||||
break;
|
||||
} else if(message.pFlags && message.pFlags.unread) {
|
||||
++removeUnread;
|
||||
}
|
||||
}
|
||||
|
||||
if(removeUnread && dialog.unread_count) dialog.unread_count -= removeUnread;
|
||||
|
||||
dialogs.push(dialog);
|
||||
|
||||
peers[dialog.peerID] = appPeersManager.getPeer(dialog.peerID);
|
||||
}
|
||||
|
||||
const us = apiUpdatesManager.updatesState;
|
||||
const updates = {
|
||||
seq: us.seq,
|
||||
pts: us.pts,
|
||||
date: us.date
|
||||
};
|
||||
|
||||
const contactsList = [...appUsersManager.contactsList];
|
||||
for(const userID of contactsList) {
|
||||
if(!peers[userID]) {
|
||||
peers[userID] = appUsersManager.getUser(userID);
|
||||
}
|
||||
}
|
||||
|
||||
const filters = this.filtersStorage.filters;
|
||||
|
||||
AppStorage.set({
|
||||
state: {
|
||||
dialogs,
|
||||
messages,
|
||||
allDialogsLoaded: this.dialogsStorage.allDialogsLoaded,
|
||||
peers,
|
||||
contactsList,
|
||||
filters,
|
||||
updates,
|
||||
maxSeenMsgID: this.maxSeenID
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getInputEntities(entities: any) {
|
||||
var sendEntites = copy(entities);
|
||||
@ -2232,13 +2086,27 @@ export class AppMessagesManager {
|
||||
}
|
||||
|
||||
public reloadConversation(peerID: number | number[]) {
|
||||
let peers = [].concat(peerID).map(peerID => appPeersManager.getInputPeerByID(peerID));
|
||||
[].concat(peerID).forEach(peerID => {
|
||||
if(!this.reloadConversationsPeers.includes(peerID)) {
|
||||
this.reloadConversationsPeers.push(peerID);
|
||||
this.log('will reloadConversation', peerID);
|
||||
}
|
||||
});
|
||||
|
||||
this.log('will reloadConversation', peerID);
|
||||
if(this.reloadConversationsPromise) return this.reloadConversationsPromise;
|
||||
return this.reloadConversationsPromise = new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
let peers = this.reloadConversationsPeers.map(peerID => appPeersManager.getInputPeerByID(peerID));
|
||||
this.reloadConversationsPeers.length = 0;
|
||||
|
||||
return apiManager.invokeApi('messages.getPeerDialogs', {
|
||||
peers: peers
|
||||
}).then(this.applyConversations.bind(this));
|
||||
apiManager.invokeApi('messages.getPeerDialogs', {peers}).then((result) => {
|
||||
this.applyConversations(result);
|
||||
resolve();
|
||||
}, reject).finally(() => {
|
||||
this.reloadConversationsPromise = null;
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
private doFlushHistory(inputPeer: any, justClear: boolean): Promise<true> {
|
||||
@ -2819,7 +2687,6 @@ export class AppMessagesManager {
|
||||
if(wasDialogBefore && wasDialogBefore.pFlags && wasDialogBefore.pFlags.pinned) {
|
||||
if(!dialog.pFlags) dialog.pFlags = {};
|
||||
dialog.pFlags.pinned = true;
|
||||
dialog.pinnedIndex = wasDialogBefore.pinnedIndex;
|
||||
}
|
||||
|
||||
this.saveConversation(dialog);
|
||||
@ -3635,8 +3502,8 @@ export class AppMessagesManager {
|
||||
this.newDialogsToHandle[peerID] = dialog;
|
||||
|
||||
if(dialog.pFlags?.pinned) {
|
||||
delete dialog.pinnedIndex;
|
||||
delete dialog.pFlags.pinned;
|
||||
this.dialogsStorage.pinnedOrders[folder_id].findAndSplice(p => p == dialog.peerID);
|
||||
}
|
||||
|
||||
dialog.folder_id = folder_id;
|
||||
@ -3649,6 +3516,7 @@ export class AppMessagesManager {
|
||||
}
|
||||
|
||||
case 'updateDialogPinned': {
|
||||
const folderID = update.folder_id ?? 0;
|
||||
this.log('updateDialogPinned', update);
|
||||
const peerID = appPeersManager.getPeerID(update.peer.peer);
|
||||
const foundDialog = this.getDialogByPeerID(peerID);
|
||||
@ -3672,7 +3540,7 @@ export class AppMessagesManager {
|
||||
|
||||
if(!update.pFlags.pinned) {
|
||||
delete dialog.pFlags.pinned;
|
||||
delete dialog.pinnedIndex;
|
||||
this.dialogsStorage.pinnedOrders[folderID].findAndSplice(p => p == dialog.peerID);
|
||||
} else { // means set
|
||||
dialog.pFlags.pinned = true;
|
||||
}
|
||||
@ -3713,7 +3581,7 @@ export class AppMessagesManager {
|
||||
|
||||
//this.log('before order:', this.dialogsStorage[0].map(d => d.peerID));
|
||||
|
||||
this.dialogsStorage.pinnedIndexes[folderID] = 0;
|
||||
this.dialogsStorage.pinnedOrders[folderID].length = 0;
|
||||
let willHandle = false;
|
||||
update.order.reverse(); // index must be higher
|
||||
update.order.forEach((peer: any) => {
|
||||
@ -3728,7 +3596,6 @@ export class AppMessagesManager {
|
||||
}
|
||||
|
||||
const dialog = foundDialog[0];
|
||||
delete dialog.pinnedIndex;
|
||||
dialog.pFlags.pinned = true;
|
||||
this.dialogsStorage.generateIndexForDialog(dialog);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
//import { logger } from "../polyfill";
|
||||
import appDialogsManager, { AppArchivedTab, archivedTab } from "./appDialogsManager";
|
||||
import { $rootScope } from "../utils";
|
||||
import { $rootScope, findUpTag, findUpClassName } from "../utils";
|
||||
import appImManager from "./appImManager";
|
||||
import AppSearch, { SearchGroup } from "../../components/appSearch";
|
||||
import { parseMenuButtonsTo } from "../../components/misc";
|
||||
@ -19,6 +19,8 @@ import AppEditFolderTab from "../../components/sidebarLeft/editFolder";
|
||||
import AppIncludedChatsTab from "../../components/sidebarLeft/includedChats";
|
||||
import SidebarSlider from "../../components/slider";
|
||||
import SearchInput from "../../components/searchInput";
|
||||
import appStateManager from "./appStateManager";
|
||||
import appChatsManager from "./appChatsManager";
|
||||
|
||||
AvatarElement;
|
||||
|
||||
@ -92,6 +94,11 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
};
|
||||
private globalSearch: AppSearch;
|
||||
|
||||
// peerIDs
|
||||
private recentSearch: number[] = [];
|
||||
private recentSearchLoaded = false;
|
||||
private recentSearchClearBtn: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super(document.getElementById('column-left') as HTMLDivElement, {
|
||||
[AppSidebarLeft.SLIDERITEMSIDS.archived]: archivedTab,
|
||||
@ -127,7 +134,41 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
this.menuEl = this.toolsBtn.querySelector('.btn-menu');
|
||||
this.newBtnMenu = this.sidebarEl.querySelector('#new-menu');
|
||||
|
||||
this.globalSearch = new AppSearch(this.searchContainer, this.searchInput, this.searchGroups);
|
||||
this.globalSearch = new AppSearch(this.searchContainer, this.searchInput, this.searchGroups, (count) => {
|
||||
if(!count && !this.searchInput.value.trim()) {
|
||||
this.globalSearch.reset();
|
||||
this.searchGroups.people.setActive();
|
||||
this.renderRecentSearch();
|
||||
}
|
||||
});
|
||||
this.searchContainer.addEventListener('click', (e) => {
|
||||
const target = findUpTag(e.target, 'LI') as HTMLElement;
|
||||
if(!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchGroup = findUpClassName(target, 'search-group');
|
||||
if(!searchGroup || searchGroup.classList.contains('search-group-recent') || searchGroup.classList.contains('search-group-people')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerID = +target.getAttribute('data-peerID');
|
||||
if(this.recentSearch[0] != peerID) {
|
||||
this.recentSearch.findAndSplice(p => p == peerID);
|
||||
this.recentSearch.unshift(peerID);
|
||||
if(this.recentSearch.length > 20) {
|
||||
this.recentSearch.length = 20;
|
||||
}
|
||||
|
||||
this.renderRecentSearch();
|
||||
appStateManager.pushToState('recentSearch', this.recentSearch);
|
||||
for(const peerID of this.recentSearch) {
|
||||
appStateManager.pushPeer(peerID);
|
||||
}
|
||||
|
||||
clearRecentSearchBtn.style.display = '';
|
||||
}
|
||||
}, {capture: true});
|
||||
|
||||
let peopleContainer = document.createElement('div');
|
||||
peopleContainer.classList.add('search-group-scrollable');
|
||||
@ -160,6 +201,7 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.settings);
|
||||
});
|
||||
|
||||
let firstTime = true;
|
||||
this.searchInput.input.addEventListener('focus', (e) => {
|
||||
this.toolsBtn.classList.remove('active');
|
||||
this.backBtn.classList.add('active');
|
||||
@ -167,6 +209,12 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
void this.searchContainer.offsetWidth; // reflow
|
||||
this.searchContainer.classList.add('active');
|
||||
|
||||
if(firstTime) {
|
||||
this.searchGroups.people.setActive();
|
||||
this.renderRecentSearch();
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
/* this.searchInput.addEventListener('blur', (e) => {
|
||||
if(!this.searchInput.value) {
|
||||
this.toolsBtn.classList.add('active');
|
||||
@ -181,13 +229,11 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
this.toolsBtn.classList.add('active');
|
||||
this.backBtn.classList.remove('active');
|
||||
this.searchContainer.classList.remove('active');
|
||||
firstTime = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.searchContainer.classList.add('hide');
|
||||
this.globalSearch.reset();
|
||||
|
||||
this.searchGroups.people.setActive();
|
||||
//this.searchGroups.recent.setActive();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
@ -207,25 +253,45 @@ export class AppSidebarLeft extends SidebarSlider {
|
||||
this.archivedCount.innerText = '' + e.detail.count;
|
||||
});
|
||||
|
||||
appUsersManager.getTopPeers().then(categories => {
|
||||
appUsersManager.getTopPeers().then(peers => {
|
||||
//console.log('got top categories:', categories);
|
||||
|
||||
let category = categories[0];
|
||||
if(!category || !category.peers) {
|
||||
return;
|
||||
}
|
||||
|
||||
category.peers.forEach((topPeer: {
|
||||
_: 'topPeer',
|
||||
peer: any,
|
||||
rating: number
|
||||
}) => {
|
||||
let peerID = appPeersManager.getPeerID(topPeer.peer);
|
||||
peers.forEach((peerID) => {
|
||||
let {dialog, dom} = appDialogsManager.addDialog(peerID, this.searchGroups.people.list, false, true, true);
|
||||
|
||||
|
||||
this.searchGroups.people.setActive();
|
||||
});
|
||||
});
|
||||
|
||||
this.renderRecentSearch();
|
||||
const clearRecentSearchBtn = this.recentSearchClearBtn = document.createElement('button');
|
||||
clearRecentSearchBtn.classList.add('btn-icon', 'tgico-close');
|
||||
this.searchGroups.recent.nameEl.append(clearRecentSearchBtn);
|
||||
clearRecentSearchBtn.addEventListener('click', () => {
|
||||
this.recentSearch = [];
|
||||
appStateManager.pushToState('recentSearch', this.recentSearch);
|
||||
this.renderRecentSearch();
|
||||
clearRecentSearchBtn.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
public renderRecentSearch() {
|
||||
appStateManager.getState().then(state => {
|
||||
if(state && !this.recentSearchLoaded && Array.isArray(state.recentSearch)) {
|
||||
this.recentSearch = state.recentSearch;
|
||||
this.recentSearchLoaded = true;
|
||||
}
|
||||
|
||||
this.searchGroups.recent.list.innerHTML = '';
|
||||
this.recentSearchClearBtn.style.display = this.recentSearch.length ? '' : 'none';
|
||||
|
||||
this.recentSearch.slice(0, 20).forEach(peerID => {
|
||||
let {dialog, dom} = appDialogsManager.addDialog(peerID, this.searchGroups.recent.list, false, true, false, true);
|
||||
|
||||
dom.lastMessageSpan.innerText = peerID > 0 ? appUsersManager.getUserStatusString(peerID) : appChatsManager.getChatMembersString(peerID);
|
||||
});
|
||||
|
||||
this.searchGroups.recent.setActive();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -420,8 +420,8 @@ export class AppSidebarRight extends SidebarSlider {
|
||||
private loadedAllMedia: {[type: string]: boolean} = {};
|
||||
|
||||
public sharedMediaTypes = [
|
||||
'members',
|
||||
//'inputMessagesFilterContacts',
|
||||
//'members',
|
||||
'inputMessagesFilterContacts',
|
||||
'inputMessagesFilterPhotoVideo',
|
||||
'inputMessagesFilterDocument',
|
||||
'inputMessagesFilterUrl',
|
||||
@ -1145,9 +1145,9 @@ export class AppSidebarRight extends SidebarSlider {
|
||||
});
|
||||
}
|
||||
|
||||
let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement;
|
||||
//let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement;
|
||||
if(peerID > 0) {
|
||||
membersLi.style.display = 'none';
|
||||
//membersLi.style.display = 'none';
|
||||
|
||||
let user = appUsersManager.getUser(peerID);
|
||||
if(user.phone && peerID != $rootScope.myID) {
|
||||
@ -1167,7 +1167,7 @@ export class AppSidebarRight extends SidebarSlider {
|
||||
//this.log('userFull', userFull);
|
||||
});
|
||||
} else {
|
||||
membersLi.style.display = appPeersManager.isBroadcast(peerID) ? 'none' : '';
|
||||
//membersLi.style.display = appPeersManager.isBroadcast(peerID) ? 'none' : '';
|
||||
let chat = appPeersManager.getPeer(peerID);
|
||||
|
||||
appProfileManager.getChatFull(chat.id).then((chatFull: any) => {
|
||||
|
195
src/lib/appManagers/appStateManager.ts
Normal file
195
src/lib/appManagers/appStateManager.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import AppStorage from '../storage';
|
||||
import appMessagesManager, { Dialog, DialogsStorage, FiltersStorage } from './appMessagesManager';
|
||||
import appMessagesIDsManager from './appMessagesIDsManager';
|
||||
import appPeersManager from './appPeersManager';
|
||||
import appChatsManager from './appChatsManager';
|
||||
import appUsersManager from './appUsersManager';
|
||||
import apiUpdatesManager from './apiUpdatesManager';
|
||||
import { copy } from '../utils';
|
||||
import { logger } from '../polyfill';
|
||||
|
||||
export class AppStateManager {
|
||||
public loaded: Promise<any>;
|
||||
private log = logger('STATE'/* , LogLevels.error */);
|
||||
|
||||
private state: any = {};
|
||||
private peers: {[peerID: number]: any} = {};
|
||||
|
||||
constructor() {
|
||||
this.loadSavedState();
|
||||
}
|
||||
|
||||
public loadSavedState() {
|
||||
if(this.loaded) return this.loaded;
|
||||
return this.loaded = new Promise((resolve, reject) => {
|
||||
AppStorage.get<{
|
||||
dialogs: Dialog[],
|
||||
allDialogsLoaded: DialogsStorage['allDialogsLoaded'],
|
||||
peers: any[],
|
||||
messages: any[],
|
||||
contactsList: number[],
|
||||
updates: any,
|
||||
filters: FiltersStorage['filters'],
|
||||
maxSeenMsgID: number
|
||||
}>('state').then((state) => {
|
||||
const {dialogs, allDialogsLoaded, peers, messages, contactsList, maxSeenMsgID, updates, filters} = state;
|
||||
this.state = state ?? {};
|
||||
this.log('state res', dialogs, messages);
|
||||
|
||||
if(maxSeenMsgID && !appMessagesIDsManager.getMessageIDInfo(maxSeenMsgID)[1]) {
|
||||
appMessagesManager.maxSeenID = maxSeenMsgID;
|
||||
}
|
||||
|
||||
//return resolve();
|
||||
|
||||
if(peers) {
|
||||
for(let peerID in peers) {
|
||||
let peer = peers[peerID];
|
||||
if(+peerID < 0) appChatsManager.saveApiChat(peer);
|
||||
else appUsersManager.saveApiUser(peer);
|
||||
}
|
||||
}
|
||||
|
||||
if(contactsList && Array.isArray(contactsList) && contactsList.length) {
|
||||
contactsList.forEach(userID => {
|
||||
appUsersManager.pushContact(userID);
|
||||
});
|
||||
appUsersManager.contactsFillPromise = Promise.resolve(appUsersManager.contactsList);
|
||||
}
|
||||
|
||||
if(messages) {
|
||||
/* let tempID = this.tempID;
|
||||
|
||||
for(let message of messages) {
|
||||
if(message.id < tempID) {
|
||||
tempID = message.id;
|
||||
}
|
||||
}
|
||||
|
||||
if(tempID != this.tempID) {
|
||||
this.log('Set tempID to:', tempID);
|
||||
this.tempID = tempID;
|
||||
} */
|
||||
|
||||
appMessagesManager.saveMessages(messages);
|
||||
|
||||
// FIX FILE_REFERENCE_EXPIRED KOSTIL'1999
|
||||
for(let message of messages) {
|
||||
if(message.media) {
|
||||
appMessagesManager.wrapSingleMessage(message.mid, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(allDialogsLoaded) {
|
||||
appMessagesManager.dialogsStorage.allDialogsLoaded = allDialogsLoaded;
|
||||
}
|
||||
|
||||
if(filters) {
|
||||
for(const filterID in filters) {
|
||||
appMessagesManager.filtersStorage.saveDialogFilter(filters[filterID], false);
|
||||
}
|
||||
}
|
||||
|
||||
if(dialogs) {
|
||||
dialogs.forEachReverse(dialog => {
|
||||
appMessagesManager.saveConversation(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
apiUpdatesManager.attach(updates ?? null);
|
||||
|
||||
resolve(state);
|
||||
}).catch(resolve).finally(() => {
|
||||
setInterval(() => this.saveState(), 10000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this.loadSavedState();
|
||||
}
|
||||
|
||||
public saveState() {
|
||||
const messages: any[] = [];
|
||||
const dialogs: Dialog[] = [];
|
||||
const peers: {[peerID: number]: any} = this.peers;
|
||||
|
||||
for(const folderID in appMessagesManager.dialogsStorage.byFolders) {
|
||||
const folder = appMessagesManager.dialogsStorage.getFolder(+folderID);
|
||||
|
||||
for(let dialog of folder) {
|
||||
const historyStorage = appMessagesManager.historiesStorage[dialog.peerID];
|
||||
const history = [].concat(historyStorage?.pending ?? [], historyStorage?.history ?? []);
|
||||
|
||||
dialog = copy(dialog);
|
||||
let removeUnread = 0;
|
||||
for(const mid of history) {
|
||||
const message = appMessagesManager.getMessage(mid);
|
||||
if(/* message._ != 'messageEmpty' && */message.id > 0) {
|
||||
messages.push(message);
|
||||
|
||||
if(message.fromID != dialog.peerID) {
|
||||
peers[message.fromID] = appPeersManager.getPeer(message.fromID);
|
||||
}
|
||||
|
||||
dialog.top_message = message.mid;
|
||||
|
||||
break;
|
||||
} else if(message.pFlags && message.pFlags.unread) {
|
||||
++removeUnread;
|
||||
}
|
||||
}
|
||||
|
||||
if(removeUnread && dialog.unread_count) dialog.unread_count -= removeUnread;
|
||||
|
||||
dialogs.push(dialog);
|
||||
|
||||
peers[dialog.peerID] = appPeersManager.getPeer(dialog.peerID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const us = apiUpdatesManager.updatesState;
|
||||
const updates = {
|
||||
seq: us.seq,
|
||||
pts: us.pts,
|
||||
date: us.date
|
||||
};
|
||||
|
||||
const contactsList = [...appUsersManager.contactsList];
|
||||
for(const userID of contactsList) {
|
||||
if(!peers[userID]) {
|
||||
peers[userID] = appUsersManager.getUser(userID);
|
||||
}
|
||||
}
|
||||
|
||||
const filters = appMessagesManager.filtersStorage.filters;
|
||||
//const pinnedOrders = appMessagesManager.dialogsStorage.pinnedOrders;
|
||||
|
||||
AppStorage.set({
|
||||
state: Object.assign({}, this.state, {
|
||||
dialogs,
|
||||
messages,
|
||||
allDialogsLoaded: appMessagesManager.dialogsStorage.allDialogsLoaded,
|
||||
peers,
|
||||
contactsList,
|
||||
filters,
|
||||
//pinnedOrders,
|
||||
updates,
|
||||
maxSeenMsgID: appMessagesManager.maxSeenID
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public pushToState(key: string, value: any) {
|
||||
this.state[key] = value;
|
||||
}
|
||||
|
||||
public pushPeer(peerID: number) {
|
||||
this.peers[peerID] = appPeersManager.getPeer(peerID);
|
||||
}
|
||||
}
|
||||
|
||||
const appStateManager = new AppStateManager();
|
||||
export default appStateManager;
|
@ -6,6 +6,8 @@ import apiManager from '../mtproto/mtprotoworker';
|
||||
import serverTimeManager from "../mtproto/serverTimeManager";
|
||||
import { formatPhoneNumber } from "../../components/misc";
|
||||
import searchIndexManager from "../searchIndexManager";
|
||||
import appPeersManager from "./appPeersManager";
|
||||
import appStateManager from "./appStateManager";
|
||||
|
||||
export type User = {
|
||||
_: 'user',
|
||||
@ -45,6 +47,8 @@ export class AppUsersManager {
|
||||
public contactsList: Set<number> = new Set();
|
||||
public myID: number;
|
||||
|
||||
public getPeersPromise: Promise<number[]>;
|
||||
|
||||
constructor() {
|
||||
apiManager.getUserID().then((id) => {
|
||||
this.myID = id;
|
||||
@ -510,19 +514,39 @@ export class AppUsersManager {
|
||||
});
|
||||
}
|
||||
|
||||
public getTopPeers() {
|
||||
return apiManager.invokeApi('contacts.getTopPeers', {
|
||||
flags: 1,
|
||||
correspondents: true,
|
||||
offset: 0,
|
||||
limit: 30,
|
||||
hash: 0,
|
||||
}).then((peers: any) => {
|
||||
//console.log(peers);
|
||||
this.saveApiUsers(peers.users);
|
||||
appChatsManager.saveApiChats(peers.chats);
|
||||
public getTopPeers(): Promise<number[]> {
|
||||
if(this.getPeersPromise) return this.getPeersPromise;
|
||||
|
||||
return peers.categories;
|
||||
return this.getPeersPromise = appStateManager.getState().then((state) => {
|
||||
if(state?.topPeers?.length) {
|
||||
return state.topPeers;
|
||||
}
|
||||
|
||||
return apiManager.invokeApi('contacts.getTopPeers', {
|
||||
flags: 1,
|
||||
correspondents: true,
|
||||
offset: 0,
|
||||
limit: 30,
|
||||
hash: 0,
|
||||
}).then((result: any) => {
|
||||
//console.log(result);
|
||||
this.saveApiUsers(result.users);
|
||||
appChatsManager.saveApiChats(result.chats);
|
||||
|
||||
const peerIDs = result.categories[0].peers.map((topPeer: {
|
||||
_: 'topPeer',
|
||||
peer: any,
|
||||
rating: number
|
||||
}) => {
|
||||
const peerID = appPeersManager.getPeerID(topPeer.peer);
|
||||
appStateManager.pushPeer(peerID);
|
||||
return peerID;
|
||||
});
|
||||
|
||||
appStateManager.pushToState('topPeers', peerIDs);
|
||||
|
||||
return peerIDs;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -15,7 +15,8 @@ type RLottieOptions = {
|
||||
loop?: boolean,
|
||||
width?: number,
|
||||
height?: number,
|
||||
group?: string
|
||||
group?: string,
|
||||
noCache?: true
|
||||
};
|
||||
|
||||
export class RLottiePlayer {
|
||||
@ -96,13 +97,15 @@ export class RLottiePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
// проверка на размер уже после скейлинга, сделано для попапа и сайдбарfа, где стикеры 80х80 и 68х68, туда нужно 75%
|
||||
if(isApple && this.width > 100 && this.height > 100) {
|
||||
this.cachingDelta = 2; //2 // 50%
|
||||
} else if(this.width < 100 && this.height < 100) {
|
||||
this.cachingDelta = Infinity; // 100%
|
||||
} else {
|
||||
this.cachingDelta = 4; // 75%
|
||||
if(!options.noCache) {
|
||||
// проверка на размер уже после скейлинга, сделано для попапа и сайдбарfа, где стикеры 80х80 и 68х68, туда нужно 75%
|
||||
if(isApple && this.width > 100 && this.height > 100) {
|
||||
this.cachingDelta = 2; //2 // 50%
|
||||
} else if(this.width < 100 && this.height < 100) {
|
||||
this.cachingDelta = Infinity; // 100%
|
||||
} else {
|
||||
this.cachingDelta = 4; // 75%
|
||||
}
|
||||
}
|
||||
|
||||
// if(isApple) {
|
||||
@ -284,6 +287,10 @@ export class RLottiePlayer {
|
||||
} else if(isSafari) {
|
||||
this.sendQuery('renderFrame', frameNo);
|
||||
} else {
|
||||
if(!this.clamped.length) { // fix detached
|
||||
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
|
||||
}
|
||||
|
||||
this.sendQuery('renderFrame', frameNo, this.clamped);
|
||||
}
|
||||
}
|
||||
@ -471,6 +478,7 @@ class QueryableWorker {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('transfer', transfer);
|
||||
this.worker.postMessage({
|
||||
'queryMethod': queryMethod,
|
||||
'queryMethodArguments': args
|
||||
|
@ -321,6 +321,7 @@ class MTPNetworker {
|
||||
});
|
||||
}
|
||||
|
||||
// тут можно сделать таймаут и выводить дисконнект
|
||||
public pushMessage(message: {
|
||||
msg_id: string,
|
||||
seq_no: number,
|
||||
|
@ -55,7 +55,7 @@ export class Obfuscation {
|
||||
this.encNew = new CTR(encKey, encIv);
|
||||
this.decNew = new CTR(decKey, decIv);
|
||||
|
||||
initPayload.set(intermediatePacketCodec.obfuscateTag, 56);
|
||||
initPayload.set(codec.obfuscateTag, 56);
|
||||
const encrypted = this.encode(initPayload);
|
||||
|
||||
initPayload.set(encrypted.slice(56, 64), 56);
|
||||
@ -132,10 +132,8 @@ export default class Socket extends MTTransport {
|
||||
constructor(dcID: number, url: string) {
|
||||
super(dcID, url);
|
||||
|
||||
this.log = logger(`WS-${dcID}`, LogLevels.log | LogLevels.error);
|
||||
|
||||
this.log = logger(`WS-${dcID}`, LogLevels.log/* | LogLevels.error | LogLevels.debug */);
|
||||
this.log('constructor');
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@ -157,10 +155,14 @@ export default class Socket extends MTTransport {
|
||||
handleOpen = () => {
|
||||
this.log('opened');
|
||||
|
||||
this.log.debug('sending init packet');
|
||||
this.ws.send(this.obfuscation.init(this.codec));
|
||||
this.connected = true;
|
||||
|
||||
this.releasePending();
|
||||
//setTimeout(() => {
|
||||
this.connected = true;
|
||||
|
||||
this.releasePending();
|
||||
//}, 3e3);
|
||||
};
|
||||
|
||||
handleClose = (event: CloseEvent) => {
|
||||
@ -218,6 +220,8 @@ export default class Socket extends MTTransport {
|
||||
send = (body: Uint8Array) => {
|
||||
this.log.debug('-> body length to pending:', body.length);
|
||||
|
||||
//return;
|
||||
|
||||
if(this.networker) {
|
||||
this.pending.push({body});
|
||||
this.releasePending();
|
||||
|
@ -84,13 +84,13 @@ var markdownEntities = {
|
||||
'__': 'messageEntityItalic'
|
||||
}
|
||||
function getEmojiSpritesheetCoords(emojiCode) {
|
||||
let emojiInfo = emojiData[emojiCode.replace(/\ufe0f/g, '')];
|
||||
let emojiInfo = emojiData[emojiCode/* .replace(/\ufe0f/g, '') */];
|
||||
if(emojiInfo === undefined) {
|
||||
//console.error('no emoji by code:', emojiCode, emojiCode && emojiCode.length, new TextEncoder().encode(emojiCode), emojiUnicode(emojiCode));
|
||||
return null;
|
||||
}
|
||||
|
||||
return emojiUnicode(emojiCode);
|
||||
return emojiUnicode(emojiCode).replace(/(-fe0f|fe0f)/g, '');
|
||||
}
|
||||
function parseEntities(text, options = {}) {
|
||||
var match;
|
||||
|
@ -143,9 +143,9 @@ export function getRichElementValue (node, lines, line, selNode, selOffset) {
|
||||
|
||||
export const $rootScope = {
|
||||
$broadcast: (name/* : string */, detail/*? : any */) => {
|
||||
/* if(name != 'user_update') {
|
||||
if(name != 'user_update') {
|
||||
console.log(dT(), 'Broadcasting ' + name + ' event, with args:', detail);
|
||||
} */
|
||||
}
|
||||
|
||||
let myCustomEvent = new CustomEvent(name, {detail});
|
||||
document.dispatchEvent(myCustomEvent);
|
||||
@ -463,7 +463,7 @@ export function calcImageInBox (imageW, imageH, boxW, boxH, noZooom) {
|
||||
* @returns {String} The base 16 unicode code.
|
||||
*/
|
||||
export function emojiUnicode(input) {
|
||||
let pairs = emojiUnicode.raw(input).split(' ').map(val => parseInt(val).toString(16)).filter(p => p != 'fe0f');
|
||||
let pairs = emojiUnicode.raw(input).split(' ').map(val => parseInt(val).toString(16))/* .filter(p => p != 'fe0f') */;
|
||||
if(pairs.length && pairs[0].length == 2) pairs[0] = '00' + pairs[0];
|
||||
return pairs.join('-');
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import LottieLoader, { RLottiePlayer } from '../lib/lottieLoader';
|
||||
import apiManager from '../lib/mtproto/mtprotoworker';
|
||||
import Page from './page';
|
||||
import { App } from '../lib/mtproto/mtproto_config';
|
||||
import { mediaSizes } from '../lib/config';
|
||||
|
||||
let authCode: {
|
||||
_: string, // 'auth.sentCode'
|
||||
@ -235,13 +236,14 @@ let onFirstMount = (): Promise<any> => {
|
||||
});
|
||||
|
||||
let imageDiv = page.pageEl.querySelector('.auth-image') as HTMLDivElement;
|
||||
const size = mediaSizes.isMobile ? 100 : 166;
|
||||
return Promise.all([
|
||||
LottieLoader.loadAnimationFromURL({
|
||||
container: imageDiv,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
width: 166,
|
||||
height: 166
|
||||
width: size,
|
||||
height: size
|
||||
}, 'assets/img/TwoFactorSetupMonkeyIdle.tgs').then(animation => {
|
||||
idleAnimation = animation;
|
||||
}),
|
||||
@ -250,8 +252,8 @@ let onFirstMount = (): Promise<any> => {
|
||||
container: imageDiv,
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
width: 166,
|
||||
height: 166
|
||||
width: size,
|
||||
height: size
|
||||
}, 'assets/img/TwoFactorSetupMonkeyTracking.tgs').then(_animation => {
|
||||
animation = _animation;
|
||||
|
||||
|
@ -7,6 +7,7 @@ import LottieLoader, { RLottiePlayer } from '../lib/lottieLoader';
|
||||
//import passwordManager from '../lib/mtproto/passwordManager';
|
||||
import apiManager from '../lib/mtproto/mtprotoworker';
|
||||
import Page from './page';
|
||||
import { mediaSizes } from '../lib/config';
|
||||
|
||||
let onFirstMount = (): Promise<any> => {
|
||||
let needFrame = 0;
|
||||
@ -30,21 +31,23 @@ let onFirstMount = (): Promise<any> => {
|
||||
};
|
||||
|
||||
toggleVisible.addEventListener('click', function(this, e) {
|
||||
if(!passwordVisible) {
|
||||
passwordVisible = !passwordVisible;
|
||||
|
||||
if(passwordVisible) {
|
||||
this.classList.add('tgico-eye2');
|
||||
passwordInput.setAttribute('type', 'text');
|
||||
animation.setDirection(-1);
|
||||
needFrame = 0;
|
||||
animation.setDirection(1);
|
||||
animation.curFrame = 0;
|
||||
needFrame = 16;
|
||||
animation.play();
|
||||
} else {
|
||||
this.classList.remove('tgico-eye2');
|
||||
passwordInput.setAttribute('type', 'password');
|
||||
animation.setDirection(1);
|
||||
needFrame = 49;
|
||||
animation.setDirection(-1);
|
||||
animation.curFrame = 16;
|
||||
needFrame = 0;
|
||||
animation.play();
|
||||
}
|
||||
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
|
||||
btnNext.addEventListener('click', function(this, e) {
|
||||
@ -90,14 +93,18 @@ let onFirstMount = (): Promise<any> => {
|
||||
/* passwordInput.addEventListener('input', function(this, e) {
|
||||
|
||||
}); */
|
||||
const size = mediaSizes.isMobile ? 100 : 166;
|
||||
return Promise.all([
|
||||
LottieLoader.loadAnimationFromURL({
|
||||
container: page.pageEl.querySelector('.auth-image'),
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
width: 166,
|
||||
height: 166
|
||||
}, 'assets/img/TwoFactorSetupMonkeyClose.tgs').then(_animation => {
|
||||
width: size,
|
||||
height: size,
|
||||
noCache: true
|
||||
//}, 'assets/img/TwoFactorSetupMonkeyClose.tgs').then(_animation => {
|
||||
}, 'assets/img/TwoFactorSetupMonkeyPeek.tgs').then(_animation => {
|
||||
//return;
|
||||
animation = _animation;
|
||||
animation.addListener('enterFrame', currentFrame => {
|
||||
//console.log('enterFrame', e, needFrame);
|
||||
@ -110,7 +117,7 @@ let onFirstMount = (): Promise<any> => {
|
||||
});
|
||||
|
||||
needFrame = 49;
|
||||
animation.play();
|
||||
//animation.play();
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
@ -272,7 +272,7 @@
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
margin-top: 4px;
|
||||
margin-right: -2px;
|
||||
margin-right: -3px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
@ -2,28 +2,53 @@
|
||||
//display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.folders-tabs-scrollable {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
|
||||
.scrollable {
|
||||
position: relative;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.menu-horizontal {
|
||||
border-bottom: none;
|
||||
|
||||
ul {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 9px 16px 7px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
span.unread-count {
|
||||
margin-left: 5px;
|
||||
background: #50a2e9;
|
||||
height: 20px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
line-height: 22px;
|
||||
margin-top: 3px;
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#chats-container {
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.folders-tabs-scrollable {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
.scrollable {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-horizontal {
|
||||
background: #fff;
|
||||
|
||||
ul {
|
||||
justify-content: space-between
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-slider {
|
||||
@ -216,6 +241,11 @@
|
||||
width: 380px;
|
||||
margin: 0 auto;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chats-container {
|
||||
@ -227,6 +257,15 @@
|
||||
margin-top: 14px;
|
||||
margin-left: 23px;
|
||||
color: #707579;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-folder-container {
|
||||
.input-wrapper {
|
||||
width: 380px;
|
||||
margin: 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,6 +329,10 @@
|
||||
margin-left: 1.438rem;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 1.438rem;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-left-h2 {
|
||||
@ -314,7 +357,7 @@
|
||||
.sticker-container {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
margin: 1px auto 32px;
|
||||
margin: 1px auto 29px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@ -328,8 +371,9 @@
|
||||
.sidebar-left-h2 {
|
||||
color: #707579;
|
||||
font-size: 15px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 15px;
|
||||
// padding-top: 7px;
|
||||
// padding-bottom: 15px;
|
||||
padding: 7px 16px 15px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@ -351,22 +395,27 @@
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.folders-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
// .folders-container {
|
||||
// padding: 0 16px;
|
||||
// }
|
||||
|
||||
.category {
|
||||
padding: 7px 0 11px 0;
|
||||
padding: 7px 16px 11px 16px;
|
||||
display: flex;
|
||||
padding-bottom: 11px;
|
||||
//padding-bottom: 11px;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
|
||||
p {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
color: #707579;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@ -386,7 +435,8 @@
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
.input-wrapper {
|
||||
width: 328px;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.input-field input {
|
||||
@ -410,6 +460,10 @@
|
||||
.rp {
|
||||
padding: 8px 3px !important;
|
||||
height: 48px !important;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,6 +483,21 @@
|
||||
|
||||
.folder-categories {
|
||||
width: 100%;
|
||||
|
||||
.checkbox {
|
||||
margin-top: -9px !important;;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
|
||||
[type="checkbox"]+span {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked+span:before {
|
||||
top: 5px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-category-button {
|
||||
@ -440,6 +509,7 @@
|
||||
user-select: none;
|
||||
margin-left: 32px;
|
||||
font-size: 16px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&.blue, &.blue:before {
|
||||
@ -466,31 +536,64 @@
|
||||
font-weight: 500;
|
||||
padding: 6px 0 8px 16px;
|
||||
}
|
||||
.selector {
|
||||
ul {
|
||||
li > .rp {
|
||||
margin: 0 !important;
|
||||
padding: 7px 12px !important;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.dialog-avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
span.user-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-caption {
|
||||
padding: 0px 0px 0 14px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
span.user-last-message {
|
||||
font-size: 15px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
ul {
|
||||
li > .rp {
|
||||
margin: 0 !important;
|
||||
padding: 7px 12px !important;
|
||||
height: 62px;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dialog-avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
span.user-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-caption {
|
||||
padding: 0px 0px 0 14px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
span.user-last-message {
|
||||
font-size: 15px;
|
||||
margin-top: -1px;
|
||||
[type="checkbox"]+span {
|
||||
padding-left: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox [type="checkbox"]+span:after {
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-color: #dadbdc;
|
||||
}
|
||||
|
||||
.checkbox [type="checkbox"]:checked+span:after {
|
||||
background-color: #4EA4F6;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.folder-category-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.search-group-recent {
|
||||
.search-group__name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -294,7 +294,7 @@
|
||||
opacity: 1;
|
||||
transition: opacity .2s ease;
|
||||
|
||||
&.thumbnail {
|
||||
html:not(.is-mac) &.thumbnail {
|
||||
filter: blur(7px);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,10 @@
|
||||
position: relative;
|
||||
max-height: inherit; // fix safari
|
||||
}
|
||||
|
||||
avatar-element:before {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
|
@ -2,12 +2,27 @@
|
||||
max-width: 720px; // 360 + 360 / 2
|
||||
overflow: hidden;
|
||||
|
||||
.btn-primary {
|
||||
@include respond-to(handhelds) {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
@include respond-to(handhelds) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
margin-top: 49px;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
margin-top: 41px;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
@ -50,6 +65,18 @@
|
||||
height: auto;
|
||||
} */
|
||||
}
|
||||
|
||||
.page-password {
|
||||
.input-wrapper {
|
||||
@include respond-to(handhelds) {
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-sign, .page-signUp {
|
||||
@ -85,6 +112,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.page-signQR {
|
||||
.auth-image {
|
||||
@include respond-to(handhelds) {
|
||||
width: 166px;
|
||||
height: 166px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* .page-signQR {
|
||||
.auth-image {
|
||||
position: relative;
|
||||
@ -106,5 +142,16 @@
|
||||
.auth-image {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#signUp {
|
||||
@include respond-to(handhelds) {
|
||||
margin-top: 100px;
|
||||
}
|
||||
}
|
||||
}
|
@ -152,4 +152,10 @@
|
||||
|
||||
.popup-create-poll.popup-new-media .btn-primary {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.popup-new-media.popup-send-photo {
|
||||
.popup-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
@ -202,6 +202,11 @@ h4 {
|
||||
//margin: 1.5rem 0 1rem 0;
|
||||
margin: 22px 0 14px;
|
||||
line-height: 110%;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
font-size: 20px;
|
||||
margin: 2px 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
@ -766,7 +771,7 @@ avatar-element {
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
||||
border: solid #bdbdbd;
|
||||
border: solid #707579;
|
||||
border-radius: 1px;
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
@ -805,6 +810,10 @@ avatar-element {
|
||||
transition: .2s border-color;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
height: 50px;
|
||||
}
|
||||
/* font-weight: 500; */
|
||||
|
||||
/* &:hover {
|
||||
@ -869,9 +878,13 @@ avatar-element {
|
||||
margin: 1.25rem 0;
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 0 19px;
|
||||
padding: 0 18px;
|
||||
/* font-weight: 500; */
|
||||
position: relative;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
margin-bottom: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
[type="checkbox"] {
|
||||
@ -1036,6 +1049,11 @@ input:focus, button:focus {
|
||||
width: 166px;
|
||||
height: 166px;
|
||||
margin: 0 auto 18px;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* .phone-wrapper {
|
||||
@ -1053,6 +1071,10 @@ input:focus, button:focus {
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
html.no-touch &:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user