[Perf] Extract users.statusesById to a separate global store

This commit is contained in:
Alexander Zinchuk 2021-12-04 13:44:03 +01:00
parent 5563b0b4f4
commit 865ed08d82
29 changed files with 356 additions and 235 deletions

View File

@ -47,7 +47,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
...(lastName && { lastName }), ...(lastName && { lastName }),
username: mtpUser.username || '', username: mtpUser.username || '',
phoneNumber: mtpUser.phone || '', phoneNumber: mtpUser.phone || '',
status: buildApiUserStatus(mtpUser.status), noStatus: !mtpUser.status,
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }), ...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
...(avatarHash && { avatarHash }), ...(avatarHash && { avatarHash }),
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
@ -88,3 +88,23 @@ function buildApiBotCommands(botId: string, botInfo: GramJs.BotInfo) {
description, description,
})) as ApiBotCommand[]; })) as ApiBotCommand[];
} }
export function buildApiUsersAndStatuses(mtpUsers: GramJs.TypeUser[]) {
const userStatusesById: Record<string, ApiUserStatus> = {};
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 };
}

View File

@ -28,7 +28,7 @@ import {
buildApiChatBotCommands, buildApiChatBotCommands,
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import localDb from '../localDb'; import localDb from '../localDb';
import { import {
@ -146,13 +146,11 @@ export async function fetchChats({
} }
}); });
const users = (resultPinned ? resultPinned.users : []).concat(result.users)
.map(buildApiUser)
.filter<ApiUser>(Boolean as any);
const chatIds = chats.map((chat) => chat.id); 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) { if (result instanceof GramJs.messages.DialogsSlice) {
totalChatCount = result.count; totalChatCount = result.count;
} else { } else {
@ -163,6 +161,7 @@ export async function fetchChats({
chatIds, chatIds,
chats, chats,
users, users,
userStatusesById,
draftsById, draftsById,
replyingToById, replyingToById,
orderedPinnedIds: withPinned ? orderedPinnedIds : undefined, orderedPinnedIds: withPinned ? orderedPinnedIds : undefined,

View File

@ -14,7 +14,7 @@ import {
buildMtpPeerId, buildMtpPeerId,
getEntityTypeById, getEntityTypeById,
} from '../gramjsBuilders'; } from '../gramjsBuilders';
import { buildApiUser, buildApiUserFromFull } from '../apiBuilders/users'; import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPhoto } from '../apiBuilders/common';
import localDb from '../localDb'; import localDb from '../localDb';
@ -139,7 +139,7 @@ export async function fetchUsers({ users }: { users: ApiUser[] }) {
} }
}); });
return result.map(buildApiUser).filter<ApiUser>(Boolean as any); return buildApiUsersAndStatuses(result);
} }
export function updateContact({ export function updateContact({

View File

@ -10,7 +10,7 @@ export interface ApiUser {
type: ApiUserType; type: ApiUserType;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
status?: ApiUserStatus; noStatus?: boolean;
username: string; username: string;
phoneNumber: string; phoneNumber: string;
accessHash?: string; accessHash?: string;

View File

@ -1,7 +1,9 @@
import { MouseEvent as ReactMouseEvent } from 'react'; import { MouseEvent as ReactMouseEvent } from 'react';
import React, { FC, memo, useCallback } from '../../lib/teact/teact'; 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 { IS_TEST } from '../../config';
import { import {
@ -26,9 +28,9 @@ import './Avatar.scss';
type OwnProps = { type OwnProps = {
className?: string; className?: string;
size?: 'micro' | 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; size?: 'micro' | 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
withOnlineStatus?: boolean;
chat?: ApiChat; chat?: ApiChat;
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
@ -40,8 +42,8 @@ const Avatar: FC<OwnProps> = ({
size = 'large', size = 'large',
chat, chat,
user, user,
userStatus,
text, text,
withOnlineStatus,
isSavedMessages, isSavedMessages,
lastSyncTime, lastSyncTime,
onClick, onClick,
@ -86,7 +88,7 @@ const Avatar: FC<OwnProps> = ({
content = getFirstLetters(text, 2); content = getFirstLetters(text, 2);
} }
const isOnline = !isSavedMessages && user && isUserOnline(user); const isOnline = !isSavedMessages && user && userStatus && isUserOnline(user, userStatus);
const fullClassName = buildClassName( const fullClassName = buildClassName(
`Avatar size-${size}`, `Avatar size-${size}`,
className, className,
@ -94,7 +96,7 @@ const Avatar: FC<OwnProps> = ({
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',
withOnlineStatus && isOnline && 'online', isOnline && 'online',
onClick && 'interactive', onClick && 'interactive',
(!isSavedMessages && !blobUrl) && 'no-photo', (!isSavedMessages && !blobUrl) && 'no-photo',
); );

View File

@ -4,11 +4,11 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn'; 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 { GlobalActions, GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../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 { getUserFullName, getUserStatus, isUserOnline } from '../../modules/helpers';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
@ -34,6 +34,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus;
isSavedMessages?: boolean; isSavedMessages?: boolean;
areMessagesLoaded: boolean; areMessagesLoaded: boolean;
serverTimeOffset: number; serverTimeOffset: number;
@ -52,6 +53,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
noStatusOrTyping, noStatusOrTyping,
noRtl, noRtl,
user, user,
userStatus,
isSavedMessages, isSavedMessages,
areMessagesLoaded, areMessagesLoaded,
lastSyncTime, lastSyncTime,
@ -106,9 +108,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
} }
return ( return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}> <div className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}>
{withUsername && user.username && <span className="handle">{user.username}</span>} {withUsername && user.username && <span className="handle">{user.username}</span>}
<span className="user-status" dir="auto">{getUserStatus(lang, user, serverTimeOffset)}</span> <span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
</div> </div>
); );
} }
@ -143,11 +145,12 @@ export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { lastSyncTime, serverTimeOffset } = global; const { lastSyncTime, serverTimeOffset } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const userStatus = selectUserStatus(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); const areMessagesLoaded = Boolean(selectChatMessages(global, userId));
return { return {
lastSyncTime, user, isSavedMessages, areMessagesLoaded, serverTimeOffset, lastSyncTime, user, userStatus, isSavedMessages, areMessagesLoaded, serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']),

View File

@ -3,12 +3,12 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn'; 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 { GlobalActions, GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { IS_TOUCH_ENV } from '../../util/environment'; import { IS_TOUCH_ENV } from '../../util/environment';
import { selectChat, selectUser } from '../../modules/selectors'; import { selectChat, selectUser, selectUserStatus } from '../../modules/selectors';
import { import {
getUserFullName, getUserStatus, isChatChannel, isUserOnline, getUserFullName, getUserStatus, isChatChannel, isUserOnline,
} from '../../modules/helpers'; } from '../../modules/helpers';
@ -32,6 +32,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus;
chat?: ApiChat; chat?: ApiChat;
isSavedMessages?: boolean; isSavedMessages?: boolean;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
@ -43,6 +44,7 @@ type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
forceShowSelf, forceShowSelf,
user, user,
userStatus,
chat, chat,
isSavedMessages, isSavedMessages,
connectionState, connectionState,
@ -162,8 +164,8 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
function renderStatus() { function renderStatus() {
if (user) { if (user) {
return ( return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}> <div className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}>
<span className="user-status" dir="auto">{getUserStatus(lang, user, serverTimeOffset)}</span> <span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
</div> </div>
); );
} }
@ -227,6 +229,7 @@ export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { connectionState, serverTimeOffset } = global; const { connectionState, serverTimeOffset } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const userStatus = selectUserStatus(global, userId);
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { animationLevel } = global.settings.byKey; const { animationLevel } = global.settings.byKey;
@ -234,6 +237,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
connectionState, connectionState,
user, user,
userStatus,
chat, chat,
isSavedMessages, isSavedMessages,
animationLevel, animationLevel,

View File

@ -7,7 +7,7 @@ import useLang, { LangFn } from '../../../hooks/useLang';
import { GlobalActions } from '../../../global/types'; import { GlobalActions } from '../../../global/types';
import { import {
ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, MAIN_THREAD_ID, ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, MAIN_THREAD_ID, ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import { ANIMATION_END_DELAY } from '../../../config'; import { ANIMATION_END_DELAY } from '../../../config';
@ -30,7 +30,7 @@ import {
} from '../../../modules/helpers'; } from '../../../modules/helpers';
import { import {
selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList, selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList,
selectNotifySettings, selectNotifyExceptions, selectNotifySettings, selectNotifyExceptions, selectUserStatus,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
import { renderActionMessageText } from '../../common/helpers/renderActionMessageText'; import { renderActionMessageText } from '../../common/helpers/renderActionMessageText';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -67,7 +67,8 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
isMuted?: boolean; isMuted?: boolean;
privateChatUser?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
usersById?: Record<string, ApiUser>; usersById?: Record<string, ApiUser>;
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
@ -94,7 +95,8 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
isPinned, isPinned,
chat, chat,
isMuted, isMuted,
privateChatUser, user,
userStatus,
actionTargetUserIds, actionTargetUserIds,
usersById, usersById,
lastMessageSender, lastMessageSender,
@ -196,7 +198,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
const contextActions = useChatContextActions({ const contextActions = useChatContextActions({
chat, chat,
privateChatUser, user,
handleDelete, handleDelete,
handleChatFolderChange, handleChatFolderChange,
folderId, folderId,
@ -281,9 +283,9 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="status"> <div className="status">
<Avatar <Avatar
chat={chat} chat={chat}
user={privateChatUser} user={user}
withOnlineStatus userStatus={userStatus}
isSavedMessages={privateChatUser?.isSelf} isSavedMessages={user?.isSelf}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
/> />
{chat.isCallActive && chat.isCallNotEmpty && ( {chat.isCallActive && chat.isCallNotEmpty && (
@ -292,7 +294,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
</div> </div>
<div className="info"> <div className="info">
<div className="title"> <div className="title">
<h3>{renderText(getChatTitle(lang, chat, privateChatUser))}</h3> <h3>{renderText(getChatTitle(lang, chat, user))}</h3>
{chat.isVerified && <VerifiedIcon />} {chat.isVerified && <VerifiedIcon />}
{isMuted && <i className="icon-muted" />} {isMuted && <i className="icon-muted" />}
{chat.lastMessage && ( {chat.lastMessage && (
@ -377,7 +379,10 @@ export default memo(withGlobal<OwnProps>(
canScrollDown: isSelected && messageListType === 'thread', canScrollDown: isSelected && messageListType === 'thread',
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }), ...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }),
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }), ...(privateChatUserId && {
user: selectUser(global, privateChatUserId),
userStatus: selectUserStatus(global, privateChatUserId),
}),
...(actionTargetUserIds && { usersById }), ...(actionTargetUserIds && { usersById }),
}; };
}, },

View File

@ -4,7 +4,7 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types'; 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 { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
@ -27,6 +27,7 @@ export type OwnProps = {
type StateProps = { type StateProps = {
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
contactIds?: string[]; contactIds?: string[];
serverTimeOffset: number; serverTimeOffset: number;
}; };
@ -36,8 +37,15 @@ type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
const runThrottled = throttle((cb) => cb(), 60000, true); const runThrottled = throttle((cb) => cb(), 60000, true);
const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({ const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
isActive, onReset, isActive,
filter, usersById, contactIds, loadContactList, openChat, serverTimeOffset, filter,
usersById,
userStatusesById,
contactIds,
serverTimeOffset,
onReset,
loadContactList,
openChat,
}) => { }) => {
// Due to the parent Transition, this component never gets unmounted, // Due to the parent Transition, this component never gets unmounted,
// that's why we use throttled API call on every update. // that's why we use throttled API call on every update.
@ -67,8 +75,8 @@ const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
return fullName && searchWords(fullName, filter); return fullName && searchWords(fullName, filter);
}) : contactIds; }) : contactIds;
return sortUserIds(resultIds, usersById, undefined, serverTimeOffset); return sortUserIds(resultIds, usersById, userStatusesById, undefined, serverTimeOffset);
}, [contactIds, filter, usersById, serverTimeOffset]); }, [contactIds, filter, usersById, userStatusesById, serverTimeOffset]);
const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter)); const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter));
@ -99,10 +107,11 @@ const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const { userIds: contactIds } = global.contactList || {}; const { userIds: contactIds } = global.contactList || {};
const { byId: usersById } = global.users; const { byId: usersById, statusesById: userStatusesById } = global.users;
return { return {
usersById, usersById,
userStatusesById,
contactIds, contactIds,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
}; };

View File

@ -83,7 +83,6 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
<Avatar <Avatar
chat={chat} chat={chat}
user={privateChatUser} user={privateChatUser}
withOnlineStatus
isSavedMessages={privateChatUser?.isSelf} isSavedMessages={privateChatUser?.isSelf}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
/> />

View File

@ -27,7 +27,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
privateChatUser?: ApiUser; user?: ApiUser;
isPinned?: boolean; isPinned?: boolean;
isMuted?: boolean; isMuted?: boolean;
}; };
@ -35,7 +35,7 @@ type StateProps = {
const LeftSearchResultChat: FC<OwnProps & StateProps> = ({ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
chatId, chatId,
chat, chat,
privateChatUser, user,
isPinned, isPinned,
isMuted, isMuted,
withUsername, withUsername,
@ -46,7 +46,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
const contextActions = useChatContextActions({ const contextActions = useChatContextActions({
chat, chat,
privateChatUser, user,
isPinned, isPinned,
isMuted, isMuted,
handleDelete: openDeleteModal, handleDelete: openDeleteModal,
@ -93,7 +93,7 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const privateChatUserId = chat && getPrivateChatUserId(chat); 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 isPinned = selectIsChatPinned(global, chatId);
const isMuted = chat const isMuted = chat
? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)) ? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global))
@ -101,7 +101,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
privateChatUser, user,
isPinned, isPinned,
isMuted, isMuted,
}; };

View File

@ -37,6 +37,7 @@ import {
selectChatBot, selectChatBot,
selectChatUser, selectChatUser,
selectChatMessage, selectChatMessage,
selectUserStatus,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
import { import {
getAllowedAttachmentOptions, getAllowedAttachmentOptions,
@ -106,38 +107,40 @@ type OwnProps = {
onDropHide: NoneToVoidFunction; onDropHide: NoneToVoidFunction;
}; };
type StateProps = { type StateProps =
editingMessage?: ApiMessage; {
chat?: ApiChat; editingMessage?: ApiMessage;
draft?: ApiFormattedText; chat?: ApiChat;
isChatWithBot?: boolean; draft?: ApiFormattedText;
isChatWithSelf?: boolean; isChatWithBot?: boolean;
isRightColumnShown?: boolean; isChatWithSelf?: boolean;
isSelectModeActive?: boolean; isRightColumnShown?: boolean;
isForwarding?: boolean; isSelectModeActive?: boolean;
isPollModalOpen?: boolean; isForwarding?: boolean;
botKeyboardMessageId?: number; isPollModalOpen?: boolean;
botKeyboardPlaceholder?: string; botKeyboardMessageId?: number;
withScheduledButton?: boolean; botKeyboardPlaceholder?: string;
shouldSchedule?: boolean; withScheduledButton?: boolean;
canScheduleUntilOnline?: boolean; shouldSchedule?: boolean;
stickersForEmoji?: ApiSticker[]; canScheduleUntilOnline?: boolean;
groupChatMembers?: ApiChatMember[]; stickersForEmoji?: ApiSticker[];
currentUserId?: string; groupChatMembers?: ApiChatMember[];
usersById?: Record<string, ApiUser>; currentUserId?: string;
recentEmojis: string[]; usersById?: Record<string, ApiUser>;
lastSyncTime?: number; recentEmojis: string[];
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; lastSyncTime?: number;
shouldSuggestStickers?: boolean; contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
baseEmojiKeywords?: Record<string, string[]>; shouldSuggestStickers?: boolean;
emojiKeywords?: Record<string, string[]>; baseEmojiKeywords?: Record<string, string[]>;
serverTimeOffset: number; emojiKeywords?: Record<string, string[]>;
topInlineBotIds?: string[]; serverTimeOffset: number;
isInlineBotLoading: boolean; topInlineBotIds?: string[];
inlineBots?: Record<string, false | InlineBotSettings>; isInlineBotLoading: boolean;
botCommands?: ApiBotCommand[] | false; inlineBots?: Record<string, false | InlineBotSettings>;
chatBotCommands?: ApiBotCommand[]; botCommands?: ApiBotCommand[] | false;
} & Pick<GlobalState, 'connectionState'>; chatBotCommands?: ApiBotCommand[];
}
& Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
'sendMessage' | 'editMessage' | 'saveDraft' | 'forwardMessages' | 'sendMessage' | 'editMessage' | 'saveDraft' | 'forwardMessages' |
@ -1071,9 +1074,9 @@ export default memo(withGlobal<OwnProps>(
chat, chat,
isChatWithBot, isChatWithBot,
isChatWithSelf, isChatWithSelf,
canScheduleUntilOnline: ( canScheduleUntilOnline: Boolean(
!isChatWithSelf && !isChatWithBot !isChatWithSelf && !isChatWithBot && chat && chatUser
&& (chat && chatUser && isUserId(chatId) && chatUser.status && Boolean(chatUser.status.wasOnline)) && isUserId(chatId) && selectUserStatus(global, chatId)?.wasOnline,
), ),
isRightColumnShown: selectIsRightColumnShown(global), isRightColumnShown: selectIsRightColumnShown(global),
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),

View File

@ -4,11 +4,12 @@ import React, {
import { withGlobal } from '../../lib/teact/teactn'; import { withGlobal } from '../../lib/teact/teactn';
import { import {
MAIN_THREAD_ID,
ApiMessage, ApiMessage,
ApiChat,
ApiChatMember, ApiChatMember,
ApiUser, ApiUser,
ApiChat, ApiUserStatus,
MAIN_THREAD_ID,
} from '../../api/types'; } from '../../api/types';
import { GlobalActions } from '../../global/types'; import { GlobalActions } from '../../global/types';
import { import {
@ -87,6 +88,7 @@ type StateProps = {
commonChatIds?: string[]; commonChatIds?: string[];
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
isRightColumnShown: boolean; isRightColumnShown: boolean;
isRestricted?: boolean; isRestricted?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
@ -129,6 +131,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
commonChatIds, commonChatIds,
members, members,
usersById, usersById,
userStatusesById,
chatsById, chatsById,
isRightColumnShown, isRightColumnShown,
isRestricted, isRestricted,
@ -168,7 +171,8 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
isRightColumnShown, loadMoreMembers, loadCommonChats, searchMediaMessagesLocal, tabType, mediaSearchType, members, 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); const activeKey = tabs.findIndex(({ type }) => type === resultType);
@ -487,7 +491,7 @@ export default memo(withGlobal<OwnProps>(
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {};
const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {};
const { byId: usersById } = global.users; const { byId: usersById, statusesById: userStatusesById } = global.users;
const { byId: chatsById } = global.chats; const { byId: chatsById } = global.chats;
const isGroup = chat && isChatGroup(chat); const isGroup = chat && isChatGroup(chat);
@ -532,6 +536,7 @@ export default memo(withGlobal<OwnProps>(
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
activeDownloadIds, activeDownloadIds,
usersById, usersById,
userStatusesById,
chatsById, chatsById,
...(hasMembersTab && members && { members }), ...(hasMembersTab && members && { members }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),

View File

@ -1,7 +1,7 @@
import { useMemo, useRef } from '../../../lib/teact/teact'; import { useMemo, useRef } from '../../../lib/teact/teact';
import { import {
ApiChat, ApiChatMember, ApiMessage, ApiUser, ApiChat, ApiChatMember, ApiMessage, ApiUser, ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import { ProfileTabType, SharedMediaType } from '../../../types'; import { ProfileTabType, SharedMediaType } from '../../../types';
@ -20,6 +20,7 @@ export default function useProfileViewportIds(
groupChatMembers?: ApiChatMember[], groupChatMembers?: ApiChatMember[],
commonChatIds?: string[], commonChatIds?: string[],
usersById?: Record<string, ApiUser>, usersById?: Record<string, ApiUser>,
userStatusesById?: Record<string, ApiUserStatus>,
chatsById?: Record<string, ApiChat>, chatsById?: Record<string, ApiChat>,
chatMessages?: Record<number, ApiMessage>, chatMessages?: Record<number, ApiMessage>,
foundIds?: number[], foundIds?: number[],
@ -30,12 +31,18 @@ export default function useProfileViewportIds(
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType; const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
const memberIds = useMemo(() => { const memberIds = useMemo(() => {
if (!groupChatMembers || !usersById) { if (!groupChatMembers || !usersById || !userStatusesById) {
return undefined; return undefined;
} }
return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); return sortUserIds(
}, [groupChatMembers, serverTimeOffset, usersById]); groupChatMembers.map(({ userId }) => userId),
usersById,
userStatusesById,
undefined,
serverTimeOffset,
);
}, [groupChatMembers, serverTimeOffset, usersById, userStatusesById]);
const chatIds = useMemo(() => { const chatIds = useMemo(() => {
if (!commonChatIds || !chatsById) { if (!commonChatIds || !chatsById) {

View File

@ -3,7 +3,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; 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 { GlobalActions } from '../../../global/types';
import { selectChat } from '../../../modules/selectors'; import { selectChat } from '../../../modules/selectors';
import { sortUserIds, isChatChannel } from '../../../modules/helpers'; import { sortUserIds, isChatChannel } from '../../../modules/helpers';
@ -22,6 +22,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[]; members?: ApiChatMember[];
isChannel?: boolean; isChannel?: boolean;
serverTimeOffset: number; serverTimeOffset: number;
@ -32,6 +33,7 @@ type DispatchProps = Pick<GlobalActions, 'openUserInfo'>;
const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
members, members,
usersById, usersById,
userStatusesById,
isChannel, isChannel,
openUserInfo, openUserInfo,
onClose, onClose,
@ -43,8 +45,14 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
return undefined; return undefined;
} }
return sortUserIds(members.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); return sortUserIds(
}, [members, serverTimeOffset, usersById]); members.map(({ userId }) => userId),
usersById,
userStatusesById,
undefined,
serverTimeOffset,
);
}, [members, serverTimeOffset, usersById, userStatusesById]);
const handleMemberClick = useCallback((id: string) => { const handleMemberClick = useCallback((id: string) => {
openUserInfo({ id }); openUserInfo({ id });
@ -83,13 +91,14 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { byId: usersById } = global.users; const { byId: usersById, statusesById: userStatusesById } = global.users;
const members = chat?.fullInfo?.members; const members = chat?.fullInfo?.members;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
return { return {
members, members,
usersById, usersById,
userStatusesById,
isChannel, isChannel,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
}; };

View File

@ -3,7 +3,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { ApiChatMember, ApiUser } from '../../../api/types'; import { ApiChatMember, ApiUser, ApiUserStatus } from '../../../api/types';
import { ManagementScreens } from '../../../types'; import { ManagementScreens } from '../../../types';
import { selectChat } from '../../../modules/selectors'; import { selectChat } from '../../../modules/selectors';
@ -24,6 +24,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[]; members?: ApiChatMember[];
isChannel?: boolean; isChannel?: boolean;
serverTimeOffset: number; serverTimeOffset: number;
@ -31,6 +32,7 @@ type StateProps = {
const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
usersById, usersById,
userStatusesById,
members, members,
isChannel, isChannel,
onScreenSelect, onScreenSelect,
@ -48,9 +50,12 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
return sortUserIds( return sortUserIds(
members.filter((member) => !member.isOwner).map(({ userId }) => userId), 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) => { const handleExceptionMemberClick = useCallback((memberId: string) => {
onChatMemberSelect(memberId); onChatMemberSelect(memberId);
@ -88,13 +93,14 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { byId: usersById } = global.users; const { byId: usersById, statusesById: userStatusesById } = global.users;
const members = chat?.fullInfo?.members; const members = chat?.fullInfo?.members;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
return { return {
members, members,
usersById, usersById,
userStatusesById,
isChannel, isChannel,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
}; };

View File

@ -190,9 +190,13 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE; cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
} }
if (cached.groupCalls === undefined) { if (!cached.groupCalls) {
cached.groupCalls = initialState.groupCalls; cached.groupCalls = initialState.groupCalls;
} }
if (!cached.users.statusesById) {
cached.users.statusesById = {};
}
} }
function updateCache() { function updateCache() {
@ -251,7 +255,7 @@ function reduceShowChatInfo(global: GlobalState): boolean {
} }
function reduceUsers(global: GlobalState): GlobalState['users'] { function reduceUsers(global: GlobalState): GlobalState['users'] {
const { users: { byId, selectedId } } = global; const { users: { byId, statusesById, selectedId } } = global;
const idsToSave = [ const idsToSave = [
...(global.chats.listIds.active || []).slice(0, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT).filter(isUserId), ...(global.chats.listIds.active || []).slice(0, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT).filter(isUserId),
...Object.keys(byId), ...Object.keys(byId),
@ -259,6 +263,7 @@ function reduceUsers(global: GlobalState): GlobalState['users'] {
return { return {
byId: pick(byId, idsToSave), byId: pick(byId, idsToSave),
statusesById: pick(statusesById, idsToSave),
selectedId: window.innerWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN ? selectedId : undefined, selectedId: window.innerWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN ? selectedId : undefined,
}; };
} }

View File

@ -29,6 +29,7 @@ export const INITIAL_STATE: GlobalState = {
users: { users: {
byId: {}, byId: {},
statusesById: {},
}, },
chats: { chats: {

View File

@ -3,6 +3,7 @@ import {
ApiMessage, ApiMessage,
ApiThreadInfo, ApiThreadInfo,
ApiUser, ApiUser,
ApiUserStatus,
ApiUpdateAuthorizationStateType, ApiUpdateAuthorizationStateType,
ApiUpdateConnectionStateType, ApiUpdateConnectionStateType,
ApiStickerSet, ApiStickerSet,
@ -127,6 +128,7 @@ export type GlobalState = {
users: { users: {
byId: Record<string, ApiUser>; byId: Record<string, ApiUser>;
statusesById: Record<string, ApiUserStatus>;
// TODO Remove // TODO Remove
selectedId?: string; selectedId?: string;
}; };

View File

@ -11,7 +11,7 @@ import useLang from './useLang';
export default ({ export default ({
chat, chat,
privateChatUser, user,
handleDelete, handleDelete,
handleChatFolderChange, handleChatFolderChange,
folderId, folderId,
@ -19,7 +19,7 @@ export default ({
isMuted, isMuted,
}: { }: {
chat: ApiChat | undefined; chat: ApiChat | undefined;
privateChatUser: ApiUser | undefined; user: ApiUser | undefined;
handleDelete: () => void; handleDelete: () => void;
handleChatFolderChange: () => void; handleChatFolderChange: () => void;
folderId?: number; folderId?: number;
@ -28,7 +28,7 @@ export default ({
}, isInSearch = false) => { }, isInSearch = false) => {
const lang = useLang(); const lang = useLang();
const { isSelf } = privateChatUser || {}; const { isSelf } = user || {};
return useMemo(() => { return useMemo(() => {
if (!chat) { if (!chat) {

View File

@ -22,6 +22,7 @@ import { callApi } from '../../../api/gramjs';
import { import {
addChats, addChats,
addUsers, addUsers,
addUserStatuses,
replaceThreadParam, replaceThreadParam,
updateChatListIds, updateChatListIds,
updateChats, updateChats,
@ -1007,6 +1008,8 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: string, off
global = getGlobal(); global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addUserStatuses(global, result.userStatusesById);
global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = updateChatListIds(global, listType, chatIds); global = updateChatListIds(global, listType, chatIds);
global = updateChatListSecondaryInfo(global, listType, result); global = updateChatListSecondaryInfo(global, listType, result);

View File

@ -17,7 +17,9 @@ import {
replaceChatListIds, replaceChatListIds,
replaceChats, replaceChats,
replaceUsers, replaceUsers,
replaceUserStatuses,
updateUsers, updateUsers,
addUserStatuses,
updateChats, updateChats,
updateChatListSecondaryInfo, updateChatListSecondaryInfo,
updateThreadInfos, updateThreadInfos,
@ -146,16 +148,10 @@ async function loadAndReplaceChats() {
savedUsers.push(...result.users); savedUsers.push(...result.users);
savedChats.push(...result.chats); savedChats.push(...result.chats);
global = replaceUserStatuses(global, result.userStatusesById);
global = replaceChats(global, buildCollectionByKey(savedChats, 'id')); global = replaceChats(global, buildCollectionByKey(savedChats, 'id'));
global = replaceChatListIds(global, 'active', result.chatIds); global = replaceChatListIds(global, 'active', result.chatIds);
global = {
...global,
chats: {
...global.chats,
},
};
global = updateChatListSecondaryInfo(global, 'active', result); global = updateChatListSecondaryInfo(global, 'active', result);
Object.keys(result.draftsById).forEach((chatId) => { Object.keys(result.draftsById).forEach((chatId) => {
@ -190,10 +186,14 @@ async function loadAndReplaceArchivedChats() {
} }
let global = getGlobal(); let global = getGlobal();
global = updateUsers(global, buildCollectionByKey(result.users, 'id')); global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
global = addUserStatuses(global, result.userStatusesById);
global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = replaceChatListIds(global, 'archived', result.chatIds); global = replaceChatListIds(global, 'archived', result.chatIds);
global = updateChatListSecondaryInfo(global, 'archived', result); global = updateChatListSecondaryInfo(global, 'archived', result);
setGlobal(global); setGlobal(global);
} }
@ -337,13 +337,16 @@ async function loadAndUpdateUsers() {
...(contactIds || []), ...(contactIds || []),
].map((id) => selectUser(global, id)).filter<ApiUser>(Boolean as any); ].map((id) => selectUser(global, id)).filter<ApiUser>(Boolean as any);
const updatedUsers = await callApi('fetchUsers', { users }); const result = await callApi('fetchUsers', { users });
if (!updatedUsers) { if (!result) {
return; return;
} }
const { users: updatedUsers, userStatusesById } = result;
global = getGlobal(); global = getGlobal();
global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id')); global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id'));
global = addUserStatuses(global, userStatusesById);
setGlobal(global); setGlobal(global);
} }

View File

@ -11,7 +11,7 @@ import { isUserBot, isUserId } from '../../helpers';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors'; import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
import { import {
addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers, addChats, addUsers, replaceUserStatuses, updateChat, updateManagementProgress, updateUser, updateUsers,
updateUserSearch, updateUserSearchFetchingStatus, updateUserSearch, updateUserSearchFetchingStatus,
} from '../../reducers'; } from '../../reducers';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
@ -40,13 +40,21 @@ addReducer('loadUser', (global, actions, payload) => {
} }
(async () => { (async () => {
const updatedUsers = await callApi('fetchUsers', { users: [user] }); const result = await callApi('fetchUsers', { users: [user] });
if (!updatedUsers) { if (!result) {
return; return;
} }
const { users, userStatusesById } = result;
global = getGlobal(); global = getGlobal();
global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id'));
global = updateUsers(global, buildCollectionByKey(users, 'id'));
setGlobal(replaceUserStatuses(global, {
...global.users.statusesById,
...userStatusesById,
}));
setGlobal(global); setGlobal(global);
})(); })();
}); });

View File

@ -2,30 +2,29 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import { ApiUpdate, ApiUserStatus } from '../../../api/types'; import { ApiUpdate, ApiUserStatus } from '../../../api/types';
import { deleteUser, updateUser } from '../../reducers'; import { deleteUser, replaceUserStatuses, updateUser } from '../../reducers';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
const STATUS_UPDATE_THROTTLE = 3000; const STATUS_UPDATE_THROTTLE = 3000;
const flushStatusUpdatesThrottled = throttle(flushStatusUpdates, STATUS_UPDATE_THROTTLE, true); const flushStatusUpdatesThrottled = throttle(flushStatusUpdates, STATUS_UPDATE_THROTTLE, true);
let pendingStatusUpdates: [string, ApiUserStatus][] = []; let pendingStatusUpdates: Record<string, ApiUserStatus> = {};
function scheduleStatusUpdate(userId: string, statusUpdate: ApiUserStatus) { function scheduleStatusUpdate(userId: string, statusUpdate: ApiUserStatus) {
pendingStatusUpdates.push([userId, statusUpdate]); pendingStatusUpdates[userId] = statusUpdate;
flushStatusUpdatesThrottled(); flushStatusUpdatesThrottled();
} }
function flushStatusUpdates() { function flushStatusUpdates() {
let global = getGlobal(); const global = getGlobal();
pendingStatusUpdates.forEach(([userId, statusUpdate]) => {
global = updateUser(global, userId, {
status: statusUpdate,
});
});
setGlobal(global);
pendingStatusUpdates = []; setGlobal(replaceUserStatuses(global, {
...global.users.statusesById,
...pendingStatusUpdates,
}));
pendingStatusUpdates = {};
} }
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {

View File

@ -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 { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { formatFullDate, formatTime } from '../../util/dateFormat'; import { formatFullDate, formatTime } from '../../util/dateFormat';
@ -65,7 +65,9 @@ export function getUserFullName(user?: ApiUser) {
return undefined; 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) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) {
return lang('ServiceNotifications').toLowerCase(); return lang('ServiceNotifications').toLowerCase();
} }
@ -74,11 +76,11 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num
return lang('Bot'); return lang('Bot');
} }
if (!user.status) { if (!userStatus) {
return ''; return '';
} }
switch (user.status.type) { switch (userStatus.type) {
case 'userStatusEmpty': { case 'userStatusEmpty': {
return lang('ALongTimeAgo'); return lang('ALongTimeAgo');
} }
@ -92,7 +94,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num
} }
case 'userStatusOffline': { case 'userStatusOffline': {
const { wasOnline } = user.status; const { wasOnline } = userStatus;
if (!wasOnline) return lang('LastSeen.Offline'); if (!wasOnline) return lang('LastSeen.Offline');
@ -156,10 +158,10 @@ export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: num
} }
} }
export function isUserOnline(user: ApiUser) { export function isUserOnline(user: ApiUser, userStatus?: ApiUserStatus) {
const { id, status, type } = user; const { id, type } = user;
if (!status) { if (!userStatus) {
return false; return false;
} }
@ -167,11 +169,11 @@ export function isUserOnline(user: ApiUser) {
return false; return false;
} }
return status.type === 'userStatusOnline' && type !== 'userTypeBot'; return userStatus.type === 'userStatusOnline' && type !== 'userTypeBot';
} }
export function isDeletedUser(user: ApiUser) { 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; return false;
} }
@ -190,6 +192,7 @@ export function getCanAddContact(user: ApiUser) {
export function sortUserIds( export function sortUserIds(
userIds: string[], userIds: string[],
usersById: Record<string, ApiUser>, usersById: Record<string, ApiUser>,
userStatusesById: Record<string, ApiUserStatus>,
priorityIds?: string[], priorityIds?: string[],
serverTimeOffset = 0, serverTimeOffset = 0,
) { ) {
@ -204,17 +207,18 @@ export function sortUserIds(
} }
const user = usersById[id]; const user = usersById[id];
if (!user || !user.status) { const userStatus = userStatusesById[id];
if (!user || !userStatus) {
return 0; return 0;
} }
if (user.status.type === 'userStatusOnline') { if (userStatus.type === 'userStatusOnline') {
return user.status.expires; return userStatus.expires;
} else if (user.status.type === 'userStatusOffline' && user.status.wasOnline) { } else if (userStatus.type === 'userStatusOffline' && userStatus.wasOnline) {
return user.status.wasOnline; return userStatus.wasOnline;
} }
switch (user.status.type) { switch (userStatus.type) {
case 'userStatusRecently': case 'userStatusRecently':
return now - 60 * 60 * 24; return now - 60 * 60 * 24;
case 'userStatusLastWeek': case 'userStatusLastWeek':

View File

@ -2,7 +2,7 @@ import { GlobalState } from '../../global/types';
import { ApiChat, ApiPhoto } from '../../api/types'; import { ApiChat, ApiPhoto } from '../../api/types';
import { ARCHIVED_FOLDER_ID } from '../../config'; import { ARCHIVED_FOLDER_ID } from '../../config';
import { omit } from '../../util/iteratees'; import { mapValues, omit } from '../../util/iteratees';
import { selectChatListType } from '../selectors'; import { selectChatListType } from '../selectors';
export function replaceChatListIds( export function replaceChatListIds(
@ -48,26 +48,6 @@ export function replaceChats(global: GlobalState, newById: Record<string, ApiCha
}; };
} }
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedChat(
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, 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( export function updateChat(
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto, global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
): GlobalState { ): GlobalState {
@ -81,52 +61,67 @@ export function updateChat(
}); });
} }
export function updateChats(global: GlobalState, updatedById: Record<string, ApiChat>): GlobalState { export function updateChats(global: GlobalState, newById: Record<string, ApiChat>): GlobalState {
const updatedChats = Object.keys(updatedById).reduce<Record<string, ApiChat>>((acc, id) => { const updatedById = mapValues(newById, (chat, id) => {
const updatedChat = getUpdatedChat(global, id, updatedById[id]); return getUpdatedChat(global, id, chat);
if (updatedChat) { });
acc[id] = updatedChat;
}
return acc;
}, {});
global = replaceChats(global, { global = replaceChats(global, {
...global.chats.byId, ...global.chats.byId,
...updatedChats, ...updatedById,
}); });
return global; return global;
} }
// @optimization Allows to avoid redundant updates which cause a lot of renders // @optimization Allows to avoid redundant updates which cause a lot of renders
export function addChats(global: GlobalState, addedById: Record<string, ApiChat>): GlobalState { export function addChats(global: GlobalState, newById: Record<string, ApiChat>): GlobalState {
const { byId } = global.chats; const { byId } = global.chats;
let isAdded = false; let isAdded = false;
const addedChats = Object.keys(addedById).reduce<Record<string, ApiChat>>((acc, id) => { const addedById = Object.keys(newById).reduce<Record<string, ApiChat>>((acc, id) => {
if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { if (!byId[id] || (byId[id].isMin && !newById[id].isMin)) {
const updatedChat = getUpdatedChat(global, id, addedById[id]); acc[id] = getUpdatedChat(global, id, newById[id]);
if (updatedChat) {
acc[id] = updatedChat;
if (!isAdded) { if (!isAdded) {
isAdded = true; isAdded = true;
}
} }
} }
return acc; return acc;
}, {}); }, {});
if (isAdded) { if (!isAdded) {
global = replaceChats(global, { return global;
...global.chats.byId,
...addedChats,
});
} }
global = replaceChats(global, {
...byId,
...addedById,
});
return global; return global;
} }
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedChat(
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, 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( export function updateChatListType(
global: GlobalState, global: GlobalState,
chatId: string, chatId: string,

View File

@ -1,7 +1,7 @@
import { GlobalState } from '../../global/types'; 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'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
export function replaceUsers(global: GlobalState, newById: Record<string, ApiUser>): GlobalState { export function replaceUsers(global: GlobalState, newById: Record<string, ApiUser>): GlobalState {
@ -14,24 +14,6 @@ export function replaceUsers(global: GlobalState, newById: Record<string, ApiUse
}; };
} }
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedUser(global: GlobalState, userId: string, userUpdate: Partial<ApiUser>): 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 { function updateContactList(global: GlobalState, updatedUsers: ApiUser[]): GlobalState {
const { userIds: contactUserIds } = global.contactList || {}; 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<string, ApiUser>): GlobalState { export function updateUsers(global: GlobalState, newById: Record<string, ApiUser>): GlobalState {
const updatedUsers = Object.keys(updatedById).reduce<Record<string, ApiUser>>((acc, id) => { const updatedById = mapValues(newById, (user, id) => {
const updatedUser = getUpdatedUser(global, id, updatedById[id]); return getUpdatedUser(global, id, user);
if (updatedUser) { });
acc[id] = updatedUser;
}
return acc;
}, {});
global = updateContactList(global, Object.values(updatedUsers));
global = replaceUsers(global, { global = replaceUsers(global, {
...global.users.byId, ...global.users.byId,
...updatedUsers, ...updatedById,
}); });
global = updateContactList(global, Object.values(updatedById));
return global; return global;
} }
// @optimization Allows to avoid redundant updates which cause a lot of renders // @optimization Allows to avoid redundant updates which cause a lot of renders
export function addUsers(global: GlobalState, addedById: Record<string, ApiUser>): GlobalState { export function addUsers(global: GlobalState, newById: Record<string, ApiUser>): GlobalState {
const { byId } = global.users; const { byId } = global.users;
let isAdded = false; let isAdded = false;
const addedUsers = Object.keys(addedById).reduce<Record<string, ApiUser>>((acc, id) => { const addedById = Object.keys(newById).reduce<Record<string, ApiUser>>((acc, id) => {
if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { if (!byId[id] || (byId[id].isMin && !newById[id].isMin)) {
const updatedUser = getUpdatedUser(global, id, addedById[id]); acc[id] = getUpdatedUser(global, id, newById[id]);
if (updatedUser) {
acc[id] = updatedUser;
if (!isAdded) { if (!isAdded) {
isAdded = true; isAdded = true;
}
} }
} }
return acc; return acc;
}, {}); }, {});
if (isAdded) { if (!isAdded) {
global = replaceUsers(global, { return global;
...global.users.byId,
...addedUsers,
});
global = updateContactList(global, Object.values(addedUsers));
} }
global = replaceUsers(global, {
...byId,
...addedById,
});
global = updateContactList(global, Object.values(addedById));
return global; return global;
} }
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedUser(global: GlobalState, userId: string, userUpdate: Partial<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;
}
export function updateSelectedUserId(global: GlobalState, selectedId?: string): GlobalState { export function updateSelectedUserId(global: GlobalState, selectedId?: string): GlobalState {
if (global.users.selectedId === selectedId) { if (global.users.selectedId === selectedId) {
return global; return global;
@ -182,3 +177,30 @@ export function updateUserBlockedState(global: GlobalState, userId: string, isBl
}, },
}); });
} }
export function replaceUserStatuses(global: GlobalState, newById: Record<string, ApiUserStatus>): 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<string, ApiUserStatus>): 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;
}

View File

@ -46,7 +46,11 @@ export function selectChatOnlineCount(global: GlobalState, chat: ApiChat) {
} }
return chat.fullInfo.members.reduce((onlineCount, { userId }) => { 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; return onlineCount + 1;
} }

View File

@ -1,10 +1,14 @@
import { GlobalState } from '../../global/types'; 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 { export function selectUser(global: GlobalState, userId: string): ApiUser | undefined {
return global.users.byId[userId]; 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) { export function selectIsUserBlocked(global: GlobalState, userId: string) {
const user = selectUser(global, userId); const user = selectUser(global, userId);