|
|
|
|
/*
|
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
//import apiManager from '../mtproto/apiManager';
|
|
|
|
|
import animationIntersector from '../../components/animationIntersector';
|
|
|
|
|
import appSidebarLeft, { LEFT_COLUMN_ACTIVE_CLASSNAME } from "../../components/sidebarLeft";
|
|
|
|
|
import appSidebarRight, { RIGHT_COLUMN_ACTIVE_CLASSNAME } from '../../components/sidebarRight';
|
|
|
|
|
import mediaSizes, { ScreenSize } from '../../helpers/mediaSizes';
|
|
|
|
|
import { logger, LogTypes } from "../logger";
|
|
|
|
|
import apiManager from '../mtproto/mtprotoworker';
|
|
|
|
|
import rootScope from '../rootScope';
|
|
|
|
|
import apiUpdatesManager from './apiUpdatesManager';
|
|
|
|
|
import appUsersManager from "./appUsersManager";
|
|
|
|
|
import Chat, { ChatType } from '../../components/chat/chat';
|
|
|
|
|
import appChatsManager from './appChatsManager';
|
|
|
|
|
import appDocsManager from './appDocsManager';
|
|
|
|
|
import appInlineBotsManager from './appInlineBotsManager';
|
|
|
|
|
import appMessagesManager from './appMessagesManager';
|
|
|
|
|
import appPeersManager from './appPeersManager';
|
|
|
|
|
import appPhotosManager from './appPhotosManager';
|
|
|
|
|
import appProfileManager from './appProfileManager';
|
|
|
|
|
import appStickersManager from './appStickersManager';
|
|
|
|
|
import appWebPagesManager from './appWebPagesManager';
|
|
|
|
|
import PopupNewMedia from '../../components/popups/newMedia';
|
|
|
|
|
import MarkupTooltip from '../../components/chat/markupTooltip';
|
|
|
|
|
import { isTouchSupported } from '../../helpers/touchSupport';
|
|
|
|
|
import appPollsManager from './appPollsManager';
|
|
|
|
|
import SetTransition from '../../components/singleTransition';
|
|
|
|
|
import ChatDragAndDrop from '../../components/chat/dragAndDrop';
|
|
|
|
|
import { debounce, pause, doubleRaf } from '../../helpers/schedulers';
|
|
|
|
|
import lottieLoader from '../lottieLoader';
|
|
|
|
|
import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
|
|
|
|
import appDraftsManager from './appDraftsManager';
|
|
|
|
|
import serverTimeManager from '../mtproto/serverTimeManager';
|
|
|
|
|
import stateStorage from '../stateStorage';
|
|
|
|
|
import appDownloadManager from './appDownloadManager';
|
|
|
|
|
import { AppStateManager } from './appStateManager';
|
|
|
|
|
import { MOUNT_CLASS_TO } from '../../config/debug';
|
|
|
|
|
import appNavigationController from '../../components/appNavigationController';
|
|
|
|
|
import appNotificationsManager from './appNotificationsManager';
|
|
|
|
|
import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
|
|
|
|
|
import { i18n, LangPackKey } from '../langPack';
|
|
|
|
|
import { SendMessageAction } from '../../layer';
|
|
|
|
|
import { hslaStringToHex } from '../../helpers/color';
|
|
|
|
|
import { copy, getObjectKeysAndSort } from '../../helpers/object';
|
|
|
|
|
import { getFilesFromEvent } from '../../helpers/files';
|
|
|
|
|
import PeerTitle from '../../components/peerTitle';
|
|
|
|
|
import PopupPeer from '../../components/popups/peer';
|
|
|
|
|
import { SliceEnd } from '../../helpers/slicedArray';
|
|
|
|
|
import blurActiveElement from '../../helpers/dom/blurActiveElement';
|
|
|
|
|
import { cancelEvent } from '../../helpers/dom/cancelEvent';
|
|
|
|
|
import disableTransition from '../../helpers/dom/disableTransition';
|
|
|
|
|
import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
|
|
|
|
|
import replaceContent from '../../helpers/dom/replaceContent';
|
|
|
|
|
import whichChild from '../../helpers/dom/whichChild';
|
|
|
|
|
import appEmojiManager from './appEmojiManager';
|
|
|
|
|
import PopupElement from '../../components/popups';
|
|
|
|
|
import singleInstance from '../mtproto/singleInstance';
|
|
|
|
|
|
|
|
|
|
//console.log('appImManager included33!');
|
|
|
|
|
|
|
|
|
|
appSidebarLeft; // just to include
|
|
|
|
|
|
|
|
|
|
export const CHAT_ANIMATION_GROUP = 'chat';
|
|
|
|
|
const FOCUS_EVENT_NAME = isTouchSupported ? 'touchstart' : 'mousemove';
|
|
|
|
|
|
|
|
|
|
export type ChatSavedPosition = {
|
|
|
|
|
mids: number[],
|
|
|
|
|
top: number
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class AppImManager {
|
|
|
|
|
public columnEl = document.getElementById('column-center') as HTMLDivElement;
|
|
|
|
|
public chatsContainer: HTMLElement;
|
|
|
|
|
|
|
|
|
|
public offline = false;
|
|
|
|
|
public updateStatusInterval = 0;
|
|
|
|
|
|
|
|
|
|
public log: ReturnType<typeof logger>;
|
|
|
|
|
|
|
|
|
|
public setPeerPromise: Promise<void> = null;
|
|
|
|
|
|
|
|
|
|
public tabId = -1;
|
|
|
|
|
|
|
|
|
|
public chats: Chat[] = [];
|
|
|
|
|
private prevTab: HTMLElement;
|
|
|
|
|
private chatsSelectTabDebounced: () => void;
|
|
|
|
|
|
|
|
|
|
public markupTooltip: MarkupTooltip;
|
|
|
|
|
private themeColorElem: Element;
|
|
|
|
|
private backgroundPromises: {[slug: string]: Promise<string>} = {};
|
|
|
|
|
|
|
|
|
|
get myId() {
|
|
|
|
|
return rootScope.myId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get chat(): Chat {
|
|
|
|
|
return this.chats[this.chats.length - 1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
apiUpdatesManager.attach();
|
|
|
|
|
|
|
|
|
|
this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error);
|
|
|
|
|
|
|
|
|
|
this.selectTab(0);
|
|
|
|
|
|
|
|
|
|
window.addEventListener('blur', () => {
|
|
|
|
|
animationIntersector.checkAnimations(true);
|
|
|
|
|
|
|
|
|
|
this.offline = rootScope.idle.isIDLE = true;
|
|
|
|
|
this.updateStatus();
|
|
|
|
|
clearInterval(this.updateStatusInterval);
|
|
|
|
|
rootScope.dispatchEvent('idle', rootScope.idle.isIDLE);
|
|
|
|
|
|
|
|
|
|
window.addEventListener('focus', () => {
|
|
|
|
|
this.offline = rootScope.idle.isIDLE = false;
|
|
|
|
|
this.updateStatus();
|
|
|
|
|
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
|
|
|
|
|
|
|
|
|
|
// в обратном порядке
|
|
|
|
|
animationIntersector.checkAnimations(false);
|
|
|
|
|
|
|
|
|
|
rootScope.dispatchEvent('idle', rootScope.idle.isIDLE);
|
|
|
|
|
}, {once: true});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// * Prevent setting online after reloading page
|
|
|
|
|
window.addEventListener(FOCUS_EVENT_NAME, () => {
|
|
|
|
|
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
|
|
|
|
|
this.updateStatus();
|
|
|
|
|
|
|
|
|
|
this.offline = rootScope.idle.isIDLE = false;
|
|
|
|
|
rootScope.dispatchEvent('idle', rootScope.idle.isIDLE);
|
|
|
|
|
}, {once: true, passive: true});
|
|
|
|
|
|
|
|
|
|
this.chatsContainer = document.createElement('div');
|
|
|
|
|
this.chatsContainer.classList.add('chats-container', 'tabs-container');
|
|
|
|
|
this.chatsContainer.dataset.animation = 'navigation';
|
|
|
|
|
|
|
|
|
|
this.columnEl.append(this.chatsContainer);
|
|
|
|
|
|
|
|
|
|
this.createNewChat();
|
|
|
|
|
this.chatsSelectTab(this.chat.container);
|
|
|
|
|
|
|
|
|
|
appNavigationController.onHashChange = this.onHashChange;
|
|
|
|
|
//window.addEventListener('hashchange', this.onHashChange);
|
|
|
|
|
|
|
|
|
|
this.setSettings();
|
|
|
|
|
rootScope.addEventListener('settings_updated', this.setSettings);
|
|
|
|
|
|
|
|
|
|
useHeavyAnimationCheck(() => {
|
|
|
|
|
animationIntersector.setOnlyOnePlayableGroup('lock');
|
|
|
|
|
animationIntersector.checkAnimations(true);
|
|
|
|
|
}, () => {
|
|
|
|
|
animationIntersector.setOnlyOnePlayableGroup('');
|
|
|
|
|
animationIntersector.checkAnimations(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.applyCurrentTheme();
|
|
|
|
|
|
|
|
|
|
// * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar
|
|
|
|
|
mediaSizes.addEventListener('changeScreen', (from, to) => {
|
|
|
|
|
if(document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)
|
|
|
|
|
&& document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) {
|
|
|
|
|
appSidebarRight.toggleSidebar(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('history_focus', (e) => {
|
|
|
|
|
let {peerId, mid} = e;
|
|
|
|
|
if(mid) {
|
|
|
|
|
mid = appMessagesManager.generateMessageId(mid); // because mid can come from notification, i.e. server message id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setInnerPeer(peerId, mid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('peer_changing', (chat) => {
|
|
|
|
|
this.saveChatPosition(chat);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('theme_change', () => {
|
|
|
|
|
this.applyCurrentTheme();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('instance_deactivated', () => {
|
|
|
|
|
const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true});
|
|
|
|
|
const c = document.createElement('div');
|
|
|
|
|
c.classList.add('instance-deactivated-container');
|
|
|
|
|
(popup as any).container.replaceWith(c);
|
|
|
|
|
|
|
|
|
|
const header = document.createElement('div');
|
|
|
|
|
header.classList.add('header');
|
|
|
|
|
header.append(i18n('Deactivated.Title'));
|
|
|
|
|
|
|
|
|
|
const subtitle = document.createElement('div');
|
|
|
|
|
subtitle.classList.add('subtitle');
|
|
|
|
|
subtitle.append(i18n('Deactivated.Subtitle'));
|
|
|
|
|
|
|
|
|
|
c.append(header, subtitle);
|
|
|
|
|
|
|
|
|
|
document.body.classList.add('deactivated');
|
|
|
|
|
|
|
|
|
|
(popup as any).onClose = () => {
|
|
|
|
|
document.body.classList.add('deactivated-backwards');
|
|
|
|
|
|
|
|
|
|
singleInstance.activateInstance();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
document.body.classList.remove('deactivated', 'deactivated-backwards');
|
|
|
|
|
}, 333);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
popup.show();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stateStorage.get('chatPositions').then((c) => {
|
|
|
|
|
stateStorage.setToCache('chatPositions', c || {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
(window as any).showMaskedAlert = (element: HTMLAnchorElement, e: Event) => {
|
|
|
|
|
cancelEvent(null);
|
|
|
|
|
|
|
|
|
|
const href = element.href;
|
|
|
|
|
|
|
|
|
|
const a = element.cloneNode(true) as HTMLAnchorElement;
|
|
|
|
|
a.className = 'anchor-url';
|
|
|
|
|
a.innerText = href;
|
|
|
|
|
a.removeAttribute('onclick');
|
|
|
|
|
|
|
|
|
|
new PopupPeer('popup-masked-url', {
|
|
|
|
|
titleLangKey: 'OpenUrlTitle',
|
|
|
|
|
descriptionLangKey: 'OpenUrlAlert2',
|
|
|
|
|
descriptionLangArgs: [a],
|
|
|
|
|
buttons: [{
|
|
|
|
|
langKey: 'Open',
|
|
|
|
|
callback: () => {
|
|
|
|
|
a.click();
|
|
|
|
|
},
|
|
|
|
|
}]
|
|
|
|
|
}).show();
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(window as any).execBotCommand = (element: HTMLAnchorElement, e: Event) => {
|
|
|
|
|
cancelEvent(null);
|
|
|
|
|
|
|
|
|
|
const href = element.href;
|
|
|
|
|
const params = this.parseUriParams(href);
|
|
|
|
|
if(!params) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {command, bot} = params;
|
|
|
|
|
|
|
|
|
|
/* const promise = bot ? this.openUsername(bot).then(() => this.chat.peerId) : Promise.resolve(this.chat.peerId);
|
|
|
|
|
promise.then(peerId => {
|
|
|
|
|
appMessagesManager.sendText(peerId, '/' + command);
|
|
|
|
|
}); */
|
|
|
|
|
|
|
|
|
|
appMessagesManager.sendText(this.chat.peerId, '/' + command + (bot ? '@' + bot : ''));
|
|
|
|
|
|
|
|
|
|
//console.log(command, bot);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(window as any).searchByHashtag = (element: HTMLAnchorElement, e: Event) => {
|
|
|
|
|
cancelEvent(null);
|
|
|
|
|
|
|
|
|
|
const href = element.href;
|
|
|
|
|
const params = this.parseUriParams(href);
|
|
|
|
|
if(!params) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {hashtag} = params;
|
|
|
|
|
this.chat.initSearch('#' + hashtag + ' ');
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parseUriParams(uri: string, splitted = uri.split('?')) {
|
|
|
|
|
if(!splitted[1]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const params: any = {};
|
|
|
|
|
splitted[1].split('&').forEach(item => {
|
|
|
|
|
params[item.split('=')[0]] = decodeURIComponent(item.split('=')[1]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return params;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onHashChange = () => {
|
|
|
|
|
const hash = location.hash;
|
|
|
|
|
const splitted = hash.split('?');
|
|
|
|
|
|
|
|
|
|
const params = this.parseUriParams(hash, splitted);
|
|
|
|
|
|
|
|
|
|
this.log('hashchange', hash, splitted[0], params);
|
|
|
|
|
|
|
|
|
|
switch(splitted[0]) {
|
|
|
|
|
case '#/im': {
|
|
|
|
|
const p = params.p;
|
|
|
|
|
let postId = params.post !== undefined ? appMessagesManager.generateMessageId(+params.post) : undefined;
|
|
|
|
|
|
|
|
|
|
switch(p[0]) {
|
|
|
|
|
case '@': {
|
|
|
|
|
this.openUsername(p, postId);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: { // peerId
|
|
|
|
|
this.setInnerPeer(postId ? -+p : +p, postId);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//appNavigationController.replaceState();
|
|
|
|
|
//location.hash = '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public openUsername(username: string, msgId?: number) {
|
|
|
|
|
return appUsersManager.resolveUsername(username).then(peer => {
|
|
|
|
|
const isUser = peer._ === 'user';
|
|
|
|
|
const peerId = isUser ? peer.id : -peer.id;
|
|
|
|
|
|
|
|
|
|
return this.setInnerPeer(peerId, msgId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setCurrentBackground(broadcastEvent = false) {
|
|
|
|
|
const theme = rootScope.getTheme();
|
|
|
|
|
|
|
|
|
|
if(theme.background.type === 'image' || (theme.background.type === 'default' && theme.background.slug)) {
|
|
|
|
|
const defaultTheme = AppStateManager.STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
|
|
|
|
const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
|
|
|
|
theme.background.slug === defaultTheme.background.slug;
|
|
|
|
|
|
|
|
|
|
if(!isDefaultBackground) {
|
|
|
|
|
return this.getBackground(theme.background.slug).then((url) => {
|
|
|
|
|
return this.setBackground(url, broadcastEvent);
|
|
|
|
|
}, () => { // * if NO_ENTRY_FOUND
|
|
|
|
|
theme.background = copy(defaultTheme.background); // * reset background
|
|
|
|
|
return this.setBackground('', true);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.setBackground('', broadcastEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getBackground(slug: string) {
|
|
|
|
|
if(this.backgroundPromises[slug]) return this.backgroundPromises[slug];
|
|
|
|
|
return this.backgroundPromises[slug] = appDownloadManager.cacheStorage.getFile('backgrounds/' + slug).then(blob => {
|
|
|
|
|
return URL.createObjectURL(blob);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setBackground(url: string, broadcastEvent = true): Promise<void> {
|
|
|
|
|
const promises = this.chats.map(chat => chat.setBackground(url));
|
|
|
|
|
return promises[promises.length - 1].then(() => {
|
|
|
|
|
if(broadcastEvent) {
|
|
|
|
|
rootScope.dispatchEvent('background_change');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public saveChatPosition(chat: Chat) {
|
|
|
|
|
if(!(['chat', 'discussion'] as ChatType[]).includes(chat.type) || !chat.peerId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//const bubble = chat.bubbles.getBubbleByPoint('top');
|
|
|
|
|
//if(bubble) {
|
|
|
|
|
//const top = bubble.getBoundingClientRect().top;
|
|
|
|
|
const top = chat.bubbles.scrollable.scrollTop;
|
|
|
|
|
|
|
|
|
|
const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : '');
|
|
|
|
|
|
|
|
|
|
const chatPositions = stateStorage.getFromCache('chatPositions');
|
|
|
|
|
if(!(chat.bubbles.scrollable.getDistanceToEnd() <= 16 && chat.bubbles.scrollable.loadedAll.bottom) && Object.keys(chat.bubbles.bubbles).length) {
|
|
|
|
|
const position = {
|
|
|
|
|
mids: getObjectKeysAndSort(chat.bubbles.bubbles, 'desc'),
|
|
|
|
|
top
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
chatPositions[key] = position;
|
|
|
|
|
|
|
|
|
|
this.log('saved chat position:', position);
|
|
|
|
|
} else {
|
|
|
|
|
delete chatPositions[key];
|
|
|
|
|
|
|
|
|
|
this.log('deleted chat position');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stateStorage.set({chatPositions}, true);
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getChatSavedPosition(chat: Chat): ChatSavedPosition {
|
|
|
|
|
if(!(['chat', 'discussion'] as ChatType[]).includes(chat.type) || !chat.peerId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : '');
|
|
|
|
|
const cache = stateStorage.getFromCache('chatPositions');
|
|
|
|
|
return cache && cache[key];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public applyHighlightningColor() {
|
|
|
|
|
let hsla: string;
|
|
|
|
|
const theme = rootScope.getTheme();
|
|
|
|
|
if(theme.background.highlightningColor) {
|
|
|
|
|
hsla = theme.background.highlightningColor;
|
|
|
|
|
document.documentElement.style.setProperty('--message-highlightning-color', hsla);
|
|
|
|
|
} else {
|
|
|
|
|
document.documentElement.style.removeProperty('--message-highlightning-color');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let themeColor = '#ffffff';
|
|
|
|
|
if(hsla) {
|
|
|
|
|
themeColor = hslaStringToHex(hsla);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.themeColorElem === undefined) {
|
|
|
|
|
this.themeColorElem = document.head.querySelector('[name="theme-color"]') as Element || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.themeColorElem) {
|
|
|
|
|
this.themeColorElem.setAttribute('content', themeColor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public applyCurrentTheme(slug?: string, backgroundUrl?: string, broadcastEvent?: boolean) {
|
|
|
|
|
this.applyHighlightningColor();
|
|
|
|
|
|
|
|
|
|
rootScope.setTheme();
|
|
|
|
|
|
|
|
|
|
if(backgroundUrl) {
|
|
|
|
|
this.backgroundPromises[slug] = Promise.resolve(backgroundUrl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.setCurrentBackground(broadcastEvent === undefined ? !!slug : broadcastEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setSettings = () => {
|
|
|
|
|
document.documentElement.style.setProperty('--messages-text-size', rootScope.settings.messagesTextSize + 'px');
|
|
|
|
|
|
|
|
|
|
document.body.classList.toggle('animation-level-0', !rootScope.settings.animationsEnabled);
|
|
|
|
|
document.body.classList.toggle('animation-level-1', false);
|
|
|
|
|
document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled);
|
|
|
|
|
|
|
|
|
|
this.chatsSelectTabDebounced = debounce(() => {
|
|
|
|
|
const topbar = this.chat.topbar;
|
|
|
|
|
if(topbar.pinnedMessage) { // * буду молиться богам, чтобы это ничего не сломало, но это исправляет получение пиннеда после анимации
|
|
|
|
|
topbar.pinnedMessage.setCorrectIndex(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apiManager.setQueueId(this.chat.bubbles.lazyLoadQueue.queueId);
|
|
|
|
|
}, rootScope.settings.animationsEnabled ? 250 : 0, false, true);
|
|
|
|
|
|
|
|
|
|
lottieLoader.setLoop(rootScope.settings.stickers.loop);
|
|
|
|
|
animationIntersector.checkAnimations(false);
|
|
|
|
|
|
|
|
|
|
for(const chat of this.chats) {
|
|
|
|
|
chat.setAutoDownloadMedia();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом
|
|
|
|
|
// * (или под текущим чатом) чтобы правильно отрендерить чат (напр. scrollTop)
|
|
|
|
|
private chatsSelectTab(tab: HTMLElement, animate?: boolean) {
|
|
|
|
|
if(this.prevTab === tab) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(animate === false && this.prevTab) { // * will be used for Safari iOS history swipe
|
|
|
|
|
disableTransition([tab, this.prevTab].filter(Boolean));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.prevTab) {
|
|
|
|
|
this.prevTab.classList.remove('active');
|
|
|
|
|
this.chatsSelectTabDebounced();
|
|
|
|
|
|
|
|
|
|
// ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс
|
|
|
|
|
if(rootScope.settings.animationsEnabled && animate !== false) {
|
|
|
|
|
dispatchHeavyAnimationEvent(pause(250 + 150), 250 + 150);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prevIdx = whichChild(this.prevTab);
|
|
|
|
|
const idx = whichChild(tab);
|
|
|
|
|
if(idx > prevIdx) {
|
|
|
|
|
appNavigationController.pushItem({
|
|
|
|
|
type: 'chat',
|
|
|
|
|
onPop: (canAnimate) => {
|
|
|
|
|
this.setPeer(0, undefined, canAnimate);
|
|
|
|
|
blurActiveElement();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tab.classList.add('active');
|
|
|
|
|
this.prevTab = tab;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private init() {
|
|
|
|
|
document.addEventListener('paste', this.onDocumentPaste, true);
|
|
|
|
|
|
|
|
|
|
const IGNORE_KEYS = new Set(['PageUp', 'PageDown', 'Meta', 'Control']);
|
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if(rootScope.overlayIsActive || IGNORE_KEYS.has(e.key)) return;
|
|
|
|
|
|
|
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
|
|
|
|
|
//if(target.tagName === 'INPUT') return;
|
|
|
|
|
|
|
|
|
|
//this.log('onkeydown', e, document.activeElement);
|
|
|
|
|
|
|
|
|
|
const chat = this.chat;
|
|
|
|
|
|
|
|
|
|
if(e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT') {
|
|
|
|
|
return;
|
|
|
|
|
} else if(e.code === 'ArrowUp') {
|
|
|
|
|
if(!chat.input.editMsgId && chat.input.isInputEmpty()) {
|
|
|
|
|
const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId);
|
|
|
|
|
const slice = historyStorage.history.slice;
|
|
|
|
|
if(slice.isEnd(SliceEnd.Bottom) && slice.length) {
|
|
|
|
|
let goodMid: number;
|
|
|
|
|
for(const mid of slice) {
|
|
|
|
|
const message = chat.getMessage(mid);
|
|
|
|
|
const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out;
|
|
|
|
|
|
|
|
|
|
if(good) {
|
|
|
|
|
if(appMessagesManager.canEditMessage(chat.getMessage(mid), 'text')) {
|
|
|
|
|
goodMid = mid;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// * this check will allow editing only last message
|
|
|
|
|
//break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(goodMid) {
|
|
|
|
|
chat.input.initMessageEditing(goodMid);
|
|
|
|
|
cancelEvent(e); // * prevent from scrolling
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else if(e.code === 'ArrowDown') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(chat.input.messageInput &&
|
|
|
|
|
e.target !== chat.input.messageInput &&
|
|
|
|
|
target.tagName !== 'INPUT' &&
|
|
|
|
|
!target.hasAttribute('contenteditable') &&
|
|
|
|
|
!isTouchSupported &&
|
|
|
|
|
(!mediaSizes.isMobile || this.tabId === 1) &&
|
|
|
|
|
!this.chat.selection.isSelecting &&
|
|
|
|
|
!this.chat.input.recording) {
|
|
|
|
|
chat.input.messageInput.focus();
|
|
|
|
|
placeCaretAtEnd(chat.input.messageInput);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.body.addEventListener('keydown', onKeyDown);
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('history_multiappend', (e) => {
|
|
|
|
|
const msgIdsByPeer = e;
|
|
|
|
|
|
|
|
|
|
for(const peerId in msgIdsByPeer) {
|
|
|
|
|
appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, Array.from(msgIdsByPeer[peerId]));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rootScope.addEventListener('history_delete', (e) => {
|
|
|
|
|
const {peerId, msgs} = e;
|
|
|
|
|
|
|
|
|
|
const mids = Object.keys(msgs).map(s => +s);
|
|
|
|
|
appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, mids);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calls when message successfully sent and we have an id
|
|
|
|
|
rootScope.addEventListener('message_sent', (e) => {
|
|
|
|
|
const {storage, tempId, mid} = e;
|
|
|
|
|
const message = appMessagesManager.getMessageFromStorage(storage, mid);
|
|
|
|
|
appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if(!isTouchSupported) {
|
|
|
|
|
this.attachDragAndDropListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//if(!isTouchSupported) {
|
|
|
|
|
this.markupTooltip = new MarkupTooltip(this);
|
|
|
|
|
this.markupTooltip.handleSelection();
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private attachDragAndDropListeners() {
|
|
|
|
|
const drops: ChatDragAndDrop[] = [];
|
|
|
|
|
let mounted = false;
|
|
|
|
|
const toggle = async(e: DragEvent, mount: boolean) => {
|
|
|
|
|
if(mount === mounted) return;
|
|
|
|
|
|
|
|
|
|
const _types = e.dataTransfer.types;
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const isFiles = _types.contains ? _types.contains('Files') : _types.indexOf('Files') >= 0;
|
|
|
|
|
|
|
|
|
|
if(!isFiles || !this.canDrag()) { // * skip dragging text case
|
|
|
|
|
counter = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(mount && !drops.length) {
|
|
|
|
|
const types: string[] = await getFilesFromEvent(e, true)
|
|
|
|
|
const force = isFiles && !types.length; // * can't get file items not from 'drop' on Safari
|
|
|
|
|
|
|
|
|
|
const foundMedia = types.filter(t => ['image', 'video'].includes(t.split('/')[0])).length;
|
|
|
|
|
const foundDocuments = types.length - foundMedia;
|
|
|
|
|
|
|
|
|
|
this.log('drag files', types);
|
|
|
|
|
|
|
|
|
|
if(types.length || force) {
|
|
|
|
|
drops.push(new ChatDragAndDrop(dropsContainer, {
|
|
|
|
|
icon: 'dragfiles',
|
|
|
|
|
header: 'Chat.DropTitle',
|
|
|
|
|
subtitle: 'Chat.DropAsFilesDesc',
|
|
|
|
|
onDrop: (e: DragEvent) => {
|
|
|
|
|
toggle(e, false);
|
|
|
|
|
appImManager.log('drop', e);
|
|
|
|
|
appImManager.onDocumentPaste(e, 'document');
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if((foundMedia && !foundDocuments) || force) {
|
|
|
|
|
drops.push(new ChatDragAndDrop(dropsContainer, {
|
|
|
|
|
icon: 'dragmedia',
|
|
|
|
|
header: 'Chat.DropTitle',
|
|
|
|
|
subtitle: 'Chat.DropQuickDesc',
|
|
|
|
|
onDrop: (e: DragEvent) => {
|
|
|
|
|
toggle(e, false);
|
|
|
|
|
appImManager.log('drop', e);
|
|
|
|
|
appImManager.onDocumentPaste(e, 'media');
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.chat.container.append(dropsContainer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//if(!mount) return;
|
|
|
|
|
|
|
|
|
|
SetTransition(dropsContainer, 'is-visible', mount, 200, () => {
|
|
|
|
|
if(!mount) {
|
|
|
|
|
drops.forEach(drop => {
|
|
|
|
|
drop.destroy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
drops.length = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if(mount) {
|
|
|
|
|
drops.forEach(drop => {
|
|
|
|
|
drop.setPath();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
counter = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.body.classList.toggle('is-dragging', mount);
|
|
|
|
|
mounted = mount;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* document.body.addEventListener('dragover', (e) => {
|
|
|
|
|
cancelEvent(e);
|
|
|
|
|
}); */
|
|
|
|
|
|
|
|
|
|
let counter = 0;
|
|
|
|
|
document.body.addEventListener('dragenter', (e) => {
|
|
|
|
|
counter++;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.body.addEventListener('dragover', (e) => {
|
|
|
|
|
//this.log('dragover', e/* , e.dataTransfer.types[0] */);
|
|
|
|
|
toggle(e, true);
|
|
|
|
|
cancelEvent(e);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.body.addEventListener('dragleave', (e) => {
|
|
|
|
|
//this.log('dragleave', e, counter);
|
|
|
|
|
//if((e.pageX <= 0 || e.pageX >= appPhotosManager.windowW) || (e.pageY <= 0 || e.pageY >= appPhotosManager.windowH)) {
|
|
|
|
|
counter--;
|
|
|
|
|
if(counter === 0) {
|
|
|
|
|
//if(!findUpClassName(e.target, 'drops-container')) {
|
|
|
|
|
toggle(e, false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const dropsContainer = document.createElement('div');
|
|
|
|
|
dropsContainer.classList.add('drops-container');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private canDrag() {
|
|
|
|
|
const peerId = this.chat?.peerId;
|
|
|
|
|
return !(!peerId || rootScope.overlayIsActive || (peerId < 0 && !appChatsManager.hasRights(peerId, 'send_media')));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onDocumentPaste = (e: ClipboardEvent | DragEvent, attachType?: 'media' | 'document') => {
|
|
|
|
|
if(!this.canDrag()) return;
|
|
|
|
|
|
|
|
|
|
//console.log('document paste');
|
|
|
|
|
//console.log('item', event.clipboardData.getData());
|
|
|
|
|
|
|
|
|
|
if(e instanceof DragEvent) {
|
|
|
|
|
const _types = e.dataTransfer.types;
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const isFiles = _types.contains ? _types.contains('Files') : _types.indexOf('Files') >= 0;
|
|
|
|
|
if(isFiles) {
|
|
|
|
|
cancelEvent(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFilesFromEvent(e).then((files: File[]) => {
|
|
|
|
|
if(files.length) {
|
|
|
|
|
if(attachType === 'media' && files.find(file => !['image', 'video'].includes(file.type.split('/')[0]))) {
|
|
|
|
|
attachType = 'document';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chatInput = this.chat.input;
|
|
|
|
|
chatInput.willAttachType = attachType || (files[0].type.indexOf('image/') === 0 ? 'media' : "document");
|
|
|
|
|
new PopupNewMedia(this.chat, files, chatInput.willAttachType);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public selectTab(id: number, animate?: boolean) {
|
|
|
|
|
if(animate === false) { // * will be used for Safari iOS history swipe
|
|
|
|
|
disableTransition([appSidebarLeft.sidebarEl, this.columnEl, appSidebarRight.sidebarEl]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id === 0);
|
|
|
|
|
|
|
|
|
|
const prevTabId = this.tabId;
|
|
|
|
|
|
|
|
|
|
this.log('selectTab', id, prevTabId);
|
|
|
|
|
|
|
|
|
|
let animationPromise: Promise<any> = doubleRaf();
|
|
|
|
|
if(prevTabId !== -1 && prevTabId !== id && rootScope.settings.animationsEnabled && animate !== false) {
|
|
|
|
|
const transitionTime = (mediaSizes.isMobile ? 250 : 200) + 100; // * cause transition time could be > 250ms
|
|
|
|
|
animationPromise = pause(transitionTime);
|
|
|
|
|
dispatchHeavyAnimationEvent(animationPromise, transitionTime);
|
|
|
|
|
|
|
|
|
|
this.columnEl.classList.add('disable-hover');
|
|
|
|
|
animationPromise.finally(() => {
|
|
|
|
|
this.columnEl.classList.remove('disable-hover');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.tabId = id;
|
|
|
|
|
blurActiveElement();
|
|
|
|
|
if(mediaSizes.isMobile && prevTabId === 2 && id < 2) {
|
|
|
|
|
document.body.classList.remove(RIGHT_COLUMN_ACTIVE_CLASSNAME);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(prevTabId !== -1 && id > prevTabId) {
|
|
|
|
|
if(id < 2 || !appNavigationController.findItemByType('im')) {
|
|
|
|
|
appNavigationController.pushItem({
|
|
|
|
|
type: 'im',
|
|
|
|
|
onPop: (canAnimate) => {
|
|
|
|
|
//this.selectTab(prevTabId, !isSafari);
|
|
|
|
|
this.setPeer(0, undefined, canAnimate);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rootScope.dispatchEvent('im_tab_change', id);
|
|
|
|
|
|
|
|
|
|
//this._selectTab(id, mediaSizes.isMobile);
|
|
|
|
|
//document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, id === 2);
|
|
|
|
|
|
|
|
|
|
return animationPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public updateStatus() {
|
|
|
|
|
if(!this.myId) return Promise.resolve();
|
|
|
|
|
|
|
|
|
|
appUsersManager.setUserStatus(this.myId, this.offline);
|
|
|
|
|
return apiManager.invokeApiSingle('account.updateStatus', {offline: this.offline});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private createNewChat() {
|
|
|
|
|
const chat = new Chat(this,
|
|
|
|
|
appChatsManager,
|
|
|
|
|
appDocsManager,
|
|
|
|
|
appInlineBotsManager,
|
|
|
|
|
appMessagesManager,
|
|
|
|
|
appPeersManager,
|
|
|
|
|
appPhotosManager,
|
|
|
|
|
appProfileManager,
|
|
|
|
|
appStickersManager,
|
|
|
|
|
appUsersManager,
|
|
|
|
|
appWebPagesManager,
|
|
|
|
|
appPollsManager,
|
|
|
|
|
apiManager,
|
|
|
|
|
appDraftsManager,
|
|
|
|
|
serverTimeManager,
|
|
|
|
|
stateStorage,
|
|
|
|
|
appNotificationsManager,
|
|
|
|
|
appEmojiManager
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if(this.chats.length) {
|
|
|
|
|
chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.chats.push(chat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private spliceChats(fromIndex: number, justReturn = true, animate?: boolean, spliced?: Chat[]) {
|
|
|
|
|
if(fromIndex >= this.chats.length) return;
|
|
|
|
|
|
|
|
|
|
if(this.chats.length > 1 && justReturn) {
|
|
|
|
|
rootScope.dispatchEvent('peer_changing', this.chat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!spliced) {
|
|
|
|
|
spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// * -1 because one item is being sliced when closing the chat by calling .removeByType
|
|
|
|
|
for(let i = 0; i < spliced.length - 1; ++i) {
|
|
|
|
|
appNavigationController.removeByType('chat', true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// * fix middle chat z-index on animation
|
|
|
|
|
if(spliced.length > 1) {
|
|
|
|
|
spliced.slice(0, -1).forEach(chat => {
|
|
|
|
|
chat.container.remove();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.chatsSelectTab(this.chat.container, animate);
|
|
|
|
|
|
|
|
|
|
if(justReturn) {
|
|
|
|
|
rootScope.dispatchEvent('peer_changed', this.chat.peerId);
|
|
|
|
|
|
|
|
|
|
const searchTab = appSidebarRight.getTab(AppPrivateSearchTab);
|
|
|
|
|
if(searchTab) {
|
|
|
|
|
searchTab.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isSet = appSidebarRight.sharedMediaTab.setPeer(this.chat.peerId, this.chat.threadId);
|
|
|
|
|
if(isSet) {
|
|
|
|
|
appSidebarRight.sharedMediaTab.loadSidebarMedia(true);
|
|
|
|
|
appSidebarRight.sharedMediaTab.fillProfileElements();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* setTimeout(() => {
|
|
|
|
|
appSidebarRight.sharedMediaTab.loadSidebarMedia(false);
|
|
|
|
|
}); */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spliced.forEach(chat => {
|
|
|
|
|
chat.beforeDestroy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
//chat.setPeer(0);
|
|
|
|
|
spliced.forEach(chat => {
|
|
|
|
|
chat.destroy();
|
|
|
|
|
});
|
|
|
|
|
}, 250 + 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setPeer(peerId: number, lastMsgId?: number, animate?: boolean): boolean {
|
|
|
|
|
if(this.init) {
|
|
|
|
|
this.init();
|
|
|
|
|
this.init = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chat = this.chat;
|
|
|
|
|
const chatIndex = this.chats.indexOf(chat);
|
|
|
|
|
|
|
|
|
|
if(!peerId) {
|
|
|
|
|
if(chatIndex > 0) {
|
|
|
|
|
this.spliceChats(chatIndex, undefined, animate);
|
|
|
|
|
return;
|
|
|
|
|
} else if(mediaSizes.activeScreen === ScreenSize.medium) { // * floating sidebar case
|
|
|
|
|
this.selectTab(+!this.tabId, animate);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else if(chatIndex > 0 && chat.peerId && chat.peerId !== peerId) {
|
|
|
|
|
// const firstChat = this.chats[0];
|
|
|
|
|
// if(firstChat.peerId !== chat.peerId) {
|
|
|
|
|
/* // * slice idx > 0, set background and slice first, so new one will be the first
|
|
|
|
|
const spliced = this.chats.splice(1, this.chats.length - 1);
|
|
|
|
|
this.createNewChat();
|
|
|
|
|
this.chats.splice(0, 1); */
|
|
|
|
|
const spliced = this.chats.splice(1, this.chats.length - 1);
|
|
|
|
|
if(this.chat.peerId === peerId) {
|
|
|
|
|
this.spliceChats(0, true, true, spliced);
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
const ret = this.setPeer(peerId, lastMsgId);
|
|
|
|
|
this.spliceChats(0, false, false, spliced);
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
// } else {
|
|
|
|
|
// this.spliceChats(1, false, animate);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
//return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// * don't reset peer if returning
|
|
|
|
|
if(peerId === chat.peerId && mediaSizes.activeScreen <= ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) {
|
|
|
|
|
this.selectTab(1, animate);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(peerId || mediaSizes.activeScreen !== ScreenSize.mobile) {
|
|
|
|
|
const result = chat.setPeer(peerId, lastMsgId);
|
|
|
|
|
|
|
|
|
|
// * wait for cached render
|
|
|
|
|
const promise = result?.cached ? result.promise : Promise.resolve();
|
|
|
|
|
if(peerId) {
|
|
|
|
|
promise.then(() => {
|
|
|
|
|
//window.requestAnimationFrame(() => {
|
|
|
|
|
setTimeout(() => { // * setTimeout is better here
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.chatsSelectTab(this.chat.container);
|
|
|
|
|
}, 0);
|
|
|
|
|
this.selectTab(1, animate);
|
|
|
|
|
}, 0);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!peerId) {
|
|
|
|
|
this.selectTab(0, animate);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setInnerPeer(peerId: number, lastMsgId?: number, type: ChatType = 'chat', threadId?: number) {
|
|
|
|
|
// * prevent opening already opened peer
|
|
|
|
|
const existingIndex = this.chats.findIndex(chat => chat.peerId === peerId && chat.type === type);
|
|
|
|
|
if(existingIndex !== -1) {
|
|
|
|
|
this.spliceChats(existingIndex + 1);
|
|
|
|
|
return this.setPeer(peerId, lastMsgId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chat = this.chat;
|
|
|
|
|
if(!chat.init) { // * use first not inited chat
|
|
|
|
|
this.createNewChat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(type) {
|
|
|
|
|
this.chat.setType(type);
|
|
|
|
|
|
|
|
|
|
if(threadId) {
|
|
|
|
|
this.chat.threadId = threadId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//this.chatsSelectTab(this.chat.container);
|
|
|
|
|
|
|
|
|
|
return this.setPeer(peerId, lastMsgId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public openScheduled(peerId: number) {
|
|
|
|
|
this.setInnerPeer(peerId, undefined, 'scheduled');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getTypingElement(action: SendMessageAction) {
|
|
|
|
|
const el = document.createElement('span');
|
|
|
|
|
el.classList.add('peer-typing');
|
|
|
|
|
el.dataset.action = action._;
|
|
|
|
|
switch(action._) {
|
|
|
|
|
case 'sendMessageTypingAction': {
|
|
|
|
|
//default: {
|
|
|
|
|
const c = 'peer-typing-text';
|
|
|
|
|
el.classList.add(c);
|
|
|
|
|
for(let i = 0; i < 3; ++i) {
|
|
|
|
|
const dot = document.createElement('span');
|
|
|
|
|
dot.className = c + '-dot';
|
|
|
|
|
el.append(dot);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'sendMessageUploadAudioAction':
|
|
|
|
|
case 'sendMessageUploadDocumentAction':
|
|
|
|
|
case 'sendMessageUploadRoundAction':
|
|
|
|
|
case 'sendMessageUploadVideoAction':
|
|
|
|
|
case 'sendMessageUploadPhotoAction': {
|
|
|
|
|
const c = 'peer-typing-upload';
|
|
|
|
|
el.classList.add(c);
|
|
|
|
|
/* const trail = document.createElement('span');
|
|
|
|
|
trail.className = c + '-trail';
|
|
|
|
|
el.append(trail); */
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'sendMessageRecordAudioAction':
|
|
|
|
|
case 'sendMessageRecordRoundAction':
|
|
|
|
|
case 'sendMessageRecordVideoAction': {
|
|
|
|
|
const c = 'peer-typing-record';
|
|
|
|
|
el.classList.add(c);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getPeerTyping(peerId: number, container?: HTMLElement) {
|
|
|
|
|
if(!appUsersManager.isBot(peerId)) {
|
|
|
|
|
const typings = appChatsManager.getPeerTypings(peerId);
|
|
|
|
|
if(!typings || !typings.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const typing = typings[0];
|
|
|
|
|
|
|
|
|
|
const langPackKeys: {
|
|
|
|
|
[peerType in 'private' | 'chat' | 'multi']?: Partial<{[action in SendMessageAction['_']]: LangPackKey}>
|
|
|
|
|
} = {
|
|
|
|
|
private: {
|
|
|
|
|
'sendMessageTypingAction': 'Peer.Activity.User.TypingText',
|
|
|
|
|
'sendMessageUploadAudioAction': 'Peer.Activity.User.SendingFile',
|
|
|
|
|
'sendMessageUploadDocumentAction': 'Peer.Activity.User.SendingFile',
|
|
|
|
|
'sendMessageUploadPhotoAction': 'Peer.Activity.User.SendingPhoto',
|
|
|
|
|
'sendMessageUploadVideoAction': 'Peer.Activity.User.SendingVideo',
|
|
|
|
|
'sendMessageUploadRoundAction': 'Peer.Activity.User.SendingVideo',
|
|
|
|
|
'sendMessageRecordVideoAction': 'Peer.Activity.User.RecordingVideo',
|
|
|
|
|
'sendMessageRecordAudioAction': 'Peer.Activity.User.RecordingAudio',
|
|
|
|
|
'sendMessageRecordRoundAction': 'Peer.Activity.User.RecordingVideo',
|
|
|
|
|
'sendMessageGamePlayAction': 'Peer.Activity.User.PlayingGame'
|
|
|
|
|
},
|
|
|
|
|
chat: {
|
|
|
|
|
'sendMessageTypingAction': 'Peer.Activity.Chat.TypingText',
|
|
|
|
|
'sendMessageUploadAudioAction': 'Peer.Activity.Chat.SendingFile',
|
|
|
|
|
'sendMessageUploadDocumentAction': 'Peer.Activity.Chat.SendingFile',
|
|
|
|
|
'sendMessageUploadPhotoAction': 'Peer.Activity.Chat.SendingPhoto',
|
|
|
|
|
'sendMessageUploadVideoAction': 'Peer.Activity.Chat.SendingVideo',
|
|
|
|
|
'sendMessageUploadRoundAction': 'Peer.Activity.Chat.SendingVideo',
|
|
|
|
|
'sendMessageRecordVideoAction': 'Peer.Activity.Chat.RecordingVideo',
|
|
|
|
|
'sendMessageRecordAudioAction': 'Peer.Activity.Chat.RecordingAudio',
|
|
|
|
|
'sendMessageRecordRoundAction': 'Peer.Activity.Chat.RecordingVideo',
|
|
|
|
|
'sendMessageGamePlayAction': 'Peer.Activity.Chat.PlayingGame'
|
|
|
|
|
},
|
|
|
|
|
multi: {
|
|
|
|
|
'sendMessageTypingAction': 'Peer.Activity.Chat.Multi.TypingText1',
|
|
|
|
|
'sendMessageUploadAudioAction': 'Peer.Activity.Chat.Multi.SendingFile1',
|
|
|
|
|
'sendMessageUploadDocumentAction': 'Peer.Activity.Chat.Multi.SendingFile1',
|
|
|
|
|
'sendMessageUploadPhotoAction': 'Peer.Activity.Chat.Multi.SendingPhoto1',
|
|
|
|
|
'sendMessageUploadVideoAction': 'Peer.Activity.Chat.Multi.SendingVideo1',
|
|
|
|
|
'sendMessageUploadRoundAction': 'Peer.Activity.Chat.Multi.SendingVideo1',
|
|
|
|
|
'sendMessageRecordVideoAction': 'Peer.Activity.Chat.Multi.RecordingVideo1',
|
|
|
|
|
'sendMessageRecordAudioAction': 'Peer.Activity.Chat.Multi.RecordingAudio1',
|
|
|
|
|
'sendMessageRecordRoundAction': 'Peer.Activity.Chat.Multi.RecordingVideo1',
|
|
|
|
|
'sendMessageGamePlayAction': 'Peer.Activity.Chat.Multi.PlayingGame1'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mapa = peerId > 0 ? langPackKeys.private : (typings.length > 1 ? langPackKeys.multi : langPackKeys.chat);
|
|
|
|
|
let action = typing.action;
|
|
|
|
|
|
|
|
|
|
if(typings.length > 1) {
|
|
|
|
|
const s: any = {};
|
|
|
|
|
typings.forEach(typing => {
|
|
|
|
|
const type = typing.action._;
|
|
|
|
|
if(s[type] === undefined) s[type] = 0;
|
|
|
|
|
++s[type];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if(Object.keys(s).length > 1) {
|
|
|
|
|
action = {
|
|
|
|
|
_: 'sendMessageTypingAction'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const langPackKey = mapa[action._];
|
|
|
|
|
if(!langPackKey) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!container) {
|
|
|
|
|
container = document.createElement('span');
|
|
|
|
|
container.classList.add('online', 'peer-typing-container');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let typingElement = container.firstElementChild as HTMLElement;
|
|
|
|
|
if(!typingElement) {
|
|
|
|
|
typingElement = this.getTypingElement(action);
|
|
|
|
|
container.prepend(typingElement);
|
|
|
|
|
} else {
|
|
|
|
|
if(typingElement.dataset.action !== action._) {
|
|
|
|
|
typingElement.replaceWith(this.getTypingElement(action));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let args: any[];
|
|
|
|
|
if(peerId < 0) {
|
|
|
|
|
args = [
|
|
|
|
|
new PeerTitle({peerId: typing.userId, onlyFirstName: true}).element,
|
|
|
|
|
typings.length - 1
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
const descriptionElement = i18n(langPackKey, args);
|
|
|
|
|
descriptionElement.classList.add('peer-typing-description');
|
|
|
|
|
|
|
|
|
|
if(container.childElementCount > 1) container.lastElementChild.replaceWith(descriptionElement);
|
|
|
|
|
else container.append(descriptionElement);
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getPeerStatus(peerId: number) {
|
|
|
|
|
let subtitle: HTMLElement;
|
|
|
|
|
if(!peerId) return '';
|
|
|
|
|
|
|
|
|
|
if(peerId < 0) { // not human
|
|
|
|
|
let span = this.getPeerTyping(peerId);
|
|
|
|
|
if(span) {
|
|
|
|
|
return span;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chatInfo = await appProfileManager.getChatFull(-peerId) as any;
|
|
|
|
|
this.chat.log('chatInfo res:', chatInfo);
|
|
|
|
|
|
|
|
|
|
const participants_count = chatInfo.participants_count || (chatInfo.participants && chatInfo.participants.participants && chatInfo.participants.participants.length) || 1;
|
|
|
|
|
//if(participants_count) {
|
|
|
|
|
subtitle = appChatsManager.getChatMembersString(-peerId);
|
|
|
|
|
|
|
|
|
|
if(participants_count < 2) return subtitle;
|
|
|
|
|
/* const onlines = await appChatsManager.getOnlines(chat.id);
|
|
|
|
|
if(onlines > 1) {
|
|
|
|
|
subtitle += ', ' + numberThousandSplitter(onlines) + ' online';
|
|
|
|
|
} */
|
|
|
|
|
|
|
|
|
|
return subtitle;
|
|
|
|
|
//}
|
|
|
|
|
} else { // user
|
|
|
|
|
const user = appUsersManager.getUser(peerId);
|
|
|
|
|
|
|
|
|
|
if(rootScope.myId === peerId) {
|
|
|
|
|
return '';
|
|
|
|
|
} else if(user) {
|
|
|
|
|
subtitle = appUsersManager.getUserStatusString(user.id);
|
|
|
|
|
|
|
|
|
|
if(!appUsersManager.isBot(peerId)) {
|
|
|
|
|
let span = this.getPeerTyping(peerId);
|
|
|
|
|
if(!span && user.status?._ === 'userStatusOnline') {
|
|
|
|
|
span = document.createElement('span');
|
|
|
|
|
span.classList.add('online');
|
|
|
|
|
span.append(subtitle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(span) {
|
|
|
|
|
return span;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return subtitle;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setPeerStatus(peerId: number, element: HTMLElement, needClear: boolean, useWhitespace: boolean, middleware: () => boolean) {
|
|
|
|
|
if(needClear) {
|
|
|
|
|
element.innerHTML = useWhitespace ? '' : ''; // ! HERE U CAN FIND WHITESPACE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// * good good good
|
|
|
|
|
const typingContainer = element.querySelector('.peer-typing-container') as HTMLElement;
|
|
|
|
|
if(typingContainer && this.getPeerTyping(peerId, typingContainer)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.getPeerStatus(peerId).then((subtitle) => {
|
|
|
|
|
if(!middleware()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
replaceContent(element, subtitle || (useWhitespace ? '' : ''));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const appImManager = new AppImManager();
|
|
|
|
|
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appImManager = appImManager);
|
|
|
|
|
export default appImManager;
|