[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 }),
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<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,
} 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<ApiUser>(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,

View File

@ -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<ApiUser>(Boolean as any);
return buildApiUsersAndStatuses(result);
}
export function updateContact({

View File

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

View File

@ -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<OwnProps> = ({
size = 'large',
chat,
user,
userStatus,
text,
withOnlineStatus,
isSavedMessages,
lastSyncTime,
onClick,
@ -86,7 +88,7 @@ const Avatar: FC<OwnProps> = ({
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<OwnProps> = ({
isSavedMessages && 'saved-messages',
isDeleted && 'deleted-account',
isReplies && 'replies-bot-account',
withOnlineStatus && isOnline && 'online',
isOnline && 'online',
onClick && 'interactive',
(!isSavedMessages && !blobUrl) && 'no-photo',
);

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
noStatusOrTyping,
noRtl,
user,
userStatus,
isSavedMessages,
areMessagesLoaded,
lastSyncTime,
@ -106,9 +108,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
}
return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}>
<div className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}>
{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>
);
}
@ -143,11 +145,12 @@ export default memo(withGlobal<OwnProps>(
(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']),

View File

@ -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<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
forceShowSelf,
user,
userStatus,
chat,
isSavedMessages,
connectionState,
@ -162,8 +164,8 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
function renderStatus() {
if (user) {
return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}>
<span className="user-status" dir="auto">{getUserStatus(lang, user, serverTimeOffset)}</span>
<div className={`status ${isUserOnline(user, userStatus) ? 'online' : ''}`}>
<span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
</div>
);
}
@ -227,6 +229,7 @@ export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
return {
connectionState,
user,
userStatus,
chat,
isSavedMessages,
animationLevel,

View File

@ -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<string, ApiUser>;
actionTargetMessage?: ApiMessage;
@ -94,7 +95,8 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
isPinned,
chat,
isMuted,
privateChatUser,
user,
userStatus,
actionTargetUserIds,
usersById,
lastMessageSender,
@ -196,7 +198,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
const contextActions = useChatContextActions({
chat,
privateChatUser,
user,
handleDelete,
handleChatFolderChange,
folderId,
@ -281,9 +283,9 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="status">
<Avatar
chat={chat}
user={privateChatUser}
withOnlineStatus
isSavedMessages={privateChatUser?.isSelf}
user={user}
userStatus={userStatus}
isSavedMessages={user?.isSelf}
lastSyncTime={lastSyncTime}
/>
{chat.isCallActive && chat.isCallNotEmpty && (
@ -292,7 +294,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
</div>
<div className="info">
<div className="title">
<h3>{renderText(getChatTitle(lang, chat, privateChatUser))}</h3>
<h3>{renderText(getChatTitle(lang, chat, user))}</h3>
{chat.isVerified && <VerifiedIcon />}
{isMuted && <i className="icon-muted" />}
{chat.lastMessage && (
@ -377,7 +379,10 @@ export default memo(withGlobal<OwnProps>(
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 }),
};
},

View File

@ -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<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
contactIds?: string[];
serverTimeOffset: number;
};
@ -36,8 +37,15 @@ type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
const runThrottled = throttle((cb) => cb(), 60000, true);
const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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,
};

View File

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

View File

@ -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<OwnProps & StateProps> = ({
chatId,
chat,
privateChatUser,
user,
isPinned,
isMuted,
withUsername,
@ -46,7 +46,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
const contextActions = useChatContextActions({
chat,
privateChatUser,
user,
isPinned,
isMuted,
handleDelete: openDeleteModal,
@ -93,7 +93,7 @@ export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
return {
chat,
privateChatUser,
user,
isPinned,
isMuted,
};

View File

@ -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<string, ApiUser>;
recentEmojis: string[];
lastSyncTime?: number;
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
shouldSuggestStickers?: boolean;
baseEmojiKeywords?: Record<string, string[]>;
emojiKeywords?: Record<string, string[]>;
serverTimeOffset: number;
topInlineBotIds?: string[];
isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>;
botCommands?: ApiBotCommand[] | false;
chatBotCommands?: ApiBotCommand[];
} & Pick<GlobalState, 'connectionState'>;
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<string, ApiUser>;
recentEmojis: string[];
lastSyncTime?: number;
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
shouldSuggestStickers?: boolean;
baseEmojiKeywords?: Record<string, string[]>;
emojiKeywords?: Record<string, string[]>;
serverTimeOffset: number;
topInlineBotIds?: string[];
isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>;
botCommands?: ApiBotCommand[] | false;
chatBotCommands?: ApiBotCommand[];
}
& Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, (
'sendMessage' | 'editMessage' | 'saveDraft' | 'forwardMessages' |
@ -1071,9 +1074,9 @@ export default memo(withGlobal<OwnProps>(
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),

View File

@ -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<string, ApiChat>;
usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
isRightColumnShown: boolean;
isRestricted?: boolean;
lastSyncTime?: number;
@ -129,6 +131,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
commonChatIds,
members,
usersById,
userStatusesById,
chatsById,
isRightColumnShown,
isRestricted,
@ -168,7 +171,8 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
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<OwnProps>(
serverTimeOffset: global.serverTimeOffset,
activeDownloadIds,
usersById,
userStatusesById,
chatsById,
...(hasMembersTab && members && { members }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),

View File

@ -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<string, ApiUser>,
userStatusesById?: Record<string, ApiUserStatus>,
chatsById?: Record<string, ApiChat>,
chatMessages?: Record<number, ApiMessage>,
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) {

View File

@ -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<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[];
isChannel?: boolean;
serverTimeOffset: number;
@ -32,6 +33,7 @@ type DispatchProps = Pick<GlobalActions, 'openUserInfo'>;
const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
members,
usersById,
userStatusesById,
isChannel,
openUserInfo,
onClose,
@ -43,8 +45,14 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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,
};

View File

@ -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<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[];
isChannel?: boolean;
serverTimeOffset: number;
@ -31,6 +32,7 @@ type StateProps = {
const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
usersById,
userStatusesById,
members,
isChannel,
onScreenSelect,
@ -48,9 +50,12 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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,
};

View File

@ -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,
};
}

View File

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

View File

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

View File

@ -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) {

View File

@ -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);

View File

@ -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<ApiUser>(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);
}

View File

@ -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);
})();
});

View File

@ -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<string, ApiUserStatus> = {};
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) => {

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 { 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<string, ApiUser>,
userStatusesById: Record<string, ApiUserStatus>,
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':

View File

@ -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<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(
global: GlobalState, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
): GlobalState {
@ -81,52 +61,67 @@ export function updateChat(
});
}
export function updateChats(global: GlobalState, updatedById: Record<string, ApiChat>): GlobalState {
const updatedChats = Object.keys(updatedById).reduce<Record<string, ApiChat>>((acc, id) => {
const updatedChat = getUpdatedChat(global, id, updatedById[id]);
if (updatedChat) {
acc[id] = updatedChat;
}
return acc;
}, {});
export function updateChats(global: GlobalState, newById: Record<string, ApiChat>): 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<string, ApiChat>): GlobalState {
export function addChats(global: GlobalState, newById: Record<string, ApiChat>): GlobalState {
const { byId } = global.chats;
let isAdded = false;
const addedChats = Object.keys(addedById).reduce<Record<string, ApiChat>>((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<Record<string, ApiChat>>((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<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(
global: GlobalState,
chatId: string,

View File

@ -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<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 {
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 {
const updatedUsers = Object.keys(updatedById).reduce<Record<string, ApiUser>>((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<string, ApiUser>): 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<string, ApiUser>): GlobalState {
export function addUsers(global: GlobalState, newById: Record<string, ApiUser>): GlobalState {
const { byId } = global.users;
let isAdded = false;
const addedUsers = Object.keys(addedById).reduce<Record<string, ApiUser>>((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<Record<string, ApiUser>>((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<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 {
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<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 }) => {
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;
}

View File

@ -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);