From d160b2b4cb2408e417499eb195df27f5d36194a2 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 5 Jul 2023 13:13:46 +0200 Subject: [PATCH] GramJS: Validate updates order (#2957) --- src/api/gramjs/ChatAbortController.ts | 23 + src/api/gramjs/helpers.ts | 34 +- src/api/gramjs/localDb.ts | 16 +- src/api/gramjs/methods/bots.ts | 14 +- src/api/gramjs/methods/calls.ts | 53 ++- src/api/gramjs/methods/chats.ts | 167 +++++-- src/api/gramjs/methods/client.ts | 162 +++---- src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/management.ts | 28 +- src/api/gramjs/methods/media.ts | 4 - src/api/gramjs/methods/messages.ts | 102 +++-- src/api/gramjs/methods/payments.ts | 8 +- src/api/gramjs/methods/reactions.ts | 22 +- src/api/gramjs/methods/settings.ts | 34 +- src/api/gramjs/methods/statistics.ts | 26 +- src/api/gramjs/methods/symbols.ts | 6 +- src/api/gramjs/methods/users.ts | 32 +- src/api/gramjs/provider.ts | 4 +- src/api/gramjs/updateManager.ts | 412 ++++++++++++++++++ src/api/gramjs/updater.ts | 26 +- src/api/gramjs/worker/provider.ts | 50 ++- src/api/gramjs/worker/worker.ts | 10 +- src/api/types/updates.ts | 6 +- .../common/AnimatedIconFromSticker.tsx | 7 +- src/components/common/Audio.tsx | 4 +- src/components/common/Avatar.tsx | 6 +- src/components/common/ChatExtra.tsx | 17 +- src/components/common/Document.tsx | 2 +- src/components/common/GroupChatInfo.tsx | 19 +- src/components/common/PrivateChatInfo.tsx | 14 +- src/components/common/ProfilePhoto.tsx | 8 +- src/components/common/StickerView.tsx | 10 +- src/components/left/main/Chat.tsx | 8 +- src/components/left/main/ChatFolders.tsx | 11 +- src/components/left/main/ChatList.tsx | 1 - src/components/left/main/ForumPanel.tsx | 9 +- src/components/left/search/AudioResults.tsx | 11 +- src/components/left/search/ChatMessage.tsx | 4 - .../left/search/ChatMessageResults.tsx | 12 +- src/components/left/search/ChatResults.tsx | 33 +- src/components/left/search/FileResults.tsx | 10 +- src/components/left/search/LinkResults.tsx | 10 +- src/components/left/search/MediaResults.tsx | 10 +- .../search/helpers/createMapStateToProps.ts | 2 - .../left/settings/SettingsLanguage.tsx | 10 +- src/components/left/settings/SettingsMain.tsx | 17 +- src/components/main/Main.tsx | 68 ++- .../mediaViewer/hooks/useMediaProps.ts | 4 - src/components/middle/HeaderMenuContainer.tsx | 3 +- src/components/middle/MessageList.tsx | 7 +- src/components/middle/MiddleColumn.tsx | 14 +- src/components/middle/MiddleHeader.tsx | 10 +- src/components/middle/composer/Composer.tsx | 28 +- src/components/middle/composer/SymbolMenu.tsx | 11 +- .../middle/composer/hooks/useDraft.ts | 3 +- src/components/middle/message/Album.tsx | 3 - .../middle/message/AnimatedCustomEmoji.tsx | 1 - .../middle/message/AnimatedEmoji.tsx | 3 - src/components/middle/message/Game.tsx | 6 +- .../middle/message/InvoiceMediaPreview.tsx | 6 +- src/components/middle/message/Location.tsx | 4 +- src/components/middle/message/Message.tsx | 24 +- src/components/middle/message/RoundVideo.tsx | 6 +- src/components/middle/message/Sticker.tsx | 5 +- src/components/middle/message/Video.tsx | 11 +- src/components/middle/message/WebPage.tsx | 3 - src/components/right/PollResults.tsx | 12 +- src/components/right/Profile.tsx | 14 +- .../right/hooks/useProfileViewportIds.ts | 22 +- .../right/management/ManageChannel.tsx | 13 +- .../right/management/ManageGroup.tsx | 7 +- src/global/actions/all.ts | 30 +- src/global/actions/api/chats.ts | 28 +- src/global/actions/api/messages.ts | 1 + src/global/actions/api/reactions.ts | 5 +- src/global/actions/api/settings.ts | 1 + src/global/actions/api/sync.ts | 6 +- src/global/actions/apiUpdaters/initial.ts | 4 + src/global/types.ts | 2 +- src/hooks/useEnsureCustomEmoji.ts | 7 +- src/hooks/useLastSyncTime.ts | 34 -- src/hooks/useMedia.ts | 3 +- src/hooks/useMediaWithLoadProgress.ts | 4 +- src/lib/gramjs/client/TelegramClient.js | 6 +- src/lib/gramjs/extensions/Logger.js | 40 +- src/lib/gramjs/network/MTProtoSender.js | 2 +- src/lib/video-preview/mp4box.d.ts | 2 +- src/util/SortedQueue.ts | 44 ++ 88 files changed, 1265 insertions(+), 718 deletions(-) create mode 100644 src/api/gramjs/ChatAbortController.ts create mode 100644 src/api/gramjs/updateManager.ts delete mode 100644 src/hooks/useLastSyncTime.ts create mode 100644 src/util/SortedQueue.ts diff --git a/src/api/gramjs/ChatAbortController.ts b/src/api/gramjs/ChatAbortController.ts new file mode 100644 index 000000000..714d4ca6c --- /dev/null +++ b/src/api/gramjs/ChatAbortController.ts @@ -0,0 +1,23 @@ +export class ChatAbortController extends AbortController { + private threads = new Map(); + + public getThreadSignal(threadId: number): AbortSignal { + let controller = this.threads.get(threadId); + if (!controller) { + controller = new AbortController(); + this.threads.set(threadId, controller); + } + return controller.signal; + } + + public abortThread(threadId: number, reason?: string): void { + this.threads.get(threadId)?.abort(reason); + this.threads.delete(threadId); + } + + public abort(reason?: string): void { + super.abort(reason); + this.threads.forEach((controller) => controller.abort(reason)); + this.threads.clear(); + } +} diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index f1d5fbdc9..a2897818f 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -83,25 +83,30 @@ export function addPhotoToLocalDb(photo: GramJs.TypePhoto) { } } -function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel, noOverwrite = false) { +export function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel) { const id = buildApiPeerId(chat.id, chat instanceof GramJs.Chat ? 'chat' : 'channel'); - if (!noOverwrite || !localDb.chats[id]) { - localDb.chats[id] = chat; - } + const storedChat = localDb.chats[id]; + + const isStoredMin = storedChat && 'min' in storedChat && storedChat.min; + const isChatMin = 'min' in chat && chat.min; + if (storedChat && !isStoredMin && isChatMin) return; + + localDb.chats[id] = chat; } -export function addUserToLocalDb(user: GramJs.User, shouldOverwrite = false) { +export function addUserToLocalDb(user: GramJs.User) { const id = buildApiPeerId(user.id, 'user'); - if (shouldOverwrite || !localDb.users[id]) { - localDb.users[id] = user; - } + const storedUser = localDb.users[id]; + if (storedUser && !storedUser.min && user.min) return; + + localDb.users[id] = user; } -export function addEntitiesWithPhotosToLocalDb(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) { +export function addEntitiesToLocalDb(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) { entities.forEach((entity) => { - if (entity instanceof GramJs.User && entity.photo) { + if (entity instanceof GramJs.User) { addUserToLocalDb(entity); - } else if ((entity instanceof GramJs.Chat || entity instanceof GramJs.Channel) && entity.photo) { + } else if ((entity instanceof GramJs.Chat || entity instanceof GramJs.Channel)) { addChatToLocalDb(entity); } }); @@ -147,3 +152,10 @@ export function log(suffix: keyof typeof LOG_SUFFIX, ...data: any) { ); /* eslint-enable max-len */ } + +export function isResponseUpdate(result: T['__response']): result is GramJs.TypeUpdate { + return result instanceof GramJs.UpdatesTooLong || result instanceof GramJs.UpdateShortMessage + || result instanceof GramJs.UpdateShortChatMessage || result instanceof GramJs.UpdateShort + || result instanceof GramJs.UpdatesCombined || result instanceof GramJs.Updates + || result instanceof GramJs.UpdateShortSentMessage; +} diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index e581d921e..0c95f5c12 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -17,6 +17,9 @@ export interface LocalDb { stickerSets: Record; photos: Record; webDocuments: Record; + + commonBoxState: Record; + channelPtsById: Record; } const channel = IS_MULTITAB_SUPPORTED ? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) : undefined; @@ -77,17 +80,24 @@ function convertToVirtualClass(value: any): any { function createLocalDbInitial(initial?: LocalDb): LocalDb { return [ 'localMessages', 'chats', 'users', 'messages', 'documents', 'stickerSets', 'photos', 'webDocuments', + 'commonBoxState', 'channelPtsById', ] .reduce((acc: Record, key) => { const value = initial?.[key as keyof LocalDb] ?? {}; - const valueVirtualClass = Object.keys(value).reduce((acc2, key2) => { + const convertedValue = Object.keys(value).reduce((acc2, key2) => { + if (key === 'commonBoxState' || key === 'channelPtsById') { + const typedValue = value as Record; + acc2[key2] = typedValue[key2]; + return acc2; + } + acc2[key2] = convertToVirtualClass(value[key2]); return acc2; }, {} as Record); acc[key] = IS_MULTITAB_SUPPORTED - ? createProxy(key, valueVirtualClass) - : valueVirtualClass; + ? createProxy(key, convertedValue) + : convertedValue; return acc; }, {} as LocalDb) as LocalDb; } diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 4a8116e54..bdbb812e0 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -22,7 +22,7 @@ import { buildBotSwitchWebview, } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; -import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; +import { addEntitiesToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildCollectionByKey } from '../../../util/iteratees'; import { buildApiUrlAuthResult } from '../apiBuilders/misc'; @@ -104,7 +104,7 @@ export async function fetchInlineBotResults({ return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { isGallery: Boolean(result.gallery), @@ -143,7 +143,7 @@ export async function sendInlineBotResult({ ...(isSilent && { silent: true }), ...(replyingTo && { replyToMsgId: replyingTo }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), - }), true); + })); } export async function startBot({ @@ -159,7 +159,7 @@ export async function startBot({ peer: buildInputPeer(bot.id, bot.accessHash), randomId, startParam, - }), true); + })); } export async function requestWebView({ @@ -313,7 +313,7 @@ export async function sendWebViewData({ buttonText, data, randomId, - }), true); + })); } export async function loadAttachBots({ @@ -326,7 +326,7 @@ export async function loadAttachBots({ })); if (result instanceof GramJs.AttachMenuBots) { - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { hash: result.hash.toString(), bots: buildCollectionByKey(result.bots.map(buildApiAttachBot), 'id'), @@ -346,7 +346,7 @@ export async function loadAttachBot({ })); if (result instanceof GramJs.AttachMenuBotsBot) { - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { bot: buildApiAttachBot(result.bot), users: result.users.map(buildApiUser).filter(Boolean), diff --git a/src/api/gramjs/methods/calls.ts b/src/api/gramjs/methods/calls.ts index 927966117..1d9a88f6c 100644 --- a/src/api/gramjs/methods/calls.ts +++ b/src/api/gramjs/methods/calls.ts @@ -17,7 +17,7 @@ import { } from '../apiBuilders/calls'; import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; -import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb } from '../helpers'; import { GROUP_CALL_PARTICIPANTS_LIMIT } from '../../../config'; let onUpdate: OnApiUpdate; @@ -39,8 +39,8 @@ export async function getGroupCall({ return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); const users = result.users.map(buildApiUser).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); @@ -59,7 +59,9 @@ export function discardGroupCall({ }) { return invokeRequest(new GramJs.phone.DiscardGroupCall({ call: buildInputGroupCall(call), - }), true); + }), { + shouldReturnTrue: true, + }); } export function editGroupCallParticipant({ @@ -78,7 +80,9 @@ export function editGroupCallParticipant({ ...(presentationPaused !== undefined && { presentationPaused }), ...(raiseHand !== undefined && { raiseHand }), ...(volume !== undefined && { volume }), - }), true); + }), { + shouldReturnTrue: true, + }); } export function editGroupCallTitle({ @@ -89,7 +93,9 @@ export function editGroupCallTitle({ return invokeRequest(new GramJs.phone.EditGroupCallTitle({ title, call: buildInputGroupCall(groupCall), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function exportGroupCallInvite({ @@ -126,8 +132,8 @@ export async function fetchGroupCallParticipants({ return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); const users = result.users.map(buildApiUser).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); @@ -151,7 +157,9 @@ export function leaveGroupCall({ }) { return invokeRequest(new GramJs.phone.LeaveGroupCall({ call: buildInputGroupCall(call), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function joinGroupCall({ @@ -215,7 +223,9 @@ export function joinGroupCallPresentation({ params: new GramJs.DataJSON({ data: JSON.stringify(params), }), - }), true); + }), { + shouldReturnTrue: true, + }); } export function toggleGroupCallStartSubscription({ @@ -226,7 +236,10 @@ export function toggleGroupCallStartSubscription({ return invokeRequest(new GramJs.phone.ToggleGroupCallStartSubscription({ call: buildInputGroupCall(call), subscribed, - }), true, undefined, undefined, undefined, true); + }), { + shouldReturnTrue: true, + shouldIgnoreErrors: true, + }); } export function leaveGroupCallPresentation({ @@ -236,7 +249,9 @@ export function leaveGroupCallPresentation({ }) { return invokeRequest(new GramJs.phone.LeaveGroupCallPresentation({ call: buildInputGroupCall(call), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function getDhConfig() { @@ -259,7 +274,9 @@ export function discardCall({ return invokeRequest(new GramJs.phone.DiscardCall({ peer: buildInputPhoneCall(call), reason: isBusy ? new GramJs.PhoneCallDiscardReasonBusy() : new GramJs.PhoneCallDiscardReasonHangup(), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function requestCall({ @@ -286,7 +303,7 @@ export async function requestCall({ call, }); - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), @@ -302,7 +319,9 @@ export function setCallRating({ rating, peer: buildInputPhoneCall(call), comment, - }), true); + }), { + shouldReturnTrue: true, + }); } export function receivedCall({ @@ -337,7 +356,7 @@ export async function acceptCall({ call, }); - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), @@ -367,7 +386,7 @@ export async function confirmCall({ call, }); - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 217c0de03..e3a767ca4 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -59,7 +59,7 @@ import { generateRandomBigInt, } from '../gramjsBuilders'; import { - addEntitiesWithPhotosToLocalDb, + addEntitiesToLocalDb, addMessageToLocalDb, addPhotoToLocalDb, isChatFolder, @@ -69,6 +69,7 @@ import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPhoto } from '../apiBuilders/common'; import { buildStickerSet } from '../apiBuilders/symbols'; import localDb from '../localDb'; +import { updateChannelState } from '../updateManager'; import { scheduleMutedChatUpdate } from '../scheduleUnmute'; type FullChatData = { @@ -153,6 +154,10 @@ export async function fetchChats({ const peerEntity = peersByKey[getPeerKey(dialog.peer)]; const chat = buildApiChatFromDialog(dialog, peerEntity); + if (dialog.pts) { + updateChannelState(chat.id, dialog.pts); + } + if ( chat.id === SERVICE_NOTIFICATIONS_USER_ID && lastLocalServiceMessage @@ -222,13 +227,15 @@ export async function fetchChatSettings(chat: ApiChat) { const result = await invokeRequest(new GramJs.messages.GetPeerSettings({ peer: buildInputPeer(id, accessHash), - })); + }), { + abortControllerChatId: id, + }); if (!result) { return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), @@ -516,6 +523,10 @@ async function getFullChannelInfo( } } + if (result.fullChat.pts) { + updateChannelState(id, result.fullChat.pts); + } + const statusesById = { ...userStatusesById, ...bannedStatusesById, @@ -631,7 +642,9 @@ export async function createChannel({ broadcast: true, title, about, - }), undefined, true); + }), { + shouldThrow: true, + }); // `createChannel` can return a lot of different update types according to docs, // but currently channel creation returns only `Updates` type. @@ -660,7 +673,9 @@ export async function createChannel({ await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], - }), undefined, noErrorUpdate); + }), { + shouldThrow: noErrorUpdate, + }); } catch (err) { // `noErrorUpdate` will cause an exception which we don't want either } @@ -676,7 +691,10 @@ export function joinChannel({ }) { return invokeRequest(new GramJs.channels.JoinChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, - }), true, true); + }), { + shouldReturnTrue: true, + shouldThrow: true, + }); } export function deleteChatUser({ @@ -688,7 +706,9 @@ export function deleteChatUser({ return invokeRequest(new GramJs.messages.DeleteChatUser({ chatId: buildInputEntity(chat.id, chat.accessHash) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, - }), true); + }), { + shouldReturnTrue: true, + }); } export function deleteChat({ @@ -698,7 +718,9 @@ export function deleteChat({ }) { return invokeRequest(new GramJs.messages.DeleteChat({ chatId: buildInputEntity(chatId) as BigInt.BigInteger, - }), true); + }), { + shouldReturnTrue: true, + }); } export function leaveChannel({ @@ -708,7 +730,9 @@ export function leaveChannel({ }) { return invokeRequest(new GramJs.channels.LeaveChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, - }), true); + }), { + shouldReturnTrue: true, + }); } export function deleteChannel({ @@ -718,7 +742,9 @@ export function deleteChannel({ }) { return invokeRequest(new GramJs.channels.DeleteChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, - }), true); + }), { + shouldReturnTrue: true, + }); } export async function createGroupChat({ @@ -729,7 +755,9 @@ export async function createGroupChat({ const result = await invokeRequest(new GramJs.messages.CreateChat({ title, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], - }), undefined, true); + }), { + shouldThrow: true, + }); // `createChat` can return a lot of different update types according to docs, // but currently chat creation returns only `Updates` type. @@ -785,7 +813,9 @@ export async function editChatPhoto({ chatId: inputEntity as BigInt.BigInteger, photo: inputPhoto, }), - true, + { + shouldReturnTrue: true, + }, ); } @@ -826,7 +856,9 @@ export function toggleChatArchived({ peer: buildInputPeer(id, accessHash), folderId, })], - }), true); + }), { + shouldReturnTrue: true, + }); } export async function fetchChatFolders() { @@ -988,7 +1020,9 @@ export function togglePreHistoryHidden({ return invokeRequest(new GramJs.channels.TogglePreHistoryHidden({ channel: channel as GramJs.InputChannel, enabled: isEnabled, - }), true); + }), { + shouldReturnTrue: true, + }); } export function updateChatDefaultBannedRights({ @@ -1000,7 +1034,9 @@ export function updateChatDefaultBannedRights({ return invokeRequest(new GramJs.messages.EditChatDefaultBannedRights({ peer, bannedRights: buildChatBannedRights(bannedRights), - }), true); + }), { + shouldReturnTrue: true, + }); } export function updateChatMemberBannedRights({ @@ -1013,7 +1049,9 @@ export function updateChatMemberBannedRights({ channel, participant, bannedRights: buildChatBannedRights(bannedRights, untilDate), - }), true); + }), { + shouldReturnTrue: true, + }); } export function updateChatAdmin({ @@ -1027,7 +1065,9 @@ export function updateChatAdmin({ userId, adminRights: buildChatAdminRights(adminRights), rank: customTitle, - }), true); + }), { + shouldReturnTrue: true, + }); } export async function updateChatTitle(chat: ApiChat, title: string) { @@ -1041,7 +1081,9 @@ export async function updateChatTitle(chat: ApiChat, title: string) { chatId: inputEntity as BigInt.BigInteger, title, }), - true, + { + shouldReturnTrue: true, + }, ); } @@ -1073,7 +1115,9 @@ export function toggleSignatures({ return invokeRequest(new GramJs.channels.ToggleSignatures({ channel: channel as GramJs.InputChannel, enabled: isEnabled, - }), true); + }), { + shouldReturnTrue: true, + }); } type ChannelMembersFilter = @@ -1106,7 +1150,9 @@ export async function fetchMembers( filter, offset, limit: MEMBERS_LOAD_SLICE, - })); + }), { + abortControllerChatId: chatId, + }); if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) { return undefined; @@ -1144,14 +1190,17 @@ export function setDiscussionGroup({ return invokeRequest(new GramJs.channels.SetDiscussionGroup({ broadcast: buildInputPeer(channel.id, channel.accessHash), group: chat ? buildInputPeer(chat.id, chat.accessHash) : new GramJs.InputChannelEmpty(), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function migrateChat(chat: ApiChat) { const result = await invokeRequest( new GramJs.messages.MigrateChat({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger }), - undefined, - true, + { + shouldThrow: true, + }, ); // `migrateChat` can return a lot of different update types according to docs, @@ -1226,14 +1275,20 @@ export async function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpd return await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[], - }), true, noErrorUpdate); + }), { + shouldReturnTrue: true, + shouldThrow: noErrorUpdate, + }); } return await Promise.all(users.map((user) => { return invokeRequest(new GramJs.messages.AddChatUser({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, - }), true, noErrorUpdate); + }), { + shouldReturnTrue: true, + shouldThrow: noErrorUpdate, + }); })); } catch (err) { // `noErrorUpdate` will cause an exception which we don't want either @@ -1274,7 +1329,9 @@ export function deleteChatMember(chat: ApiChat, user: ApiUser) { return invokeRequest(new GramJs.messages.DeleteChatUser({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, - }), true); + }), { + shouldReturnTrue: true, + }); } } @@ -1282,14 +1339,18 @@ export function toggleJoinToSend(chat: ApiChat, isEnabled: boolean) { return invokeRequest(new GramJs.channels.ToggleJoinToSend({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, enabled: isEnabled, - }), true); + }), { + shouldReturnTrue: true, + }); } export function toggleJoinRequest(chat: ApiChat, isEnabled: boolean) { return invokeRequest(new GramJs.channels.ToggleJoinRequest({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, enabled: isEnabled, - }), true); + }), { + shouldReturnTrue: true, + }); } function preparePeers( @@ -1328,11 +1389,11 @@ function updateLocalDb(result: ( GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates | GramJs.messages.ForumTopics )) { if ('users' in result) { - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); } if ('chats' in result) { - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.chats); } if ('messages' in result) { @@ -1361,7 +1422,9 @@ export function setChatEnabledReactions({ return invokeRequest(new GramJs.messages.SetChatAvailableReactions({ peer: buildInputPeer(chat.id, chat.accessHash), availableReactions: buildInputChatReactions(enabledReactions), - }), true); + }), { + shouldReturnTrue: true, + }); } export function toggleIsProtected({ @@ -1372,7 +1435,9 @@ export function toggleIsProtected({ return invokeRequest(new GramJs.messages.ToggleNoForwards({ peer: buildInputPeer(id, accessHash), enabled: isProtected, - }), true); + }), { + shouldReturnTrue: true, + }); } export function toggleParticipantsHidden({ @@ -1383,7 +1448,9 @@ export function toggleParticipantsHidden({ return invokeRequest(new GramJs.channels.ToggleParticipantsHidden({ channel: buildInputPeer(id, accessHash), enabled: isEnabled, - }), true); + }), { + shouldReturnTrue: true, + }); } export function toggleForum({ @@ -1394,7 +1461,10 @@ export function toggleForum({ return invokeRequest(new GramJs.channels.ToggleForum({ channel: buildInputPeer(id, accessHash), enabled: isEnabled, - }), true, true); + }), { + shouldReturnTrue: true, + shouldThrow: true, + }); } export async function createTopic({ @@ -1540,7 +1610,9 @@ export function deleteTopic({ return invokeRequest(new GramJs.channels.DeleteTopicHistory({ channel: buildInputPeer(id, accessHash), topMsgId: topicId, - }), true); + }), { + shouldReturnTrue: true, + }); } export function togglePinnedTopic({ @@ -1556,7 +1628,9 @@ export function togglePinnedTopic({ channel: buildInputPeer(id, accessHash), topicId, pinned: isPinned, - }), true); + }), { + shouldReturnTrue: true, + }); } export function editTopic({ @@ -1578,7 +1652,9 @@ export function editTopic({ iconEmojiId: topicId !== GENERAL_TOPIC_ID && iconEmojiId ? BigInt(iconEmojiId) : undefined, closed: isClosed, hidden: isHidden, - }), true); + }), { + shouldReturnTrue: true, + }); } export async function checkChatlistInvite({ @@ -1613,7 +1689,10 @@ export function joinChatlistInvite({ return invokeRequest(new GramJs.chatlists.JoinChatlistInvite({ slug, peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), - }), true, true); + }), { + shouldReturnTrue: true, + shouldThrow: true, + }); } export async function fetchLeaveChatlistSuggestions({ @@ -1644,7 +1723,9 @@ export function leaveChatlist({ filterId: folderId, }), peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function createChalistInvite({ @@ -1660,7 +1741,9 @@ export async function createChalistInvite({ }), title: title || '', peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), - }), undefined, true); + }), { + shouldThrow: true, + }); if (!result || result.filter instanceof GramJs.DialogFilterDefault) return undefined; @@ -1699,7 +1782,9 @@ export async function editChatlistInvite({ slug, title, peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), - }), undefined, true); + }), { + shouldThrow: true, + }); if (!result) return undefined; return buildApiChatlistExportedInvite(result); diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 5f524f7e7..d1e794c88 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -5,7 +5,6 @@ import TelegramClient from '../../../lib/gramjs/client/TelegramClient'; import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index'; import type { TwoFaParams } from '../../../lib/gramjs/client/2fa'; - import type { ApiInitialArgs, ApiMediaFormat, @@ -21,13 +20,18 @@ import { onRequestPhoneNumber, onRequestCode, onRequestPassword, onRequestRegistration, onAuthError, onAuthReady, onCurrentUserUpdate, onRequestQrCode, onWebAuthTokenFailed, } from './auth'; -import { updater } from '../updater'; import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; import downloadMediaWithClient, { parseMediaUrl } from './media'; import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users'; import localDb, { clearLocalDb } from '../localDb'; import { buildApiPeerId } from '../apiBuilders/peers'; -import { addMessageToLocalDb, log } from '../helpers'; +import { + addMessageToLocalDb, addUserToLocalDb, isResponseUpdate, log, +} from '../helpers'; +import { ChatAbortController } from '../ChatAbortController'; +import { + updateChannelState, getDifference, init as initUpdatesManager, processUpdate, reset as resetUpdatesManager, +} from '../updateManager'; const DEFAULT_USER_AGENT = 'Unknown UserAgent'; const DEFAULT_PLATFORM = 'Unknown platform'; @@ -37,6 +41,8 @@ GramJsLogger.setLevel(DEBUG_GRAMJS ? 'debug' : 'warn'); const gramJsUpdateEventBuilder = { build: (update: object) => update }; +const CHAT_ABORT_CONTROLLERS = new Map(); + let onUpdate: OnApiUpdate; let client: TelegramClient; let isConnected = false; @@ -81,7 +87,6 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) ); client.addEventHandler(handleGramJsUpdate, gramJsUpdateEventBuilder); - client.addEventHandler(updater, gramJsUpdateEventBuilder); try { if (DEBUG) { @@ -94,6 +99,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) } try { + client.setPingCallback(getDifference); await client.start({ phoneNumber: onRequestPhoneNumber, phoneCode: onRequestCode, @@ -131,6 +137,8 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) onSessionUpdate(session.getSessionData()); onUpdate({ '@type': 'updateApiReady' }); + initUpdatesManager(invokeRequest); + void fetchCurrentUser(); } catch (err) { if (DEBUG) { @@ -150,7 +158,10 @@ export async function destroy(noLogOut = false, noClearLocalDb = false) { await invokeRequest(new GramJs.auth.LogOut()); } - if (!noClearLocalDb) clearLocalDb(); + if (!noClearLocalDb) { + clearLocalDb(); + resetUpdatesManager(); + } await client.destroy(); } @@ -170,54 +181,66 @@ function onSessionUpdate(sessionData: ApiSessionData) { }); } -function handleGramJsUpdate(update: any) { +type UpdateConfig = GramJs.UpdateConfig & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }; + +export function handleGramJsUpdate(update: any) { + processUpdate(update); + if (update instanceof connection.UpdateConnectionState) { isConnected = update.state === connection.UpdateConnectionState.connected; } else if (update instanceof GramJs.UpdatesTooLong) { void handleTerminatedSession(); - } else if (update instanceof GramJs.UpdateConfig) { - // eslint-disable-next-line no-underscore-dangle - const currentUser = (update as GramJs.UpdateConfig & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }) - ._entities - ?.find((entity) => entity instanceof GramJs.User && buildApiPeerId(entity.id, 'user') === currentUserId); - if (!(currentUser instanceof GramJs.User)) return; + } else { + const updates = 'updates' in update ? update.updates : [update]; + updates.forEach((nestedUpdate: any) => { + if (!(nestedUpdate instanceof GramJs.UpdateConfig)) return; + // eslint-disable-next-line no-underscore-dangle + const currentUser = (nestedUpdate as UpdateConfig)._entities + ?.find((entity) => entity instanceof GramJs.User && buildApiPeerId(entity.id, 'user') === currentUserId); + if (!(currentUser instanceof GramJs.User)) return; - setIsPremium({ isPremium: Boolean(currentUser.premium) }); + setIsPremium({ isPremium: Boolean(currentUser.premium) }); + }); } } -export async function invokeRequest( - request: T, - shouldReturnTrue: true, - shouldThrow?: boolean, - shouldIgnoreUpdates?: undefined, - dcId?: number, - shouldIgnoreErrors?: boolean, -): Promise; +type InvokeRequestParams = { + shouldThrow?: boolean; + shouldIgnoreUpdates?: boolean; + dcId?: number; + shouldIgnoreErrors?: boolean; + abortControllerChatId?: string; + abortControllerThreadId?: number; +}; export async function invokeRequest( request: T, - shouldReturnTrue?: boolean, - shouldThrow?: boolean, - shouldIgnoreUpdates?: boolean, - dcId?: number, - shouldIgnoreErrors?: boolean, + params?: InvokeRequestParams & { shouldReturnTrue?: false }, ): Promise; export async function invokeRequest( request: T, - shouldReturnTrue = false, - shouldThrow = false, - shouldIgnoreUpdates = false, - dcId?: number, - shouldIgnoreErrors = false, + params?: InvokeRequestParams & { shouldReturnTrue: true }, +): Promise; + +export async function invokeRequest( + request: T, + params: InvokeRequestParams & { shouldReturnTrue?: boolean } = {}, ) { - if (!isConnected) { - if (DEBUG) { - log('INVOKE ERROR', request.className, 'Client is not connected'); + const { + shouldThrow, shouldIgnoreUpdates, dcId, shouldIgnoreErrors, abortControllerChatId, abortControllerThreadId, + } = params; + const shouldReturnTrue = Boolean(params.shouldReturnTrue); + + let abortSignal: AbortSignal | undefined; + if (abortControllerChatId) { + let controller = CHAT_ABORT_CONTROLLERS.get(abortControllerChatId); + if (!controller) { + controller = new ChatAbortController(); + CHAT_ABORT_CONTROLLERS.set(abortControllerChatId, controller); } - return undefined; + abortSignal = abortControllerThreadId ? controller.getThreadSignal(abortControllerThreadId) : controller.signal; } try { @@ -225,14 +248,14 @@ export async function invokeRequest( log('INVOKE', request.className); } - const result = await client.invoke(request, dcId); + const result = await client.invoke(request, dcId, abortSignal); if (DEBUG) { log('RESPONSE', request.className, result); } - if (!shouldIgnoreUpdates) { - handleUpdates(result); + if (!shouldIgnoreUpdates && isResponseUpdate(result)) { + processUpdate(result); } return shouldReturnTrue ? result && true : result; @@ -256,36 +279,6 @@ export async function invokeRequest( } } -export function handleUpdates(result: {}) { - let manyUpdates; - let singleUpdate; - - if (result instanceof GramJs.UpdatesCombined || result instanceof GramJs.Updates) { - manyUpdates = result; - } else if (typeof result === 'object' && 'updates' in result && ( - result.updates instanceof GramJs.Updates || result.updates instanceof GramJs.UpdatesCombined - )) { - manyUpdates = result.updates; - } else if ( - result instanceof GramJs.UpdateShortMessage - || result instanceof GramJs.UpdateShortChatMessage - || result instanceof GramJs.UpdateShort - || result instanceof GramJs.UpdateShortSentMessage - ) { - singleUpdate = result; - } - - if (manyUpdates) { - injectUpdateEntities(manyUpdates); - - manyUpdates.updates.forEach((update) => { - updater(update); - }); - } else if (singleUpdate) { - updater(singleUpdate); - } -} - export async function downloadMedia( args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean }, onProgress?: ApiOnProgress, @@ -321,6 +314,18 @@ export function getTmpPassword(currentPassword: string, ttl?: number) { return client.getTmpPassword(currentPassword, ttl); } +export function abortChatRequests(params: { chatId: string; threadId?: number }) { + const { chatId, threadId } = params; + const controller = CHAT_ABORT_CONTROLLERS.get(chatId); + if (!threadId) { + controller?.abort('Chat change'); + CHAT_ABORT_CONTROLLERS.delete(chatId); + return; + } + + controller?.abortThread(threadId, 'Thread change'); +} + export async function fetchCurrentUser() { const userFull = await invokeRequest(new GramJs.users.GetFullUser({ id: new GramJs.InputUserSelf(), @@ -335,7 +340,7 @@ export async function fetchCurrentUser() { if (user.photo instanceof GramJs.Photo) { localDb.photos[user.photo.id.toString()] = user.photo; } - localDb.users[buildApiPeerId(user.id, 'user')] = user; + addUserToLocalDb(user); const currentUserFullInfo = buildApiUserFullInfo(userFull); const currentUser = buildApiUser(user)!; @@ -365,22 +370,13 @@ export function dispatchErrorUpdate(err: Error, req }); } -function injectUpdateEntities(result: GramJs.Updates | GramJs.UpdatesCombined) { - const entities = [...result.users, ...result.chats]; - - result.updates.forEach((update) => { - if (entities) { - // eslint-disable-next-line no-underscore-dangle - (update as any)._entities = entities; - } - }); -} - async function handleTerminatedSession() { try { await invokeRequest(new GramJs.users.GetFullUser({ id: new GramJs.InputUserSelf(), - }), undefined, true); + }), { + shouldThrow: true, + }); } catch (err: any) { if (err.message === 'AUTH_KEY_UNREGISTERED' || err.message === 'SESSION_REVOKED') { onUpdate({ @@ -429,6 +425,10 @@ export async function repairFileReference({ if (!result || result instanceof GramJs.messages.MessagesNotModified) return false; + if (peer && 'pts' in result) { + updateChannelState(peer.channelId.toString(), result.pts); + } + const message = result.messages[0]; if (message instanceof GramJs.MessageEmpty) return false; addMessageToLocalDb(message); diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 467e25f75..8872b55d2 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -1,5 +1,5 @@ export { - destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, + destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, abortChatRequests, } from './client'; export { diff --git a/src/api/gramjs/methods/management.ts b/src/api/gramjs/methods/management.ts index 215107667..8de28bae3 100644 --- a/src/api/gramjs/methods/management.ts +++ b/src/api/gramjs/methods/management.ts @@ -7,7 +7,7 @@ import type { } from '../../types'; import { USERNAME_PURCHASE_ERROR } from '../../../config'; -import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb } from '../helpers'; import { buildApiExportedInvite, buildChatInviteImporter } from '../apiBuilders/chats'; import { buildApiUser } from '../apiBuilders/users'; import { buildCollectionByKey } from '../../../util/iteratees'; @@ -25,7 +25,9 @@ export async function checkChatUsername({ username }: { username: string }) { const result = await invokeRequest(new GramJs.channels.CheckUsername({ channel: new GramJs.InputChannelEmpty(), username, - }), undefined, true); + }), { + shouldThrow: true, + }); return { result, error: undefined }; } catch (error) { @@ -100,10 +102,12 @@ export async function fetchExportedChatInvites({ adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser, limit, revoked: isRevoked || undefined, - })); + }), { + abortControllerChatId: peer.id, + }); if (!exportedInvites) return undefined; - addEntitiesWithPhotosToLocalDb(exportedInvites.users); + addEntitiesToLocalDb(exportedInvites.users); const invites = (exportedInvites.invites .filter((invite): invite is GramJs.ChatInviteExported => invite instanceof GramJs.ChatInviteExported)) @@ -138,7 +142,7 @@ export async function editExportedChatInvite({ if (!invite) return undefined; - addEntitiesWithPhotosToLocalDb(invite.users); + addEntitiesToLocalDb(invite.users); if (invite instanceof GramJs.messages.ExportedChatInvite && invite.invite instanceof GramJs.ChatInviteExported) { const replaceInvite = buildApiExportedInvite(invite.invite); return { @@ -222,11 +226,13 @@ export async function fetchChatInviteImporters({ ? buildInputEntity(offsetUser.id, offsetUser.accessHash) as GramJs.InputUser : new GramJs.InputUserEmpty(), limit, requested: isRequested || undefined, - })); + }), { + abortControllerChatId: peer.id, + }); if (!result) return undefined; const users = result.users.map((user) => buildApiUser(user)).filter(Boolean); - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { importers: result.importers.map((importer) => buildChatInviteImporter(importer)), users: buildCollectionByKey(users, 'id'), @@ -246,7 +252,9 @@ export function hideChatJoinRequest({ peer: buildInputPeer(peer.id, peer.accessHash), userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, approved: isApproved || undefined, - }), true); + }), { + shouldReturnTrue: true, + }); } export function hideAllChatJoinRequests({ @@ -262,7 +270,9 @@ export function hideAllChatJoinRequests({ peer: buildInputPeer(peer.id, peer.accessHash), approved: isApproved || undefined, link, - }), true); + }), { + shouldReturnTrue: true, + }); } export function hideChatReportPanel(chat: ApiChat) { diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 58f11bd08..47b9e98f4 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -86,10 +86,6 @@ async function download( entityType, entityId, sizeType, params, mediaMatchType, } = parsed; - if (!isConnected) { - return Promise.reject(new Error('ERROR: Client is not connected')); - } - if (entityType === 'staticMap') { const accessHash = entityId; const parsedParams = new URLSearchParams(params); diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 9f263f918..34775b22d 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -30,7 +30,7 @@ import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; -import { handleUpdates, invokeRequest, uploadFile } from './client'; +import { handleGramJsUpdate, invokeRequest, uploadFile } from './client'; import { buildApiMessage, buildLocalForwardedMessage, @@ -61,7 +61,7 @@ import { import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats'; import { fetchFile } from '../../../util/files'; import { - addEntitiesWithPhotosToLocalDb, + addEntitiesToLocalDb, addMessageToLocalDb, deserializeBytes, resolveMessageApiChatId, @@ -71,7 +71,7 @@ import { requestChatUpdate } from './chats'; import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage'; import { getServerTimeOffset } from '../../../util/serverTime'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; -import { updater } from '../updater'; +import { updateChannelState } from '../updateManager'; const FAST_SEND_TIMEOUT = 1000; const INPUT_WAVEFORM_LENGTH = 63; @@ -117,7 +117,11 @@ export async function fetchMessages({ offsetId: Math.min(offsetId, MAX_INT_32), }), ...pagination, - }), undefined, true); + }), { + shouldThrow: true, + abortControllerChatId: chat.id, + abortControllerThreadId: threadId, + }); } catch (err: any) { if (err.message === 'CHANNEL_PRIVATE') { onUpdate({ @@ -167,8 +171,10 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message : new GramJs.messages.GetMessages({ id: [new GramJs.InputMessageID({ id: messageId })], }), - undefined, - true, + { + shouldThrow: true, + abortControllerChatId: chat.id, + }, ); } catch (err: any) { const { message } = err; @@ -191,6 +197,10 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message return undefined; } + if ('pts' in result) { + updateChannelState(chat.id, result.pts); + } + const mtpMessage = result.messages[0]; if (!mtpMessage) { return undefined; @@ -355,7 +365,10 @@ export function sendMessage( ...(noWebPage && { noWebpage: noWebPage }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(shouldUpdateStickerSetOrder && { updateStickersetsOrder: shouldUpdateStickerSetOrder }), - }), false, true, true); + }), { + shouldThrow: true, + shouldIgnoreUpdates: true, + }); if (update) handleLocalMessageUpdate(localMessage, update); } catch (error: any) { onUpdate({ @@ -476,7 +489,9 @@ function sendGroupedMedia( ...(isSilent && { silent: isSilent }), ...(scheduledAt && { scheduleDate: scheduledAt }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), - }), false, undefined, true); + }), { + shouldIgnoreUpdates: true, + }); if (update) handleMultipleLocalMessagesUpdate(localMessages, update); })(); @@ -573,7 +588,7 @@ export async function editMessage({ id: message.id, ...(isScheduled && { scheduleDate: message.date }), ...(noWebPage && { noWebpage: noWebPage }), - }), true); + })); } export async function rescheduleMessage({ @@ -589,7 +604,7 @@ export async function rescheduleMessage({ peer: buildInputPeer(chat.id, chat.accessHash), id: message.id, scheduleDate: scheduledAt, - }), true); + })); } async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) { @@ -680,7 +695,7 @@ export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; thre await invokeRequest(new GramJs.messages.UnpinAllMessages({ peer: buildInputPeer(chat.id, chat.accessHash), ...(threadId && { topMsgId: threadId }), - }), true); + })); } export async function deleteMessages({ @@ -791,7 +806,11 @@ export async function sendMessageAction({ peer: buildInputPeer(peer.id, peer.accessHash), topMsgId: threadId, action: gramAction, - }), undefined, true); + }), { + shouldThrow: true, + abortControllerChatId: peer.id, + abortControllerThreadId: threadId, + }); return result; } catch (error) { // Prevent error from being displayed in UI @@ -964,8 +983,8 @@ export async function requestThreadInfoUpdate({ }); } - addEntitiesWithPhotosToLocalDb(topMessageResult.users); - addEntitiesWithPhotosToLocalDb(topMessageResult.chats); + addEntitiesToLocalDb(topMessageResult.users); + addEntitiesToLocalDb(topMessageResult.chats); const users = topMessageResult.users.map(buildApiUser).filter(Boolean); @@ -1023,7 +1042,10 @@ export async function searchMessagesLocal({ minDate, maxDate, ...pagination, - })); + }), { + abortControllerChatId: chat.id, + abortControllerThreadId: topMessageId, + }); if ( !result @@ -1170,7 +1192,7 @@ export async function sendPollVote({ peer: buildInputPeer(id, accessHash), msgId: messageId, options: options.map(deserializeBytes), - }), true); + })); } export async function closePoll({ @@ -1309,7 +1331,9 @@ export async function forwardMessages({ ...(toThreadId && { topMsgId: toThreadId }), ...(scheduledAt && { scheduleDate: scheduledAt }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), - }), false, undefined, true); + }), { + shouldIgnoreUpdates: true, + }); if (update) handleMultipleLocalMessagesUpdate(localMessages, update); } @@ -1343,7 +1367,9 @@ export async function fetchScheduledHistory({ chat }: { chat: ApiChat }) { const result = await invokeRequest(new GramJs.messages.GetScheduledHistory({ peer: buildInputPeer(id, accessHash), - })); + }), { + abortControllerChatId: id, + }); if ( !result @@ -1368,15 +1394,15 @@ export async function sendScheduledMessages({ chat, ids }: { chat: ApiChat; ids: await invokeRequest(new GramJs.messages.SendScheduledMessages({ peer: buildInputPeer(id, accessHash), id: ids, - }), true); + })); } function updateLocalDb(result: ( GramJs.messages.MessagesSlice | GramJs.messages.Messages | GramJs.messages.ChannelMessages | GramJs.messages.DiscussionMessage | GramJs.messages.SponsoredMessages )) { - addEntitiesWithPhotosToLocalDb(result.users); - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); result.messages.forEach((message) => { if ((message instanceof GramJs.Message && isMessageWithMedia(message)) @@ -1396,7 +1422,10 @@ export async function fetchPinnedMessages({ chat, threadId }: { chat: ApiChat; t limit: PINNED_MESSAGES_LIMIT, topMsgId: threadId, }, - )); + ), { + abortControllerChatId: chat.id, + abortControllerThreadId: threadId, + }); if ( !result @@ -1441,14 +1470,17 @@ export async function fetchSendAs({ }) { const result = await invokeRequest(new GramJs.channels.GetSendAs({ peer: buildInputPeer(chat.id, chat.accessHash), - }), undefined, undefined, undefined, undefined, true); + }), { + shouldIgnoreErrors: true, + abortControllerChatId: chat.id, + }); if (!result) { return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); const users = result.users.map(buildApiUser).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); @@ -1507,7 +1539,9 @@ export function readAllMentions({ }) { return invokeRequest(new GramJs.messages.ReadMentions({ peer: buildInputPeer(chat.id, chat.accessHash), - }), true); + }), { + shouldReturnTrue: true, + }); } export function readAllReactions({ @@ -1517,7 +1551,9 @@ export function readAllReactions({ }) { return invokeRequest(new GramJs.messages.ReadReactions({ peer: buildInputPeer(chat.id, chat.accessHash), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function fetchUnreadMentions({ @@ -1652,13 +1688,17 @@ export async function translateText(params: TranslateTextParams) { function handleMultipleLocalMessagesUpdate( localMessages: Record, update: GramJs.TypeUpdates, ) { - if (!('updates' in update)) return; + if (!('updates' in update)) { + handleGramJsUpdate(update); + return; + } + update.updates.forEach((u) => { if (u instanceof GramJs.UpdateMessageID) { const localMessage = localMessages[u.randomId.toString()]; handleLocalMessageUpdate(localMessage, u); } else { - updater(u); + handleGramJsUpdate(u); } }); } @@ -1672,7 +1712,7 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU } if (!messageUpdate) { - handleUpdates(update); + handleGramJsUpdate(update); return; } @@ -1719,5 +1759,5 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU }, }); - handleUpdates(update); + handleGramJsUpdate(update); } diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 929190f81..d9d282c39 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -14,7 +14,7 @@ import type { } from '../../types'; import localDb from '../localDb'; import { - addEntitiesWithPhotosToLocalDb, + addEntitiesToLocalDb, deserializeBytes, serializeBytes, } from '../helpers'; @@ -121,7 +121,7 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) { localDb.webDocuments[result.photo.url] = result.photo; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { form: buildApiPaymentForm(result), @@ -140,7 +140,7 @@ export async function getReceipt(chat: ApiChat, msgId: number) { return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { receipt: buildApiReceipt(result), @@ -152,7 +152,7 @@ export async function fetchPremiumPromo() { const result = await invokeRequest(new GramJs.help.GetPremiumPromo()); if (!result) return undefined; - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); const users = result.users.map(buildApiUser).filter(Boolean); result.videos.forEach((video) => { diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts index a5ad54738..8e78ab881 100644 --- a/src/api/gramjs/methods/reactions.ts +++ b/src/api/gramjs/methods/reactions.ts @@ -9,7 +9,7 @@ import { buildApiUser } from '../apiBuilders/users'; import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/messages'; import { invokeRequest } from './client'; import localDb from '../localDb'; -import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb } from '../helpers'; export function sendWatchingEmojiInteraction({ chat, @@ -22,7 +22,9 @@ export function sendWatchingEmojiInteraction({ action: new GramJs.SendMessageEmojiInteractionSeen({ emoticon, }), - })); + }), { + abortControllerChatId: chat.id, + }); } export function sendEmojiInteraction({ @@ -48,7 +50,9 @@ export function sendEmojiInteraction({ }), }), }), - })); + }), { + abortControllerChatId: chat.id, + }); } export async function getAvailableReactions() { @@ -92,7 +96,10 @@ export function sendReaction({ peer: buildInputPeer(chat.id, chat.accessHash), msgId: messageId, ...(shouldAddToRecent && { addToRecent: true }), - }), true, true); + }), { + shouldReturnTrue: true, + shouldThrow: true, + }); } export function fetchMessageReactions({ @@ -103,7 +110,10 @@ export function fetchMessageReactions({ return invokeRequest(new GramJs.messages.GetMessagesReactions({ id: ids, peer: buildInputPeer(chat.id, chat.accessHash), - }), true); + }), { + shouldReturnTrue: true, + abortControllerChatId: chat.id, + }); } export async function fetchMessageReactionsList({ @@ -123,7 +133,7 @@ export async function fetchMessageReactionsList({ return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); const { nextOffset, reactions, count } = result; diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 6506726f2..635b1bf52 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -36,7 +36,7 @@ import { import { getClient, invokeRequest, uploadFile } from './client'; import { buildCollectionByKey } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; -import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb, addPhotoToLocalDb } from '../helpers'; import localDb from '../localDb'; const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz', 'en']; @@ -54,14 +54,18 @@ export function updateProfile({ firstName: firstName || '', lastName: lastName || '', about: about || '', - }), true); + }), { + shouldReturnTrue: true, + }); } export async function checkUsername(username: string) { try { const result = await invokeRequest(new GramJs.account.CheckUsername({ username, - }), undefined, true); + }), { + shouldThrow: true, + }); return { result, error: undefined }; } catch (error) { @@ -79,7 +83,9 @@ export async function checkUsername(username: string) { } export function updateUsername(username: string) { - return invokeRequest(new GramJs.account.UpdateUsername({ username }), true); + return invokeRequest(new GramJs.account.UpdateUsername({ username }), { + shouldReturnTrue: true, + }); } export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean) { @@ -90,7 +96,7 @@ export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean) })); if (!result) return undefined; - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); if (result.photo instanceof GramJs.Photo) { addPhotoToLocalDb(result.photo); return { @@ -110,7 +116,7 @@ export async function uploadProfilePhoto(file: File, isFallback?: boolean, isVid if (!result) return undefined; - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); if (result.photo instanceof GramJs.Photo) { addPhotoToLocalDb(result.photo); return { @@ -137,7 +143,7 @@ export async function uploadContactProfilePhoto({ if (!result) return undefined; - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); const users = result.users.map(buildApiUser).filter(Boolean); @@ -157,7 +163,9 @@ export async function uploadContactProfilePhoto({ export async function deleteProfilePhotos(photos: ApiPhoto[]) { const photoIds = photos.map(buildInputPhoto).filter(Boolean); - const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), true); + const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), { + shouldReturnTrue: true, + }); if (isDeleted) { photos.forEach((photo) => { delete localDb.photos[photo.id]; @@ -271,7 +279,7 @@ export async function fetchWebAuthorizations() { if (!result) { return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), @@ -290,7 +298,9 @@ export function terminateAllWebAuthorizations() { export async function fetchNotificationExceptions() { const result = await invokeRequest(new GramJs.account.GetNotifyExceptions({ compareSound: true, - }), undefined, undefined, true); + }), { + shouldIgnoreUpdates: true, + }); if (!(result instanceof GramJs.Updates || result instanceof GramJs.UpdatesCombined)) { return undefined; @@ -582,8 +592,8 @@ function updateLocalDb( GramJs.Updates | GramJs.UpdatesCombined ), ) { - addEntitiesWithPhotosToLocalDb(result.users); - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); } export async function fetchCountryList({ langCode = 'en' }: { langCode?: LangCode }) { diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index da485d00a..c1c7eeebf 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -6,7 +6,7 @@ import type { } from '../../types'; import { invokeRequest } from './client'; -import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb } from '../helpers'; import { buildInputEntity } from '../gramjsBuilders'; import { buildChannelStatistics, buildGroupStatistics, buildMessageStatistics, buildMessagePublicForwards, buildGraph, @@ -18,7 +18,9 @@ export async function fetchChannelStatistics({ }: { chat: ApiChat; dcId?: number }) { const result = await invokeRequest(new GramJs.stats.GetBroadcastStats({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, - }), undefined, undefined, undefined, dcId); + }), { + dcId, + }); if (!result) { return undefined; @@ -35,13 +37,15 @@ export async function fetchGroupStatistics({ }: { chat: ApiChat; dcId?: number }) { const result = await invokeRequest(new GramJs.stats.GetMegagroupStats({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, - }), undefined, undefined, undefined, dcId); + }), { + dcId, + }); if (!result) { return undefined; } - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); return { users: result.users.map(buildApiUser).filter(Boolean), @@ -61,7 +65,9 @@ export async function fetchMessageStatistics({ const result = await invokeRequest(new GramJs.stats.GetMessageStats({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, msgId: messageId, - }), undefined, undefined, undefined, dcId); + }), { + dcId, + }); if (!result) { return undefined; @@ -83,14 +89,16 @@ export async function fetchMessagePublicForwards({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, msgId: messageId, offsetPeer: new GramJs.InputPeerEmpty(), - }), undefined, undefined, undefined, dcId); + }), { + dcId, + }); if (!result) { return undefined; } if ('chats' in result) { - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.chats); } return buildMessagePublicForwards(result); @@ -110,7 +118,9 @@ export async function fetchStatisticsAsyncGraph({ const result = await invokeRequest(new GramJs.stats.LoadAsyncGraph({ token, ...(x && { x: BigInt(x) }), - }), undefined, undefined, undefined, dcId); + }), { + dcId, + }); if (!result) { return undefined; diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index d6dca1b47..56e3fe970 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -164,7 +164,9 @@ export async function fetchStickers( stickerset: 'id' in stickerSetInfo ? buildInputStickerSet(stickerSetInfo.id, stickerSetInfo.accessHash) : buildInputStickerSetShortName(stickerSetInfo.shortName), - }), undefined, true); + }), { + shouldThrow: true, + }); if (!(result instanceof GramJs.messages.StickerSet)) { return undefined; @@ -314,7 +316,7 @@ export function saveGif({ gif, shouldUnsave }: { gif: ApiVideo; shouldUnsave?: b unsave: shouldUnsave, }); - return invokeRequest(request, true); + return invokeRequest(request, { shouldReturnTrue: true }); } export async function installStickerSet({ stickerSetId, accessHash }: { stickerSetId: string; accessHash: string }) { diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 39a9736aa..5afcc6ac8 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -18,7 +18,7 @@ import { import { buildApiUser, buildApiUserFullInfo, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiPhoto } from '../apiBuilders/common'; -import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers'; +import { addEntitiesToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers'; import { buildApiPeerId } from '../apiBuilders/peers'; import localDb from '../localDb'; @@ -47,7 +47,7 @@ export async function fetchFullUser({ } updateLocalDb(result); - addUserToLocalDb(result.users[0], true); + addEntitiesToLocalDb(result.users); if (result.fullUser.profilePhoto instanceof GramJs.Photo) { localDb.photos[result.fullUser.profilePhoto.id.toString()] = result.fullUser.profilePhoto; @@ -142,11 +142,7 @@ export async function fetchContactList() { return undefined; } - result.users.forEach((user) => { - if (user instanceof GramJs.User) { - addUserToLocalDb(user, true); - } - }); + addEntitiesToLocalDb(result.users); const { users, userStatusesById } = buildApiUsersAndStatuses(result.users); @@ -165,11 +161,7 @@ export async function fetchUsers({ users }: { users: ApiUser[] }) { return undefined; } - result.forEach((user) => { - if (user instanceof GramJs.User) { - addUserToLocalDb(user, true); - } - }); + addEntitiesToLocalDb(result); return buildApiUsersAndStatuses(result); } @@ -219,7 +211,9 @@ export function updateContact({ lastName, phone: phoneNumber, ...(shouldSharePhoneNumber && { addPhonePrivacyException: shouldSharePhoneNumber }), - }), true); + }), { + shouldReturnTrue: true, + }); } export async function deleteContact({ @@ -294,18 +288,22 @@ export function reportSpam(userOrChat: ApiUser | ApiChat) { return invokeRequest(new GramJs.messages.ReportSpam({ peer: buildInputPeer(id, accessHash), - }), true); + }), { + shouldReturnTrue: true, + }); } export function updateEmojiStatus(emojiStatus: ApiSticker, expires?: number) { return invokeRequest(new GramJs.account.UpdateEmojiStatus({ emojiStatus: buildInputEmojiStatus(emojiStatus, expires), - }), true); + }), { + shouldReturnTrue: true, + }); } function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) { if ('chats' in result) { - addEntitiesWithPhotosToLocalDb(result.chats); + addEntitiesToLocalDb(result.chats); } if ('photos' in result) { @@ -313,6 +311,6 @@ function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice } if ('users' in result) { - addEntitiesWithPhotosToLocalDb(result.users); + addEntitiesToLocalDb(result.users); } } diff --git a/src/api/gramjs/provider.ts b/src/api/gramjs/provider.ts index 9cc994554..c99abc33b 100644 --- a/src/api/gramjs/provider.ts +++ b/src/api/gramjs/provider.ts @@ -26,7 +26,7 @@ import * as methods from './methods'; let onUpdate: OnApiUpdate; -export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs, initialLocalDb?: LocalDb) { +export function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs, initialLocalDb?: LocalDb) { onUpdate = _onUpdate; initUpdater(handleUpdate); @@ -43,7 +43,7 @@ export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArg if (initialLocalDb) updateFullLocalDb(initialLocalDb); - await initClient(handleUpdate, initialArgs); + initClient(handleUpdate, initialArgs); } export function callApi(fnName: T, ...args: MethodArgs): MethodResponse { diff --git a/src/api/gramjs/updateManager.ts b/src/api/gramjs/updateManager.ts new file mode 100644 index 000000000..d1e06dc71 --- /dev/null +++ b/src/api/gramjs/updateManager.ts @@ -0,0 +1,412 @@ +import { Api as GramJs } from '../../lib/gramjs'; +import type { Update } from './updater'; +import type { invokeRequest } from './methods/client'; + +import { UpdateConnectionState, UpdateServerTimeOffset } from '../../lib/gramjs/network'; +import { DEBUG } from '../../config'; +import localDb from './localDb'; +import SortedQueue from '../../util/SortedQueue'; +import { dispatchUserAndChatUpdates, requestSync, updater } from './updater'; +import { addEntitiesToLocalDb } from './helpers'; +import { buildInputEntity } from './gramjsBuilders'; +import { buildApiPeerId } from './apiBuilders/peers'; + +export type State = { + seq: number; + date: number; + pts: number; + qts: number; +}; +type SeqUpdate = GramJs.Updates | GramJs.UpdatesCombined; +type PtsUpdate = GramJs.TypeUpdate & { pts: number }; + +const COMMON_BOX_QUEUE_ID = '0'; +const CHANNEL_DIFFERENCE_LIMIT = 1000; +const UPDATE_WAIT_TIMEOUT = 500; + +let invoke: typeof invokeRequest; +let isInited = false; + +let seqTimeout: ReturnType | undefined; +const PTS_TIMEOUTS = new Map>(); + +const SEQ_QUEUE = new SortedQueue(seqComparator); +const PTS_QUEUE = new Map>(); + +export async function init(invokeReq: typeof invokeRequest) { + invoke = invokeReq; + + await loadRemoteState(); + isInited = true; + + scheduleGetDifference(); +} + +export function applyState(state: State) { + localDb.commonBoxState.seq = state.seq; + localDb.commonBoxState.date = state.date; + localDb.commonBoxState.pts = state.pts; + localDb.commonBoxState.qts = state.qts; +} + +export function processUpdate(update: Update) { + if (update instanceof UpdateConnectionState) { + if (update.state === UpdateConnectionState.connected && isInited) { + scheduleGetDifference(); + } + + updater(update); + return; + } + + if (update instanceof UpdateServerTimeOffset) { + updater(update); + return; + } + + if (localDb.commonBoxState.seq === undefined) { + // Drop updates received before first sync + return; + } + + if (update instanceof GramJs.Updates || update instanceof GramJs.UpdatesCombined) { + saveSeqUpdate(update); + return; + } + + if ('pts' in update) { + if (update instanceof GramJs.UpdateChannelTooLong) { + getChannelDifference(getUpdateChannelId(update)); + return; + } + savePtsUpdate(update); + return; + } + + updater(update); +} + +export function updateChannelState(channelId: string, pts: number) { + const channel = localDb.chats[channelId]; + if (!(channel instanceof GramJs.Channel)) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error(`[UpdateManager] Channel ${channelId} not found in localDb`); + } + return; + } + + const currentState = localDb.channelPtsById[channelId]; + + if (currentState && currentState < pts) { + scheduleGetChannelDifference(channelId); + return; + } + + localDb.channelPtsById[channelId] = pts; +} + +function applyUpdate(updateObject: SeqUpdate | PtsUpdate) { + if ('seq' in updateObject) { + if (updateObject.seq) localDb.commonBoxState.seq = updateObject.seq; + localDb.commonBoxState.date = updateObject.date; + } + + if ('qts' in updateObject) { + localDb.commonBoxState.qts = updateObject.qts; + } + + if ('pts' in updateObject) { + const channelId = getUpdateChannelId(updateObject); + if (channelId !== COMMON_BOX_QUEUE_ID) { + localDb.channelPtsById[channelId] = updateObject.pts; + } else { + localDb.commonBoxState.pts = updateObject.pts; + } + } + + if (updateObject instanceof GramJs.UpdatesCombined || updateObject instanceof GramJs.Updates) { + const entities = updateObject.users.concat(updateObject.chats); + + updateObject.updates.forEach((update) => { + if (entities) { + // eslint-disable-next-line no-underscore-dangle + (update as any)._entities = entities; + } + + processUpdate(update); + }); + } else { + updater(updateObject); + } +} + +function saveSeqUpdate(update: GramJs.Updates | GramJs.UpdatesCombined) { + SEQ_QUEUE.add(update); + + popSeqQueue(); +} + +function savePtsUpdate(update: PtsUpdate) { + const channelId = getUpdateChannelId(update); + + const ptsQueue = PTS_QUEUE.get(channelId) || new SortedQueue(ptsComparator); + ptsQueue.add(update); + + PTS_QUEUE.set(channelId, ptsQueue); + + popPtsQueue(channelId); +} + +function popSeqQueue() { + if (!SEQ_QUEUE.size) return; + + const update = SEQ_QUEUE.pop()!; + const localSeq = localDb.commonBoxState.seq; + const seqStart = 'seqStart' in update ? update.seqStart : update.seq; + + if (seqStart === 0 || seqStart === localSeq + 1) { + clearTimeout(seqTimeout); + seqTimeout = undefined; + + applyUpdate(update); + } else if (seqStart > localSeq + 1) { + SEQ_QUEUE.add(update); // Return update to queue + scheduleGetDifference(); + return; // Prevent endless loop + } + + popSeqQueue(); +} + +function popPtsQueue(channelId: string) { + const ptsQueue = PTS_QUEUE.get(channelId); + if (!ptsQueue?.size) return; + + const update = ptsQueue.pop()!; + const localPts = channelId === COMMON_BOX_QUEUE_ID ? localDb.commonBoxState.pts : localDb.channelPtsById[channelId]; + const pts = update.pts; + const ptsCount = getPtsCount(update); + + if (localPts === undefined) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error('[UpdateManager] Got pts update without local state', channelId); + } + return; + } + + if (pts === localPts + ptsCount) { + clearTimeout(PTS_TIMEOUTS.get(channelId)); + PTS_TIMEOUTS.delete(channelId); + + applyUpdate(update); + } else if (pts > localPts + ptsCount) { + ptsQueue.add(update); // Return update to queue + if (channelId === COMMON_BOX_QUEUE_ID) { + scheduleGetDifference(); + } else { + scheduleGetChannelDifference(channelId); + } + return; // Prevent endless loop + } + + popPtsQueue(channelId); +} + +function scheduleGetChannelDifference(channelId: string) { + if (PTS_TIMEOUTS.has(channelId)) return; + + const timeout = setTimeout(async () => { + await getChannelDifference(channelId); + PTS_TIMEOUTS.delete(channelId); + }, UPDATE_WAIT_TIMEOUT); + PTS_TIMEOUTS.set(channelId, timeout); +} + +function scheduleGetDifference() { + if (seqTimeout) return; + + seqTimeout = setTimeout(async () => { + await getDifference(); + seqTimeout = undefined; + }, UPDATE_WAIT_TIMEOUT); +} + +function getUpdateChannelId(update: Update) { + if ('channelId' in update && 'pts' in update) { + return buildApiPeerId(update.channelId, 'channel'); + } + + if (update instanceof GramJs.UpdateNewChannelMessage || update instanceof GramJs.UpdateEditChannelMessage) { + const peer = update.message.peerId as GramJs.PeerChannel; + return buildApiPeerId(peer.channelId, 'channel'); + } + + return COMMON_BOX_QUEUE_ID; +} + +export async function getDifference() { + if (!isInited) { + throw new Error('UpdatesManager not initialized'); + } + + if (!localDb.commonBoxState?.date) { + forceSync(); + return; + } + + const response = await invoke(new GramJs.updates.GetDifference({ + pts: localDb.commonBoxState.pts, + date: localDb.commonBoxState.date, + qts: localDb.commonBoxState.qts, + })); + + SEQ_QUEUE.clear(); + PTS_QUEUE.get(COMMON_BOX_QUEUE_ID)?.clear(); + + if (!response || response instanceof GramJs.updates.DifferenceTooLong) { + forceSync(); + return; + } + + if (response instanceof GramJs.updates.DifferenceEmpty) { + localDb.commonBoxState.seq = response.seq; + localDb.commonBoxState.date = response.date; + return; + } + + processDifference(response); + + const newState = response instanceof GramJs.updates.DifferenceSlice ? response.intermediateState : response.state; + applyState(newState); + + if (response instanceof GramJs.updates.DifferenceSlice) { + getDifference(); + } +} + +async function getChannelDifference(channelId: string) { + const channel = localDb.chats[channelId]; + if (!channel || !(channel instanceof GramJs.Channel) || !channel.accessHash || !localDb.channelPtsById[channelId]) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error('[UpdateManager] Channel for difference not found', channelId, channel); + } + return; + } + + const response = await invoke(new GramJs.updates.GetChannelDifference({ + channel: buildInputEntity(channelId, channel.accessHash.toString()) as GramJs.InputChannel, + pts: localDb.channelPtsById[channelId], + filter: new GramJs.ChannelMessagesFilterEmpty(), + limit: CHANNEL_DIFFERENCE_LIMIT, + })); + + PTS_QUEUE.delete(channelId); + + if (!response) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('[UpdatesManager] Failed to get ChannelDifference', channelId, channel); + } + return; + } + + if (response instanceof GramJs.updates.ChannelDifferenceTooLong) { + forceSync(); + return; + } + + localDb.channelPtsById[channelId] = response.pts; + + if (response instanceof GramJs.updates.ChannelDifferenceEmpty) { + return; + } + + processDifference(response); + + if (!response.final) { + getChannelDifference(channelId); + } +} + +function forceSync() { + reset(); + + requestSync(); + + loadRemoteState(); +} + +export function reset() { + PTS_QUEUE.clear(); + SEQ_QUEUE.clear(); + + clearTimeout(seqTimeout); + seqTimeout = undefined; + + PTS_TIMEOUTS.forEach((timeout) => { + clearTimeout(timeout); + }); + PTS_TIMEOUTS.clear(); + + localDb.commonBoxState = {}; + + Object.keys(localDb.channelPtsById).forEach((channelId) => { + localDb.channelPtsById[channelId] = 0; + }); + + isInited = false; +} + +async function loadRemoteState() { + const remoteState = await invoke(new GramJs.updates.GetState()); + if (!remoteState) return; + + applyState(remoteState); + + isInited = true; +} + +function processDifference( + difference: GramJs.updates.Difference | GramJs.updates.DifferenceSlice | GramJs.updates.ChannelDifference, +) { + difference.newMessages.forEach((message) => { + updater(new GramJs.UpdateNewMessage({ + message, + pts: 0, + ptsCount: 0, + })); + }); + + addEntitiesToLocalDb(difference.users); + addEntitiesToLocalDb(difference.chats); + + dispatchUserAndChatUpdates(difference.users); + dispatchUserAndChatUpdates(difference.chats); + + difference.otherUpdates.forEach((update) => { + processUpdate(update); + }); +} + +function getPtsCount(update: PtsUpdate) { + return 'ptsCount' in update ? update.ptsCount : 0; +} + +function seqComparator(a: SeqUpdate, b: SeqUpdate) { + const seqA = 'seqStart' in a ? a.seqStart : a.seq; + const seqB = 'seqStart' in b ? b.seqStart : b.seq; + + return seqA - seqB; +} + +function ptsComparator(a: PtsUpdate, b: PtsUpdate) { + const diff = a.pts - b.pts; + if (diff !== 0) { + return diff; + } + + return getPtsCount(b) - getPtsCount(a); +} diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 56bbc5df4..8835c0bc3 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -42,7 +42,7 @@ import localDb from './localDb'; import { omitVirtualClassFields } from './apiBuilders/helpers'; import { addMessageToLocalDb, - addEntitiesWithPhotosToLocalDb, + addEntitiesToLocalDb, addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes, @@ -68,7 +68,7 @@ import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols import { buildApiBotMenuButton } from './apiBuilders/bots'; import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute'; -type Update = ( +export type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } ) | typeof connection.UpdateConnectionState; @@ -82,7 +82,7 @@ export function init(_onUpdate: OnApiUpdate) { const sentMessageIds = new Set(); -function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) { +export function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) { entities .filter((e) => e instanceof GramJs.User) .map(buildApiUser) @@ -117,6 +117,12 @@ function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat }); } +export function requestSync() { + onUpdate({ + '@type': 'requestSync', + }); +} + export function updater(update: Update) { if (update instanceof connection.UpdateServerTimeOffset) { setServerTimeOffset(update.timeOffset); @@ -160,7 +166,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -930,7 +936,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -948,7 +954,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -961,7 +967,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -975,7 +981,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -1013,7 +1019,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } @@ -1027,7 +1033,7 @@ export function updater(update: Update) { // eslint-disable-next-line no-underscore-dangle const entities = update._entities; if (entities) { - addEntitiesWithPhotosToLocalDb(entities); + addEntitiesToLocalDb(entities); dispatchUserAndChatUpdates(entities); } onUpdate({ '@type': 'updateConfig' }); diff --git a/src/api/gramjs/worker/provider.ts b/src/api/gramjs/worker/provider.ts index 1138fcb02..5b7c3b55c 100644 --- a/src/api/gramjs/worker/provider.ts +++ b/src/api/gramjs/worker/provider.ts @@ -10,6 +10,7 @@ import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../../config'; import generateIdFor from '../../../util/generateIdFor'; import { pause } from '../../../util/schedulers'; import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole'; +import Deferred from '../../../util/Deferred'; type RequestStates = { messageId: string; @@ -32,6 +33,9 @@ const savedLocalDb: LocalDb = { stickerSets: {}, photos: {}, webDocuments: {}, + + commonBoxState: {}, + channelPtsById: {}, }; // TODO Re-use `util/WorkerConnector.ts` @@ -57,6 +61,10 @@ export function initApiOnMasterTab(initialArgs: ApiInitialArgs) { let updateCallback: OnApiUpdate; +let localApiRequestsQueue: { fnName: any; args: any; deferred: Deferred }[] = []; +let apiRequestsQueue: { fnName: any; args: any; deferred: Deferred }[] = []; +let isInited = false; + export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) { updateCallback = onUpdate; @@ -82,6 +90,22 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) { return makeRequest({ type: 'initApi', args: [initialArgs, savedLocalDb], + }).then(() => { + isInited = true; + + apiRequestsQueue.forEach((request) => { + callApi(request.fnName, ...request.args) + .then(request.deferred.resolve) + .catch(request.deferred.reject); + }); + apiRequestsQueue = []; + + localApiRequestsQueue.forEach((request) => { + callApiLocal(request.fnName, ...request.args) + .then(request.deferred.resolve) + .catch(request.deferred.reject); + }); + localApiRequestsQueue = []; }); } @@ -108,13 +132,11 @@ export function callApiOnMasterTab(payload: any) { * Mostly needed to disconnect worker when re-electing master */ export function callApiLocal(fnName: T, ...args: MethodArgs) { - if (!worker) { - if (DEBUG) { - // eslint-disable-next-line no-console - console.warn('API is not initialized'); - } + if (!isInited) { + const deferred = new Deferred(); + localApiRequestsQueue.push({ fnName, args, deferred }); - return undefined; + return deferred.promise as MethodResponse; } const promise = makeRequest({ @@ -150,13 +172,11 @@ export function callApiLocal(fnName: T, ...args: Method } export function callApi(fnName: T, ...args: MethodArgs) { - if (!worker && isMasterTab) { - if (DEBUG) { - // eslint-disable-next-line no-console - console.warn('API is not initialized'); - } + if (!isInited && isMasterTab) { + const deferred = new Deferred(); + apiRequestsQueue.push({ fnName, args, deferred }); - return undefined; + return deferred.promise as MethodResponse; } const promise = isMasterTab ? makeRequest({ @@ -236,7 +256,8 @@ function subscribeToWorker(onUpdate: OnApiUpdate) { }); } -export function handleMethodResponse(data: { messageId: string; +export function handleMethodResponse(data: { + messageId: string; response?: ThenArg>; error?: { message: string }; }) { @@ -250,7 +271,8 @@ export function handleMethodResponse(data: { messageId: string; } } -export function handleMethodCallback(data: { messageId: string; +export function handleMethodCallback(data: { + messageId: string; callbackArgs: any[]; }) { requestStates.get(data.messageId)?.callback?.(...data.callbackArgs); diff --git a/src/api/gramjs/worker/worker.ts b/src/api/gramjs/worker/worker.ts index 447c7119a..91ddea830 100644 --- a/src/api/gramjs/worker/worker.ts +++ b/src/api/gramjs/worker/worker.ts @@ -21,7 +21,15 @@ onmessage = async (message: OriginMessageEvent) => { switch (data.type) { case 'initApi': { - await initApi(onUpdate, data.args[0], data.args[1]); + const { messageId, args } = data; + await initApi(onUpdate, args[0], args[1]); + if (messageId) { + sendToOrigin({ + type: 'methodResponse', + messageId, + response: true, + }); + } break; } case 'callMethod': { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index dc005ba9a..87f15d0ed 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -619,6 +619,10 @@ export type ApiRequestInitApi = { '@type': 'requestInitApi'; }; +export type ApiRequestSync = { + '@type': 'requestSync'; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -645,7 +649,7 @@ export type ApiUpdate = ( ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | - ApiUpdateRecentReactions | ApiRequestInitApi + ApiUpdateRecentReactions | ApiRequestInitApi | ApiRequestSync ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/components/common/AnimatedIconFromSticker.tsx b/src/components/common/AnimatedIconFromSticker.tsx index 5de9da42e..775e1ebc3 100644 --- a/src/components/common/AnimatedIconFromSticker.tsx +++ b/src/components/common/AnimatedIconFromSticker.tsx @@ -12,11 +12,11 @@ import AnimatedIconWithPreview from './AnimatedIconWithPreview'; type OwnProps = Partial - & { sticker?: ApiSticker; noLoad?: boolean; forcePreview?: boolean; lastSyncTime?: number }; + & { sticker?: ApiSticker; noLoad?: boolean; forcePreview?: boolean }; function AnimatedIconFromSticker(props: OwnProps) { const { - sticker, noLoad, forcePreview, lastSyncTime, ...otherProps + sticker, noLoad, forcePreview, ...otherProps } = props; const thumbDataUri = sticker?.thumbnail?.dataUri; @@ -25,9 +25,8 @@ function AnimatedIconFromSticker(props: OwnProps) { sticker ? getStickerPreviewHash(sticker.id) : undefined, noLoad && !forcePreview, ApiMediaFormat.BlobUrl, - lastSyncTime, ); - const tgsUrl = useMedia(localMediaHash, noLoad, undefined, lastSyncTime); + const tgsUrl = useMedia(localMediaHash, noLoad); return ( = ({ uploadProgress, origin, date, - lastSyncTime, noAvatars, className, isSelectable, @@ -114,7 +112,7 @@ const Audio: FC = ({ const { isMobile } = useAppLayout(); const [isActivated, setIsActivated] = useState(false); - const shouldLoad = (isActivated || PRELOAD) && lastSyncTime; + const shouldLoad = isActivated || PRELOAD; const coverHash = getMessageMediaHash(message, 'pictogram'); const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl); diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 018508e6d..61c0649f5 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -53,7 +53,6 @@ type OwnProps = { withVideo?: boolean; loopIndefinitely?: boolean; noPersonalPhoto?: boolean; - lastSyncTime?: number; observeIntersection?: ObserveFn; onClick?: (e: ReactMouseEvent, hasMedia: boolean) => void; }; @@ -69,7 +68,6 @@ const Avatar: FC = ({ isSavedMessages, withVideo, loopIndefinitely, - lastSyncTime, noPersonalPhoto, onClick, }) => { @@ -98,8 +96,8 @@ const Avatar: FC = ({ } } - const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl, lastSyncTime); - const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl, lastSyncTime); + const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); + const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl); const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl); // `videoBlobUrl` can be taken from memory cache, so we need to check `shouldLoadVideo` again const shouldPlayVideo = Boolean(videoBlobUrl && shouldLoadVideo); diff --git a/src/components/common/ChatExtra.tsx b/src/components/common/ChatExtra.tsx index 2b3909096..4726795e5 100644 --- a/src/components/common/ChatExtra.tsx +++ b/src/components/common/ChatExtra.tsx @@ -1,10 +1,9 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useState, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { GlobalState } from '../../global/types'; +import type { FC } from '../../lib/teact/teact'; import type { ApiChat, ApiCountryCode, ApiUser, ApiUsername, } from '../../api/types'; @@ -56,13 +55,11 @@ type StateProps = topicId?: number; description?: string; chatInviteLink?: string; - } - & Pick; + }; const runDebounced = debounce((cb) => cb(), 500, false); const ChatExtra: FC = ({ - lastSyncTime, user, chat, forceShowSelf, @@ -96,10 +93,9 @@ const ChatExtra: FC = ({ }, [isMuted]); useEffect(() => { - if (lastSyncTime && userId) { - loadFullUser({ userId }); - } - }, [loadFullUser, userId, lastSyncTime]); + if (!userId) return; + loadFullUser({ userId }); + }, [userId]); const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); @@ -264,7 +260,7 @@ const ChatExtra: FC = ({ export default memo(withGlobal( (global, { chatOrUserId }): StateProps => { - const { lastSyncTime, countryList: { phoneCodes: phoneCodeList } } = global; + const { countryList: { phoneCodes: phoneCodeList } } = global; const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; @@ -284,7 +280,6 @@ export default memo(withGlobal( ); return { - lastSyncTime, phoneCodeList, chat, user, diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 68410ec8c..a5aed21e7 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -93,7 +93,7 @@ const Document: FC = ({ const documentHash = getMessageMediaHash(message, 'download'); const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress( - documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, undefined, true, + documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, true, ); const isLoaded = Boolean(mediaData); diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 2ec8ced61..4cc0756f0 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -1,12 +1,10 @@ -import type { MouseEvent as ReactMouseEvent } from 'react'; -import type { FC } from '../../lib/teact/teact'; import React, { useEffect, memo, useMemo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { ApiChat, ApiTopic, ApiThreadInfo, ApiTypingStatus, } from '../../api/types'; -import type { GlobalState } from '../../global/types'; import type { LangFn } from '../../hooks/useLang'; import { MediaViewerOrigin } from '../../types'; @@ -62,8 +60,7 @@ type StateProps = onlineCount?: number; areMessagesLoaded: boolean; messagesCount?: number; - } - & Pick; + }; const GroupChatInfo: FC = ({ typingStatus, @@ -82,7 +79,6 @@ const GroupChatInfo: FC = ({ chat, onlineCount, areMessagesLoaded, - lastSyncTime, topic, messagesCount, onClick, @@ -93,19 +89,21 @@ const GroupChatInfo: FC = ({ loadProfilePhotos, } = getActions(); + const lang = useLang(); + const isSuperGroup = chat && isChatSuperGroup(chat); const isTopic = Boolean(chat?.isForum && threadInfo && topic); const { id: chatId, isMin, isRestricted } = chat || {}; useEffect(() => { - if (chatId && !isMin && lastSyncTime) { + if (chatId && !isMin) { if (withFullInfo) loadFullChat({ chatId }); if (withMediaViewer) loadProfilePhotos({ profileId: chatId }); } - }, [chatId, isMin, lastSyncTime, withFullInfo, loadFullChat, loadProfilePhotos, isSuperGroup, withMediaViewer]); + }, [chatId, isMin, withFullInfo, loadFullChat, loadProfilePhotos, isSuperGroup, withMediaViewer]); const handleAvatarViewerOpen = useLastCallback( - (e: ReactMouseEvent, hasMedia: boolean) => { + (e: React.MouseEvent, hasMedia: boolean) => { if (chat && hasMedia) { e.stopPropagation(); openMediaViewer({ @@ -117,7 +115,6 @@ const GroupChatInfo: FC = ({ }, ); - const lang = useLang(); const mainUsername = useMemo(() => chat && withUsername && getMainUsername(chat), [chat, withUsername]); if (!chat) { @@ -225,7 +222,6 @@ function getGroupStatus(lang: LangFn, chat: ApiChat) { export default memo(withGlobal( (global, { chatId, threadId }): StateProps => { - const { lastSyncTime } = global; const chat = selectChat(global, chatId); const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; @@ -234,7 +230,6 @@ export default memo(withGlobal( const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!); return { - lastSyncTime, chat, threadInfo, onlineCount, diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 1ae59c7b1..64625547f 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -5,7 +5,6 @@ import type { FC } from '../../lib/teact/teact'; import type { ApiUser, ApiTypingStatus, ApiUserStatus, ApiChatMember, } from '../../api/types'; -import type { GlobalState } from '../../global/types'; import { MediaViewerOrigin } from '../../types'; import { @@ -49,8 +48,7 @@ type StateProps = userStatus?: ApiUserStatus; isSavedMessages?: boolean; areMessagesLoaded: boolean; - } - & Pick; + }; const PrivateChatInfo: FC = ({ typingStatus, @@ -69,7 +67,6 @@ const PrivateChatInfo: FC = ({ userStatus, isSavedMessages, areMessagesLoaded, - lastSyncTime, adminMember, }) => { const { @@ -78,14 +75,16 @@ const PrivateChatInfo: FC = ({ loadProfilePhotos, } = getActions(); + const lang = useLang(); + const { id: userId } = user || {}; useEffect(() => { - if (userId && lastSyncTime) { + if (userId) { if (withFullInfo) loadFullUser({ userId }); if (withMediaViewer) loadProfilePhotos({ profileId: userId }); } - }, [userId, loadFullUser, loadProfilePhotos, lastSyncTime, withFullInfo, withMediaViewer]); + }, [userId, withFullInfo, withMediaViewer]); const handleAvatarViewerOpen = useLastCallback( (e: React.MouseEvent, hasMedia: boolean) => { @@ -100,7 +99,6 @@ const PrivateChatInfo: FC = ({ }, ); - const lang = useLang(); const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]); if (!user) { @@ -189,14 +187,12 @@ const PrivateChatInfo: FC = ({ export default memo(withGlobal( (global, { userId, forceShowSelf }): StateProps => { - const { lastSyncTime } = global; const user = selectUser(global, userId); const userStatus = selectUserStatus(global, userId); const isSavedMessages = !forceShowSelf && user && user.isSelf; const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); return { - lastSyncTime, user, userStatus, isSavedMessages, diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 5a97413a0..3a8fa90b5 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -33,7 +33,6 @@ type OwnProps = { user?: ApiUser; isSavedMessages?: boolean; photo?: ApiPhoto; - lastSyncTime?: number; canPlayVideo: boolean; onClick: NoneToVoidFunction; }; @@ -44,7 +43,6 @@ const ProfilePhoto: FC = ({ photo, isSavedMessages, canPlayVideo, - lastSyncTime, onClick, }) => { // eslint-disable-next-line no-null/no-null @@ -60,13 +58,13 @@ const ProfilePhoto: FC = ({ const { isVideo } = photo || {}; const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal'); - const avatarBlobUrl = useMedia(avatarHash, undefined, undefined, lastSyncTime); + const avatarBlobUrl = useMedia(avatarHash); const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`; - const photoBlobUrl = useMedia(photoHash, undefined, undefined, lastSyncTime); + const photoBlobUrl = useMedia(photoHash); const videoHash = canHaveMedia && photo && isVideo && getVideoAvatarMediaHash(photo); - const videoBlobUrl = useMedia(videoHash, undefined, undefined, lastSyncTime); + const videoBlobUrl = useMedia(videoHash); const fullMediaData = videoBlobUrl || photoBlobUrl; const [isVideoReady, markVideoReady] = useFlag(); diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index 776a8e30d..48fb256b0 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -48,7 +48,6 @@ type OwnProps = { withSharedAnimation?: boolean; sharedCanvasRef?: React.RefObject; withTranslucentThumb?: boolean; // With shared canvas thumbs are opaque by default to provide better transition effect - cacheBuster?: number; onVideoEnded?: AnyToVoidFunction; onAnimatedStickerLoop?: AnyToVoidFunction; }; @@ -77,7 +76,6 @@ const StickerView: FC = ({ withSharedAnimation, withTranslucentThumb, sharedCanvasRef, - cacheBuster, onVideoEnded, onAnimatedStickerLoop, }) => { @@ -102,9 +100,7 @@ const StickerView: FC = ({ const thumbDataUri = useThumbnail(sticker); // Use preview instead of thumb but only if it's already loaded or when playing an animation is disabled const previewMediaDataFromCache: string | undefined = mediaLoader.getFromMemory(previewMediaHash); - const previewMediaData = useMedia( - previewMediaHash, Boolean(previewMediaDataFromCache || !noPlay), undefined, cacheBuster, - ); + const previewMediaData = useMedia(previewMediaHash, Boolean(previewMediaDataFromCache || !noPlay)); const thumbData = customColor ? thumbDataUri : (previewMediaData || thumbDataUri); const shouldForcePreview = isUnsupportedVideo || (isStatic && isSmall); @@ -113,7 +109,7 @@ const StickerView: FC = ({ // If preloaded preview is forced, it will render as thumb, so no need to load it again const shouldSkipFullMedia = Boolean(fullMediaHash === previewMediaHash && previewMediaData); - const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia, undefined, cacheBuster); + const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia); // If Lottie data is loaded we will only render thumb if it's good enough (from preview) const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !previewMediaData)); // Delay mounting on Android until heavy animation ends @@ -129,7 +125,7 @@ const StickerView: FC = ({ const coords = useCoordsInSharedCanvas(containerRef, sharedCanvasRef); // Preload preview for Message Input and local message - useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster); + useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview); const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []); const renderId = [ diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index dac029832..e0e18d170 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -88,7 +88,6 @@ type StateProps = { isSelectedForum?: boolean; canScrollDown?: boolean; canChangeFolder?: boolean; - lastSyncTime?: number; lastMessageTopic?: ApiTopic; typingStatus?: ApiTypingStatus; withInterfaceAnimations?: boolean; @@ -117,7 +116,6 @@ const Chat: FC = ({ isSelectedForum, canScrollDown, canChangeFolder, - lastSyncTime, lastMessageTopic, typingStatus, onDragEnter, @@ -219,10 +217,10 @@ const Chat: FC = ({ // Load the forum topics to display unread count badge useEffect(() => { - if (isIntersecting && lastSyncTime && isForum && chat && chat.listedTopicIds === undefined) { + if (isIntersecting && isForum && chat && chat.listedTopicIds === undefined) { loadTopics({ chatId }); } - }, [chat, chatId, isForum, isIntersecting, lastSyncTime, loadTopics]); + }, [chat, chatId, isForum, isIntersecting]); if (!chat) { return undefined; @@ -254,7 +252,6 @@ const Chat: FC = ({ user={user} userStatus={userStatus} isSavedMessages={user?.isSelf} - lastSyncTime={lastSyncTime} /> {chat.isCallActive && chat.isCallNotEmpty && ( @@ -363,7 +360,6 @@ export default memo(withGlobal( isSelectedForum, canScrollDown: isSelected && messageListType === 'thread', canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1, - lastSyncTime: global.lastSyncTime, ...(isOutgoing && chat.lastMessage && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage), }), diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 850f3b55e..b3e119e13 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -43,7 +43,6 @@ type StateProps = { orderedFolderIds?: number[]; activeChatFolder: number; currentUserId?: string; - lastSyncTime?: number; shouldSkipHistoryAnimations?: boolean; maxFolders: number; maxFolderInvites: number; @@ -63,7 +62,6 @@ const ChatFolders: FC = ({ activeChatFolder, currentUserId, isForumPanelOpen, - lastSyncTime, shouldSkipHistoryAnimations, maxFolders, shouldHideFolderTabs, @@ -88,10 +86,8 @@ const ChatFolders: FC = ({ const lang = useLang(); useEffect(() => { - if (lastSyncTime) { - loadChatFolders(); - } - }, [lastSyncTime, loadChatFolders]); + loadChatFolders(); + }, []); const allChatsFolder: ApiChatFolder = useMemo(() => { return { @@ -275,7 +271,6 @@ const ChatFolders: FC = ({ folderId={isFolder ? activeFolder.id : undefined} isActive={isActive} isForumPanelOpen={isForumPanelOpen} - lastSyncTime={lastSyncTime} foldersDispatch={foldersDispatch} onSettingsScreenSelect={onSettingsScreenSelect} onLeftColumnContentChange={onLeftColumnContentChange} @@ -331,7 +326,6 @@ export default memo(withGlobal( }, }, currentUserId, - lastSyncTime, archiveSettings, } = global; const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global); @@ -342,7 +336,6 @@ export default memo(withGlobal( orderedFolderIds, activeChatFolder, currentUserId, - lastSyncTime, shouldSkipHistoryAnimations, hasArchivedChats: Boolean(archived?.length), maxFolders: selectCurrentLimit(global, 'dialogFilters'), diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index bf8e3c954..3eeb7c2f5 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -41,7 +41,6 @@ type OwnProps = { canDisplayArchive?: boolean; archiveSettings: GlobalState['archiveSettings']; isForumPanelOpen?: boolean; - lastSyncTime?: number; foldersDispatch: FolderEditDispatch; onSettingsScreenSelect: (screen: SettingsScreens) => void; onLeftColumnContentChange: (content: LeftColumnContent) => void; diff --git a/src/components/left/main/ForumPanel.tsx b/src/components/left/main/ForumPanel.tsx index 443e5311c..3b89c263f 100644 --- a/src/components/left/main/ForumPanel.tsx +++ b/src/components/left/main/ForumPanel.tsx @@ -54,7 +54,6 @@ type OwnProps = { type StateProps = { chat?: ApiChat; currentTopicId?: number; - lastSyncTime?: number; withInterfaceAnimations?: boolean; }; @@ -65,7 +64,6 @@ const ForumPanel: FC = ({ currentTopicId, isOpen, isHidden, - lastSyncTime, onTopicSearch, onCloseAnimationEnd, onOpenAnimationStart, @@ -85,10 +83,10 @@ const ForumPanel: FC = ({ const { isMobile } = useAppLayout(); useEffect(() => { - if (lastSyncTime && chat && !chat.topics) { + if (chat && !chat.topics) { loadTopics({ chatId: chat.id }); } - }, [chat, lastSyncTime, loadTopics]); + }, [chat, loadTopics]); const [isScrolled, setIsScrolled] = useState(false); const lang = useLang(); @@ -126,7 +124,7 @@ const ForumPanel: FC = ({ const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id); const [viewportIds, getMore] = useInfiniteScroll(() => { - if (!chat || !lastSyncTime) return; + if (!chat) return; loadTopics({ chatId: chat.id }); }, orderedIds, !chat?.topicsCount || orderedIds.length >= chat.topicsCount, TOPICS_SLICE); @@ -294,7 +292,6 @@ export default memo(withGlobal( return { chat, - lastSyncTime: global.lastSyncTime, currentTopicId: chatId === currentChatId ? currentThreadId : undefined, withInterfaceAnimations: selectCanAnimateInterface(global), }; diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index 9b73c4de9..a666d6b99 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -1,16 +1,17 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import { AudioOrigin, LoadMoreDirection } from '../../../types'; +import type { StateProps } from './helpers/createMapStateToProps'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; -import type { StateProps } from './helpers/createMapStateToProps'; import { createMapStateToProps } from './helpers/createMapStateToProps'; import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; import { getSenderName } from './helpers/getSenderName'; import { throttle } from '../../../util/schedulers'; + import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useLang from '../../../hooks/useLang'; @@ -35,7 +36,6 @@ const AudioResults: FC = ({ usersById, globalMessagesByChatId, foundIds, - lastSyncTime, activeDownloads, }) => { const { @@ -47,7 +47,7 @@ const AudioResults: FC = ({ const lang = useLang(); const currentType = isVoice ? 'voice' : 'audio'; const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: currentType, @@ -55,7 +55,7 @@ const AudioResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [currentType, lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [currentType, searchMessagesGlobal, searchQuery]); const foundMessages = useMemo(() => { if (!foundIds || !globalMessagesByChatId) { @@ -98,7 +98,6 @@ const AudioResults: FC = ({ origin={AudioOrigin.Search} senderTitle={getSenderName(lang, message, chatsById, usersById)} date={message.date} - lastSyncTime={lastSyncTime} className="scroll-item" onPlay={handlePlayAudio} onDateClick={handleMessageFocus} diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index d8ab39c8d..89d333d77 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -44,7 +44,6 @@ type StateProps = { chat?: ApiChat; privateChatUser?: ApiUser; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; - lastSyncTime?: number; }; const ChatMessage: FC = ({ @@ -53,7 +52,6 @@ const ChatMessage: FC = ({ chatId, chat, privateChatUser, - lastSyncTime, }) => { const { focusMessage } = getActions(); @@ -85,7 +83,6 @@ const ChatMessage: FC = ({ chat={chat} user={privateChatUser} isSavedMessages={privateChatUser?.isSelf} - lastSyncTime={lastSyncTime} />
@@ -147,7 +144,6 @@ export default memo(withGlobal( return { chat, - lastSyncTime: global.lastSyncTime, ...(privateChatUserId && { privateChatUser }), }; }, diff --git a/src/components/left/search/ChatMessageResults.tsx b/src/components/left/search/ChatMessageResults.tsx index fdaff8433..64a84569e 100644 --- a/src/components/left/search/ChatMessageResults.tsx +++ b/src/components/left/search/ChatMessageResults.tsx @@ -1,7 +1,7 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; @@ -9,6 +9,7 @@ import { selectTabState } from '../../../global/selectors'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { throttle } from '../../../util/schedulers'; import { renderMessageSummary } from '../../common/helpers/renderMessageText'; + import useLang from '../../../hooks/useLang'; import useAppLayout from '../../../hooks/useAppLayout'; @@ -33,7 +34,6 @@ type StateProps = { fetchingStatus?: { chats?: boolean; messages?: boolean }; foundTopicIds?: number[]; searchChatId?: string; - lastSyncTime?: number; }; const runThrottled = throttle((cb) => cb(), 500, true); @@ -45,7 +45,6 @@ const ChatMessageResults: FC = ({ globalMessagesByChatId, chatsById, fetchingStatus, - lastSyncTime, foundTopicIds, searchChatId, onSearchDateSelect, @@ -57,7 +56,7 @@ const ChatMessageResults: FC = ({ const { isMobile } = useAppLayout(); const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: 'text', @@ -65,7 +64,7 @@ const ChatMessageResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [searchQuery]); const handleTopicClick = useCallback( (id: number) => { @@ -167,7 +166,7 @@ const ChatMessageResults: FC = ({ export default memo(withGlobal( (global): StateProps => { const { byId: chatsById } = global.chats; - const { currentUserId, messages: { byChatId: globalMessagesByChatId }, lastSyncTime } = global; + const { currentUserId, messages: { byChatId: globalMessagesByChatId } } = global; const { fetchingStatus, resultsByType, foundTopicIds, chatId: searchChatId, } = selectTabState(global).globalSearch; @@ -181,7 +180,6 @@ export default memo(withGlobal( chatsById, fetchingStatus, foundTopicIds, - lastSyncTime, searchChatId, }; }, diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 42734ac11..5caead77c 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -1,9 +1,9 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; @@ -49,7 +49,6 @@ type StateProps = { globalMessagesByChatId?: Record }>; chatsById: Record; fetchingStatus?: { chats?: boolean; messages?: boolean }; - lastSyncTime?: number; }; const MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH = 4; @@ -58,10 +57,21 @@ const LESS_LIST_ITEMS_AMOUNT = 5; const runThrottled = throttle((cb) => cb(), 500, false); const ChatResults: FC = ({ - searchQuery, searchDate, dateSearchQuery, currentUserId, - localContactIds, localChatIds, localUserIds, globalChatIds, globalUserIds, - foundIds, globalMessagesByChatId, chatsById, fetchingStatus, lastSyncTime, - onReset, onSearchDateSelect, + searchQuery, + searchDate, + dateSearchQuery, + currentUserId, + localContactIds, + localChatIds, + localUserIds, + globalChatIds, + globalUserIds, + foundIds, + globalMessagesByChatId, + chatsById, + fetchingStatus, + onReset, + onSearchDateSelect, }) => { const { openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, @@ -77,7 +87,7 @@ const ChatResults: FC = ({ const [shouldShowMoreGlobal, setShouldShowMoreGlobal] = useState(false); const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: 'text', @@ -85,7 +95,7 @@ const ChatResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [searchQuery]); const handleChatClick = useCallback( (id: string) => { @@ -293,6 +303,9 @@ export default memo(withGlobal( const { byId: chatsById } = global.chats; const { userIds: localContactIds } = global.contactList || {}; + const { + currentUserId, messages, + } = global; if (!localContactIds) { return { @@ -300,9 +313,6 @@ export default memo(withGlobal( }; } - const { - currentUserId, messages, lastSyncTime, - } = global; const { fetchingStatus, globalResults, localResults, resultsByType, } = selectTabState(global).globalSearch; @@ -322,7 +332,6 @@ export default memo(withGlobal( globalMessagesByChatId, chatsById, fetchingStatus, - lastSyncTime, }; }, )(ChatResults)); diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index a336678ff..f2fd1c6ee 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -1,20 +1,21 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo, useRef, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiMessage } from '../../../api/types'; +import type { StateProps } from './helpers/createMapStateToProps'; import { LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; -import type { StateProps } from './helpers/createMapStateToProps'; import { createMapStateToProps } from './helpers/createMapStateToProps'; import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; import { getSenderName } from './helpers/getSenderName'; import { throttle } from '../../../util/schedulers'; import { getMessageDocument } from '../../../global/helpers'; + import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useLang from '../../../hooks/useLang'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; @@ -41,7 +42,6 @@ const FileResults: FC = ({ globalMessagesByChatId, foundIds, activeDownloads, - lastSyncTime, }) => { const { searchMessagesGlobal, @@ -59,7 +59,7 @@ const FileResults: FC = ({ }); const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: CURRENT_TYPE, @@ -67,7 +67,7 @@ const FileResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [searchQuery]); const foundMessages = useMemo(() => { if (!foundIds || !globalMessagesByChatId) { diff --git a/src/components/left/search/LinkResults.tsx b/src/components/left/search/LinkResults.tsx index a0143761d..350816d7d 100644 --- a/src/components/left/search/LinkResults.tsx +++ b/src/components/left/search/LinkResults.tsx @@ -1,18 +1,19 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo, useRef, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import { LoadMoreDirection } from '../../../types'; +import type { StateProps } from './helpers/createMapStateToProps'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; -import type { StateProps } from './helpers/createMapStateToProps'; import { createMapStateToProps } from './helpers/createMapStateToProps'; import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; import { getSenderName } from './helpers/getSenderName'; import { throttle } from '../../../util/schedulers'; + import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useLang from '../../../hooks/useLang'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; @@ -38,7 +39,6 @@ const LinkResults: FC = ({ usersById, globalMessagesByChatId, foundIds, - lastSyncTime, isChatProtected, }) => { const { @@ -57,7 +57,7 @@ const LinkResults: FC = ({ }); const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: CURRENT_TYPE, @@ -65,7 +65,7 @@ const LinkResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [searchQuery]); const foundMessages = useMemo(() => { if (!foundIds || !globalMessagesByChatId) { diff --git a/src/components/left/search/MediaResults.tsx b/src/components/left/search/MediaResults.tsx index 7539f823b..89eef7941 100644 --- a/src/components/left/search/MediaResults.tsx +++ b/src/components/left/search/MediaResults.tsx @@ -1,17 +1,18 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo, useRef, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; +import type { StateProps } from './helpers/createMapStateToProps'; import { LoadMoreDirection, MediaViewerOrigin } from '../../../types'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; -import type { StateProps } from './helpers/createMapStateToProps'; import { createMapStateToProps } from './helpers/createMapStateToProps'; import buildClassName from '../../../util/buildClassName'; import { throttle } from '../../../util/schedulers'; + import useLang from '../../../hooks/useLang'; import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; @@ -36,7 +37,6 @@ const MediaResults: FC = ({ isLoading, globalMessagesByChatId, foundIds, - lastSyncTime, isChatProtected, }) => { const { @@ -55,7 +55,7 @@ const MediaResults: FC = ({ }); const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { - if (lastSyncTime && direction === LoadMoreDirection.Backwards) { + if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ type: CURRENT_TYPE, @@ -63,7 +63,7 @@ const MediaResults: FC = ({ }); } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading - }, [lastSyncTime, searchMessagesGlobal, searchQuery]); + }, [searchMessagesGlobal, searchQuery]); const foundMessages = useMemo(() => { if (!foundIds || !globalMessagesByChatId) { diff --git a/src/components/left/search/helpers/createMapStateToProps.ts b/src/components/left/search/helpers/createMapStateToProps.ts index f769e6f80..35910f75d 100644 --- a/src/components/left/search/helpers/createMapStateToProps.ts +++ b/src/components/left/search/helpers/createMapStateToProps.ts @@ -13,7 +13,6 @@ export type StateProps = { usersById: Record; globalMessagesByChatId?: Record }>; foundIds?: string[]; - lastSyncTime?: number; searchChatId?: string; activeDownloads: TabState['activeDownloads']['byChatId']; isChatProtected?: boolean; @@ -49,7 +48,6 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) { searchChatId: chatId, activeDownloads, isChatProtected: chatId ? selectChat(global, chatId)?.isProtected : undefined, - lastSyncTime: global.lastSyncTime, }; }; } diff --git a/src/components/left/settings/SettingsLanguage.tsx b/src/components/left/settings/SettingsLanguage.tsx index bd6924c4b..d9adc227a 100644 --- a/src/components/left/settings/SettingsLanguage.tsx +++ b/src/components/left/settings/SettingsLanguage.tsx @@ -26,9 +26,7 @@ type OwnProps = { onScreenSelect: (screen: SettingsScreens) => void; }; -type StateProps = { - lastSyncTime?: number; -} & Pick; +type StateProps = Pick; const SettingsLanguage: FC = ({ isActive, @@ -36,7 +34,6 @@ const SettingsLanguage: FC = ({ language, canTranslate, doNotTranslate, - lastSyncTime, onScreenSelect, onReset, }) => { @@ -52,10 +49,10 @@ const SettingsLanguage: FC = ({ const lang = useLang(); useEffect(() => { - if (lastSyncTime && !languages?.length) { + if (!languages?.length) { loadLanguages(); } - }, [languages, lastSyncTime, loadLanguages]); + }, [languages]); const handleChange = useCallback((langCode: string) => { setSelectedLanguage(langCode); @@ -161,7 +158,6 @@ export default memo(withGlobal( } = global.settings.byKey; return { - lastSyncTime: global.lastSyncTime, languages, language, canTranslate, diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 9e4ee4083..c1c639698 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -1,7 +1,7 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useEffect } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import { SettingsScreens } from '../../../types'; import type { ApiUser } from '../../../api/types'; @@ -23,7 +23,6 @@ type OwnProps = { type StateProps = { sessionCount: number; currentUser?: ApiUser; - lastSyncTime?: number; canBuyPremium?: boolean; }; @@ -33,7 +32,6 @@ const SettingsMain: FC = ({ onReset, currentUser, sessionCount, - lastSyncTime, canBuyPremium, }) => { const { @@ -46,10 +44,10 @@ const SettingsMain: FC = ({ const profileId = currentUser?.id; useEffect(() => { - if (profileId && lastSyncTime) { + if (profileId) { loadProfilePhotos({ profileId }); } - }, [lastSyncTime, profileId, loadProfilePhotos]); + }, [profileId, loadProfilePhotos]); useHistoryBack({ isActive, @@ -57,10 +55,8 @@ const SettingsMain: FC = ({ }); useEffect(() => { - if (lastSyncTime) { - loadAuthorizations(); - } - }, [lastSyncTime, loadAuthorizations]); + loadAuthorizations(); + }, []); return (
@@ -160,12 +156,11 @@ const SettingsMain: FC = ({ export default memo(withGlobal( (global): StateProps => { - const { currentUserId, lastSyncTime } = global; + const { currentUserId } = global; return { sessionCount: global.activeSessions.orderedHashes.length, currentUser: currentUserId ? selectUser(global, currentUserId) : undefined, - lastSyncTime, canBuyPremium: !selectIsPremiumPurchaseBlocked(global), }; }, diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 008e73e95..552b84b40 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../lib/teact/teact'; import React, { useEffect, memo, useState, useRef, useLayoutEffect, } from '../../lib/teact/teact'; @@ -6,6 +5,7 @@ import { addExtraClass } from '../../lib/teact/teact-dom'; import { requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { getActions, getGlobal, withGlobal } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { LangCode } from '../../types'; import type { ApiAttachBot, @@ -45,7 +45,6 @@ import { Bundles, loadBundle } from '../../util/moduleLoader'; import updateIcon from '../../util/updateIcon'; import useLastCallback from '../../hooks/useLastCallback'; -import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBeforeUnload from '../../hooks/useBeforeUnload'; import useSyncEffect from '../../hooks/useSyncEffect'; @@ -104,7 +103,6 @@ export interface OwnProps { type StateProps = { isMasterTab?: boolean; chat?: ApiChat; - lastSyncTime?: number; isLeftColumnOpen: boolean; isMiddleColumnOpen: boolean; isRightColumnOpen: boolean; @@ -148,6 +146,7 @@ type StateProps = { chatlistModal?: TabState['chatlistModal']; noRightColumnAnimation?: boolean; withInterfaceAnimations?: boolean; + isSynced?: boolean; }; const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min @@ -158,7 +157,6 @@ const REACTION_PICKER_LOADING_DELAY_MS = 7000; // 7 sec let DEBUG_isLogged = false; const Main: FC = ({ - lastSyncTime, isMobile, isLeftColumnOpen, isMiddleColumnOpen, @@ -204,6 +202,7 @@ const Main: FC = ({ isMasterTab, chatlistModal, noRightColumnAnimation, + isSynced, }) => { const { initMain, @@ -299,7 +298,7 @@ const Main: FC = ({ // Initial API calls useEffect(() => { - if (lastSyncTime && isMasterTab) { + if (isMasterTab && isSynced) { updateIsOnline(true); loadConfig(); loadAppConfig(); @@ -320,45 +319,40 @@ const Main: FC = ({ loadRecentReactions(); loadFeaturedEmojiStickers(); } - }, [ - lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, - loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList, - loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, loadTopReactions, - loadDefaultStatusIcons, loadRecentReactions, loadRecentEmojiStatuses, isCurrentUserPremium, isMasterTab, initMain, - ]); + }, [isMasterTab, isSynced]); // Initial Premium API calls useEffect(() => { - if (lastSyncTime && isMasterTab && isCurrentUserPremium) { + if (isMasterTab && isCurrentUserPremium) { loadDefaultStatusIcons(); loadRecentEmojiStatuses(); } - }, [isCurrentUserPremium, isMasterTab, lastSyncTime, loadDefaultStatusIcons, loadRecentEmojiStatuses]); + }, [isCurrentUserPremium, isMasterTab]); // Language-based API calls useEffect(() => { - if (lastSyncTime && isMasterTab) { + if (isMasterTab) { if (language !== BASE_EMOJI_KEYWORD_LANG) { loadEmojiKeywords({ language: language! }); } loadCountryList({ langCode: language }); } - }, [language, lastSyncTime, loadCountryList, loadEmojiKeywords, isMasterTab]); + }, [language, isMasterTab]); // Re-fetch cached saved emoji for `localDb` - useEffectWithPrevDeps(([prevLastSyncTime]) => { - if (!prevLastSyncTime && lastSyncTime && isMasterTab) { + useEffect(() => { + if (isMasterTab) { loadCustomEmojis({ ids: Object.keys(getGlobal().customEmojis.byId), ignoreCache: true, }); } - }, [lastSyncTime, isMasterTab, loadCustomEmojis]); + }, [isMasterTab]); // Sticker sets useEffect(() => { - if (lastSyncTime && isMasterTab) { + if (isMasterTab && isSynced) { if (!addedSetIds || !addedCustomEmojiIds) { loadStickerSets(); loadFavoriteStickers(); @@ -368,45 +362,40 @@ const Main: FC = ({ loadAddedStickers(); } } - }, [ - lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds, - isMasterTab, - ]); + }, [addedSetIds, addedCustomEmojiIds, isMasterTab, isSynced]); // Check version when service chat is ready useEffect(() => { - if (lastSyncTime && isServiceChatReady && isMasterTab) { + if (isServiceChatReady && isMasterTab) { checkVersionNotification(); } - }, [lastSyncTime, isServiceChatReady, checkVersionNotification, isMasterTab]); + }, [isServiceChatReady, isMasterTab]); // Ensure time format useEffect(() => { - if (lastSyncTime && !wasTimeFormatSetManually) { + if (!wasTimeFormatSetManually) { ensureTimeFormat(); } - }, [lastSyncTime, wasTimeFormatSetManually, ensureTimeFormat]); + }, [wasTimeFormatSetManually]); // Parse deep link useEffect(() => { const parsedInitialLocationHash = parseInitialLocationHash(); - if (lastSyncTime && parsedInitialLocationHash?.tgaddr) { + if (parsedInitialLocationHash?.tgaddr) { processDeepLink(decodeURIComponent(parsedInitialLocationHash.tgaddr)); } - }, [lastSyncTime]); + }, []); - useEffectWithPrevDeps(([prevLastSyncTime]) => { + useEffect(() => { const parsedLocationHash = parseLocationHash(); if (!parsedLocationHash) return; - if (!prevLastSyncTime && lastSyncTime) { - openChat({ - id: parsedLocationHash.chatId, - threadId: parsedLocationHash.threadId, - type: parsedLocationHash.type, - }); - } - }, [lastSyncTime, openChat]); + openChat({ + id: parsedLocationHash.chatId, + threadId: parsedLocationHash.threadId, + type: parsedLocationHash.type, + }); + }, []); // Restore Transition slide class after async rendering useLayoutEffect(() => { @@ -579,7 +568,6 @@ export default memo(withGlobal( language, wasTimeFormatSetManually, }, }, - lastSyncTime, } = global; const { @@ -623,7 +611,6 @@ export default memo(withGlobal( const deleteFolderDialog = deleteFolderDialogModal ? selectChatFolder(global, deleteFolderDialogModal) : undefined; return { - lastSyncTime, isLeftColumnOpen: isLeftColumnShown, isMiddleColumnOpen: Boolean(chatId), isRightColumnOpen: selectIsRightColumnShown(global, isMobile), @@ -668,6 +655,7 @@ export default memo(withGlobal( requestedDraft, chatlistModal, noRightColumnAnimation, + isSynced: global.isSynced, }; }, )(Main)); diff --git a/src/components/mediaViewer/hooks/useMediaProps.ts b/src/components/mediaViewer/hooks/useMediaProps.ts index 157cac952..725b75f92 100644 --- a/src/components/mediaViewer/hooks/useMediaProps.ts +++ b/src/components/mediaViewer/hooks/useMediaProps.ts @@ -33,7 +33,6 @@ type UseMediaProps = { message?: ApiMessage; avatarOwner?: ApiChat | ApiUser; origin?: MediaViewerOrigin; - lastSyncTime?: number; delay: number | false; }; @@ -88,7 +87,6 @@ export const useMediaProps = ({ && getMessageMediaHash(message, 'pictogram'), undefined, ApiMediaFormat.BlobUrl, - undefined, delay, ); const previewMediaHash = getMediaHash(); @@ -96,7 +94,6 @@ export const useMediaProps = ({ previewMediaHash, undefined, ApiMediaFormat.BlobUrl, - undefined, delay, ); const { @@ -106,7 +103,6 @@ export const useMediaProps = ({ getMediaHash(true), undefined, message && getMessageMediaFormat(message, 'full'), - undefined, delay, ); diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 73474c994..e80f79e50 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -589,8 +589,7 @@ export default memo(withGlobal( const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined; const chatFullInfo = !isPrivate ? selectChatFullInfo(global, chatId) : undefined; const canGiftPremium = Boolean( - global.lastSyncTime - && userFullInfo?.premiumGifts?.length + userFullInfo?.premiumGifts?.length && !selectIsPremiumPurchaseBlocked(global), ); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 06e543b59..8f421748c 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -128,7 +128,6 @@ type StateProps = { botInfo?: ApiBotInfo; threadTopMessageId?: number; hasLinkedChat?: boolean; - lastSyncTime?: number; topic?: ApiTopic; noMessageSendingAnimation?: boolean; isServiceNotificationsChat?: boolean; @@ -178,7 +177,6 @@ const MessageList: FC = ({ botInfo, threadTopMessageId, hasLinkedChat, - lastSyncTime, withBottomShift, withDefaultBg, topic, @@ -238,10 +236,10 @@ const MessageList: FC = ({ }, [firstUnreadId]); useEffect(() => { - if (!isCurrentUserPremium && isChannelChat && isReady && lastSyncTime) { + if (!isCurrentUserPremium && isChannelChat && isReady) { loadSponsoredMessages({ chatId }); } - }, [isCurrentUserPremium, chatId, isReady, isChannelChat, lastSyncTime, loadSponsoredMessages]); + }, [isCurrentUserPremium, chatId, isReady, isChannelChat]); // Updated only once when messages are loaded (as we want the unread divider to keep its position) useSyncEffect(() => { @@ -738,7 +736,6 @@ export default memo(withGlobal( botInfo, threadTopMessageId, hasLinkedChat: chatFullInfo ? Boolean(chatFullInfo.linkedChatId) : undefined, - lastSyncTime: global.lastSyncTime, topic, noMessageSendingAnimation: !selectPerformanceSettingsValue(global, 'messageSendingAnimations'), isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID, diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 3f37909bb..952c1a2a6 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -1,4 +1,3 @@ -import type { RefObject } from 'react'; import React, { useEffect, useState, memo, useMemo, } from '../../lib/teact/teact'; @@ -93,7 +92,7 @@ import './MiddleColumn.scss'; import styles from './MiddleColumn.module.scss'; interface OwnProps { - leftColumnRef: RefObject; + leftColumnRef: React.RefObject; isMobile?: boolean; } @@ -138,7 +137,6 @@ type StateProps = { activeEmojiInteractions?: ActiveEmojiInteraction[]; shouldJoinToSend?: boolean; shouldSendJoinRequest?: boolean; - lastSyncTime?: number; pinnedIds?: number[]; topMessageId?: number; }; @@ -192,7 +190,6 @@ function MiddleColumn({ shouldJoinToSend, shouldSendJoinRequest, shouldLoadFullChat, - lastSyncTime, pinnedIds, topMessageId, }: OwnProps & StateProps) { @@ -320,10 +317,10 @@ function MiddleColumn({ }, [chatId, isPrivate, loadUser]); useEffect(() => { - if (!areChatSettingsLoaded && lastSyncTime) { + if (!areChatSettingsLoaded) { loadChatSettings({ chatId: chatId! }); } - }, [chatId, isPrivate, areChatSettingsLoaded, lastSyncTime, loadChatSettings]); + }, [chatId, isPrivate, areChatSettingsLoaded]); useEffect(() => { if (chatId && shouldLoadFullChat && isReady) { @@ -662,7 +659,7 @@ export default memo(withGlobal( messageLanguageModal, } = selectTabState(global); const currentMessageList = selectCurrentMessageList(global); - const { leftColumnWidth, lastSyncTime } = global; + const { leftColumnWidth } = global; const state: StateProps = { theme, @@ -682,7 +679,6 @@ export default memo(withGlobal( currentTransitionKey: Math.max(0, messageLists.length - 1), activeEmojiInteractions, leftColumnWidth, - lastSyncTime, }; if (!currentMessageList) { @@ -711,7 +707,7 @@ export default memo(withGlobal( const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id)); const canStartBot = !canRestartBot && isBotNotStarted; const shouldLoadFullChat = Boolean( - chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id) && lastSyncTime, + chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id), ); const replyingToId = selectReplyingToId(global, chatId, threadId); const shouldBlockSendInForum = chat?.isForum diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index c94012748..de1cb434f 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -1,10 +1,10 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, useRef, } from '../../lib/teact/teact'; import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { getActions, withGlobal } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { GlobalState, MessageListType } from '../../global/types'; import type { Signal } from '../../util/signals'; import type { @@ -101,7 +101,6 @@ type StateProps = { messagesCount?: number; isComments?: boolean; isChatWithSelf?: boolean; - lastSyncTime?: number; hasButtonInHeader?: boolean; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; @@ -128,7 +127,6 @@ const MiddleHeader: FC = ({ messagesCount, isComments, isChatWithSelf, - lastSyncTime, hasButtonInHeader, shouldSkipHistoryAnimations, currentTransitionKey, @@ -166,10 +164,10 @@ const MiddleHeader: FC = ({ const isForum = chat?.isForum; useEffect(() => { - if (lastSyncTime && isReady && (threadId === MAIN_THREAD_ID || isForum)) { + if (isReady && (threadId === MAIN_THREAD_ID || isForum)) { loadPinnedMessages({ chatId, threadId }); } - }, [chatId, loadPinnedMessages, lastSyncTime, threadId, isReady, isForum]); + }, [chatId, threadId, isReady, isForum]); useEnsureMessage(chatId, pinnedMessageId, pinnedMessage); @@ -484,7 +482,6 @@ export default memo(withGlobal( const { isLeftColumnShown, shouldSkipHistoryAnimations, audioPlayer, messageLists, } = selectTabState(global); - const { lastSyncTime } = global; const chat = selectChat(global, chatId); const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer; @@ -523,7 +520,6 @@ export default memo(withGlobal( chat, messagesCount, isChatWithSelf: selectIsChatWithSelf(global, chatId), - lastSyncTime, shouldSkipHistoryAnimations, currentTransitionKey: Math.max(0, messageLists.length - 1), connectionState: global.connectionState, diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 3d562a8c1..47ac64c28 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -1,10 +1,10 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { TabState, MessageListType, GlobalState, ApiDraft, MessageList, } from '../../../global/types'; @@ -179,7 +179,6 @@ type StateProps = groupChatMembers?: ApiChatMember[]; currentUserId?: string; recentEmojis: string[]; - lastSyncTime?: number; contentToBeScheduled?: TabState['contentToBeScheduled']; shouldSuggestStickers?: boolean; shouldSuggestCustomEmoji?: boolean; @@ -207,8 +206,7 @@ type StateProps = attachmentSettings: GlobalState['attachmentSettings']; slowMode?: ApiChatFullInfo['slowMode']; shouldUpdateStickerSetOrder?: boolean; - } - & Pick; + }; enum MainButtonState { Send = 'send', @@ -251,7 +249,6 @@ const Composer: FC = ({ isForCurrentMessageList, isCurrentUserPremium, canSendVoiceByPrivacy, - connectionState, isChatWithBot, isChatWithSelf, isChannel, @@ -269,7 +266,6 @@ const Composer: FC = ({ topInlineBotIds, currentUserId, captionLimit, - lastSyncTime, contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, @@ -349,16 +345,16 @@ const Composer: FC = ({ }, [chatId]); useEffect(() => { - if (chatId && lastSyncTime && isReady) { + if (chatId && isReady) { loadScheduledHistory({ chatId }); } - }, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]); + }, [isReady, chatId, loadScheduledHistory, threadId]); useEffect(() => { - if (chatId && chat && lastSyncTime && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) { + if (chatId && chat && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) { loadSendAs({ chatId }); } - }, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsPeerIds]); + }, [chat, chatId, isReady, loadSendAs, sendAsPeerIds]); const shouldAnimateSendAsButtonRef = useRef(false); useSyncEffect(([prevChatId, prevSendAsPeerIds]) => { @@ -509,7 +505,7 @@ const Composer: FC = ({ help: inlineBotHelp, loadMore: loadMoreForInlineBot, } = useInlineBotTooltip( - Boolean(isReady && isForCurrentMessageList && !hasAttachments && lastSyncTime), + Boolean(isReady && isForCurrentMessageList && !hasAttachments), chatId, getHtml, inlineBots, @@ -564,7 +560,7 @@ const Composer: FC = ({ insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId); }); - useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage, lastSyncTime); + useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage); const resetComposer = useLastCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { @@ -742,7 +738,7 @@ const Composer: FC = ({ isSilent?: boolean; scheduledAt?: number; }) => { - if (connectionState !== 'connectionStateReady' || !currentMessageList) { + if (!currentMessageList) { return; } @@ -790,7 +786,7 @@ const Composer: FC = ({ }); const handleSend = useLastCallback(async (isSilent = false, scheduledAt?: number) => { - if (connectionState !== 'connectionStateReady' || !currentMessageList) { + if (!currentMessageList) { return; } @@ -1008,7 +1004,7 @@ const Composer: FC = ({ const handleInlineBotSelect = useLastCallback(( inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean, ) => { - if (connectionState !== 'connectionStateReady' || !currentMessageList) { + if (!currentMessageList) { return; } @@ -1627,7 +1623,6 @@ export default memo(withGlobal( return { isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), - connectionState: global.connectionState, replyingToId, draft: selectDraft(global, chatId, threadId), chat, @@ -1652,7 +1647,6 @@ export default memo(withGlobal( groupChatMembers: chatFullInfo?.members, topInlineBotIds: global.topInlineBots?.userIds, currentUserId, - lastSyncTime: global.lastSyncTime, contentToBeScheduled: tabState.contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx index 4deca366e..ace4731d7 100644 --- a/src/components/middle/composer/SymbolMenu.tsx +++ b/src/components/middle/composer/SymbolMenu.tsx @@ -68,7 +68,6 @@ export type OwnProps = { type StateProps = { isLeftColumnShown: boolean; isCurrentUserPremium?: boolean; - lastSyncTime?: number; isBackgroundTranslucent?: boolean; }; @@ -82,7 +81,6 @@ const SymbolMenu: FC = ({ canSendGifs, isLeftColumnShown, isCurrentUserPremium, - lastSyncTime, onLoad, onClose, onEmojiSelect, @@ -112,6 +110,8 @@ const SymbolMenu: FC = ({ const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, isMobile); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onClose, false, false); + const lang = useLang(); + if (!isActivated && isOpen) { isActivated = true; } @@ -127,10 +127,10 @@ const SymbolMenu: FC = ({ }, [canSendPlainText]); useEffect(() => { - if (lastSyncTime && isCurrentUserPremium) { + if (isCurrentUserPremium) { loadPremiumSetStickers(); } - }, [isCurrentUserPremium, lastSyncTime, loadPremiumSetStickers]); + }, [isCurrentUserPremium, loadPremiumSetStickers]); useLayoutEffect(() => { if (!isMobile || !isOpen || isAttachmentModal) { @@ -204,8 +204,6 @@ const SymbolMenu: FC = ({ onStickerSelect?.(sticker, isSilent, shouldSchedule, true, canUpdateStickerSetsOrder); }); - const lang = useLang(); - function renderContent(isActive: boolean, isFrom: boolean) { switch (activeTab) { case SymbolMenuTabs.Emoji: @@ -350,7 +348,6 @@ export default memo(withGlobal( return { isLeftColumnShown: selectTabState(global).isLeftColumnShown, isCurrentUserPremium: selectIsCurrentUserPremium(global), - lastSyncTime: global.lastSyncTime, isBackgroundTranslucent: selectIsContextMenuTranslucent(global), }; }, diff --git a/src/components/middle/composer/hooks/useDraft.ts b/src/components/middle/composer/hooks/useDraft.ts index 6a35afa39..57a797879 100644 --- a/src/components/middle/composer/hooks/useDraft.ts +++ b/src/components/middle/composer/hooks/useDraft.ts @@ -37,14 +37,13 @@ const useDraft = ( getHtml: Signal, setHtml: (html: string) => void, editedMessage: ApiMessage | undefined, - lastSyncTime?: number, ) => { const { saveDraft, clearDraft, loadCustomEmojis } = getActions(); const isEditing = Boolean(editedMessage); const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}, shouldForce = false) => { - if (isEditing || !lastSyncTime) return; + if (isEditing) return; const html = getHtml(); diff --git a/src/components/middle/message/Album.tsx b/src/components/middle/message/Album.tsx index 9e18e8210..52d2dee6b 100644 --- a/src/components/middle/message/Album.tsx +++ b/src/components/middle/message/Album.tsx @@ -32,7 +32,6 @@ type OwnProps = { album: IAlbum; observeIntersection: ObserveFn; hasCustomAppendix?: boolean; - lastSyncTime?: number; isOwn: boolean; isProtected?: boolean; albumLayout: IAlbumLayout; @@ -49,7 +48,6 @@ const Album: FC = ({ album, observeIntersection, hasCustomAppendix, - lastSyncTime, isOwn, isProtected, albumLayout, @@ -107,7 +105,6 @@ const Album: FC = ({ canAutoLoad={canAutoLoad} canAutoPlay={canAutoPlay} uploadProgress={uploadProgress} - lastSyncTime={lastSyncTime} dimensions={dimensions} isProtected={isProtected} onClick={onMediaClick} diff --git a/src/components/middle/message/AnimatedCustomEmoji.tsx b/src/components/middle/message/AnimatedCustomEmoji.tsx index 606fd661d..506662657 100644 --- a/src/components/middle/message/AnimatedCustomEmoji.tsx +++ b/src/components/middle/message/AnimatedCustomEmoji.tsx @@ -24,7 +24,6 @@ type OwnProps = { customEmojiId: string; withEffects?: boolean; isOwn?: boolean; - lastSyncTime?: number; forceLoadPreview?: boolean; messageId?: number; chatId?: string; diff --git a/src/components/middle/message/AnimatedEmoji.tsx b/src/components/middle/message/AnimatedEmoji.tsx index d60865cdb..63d7f432a 100644 --- a/src/components/middle/message/AnimatedEmoji.tsx +++ b/src/components/middle/message/AnimatedEmoji.tsx @@ -25,7 +25,6 @@ type OwnProps = { withEffects?: boolean; isOwn?: boolean; observeIntersection?: ObserveFn; - lastSyncTime?: number; forceLoadPreview?: boolean; messageId?: number; chatId?: string; @@ -43,7 +42,6 @@ const QUALITY = 1; const AnimatedEmoji: FC = ({ isOwn, observeIntersection, - lastSyncTime, forceLoadPreview, messageId, chatId, @@ -67,7 +65,6 @@ const AnimatedEmoji: FC = ({ quality={QUALITY} noLoad={!isIntersecting} forcePreview={forceLoadPreview} - lastSyncTime={lastSyncTime} play={isIntersecting} forceOnHeavyAnimation ref={ref} diff --git a/src/components/middle/message/Game.tsx b/src/components/middle/message/Game.tsx index 627d90322..6e7886942 100644 --- a/src/components/middle/message/Game.tsx +++ b/src/components/middle/message/Game.tsx @@ -20,13 +20,11 @@ const DEFAULT_PREVIEW_DIMENSIONS = { type OwnProps = { message: ApiMessage; canAutoLoadMedia?: boolean; - lastSyncTime?: number; }; const Game: FC = ({ message, canAutoLoadMedia, - lastSyncTime, }) => { const { clickBotInlineButton } = getActions(); const game = message.content.game!; @@ -34,8 +32,8 @@ const Game: FC = ({ title, description, } = game; - const photoHash = Boolean(lastSyncTime) && getGamePreviewPhotoHash(game); - const videoHash = Boolean(lastSyncTime) && getGamePreviewVideoHash(game); + const photoHash = getGamePreviewPhotoHash(game); + const videoHash = getGamePreviewVideoHash(game); const photoBlobUrl = useMedia(photoHash, !canAutoLoadMedia); const videoBlobUrl = useMedia(videoHash, !canAutoLoadMedia); diff --git a/src/components/middle/message/InvoiceMediaPreview.tsx b/src/components/middle/message/InvoiceMediaPreview.tsx index 6cdabd25d..69519711d 100644 --- a/src/components/middle/message/InvoiceMediaPreview.tsx +++ b/src/components/middle/message/InvoiceMediaPreview.tsx @@ -19,14 +19,14 @@ import styles from './InvoiceMediaPreview.module.scss'; type OwnProps = { message: ApiMessage; - lastSyncTime?: number; + isConnected: boolean; }; const POLLING_INTERVAL = 30000; const InvoiceMediaPreview: FC = ({ message, - lastSyncTime, + isConnected, }) => { const { openInvoice, loadExtendedMedia } = getActions(); const lang = useLang(); @@ -38,7 +38,7 @@ const InvoiceMediaPreview: FC = ({ loadExtendedMedia({ chatId, ids: [id] }); }); - useInterval(refreshExtendedMedia, lastSyncTime ? POLLING_INTERVAL : undefined); + useInterval(refreshExtendedMedia, isConnected ? POLLING_INTERVAL : undefined); const { amount, diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index 722740d58..e69ddb286 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -54,7 +54,6 @@ const SVG_PIN = { __html: '

{renderText(summary.question, ['emoji', 'br'])}

- {Boolean(lastSyncTime) && summary.answers.map((answer) => ( + {summary.answers.map((answer) => ( = ({ totalVoters={results.totalVoters!} /> ))} - {!lastSyncTime && }
); @@ -75,7 +75,6 @@ export default memo(withGlobal( const { pollResults: { chatId, messageId }, } = selectTabState(global); - const { lastSyncTime } = global; if (!chatId || !messageId) { return {}; @@ -87,7 +86,6 @@ export default memo(withGlobal( return { chat, message, - lastSyncTime, }; }, )(PollResults)); diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index ff0365711..d929ddc32 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -1,9 +1,9 @@ -import type { FC } from '../../lib/teact/teact'; import React, { useEffect, useMemo, useRef, useState, memo, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { ApiMessage, ApiChat, @@ -98,7 +98,6 @@ type StateProps = { userStatusesById: Record; isRightColumnShown: boolean; isRestricted?: boolean; - lastSyncTime?: number; activeDownloadIds?: number[]; isChatProtected?: boolean; }; @@ -138,7 +137,6 @@ const Profile: FC = ({ chatsById, isRightColumnShown, isRestricted, - lastSyncTime, activeDownloadIds, isChatProtected, }) => { @@ -190,7 +188,6 @@ const Profile: FC = ({ chatsById, messagesById, foundIds, - lastSyncTime, topicId, ); const isFirstTab = resultType === 'members' || (!hasMembersTab && resultType === 'media'); @@ -224,10 +221,8 @@ const Profile: FC = ({ const profileId = resolvedUserId || chatId; useEffect(() => { - if (lastSyncTime) { - loadProfilePhotos({ profileId }); - } - }, [loadProfilePhotos, profileId, lastSyncTime]); + loadProfilePhotos({ profileId }); + }, [profileId]); const handleSelectMedia = useLastCallback((mediaId: number) => { openMediaViewer({ @@ -398,7 +393,6 @@ const Profile: FC = ({ message={messagesById[id]} origin={AudioOrigin.SharedMedia} date={messagesById[id].date} - lastSyncTime={lastSyncTime} className="scroll-item" onPlay={handlePlayAudio} onDateClick={handleMessageFocus} @@ -415,7 +409,6 @@ const Profile: FC = ({ senderTitle={getSenderName(lang, messagesById[id], chatsById, usersById)} origin={AudioOrigin.SharedMedia} date={messagesById[id].date} - lastSyncTime={lastSyncTime} className="scroll-item" onPlay={handlePlayAudio} onDateClick={handleMessageFocus} @@ -565,7 +558,6 @@ export default memo(withGlobal( currentUserId: global.currentUserId, isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRestricted: chat?.isRestricted, - lastSyncTime: global.lastSyncTime, activeDownloadIds: activeDownloads?.ids, usersById, userStatusesById, diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index bdaf484d6..9bfe78de8 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -23,7 +23,6 @@ export default function useProfileViewportIds( chatsById?: Record, chatMessages?: Record, foundIds?: number[], - lastSyncTime?: number, topicId?: number, ) { const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType; @@ -49,31 +48,31 @@ export default function useProfileViewportIds( }, [chatsById, commonChatIds]); const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForLoadableItems( - resultType, loadMoreMembers, lastSyncTime, memberIds, + loadMoreMembers, memberIds, ); const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia( - 'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, topicId, + 'media', resultType, searchMessages, chatMessages, foundIds, topicId, ); const [documentViewportIds, getMoreDocuments, noProfileInfoForDocuments] = useInfiniteScrollForSharedMedia( - 'documents', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, topicId, + 'documents', resultType, searchMessages, chatMessages, foundIds, topicId, ); const [linkViewportIds, getMoreLinks, noProfileInfoForLinks] = useInfiniteScrollForSharedMedia( - 'links', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, topicId, + 'links', resultType, searchMessages, chatMessages, foundIds, topicId, ); const [audioViewportIds, getMoreAudio, noProfileInfoForAudio] = useInfiniteScrollForSharedMedia( - 'audio', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, topicId, + 'audio', resultType, searchMessages, chatMessages, foundIds, topicId, ); const [voiceViewportIds, getMoreVoices, noProfileInfoForVoices] = useInfiniteScrollForSharedMedia( - 'voice', resultType, searchMessages, lastSyncTime, chatMessages, foundIds, topicId, + 'voice', resultType, searchMessages, chatMessages, foundIds, topicId, ); const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems( - resultType, loadCommonChats, lastSyncTime, chatIds, + loadCommonChats, chatIds, ); let viewportIds: number[] | string[] | undefined; @@ -122,13 +121,11 @@ export default function useProfileViewportIds( } function useInfiniteScrollForLoadableItems( - currentResultType?: ProfileTabType, handleLoadMore?: AnyToVoidFunction, - lastSyncTime?: number, itemIds?: string[], ) { const [viewportIds, getMore] = useInfiniteScroll( - lastSyncTime ? handleLoadMore : undefined, + handleLoadMore, itemIds, undefined, MEMBERS_SLICE, @@ -143,7 +140,6 @@ function useInfiniteScrollForSharedMedia( forSharedMediaType: SharedMediaType, currentResultType?: ProfileTabType, handleLoadMore?: AnyToVoidFunction, - lastSyncTime?: number, chatMessages?: Record, foundIds?: number[], topicId?: number, @@ -165,7 +161,7 @@ function useInfiniteScrollForSharedMedia( }, [chatMessages, foundIds, currentResultType, forSharedMediaType]); const [viewportIds, getMore] = useInfiniteScroll( - lastSyncTime ? handleLoadMore : undefined, + handleLoadMore, messageIdsRef.current, undefined, forSharedMediaType === 'media' ? SHARED_MEDIA_SLICE : MESSAGE_SEARCH_SLICE, diff --git a/src/components/right/management/ManageChannel.tsx b/src/components/right/management/ManageChannel.tsx index 67d51b28b..4f75d8a25 100644 --- a/src/components/right/management/ManageChannel.tsx +++ b/src/components/right/management/ManageChannel.tsx @@ -45,7 +45,6 @@ type StateProps = { canChangeInfo?: boolean; canInvite?: boolean; exportedInvites?: ApiExportedInvite[]; - lastSyncTime?: number; availableReactions?: ApiAvailableReaction[]; }; @@ -61,7 +60,6 @@ const ManageChannel: FC = ({ canChangeInfo, canInvite, exportedInvites, - lastSyncTime, isActive, availableReactions, onScreenSelect, @@ -98,12 +96,10 @@ const ManageChannel: FC = ({ }); useEffect(() => { - if (lastSyncTime) { - loadExportedChatInvites({ chatId }); - loadExportedChatInvites({ chatId, isRevoked: true }); - loadChatJoinRequests({ chatId }); - } - }, [chatId, loadExportedChatInvites, lastSyncTime, loadChatJoinRequests]); + loadExportedChatInvites({ chatId }); + loadExportedChatInvites({ chatId, isRevoked: true }); + loadChatJoinRequests({ chatId }); + }, [chatId]); useEffect(() => { if (progress === ManagementProgress.Complete) { @@ -378,7 +374,6 @@ export default memo(withGlobal( isSignaturesShown, canChangeInfo: getHasAdminRight(chat, 'changeInfo'), canInvite: getHasAdminRight(chat, 'inviteUsers'), - lastSyncTime: global.lastSyncTime, exportedInvites: invites, availableReactions: global.availableReactions, }; diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index 3270d9d37..cfff6f4a3 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -56,7 +56,6 @@ type StateProps = { canInvite?: boolean; canEditForum?: boolean; exportedInvites?: ApiExportedInvite[]; - lastSyncTime?: number; isChannelsPremiumLimitReached: boolean; availableReactions?: ApiAvailableReaction[]; }; @@ -98,7 +97,6 @@ const ManageGroup: FC = ({ canEditForum, isActive, exportedInvites, - lastSyncTime, isChannelsPremiumLimitReached, availableReactions, onScreenSelect, @@ -140,12 +138,12 @@ const ManageGroup: FC = ({ }); useEffect(() => { - if (lastSyncTime && canInvite) { + if (canInvite) { loadExportedChatInvites({ chatId }); loadExportedChatInvites({ chatId, isRevoked: true }); loadChatJoinRequests({ chatId }); } - }, [chatId, loadExportedChatInvites, lastSyncTime, canInvite, loadChatJoinRequests]); + }, [chatId, canInvite]); // Resetting `isForum` switch on flood wait error useEffect(() => { @@ -509,7 +507,6 @@ export default memo(withGlobal( canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'), canInvite: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'inviteUsers'), exportedInvites: invites, - lastSyncTime: global.lastSyncTime, isChannelsPremiumLimitReached: limitReachedModal?.limit === 'channels', availableReactions: global.availableReactions, canEditForum, diff --git a/src/global/actions/all.ts b/src/global/actions/all.ts index 0172db771..0824b67ec 100644 --- a/src/global/actions/all.ts +++ b/src/global/actions/all.ts @@ -1,18 +1,3 @@ -import './ui/initial'; -import './ui/chats'; -import './ui/messages'; -import './ui/globalSearch'; -import './ui/localSearch'; -import './ui/stickerSearch'; -import './ui/users'; -import './ui/settings'; -import './ui/misc'; -import './ui/payments'; -import './ui/calls'; -import './ui/mediaViewer'; -import './ui/passcode'; -import './ui/reactions'; - import './api/initial'; import './api/chats'; import './api/messages'; @@ -30,6 +15,21 @@ import './api/payments'; import './api/reactions'; import './api/statistics'; +import './ui/initial'; +import './ui/chats'; +import './ui/messages'; +import './ui/globalSearch'; +import './ui/localSearch'; +import './ui/stickerSearch'; +import './ui/users'; +import './ui/settings'; +import './ui/misc'; +import './ui/payments'; +import './ui/calls'; +import './ui/mediaViewer'; +import './ui/passcode'; +import './ui/reactions'; + import './apiUpdaters/initial'; import './apiUpdaters/chats'; import './apiUpdaters/messages'; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 84fba1b51..5d4d1a8ab 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -129,8 +129,34 @@ addActionHandler('preloadTopChatMessages', async (global, actions): Promise { const { - id, threadId = MAIN_THREAD_ID, noRequestThreadInfoUpdate, + id, threadId = MAIN_THREAD_ID, noRequestThreadInfoUpdate, tabId = getCurrentTabId(), } = payload; + + const currentMessageList = selectCurrentMessageList(global, tabId); + const currentChatId = currentMessageList?.chatId; + const currentThreadId = currentMessageList?.threadId; + + if (currentChatId && (currentChatId !== id || currentThreadId !== threadId)) { + const [isChatOpened, isThreadOpened] = Object.values(global.byTabId) + .reduce(([accHasChatOpened, accHasThreadOpened], { id: otherTabId }) => { + if (otherTabId === tabId || (accHasChatOpened && accHasThreadOpened)) { + return [accHasChatOpened, accHasThreadOpened]; + } + + const otherMessageList = selectCurrentMessageList(global, otherTabId); + const isSameChat = otherMessageList?.chatId === currentChatId; + const isSameThread = isSameChat && otherMessageList?.threadId === currentThreadId; + + return [accHasChatOpened || isSameChat, accHasThreadOpened || isSameThread]; + }, [currentChatId === id, false]); + + const shouldAbortChatRequests = !isChatOpened || !isThreadOpened; + + if (shouldAbortChatRequests) { + callApi('abortChatRequests', { chatId: currentChatId, threadId: isChatOpened ? currentThreadId : undefined }); + } + } + if (!id) { return; } diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index c0c06cc3f..7185edbe4 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -573,6 +573,7 @@ addActionHandler('reportMessages', async (global, actions, payload): Promise => { const { action, chatId, threadId } = payload!; + if (global.connectionState !== 'connectionStateReady') return; if (chatId === global.currentUserId) return; // Message actions are disabled in Saved Messages const chat = selectChat(global, chatId)!; diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 5300674ef..33a9ff6ae 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -84,6 +84,7 @@ addActionHandler('sendEmojiInteraction', (global, actions, payload): ActionRetur const { messageId, chatId, emoji, interactions, } = payload!; + if (global.connectionState !== 'connectionStateReady') return; const chat = selectChat(global, chatId); @@ -296,7 +297,9 @@ addActionHandler('sendWatchingEmojiInteraction', (global, actions, payload): Act return undefined; } - callApi('sendWatchingEmojiInteraction', { chat, emoticon }); + if (global.connectionState === 'connectionStateReady') { + callApi('sendWatchingEmojiInteraction', { chat, emoticon }); + } return updateTabState(global, { activeEmojiInteractions: tabState.activeEmojiInteractions.map((activeEmojiInteraction) => { diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index bb2bd6869..8122b9fd5 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -594,6 +594,7 @@ function buildInputPrivacyRules(global: GlobalState, { } addActionHandler('updateIsOnline', (global, actions, payload): ActionReturnType => { + if (global.connectionState !== 'connectionStateReady') return; callApi('updateIsOnline', payload); }); diff --git a/src/global/actions/api/sync.ts b/src/global/actions/api/sync.ts index 74aa3a0f9..b7d1c1ddc 100644 --- a/src/global/actions/api/sync.ts +++ b/src/global/actions/api/sync.ts @@ -70,8 +70,8 @@ addActionHandler('sync', (global, actions): ActionReturnType => { global = getGlobal(); global = { ...global, - lastSyncTime: Date.now(), isSyncing: false, + isSynced: true, }; setGlobal(global); @@ -244,9 +244,9 @@ function loadTopMessages(chat: ApiChat, threadId: number, lastReadInboxId?: numb let previousGlobal: GlobalState | undefined; // RAF can be unreliable when device goes into sleep mode, so sync logic is handled outside any component addCallback((global: GlobalState) => { - const { connectionState, authState } = global; + const { connectionState, authState, isSynced } = global; const { isMasterTab } = selectTabState(global); - if (!isMasterTab || (previousGlobal?.connectionState === connectionState + if (!isMasterTab || isSynced || (previousGlobal?.connectionState === connectionState && previousGlobal?.authState === authState)) { previousGlobal = global; return; diff --git a/src/global/actions/apiUpdaters/initial.ts b/src/global/actions/apiUpdaters/initial.ts index b17b6cad2..4c47579d4 100644 --- a/src/global/actions/apiUpdaters/initial.ts +++ b/src/global/actions/apiUpdaters/initial.ts @@ -62,6 +62,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { actions.initApi(); break; + case 'requestSync': + actions.sync(); + break; + case 'error': { Object.values(global.byTabId).forEach(({ id: tabId }) => { const paymentShippingError = getShippingError(update.error); diff --git a/src/global/types.ts b/src/global/types.ts index a37aa954b..f2c6e122e 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -589,7 +589,7 @@ export type GlobalState = { currentUserId?: string; isSyncing?: boolean; isUpdateAvailable?: boolean; - lastSyncTime?: number; + isSynced?: boolean; leftColumnWidth?: number; lastIsChatInfoShown?: boolean; initialUnreadNotifications?: number; diff --git a/src/hooks/useEnsureCustomEmoji.ts b/src/hooks/useEnsureCustomEmoji.ts index 7fff8295d..a9800c7a7 100644 --- a/src/hooks/useEnsureCustomEmoji.ts +++ b/src/hooks/useEnsureCustomEmoji.ts @@ -3,8 +3,6 @@ import { addCustomEmojiInputRenderCallback } from '../util/customEmojiManager'; import { throttle } from '../util/schedulers'; -import useLastSyncTime from './useLastSyncTime'; - let LOAD_QUEUE = new Set(); const RENDER_HISTORY = new Set(); const THROTTLE = 200; @@ -44,7 +42,6 @@ function notifyCustomEmojiRender(emojiId: string) { addCustomEmojiInputRenderCallback(notifyCustomEmojiRender); export default function useEnsureCustomEmoji(id?: string) { - const lastSyncTime = useLastSyncTime(); if (!id) return; notifyCustomEmojiRender(id); @@ -53,7 +50,5 @@ export default function useEnsureCustomEmoji(id?: string) { } LOAD_QUEUE.add(id); - if (lastSyncTime) { - loadFromQueue(); - } + loadFromQueue(); } diff --git a/src/hooks/useLastSyncTime.ts b/src/hooks/useLastSyncTime.ts deleted file mode 100644 index 9541521b3..000000000 --- a/src/hooks/useLastSyncTime.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect, useState } from '../lib/teact/teact'; -import { addCallback } from '../lib/teact/teactn'; -import { getGlobal } from '../global'; - -import type { GlobalState } from '../global/types'; - -type LastSyncTimeSetter = (time: number) => void; - -const handlers = new Set(); -let prevGlobal: GlobalState | undefined; - -addCallback((global: GlobalState) => { - if (global.lastSyncTime && global.lastSyncTime !== prevGlobal?.lastSyncTime) { - for (const handler of handlers) { - handler(global.lastSyncTime); - } - } - - prevGlobal = global; -}); - -export default function useLastSyncTime() { - const [lastSyncTime, setLastSyncTime] = useState(getGlobal().lastSyncTime); - - useEffect(() => { - handlers.add(setLastSyncTime); - - return () => { - handlers.delete(setLastSyncTime); - }; - }, []); - - return lastSyncTime; -} diff --git a/src/hooks/useMedia.ts b/src/hooks/useMedia.ts index 2369857d5..e09051773 100644 --- a/src/hooks/useMedia.ts +++ b/src/hooks/useMedia.ts @@ -9,7 +9,6 @@ const useMedia = ( mediaHash: string | false | undefined, noLoad = false, mediaFormat = ApiMediaFormat.BlobUrl, - cacheBuster?: number, delay?: number | false, ) => { const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; @@ -28,7 +27,7 @@ const useMedia = ( } }); } - }, [noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, delay]); + }, [noLoad, mediaHash, mediaData, mediaFormat, forceUpdate, delay]); return mediaData; }; diff --git a/src/hooks/useMediaWithLoadProgress.ts b/src/hooks/useMediaWithLoadProgress.ts index c909fe5d3..635de0e59 100644 --- a/src/hooks/useMediaWithLoadProgress.ts +++ b/src/hooks/useMediaWithLoadProgress.ts @@ -17,7 +17,6 @@ export default function useMediaWithLoadProgress( mediaHash: string | undefined, noLoad = false, mediaFormat = ApiMediaFormat.BlobUrl, - cacheBuster?: number, delay?: number | false, isHtmlAllowed = false, ) { @@ -66,8 +65,7 @@ export default function useMediaWithLoadProgress( } } }, [ - noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, isStreaming, delay, handleProgress, - isHtmlAllowed, id, + noLoad, mediaHash, mediaData, mediaFormat, forceUpdate, isStreaming, delay, handleProgress, isHtmlAllowed, id, ]); useEffect(() => { diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index b7e674b74..9858155da 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -988,16 +988,12 @@ class TelegramClient { for (const x of [...update.users, ...update.chats]) { entities.push(x); } - for (const u of update.updates) { - this._processUpdate(u, update.updates, entities); - } + this._processUpdate(update, entities); } else if (update instanceof constructors.UpdateShort) { this._processUpdate(update.update, undefined); } else { this._processUpdate(update, undefined); } - // TODO add caching - // this._stateCache.update(update) } _processUpdate(update, entities) { diff --git a/src/lib/gramjs/extensions/Logger.js b/src/lib/gramjs/extensions/Logger.js index c90e08fd6..23d096558 100644 --- a/src/lib/gramjs/extensions/Logger.js +++ b/src/lib/gramjs/extensions/Logger.js @@ -14,29 +14,14 @@ class Logger { _level = level || 'debug'; } - this.isBrowser = typeof process === 'undefined' - || process.type === 'renderer' - || process.browser === true - || process.__nwjs; - if (!this.isBrowser) { - this.colors = { - start: '\x1b[2m', - warn: '\x1b[35m', - info: '\x1b[33m', - debug: '\x1b[36m', - error: '\x1b[31m', - end: '\x1b[0m', - }; - } else { - this.colors = { - start: '%c', - warn: 'color : #ff00ff', - info: 'color : #ffff00', - debug: 'color : #00ffff', - error: 'color : #ff0000', - end: '', - }; - } + this.colors = { + start: '%c', + warn: 'color : #ff00ff', + info: 'color : #ffff00', + debug: 'color : #00ffff', + error: 'color : #ff0000', + end: '', + }; this.messageFormat = '[%t] [%l] - [%m]'; } @@ -97,13 +82,8 @@ class Logger { return; } if (this.canSend(level)) { - if (!this.isBrowser) { - // eslint-disable-next-line no-console - console.log(color + this.format(message, level) + this.colors.end); - } else { - // eslint-disable-next-line no-console - console.log(this.colors.start + this.format(message, level), color); - } + // eslint-disable-next-line no-console + console.log(this.colors.start + this.format(message, level), color); } } } diff --git a/src/lib/gramjs/network/MTProtoSender.js b/src/lib/gramjs/network/MTProtoSender.js index febb9b938..01a5963d0 100644 --- a/src/lib/gramjs/network/MTProtoSender.js +++ b/src/lib/gramjs/network/MTProtoSender.js @@ -749,7 +749,7 @@ class MTProtoSender { /** * Handles a server acknowledge about our messages. Normally these can be ignored */ - _handleAck() { } + _handleAck() {} /** * Handles future salt results, which don't come inside a diff --git a/src/lib/video-preview/mp4box.d.ts b/src/lib/video-preview/mp4box.d.ts index 59fa979e8..23903c64b 100644 --- a/src/lib/video-preview/mp4box.d.ts +++ b/src/lib/video-preview/mp4box.d.ts @@ -88,6 +88,6 @@ declare module 'mp4box' { export function createFile(): MP4File; - export { }; + export {}; } diff --git a/src/util/SortedQueue.ts b/src/util/SortedQueue.ts new file mode 100644 index 000000000..fbe16763b --- /dev/null +++ b/src/util/SortedQueue.ts @@ -0,0 +1,44 @@ +export default class SortedQueue { + private queue: T[]; + + constructor(private comparator: (a: T, b: T) => number) { + this.queue = []; + } + + add(item: T): void { + const index = this.binarySearch(item); + this.queue.splice(index, 0, item); + } + + pop(): T | undefined { + return this.queue.shift(); + } + + get size(): number { + return this.queue.length; + } + + clear(): void { + this.queue = []; + } + + private binarySearch(item: T): number { + let left = 0; + let right = this.queue.length; + + while (left < right) { + const middle = Math.floor((left + right) / 2); + const comparison = this.comparator(item, this.queue[middle]); + + if (comparison === 0) { + return middle; + } else if (comparison > 0) { + left = middle + 1; + } else { + right = middle; + } + } + + return left; + } +}