From 865ed08d821eee49f0290be85c4ac8ab394b9aeb Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 4 Dec 2021 13:44:03 +0100 Subject: [PATCH] [Perf] Extract `users.statusesById` to a separate global store --- src/api/gramjs/apiBuilders/users.ts | 22 +++- src/api/gramjs/methods/chats.ts | 9 +- src/api/gramjs/methods/users.ts | 4 +- src/api/types/users.ts | 2 +- src/components/common/Avatar.tsx | 12 +- src/components/common/PrivateChatInfo.tsx | 13 +- src/components/common/ProfileInfo.tsx | 12 +- src/components/left/main/Chat.tsx | 25 ++-- src/components/left/main/ContactList.tsx | 21 +++- src/components/left/search/ChatMessage.tsx | 1 - .../left/search/LeftSearchResultChat.tsx | 10 +- src/components/middle/composer/Composer.tsx | 73 +++++------ src/components/right/Profile.tsx | 13 +- .../right/hooks/useProfileViewportIds.ts | 15 ++- .../right/management/ManageGroupMembers.tsx | 17 ++- .../ManageGroupUserPermissionsCreate.tsx | 14 ++- src/global/cache.ts | 9 +- src/global/initial.ts | 1 + src/global/types.ts | 2 + src/hooks/useChatContextActions.ts | 6 +- src/modules/actions/api/chats.ts | 3 + src/modules/actions/api/sync.ts | 23 ++-- src/modules/actions/api/users.ts | 16 ++- src/modules/actions/apiUpdaters/users.ts | 21 ++-- src/modules/helpers/users.ts | 36 +++--- src/modules/reducers/chats.ts | 83 ++++++------- src/modules/reducers/users.ts | 116 +++++++++++------- src/modules/selectors/chats.ts | 6 +- src/modules/selectors/users.ts | 6 +- 29 files changed, 356 insertions(+), 235 deletions(-) diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index cd2dafebb..32dfc7aed 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -47,7 +47,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { ...(lastName && { lastName }), username: mtpUser.username || '', phoneNumber: mtpUser.phone || '', - status: buildApiUserStatus(mtpUser.status), + noStatus: !mtpUser.status, ...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }), ...(avatarHash && { avatarHash }), ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), @@ -88,3 +88,23 @@ function buildApiBotCommands(botId: string, botInfo: GramJs.BotInfo) { description, })) as ApiBotCommand[]; } + +export function buildApiUsersAndStatuses(mtpUsers: GramJs.TypeUser[]) { + const userStatusesById: Record = {}; + const users: ApiUser[] = []; + + mtpUsers.forEach((mtpUser) => { + const user = buildApiUser(mtpUser); + if (!user) { + return; + } + + users.push(user); + + if ('status' in mtpUser) { + userStatusesById[user.id] = buildApiUserStatus(mtpUser.status); + } + }); + + return { users, userStatusesById }; +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index f5aa4148e..4a09ee227 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -28,7 +28,7 @@ import { buildApiChatBotCommands, } from '../apiBuilders/chats'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; -import { buildApiUser } from '../apiBuilders/users'; +import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildCollectionByKey } from '../../../util/iteratees'; import localDb from '../localDb'; import { @@ -146,13 +146,11 @@ export async function fetchChats({ } }); - const users = (resultPinned ? resultPinned.users : []).concat(result.users) - .map(buildApiUser) - .filter(Boolean as any); const chatIds = chats.map((chat) => chat.id); - let totalChatCount: number; + const { users, userStatusesById } = buildApiUsersAndStatuses((resultPinned?.users || []).concat(result.users)); + let totalChatCount: number; if (result instanceof GramJs.messages.DialogsSlice) { totalChatCount = result.count; } else { @@ -163,6 +161,7 @@ export async function fetchChats({ chatIds, chats, users, + userStatusesById, draftsById, replyingToById, orderedPinnedIds: withPinned ? orderedPinnedIds : undefined, diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 9d13cca24..905fd5e89 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -14,7 +14,7 @@ import { buildMtpPeerId, getEntityTypeById, } from '../gramjsBuilders'; -import { buildApiUser, buildApiUserFromFull } from '../apiBuilders/users'; +import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiPhoto } from '../apiBuilders/common'; import localDb from '../localDb'; @@ -139,7 +139,7 @@ export async function fetchUsers({ users }: { users: ApiUser[] }) { } }); - return result.map(buildApiUser).filter(Boolean as any); + return buildApiUsersAndStatuses(result); } export function updateContact({ diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 47071d890..c723bef7b 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -10,7 +10,7 @@ export interface ApiUser { type: ApiUserType; firstName?: string; lastName?: string; - status?: ApiUserStatus; + noStatus?: boolean; username: string; phoneNumber: string; accessHash?: string; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index e88bc210c..aaa71ad22 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -1,7 +1,9 @@ import { MouseEvent as ReactMouseEvent } from 'react'; import React, { FC, memo, useCallback } from '../../lib/teact/teact'; -import { ApiChat, ApiMediaFormat, ApiUser } from '../../api/types'; +import { + ApiChat, ApiMediaFormat, ApiUser, ApiUserStatus, +} from '../../api/types'; import { IS_TEST } from '../../config'; import { @@ -26,9 +28,9 @@ import './Avatar.scss'; type OwnProps = { className?: string; size?: 'micro' | 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; - withOnlineStatus?: boolean; chat?: ApiChat; user?: ApiUser; + userStatus?: ApiUserStatus; text?: string; isSavedMessages?: boolean; lastSyncTime?: number; @@ -40,8 +42,8 @@ const Avatar: FC = ({ size = 'large', chat, user, + userStatus, text, - withOnlineStatus, isSavedMessages, lastSyncTime, onClick, @@ -86,7 +88,7 @@ const Avatar: FC = ({ content = getFirstLetters(text, 2); } - const isOnline = !isSavedMessages && user && isUserOnline(user); + const isOnline = !isSavedMessages && user && userStatus && isUserOnline(user, userStatus); const fullClassName = buildClassName( `Avatar size-${size}`, className, @@ -94,7 +96,7 @@ const Avatar: FC = ({ isSavedMessages && 'saved-messages', isDeleted && 'deleted-account', isReplies && 'replies-bot-account', - withOnlineStatus && isOnline && 'online', + isOnline && 'online', onClick && 'interactive', (!isSavedMessages && !blobUrl) && 'no-photo', ); diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index d15a7d39d..b1f172006 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -4,11 +4,11 @@ import React, { } from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; -import { ApiUser, ApiTypingStatus } from '../../api/types'; +import { ApiUser, ApiTypingStatus, ApiUserStatus } from '../../api/types'; import { GlobalActions, GlobalState } from '../../global/types'; import { MediaViewerOrigin } from '../../types'; -import { selectChatMessages, selectUser } from '../../modules/selectors'; +import { selectChatMessages, selectUser, selectUserStatus } from '../../modules/selectors'; import { getUserFullName, getUserStatus, isUserOnline } from '../../modules/helpers'; import renderText from './helpers/renderText'; import { pick } from '../../util/iteratees'; @@ -34,6 +34,7 @@ type OwnProps = { type StateProps = { user?: ApiUser; + userStatus?: ApiUserStatus; isSavedMessages?: boolean; areMessagesLoaded: boolean; serverTimeOffset: number; @@ -52,6 +53,7 @@ const PrivateChatInfo: FC = ({ noStatusOrTyping, noRtl, user, + userStatus, isSavedMessages, areMessagesLoaded, lastSyncTime, @@ -106,9 +108,9 @@ const PrivateChatInfo: FC = ({ } return ( -
+
{withUsername && user.username && {user.username}} - {getUserStatus(lang, user, serverTimeOffset)} + {getUserStatus(lang, user, userStatus, serverTimeOffset)}
); } @@ -143,11 +145,12 @@ export default memo(withGlobal( (global, { userId, forceShowSelf }): StateProps => { const { lastSyncTime, serverTimeOffset } = 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, isSavedMessages, areMessagesLoaded, serverTimeOffset, + lastSyncTime, user, userStatus, isSavedMessages, areMessagesLoaded, serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index f90a3e463..9efba47b6 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -3,12 +3,12 @@ import React, { } from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; -import { ApiUser, ApiChat } from '../../api/types'; +import { ApiUser, ApiChat, ApiUserStatus } from '../../api/types'; import { GlobalActions, GlobalState } from '../../global/types'; import { MediaViewerOrigin } from '../../types'; import { IS_TOUCH_ENV } from '../../util/environment'; -import { selectChat, selectUser } from '../../modules/selectors'; +import { selectChat, selectUser, selectUserStatus } from '../../modules/selectors'; import { getUserFullName, getUserStatus, isChatChannel, isUserOnline, } from '../../modules/helpers'; @@ -32,6 +32,7 @@ type OwnProps = { type StateProps = { user?: ApiUser; + userStatus?: ApiUserStatus; chat?: ApiChat; isSavedMessages?: boolean; animationLevel: 0 | 1 | 2; @@ -43,6 +44,7 @@ type DispatchProps = Pick; const ProfileInfo: FC = ({ forceShowSelf, user, + userStatus, chat, isSavedMessages, connectionState, @@ -162,8 +164,8 @@ const ProfileInfo: FC = ({ function renderStatus() { if (user) { return ( -
- {getUserStatus(lang, user, serverTimeOffset)} +
+ {getUserStatus(lang, user, userStatus, serverTimeOffset)}
); } @@ -227,6 +229,7 @@ export default memo(withGlobal( (global, { userId, forceShowSelf }): StateProps => { const { connectionState, serverTimeOffset } = global; const user = selectUser(global, userId); + const userStatus = selectUserStatus(global, userId); const chat = selectChat(global, userId); const isSavedMessages = !forceShowSelf && user && user.isSelf; const { animationLevel } = global.settings.byKey; @@ -234,6 +237,7 @@ export default memo(withGlobal( return { connectionState, user, + userStatus, chat, isSavedMessages, animationLevel, diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 2ff43451e..242fda2fa 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -7,7 +7,7 @@ import useLang, { LangFn } from '../../../hooks/useLang'; import { GlobalActions } from '../../../global/types'; import { - ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, MAIN_THREAD_ID, + ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, MAIN_THREAD_ID, ApiUserStatus, } from '../../../api/types'; import { ANIMATION_END_DELAY } from '../../../config'; @@ -30,7 +30,7 @@ import { } from '../../../modules/helpers'; import { selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList, - selectNotifySettings, selectNotifyExceptions, + selectNotifySettings, selectNotifyExceptions, selectUserStatus, } from '../../../modules/selectors'; import { renderActionMessageText } from '../../common/helpers/renderActionMessageText'; import renderText from '../../common/helpers/renderText'; @@ -67,7 +67,8 @@ type OwnProps = { type StateProps = { chat?: ApiChat; isMuted?: boolean; - privateChatUser?: ApiUser; + user?: ApiUser; + userStatus?: ApiUserStatus; actionTargetUserIds?: string[]; usersById?: Record; actionTargetMessage?: ApiMessage; @@ -94,7 +95,8 @@ const Chat: FC = ({ isPinned, chat, isMuted, - privateChatUser, + user, + userStatus, actionTargetUserIds, usersById, lastMessageSender, @@ -196,7 +198,7 @@ const Chat: FC = ({ const contextActions = useChatContextActions({ chat, - privateChatUser, + user, handleDelete, handleChatFolderChange, folderId, @@ -281,9 +283,9 @@ const Chat: FC = ({
{chat.isCallActive && chat.isCallNotEmpty && ( @@ -292,7 +294,7 @@ const Chat: FC = ({
-

{renderText(getChatTitle(lang, chat, privateChatUser))}

+

{renderText(getChatTitle(lang, chat, user))}

{chat.isVerified && } {isMuted && } {chat.lastMessage && ( @@ -377,7 +379,10 @@ export default memo(withGlobal( canScrollDown: isSelected && messageListType === 'thread', lastSyncTime: global.lastSyncTime, ...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }), - ...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }), + ...(privateChatUserId && { + user: selectUser(global, privateChatUserId), + userStatus: selectUserStatus(global, privateChatUserId), + }), ...(actionTargetUserIds && { usersById }), }; }, diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index f98abc588..44b9d24b0 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { ApiUser } from '../../../api/types'; +import { ApiUser, ApiUserStatus } from '../../../api/types'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import { throttle } from '../../../util/schedulers'; @@ -27,6 +27,7 @@ export type OwnProps = { type StateProps = { usersById: Record; + userStatusesById: Record; contactIds?: string[]; serverTimeOffset: number; }; @@ -36,8 +37,15 @@ type DispatchProps = Pick; const runThrottled = throttle((cb) => cb(), 60000, true); const ContactList: FC = ({ - isActive, onReset, - filter, usersById, contactIds, loadContactList, openChat, serverTimeOffset, + isActive, + filter, + usersById, + userStatusesById, + contactIds, + serverTimeOffset, + onReset, + loadContactList, + openChat, }) => { // Due to the parent Transition, this component never gets unmounted, // that's why we use throttled API call on every update. @@ -67,8 +75,8 @@ const ContactList: FC = ({ return fullName && searchWords(fullName, filter); }) : contactIds; - return sortUserIds(resultIds, usersById, undefined, serverTimeOffset); - }, [contactIds, filter, usersById, serverTimeOffset]); + return sortUserIds(resultIds, usersById, userStatusesById, undefined, serverTimeOffset); + }, [contactIds, filter, usersById, userStatusesById, serverTimeOffset]); const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter)); @@ -99,10 +107,11 @@ const ContactList: FC = ({ export default memo(withGlobal( (global): StateProps => { const { userIds: contactIds } = global.contactList || {}; - const { byId: usersById } = global.users; + const { byId: usersById, statusesById: userStatusesById } = global.users; return { usersById, + userStatusesById, contactIds, serverTimeOffset: global.serverTimeOffset, }; diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index a982bedae..331d089ac 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -83,7 +83,6 @@ const ChatMessage: FC = ({ diff --git a/src/components/left/search/LeftSearchResultChat.tsx b/src/components/left/search/LeftSearchResultChat.tsx index 232f5df5b..4389e563f 100644 --- a/src/components/left/search/LeftSearchResultChat.tsx +++ b/src/components/left/search/LeftSearchResultChat.tsx @@ -27,7 +27,7 @@ type OwnProps = { type StateProps = { chat?: ApiChat; - privateChatUser?: ApiUser; + user?: ApiUser; isPinned?: boolean; isMuted?: boolean; }; @@ -35,7 +35,7 @@ type StateProps = { const LeftSearchResultChat: FC = ({ chatId, chat, - privateChatUser, + user, isPinned, isMuted, withUsername, @@ -46,7 +46,7 @@ const LeftSearchResultChat: FC = ({ const contextActions = useChatContextActions({ chat, - privateChatUser, + user, isPinned, isMuted, handleDelete: openDeleteModal, @@ -93,7 +93,7 @@ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); const privateChatUserId = chat && getPrivateChatUserId(chat); - const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; + const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; const isPinned = selectIsChatPinned(global, chatId); const isMuted = chat ? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)) @@ -101,7 +101,7 @@ export default memo(withGlobal( return { chat, - privateChatUser, + user, isPinned, isMuted, }; diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 84f0e64aa..5aa6ac081 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -37,6 +37,7 @@ import { selectChatBot, selectChatUser, selectChatMessage, + selectUserStatus, } from '../../../modules/selectors'; import { getAllowedAttachmentOptions, @@ -106,38 +107,40 @@ type OwnProps = { onDropHide: NoneToVoidFunction; }; -type StateProps = { - editingMessage?: ApiMessage; - chat?: ApiChat; - draft?: ApiFormattedText; - isChatWithBot?: boolean; - isChatWithSelf?: boolean; - isRightColumnShown?: boolean; - isSelectModeActive?: boolean; - isForwarding?: boolean; - isPollModalOpen?: boolean; - botKeyboardMessageId?: number; - botKeyboardPlaceholder?: string; - withScheduledButton?: boolean; - shouldSchedule?: boolean; - canScheduleUntilOnline?: boolean; - stickersForEmoji?: ApiSticker[]; - groupChatMembers?: ApiChatMember[]; - currentUserId?: string; - usersById?: Record; - recentEmojis: string[]; - lastSyncTime?: number; - contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; - shouldSuggestStickers?: boolean; - baseEmojiKeywords?: Record; - emojiKeywords?: Record; - serverTimeOffset: number; - topInlineBotIds?: string[]; - isInlineBotLoading: boolean; - inlineBots?: Record; - botCommands?: ApiBotCommand[] | false; - chatBotCommands?: ApiBotCommand[]; -} & Pick; +type StateProps = + { + editingMessage?: ApiMessage; + chat?: ApiChat; + draft?: ApiFormattedText; + isChatWithBot?: boolean; + isChatWithSelf?: boolean; + isRightColumnShown?: boolean; + isSelectModeActive?: boolean; + isForwarding?: boolean; + isPollModalOpen?: boolean; + botKeyboardMessageId?: number; + botKeyboardPlaceholder?: string; + withScheduledButton?: boolean; + shouldSchedule?: boolean; + canScheduleUntilOnline?: boolean; + stickersForEmoji?: ApiSticker[]; + groupChatMembers?: ApiChatMember[]; + currentUserId?: string; + usersById?: Record; + recentEmojis: string[]; + lastSyncTime?: number; + contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; + shouldSuggestStickers?: boolean; + baseEmojiKeywords?: Record; + emojiKeywords?: Record; + serverTimeOffset: number; + topInlineBotIds?: string[]; + isInlineBotLoading: boolean; + inlineBots?: Record; + botCommands?: ApiBotCommand[] | false; + chatBotCommands?: ApiBotCommand[]; + } + & Pick; type DispatchProps = Pick( chat, isChatWithBot, isChatWithSelf, - canScheduleUntilOnline: ( - !isChatWithSelf && !isChatWithBot - && (chat && chatUser && isUserId(chatId) && chatUser.status && Boolean(chatUser.status.wasOnline)) + canScheduleUntilOnline: Boolean( + !isChatWithSelf && !isChatWithBot && chat && chatUser + && isUserId(chatId) && selectUserStatus(global, chatId)?.wasOnline, ), isRightColumnShown: selectIsRightColumnShown(global), isSelectModeActive: selectIsInSelectMode(global), diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index d7986d401..f0bdbaba6 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -4,11 +4,12 @@ import React, { import { withGlobal } from '../../lib/teact/teactn'; import { + MAIN_THREAD_ID, ApiMessage, + ApiChat, ApiChatMember, ApiUser, - ApiChat, - MAIN_THREAD_ID, + ApiUserStatus, } from '../../api/types'; import { GlobalActions } from '../../global/types'; import { @@ -87,6 +88,7 @@ type StateProps = { commonChatIds?: string[]; chatsById: Record; usersById: Record; + userStatusesById: Record; isRightColumnShown: boolean; isRestricted?: boolean; lastSyncTime?: number; @@ -129,6 +131,7 @@ const Profile: FC = ({ commonChatIds, members, usersById, + userStatusesById, chatsById, isRightColumnShown, isRestricted, @@ -168,7 +171,8 @@ const Profile: FC = ({ const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( isRightColumnShown, loadMoreMembers, loadCommonChats, searchMediaMessagesLocal, tabType, mediaSearchType, members, - commonChatIds, usersById, chatsById, chatMessages, foundIds, chatId, lastSyncTime, serverTimeOffset, + commonChatIds, usersById, userStatusesById, chatsById, chatMessages, foundIds, chatId, lastSyncTime, + serverTimeOffset, ); const activeKey = tabs.findIndex(({ type }) => type === resultType); @@ -487,7 +491,7 @@ export default memo(withGlobal( const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; - const { byId: usersById } = global.users; + const { byId: usersById, statusesById: userStatusesById } = global.users; const { byId: chatsById } = global.chats; const isGroup = chat && isChatGroup(chat); @@ -532,6 +536,7 @@ export default memo(withGlobal( serverTimeOffset: global.serverTimeOffset, activeDownloadIds, usersById, + userStatusesById, chatsById, ...(hasMembersTab && members && { members }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index 334fc2e9c..b218e7a29 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -1,7 +1,7 @@ import { useMemo, useRef } from '../../../lib/teact/teact'; import { - ApiChat, ApiChatMember, ApiMessage, ApiUser, + ApiChat, ApiChatMember, ApiMessage, ApiUser, ApiUserStatus, } from '../../../api/types'; import { ProfileTabType, SharedMediaType } from '../../../types'; @@ -20,6 +20,7 @@ export default function useProfileViewportIds( groupChatMembers?: ApiChatMember[], commonChatIds?: string[], usersById?: Record, + userStatusesById?: Record, chatsById?: Record, chatMessages?: Record, foundIds?: number[], @@ -30,12 +31,18 @@ export default function useProfileViewportIds( const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType; const memberIds = useMemo(() => { - if (!groupChatMembers || !usersById) { + if (!groupChatMembers || !usersById || !userStatusesById) { return undefined; } - return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); - }, [groupChatMembers, serverTimeOffset, usersById]); + return sortUserIds( + groupChatMembers.map(({ userId }) => userId), + usersById, + userStatusesById, + undefined, + serverTimeOffset, + ); + }, [groupChatMembers, serverTimeOffset, usersById, userStatusesById]); const chatIds = useMemo(() => { if (!commonChatIds || !chatsById) { diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index abce5caa7..0188ba755 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -3,7 +3,7 @@ import React, { } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; -import { ApiChatMember, ApiUser } from '../../../api/types'; +import { ApiChatMember, ApiUser, ApiUserStatus } from '../../../api/types'; import { GlobalActions } from '../../../global/types'; import { selectChat } from '../../../modules/selectors'; import { sortUserIds, isChatChannel } from '../../../modules/helpers'; @@ -22,6 +22,7 @@ type OwnProps = { type StateProps = { usersById: Record; + userStatusesById: Record; members?: ApiChatMember[]; isChannel?: boolean; serverTimeOffset: number; @@ -32,6 +33,7 @@ type DispatchProps = Pick; const ManageGroupMembers: FC = ({ members, usersById, + userStatusesById, isChannel, openUserInfo, onClose, @@ -43,8 +45,14 @@ const ManageGroupMembers: FC = ({ return undefined; } - return sortUserIds(members.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); - }, [members, serverTimeOffset, usersById]); + return sortUserIds( + members.map(({ userId }) => userId), + usersById, + userStatusesById, + undefined, + serverTimeOffset, + ); + }, [members, serverTimeOffset, usersById, userStatusesById]); const handleMemberClick = useCallback((id: string) => { openUserInfo({ id }); @@ -83,13 +91,14 @@ const ManageGroupMembers: FC = ({ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); - const { byId: usersById } = global.users; + const { byId: usersById, statusesById: userStatusesById } = global.users; const members = chat?.fullInfo?.members; const isChannel = chat && isChatChannel(chat); return { members, usersById, + userStatusesById, isChannel, serverTimeOffset: global.serverTimeOffset, }; diff --git a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx index 42edcbc27..58e11c45b 100644 --- a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx +++ b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx @@ -3,7 +3,7 @@ import React, { } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; -import { ApiChatMember, ApiUser } from '../../../api/types'; +import { ApiChatMember, ApiUser, ApiUserStatus } from '../../../api/types'; import { ManagementScreens } from '../../../types'; import { selectChat } from '../../../modules/selectors'; @@ -24,6 +24,7 @@ type OwnProps = { type StateProps = { usersById: Record; + userStatusesById: Record; members?: ApiChatMember[]; isChannel?: boolean; serverTimeOffset: number; @@ -31,6 +32,7 @@ type StateProps = { const ManageGroupUserPermissionsCreate: FC = ({ usersById, + userStatusesById, members, isChannel, onScreenSelect, @@ -48,9 +50,12 @@ const ManageGroupUserPermissionsCreate: FC = ({ return sortUserIds( members.filter((member) => !member.isOwner).map(({ userId }) => userId), - usersById, undefined, serverTimeOffset, + usersById, + userStatusesById, + undefined, + serverTimeOffset, ); - }, [members, serverTimeOffset, usersById]); + }, [members, serverTimeOffset, usersById, userStatusesById]); const handleExceptionMemberClick = useCallback((memberId: string) => { onChatMemberSelect(memberId); @@ -88,13 +93,14 @@ const ManageGroupUserPermissionsCreate: FC = ({ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); - const { byId: usersById } = global.users; + const { byId: usersById, statusesById: userStatusesById } = global.users; const members = chat?.fullInfo?.members; const isChannel = chat && isChatChannel(chat); return { members, usersById, + userStatusesById, isChannel, serverTimeOffset: global.serverTimeOffset, }; diff --git a/src/global/cache.ts b/src/global/cache.ts index d9ff356f9..9f6d87013 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -190,9 +190,13 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE; } - if (cached.groupCalls === undefined) { + if (!cached.groupCalls) { cached.groupCalls = initialState.groupCalls; } + + if (!cached.users.statusesById) { + cached.users.statusesById = {}; + } } function updateCache() { @@ -251,7 +255,7 @@ function reduceShowChatInfo(global: GlobalState): boolean { } function reduceUsers(global: GlobalState): GlobalState['users'] { - const { users: { byId, selectedId } } = global; + const { users: { byId, statusesById, selectedId } } = global; const idsToSave = [ ...(global.chats.listIds.active || []).slice(0, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT).filter(isUserId), ...Object.keys(byId), @@ -259,6 +263,7 @@ function reduceUsers(global: GlobalState): GlobalState['users'] { return { byId: pick(byId, idsToSave), + statusesById: pick(statusesById, idsToSave), selectedId: window.innerWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN ? selectedId : undefined, }; } diff --git a/src/global/initial.ts b/src/global/initial.ts index 9c5453a72..8a1a9913e 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -29,6 +29,7 @@ export const INITIAL_STATE: GlobalState = { users: { byId: {}, + statusesById: {}, }, chats: { diff --git a/src/global/types.ts b/src/global/types.ts index b2a724285..2a89e93ae 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -3,6 +3,7 @@ import { ApiMessage, ApiThreadInfo, ApiUser, + ApiUserStatus, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType, ApiStickerSet, @@ -127,6 +128,7 @@ export type GlobalState = { users: { byId: Record; + statusesById: Record; // TODO Remove selectedId?: string; }; diff --git a/src/hooks/useChatContextActions.ts b/src/hooks/useChatContextActions.ts index 65930830b..d1b33aa47 100644 --- a/src/hooks/useChatContextActions.ts +++ b/src/hooks/useChatContextActions.ts @@ -11,7 +11,7 @@ import useLang from './useLang'; export default ({ chat, - privateChatUser, + user, handleDelete, handleChatFolderChange, folderId, @@ -19,7 +19,7 @@ export default ({ isMuted, }: { chat: ApiChat | undefined; - privateChatUser: ApiUser | undefined; + user: ApiUser | undefined; handleDelete: () => void; handleChatFolderChange: () => void; folderId?: number; @@ -28,7 +28,7 @@ export default ({ }, isInSearch = false) => { const lang = useLang(); - const { isSelf } = privateChatUser || {}; + const { isSelf } = user || {}; return useMemo(() => { if (!chat) { diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 0eec22192..e7c1e06f2 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -22,6 +22,7 @@ import { callApi } from '../../../api/gramjs'; import { addChats, addUsers, + addUserStatuses, replaceThreadParam, updateChatListIds, updateChats, @@ -1007,6 +1008,8 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: string, off global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); + global = addUserStatuses(global, result.userStatusesById); + global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateChatListIds(global, listType, chatIds); global = updateChatListSecondaryInfo(global, listType, result); diff --git a/src/modules/actions/api/sync.ts b/src/modules/actions/api/sync.ts index cd4e07c41..c0f787008 100644 --- a/src/modules/actions/api/sync.ts +++ b/src/modules/actions/api/sync.ts @@ -17,7 +17,9 @@ import { replaceChatListIds, replaceChats, replaceUsers, + replaceUserStatuses, updateUsers, + addUserStatuses, updateChats, updateChatListSecondaryInfo, updateThreadInfos, @@ -146,16 +148,10 @@ async function loadAndReplaceChats() { savedUsers.push(...result.users); savedChats.push(...result.chats); + global = replaceUserStatuses(global, result.userStatusesById); + global = replaceChats(global, buildCollectionByKey(savedChats, 'id')); global = replaceChatListIds(global, 'active', result.chatIds); - - global = { - ...global, - chats: { - ...global.chats, - }, - }; - global = updateChatListSecondaryInfo(global, 'active', result); Object.keys(result.draftsById).forEach((chatId) => { @@ -190,10 +186,14 @@ async function loadAndReplaceArchivedChats() { } let global = getGlobal(); + global = updateUsers(global, buildCollectionByKey(result.users, 'id')); + global = addUserStatuses(global, result.userStatusesById); + global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = replaceChatListIds(global, 'archived', result.chatIds); global = updateChatListSecondaryInfo(global, 'archived', result); + setGlobal(global); } @@ -337,13 +337,16 @@ async function loadAndUpdateUsers() { ...(contactIds || []), ].map((id) => selectUser(global, id)).filter(Boolean as any); - const updatedUsers = await callApi('fetchUsers', { users }); - if (!updatedUsers) { + const result = await callApi('fetchUsers', { users }); + if (!result) { return; } + const { users: updatedUsers, userStatusesById } = result; + global = getGlobal(); global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id')); + global = addUserStatuses(global, userStatusesById); setGlobal(global); } diff --git a/src/modules/actions/api/users.ts b/src/modules/actions/api/users.ts index 125ae13f8..cf93f6217 100644 --- a/src/modules/actions/api/users.ts +++ b/src/modules/actions/api/users.ts @@ -11,7 +11,7 @@ import { isUserBot, isUserId } from '../../helpers'; import { callApi } from '../../../api/gramjs'; import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors'; import { - addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers, + addChats, addUsers, replaceUserStatuses, updateChat, updateManagementProgress, updateUser, updateUsers, updateUserSearch, updateUserSearchFetchingStatus, } from '../../reducers'; import { getServerTime } from '../../../util/serverTime'; @@ -40,13 +40,21 @@ addReducer('loadUser', (global, actions, payload) => { } (async () => { - const updatedUsers = await callApi('fetchUsers', { users: [user] }); - if (!updatedUsers) { + const result = await callApi('fetchUsers', { users: [user] }); + if (!result) { return; } + const { users, userStatusesById } = result; + global = getGlobal(); - global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id')); + + global = updateUsers(global, buildCollectionByKey(users, 'id')); + setGlobal(replaceUserStatuses(global, { + ...global.users.statusesById, + ...userStatusesById, + })); + setGlobal(global); })(); }); diff --git a/src/modules/actions/apiUpdaters/users.ts b/src/modules/actions/apiUpdaters/users.ts index 943fb0105..40e102fd9 100644 --- a/src/modules/actions/apiUpdaters/users.ts +++ b/src/modules/actions/apiUpdaters/users.ts @@ -2,30 +2,29 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; import { ApiUpdate, ApiUserStatus } from '../../../api/types'; -import { deleteUser, updateUser } from '../../reducers'; +import { deleteUser, replaceUserStatuses, updateUser } from '../../reducers'; import { throttle } from '../../../util/schedulers'; const STATUS_UPDATE_THROTTLE = 3000; const flushStatusUpdatesThrottled = throttle(flushStatusUpdates, STATUS_UPDATE_THROTTLE, true); -let pendingStatusUpdates: [string, ApiUserStatus][] = []; +let pendingStatusUpdates: Record = {}; function scheduleStatusUpdate(userId: string, statusUpdate: ApiUserStatus) { - pendingStatusUpdates.push([userId, statusUpdate]); + pendingStatusUpdates[userId] = statusUpdate; flushStatusUpdatesThrottled(); } function flushStatusUpdates() { - let global = getGlobal(); - pendingStatusUpdates.forEach(([userId, statusUpdate]) => { - global = updateUser(global, userId, { - status: statusUpdate, - }); - }); - setGlobal(global); + const global = getGlobal(); - pendingStatusUpdates = []; + setGlobal(replaceUserStatuses(global, { + ...global.users.statusesById, + ...pendingStatusUpdates, + })); + + pendingStatusUpdates = {}; } addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { diff --git a/src/modules/helpers/users.ts b/src/modules/helpers/users.ts index 08e6b4cde..6f09bd141 100644 --- a/src/modules/helpers/users.ts +++ b/src/modules/helpers/users.ts @@ -1,4 +1,4 @@ -import { ApiChat, ApiUser } from '../../api/types'; +import { ApiChat, ApiUser, ApiUserStatus } from '../../api/types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { formatFullDate, formatTime } from '../../util/dateFormat'; @@ -65,7 +65,9 @@ export function getUserFullName(user?: ApiUser) { return undefined; } -export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: number) { +export function getUserStatus( + lang: LangFn, user: ApiUser, userStatus: ApiUserStatus | undefined, serverTimeOffset: number, +) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) { return lang('ServiceNotifications').toLowerCase(); } @@ -74,11 +76,11 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num return lang('Bot'); } - if (!user.status) { + if (!userStatus) { return ''; } - switch (user.status.type) { + switch (userStatus.type) { case 'userStatusEmpty': { return lang('ALongTimeAgo'); } @@ -92,7 +94,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num } case 'userStatusOffline': { - const { wasOnline } = user.status; + const { wasOnline } = userStatus; if (!wasOnline) return lang('LastSeen.Offline'); @@ -156,10 +158,10 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num } } -export function isUserOnline(user: ApiUser) { - const { id, status, type } = user; +export function isUserOnline(user: ApiUser, userStatus?: ApiUserStatus) { + const { id, type } = user; - if (!status) { + if (!userStatus) { return false; } @@ -167,11 +169,11 @@ export function isUserOnline(user: ApiUser) { return false; } - return status.type === 'userStatusOnline' && type !== 'userTypeBot'; + return userStatus.type === 'userStatusOnline' && type !== 'userTypeBot'; } export function isDeletedUser(user: ApiUser) { - if (!user.status || user.type === 'userTypeBot' || user.id === SERVICE_NOTIFICATIONS_USER_ID) { + if (user.noStatus || user.type === 'userTypeBot' || user.id === SERVICE_NOTIFICATIONS_USER_ID) { return false; } @@ -190,6 +192,7 @@ export function getCanAddContact(user: ApiUser) { export function sortUserIds( userIds: string[], usersById: Record, + userStatusesById: Record, priorityIds?: string[], serverTimeOffset = 0, ) { @@ -204,17 +207,18 @@ export function sortUserIds( } const user = usersById[id]; - if (!user || !user.status) { + const userStatus = userStatusesById[id]; + if (!user || !userStatus) { return 0; } - if (user.status.type === 'userStatusOnline') { - return user.status.expires; - } else if (user.status.type === 'userStatusOffline' && user.status.wasOnline) { - return user.status.wasOnline; + if (userStatus.type === 'userStatusOnline') { + return userStatus.expires; + } else if (userStatus.type === 'userStatusOffline' && userStatus.wasOnline) { + return userStatus.wasOnline; } - switch (user.status.type) { + switch (userStatus.type) { case 'userStatusRecently': return now - 60 * 60 * 24; case 'userStatusLastWeek': diff --git a/src/modules/reducers/chats.ts b/src/modules/reducers/chats.ts index a4518515c..7bca789f3 100644 --- a/src/modules/reducers/chats.ts +++ b/src/modules/reducers/chats.ts @@ -2,7 +2,7 @@ import { GlobalState } from '../../global/types'; import { ApiChat, ApiPhoto } from '../../api/types'; import { ARCHIVED_FOLDER_ID } from '../../config'; -import { omit } from '../../util/iteratees'; +import { mapValues, omit } from '../../util/iteratees'; import { selectChatListType } from '../selectors'; export function replaceChatListIds( @@ -48,26 +48,6 @@ export function replaceChats(global: GlobalState, newById: Record, photo?: ApiPhoto, -): ApiChat { - const { byId } = global.chats; - const chat = byId[chatId]; - const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin; - const updatedChat = { - ...chat, - ...(shouldOmitMinInfo ? omit(chatUpdate, ['isMin', 'accessHash']) : chatUpdate), - ...(photo && { photos: [photo, ...(chat.photos || [])] }), - }; - - if (!updatedChat.id || !updatedChat.type) { - return updatedChat; - } - - return updatedChat; -} - export function updateChat( global: GlobalState, chatId: string, chatUpdate: Partial, photo?: ApiPhoto, ): GlobalState { @@ -81,52 +61,67 @@ export function updateChat( }); } -export function updateChats(global: GlobalState, updatedById: Record): GlobalState { - const updatedChats = Object.keys(updatedById).reduce>((acc, id) => { - const updatedChat = getUpdatedChat(global, id, updatedById[id]); - if (updatedChat) { - acc[id] = updatedChat; - } - return acc; - }, {}); +export function updateChats(global: GlobalState, newById: Record): GlobalState { + const updatedById = mapValues(newById, (chat, id) => { + return getUpdatedChat(global, id, chat); + }); global = replaceChats(global, { ...global.chats.byId, - ...updatedChats, + ...updatedById, }); return global; } // @optimization Allows to avoid redundant updates which cause a lot of renders -export function addChats(global: GlobalState, addedById: Record): GlobalState { +export function addChats(global: GlobalState, newById: Record): GlobalState { const { byId } = global.chats; let isAdded = false; - const addedChats = Object.keys(addedById).reduce>((acc, id) => { - if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { - const updatedChat = getUpdatedChat(global, id, addedById[id]); - if (updatedChat) { - acc[id] = updatedChat; + const addedById = Object.keys(newById).reduce>((acc, id) => { + if (!byId[id] || (byId[id].isMin && !newById[id].isMin)) { + acc[id] = getUpdatedChat(global, id, newById[id]); - if (!isAdded) { - isAdded = true; - } + if (!isAdded) { + isAdded = true; } } return acc; }, {}); - if (isAdded) { - global = replaceChats(global, { - ...global.chats.byId, - ...addedChats, - }); + if (!isAdded) { + return global; } + global = replaceChats(global, { + ...byId, + ...addedById, + }); + return global; } +// @optimization Don't spread/unspread global for each element, do it in a batch +function getUpdatedChat( + global: GlobalState, chatId: string, chatUpdate: Partial, photo?: ApiPhoto, +) { + const { byId } = global.chats; + const chat = byId[chatId]; + const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin; + const updatedChat: ApiChat = { + ...chat, + ...(shouldOmitMinInfo ? omit(chatUpdate, ['isMin', 'accessHash']) : chatUpdate), + ...(photo && { photos: [photo, ...(chat.photos || [])] }), + }; + + if (!updatedChat.id || !updatedChat.type) { + return updatedChat; + } + + return updatedChat; +} + export function updateChatListType( global: GlobalState, chatId: string, diff --git a/src/modules/reducers/users.ts b/src/modules/reducers/users.ts index 179f2559f..707b9ee82 100644 --- a/src/modules/reducers/users.ts +++ b/src/modules/reducers/users.ts @@ -1,7 +1,7 @@ import { GlobalState } from '../../global/types'; -import { ApiUser } from '../../api/types'; +import { ApiUser, ApiUserStatus } from '../../api/types'; -import { omit } from '../../util/iteratees'; +import { mapValues, omit, pick } from '../../util/iteratees'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; export function replaceUsers(global: GlobalState, newById: Record): GlobalState { @@ -14,24 +14,6 @@ export function replaceUsers(global: GlobalState, newById: Record): ApiUser { - const { byId } = global.users; - const user = byId[userId]; - const shouldOmitMinInfo = userUpdate.isMin && user && !user.isMin; - - const updatedUser = { - ...user, - ...(shouldOmitMinInfo ? omit(userUpdate, ['isMin', 'accessHash']) : userUpdate), - }; - - if (!updatedUser.id || !updatedUser.type) { - return user; - } - - return updatedUser; -} - function updateContactList(global: GlobalState, updatedUsers: ApiUser[]): GlobalState { const { userIds: contactUserIds } = global.contactList || {}; @@ -67,56 +49,69 @@ export function updateUser(global: GlobalState, userId: string, userUpdate: Part }); } -export function updateUsers(global: GlobalState, updatedById: Record): GlobalState { - const updatedUsers = Object.keys(updatedById).reduce>((acc, id) => { - const updatedUser = getUpdatedUser(global, id, updatedById[id]); - if (updatedUser) { - acc[id] = updatedUser; - } - return acc; - }, {}); - - global = updateContactList(global, Object.values(updatedUsers)); +export function updateUsers(global: GlobalState, newById: Record): GlobalState { + const updatedById = mapValues(newById, (user, id) => { + return getUpdatedUser(global, id, user); + }); global = replaceUsers(global, { ...global.users.byId, - ...updatedUsers, + ...updatedById, }); + global = updateContactList(global, Object.values(updatedById)); + return global; } // @optimization Allows to avoid redundant updates which cause a lot of renders -export function addUsers(global: GlobalState, addedById: Record): GlobalState { +export function addUsers(global: GlobalState, newById: Record): GlobalState { const { byId } = global.users; let isAdded = false; - const addedUsers = Object.keys(addedById).reduce>((acc, id) => { - if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { - const updatedUser = getUpdatedUser(global, id, addedById[id]); - if (updatedUser) { - acc[id] = updatedUser; + const addedById = Object.keys(newById).reduce>((acc, id) => { + if (!byId[id] || (byId[id].isMin && !newById[id].isMin)) { + acc[id] = getUpdatedUser(global, id, newById[id]); - if (!isAdded) { - isAdded = true; - } + if (!isAdded) { + isAdded = true; } } return acc; }, {}); - if (isAdded) { - global = replaceUsers(global, { - ...global.users.byId, - ...addedUsers, - }); - - global = updateContactList(global, Object.values(addedUsers)); + if (!isAdded) { + return global; } + global = replaceUsers(global, { + ...byId, + ...addedById, + }); + + global = updateContactList(global, Object.values(addedById)); + return global; } +// @optimization Don't spread/unspread global for each element, do it in a batch +function getUpdatedUser(global: GlobalState, userId: string, userUpdate: Partial) { + const { byId } = global.users; + const user = byId[userId]; + const shouldOmitMinInfo = userUpdate.isMin && user && !user.isMin; + + const updatedUser = { + ...user, + ...(shouldOmitMinInfo ? omit(userUpdate, ['isMin', 'accessHash']) : userUpdate), + }; + + if (!updatedUser.id || !updatedUser.type) { + return user; + } + + return updatedUser; +} + export function updateSelectedUserId(global: GlobalState, selectedId?: string): GlobalState { if (global.users.selectedId === selectedId) { return global; @@ -182,3 +177,30 @@ export function updateUserBlockedState(global: GlobalState, userId: string, isBl }, }); } + +export function replaceUserStatuses(global: GlobalState, newById: Record): GlobalState { + return { + ...global, + users: { + ...global.users, + statusesById: newById, + }, + }; +} + +// @optimization Allows to avoid redundant updates which cause a lot of renders +export function addUserStatuses(global: GlobalState, newById: Record): GlobalState { + const { statusesById } = global.users; + + const newKeys = Object.keys(newById).filter((id) => !statusesById[id]); + if (!newKeys.length) { + return global; + } + + global = replaceUserStatuses(global, { + ...statusesById, + ...pick(newById, newKeys), + }); + + return global; +} diff --git a/src/modules/selectors/chats.ts b/src/modules/selectors/chats.ts index abd0c27a7..095d7d0e1 100644 --- a/src/modules/selectors/chats.ts +++ b/src/modules/selectors/chats.ts @@ -46,7 +46,11 @@ export function selectChatOnlineCount(global: GlobalState, chat: ApiChat) { } return chat.fullInfo.members.reduce((onlineCount, { userId }) => { - if (global.users.byId[userId] && isUserOnline(global.users.byId[userId]) && userId !== global.currentUserId) { + if ( + userId !== global.currentUserId + && global.users.byId[userId] + && isUserOnline(global.users.byId[userId], global.users.statusesById[userId]) + ) { return onlineCount + 1; } diff --git a/src/modules/selectors/users.ts b/src/modules/selectors/users.ts index 77bc378f8..ffde12b6e 100644 --- a/src/modules/selectors/users.ts +++ b/src/modules/selectors/users.ts @@ -1,10 +1,14 @@ import { GlobalState } from '../../global/types'; -import { ApiChat, ApiUser } from '../../api/types'; +import { ApiChat, ApiUser, ApiUserStatus } from '../../api/types'; export function selectUser(global: GlobalState, userId: string): ApiUser | undefined { return global.users.byId[userId]; } +export function selectUserStatus(global: GlobalState, userId: string): ApiUserStatus | undefined { + return global.users.statusesById[userId]; +} + export function selectIsUserBlocked(global: GlobalState, userId: string) { const user = selectUser(global, userId);