Handle RTL on search inputs
Debounce pinned message animation Debounce lazy load queue
This commit is contained in:
parent
402ae16d98
commit
285e56f233
@ -6,7 +6,7 @@ import appPeersManager from '../lib/appManagers/appPeersManager';
|
|||||||
import appMessagesManager from "../lib/appManagers/appMessagesManager";
|
import appMessagesManager from "../lib/appManagers/appMessagesManager";
|
||||||
import { formatPhoneNumber } from "./misc";
|
import { formatPhoneNumber } from "./misc";
|
||||||
import appChatsManager from "../lib/appManagers/appChatsManager";
|
import appChatsManager from "../lib/appManagers/appChatsManager";
|
||||||
import SearchInput from "./searchInput";
|
import InputSearch from "./inputSearch";
|
||||||
import rootScope from "../lib/rootScope";
|
import rootScope from "../lib/rootScope";
|
||||||
import { escapeRegExp } from "../helpers/string";
|
import { escapeRegExp } from "../helpers/string";
|
||||||
import searchIndexManager from "../lib/searchIndexManager";
|
import searchIndexManager from "../lib/searchIndexManager";
|
||||||
@ -81,7 +81,7 @@ export default class AppSearch {
|
|||||||
|
|
||||||
private scrollable: Scrollable;
|
private scrollable: Scrollable;
|
||||||
|
|
||||||
constructor(public container: HTMLElement, public searchInput: SearchInput, public searchGroups: {[group in SearchGroupType]: SearchGroup}, public onSearch?: (count: number) => void) {
|
constructor(public container: HTMLElement, public searchInput: InputSearch, public searchGroups: {[group in SearchGroupType]: SearchGroup}, public onSearch?: (count: number) => void) {
|
||||||
this.scrollable = new Scrollable(this.container);
|
this.scrollable = new Scrollable(this.container);
|
||||||
this.listsContainer = this.scrollable.container as HTMLDivElement;
|
this.listsContainer = this.scrollable.container as HTMLDivElement;
|
||||||
for(let i in this.searchGroups) {
|
for(let i in this.searchGroups) {
|
||||||
|
@ -35,6 +35,7 @@ import LazyLoadQueue from "../lazyLoadQueue";
|
|||||||
import { AppChatsManager } from "../../lib/appManagers/appChatsManager";
|
import { AppChatsManager } from "../../lib/appManagers/appChatsManager";
|
||||||
import Chat from "./chat";
|
import Chat from "./chat";
|
||||||
import ListenerSetter from "../../helpers/listenerSetter";
|
import ListenerSetter from "../../helpers/listenerSetter";
|
||||||
|
import { pause } from "../../helpers/schedulers";
|
||||||
|
|
||||||
const IGNORE_ACTIONS = ['messageActionHistoryClear'];
|
const IGNORE_ACTIONS = ['messageActionHistoryClear'];
|
||||||
|
|
||||||
@ -1123,10 +1124,13 @@ export default class ChatBubbles {
|
|||||||
bubble = this.findNextMountedBubbleByMsgId(lastMsgId);
|
bubble = this.findNextMountedBubbleByMsgId(lastMsgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! sometimes there can be no bubble
|
||||||
|
if(bubble) {
|
||||||
this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */);
|
this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */);
|
||||||
if(!forwardingUnread) {
|
if(!forwardingUnread) {
|
||||||
this.highlightBubble(bubble);
|
this.highlightBubble(bubble);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||||
}
|
}
|
||||||
@ -1263,7 +1267,9 @@ export default class ChatBubbles {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
|
//}, 500);
|
||||||
this.messagesQueuePromise = null;
|
this.messagesQueuePromise = null;
|
||||||
}, reject);
|
}, reject);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -11,6 +11,7 @@ import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent
|
|||||||
import Chat from "./chat";
|
import Chat from "./chat";
|
||||||
import ListenerSetter from "../../helpers/listenerSetter";
|
import ListenerSetter from "../../helpers/listenerSetter";
|
||||||
import ButtonIcon from "../buttonIcon";
|
import ButtonIcon from "../buttonIcon";
|
||||||
|
import { debounce } from "../../helpers/schedulers";
|
||||||
|
|
||||||
class AnimatedSuper {
|
class AnimatedSuper {
|
||||||
static DURATION = 200;
|
static DURATION = 200;
|
||||||
@ -94,6 +95,17 @@ class AnimatedSuper {
|
|||||||
row.element.classList.toggle('is-hiding', false);
|
row.element.classList.toggle('is-hiding', false);
|
||||||
previousRow && previousRow.element.classList.toggle('is-hiding', true);
|
previousRow && previousRow.element.classList.toggle('is-hiding', true);
|
||||||
|
|
||||||
|
/* const height = row.element.getBoundingClientRect().height;
|
||||||
|
row.element.style.transform = `translateY(${fromTop ? height * -1 : height}px)`;
|
||||||
|
if(previousRow) {
|
||||||
|
previousRow.element.style.transform = `translateY(${fromTop ? height : height * -1}px)`;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* row.element.style.setProperty('--height', row.element.getBoundingClientRect().height + 'px');
|
||||||
|
if(previousRow) {
|
||||||
|
previousRow.element.style.setProperty('--height', previousRow.element.getBoundingClientRect().height + 'px');
|
||||||
|
} */
|
||||||
|
|
||||||
this.clearRows(index);
|
this.clearRows(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,6 +239,8 @@ export default class ChatPinnedMessage {
|
|||||||
public getCurrentIndexPromise: Promise<any> = null;
|
public getCurrentIndexPromise: Promise<any> = null;
|
||||||
public btnOpen: HTMLButtonElement;
|
public btnOpen: HTMLButtonElement;
|
||||||
|
|
||||||
|
public setPinnedMessage: () => void;
|
||||||
|
|
||||||
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
|
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
|
||||||
this.listenerSetter = new ListenerSetter();
|
this.listenerSetter = new ListenerSetter();
|
||||||
|
|
||||||
@ -290,6 +304,10 @@ export default class ChatPinnedMessage {
|
|||||||
this.pinnedMessageContainer.toggle(this.hidden = true);
|
this.pinnedMessageContainer.toggle(this.hidden = true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// * 200 - no lags
|
||||||
|
// * 100 - need test
|
||||||
|
this.setPinnedMessage = debounce(() => this._setPinnedMessage(), 100, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
@ -300,6 +318,8 @@ export default class ChatPinnedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setCorrectIndex(lastScrollDirection?: number) {
|
public setCorrectIndex(lastScrollDirection?: number) {
|
||||||
|
//return;
|
||||||
|
|
||||||
if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) {
|
if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -315,6 +335,8 @@ export default class ChatPinnedMessage {
|
|||||||
el = findUpClassName(el, 'bubble');
|
el = findUpClassName(el, 'bubble');
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
|
|
||||||
|
//return;
|
||||||
|
|
||||||
const mid = el.dataset.mid;
|
const mid = el.dataset.mid;
|
||||||
if(el && mid !== undefined) {
|
if(el && mid !== undefined) {
|
||||||
this.chat.log('[PM]: setCorrectIndex will test mid:', mid);
|
this.chat.log('[PM]: setCorrectIndex will test mid:', mid);
|
||||||
@ -517,7 +539,7 @@ export default class ChatPinnedMessage {
|
|||||||
/* || (!this.chatAudio.divAndCaption.container.classList.contains('hide') && to == ScreenSize.medium) */);
|
/* || (!this.chatAudio.divAndCaption.container.classList.contains('hide') && to == ScreenSize.medium) */);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPinnedMessage() {
|
public _setPinnedMessage() {
|
||||||
/////this.log('setting pinned message', message);
|
/////this.log('setting pinned message', message);
|
||||||
//return;
|
//return;
|
||||||
/* const promise: Promise<any> = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve();
|
/* const promise: Promise<any> = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve();
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import type ChatTopbar from "./topbar";
|
import type ChatTopbar from "./topbar";
|
||||||
import rootScope from "../../lib/rootScope";
|
|
||||||
import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom";
|
import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom";
|
||||||
import AppSearch, { SearchGroup } from "../appSearch";
|
import AppSearch, { SearchGroup } from "../appSearch";
|
||||||
import PopupDatePicker from "../popupDatepicker";
|
import PopupDatePicker from "../popupDatepicker";
|
||||||
import { ripple } from "../ripple";
|
import { ripple } from "../ripple";
|
||||||
import SearchInput from "../searchInput";
|
import InputSearch from "../inputSearch";
|
||||||
import type Chat from "./chat";
|
import type Chat from "./chat";
|
||||||
|
|
||||||
export default class ChatSearch {
|
export default class ChatSearch {
|
||||||
private element: HTMLElement;
|
private element: HTMLElement;
|
||||||
private backBtn: HTMLElement;
|
private backBtn: HTMLElement;
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
|
|
||||||
private results: HTMLElement;
|
private results: HTMLElement;
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ export default class ChatSearch {
|
|||||||
this.backBtn.addEventListener('click', () => {
|
this.backBtn.addEventListener('click', () => {
|
||||||
this.topbar.container.classList.remove('hide-pinned');
|
this.topbar.container.classList.remove('hide-pinned');
|
||||||
this.element.remove();
|
this.element.remove();
|
||||||
this.searchInput.remove();
|
this.inputSearch.remove();
|
||||||
this.results.remove();
|
this.results.remove();
|
||||||
this.footer.remove();
|
this.footer.remove();
|
||||||
this.footer.removeEventListener('click', this.onFooterClick);
|
this.footer.removeEventListener('click', this.onFooterClick);
|
||||||
@ -50,7 +49,7 @@ export default class ChatSearch {
|
|||||||
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
|
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
|
||||||
}, {once: true});
|
}, {once: true});
|
||||||
|
|
||||||
this.searchInput = new SearchInput('Search');
|
this.inputSearch = new InputSearch('Search');
|
||||||
|
|
||||||
// Results
|
// Results
|
||||||
this.results = document.createElement('div');
|
this.results = document.createElement('div');
|
||||||
@ -59,13 +58,13 @@ export default class ChatSearch {
|
|||||||
this.searchGroup = new SearchGroup('', 'messages', undefined, '', false);
|
this.searchGroup = new SearchGroup('', 'messages', undefined, '', false);
|
||||||
this.searchGroup.list.addEventListener('click', this.onResultsClick);
|
this.searchGroup.list.addEventListener('click', this.onResultsClick);
|
||||||
|
|
||||||
this.appSearch = new AppSearch(this.results, this.searchInput, {
|
this.appSearch = new AppSearch(this.results, this.inputSearch, {
|
||||||
messages: this.searchGroup
|
messages: this.searchGroup
|
||||||
}, (count) => {
|
}, (count) => {
|
||||||
this.foundCount = count;
|
this.foundCount = count;
|
||||||
|
|
||||||
if(!this.foundCount) {
|
if(!this.foundCount) {
|
||||||
this.foundCountEl.innerText = this.searchInput.value ? 'No results' : '';
|
this.foundCountEl.innerText = this.inputSearch.value ? 'No results' : '';
|
||||||
this.results.classList.remove('active');
|
this.results.classList.remove('active');
|
||||||
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
|
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
|
||||||
this.upBtn.setAttribute('disabled', 'true');
|
this.upBtn.setAttribute('disabled', 'true');
|
||||||
@ -113,12 +112,12 @@ export default class ChatSearch {
|
|||||||
this.topbar.container.parentElement.insertBefore(this.footer, chat.input.chatInput);
|
this.topbar.container.parentElement.insertBefore(this.footer, chat.input.chatInput);
|
||||||
|
|
||||||
// Append container
|
// Append container
|
||||||
this.element.append(this.backBtn, this.searchInput.container);
|
this.element.append(this.backBtn, this.inputSearch.container);
|
||||||
|
|
||||||
this.topbar.container.classList.add('hide-pinned');
|
this.topbar.container.classList.add('hide-pinned');
|
||||||
this.topbar.container.parentElement.append(this.element);
|
this.topbar.container.parentElement.append(this.element);
|
||||||
|
|
||||||
this.searchInput.input.focus();
|
this.inputSearch.input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDateClick = (e: MouseEvent) => {
|
onDateClick = (e: MouseEvent) => {
|
||||||
|
@ -13,6 +13,7 @@ import StickyIntersector from "../stickyIntersector";
|
|||||||
import EmojiTab from "./tabs/emoji";
|
import EmojiTab from "./tabs/emoji";
|
||||||
import GifsTab from "./tabs/gifs";
|
import GifsTab from "./tabs/gifs";
|
||||||
import StickersTab from "./tabs/stickers";
|
import StickersTab from "./tabs/stickers";
|
||||||
|
import { pause } from "../../helpers/schedulers";
|
||||||
|
|
||||||
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
|
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
|
||||||
|
|
||||||
@ -211,9 +212,7 @@ export class EmoticonsDropdown {
|
|||||||
appImManager.chat.input.saveScroll();
|
appImManager.chat.input.saveScroll();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
await new Promise((resolve) => {
|
await pause(100);
|
||||||
setTimeout(resolve, 100);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,22 @@ let init = () => {
|
|||||||
init = null;
|
init = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAndSetRTL = (input: HTMLElement) => {
|
||||||
|
//const isEmpty = isInputEmpty(input);
|
||||||
|
//console.log('input', isEmpty);
|
||||||
|
|
||||||
|
//const char = [...getRichValue(input)][0];
|
||||||
|
const char = (input instanceof HTMLInputElement ? input.value : input.innerText)[0];
|
||||||
|
let direction = 'ltr';
|
||||||
|
if(char && checkRTL(char)) {
|
||||||
|
direction = 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('RTL', direction, char);
|
||||||
|
|
||||||
|
input.style.direction = direction;
|
||||||
|
};
|
||||||
|
|
||||||
const InputField = (options: {
|
const InputField = (options: {
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
@ -51,6 +67,7 @@ const InputField = (options: {
|
|||||||
|
|
||||||
const {placeholder, label, maxLength, showLengthOn, name, plainText} = options;
|
const {placeholder, label, maxLength, showLengthOn, name, plainText} = options;
|
||||||
|
|
||||||
|
let input: HTMLElement;
|
||||||
if(!plainText) {
|
if(!plainText) {
|
||||||
if(init) {
|
if(init) {
|
||||||
init();
|
init();
|
||||||
@ -61,21 +78,9 @@ const InputField = (options: {
|
|||||||
${label ? `<label>${label}</label>` : ''}
|
${label ? `<label>${label}</label>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const input = div.firstElementChild as HTMLElement;
|
input = div.firstElementChild as HTMLElement;
|
||||||
const observer = new MutationObserver((mutationsList, observer) => {
|
const observer = new MutationObserver(() => {
|
||||||
//const isEmpty = isInputEmpty(input);
|
checkAndSetRTL(input);
|
||||||
//console.log('input', isEmpty);
|
|
||||||
|
|
||||||
//const char = [...getRichValue(input)][0];
|
|
||||||
const char = input.innerText[0];
|
|
||||||
let direction = 'ltr';
|
|
||||||
if(char && checkRTL(char)) {
|
|
||||||
direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log('RTL', direction, char);
|
|
||||||
|
|
||||||
input.style.direction = direction;
|
|
||||||
|
|
||||||
if(processInput) {
|
if(processInput) {
|
||||||
processInput();
|
processInput();
|
||||||
@ -86,21 +91,23 @@ const InputField = (options: {
|
|||||||
observer.observe(input, {characterData: true, childList: true, subtree: true});
|
observer.observe(input, {characterData: true, childList: true, subtree: true});
|
||||||
} else {
|
} else {
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<input type="text" name="${name}" ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" required="" class="input-field-input">
|
<input type="text" ${name ? `name="${name}"` : ''} ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
|
||||||
${label ? `<label>${label}</label>` : ''}
|
${label ? `<label>${label}</label>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
input = div.firstElementChild as HTMLElement;
|
||||||
|
input.addEventListener('input', () => checkAndSetRTL(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
let processInput: () => void;
|
let processInput: () => void;
|
||||||
if(maxLength) {
|
if(maxLength) {
|
||||||
const input = div.firstElementChild as HTMLInputElement;
|
|
||||||
const labelEl = div.lastElementChild as HTMLLabelElement;
|
const labelEl = div.lastElementChild as HTMLLabelElement;
|
||||||
let showingLength = false;
|
let showingLength = false;
|
||||||
|
|
||||||
processInput = () => {
|
processInput = () => {
|
||||||
const wasError = input.classList.contains('error');
|
const wasError = input.classList.contains('error');
|
||||||
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
|
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
|
||||||
const inputLength = plainText ? input.value.length : [...getRichValue(input)].length;
|
const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input)].length;
|
||||||
const diff = maxLength - inputLength;
|
const diff = maxLength - inputLength;
|
||||||
const isError = diff < 0;
|
const isError = diff < 0;
|
||||||
input.classList.toggle('error', isError);
|
input.classList.toggle('error', isError);
|
||||||
@ -117,7 +124,10 @@ const InputField = (options: {
|
|||||||
input.addEventListener('input', processInput);
|
input.addEventListener('input', processInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {container: div, input: div.firstElementChild as HTMLInputElement};
|
return {
|
||||||
|
container: div,
|
||||||
|
input: div.firstElementChild as HTMLInputElement
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InputField;
|
export default InputField;
|
@ -1,4 +1,7 @@
|
|||||||
export default class SearchInput {
|
//import { getRichValue } from "../helpers/dom";
|
||||||
|
import InputField from "./inputField";
|
||||||
|
|
||||||
|
export default class InputSearch {
|
||||||
public container: HTMLElement;
|
public container: HTMLElement;
|
||||||
public input: HTMLInputElement;
|
public input: HTMLInputElement;
|
||||||
public clearBtn: HTMLElement;
|
public clearBtn: HTMLElement;
|
||||||
@ -8,15 +11,19 @@ export default class SearchInput {
|
|||||||
public onChange: (value: string) => void;
|
public onChange: (value: string) => void;
|
||||||
|
|
||||||
constructor(placeholder: string, onChange?: (value: string) => void) {
|
constructor(placeholder: string, onChange?: (value: string) => void) {
|
||||||
this.container = document.createElement('div');
|
const inputField = InputField({
|
||||||
|
placeholder,
|
||||||
|
plainText: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container = inputField.container;
|
||||||
|
this.container.classList.remove('input-field');
|
||||||
this.container.classList.add('input-search');
|
this.container.classList.add('input-search');
|
||||||
|
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
|
|
||||||
this.input = document.createElement('input');
|
this.input = inputField.input;
|
||||||
this.input.type = 'text';
|
this.input.classList.add('input-search-input');
|
||||||
this.input.placeholder = placeholder;
|
|
||||||
this.input.autocomplete = Math.random().toString(36).substring(7);
|
|
||||||
|
|
||||||
const searchIcon = document.createElement('span');
|
const searchIcon = document.createElement('span');
|
||||||
searchIcon.classList.add('tgico', 'tgico-search');
|
searchIcon.classList.add('tgico', 'tgico-search');
|
||||||
@ -33,7 +40,7 @@ export default class SearchInput {
|
|||||||
onInput = () => {
|
onInput = () => {
|
||||||
if(!this.onChange) return;
|
if(!this.onChange) return;
|
||||||
|
|
||||||
let value = this.input.value;
|
let value = this.value;
|
||||||
|
|
||||||
//this.input.classList.toggle('is-empty', !value.trim());
|
//this.input.classList.toggle('is-empty', !value.trim());
|
||||||
|
|
||||||
@ -53,12 +60,17 @@ export default class SearchInput {
|
|||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
return this.input.value;
|
return this.input.value;
|
||||||
|
//return getRichValue(this.input);
|
||||||
}
|
}
|
||||||
|
|
||||||
set value(value: string) {
|
set value(value: string) {
|
||||||
|
//this.input.innerHTML = value;
|
||||||
this.input.value = value;
|
this.input.value = value;
|
||||||
this.prevValue = value;
|
this.prevValue = value;
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
|
|
||||||
|
const event = new Event('input', {bubbles: true, cancelable: true});
|
||||||
|
this.input.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove() {
|
public remove() {
|
@ -1,3 +1,4 @@
|
|||||||
|
import { debounce } from "../helpers/schedulers";
|
||||||
import { logger, LogLevels } from "../lib/logger";
|
import { logger, LogLevels } from "../lib/logger";
|
||||||
import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector";
|
import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector";
|
||||||
|
|
||||||
@ -22,8 +23,10 @@ export class LazyLoadQueueBase {
|
|||||||
protected unlockResolve: () => void = null;
|
protected unlockResolve: () => void = null;
|
||||||
|
|
||||||
protected log = logger('LL', LogLevels.error);
|
protected log = logger('LL', LogLevels.error);
|
||||||
|
protected processQueue: () => void;
|
||||||
|
|
||||||
constructor(protected parallelLimit = PARALLEL_LIMIT) {
|
constructor(protected parallelLimit = PARALLEL_LIMIT) {
|
||||||
|
this.processQueue = debounce(() => this._processQueue(), 20, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
@ -58,7 +61,7 @@ export class LazyLoadQueueBase {
|
|||||||
this.processQueue();
|
this.processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async processItem(item: LazyLoadElementBase) {
|
protected async processItem(item: LazyLoadElementBase) {
|
||||||
if(this.lockPromise) {
|
if(this.lockPromise) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,7 +99,7 @@ export class LazyLoadQueueBase {
|
|||||||
this.processQueue();
|
this.processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async processQueue(item?: LazyLoadElementBase) {
|
protected _processQueue(item?: LazyLoadElementBase) {
|
||||||
if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return;
|
if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@ -236,9 +239,9 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector {
|
|||||||
if(!inserted) return false;
|
if(!inserted) return false;
|
||||||
|
|
||||||
this.intersector.observe(el.div);
|
this.intersector.observe(el.div);
|
||||||
if(el.wasSeen) {
|
/* if(el.wasSeen) {
|
||||||
this.processQueue(el);
|
this.processQueue(el);
|
||||||
} else if(!el.hasOwnProperty('wasSeen')) {
|
} else */if(!el.hasOwnProperty('wasSeen')) {
|
||||||
el.wasSeen = false;
|
el.wasSeen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +152,8 @@ export default class Scrollable extends ScrollableBase {
|
|||||||
//this.log('onScroll call', this.onScrollMeasure);
|
//this.log('onScroll call', this.onScrollMeasure);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
//return;
|
||||||
|
|
||||||
if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp && !this.onAdditionalScroll)) return;
|
if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp && !this.onAdditionalScroll)) return;
|
||||||
this.onScrollMeasure = window.requestAnimationFrame(() => {
|
this.onScrollMeasure = window.requestAnimationFrame(() => {
|
||||||
this.onScrollMeasure = 0;
|
this.onScrollMeasure = 0;
|
||||||
|
@ -13,7 +13,7 @@ import AppSearch, { SearchGroup } from "../appSearch";
|
|||||||
import "../avatar";
|
import "../avatar";
|
||||||
import { parseMenuButtonsTo } from "../misc";
|
import { parseMenuButtonsTo } from "../misc";
|
||||||
import { ScrollableX } from "../scrollable";
|
import { ScrollableX } from "../scrollable";
|
||||||
import SearchInput from "../searchInput";
|
import InputSearch from "../inputSearch";
|
||||||
import SidebarSlider from "../slider";
|
import SidebarSlider from "../slider";
|
||||||
import { TransitionSlider } from "../transition";
|
import { TransitionSlider } from "../transition";
|
||||||
import AppAddMembersTab from "./tabs/addMembers";
|
import AppAddMembersTab from "./tabs/addMembers";
|
||||||
@ -81,7 +81,7 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
private backBtn: HTMLButtonElement;
|
private backBtn: HTMLButtonElement;
|
||||||
private searchContainer: HTMLDivElement;
|
private searchContainer: HTMLDivElement;
|
||||||
//private searchInput = document.getElementById('global-search') as HTMLInputElement;
|
//private searchInput = document.getElementById('global-search') as HTMLInputElement;
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
|
|
||||||
private menuEl: HTMLElement;
|
private menuEl: HTMLElement;
|
||||||
private buttons: {
|
private buttons: {
|
||||||
@ -140,9 +140,9 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
|
|
||||||
//this._selectTab(0); // make first tab as default
|
//this._selectTab(0); // make first tab as default
|
||||||
|
|
||||||
this.searchInput = new SearchInput('Telegram Search');
|
this.inputSearch = new InputSearch('Telegram Search');
|
||||||
const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header');
|
const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header');
|
||||||
sidebarHeader.append(this.searchInput.container);
|
sidebarHeader.append(this.inputSearch.container);
|
||||||
|
|
||||||
this.toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement;
|
this.toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement;
|
||||||
this.backBtn = this.sidebarEl.querySelector('.sidebar-back-button') as HTMLButtonElement;
|
this.backBtn = this.sidebarEl.querySelector('.sidebar-back-button') as HTMLButtonElement;
|
||||||
@ -161,7 +161,7 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
this.menuEl = this.toolsBtn.querySelector('.btn-menu');
|
this.menuEl = this.toolsBtn.querySelector('.btn-menu');
|
||||||
this.newBtnMenu = this.sidebarEl.querySelector('#new-menu');
|
this.newBtnMenu = this.sidebarEl.querySelector('#new-menu');
|
||||||
|
|
||||||
this.searchInput.input.addEventListener('focus', () => {
|
this.inputSearch.input.addEventListener('focus', () => {
|
||||||
this.searchGroups = {
|
this.searchGroups = {
|
||||||
//saved: new SearchGroup('', 'contacts'),
|
//saved: new SearchGroup('', 'contacts'),
|
||||||
contacts: new SearchGroup('Chats', 'contacts'),
|
contacts: new SearchGroup('Chats', 'contacts'),
|
||||||
@ -171,8 +171,8 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
recent: new SearchGroup('Recent', 'contacts', false, 'search-group-recent')
|
recent: new SearchGroup('Recent', 'contacts', false, 'search-group-recent')
|
||||||
};
|
};
|
||||||
|
|
||||||
this.globalSearch = new AppSearch(this.searchContainer, this.searchInput, this.searchGroups, (count) => {
|
this.globalSearch = new AppSearch(this.searchContainer, this.inputSearch, this.searchGroups, (count) => {
|
||||||
if(!count && !this.searchInput.value.trim()) {
|
if(!count && !this.inputSearch.value.trim()) {
|
||||||
this.globalSearch.reset();
|
this.globalSearch.reset();
|
||||||
this.searchGroups.people.toggle();
|
this.searchGroups.people.toggle();
|
||||||
this.renderRecentSearch();
|
this.renderRecentSearch();
|
||||||
@ -263,7 +263,7 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let firstTime = true;
|
let firstTime = true;
|
||||||
this.searchInput.input.addEventListener('focus', onFocus);
|
this.inputSearch.input.addEventListener('focus', onFocus);
|
||||||
onFocus();
|
onFocus();
|
||||||
|
|
||||||
this.backBtn.addEventListener('click', (e) => {
|
this.backBtn.addEventListener('click', (e) => {
|
||||||
@ -339,7 +339,7 @@ export class AppSidebarLeft extends SidebarSlider {
|
|||||||
this.recentSearchLoaded = true;
|
this.recentSearchLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.searchInput.value.trim()) {
|
if(this.inputSearch.value.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import appUsersManager from "../../../lib/appManagers/appUsersManager";
|
|||||||
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
||||||
import appSidebarLeft, { AppSidebarLeft } from "..";
|
import appSidebarLeft, { AppSidebarLeft } from "..";
|
||||||
import rootScope from "../../../lib/rootScope";
|
import rootScope from "../../../lib/rootScope";
|
||||||
import SearchInput from "../../searchInput";
|
import InputSearch from "../../inputSearch";
|
||||||
|
|
||||||
// TODO: поиск по людям глобальный, если не нашло в контактах никого
|
// TODO: поиск по людям глобальный, если не нашло в контактах никого
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export default class AppContactsTab implements SliderTab {
|
|||||||
private scrollable: Scrollable;
|
private scrollable: Scrollable;
|
||||||
private promise: Promise<void>;
|
private promise: Promise<void>;
|
||||||
|
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.container = document.getElementById('contacts-container');
|
this.container = document.getElementById('contacts-container');
|
||||||
@ -24,12 +24,12 @@ export default class AppContactsTab implements SliderTab {
|
|||||||
appDialogsManager.setListClickListener(this.list);
|
appDialogsManager.setListClickListener(this.list);
|
||||||
this.scrollable = new Scrollable(this.list.parentElement);
|
this.scrollable = new Scrollable(this.list.parentElement);
|
||||||
|
|
||||||
this.searchInput = new SearchInput('Search', (value) => {
|
this.inputSearch = new InputSearch('Search', (value) => {
|
||||||
this.list.innerHTML = '';
|
this.list.innerHTML = '';
|
||||||
this.openContacts(value);
|
this.openContacts(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.container.firstElementChild.append(this.searchInput.container);
|
this.container.firstElementChild.append(this.inputSearch.container);
|
||||||
|
|
||||||
// preload contacts
|
// preload contacts
|
||||||
// appUsersManager.getContacts();
|
// appUsersManager.getContacts();
|
||||||
@ -43,7 +43,7 @@ export default class AppContactsTab implements SliderTab {
|
|||||||
|
|
||||||
public onCloseAfterTimeout() {
|
public onCloseAfterTimeout() {
|
||||||
this.list.innerHTML = '';
|
this.list.innerHTML = '';
|
||||||
this.searchInput.value = '';
|
this.inputSearch.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public openContacts(query?: string) {
|
public openContacts(query?: string) {
|
||||||
|
@ -8,6 +8,7 @@ import AppPrivateSearchTab from "./tabs/search";
|
|||||||
import AppSharedMediaTab from "./tabs/sharedMedia";
|
import AppSharedMediaTab from "./tabs/sharedMedia";
|
||||||
//import AppForwardTab from "./tabs/forward";
|
//import AppForwardTab from "./tabs/forward";
|
||||||
import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config";
|
import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config";
|
||||||
|
import { pause } from "../../helpers/schedulers";
|
||||||
|
|
||||||
export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown';
|
export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown';
|
||||||
|
|
||||||
@ -111,14 +112,10 @@ export class AppSidebarRight extends SidebarSlider {
|
|||||||
//if(mediaSizes.isMobile) {
|
//if(mediaSizes.isMobile) {
|
||||||
//appImManager._selectTab(active ? 1 : 2);
|
//appImManager._selectTab(active ? 1 : 2);
|
||||||
appImManager.selectTab(active ? 1 : 2);
|
appImManager.selectTab(active ? 1 : 2);
|
||||||
return new Promise(resolve => {
|
return pause(mediaSizes.isMobile ? 250 : 200); // delay of slider animation
|
||||||
setTimeout(resolve, mediaSizes.isMobile ? 250 : 200); // delay of slider animation
|
|
||||||
});
|
|
||||||
//}
|
//}
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return pause(200); // delay for third column open
|
||||||
setTimeout(resolve, 200); // delay for third column open
|
|
||||||
});
|
|
||||||
//return Promise.resolve();
|
//return Promise.resolve();
|
||||||
|
|
||||||
/* return new Promise((resolve, reject) => {
|
/* return new Promise((resolve, reject) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SliderTab } from "../../slider";
|
import { SliderTab } from "../../slider";
|
||||||
import SearchInput from "../../searchInput";
|
import InputSearch from "../../inputSearch";
|
||||||
import Scrollable from "../../scrollable";
|
import Scrollable from "../../scrollable";
|
||||||
import animationIntersector from "../../animationIntersector";
|
import animationIntersector from "../../animationIntersector";
|
||||||
import appSidebarRight, { AppSidebarRight } from "..";
|
import appSidebarRight, { AppSidebarRight } from "..";
|
||||||
@ -18,7 +18,7 @@ export default class AppGifsTab implements SliderTab {
|
|||||||
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
|
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
|
||||||
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
|
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
|
||||||
//private input = this.container.querySelector('#stickers-search') as HTMLInputElement;
|
//private input = this.container.querySelector('#stickers-search') as HTMLInputElement;
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
private gifsDiv = this.contentDiv.firstElementChild as HTMLDivElement;
|
private gifsDiv = this.contentDiv.firstElementChild as HTMLDivElement;
|
||||||
private scrollable: Scrollable;
|
private scrollable: Scrollable;
|
||||||
|
|
||||||
@ -35,14 +35,14 @@ export default class AppGifsTab implements SliderTab {
|
|||||||
|
|
||||||
this.masonry = new GifsMasonry(this.gifsDiv, ANIMATIONGROUP, this.scrollable);
|
this.masonry = new GifsMasonry(this.gifsDiv, ANIMATIONGROUP, this.scrollable);
|
||||||
|
|
||||||
this.searchInput = new SearchInput('Search GIFs', (value) => {
|
this.inputSearch = new InputSearch('Search GIFs', (value) => {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.search(value);
|
this.search(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gifsDiv.addEventListener('click', this.onGifsClick);
|
this.gifsDiv.addEventListener('click', this.onGifsClick);
|
||||||
|
|
||||||
this.backBtn.parentElement.append(this.searchInput.container);
|
this.backBtn.parentElement.append(this.inputSearch.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
onGifsClick = (e: MouseEvent) => {
|
onGifsClick = (e: MouseEvent) => {
|
||||||
@ -66,7 +66,7 @@ export default class AppGifsTab implements SliderTab {
|
|||||||
public onCloseAfterTimeout() {
|
public onCloseAfterTimeout() {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.gifsDiv.innerHTML = '';
|
this.gifsDiv.innerHTML = '';
|
||||||
this.searchInput.value = '';
|
this.inputSearch.value = '';
|
||||||
animationIntersector.checkAnimations(undefined, ANIMATIONGROUP);
|
animationIntersector.checkAnimations(undefined, ANIMATIONGROUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export default class AppGifsTab implements SliderTab {
|
|||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
this.scrollable.onScrolledBottom = () => {
|
this.scrollable.onScrolledBottom = () => {
|
||||||
this.search(this.searchInput.value, false);
|
this.search(this.inputSearch.value, false);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export default class AppGifsTab implements SliderTab {
|
|||||||
this.searchPromise = appInlineBotsManager.getInlineResults(0, this.gifBotPeerId, query, this.nextOffset);
|
this.searchPromise = appInlineBotsManager.getInlineResults(0, this.gifBotPeerId, query, this.nextOffset);
|
||||||
const { results, next_offset } = await this.searchPromise;
|
const { results, next_offset } = await this.searchPromise;
|
||||||
|
|
||||||
if(this.searchInput.value != query) {
|
if(this.inputSearch.value != query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import appSidebarRight, { AppSidebarRight } from "..";
|
import appSidebarRight, { AppSidebarRight } from "..";
|
||||||
import AppSearch, { SearchGroup } from "../../appSearch";
|
import AppSearch, { SearchGroup } from "../../appSearch";
|
||||||
import SearchInput from "../../searchInput";
|
import InputSearch from "../../inputSearch";
|
||||||
import { SliderTab } from "../../slider";
|
import { SliderTab } from "../../slider";
|
||||||
|
|
||||||
export default class AppPrivateSearchTab implements SliderTab {
|
export default class AppPrivateSearchTab implements SliderTab {
|
||||||
public container: HTMLElement;
|
public container: HTMLElement;
|
||||||
public closeBtn: HTMLElement;
|
public closeBtn: HTMLElement;
|
||||||
|
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
private appSearch: AppSearch;
|
private appSearch: AppSearch;
|
||||||
|
|
||||||
private peerId = 0;
|
private peerId = 0;
|
||||||
@ -24,9 +24,9 @@ export default class AppPrivateSearchTab implements SliderTab {
|
|||||||
public init() {
|
public init() {
|
||||||
this.container = document.getElementById('search-private-container');
|
this.container = document.getElementById('search-private-container');
|
||||||
this.closeBtn = this.container.querySelector('.sidebar-close-button');
|
this.closeBtn = this.container.querySelector('.sidebar-close-button');
|
||||||
this.searchInput = new SearchInput('Search');
|
this.inputSearch = new InputSearch('Search');
|
||||||
this.closeBtn.parentElement.append(this.searchInput.container);
|
this.closeBtn.parentElement.append(this.inputSearch.container);
|
||||||
this.appSearch = new AppSearch(this.container.querySelector('.chatlist-container'), this.searchInput, {
|
this.appSearch = new AppSearch(this.container.querySelector('.chatlist-container'), this.inputSearch, {
|
||||||
messages: new SearchGroup('Private Search', 'messages')
|
messages: new SearchGroup('Private Search', 'messages')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SliderTab } from "../../slider";
|
import { SliderTab } from "../../slider";
|
||||||
import SearchInput from "../../searchInput";
|
import InputSearch from "../../inputSearch";
|
||||||
import Scrollable from "../../scrollable";
|
import Scrollable from "../../scrollable";
|
||||||
import LazyLoadQueue from "../../lazyLoadQueue";
|
import LazyLoadQueue from "../../lazyLoadQueue";
|
||||||
import { findUpClassName } from "../../../helpers/dom";
|
import { findUpClassName } from "../../../helpers/dom";
|
||||||
@ -17,7 +17,7 @@ export default class AppStickersTab implements SliderTab {
|
|||||||
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
|
private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement;
|
||||||
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
|
private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement;
|
||||||
//private input = this.container.querySelector('#stickers-search') as HTMLInputElement;
|
//private input = this.container.querySelector('#stickers-search') as HTMLInputElement;
|
||||||
private searchInput: SearchInput;
|
private inputSearch: InputSearch;
|
||||||
private setsDiv = this.contentDiv.firstElementChild as HTMLDivElement;
|
private setsDiv = this.contentDiv.firstElementChild as HTMLDivElement;
|
||||||
private scrollable: Scrollable;
|
private scrollable: Scrollable;
|
||||||
private lazyLoadQueue: LazyLoadQueue;
|
private lazyLoadQueue: LazyLoadQueue;
|
||||||
@ -27,11 +27,11 @@ export default class AppStickersTab implements SliderTab {
|
|||||||
|
|
||||||
this.lazyLoadQueue = new LazyLoadQueue();
|
this.lazyLoadQueue = new LazyLoadQueue();
|
||||||
|
|
||||||
this.searchInput = new SearchInput('Search Stickers', (value) => {
|
this.inputSearch = new InputSearch('Search Stickers', (value) => {
|
||||||
this.search(value);
|
this.search(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.backBtn.parentElement.append(this.searchInput.container);
|
this.backBtn.parentElement.append(this.inputSearch.container);
|
||||||
|
|
||||||
this.setsDiv.addEventListener('click', (e) => {
|
this.setsDiv.addEventListener('click', (e) => {
|
||||||
const sticker = findUpClassName(e.target, 'sticker-set-sticker');
|
const sticker = findUpClassName(e.target, 'sticker-set-sticker');
|
||||||
@ -76,7 +76,7 @@ export default class AppStickersTab implements SliderTab {
|
|||||||
|
|
||||||
public onCloseAfterTimeout() {
|
public onCloseAfterTimeout() {
|
||||||
this.setsDiv.innerHTML = '';
|
this.setsDiv.innerHTML = '';
|
||||||
this.searchInput.value = '';
|
this.inputSearch.value = '';
|
||||||
animationIntersector.checkAnimations(undefined, 'STICKERS-SEARCH');
|
animationIntersector.checkAnimations(undefined, 'STICKERS-SEARCH');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ export default class AppStickersTab implements SliderTab {
|
|||||||
|
|
||||||
public renderFeatured() {
|
public renderFeatured() {
|
||||||
return appStickersManager.getFeaturedStickers().then(coveredSets => {
|
return appStickersManager.getFeaturedStickers().then(coveredSets => {
|
||||||
if(this.searchInput.value) {
|
if(this.inputSearch.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +225,7 @@ export default class AppStickersTab implements SliderTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return appStickersManager.searchStickerSets(query, false).then(coveredSets => {
|
return appStickersManager.searchStickerSets(query, false).then(coveredSets => {
|
||||||
if(this.searchInput.value != query) {
|
if(this.inputSearch.value != query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,7 +572,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load: load, wasSeen: true}), Promise.resolve());
|
return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load/* : load, wasSeen: true */}), Promise.resolve());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: {
|
export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: {
|
||||||
@ -813,7 +813,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load, wasSeen: group == 'chat' && stickerType != 2}), Promise.resolve()) : load();
|
return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load/* , wasSeen: group == 'chat' && stickerType != 2 */}), Promise.resolve()) : load();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapReply(title: string, subtitle: string, message?: any) {
|
export function wrapReply(title: string, subtitle: string, message?: any) {
|
||||||
|
125
src/helpers/schedulers.ts
Normal file
125
src/helpers/schedulers.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// * Jolly Cobra's schedulers
|
||||||
|
import { AnyToVoidFunction } from "../types";
|
||||||
|
|
||||||
|
//type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow;
|
||||||
|
|
||||||
|
export function debounce<F extends AnyToVoidFunction>(
|
||||||
|
fn: F,
|
||||||
|
ms: number,
|
||||||
|
shouldRunFirst = true,
|
||||||
|
shouldRunLast = true,
|
||||||
|
) {
|
||||||
|
let waitingTimeout: number | null = null;
|
||||||
|
|
||||||
|
return (...args: Parameters<F>) => {
|
||||||
|
if(waitingTimeout) {
|
||||||
|
clearTimeout(waitingTimeout);
|
||||||
|
waitingTimeout = null;
|
||||||
|
} else if(shouldRunFirst) {
|
||||||
|
// @ts-ignore
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingTimeout = window.setTimeout(() => {
|
||||||
|
if(shouldRunLast) {
|
||||||
|
// @ts-ignore
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingTimeout = null;
|
||||||
|
}, ms);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* export function throttle<F extends AnyToVoidFunction>(
|
||||||
|
fn: F,
|
||||||
|
ms: number,
|
||||||
|
shouldRunFirst = true,
|
||||||
|
) {
|
||||||
|
let interval: number | null = null;
|
||||||
|
let isPending: boolean;
|
||||||
|
let args: Parameters<F>;
|
||||||
|
|
||||||
|
return (..._args: Parameters<F>) => {
|
||||||
|
isPending = true;
|
||||||
|
args = _args;
|
||||||
|
|
||||||
|
if (!interval) {
|
||||||
|
if (shouldRunFirst) {
|
||||||
|
isPending = false;
|
||||||
|
// @ts-ignore
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = window.setInterval(() => {
|
||||||
|
if (!isPending) {
|
||||||
|
window.clearInterval(interval!);
|
||||||
|
interval = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPending = false;
|
||||||
|
// @ts-ignore
|
||||||
|
fn(...args);
|
||||||
|
}, ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* export function throttleWithRaf<F extends AnyToVoidFunction>(fn: F) {
|
||||||
|
return throttleWith(fastRaf, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throttleWithTickEnd<F extends AnyToVoidFunction>(fn: F) {
|
||||||
|
return throttleWith(onTickEnd, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throttleWithNow<F extends AnyToVoidFunction>(fn: F) {
|
||||||
|
return throttleWith(runNow, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler, fn: F) {
|
||||||
|
let waiting = false;
|
||||||
|
let args: Parameters<F>;
|
||||||
|
|
||||||
|
return (..._args: Parameters<F>) => {
|
||||||
|
args = _args;
|
||||||
|
|
||||||
|
if (!waiting) {
|
||||||
|
waiting = true;
|
||||||
|
|
||||||
|
schedulerFn(() => {
|
||||||
|
waiting = false;
|
||||||
|
// @ts-ignore
|
||||||
|
fn(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onTickEnd(cb: NoneToVoidFunction) {
|
||||||
|
Promise.resolve().then(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runNow(fn: NoneToVoidFunction) {
|
||||||
|
fn();
|
||||||
|
} */
|
||||||
|
|
||||||
|
export const pause = (ms: number) => new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* let fastRafCallbacks: NoneToVoidFunction[] | undefined;
|
||||||
|
export function fastRaf(callback: NoneToVoidFunction) {
|
||||||
|
if (!fastRafCallbacks) {
|
||||||
|
fastRafCallbacks = [callback];
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const currentCallbacks = fastRafCallbacks!;
|
||||||
|
fastRafCallbacks = undefined;
|
||||||
|
currentCallbacks.forEach((cb) => cb());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fastRafCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
} */
|
@ -5,7 +5,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Telegram Web</title>
|
<title>Telegram Web</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/img/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/img/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
|
||||||
|
@ -210,9 +210,9 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
this.log('downloadFile', fileName, size, location, options.mimeType, process);
|
this.log('downloadFile', fileName, size, location, options.mimeType, process);
|
||||||
|
|
||||||
if(options.queueId) {
|
/* if(options.queueId) {
|
||||||
this.log.error('downloadFile queueId:', fileName, options.queueId);
|
this.log.error('downloadFile queueId:', fileName, options.queueId);
|
||||||
}
|
} */
|
||||||
|
|
||||||
if(cachedPromise) {
|
if(cachedPromise) {
|
||||||
//this.log('downloadFile cachedPromise');
|
//this.log('downloadFile cachedPromise');
|
||||||
|
@ -699,7 +699,17 @@ class TLDeserialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!constructorData) {
|
if(!constructorData) {
|
||||||
throw new Error('Constructor not found: ' + constructor + ' ' + this.fetchInt() + ' ' + this.fetchInt() + ' ' + field);
|
console.error('Constructor not found:', constructor);
|
||||||
|
|
||||||
|
let int1: number, int2: number;
|
||||||
|
try {
|
||||||
|
int1 = this.fetchInt(field);
|
||||||
|
int2 = this.fetchInt(field);
|
||||||
|
} catch(err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Constructor not found: ' + constructor + ' ' + int1 + ' ' + int2 + ' ' + field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { App } from '../lib/mtproto/mtproto_config';
|
|||||||
import serverTimeManager from '../lib/mtproto/serverTimeManager';
|
import serverTimeManager from '../lib/mtproto/serverTimeManager';
|
||||||
import { AuthAuthorization, AuthLoginToken } from '../layer';
|
import { AuthAuthorization, AuthLoginToken } from '../layer';
|
||||||
import { bytesCmp, bytesToBase64 } from '../helpers/bytes';
|
import { bytesCmp, bytesToBase64 } from '../helpers/bytes';
|
||||||
|
import { pause } from '../helpers/schedulers';
|
||||||
|
|
||||||
let onFirstMount = async() => {
|
let onFirstMount = async() => {
|
||||||
const pageElement = page.pageEl;
|
const pageElement = page.pageEl;
|
||||||
@ -102,7 +103,7 @@ let onFirstMount = async() => {
|
|||||||
let timestamp = Date.now() / 1000;
|
let timestamp = Date.now() / 1000;
|
||||||
let diff = loginToken.expires - timestamp - serverTimeManager.serverTimeOffset;
|
let diff = loginToken.expires - timestamp - serverTimeManager.serverTimeOffset;
|
||||||
|
|
||||||
await new Promise((resolve, reject) => setTimeout(resolve, diff > 5 ? 5e3 : 1e3 * diff | 0));
|
await pause(diff > 5 ? 5e3 : 1e3 * diff | 0);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
switch(err.type) {
|
switch(err.type) {
|
||||||
case 'SESSION_PASSWORD_NEEDED':
|
case 'SESSION_PASSWORD_NEEDED':
|
||||||
|
@ -337,6 +337,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animated-super-row {
|
||||||
|
--translateY: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-message-media {
|
||||||
|
--translateY: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .animated-super-row.is-hiding {
|
||||||
|
&.from-top {
|
||||||
|
transform: translateY(-16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.from-bottom {
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-message-media.is-hiding {
|
||||||
|
&.from-top {
|
||||||
|
transform: translateY(-32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.from-bottom {
|
||||||
|
transform: translateY(32px);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
&.hide ~ .tgico-pinlist, &:not(.is-many) ~ .tgico-pinlist {
|
&.hide ~ .tgico-pinlist, &:not(.is-many) ~ .tgico-pinlist {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -13,73 +13,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-search {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
//Vozmojno nado budet vernut margin-left: 22px;, tak kak eto vrode v levom bare tak po verstke, a v pravom bare dlya mobili nado 16, gde stiker seti
|
|
||||||
margin-left: 22px;
|
|
||||||
margin-right: 4px;
|
|
||||||
|
|
||||||
@include respond-to(handhelds) {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
--border-width: 1px;
|
|
||||||
background-color: var(--color-gray-hover);
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 22px;
|
|
||||||
border: var(--border-width) solid transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0px calc(1.5rem - var(--border-width)) 0 calc(42px - var(--border-width));
|
|
||||||
transition: background-color .15s ease-in-out, border-color .15s ease-in-out;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--color-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
--border-width: 2px;
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: $button-primary-background;
|
|
||||||
|
|
||||||
& + .tgico {
|
|
||||||
color: $button-primary-background;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tgico {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
color: $color-gray;
|
|
||||||
opacity: .6;
|
|
||||||
transition: all .15s ease-out;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tgico-close {
|
|
||||||
left: auto;
|
|
||||||
right: 0px;
|
|
||||||
top: 48%;
|
|
||||||
}
|
|
||||||
|
|
||||||
//input.is-empty ~ .tgico-close {
|
|
||||||
input:placeholder-shown ~ .tgico-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
//padding: 0 .5rem;
|
//padding: 0 .5rem;
|
||||||
|
@ -47,15 +47,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input, &-input {
|
input, &-input {
|
||||||
|
--height: 54px;
|
||||||
|
--padding: 1rem;
|
||||||
--border-width: 1px;
|
--border-width: 1px;
|
||||||
--border-width-top: 2px;
|
--border-width-top: 2px;
|
||||||
border: var(--border-width) solid #DADCE0;
|
border: var(--border-width) solid #DADCE0;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
//padding: 0 1rem;
|
//padding: 0 1rem;
|
||||||
padding: calc(1rem - var(--border-width-top)) calc(1rem - var(--border-width));
|
padding: calc(var(--padding) - var(--border-width-top)) calc(var(--padding) - var(--border-width));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 54px;
|
min-height: var(--height);
|
||||||
transition: .2s border-color;
|
transition: .2s border-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -126,7 +128,7 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
left: .75rem;
|
left: .75rem;
|
||||||
font-size: 0.75rem!important;
|
font-size: .75rem!important;
|
||||||
//color: #666;
|
//color: #666;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@ -143,11 +145,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||||
color: #a2acb4;
|
color: #909192;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-ms-input-placeholder { /* Microsoft Edge */
|
::-ms-input-placeholder { /* Microsoft Edge */
|
||||||
color: #a2acb4;
|
color: #909192;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus, button:focus {
|
input:focus, button:focus {
|
||||||
@ -181,3 +183,74 @@ input:focus, button:focus {
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-search {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
//Vozmojno nado budet vernut margin-left: 22px;, tak kak eto vrode v levom bare tak po verstke, a v pravom bare dlya mobili nado 16, gde stiker seti
|
||||||
|
margin-left: 22px;
|
||||||
|
margin-right: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@include respond-to(handhelds) {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
--height: 40px;
|
||||||
|
background-color: var(--color-gray-hover);
|
||||||
|
padding: 0px calc(42px - var(--border-width));
|
||||||
|
height: var(--height);
|
||||||
|
max-height: var(--height);
|
||||||
|
//line-height: calc(var(--height) + 2px - var(--border-width) * 2);
|
||||||
|
border-radius: 22px;
|
||||||
|
transition: background-color .2s ease-in-out, border-color .2s ease-in-out;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
--border-width: 2px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: $button-primary-background;
|
||||||
|
|
||||||
|
& + .tgico {
|
||||||
|
color: $button-primary-background;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* &:empty:before {
|
||||||
|
color: #909192 !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* &:empty ~ .tgico-close, */&:placeholder-shown ~ .tgico-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgico {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: $color-gray;
|
||||||
|
opacity: .6;
|
||||||
|
transition: all .2s ease-out;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgico-close {
|
||||||
|
left: auto;
|
||||||
|
right: 0px;
|
||||||
|
top: 48%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
@ -870,6 +870,7 @@ img.emoji {
|
|||||||
|
|
||||||
.animated-super {
|
.animated-super {
|
||||||
&-row {
|
&-row {
|
||||||
|
--translateY: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -877,15 +878,23 @@ img.emoji {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
transition: transform var(--pm-transition), opacity var(--pm-transition);
|
transition: transform var(--pm-transition), opacity var(--pm-transition);
|
||||||
|
|
||||||
|
/* &:not(.is-hiding) {
|
||||||
|
transform: none !important;
|
||||||
|
} */
|
||||||
|
|
||||||
&.is-hiding {
|
&.is-hiding {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
&.from-top {
|
&.from-top {
|
||||||
transform: translateY(-100%);
|
transform: translate3d(0, calc(var(--translateY) * -1), 0);
|
||||||
|
//transform: translateY(calc(var(--translateY) * -1));
|
||||||
|
//transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.from-bottom {
|
&.from-bottom {
|
||||||
transform: translateY(100%);
|
transform: translate3d(0, var(--translateY), 0);
|
||||||
|
//transform: translateY(var(--translateY));
|
||||||
|
//transform: translateY(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* &.backwards {
|
/* &.backwards {
|
||||||
|
6
src/types.d.ts
vendored
6
src/types.d.ts
vendored
@ -35,6 +35,12 @@ export type Modify<T, R> = Omit<T, keyof R> & R;
|
|||||||
|
|
||||||
export type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
|
export type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
|
||||||
|
|
||||||
|
export type AnyLiteral = Record<string, any>;
|
||||||
|
export type AnyClass = new (...args: any[]) => any;
|
||||||
|
export type AnyFunction = (...args: any) => any;
|
||||||
|
export type AnyToVoidFunction = (...args: any) => void;
|
||||||
|
export type NoneToVoidFunction = () => void;
|
||||||
|
|
||||||
export type AuthState = AuthState.signIn | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn;
|
export type AuthState = AuthState.signIn | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn;
|
||||||
export namespace AuthState {
|
export namespace AuthState {
|
||||||
export type signIn = {
|
export type signIn = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user