diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index aceed795..2d73f217 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -718,7 +718,7 @@ export default class AppSearchSuper { if(showMembersCount && (peer.participants_count || peer.participants)) { const regExp = new RegExp(`(${escapeRegExp(query)}|${escapeRegExp(cleanSearchText(query))})`, 'gi'); dom.titleSpan.innerHTML = dom.titleSpan.innerHTML.replace(regExp, '$1'); - dom.lastMessageSpan.append(appChatsManager.getChatMembersString(-peerId)); + dom.lastMessageSpan.append(appProfileManager.getChatMembersString(-peerId)); } else if(peerId === rootScope.myId) { dom.lastMessageSpan.append(i18n('Presence.YourChat')); } else { @@ -811,7 +811,7 @@ export default class AppSearchSuper { autonomous: true }); - dom.lastMessageSpan.append(peerId > 0 ? appUsersManager.getUserStatusString(peerId) : appChatsManager.getChatMembersString(peerId)); + dom.lastMessageSpan.append(peerId > 0 ? appUsersManager.getUserStatusString(peerId) : appProfileManager.getChatMembersString(-peerId)); }); if(!state.recentSearch.length) { diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 8cf85a6e..921a87f9 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -447,7 +447,7 @@ export default class AppSelectPeers { let subtitleEl: HTMLElement; if(peerId < 0) { - subtitleEl = appChatsManager.getChatMembersString(-peerId); + subtitleEl = appProfileManager.getChatMembersString(-peerId); } else if(peerId === rootScope.myId) { subtitleEl = i18n('Presence.YourChat'); } else { diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 642ee807..0f4f10c1 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -14,9 +14,10 @@ import appPhotosManager from "../lib/appManagers/appPhotosManager"; import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; import { attachClickEvent } from "../helpers/dom/clickEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent"; +import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; const onAvatarUpdate = (peerId: number) => { - appProfileManager.removeFromAvatarsCache(peerId); + appAvatarsManager.removeFromAvatarsCache(peerId); (Array.from(document.querySelectorAll('avatar-element[peer="' + peerId + '"]')) as AvatarElement[]).forEach(elem => { //console.log('updating avatar:', elem); elem.update(); @@ -180,7 +181,7 @@ export default class AvatarElement extends HTMLElement { } private r(onlyThumb = false) { - const res = appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb); + const res = appAvatarsManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb); const promise = res ? res.loadPromise : Promise.resolve(); if(this.loadPromises) { if(res && res.cached) { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 0adf3e3e..cbf97dc8 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -48,6 +48,7 @@ import ButtonCorner from "../../buttonCorner"; import { cancelEvent } from "../../../helpers/dom/cancelEvent"; import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import replaceContent from "../../../helpers/dom/replaceContent"; +import appAvatarsManager from "../../../lib/appManagers/appAvatarsManager"; let setText = (text: string, row: Row) => { //fastRaf(() => { @@ -437,7 +438,7 @@ class PeerProfileAvatars { }); } else { const photo = appPeersManager.getPeerPhoto(this.peerId); - appProfileManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); + appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); } this.avatars.append(avatar); diff --git a/src/lib/appManagers/appAvatarsManager.ts b/src/lib/appManagers/appAvatarsManager.ts new file mode 100644 index 00000000..d9f5a7dd --- /dev/null +++ b/src/lib/appManagers/appAvatarsManager.ts @@ -0,0 +1,202 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; +import replaceContent from "../../helpers/dom/replaceContent"; +import sequentialDom from "../../helpers/sequentialDom"; +import { UserProfilePhoto, ChatPhoto, InputFileLocation } from "../../layer"; +import RichTextProcessor from "../richtextprocessor"; +import rootScope from "../rootScope"; +import appDownloadManager from "./appDownloadManager"; +import appPeersManager from "./appPeersManager"; +import appPhotosManager from "./appPhotosManager"; +import appUsersManager from "./appUsersManager"; + +type PeerPhotoSize = 'photo_small' | 'photo_big'; + +export class AppAvatarsManager { + private savedAvatarURLs: { + [peerId: number]: { + [size in PeerPhotoSize]?: string | Promise + } + } = {}; + + public removeFromAvatarsCache(peerId: number) { + if(this.savedAvatarURLs[peerId]) { + delete this.savedAvatarURLs[peerId]; + } + } + + public loadAvatar(peerId: number, photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, size: PeerPhotoSize) { + const inputPeer = appPeersManager.getInputPeerById(peerId); + + let cached = false; + let getAvatarPromise: Promise; + let saved = this.savedAvatarURLs[peerId]; + if(!saved || !saved[size]) { + if(!saved) { + saved = this.savedAvatarURLs[peerId] = {}; + } + + //console.warn('will invoke downloadSmallFile:', peerId); + const peerPhotoFileLocation: InputFileLocation.inputPeerPhotoFileLocation = { + _: 'inputPeerPhotoFileLocation', + pFlags: {}, + peer: inputPeer, + photo_id: photo.photo_id + }; + + if(size === 'photo_big') { + peerPhotoFileLocation.pFlags.big = true; + } + + const downloadOptions = {dcId: photo.dc_id, location: peerPhotoFileLocation}; + + /* let str: string; + const time = Date.now(); + if(peerId === 0) { + str = `download avatar ${peerId}`; + } */ + + const promise = appDownloadManager.download(downloadOptions); + getAvatarPromise = saved[size] = promise.then(blob => { + return saved[size] = URL.createObjectURL(blob); + + /* if(str) { + console.log(str, Date.now() / 1000, Date.now() - time); + } */ + }); + } else if(typeof(saved[size]) !== 'string') { + getAvatarPromise = saved[size] as Promise; + } else { + getAvatarPromise = Promise.resolve(saved[size]); + cached = true; + } + + return {cached, loadPromise: getAvatarPromise}; + } + + public putAvatar(div: HTMLElement, peerId: number, photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, size: PeerPhotoSize, img = new Image(), onlyThumb = false) { + let {cached, loadPromise} = this.loadAvatar(peerId, photo, size); + + let renderThumbPromise: Promise; + let callback: () => void; + if(cached) { + // смотри в misc.ts: renderImageFromUrl + callback = () => { + replaceContent(div, img); + div.dataset.color = ''; + }; + } else { + const animate = rootScope.settings.animationsEnabled; + if(animate) { + img.classList.add('fade-in'); + } + + let thumbImage: HTMLImageElement; + if(photo.stripped_thumb) { + thumbImage = new Image(); + div.classList.add('avatar-relative'); + thumbImage.classList.add('avatar-photo', 'avatar-photo-thumbnail'); + img.classList.add('avatar-photo'); + const url = appPhotosManager.getPreviewURLFromBytes(photo.stripped_thumb); + renderThumbPromise = renderImageFromUrl(thumbImage, url).then(() => { + replaceContent(div, thumbImage); + }); + } + + callback = () => { + if(photo.stripped_thumb) { + div.append(img); + } else { + replaceContent(div, img); + } + + setTimeout(() => { + if(div.childElementCount) { + sequentialDom.mutateElement(img, () => { + div.dataset.color = ''; + + if(animate) { + img.classList.remove('fade-in'); + } + + if(thumbImage) { + thumbImage.remove(); + } + }); + } + }, animate ? 200 : 0); + }; + } + + const renderPromise = loadPromise + .then((url) => renderImageFromUrl(img, url/* , false */)) + .then(() => callback()); + + return {cached, loadPromise: renderThumbPromise || renderPromise}; + } + + // peerId === peerId || title + public putPhoto(div: HTMLElement, peerId: number, isDialog = false, title = '', onlyThumb = false) { + const photo = appPeersManager.getPeerPhoto(peerId); + + const size: PeerPhotoSize = 'photo_small'; + const avatarAvailable = !!photo; + const avatarRendered = div.firstElementChild && !(div.firstElementChild as HTMLElement).classList.contains('emoji'); + + const myId = rootScope.myId; + + //console.log('loadDialogPhoto location:', location, inputPeer); + if(peerId === myId && isDialog) { + div.innerText = ''; + div.dataset.color = ''; + div.classList.add('tgico-saved'); + div.classList.remove('tgico-deletedaccount'); + return; + } + + if(peerId > 0) { + const user = appUsersManager.getUser(peerId); + if(user && user.pFlags && user.pFlags.deleted) { + div.innerText = ''; + div.dataset.color = appPeersManager.getPeerColorById(peerId); + div.classList.add('tgico-deletedaccount'); + div.classList.remove('tgico-saved'); + return; + } + } + + if(!avatarAvailable || !avatarRendered || !this.savedAvatarURLs[peerId]) { + let color = ''; + if(peerId && (peerId !== myId || !isDialog)) { + color = appPeersManager.getPeerColorById(peerId); + } + + div.innerText = ''; + div.classList.remove('tgico-saved', 'tgico-deletedaccount'); + div.dataset.color = color; + + let abbr: string; + if(!title) { + const peer = appPeersManager.getPeer(peerId); + abbr = peer.initials ?? ''; + } else { + abbr = RichTextProcessor.getAbbreviation(title); + } + + div.innerHTML = abbr; + //return Promise.resolve(true); + } + + if(avatarAvailable/* && false */) { + return this.putAvatar(div, peerId, photo, size, undefined, onlyThumb); + } + } +} + +const appAvatarsManager = new AppAvatarsManager(); +export default appAvatarsManager; diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index e6b9aa0b..f5916ae6 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -10,17 +10,14 @@ */ import { MOUNT_CLASS_TO } from "../../config/debug"; -import { numberThousandSplitter } from "../../helpers/number"; import { isObject, safeReplaceObject, copy, deepEqual } from "../../helpers/object"; -import { ChannelParticipant, Chat, ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipant, ChatParticipants, ChatPhoto, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Update, Updates } from "../../layer"; -import { i18n, LangPackKey } from "../langPack"; +import { ChannelParticipant, Chat, ChatAdminRights, ChatBannedRights, ChatParticipant, ChatPhoto, InputChannel, InputChatPhoto, InputFile, InputPeer, Update, Updates } from "../../layer"; import apiManagerProxy from "../mtproto/mtprotoworker"; import apiManager from '../mtproto/mtprotoworker'; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import apiUpdatesManager from "./apiUpdatesManager"; import appPeersManager from "./appPeersManager"; -import appProfileManager from "./appProfileManager"; import appStateManager from "./appStateManager"; import appUsersManager from "./appUsersManager"; @@ -28,8 +25,6 @@ export type Channel = Chat.channel; export type ChatRights = keyof ChatBannedRights['pFlags'] | keyof ChatAdminRights['pFlags'] | 'change_type' | 'change_permissions' | 'delete_chat' | 'view_participants'; -export type UserTyping = Partial<{userId: number, action: SendMessageAction, timeout: number}>; - export class AppChatsManager { private storage = appStateManager.storages.chats; @@ -38,10 +33,6 @@ export class AppChatsManager { //private channelAccess: any; //private megagroups: {[id: number]: true}; - private megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}}; - - private typingsInPeer: {[peerId: number]: UserTyping[]}; - constructor() { this.clear(); @@ -65,11 +56,7 @@ export class AppChatsManager { chat.default_banned_rights = update.default_banned_rights; rootScope.dispatchEvent('chat_update', chatId); } - }, - - updateUserTyping: this.onUpdateUserTyping, - updateChatUserTyping: this.onUpdateUserTyping, - updateChannelUserTyping: this.onUpdateUserTyping + } }); appStateManager.getState().then((state) => { @@ -119,81 +106,6 @@ export class AppChatsManager { } else { this.chats = {}; } - - this.megagroupOnlines = {}; - this.typingsInPeer = {}; - } - - private onUpdateUserTyping = (update: Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChannelUserTyping) => { - const fromId = (update as Update.updateUserTyping).user_id || appPeersManager.getPeerId((update as Update.updateChatUserTyping).from_id); - if(rootScope.myId === fromId || update.action._ === 'speakingInGroupCallAction') { - return; - } - - const peerId = update._ === 'updateUserTyping' ? - fromId : - -((update as Update.updateChatUserTyping).chat_id || (update as Update.updateChannelUserTyping).channel_id); - const typings = this.typingsInPeer[peerId] ?? (this.typingsInPeer[peerId] = []); - let typing = typings.find(t => t.userId === fromId); - - const cancelAction = () => { - delete typing.timeout; - //typings.findAndSplice(t => t === typing); - const idx = typings.indexOf(typing); - if(idx !== -1) { - typings.splice(idx, 1); - } - - rootScope.dispatchEvent('peer_typings', {peerId, typings}); - - if(!typings.length) { - delete this.typingsInPeer[peerId]; - } - }; - - if(typing && typing.timeout !== undefined) { - clearTimeout(typing.timeout); - } - - if(update.action._ === 'sendMessageCancelAction') { - if(!typing) { - return; - } - - cancelAction(); - return; - } else { - if(!typing) { - typing = { - userId: fromId - }; - - typings.push(typing); - } - - //console.log('updateChatUserTyping', update, typings); - - typing.action = update.action; - - if(!appUsersManager.hasUser(fromId)) { - if(update._ === 'updateChatUserTyping') { - if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) { - appProfileManager.getChatFull(update.chat_id); - } - } - - //return; - } - - appUsersManager.forceUserOnline(fromId); - - typing.timeout = window.setTimeout(cancelAction, 6000); - rootScope.dispatchEvent('peer_typings', {peerId, typings}); - } - }; - - public getPeerTypings(peerId: number) { - return this.typingsInPeer[peerId]; } public saveApiChats(apiChats: any[], override?: boolean) { @@ -486,27 +398,6 @@ export class AppChatsManager { return 'g' + id; } - public getChatMembersString(id: number) { - const chat = this.getChat(id); - const chatFull = appProfileManager.chatsFull[id]; - let count: number; - if(chatFull) { - if(chatFull._ === 'channelFull') { - count = chatFull.participants_count; - } else { - count = (chatFull.participants as ChatParticipants.chatParticipants).participants?.length; - } - } else { - count = chat.participants_count || chat.participants?.participants.length; - } - - const isChannel = this.isBroadcast(id); - count = count || 1; - - let key: LangPackKey = isChannel ? 'Peer.Status.Subscribers' : 'Peer.Status.Member'; - return i18n(key, [numberThousandSplitter(count)]); - } - /* public wrapForFull(id: number, fullChat: any) { const chatFull = copy(fullChat); const chat = this.getChat(id); @@ -600,45 +491,6 @@ export class AppChatsManager { }); } - public async getOnlines(id: number): Promise { - if(this.isMegagroup(id)) { - const timestamp = Date.now() / 1000 | 0; - const cached = this.megagroupOnlines[id] ?? (this.megagroupOnlines[id] = {timestamp: 0, onlines: 1}); - if((timestamp - cached.timestamp) < 60) { - return cached.onlines; - } - - const res = await apiManager.invokeApi('messages.getOnlines', { - peer: this.getChannelInputPeer(id) - }); - - const onlines = res.onlines ?? 1; - cached.timestamp = timestamp; - cached.onlines = onlines; - - return onlines; - } else if(this.isBroadcast(id)) { - return 1; - } - - const chatInfo = await appProfileManager.getChatFull(id); - const _participants = (chatInfo as ChatFull.chatFull).participants as ChatParticipants.chatParticipants; - if(_participants && _participants.participants) { - const participants = _participants.participants; - - return participants.reduce((acc: number, participant) => { - const user = appUsersManager.getUser(participant.user_id); - if(user && user.status && user.status._ === 'userStatusOnline') { - return acc + 1; - } - - return acc; - }, 0); - } else { - return 1; - } - } - private onChatUpdated = (chatId: number, updates: any) => { //console.log('onChatUpdated', chatId, updates); @@ -647,7 +499,7 @@ export class AppChatsManager { /* updates.updates && updates.updates.length && */ this.isChannel(chatId)) { - appProfileManager.invalidateChannelParticipants(chatId); + rootScope.dispatchEvent('invalidate_participants', chatId); } }; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 1789cce5..408d7eed 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -103,6 +103,7 @@ export class AppImManager { constructor() { apiUpdatesManager.attach(); + appNotificationsManager.start(); this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error); @@ -1035,7 +1036,7 @@ export class AppImManager { public getPeerTyping(peerId: number, container?: HTMLElement) { if(!appUsersManager.isBot(peerId)) { - const typings = appChatsManager.getPeerTypings(peerId); + const typings = appProfileManager.getPeerTypings(peerId); if(!typings || !typings.length) { return; } @@ -1152,7 +1153,7 @@ export class AppImManager { const participants_count = chatInfo.participants_count || (chatInfo.participants && chatInfo.participants.participants && chatInfo.participants.participants.length) || 1; //if(participants_count) { - subtitle = appChatsManager.getChatMembersString(-peerId); + subtitle = appProfileManager.getChatMembersString(-peerId); if(participants_count < 2) return subtitle; /* const onlines = await appChatsManager.getOnlines(chat.id); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 531eadcd..1a4d1d63 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -53,6 +53,7 @@ import htmlToDocumentFragment from "../../helpers/dom/htmlToDocumentFragment"; import htmlToSpan from "../../helpers/dom/htmlToSpan"; import { REPLIES_PEER_ID } from "../mtproto/mtproto_config"; import formatCallDuration from "../../helpers/formatCallDuration"; +import appAvatarsManager from "./appAvatarsManager"; //console.trace('include'); // TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет @@ -304,8 +305,6 @@ export class AppMessagesManager { this.maxSeenId = state.maxSeenMsgId; } }); - - appNotificationsManager.start(); } public construct() { @@ -4546,7 +4545,7 @@ export class AppMessagesManager { const peerPhoto = appPeersManager.getPeerPhoto(peerId); if(peerPhoto) { - appProfileManager.loadAvatar(peerId, peerPhoto, 'photo_small').loadPromise.then(url => { + appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small').loadPromise.then(url => { if(message.pFlags.unread) { notification.image = url; appNotificationsManager.notify(notification); diff --git a/src/lib/appManagers/appNotificationsManager.ts b/src/lib/appManagers/appNotificationsManager.ts index 25f8fea2..d60f4bdf 100644 --- a/src/lib/appManagers/appNotificationsManager.ts +++ b/src/lib/appManagers/appNotificationsManager.ts @@ -706,7 +706,6 @@ export class AppNotificationsManager { } private registerDevice(tokenData: PushSubscriptionNotify) { - return; if(this.registeredDevice && deepEqual(this.registeredDevice, tokenData)) { return false; } @@ -725,7 +724,6 @@ export class AppNotificationsManager { } private unregisterDevice(tokenData: PushSubscriptionNotify) { - return; if(!this.registeredDevice) { return false; } diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index 901e7fd7..25305f5a 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -11,10 +11,9 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import { tsNow } from "../../helpers/date"; -import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; -import replaceContent from "../../helpers/dom/replaceContent"; -import sequentialDom from "../../helpers/sequentialDom"; -import { ChannelParticipantsFilter, ChannelsChannelParticipants, Chat, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, InputFileLocation, PhotoSize, Update, UserFull, UserProfilePhoto } from "../../layer"; +import { numberThousandSplitter } from "../../helpers/number"; +import { ChannelParticipantsFilter, ChannelsChannelParticipants, Chat, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, InputFileLocation, PhotoSize, SendMessageAction, Update, UserFull, UserProfilePhoto } from "../../layer"; +import { LangPackKey, i18n } from "../langPack"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; import { RichTextProcessor } from "../richtextprocessor"; @@ -22,13 +21,12 @@ import rootScope from "../rootScope"; import SearchIndex from "../searchIndex"; import apiUpdatesManager from "./apiUpdatesManager"; import appChatsManager from "./appChatsManager"; -import appDownloadManager from "./appDownloadManager"; import appNotificationsManager from "./appNotificationsManager"; import appPeersManager from "./appPeersManager"; -import appPhotosManager, { MyPhoto } from "./appPhotosManager"; +import appPhotosManager from "./appPhotosManager"; import appUsersManager, { User } from "./appUsersManager"; -type PeerPhotoSize = 'photo_small' | 'photo_big'; +export type UserTyping = Partial<{userId: number, action: SendMessageAction, timeout: number}>; export class AppProfileManager { //private botInfos: any = {}; @@ -36,11 +34,9 @@ export class AppProfileManager { public chatsFull: {[id: string]: ChatFull} = {}; private fullPromises: {[peerId: string]: Promise} = {}; - private savedAvatarURLs: { - [peerId: number]: { - [size in PeerPhotoSize]?: string | Promise - } - } = {}; + private megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}}; + + private typingsInPeer: {[peerId: number]: UserTyping[]}; constructor() { rootScope.addMultipleEventsListeners({ @@ -93,7 +89,11 @@ export class AppProfileManager { } } } - } + }, + + updateUserTyping: this.onUpdateUserTyping, + updateChatUserTyping: this.onUpdateUserTyping, + updateChannelUserTyping: this.onUpdateUserTyping }); rootScope.addEventListener('chat_update', (chatId) => { @@ -121,6 +121,13 @@ export class AppProfileManager { rootScope.dispatchEvent('chat_full_update', chatId); } }); + + rootScope.addEventListener('invalidate_participants', chatId => { + this.invalidateChannelParticipants(chatId); + }); + + this.megagroupOnlines = {}; + this.typingsInPeer = {}; } /* public saveBotInfo(botInfo: any) { @@ -457,177 +464,144 @@ export class AppProfileManager { }); } - public removeFromAvatarsCache(peerId: number) { - if(this.savedAvatarURLs[peerId]) { - delete this.savedAvatarURLs[peerId]; + public getChatMembersString(id: number) { + const chat = appChatsManager.getChat(id); + const chatFull = this.chatsFull[id]; + let count: number; + if(chatFull) { + if(chatFull._ === 'channelFull') { + count = chatFull.participants_count; + } else { + count = (chatFull.participants as ChatParticipants.chatParticipants).participants?.length; + } + } else { + count = chat.participants_count || chat.participants?.participants.length; } - } - public loadAvatar(peerId: number, photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, size: PeerPhotoSize) { - const inputPeer = appPeersManager.getInputPeerById(peerId); + const isChannel = appChatsManager.isBroadcast(id); + count = count || 1; + + let key: LangPackKey = isChannel ? 'Peer.Status.Subscribers' : 'Peer.Status.Member'; + return i18n(key, [numberThousandSplitter(count)]); + } - let cached = false; - let getAvatarPromise: Promise; - let saved = this.savedAvatarURLs[peerId]; - if(!saved || !saved[size]) { - if(!saved) { - saved = this.savedAvatarURLs[peerId] = {}; + public async getOnlines(id: number): Promise { + if(appChatsManager.isMegagroup(id)) { + const timestamp = Date.now() / 1000 | 0; + const cached = this.megagroupOnlines[id] ?? (this.megagroupOnlines[id] = {timestamp: 0, onlines: 1}); + if((timestamp - cached.timestamp) < 60) { + return cached.onlines; } - //console.warn('will invoke downloadSmallFile:', peerId); - const peerPhotoFileLocation: InputFileLocation.inputPeerPhotoFileLocation = { - _: 'inputPeerPhotoFileLocation', - pFlags: {}, - peer: inputPeer, - photo_id: photo.photo_id - }; + const res = await apiManager.invokeApi('messages.getOnlines', { + peer: appChatsManager.getChannelInputPeer(id) + }); - if(size === 'photo_big') { - peerPhotoFileLocation.pFlags.big = true; - } + const onlines = res.onlines ?? 1; + cached.timestamp = timestamp; + cached.onlines = onlines; - const downloadOptions = {dcId: photo.dc_id, location: peerPhotoFileLocation}; + return onlines; + } else if(appChatsManager.isBroadcast(id)) { + return 1; + } - /* let str: string; - const time = Date.now(); - if(peerId === 0) { - str = `download avatar ${peerId}`; - } */ + const chatInfo = await this.getChatFull(id); + const _participants = (chatInfo as ChatFull.chatFull).participants as ChatParticipants.chatParticipants; + if(_participants && _participants.participants) { + const participants = _participants.participants; - const promise = appDownloadManager.download(downloadOptions); - getAvatarPromise = saved[size] = promise.then(blob => { - return saved[size] = URL.createObjectURL(blob); + return participants.reduce((acc: number, participant) => { + const user = appUsersManager.getUser(participant.user_id); + if(user && user.status && user.status._ === 'userStatusOnline') { + return acc + 1; + } - /* if(str) { - console.log(str, Date.now() / 1000, Date.now() - time); - } */ - }); - } else if(typeof(saved[size]) !== 'string') { - getAvatarPromise = saved[size] as Promise; + return acc; + }, 0); } else { - getAvatarPromise = Promise.resolve(saved[size]); - cached = true; + return 1; } - - return {cached, loadPromise: getAvatarPromise}; } - public putAvatar(div: HTMLElement, peerId: number, photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, size: PeerPhotoSize, img = new Image(), onlyThumb = false) { - let {cached, loadPromise} = this.loadAvatar(peerId, photo, size); - - let renderThumbPromise: Promise; - let callback: () => void; - if(cached) { - // смотри в misc.ts: renderImageFromUrl - callback = () => { - replaceContent(div, img); - div.dataset.color = ''; - }; - } else { - const animate = rootScope.settings.animationsEnabled; - if(animate) { - img.classList.add('fade-in'); + private onUpdateUserTyping = (update: Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChannelUserTyping) => { + const fromId = (update as Update.updateUserTyping).user_id || appPeersManager.getPeerId((update as Update.updateChatUserTyping).from_id); + if(rootScope.myId === fromId || update.action._ === 'speakingInGroupCallAction') { + return; + } + + const peerId = update._ === 'updateUserTyping' ? + fromId : + -((update as Update.updateChatUserTyping).chat_id || (update as Update.updateChannelUserTyping).channel_id); + const typings = this.typingsInPeer[peerId] ?? (this.typingsInPeer[peerId] = []); + let typing = typings.find(t => t.userId === fromId); + + const cancelAction = () => { + delete typing.timeout; + //typings.findAndSplice(t => t === typing); + const idx = typings.indexOf(typing); + if(idx !== -1) { + typings.splice(idx, 1); } - let thumbImage: HTMLImageElement; - if(photo.stripped_thumb) { - thumbImage = new Image(); - div.classList.add('avatar-relative'); - thumbImage.classList.add('avatar-photo', 'avatar-photo-thumbnail'); - img.classList.add('avatar-photo'); - const url = appPhotosManager.getPreviewURLFromBytes(photo.stripped_thumb); - renderThumbPromise = renderImageFromUrl(thumbImage, url).then(() => { - replaceContent(div, thumbImage); - }); - } + rootScope.dispatchEvent('peer_typings', {peerId, typings}); - callback = () => { - if(photo.stripped_thumb) { - div.append(img); - } else { - replaceContent(div, img); - } + if(!typings.length) { + delete this.typingsInPeer[peerId]; + } + }; - setTimeout(() => { - if(div.childElementCount) { - sequentialDom.mutateElement(img, () => { - div.dataset.color = ''; - - if(animate) { - img.classList.remove('fade-in'); - } - - if(thumbImage) { - thumbImage.remove(); - } - }); - } - }, animate ? 200 : 0); - }; + if(typing && typing.timeout !== undefined) { + clearTimeout(typing.timeout); } - const renderPromise = loadPromise - .then((url) => renderImageFromUrl(img, url/* , false */)) - .then(() => callback()); - - return {cached, loadPromise: renderThumbPromise || renderPromise}; - } - - // peerId === peerId || title - public putPhoto(div: HTMLElement, peerId: number, isDialog = false, title = '', onlyThumb = false) { - const photo = appPeersManager.getPeerPhoto(peerId); + if(update.action._ === 'sendMessageCancelAction') { + if(!typing) { + return; + } - const size: PeerPhotoSize = 'photo_small'; - const avatarAvailable = !!photo; - const avatarRendered = div.firstElementChild && !(div.firstElementChild as HTMLElement).classList.contains('emoji'); - - const myId = rootScope.myId; - - //console.log('loadDialogPhoto location:', location, inputPeer); - if(peerId === myId && isDialog) { - div.innerText = ''; - div.dataset.color = ''; - div.classList.add('tgico-saved'); - div.classList.remove('tgico-deletedaccount'); + cancelAction(); return; } - if(peerId > 0) { - const user = appUsersManager.getUser(peerId); - if(user && user.pFlags && user.pFlags.deleted) { - div.innerText = ''; - div.dataset.color = appPeersManager.getPeerColorById(peerId); - div.classList.add('tgico-deletedaccount'); - div.classList.remove('tgico-saved'); - return; - } + if(!typing) { + typing = { + userId: fromId + }; + + typings.push(typing); } - if(!avatarAvailable || !avatarRendered || !this.savedAvatarURLs[peerId]) { - let color = ''; - if(peerId && (peerId !== myId || !isDialog)) { - color = appPeersManager.getPeerColorById(peerId); + //console.log('updateChatUserTyping', update, typings); + + typing.action = update.action; + + const hasUser = appUsersManager.hasUser(fromId); + if(!hasUser) { + // let's load user here + if(update._ === 'updateChatUserTyping') { + if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) { + appProfileManager.getChatFull(update.chat_id).then(() => { + if(typing.timeout !== undefined && appUsersManager.hasUser(fromId)) { + rootScope.dispatchEvent('peer_typings', {peerId, typings}); + } + }); + } } - div.innerText = ''; - div.classList.remove('tgico-saved', 'tgico-deletedaccount'); - div.dataset.color = color; - - let abbr: string; - if(!title) { - const peer = appPeersManager.getPeer(peerId); - abbr = peer.initials ?? ''; - } else { - abbr = RichTextProcessor.getAbbreviation(title); - } - - div.innerHTML = abbr; - //return Promise.resolve(true); + //return; + } else { + appUsersManager.forceUserOnline(fromId); } - if(avatarAvailable/* && false */) { - return this.putAvatar(div, peerId, photo, size, undefined, onlyThumb); + typing.timeout = window.setTimeout(cancelAction, 6000); + if(hasUser) { + rootScope.dispatchEvent('peer_typings', {peerId, typings}); } + }; + + public getPeerTypings(peerId: number) { + return this.typingsInPeer[peerId]; } } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 61584d71..2af3c13b 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -10,7 +10,7 @@ import type { AppMessagesManager, Dialog, MessagesStorage } from "./appManagers/ import type { Poll, PollResults } from "./appManagers/appPollsManager"; import type { MyDialogFilter } from "./storages/filters"; import type { ConnectionStatusChange } from "../types"; -import type { UserTyping } from "./appManagers/appChatsManager"; +import type { UserTyping } from "./appManagers/appProfileManager"; import type Chat from "../components/chat/chat"; import type { UserAuth } from "./mtproto/mtproto_config"; import type { State, Theme } from "./appManagers/appStateManager"; @@ -86,6 +86,7 @@ export type BroadcastEvents = { 'chat_full_update': number, 'poll_update': {poll: Poll, results: PollResults}, 'chat_update': number, + 'invalidate_participants': number, //'channel_settings': {channelId: number}, 'webpage_updated': {id: string, msgs: number[]}, diff --git a/src/pages/pageAuthCode.ts b/src/pages/pageAuthCode.ts index b500946c..241ff40c 100644 --- a/src/pages/pageAuthCode.ts +++ b/src/pages/pageAuthCode.ts @@ -9,10 +9,7 @@ import { AuthSentCode, AuthSentCodeType, AuthSignIn } from '../layer'; import appStateManager from '../lib/appManagers/appStateManager'; import apiManager from '../lib/mtproto/mtprotoworker'; import Page from './page'; -import pageIm from './pageIm'; -import pagePassword from './pagePassword'; import pageSignIn from './pageSignIn'; -import pageSignUp from './pageSignUp'; import TrackingMonkey from '../components/monkeys/tracking'; import CodeInputField from '../components/codeInputField'; import { i18n, LangPackKey } from '../lib/langPack'; @@ -72,15 +69,19 @@ let onFirstMount = (): Promise => { case 'auth.authorization': apiManager.setUserAuth(response.user.id); - pageIm.mount(); + import('./pageIm').then(m => { + m.default.mount(); + }); cleanup(); break; case 'auth.authorizationSignUpRequired': //console.log('Registration needed!'); - pageSignUp.mount({ - 'phone_number': authCode.phone_number, - 'phone_code_hash': authCode.phone_code_hash + import('./pageSignUp').then(m => { + m.default.mount({ + 'phone_number': authCode.phone_number, + 'phone_code_hash': authCode.phone_code_hash + }); }); cleanup(); @@ -96,7 +97,7 @@ let onFirstMount = (): Promise => { //console.warn('pageAuthCode: SESSION_PASSWORD_NEEDED'); good = true; err.handled = true; - await pagePassword.mount(); + await (await import('./pagePassword')).default.mount(); // lol setTimeout(() => { codeInput.value = ''; }, 300); diff --git a/src/pages/pagePassword.ts b/src/pages/pagePassword.ts index 3a15163d..94294d4a 100644 --- a/src/pages/pagePassword.ts +++ b/src/pages/pagePassword.ts @@ -10,7 +10,6 @@ import { AccountPassword } from '../layer'; import appStateManager from '../lib/appManagers/appStateManager'; import passwordManager from '../lib/mtproto/passwordManager'; import Page from './page'; -import pageIm from './pageIm'; import Button from '../components/button'; import PasswordInputField from '../components/passwordInputField'; import PasswordMonkey from '../components/monkeys/password'; @@ -91,7 +90,9 @@ let onFirstMount = (): Promise => { switch(response._) { case 'auth.authorization': clearInterval(getStateInterval); - pageIm.mount(); + import('./pageIm').then(m => { + m.default.mount(); + }); if(monkey) monkey.remove(); break; default: diff --git a/src/pages/pageSignUp.ts b/src/pages/pageSignUp.ts index 83e87bb5..48929b9d 100644 --- a/src/pages/pageSignUp.ts +++ b/src/pages/pageSignUp.ts @@ -18,7 +18,6 @@ import apiManager from '../lib/mtproto/mtprotoworker'; import RichTextProcessor from '../lib/richtextprocessor'; import LoginPage from './loginPage'; import Page from './page'; -import pageIm from './pageIm'; import blurActiveElement from '../helpers/dom/blurActiveElement'; import replaceContent from '../helpers/dom/replaceContent'; @@ -135,7 +134,9 @@ const onFirstMount = () => import('../lib/appManagers/appProfileManager').then(i apiManager.setUserAuth(response.user.id); sendAvatar().finally(() => { - pageIm.mount(); + import('./pageIm').then(m => { + m.default.mount(); + }); }); break;