[Refactoring] User: Extract nested data to a separate path (#4953)

This commit is contained in:
zubiden 2024-09-19 20:43:20 +02:00 committed by Alexander Zinchuk
parent 8b76de86ba
commit 25b4aae4cd
25 changed files with 223 additions and 141 deletions

View File

@ -5,7 +5,6 @@ import type {
ApiChat, ApiPeer, ApiSticker, ApiUser,
} from '../../types';
import { COMMON_CHATS_LIMIT } from '../../../config';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common';
import { buildApiPeerId } from '../apiBuilders/peers';
@ -87,21 +86,21 @@ export async function fetchFullUser({
};
}
export async function fetchCommonChats(id: string, accessHash?: string, maxId?: string) {
const commonChats = await invokeRequest(new GramJs.messages.GetCommonChats({
userId: buildInputEntity(id, accessHash) as GramJs.InputUser,
export async function fetchCommonChats(user: ApiUser, maxId?: string) {
const result = await invokeRequest(new GramJs.messages.GetCommonChats({
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
maxId: maxId ? buildMtpPeerId(maxId, getEntityTypeById(maxId)) : undefined,
limit: COMMON_CHATS_LIMIT,
}));
if (!commonChats) {
if (!result) {
return undefined;
}
const chats = commonChats.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const chatIds = chats.map(({ id: chatId }) => chatId);
const count = 'count' in result ? result.count : chatIds.length;
return { chatIds, isFullyLoaded: chatIds.length < COMMON_CHATS_LIMIT };
return { chatIds, count };
}
export async function fetchNearestCountry() {

View File

@ -3,7 +3,7 @@ import type { ApiBotCommand } from './bots';
import type {
ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet,
} from './messages';
import type { ApiChatInviteImporter, ApiPeerPhotos } from './misc';
import type { ApiChatInviteImporter } from './misc';
import type {
ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername,
} from './users';
@ -41,7 +41,6 @@ export interface ApiChat {
membersCount?: number;
creationDate?: number;
isSupport?: true;
profilePhotos?: ApiPeerPhotos;
draftDate?: number;
isProtected?: boolean;
fakeType?: ApiFakeType;

View File

@ -3,7 +3,6 @@ import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiPeerColor } from './chats';
import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiPeerPhotos } from './misc';
export interface ApiUser {
id: string;
@ -23,14 +22,8 @@ export interface ApiUser {
accessHash?: string;
hasVideoAvatar?: boolean;
avatarPhotoId?: string;
profilePhotos?: ApiPeerPhotos;
botPlaceholder?: string;
canBeInvitedToGroup?: boolean;
commonChats?: {
ids: string[];
maxId: string;
isFullyLoaded: boolean;
};
fakeType?: ApiFakeType;
isAttachBot?: boolean;
emojiStatus?: ApiEmojiStatus;
@ -82,6 +75,12 @@ export interface ApiUserStatus {
isReadDateRestricted?: boolean;
}
export interface ApiUserCommonChats {
ids: string[];
maxId?: string;
isFullyLoaded: boolean;
}
export interface ApiUsername {
username: string;
isActive?: boolean;

View File

@ -3,7 +3,7 @@ import React, { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type {
ApiChat, ApiSticker, ApiTopic, ApiUser, ApiUserStatus,
ApiChat, ApiPeerPhotos, ApiSticker, ApiTopic, ApiUser, ApiUserStatus,
} from '../../api/types';
import { MediaViewerOrigin } from '../../types';
@ -13,6 +13,7 @@ import {
import {
selectChat,
selectCurrentMessageList,
selectPeerPhotos,
selectTabState,
selectThreadMessagesCount,
selectUser,
@ -54,6 +55,7 @@ type StateProps =
topic?: ApiTopic;
messagesCount?: number;
emojiStatusSticker?: ApiSticker;
profilePhotos?: ApiPeerPhotos;
};
const EMOJI_STATUS_SIZE = 24;
@ -72,6 +74,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
topic,
messagesCount,
emojiStatusSticker,
profilePhotos,
peerId,
}) => {
const {
@ -84,9 +87,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const lang = useOldLang();
const userProfilePhotos = user?.profilePhotos;
const chatProfilePhotos = chat?.profilePhotos;
const photos = userProfilePhotos?.photos || chatProfilePhotos?.photos || MEMO_EMPTY_ARRAY;
const photos = profilePhotos?.photos || MEMO_EMPTY_ARRAY;
const prevMediaIndex = usePreviousDeprecated(mediaIndex);
const prevAvatarOwnerId = usePreviousDeprecated(avatarOwnerId);
const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
@ -213,7 +214,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}
function renderPhotoTabs() {
const totalPhotosLength = Math.max(photos.length, userProfilePhotos?.count || 0, chatProfilePhotos?.count || 0);
const totalPhotosLength = Math.max(photos.length, profilePhotos?.count || 0);
if (!photos || totalPhotosLength <= 1) {
return undefined;
}
@ -293,18 +294,18 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
>
<div className={styles.photoWrapper}>
{renderPhotoTabs()}
{!forceShowSelf && userProfilePhotos?.personalPhoto && (
{!forceShowSelf && profilePhotos?.personalPhoto && (
<div className={buildClassName(
styles.fallbackPhoto,
isFirst && styles.fallbackPhotoVisible,
)}
>
<div className={styles.fallbackPhotoContents}>
{lang(userProfilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
{lang(profilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
</div>
</div>
)}
{forceShowSelf && userProfilePhotos?.fallbackPhoto && (
{forceShowSelf && profilePhotos?.fallbackPhoto && (
<div className={buildClassName(
styles.fallbackPhoto,
(isFirst || isLast) && styles.fallbackPhotoVisible,
@ -313,12 +314,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
{!isLast && (
<Avatar
photo={userProfilePhotos.fallbackPhoto}
photo={profilePhotos.fallbackPhoto}
className={styles.fallbackPhotoAvatar}
size="mini"
/>
)}
{lang(userProfilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
{lang(profilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
</div>
</div>
)}
@ -368,6 +369,7 @@ export default memo(withGlobal<OwnProps>(
const user = selectUser(global, peerId);
const userStatus = selectUserStatus(global, peerId);
const chat = selectChat(global, peerId);
const profilePhotos = selectPeerPhotos(global, peerId);
const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
@ -383,6 +385,7 @@ export default memo(withGlobal<OwnProps>(
mediaIndex,
avatarOwnerId,
emojiStatusSticker,
profilePhotos,
...(topic && {
topic,
messagesCount: selectThreadMessagesCount(global, peerId, currentTopicId!),

View File

@ -6,7 +6,11 @@ import { getActions, withGlobal } from '../../global';
import type {
ApiChat,
ApiMessage, ApiPeer, ApiPhoto, ApiSponsoredMessage,
ApiMessage,
ApiPeer,
ApiPeerPhotos,
ApiPhoto,
ApiSponsoredMessage,
} from '../../api/types';
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
@ -25,6 +29,7 @@ import {
selectListedIds,
selectOutlyingListByMessageId,
selectPeer,
selectPeerPhotos,
selectPerformanceSettingsValue,
selectScheduledMessage, selectSponsoredMessage,
selectTabState,
@ -69,6 +74,7 @@ type StateProps = {
origin?: MediaViewerOrigin;
avatar?: ApiPhoto;
avatarOwner?: ApiPeer;
profilePhotos?: ApiPeerPhotos;
chatMessages?: Record<number, ApiMessage>;
sponsoredMessage?: ApiSponsoredMessage;
standaloneMedia?: MediaViewerMedia[];
@ -95,6 +101,7 @@ const MediaViewer = ({
origin,
avatar,
avatarOwner,
profilePhotos,
chatMessages,
sponsoredMessage,
standaloneMedia,
@ -132,7 +139,7 @@ const MediaViewer = ({
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
const currentItem = getMediaViewerItem({
message, avatarOwner, standaloneMedia, mediaIndex, sponsoredMessage,
message, avatarOwner, standaloneMedia, profilePhotos, mediaIndex, sponsoredMessage,
});
const { media, isSingle } = getViewableMedia(currentItem) || {};
@ -275,7 +282,7 @@ const MediaViewer = ({
if (!item || isLoadingMoreMedia) return;
if (item.type === 'avatar') {
const isNearEnd = item.mediaIndex >= item.avatarOwner.profilePhotos!.photos.length - AVATAR_LOAD_TRIGGER;
const isNearEnd = item.mediaIndex >= item.profilePhotos.photos.length - AVATAR_LOAD_TRIGGER;
if (!isNearEnd) return;
loadMoreProfilePhotos({ peerId: item.avatarOwner.id });
}
@ -299,10 +306,15 @@ const MediaViewer = ({
}
if (from.type === 'avatar') {
const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from;
const { avatarOwner: fromAvatarOwner, profilePhotos: fromProfilePhotos, mediaIndex: fromMediaIndex } = from;
const nextIndex = fromMediaIndex + direction;
if (nextIndex >= 0 && fromAvatarOwner.profilePhotos && nextIndex < fromAvatarOwner.profilePhotos.photos.length) {
return { type: 'avatar', avatarOwner: fromAvatarOwner, mediaIndex: nextIndex };
if (nextIndex >= 0 && fromProfilePhotos && nextIndex < fromProfilePhotos.photos.length) {
return {
type: 'avatar',
avatarOwner: fromAvatarOwner,
profilePhotos: fromProfilePhotos,
mediaIndex: nextIndex,
};
}
return undefined;
@ -367,7 +379,7 @@ const MediaViewer = ({
});
const handleBeforeDelete = useLastCallback(() => {
const mediaCount = avatarOwner?.profilePhotos?.photos.length
const mediaCount = profilePhotos?.photos.length
|| standaloneMedia?.length || messageMediaIds?.length || 0;
if (mediaCount <= 1 || !currentItem) {
handleClose();
@ -489,9 +501,10 @@ export default memo(withGlobal(
canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat);
}
const profilePhotos = peer?.profilePhotos;
const profilePhotos = selectPeerPhotos(global, chatId!);
return {
profilePhotos,
avatar: profilePhotos?.photos[mediaIndex!],
avatarOwner: peer,
isLoadingMoreMedia: profilePhotos?.isLoading,

View File

@ -128,8 +128,8 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
const handleUpdate = useLastCallback(() => {
if (item?.type !== 'avatar') return;
const { avatarOwner, mediaIndex } = item;
const avatarPhoto = avatarOwner.profilePhotos?.photos[mediaIndex]!;
const { avatarOwner, profilePhotos, mediaIndex } = item;
const avatarPhoto = profilePhotos?.photos[mediaIndex]!;
if (isUserId(avatarOwner.id)) {
updateProfilePhoto({ photo: avatarPhoto });
} else {
@ -170,7 +170,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal}
onConfirm={onBeforeDelete}
profileId={item.avatarOwner.id}
photo={item.avatarOwner.profilePhotos!.photos[item.mediaIndex!]}
photo={item.profilePhotos.photos[item.mediaIndex!]}
/>
) : undefined;
}
@ -390,7 +390,7 @@ export default memo(withGlobal<OwnProps>(
const message = item?.type === 'message' ? item.message : undefined;
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined;
const avatarPhoto = avatarOwner?.profilePhotos?.photos[item!.mediaIndex!];
const avatarPhoto = item?.type === 'avatar' && item.profilePhotos.photos[item.mediaIndex];
const currentMessageList = selectCurrentMessageList(global);
const { threadId } = selectCurrentMessageList(global) || {};

View File

@ -68,14 +68,15 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
if (!item || item.type === 'standalone') return undefined;
const avatarOwner = item.type === 'avatar' ? item.avatarOwner : undefined;
const avatar = avatarOwner?.profilePhotos?.photos[item.mediaIndex!];
const isFallbackAvatar = avatar?.id === avatarOwner?.profilePhotos?.fallbackPhoto?.id;
const profilePhotos = item.type === 'avatar' ? item.profilePhotos : undefined;
const avatar = profilePhotos?.photos[item.mediaIndex!];
const isFallbackAvatar = avatar?.id === profilePhotos?.fallbackPhoto?.id;
const date = item.type === 'message' ? item.message.date : avatar?.date;
if (!date) return undefined;
const formattedDate = formatMediaDateTime(lang, date * 1000, true);
const count = avatarOwner?.profilePhotos?.count
&& (avatarOwner.profilePhotos.count + (avatarOwner?.profilePhotos?.fallbackPhoto ? 1 : 0));
const count = profilePhotos?.count
&& (profilePhotos.count + (profilePhotos?.fallbackPhoto ? 1 : 0));
const countText = count && lang('Of', [item.mediaIndex! + 1, count]);
const parts: string[] = [];

View File

@ -1,4 +1,6 @@
import type { ApiMessage, ApiPeer, ApiSponsoredMessage } from '../../../api/types';
import type {
ApiMessage, ApiPeer, ApiPeerPhotos, ApiSponsoredMessage,
} from '../../../api/types';
import type { MediaViewerMedia } from '../../../types';
import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers';
@ -10,6 +12,7 @@ export type MediaViewerItem = {
} | {
type: 'avatar';
avatarOwner: ApiPeer;
profilePhotos: ApiPeerPhotos;
mediaIndex: number;
} | {
type: 'standalone';
@ -27,18 +30,20 @@ type ViewableMedia = {
};
export function getMediaViewerItem({
message, avatarOwner, standaloneMedia, mediaIndex, sponsoredMessage,
message, avatarOwner, profilePhotos, standaloneMedia, mediaIndex, sponsoredMessage,
}: {
message?: ApiMessage;
avatarOwner?: ApiPeer;
profilePhotos?: ApiPeerPhotos;
standaloneMedia?: MediaViewerMedia[];
sponsoredMessage?: ApiSponsoredMessage;
mediaIndex?: number;
}): MediaViewerItem | undefined {
if (avatarOwner) {
if (avatarOwner && profilePhotos) {
return {
type: 'avatar',
avatarOwner,
profilePhotos,
mediaIndex: mediaIndex!,
};
}
@ -81,7 +86,7 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi
}
if (params.type === 'avatar') {
const avatar = params.avatarOwner.profilePhotos?.photos[params.mediaIndex];
const avatar = params.profilePhotos?.photos[params.mediaIndex];
if (avatar) {
return {
media: avatar,

View File

@ -37,7 +37,6 @@ import {
isChatChannel,
isChatGroup,
isUserBot,
isUserId,
isUserRightBanned,
} from '../../global/helpers';
import {
@ -48,12 +47,12 @@ import {
selectCurrentSharedMediaSearch,
selectIsCurrentUserPremium,
selectIsRightColumnShown,
selectPeerFullInfo,
selectPeerStories,
selectSimilarChannelIds,
selectTabState,
selectTheme,
selectUser,
selectUserCommonChats,
selectUserFullInfo,
} from '../../global/selectors';
import { selectPremiumLimit } from '../../global/selectors/limits';
@ -110,7 +109,6 @@ type StateProps = {
theme: ISettings['theme'];
isChannel?: boolean;
currentUserId?: string;
resolvedUserId?: string;
messagesById?: Record<number, ApiMessage>;
foundIds?: number[];
mediaSearchType?: SharedMediaType;
@ -166,10 +164,8 @@ const Profile: FC<OwnProps & StateProps> = ({
chatId,
threadId,
profileState,
onProfileStateChange,
theme,
isChannel,
resolvedUserId,
currentUserId,
messagesById,
foundIds,
@ -205,6 +201,7 @@ const Profile: FC<OwnProps & StateProps> = ({
isSavedDialog,
forceScrollProfileTab,
isSynced,
onProfileStateChange,
}) => {
const {
setSharedMediaSearchType,
@ -230,7 +227,7 @@ const Profile: FC<OwnProps & StateProps> = ({
const lang = useOldLang();
const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
const profileId = isSavedDialog ? String(threadId) : (resolvedUserId || chatId);
const profileId = isSavedDialog ? String(threadId) : chatId;
const isSavedMessages = profileId === currentUserId && !isSavedDialog;
const tabs = useMemo(() => ([
@ -303,6 +300,9 @@ const Profile: FC<OwnProps & StateProps> = ({
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
const handleLoadCommonChats = useCallback(() => {
loadCommonChats({ userId: chatId });
}, [chatId]);
const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => {
loadPeerProfileStories({ peerId: chatId, offsetId });
}, [chatId]);
@ -312,7 +312,7 @@ const Profile: FC<OwnProps & StateProps> = ({
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
loadMoreMembers,
loadCommonChats,
handleLoadCommonChats,
searchSharedMediaMessages,
handleLoadPeerStories,
handleLoadStoriesArchive,
@ -746,9 +746,12 @@ export default memo(withGlobal<OwnProps>(
(global, {
chatId, threadId, isMobile,
}): StateProps => {
const user = selectUser(global, chatId);
const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentSharedMediaSearch(global) || {};
const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {};
@ -774,19 +777,13 @@ export default memo(withGlobal<OwnProps>(
const { similarChannelIds } = selectSimilarChannelIds(global, chatId) || {};
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
let hasCommonChatsTab;
let resolvedUserId;
let user;
if (isUserId(chatId)) {
resolvedUserId = chatId;
user = selectUser(global, resolvedUserId);
hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog;
}
const peer = user || chat;
const peerFullInfo = selectPeerFullInfo(global, chatId);
const peerFullInfo = userFullInfo || chatFullInfo;
const hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog
&& Boolean(userFullInfo?.commonChatsCount);
const commonChats = selectUserCommonChats(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
const hasPreviewMediaTab = userFullInfo?.botInfo?.hasPreviewMedia;
const botPreviewMedia = global.users.previewMediaByBotId[chatId];
@ -801,7 +798,6 @@ export default memo(withGlobal<OwnProps>(
return {
theme: selectTheme(global),
isChannel,
resolvedUserId,
messagesById,
foundIds,
mediaSearchType,
@ -835,7 +831,7 @@ export default memo(withGlobal<OwnProps>(
isSynced: global.isSynced,
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
...(hasCommonChatsTab && user && { commonChatIds: commonChats?.ids }),
};
},
)(Profile));

View File

@ -86,7 +86,6 @@ export const PINNED_MESSAGES_LIMIT = 50;
export const BLOCKED_LIST_LIMIT = 100;
export const PROFILE_SENSITIVE_AREA = 500;
export const TOPIC_LIST_SENSITIVE_AREA = 600;
export const COMMON_CHATS_LIMIT = 100;
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
export const STORY_LIST_LIMIT = 100;
export const API_GENERAL_ID_LIMIT = 100;

View File

@ -23,6 +23,7 @@ import {
updatePeerPhotos,
updatePeerPhotosIsLoading,
updateUser,
updateUserCommonChats,
updateUserFullInfo,
updateUsers,
updateUserSearch,
@ -31,10 +32,11 @@ import {
import {
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectPeer,
selectPeerPhotos,
selectTabState,
selectUser,
selectUserCommonChats,
selectUserFullInfo,
} from '../../selectors';
@ -56,6 +58,7 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise<void>
global = getGlobal();
const fullInfo = selectUserFullInfo(global, userId);
const { user: newUser, fullInfo: newFullInfo } = result;
const profilePhotos = selectPeerPhotos(global, userId);
const hasChangedAvatar = user.avatarPhotoId !== newUser.avatarPhotoId;
const hasChangedProfilePhoto = fullInfo?.profilePhoto?.id !== newFullInfo?.profilePhoto?.id;
const hasChangedFallbackPhoto = fullInfo?.fallbackPhoto?.id !== newFullInfo?.fallbackPhoto?.id;
@ -71,7 +74,7 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise<void>
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
setGlobal(global);
if (withPhotos || (user.profilePhotos?.count && hasChangedPhoto)) {
if (withPhotos || (profilePhotos?.count && hasChangedPhoto)) {
actions.loadMoreProfilePhotos({ peerId: userId, shouldInvalidateCache: true });
}
});
@ -156,28 +159,27 @@ addActionHandler('loadCurrentUser', (): ActionReturnType => {
});
addActionHandler('loadCommonChats', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId } = selectCurrentMessageList(global, tabId) || {};
const user = chatId ? selectUser(global, chatId) : undefined;
if (!user || isUserBot(user) || user.commonChats?.isFullyLoaded) {
const { userId } = payload;
const user = selectUser(global, userId);
const commonChats = selectUserCommonChats(global, userId);
if (!user || isUserBot(user) || commonChats?.isFullyLoaded) {
return;
}
const maxId = user.commonChats?.maxId;
const result = await callApi('fetchCommonChats', user.id, user.accessHash!, maxId);
const result = await callApi('fetchCommonChats', user, commonChats?.maxId);
if (!result) {
return;
}
const { chatIds, isFullyLoaded } = result;
const { chatIds, count } = result;
const ids = unique((commonChats?.ids || []).concat(chatIds));
global = getGlobal();
global = updateUser(global, user.id, {
commonChats: {
maxId: chatIds.length ? chatIds[chatIds.length - 1] : '0',
ids: unique((user.commonChats?.ids || []).concat(chatIds)),
isFullyLoaded,
},
global = updateUserCommonChats(global, user.id, {
maxId: chatIds.length ? chatIds[chatIds.length - 1] : undefined,
ids,
isFullyLoaded: ids.length >= count,
});
setGlobal(global);
@ -258,11 +260,12 @@ addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Prom
const user = isPrivate ? selectUser(global, peerId) : undefined;
const chat = !isPrivate ? selectChat(global, peerId) : undefined;
const peer = user || chat;
const profilePhotos = selectPeerPhotos(global, peerId);
if (!peer?.avatarPhotoId) {
return;
}
if (peer.profilePhotos && !shouldInvalidateCache && (isPreload || !peer.profilePhotos.nextOffset)) return;
if (profilePhotos && !shouldInvalidateCache && (isPreload || !profilePhotos.nextOffset)) return;
global = updatePeerPhotosIsLoading(global, peerId, true);
setGlobal(global);
@ -292,7 +295,7 @@ addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Prom
const peerFullInfo = userFullInfo || chatFullInfo;
if (!peerFullInfo) return;
const offset = peer.profilePhotos?.nextOffset;
const offset = profilePhotos?.nextOffset;
const limit = !offset || isPreload || shouldInvalidateCache ? PROFILE_PHOTOS_FIRST_LOAD_LIMIT : undefined;
const result = await callApi('fetchProfilePhotos', {

View File

@ -16,6 +16,7 @@ import {
deletePeerPhoto,
leaveChat,
removeUnreadMentions,
replacePeerPhotos,
replaceThreadParam,
updateChat,
updateChatFullInfo,
@ -548,8 +549,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
if (!photoId || peer.avatarPhotoId === photoId) {
global = updateChat(global, peerId, {
avatarPhotoId: undefined,
profilePhotos: undefined,
});
global = replacePeerPhotos(global, peerId, undefined);
} else {
global = deletePeerPhoto(global, peerId, photoId);
}

View File

@ -245,6 +245,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.topBotApps) {
cached.topBotApps = initialState.topBotApps;
}
if (!cached.users.commonChatsById) {
cached.users.commonChatsById = initialState.users.commonChatsById;
}
}
function updateCache(force?: boolean) {
@ -390,10 +393,10 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
]).slice(0, GLOBAL_STATE_CACHE_USER_LIST_LIMIT);
return {
...INITIAL_GLOBAL_STATE.users,
byId: pickTruthy(byId, idsToSave),
statusesById: pickTruthy(statusesById, idsToSave),
fullInfoById: pickTruthy(fullInfoById, idsToSave),
previewMediaByBotId: {},
};
}

View File

@ -98,6 +98,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
statusesById: {},
fullInfoById: {},
previewMediaByBotId: {},
commonChatsById: {},
},
chats: {
@ -298,6 +299,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
isHidden: false,
},
profilePhotosById: {},
monetizationInfo: {},
};

View File

@ -12,5 +12,5 @@ export * from './payments';
export * from './statistics';
export * from './stories';
export * from './translations';
export * from './peers';
export * from './password';
export * from './general';

View File

@ -1,11 +1,16 @@
import type {
ApiChat, ApiChatFullInfo, ApiPhoto, ApiUser, ApiUserFullInfo,
ApiChat, ApiChatFullInfo, ApiPeerPhotos, ApiPhoto, ApiUser, ApiUserFullInfo,
} from '../../api/types';
import type { GlobalState } from '../types';
import { uniqueByField } from '../../util/iteratees';
import { omit, uniqueByField } from '../../util/iteratees';
import { isChatChannel, isUserId } from '../helpers';
import { selectChatFullInfo, selectPeer, selectUserFullInfo } from '../selectors';
import {
selectChatFullInfo,
selectPeer,
selectPeerPhotos,
selectUserFullInfo,
} from '../selectors';
import { updateChat, updateChatFullInfo } from './chats';
import { updateUser, updateUserFullInfo } from './users';
@ -38,19 +43,38 @@ export function updatePeerPhotosIsLoading<T extends GlobalState>(
peerId: string,
isLoading: boolean,
) {
const peer = selectPeer(global, peerId);
if (!peer || !peer.profilePhotos) {
const profilePhotos = selectPeerPhotos(global, peerId);
if (!profilePhotos) {
return global;
}
return updatePeer(global, peerId, {
profilePhotos: {
...peer.profilePhotos,
isLoading,
},
return replacePeerPhotos(global, peerId, {
...profilePhotos,
isLoading,
});
}
export function replacePeerPhotos<T extends GlobalState>(
global: T,
peerId: string,
value?: ApiPeerPhotos,
) {
if (!value) {
return {
...global,
profilePhotosById: omit(global.profilePhotosById, [peerId]),
};
}
return {
...global,
profilePhotosById: {
...global.profilePhotosById,
[peerId]: value,
},
};
}
export function updatePeerPhotos<T extends GlobalState>(
global: T,
peerId: string,
@ -62,15 +86,12 @@ export function updatePeerPhotos<T extends GlobalState>(
shouldInvalidateCache?: boolean;
},
) {
const peer = selectPeer(global, peerId);
if (!peer) {
return global;
}
const profilePhotos = selectPeerPhotos(global, peerId);
const {
newPhotos, count, nextOffset, fullInfo, shouldInvalidateCache,
} = update;
const currentPhotos = peer.profilePhotos;
const currentPhotos = profilePhotos;
const profilePhoto = fullInfo.profilePhoto;
const fallbackPhoto = 'fallbackPhoto' in fullInfo ? fullInfo.fallbackPhoto : undefined;
const personalPhoto = 'personalPhoto' in fullInfo ? fullInfo.personalPhoto : undefined;
@ -89,15 +110,13 @@ export function updatePeerPhotos<T extends GlobalState>(
newPhotos.push(fallbackPhoto);
}
return updatePeer(global, peerId, {
profilePhotos: {
fallbackPhoto,
personalPhoto,
photos: newPhotos,
count,
nextOffset,
isLoading: false,
},
return replacePeerPhotos(global, peerId, {
fallbackPhoto,
personalPhoto,
photos: newPhotos,
count,
nextOffset,
isLoading: false,
});
}
@ -105,15 +124,13 @@ export function updatePeerPhotos<T extends GlobalState>(
const currentPhotoArray = hasFallbackPhoto ? currentPhotos.photos.slice(0, -1) : currentPhotos.photos;
const photos = uniqueByField([...currentPhotoArray, ...newPhotos, fallbackPhoto].filter(Boolean), 'id');
return updatePeer(global, peerId, {
profilePhotos: {
fallbackPhoto,
personalPhoto,
photos,
count,
nextOffset,
isLoading: false,
},
return replacePeerPhotos(global, peerId, {
fallbackPhoto,
personalPhoto,
photos,
count,
nextOffset,
isLoading: false,
});
}
@ -124,7 +141,8 @@ export function deletePeerPhoto<T extends GlobalState>(
isFromActionMessage?: boolean,
) {
const peer = selectPeer(global, peerId);
if (!peer || !peer.profilePhotos) {
const profilePhotos = selectPeerPhotos(global, peerId);
if (!peer || !profilePhotos) {
return global;
}
const isChannel = 'title' in peer && isChatChannel(peer);
@ -133,7 +151,7 @@ export function deletePeerPhoto<T extends GlobalState>(
const chatFullInfo = selectChatFullInfo(global, peerId);
const isAvatar = peer.avatarPhotoId === photoId && (!isChannel || isFromActionMessage);
const nextAvatarPhoto = isAvatar ? peer.profilePhotos.photos[1] : undefined;
const nextAvatarPhoto = isAvatar ? profilePhotos.photos[1] : undefined;
if (userFullInfo) {
const newFallbackPhoto = userFullInfo.fallbackPhoto?.id === photoId ? undefined : userFullInfo.fallbackPhoto;
@ -156,13 +174,17 @@ export function deletePeerPhoto<T extends GlobalState>(
const avatarPhotoId = isAvatar ? nextAvatarPhoto?.id : peer.avatarPhotoId;
const shouldKeepInPhotos = isAvatar && 'title' in peer && isChatChannel(peer);
const photos = shouldKeepInPhotos
? peer.profilePhotos.photos.filter((photo) => photo.id !== photoId) : peer.profilePhotos.photos.slice();
return updatePeer(global, peerId, {
? profilePhotos.photos.filter((photo) => photo.id !== photoId) : profilePhotos.photos.slice();
global = updatePeer(global, peerId, {
avatarPhotoId,
profilePhotos: avatarPhotoId ? {
...peer.profilePhotos,
photos,
count: peer.profilePhotos.count - 1,
} : undefined,
});
global = replacePeerPhotos(global, peerId, avatarPhotoId ? {
...profilePhotos,
photos,
count: profilePhotos.count - 1,
} : undefined);
return global;
}

View File

@ -19,7 +19,7 @@ import {
selectIsChatWithSelf,
selectPeer, selectPeerStories, selectPeerStory, selectTabState, selectUser,
} from '../selectors';
import { updatePeer } from './general';
import { updatePeer } from './peers';
import { updateTabState } from './tabs';
export function addStories<T extends GlobalState>(global: T, newStoriesByPeerId: Record<string, ApiPeerStories>): T {

View File

@ -1,5 +1,5 @@
import type {
ApiMissingInvitedUser, ApiUser, ApiUserFullInfo, ApiUserStatus,
ApiMissingInvitedUser, ApiUser, ApiUserCommonChats, ApiUserFullInfo, ApiUserStatus,
} from '../../api/types';
import type { GlobalState, TabArgs, TabState } from '../types';
@ -240,6 +240,21 @@ export function updateUserFullInfo<T extends GlobalState>(
};
}
export function updateUserCommonChats<T extends GlobalState>(
global: T, userId: string, commonChats: ApiUserCommonChats,
): T {
return {
...global,
users: {
...global.users,
commonChatsById: {
...global.users.commonChatsById,
[userId]: commonChats,
},
},
};
}
// @optimization Allows to avoid redundant updates which cause a lot of renders
export function addUserStatuses<T extends GlobalState>(global: T, newById: Record<string, ApiUserStatus>): T {
const { statusesById } = global.users;

View File

@ -1,5 +1,5 @@
import type {
ApiChat, ApiChatFullInfo, ApiChatType, ApiPeer,
ApiChat, ApiChatFullInfo, ApiChatType,
} from '../../api/types';
import type { ChatListType, GlobalState, TabArgs } from '../types';
import { MAIN_THREAD_ID } from '../../api/types';
@ -24,10 +24,6 @@ import {
selectBot, selectIsCurrentUserPremium, selectUser, selectUserFullInfo,
} from './users';
export function selectPeer<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined {
return selectUser(global, peerId) || selectChat(global, peerId);
}
export function selectChat<T extends GlobalState>(global: T, chatId: string): ApiChat | undefined {
return global.chats.byId[chatId];
}

View File

@ -11,3 +11,4 @@ export * from './settings';
export * from './statistics';
export * from './stories';
export * from './tabs';
export * from './peers';

View File

@ -64,9 +64,9 @@ import {
selectChatFullInfo,
selectChatLastMessageId,
selectIsChatWithSelf,
selectPeer,
selectRequestedChatTranslationLanguage,
} from './chats';
import { selectPeer } from './peers';
import { selectPeerStory } from './stories';
import { selectIsStickerFavorite } from './symbols';
import { selectTabState } from './tabs';

View File

@ -0,0 +1,13 @@
import type { ApiPeer } from '../../api/types';
import type { GlobalState } from '../types';
import { selectChat } from './chats';
import { selectUser } from './users';
export function selectPeer<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined {
return selectUser(global, peerId) || selectChat(global, peerId);
}
export function selectPeerPhotos<T extends GlobalState>(global: T, peerId: string) {
return global.profilePhotosById[peerId];
}

View File

@ -2,7 +2,7 @@ import type { ApiPeerStories, ApiTypeStory } from '../../api/types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { selectPeer } from './chats';
import { selectPeer } from './peers';
import { selectTabState } from './tabs';
export function selectCurrentViewedStory<T extends GlobalState>(

View File

@ -1,5 +1,5 @@
import type {
ApiUser, ApiUserFullInfo, ApiUserStatus,
ApiUser, ApiUserCommonChats, ApiUserFullInfo, ApiUserStatus,
} from '../../api/types';
import type { GlobalState } from '../types';
@ -17,6 +17,12 @@ export function selectUserFullInfo<T extends GlobalState>(global: T, userId: str
return global.users.fullInfoById[userId];
}
export function selectUserCommonChats<T extends GlobalState>(
global: T, userId: string,
): ApiUserCommonChats | undefined {
return global.users.commonChatsById[userId];
}
export function selectIsUserBlocked<T extends GlobalState>(global: T, userId: string) {
return selectUserFullInfo(global, userId)?.isBlocked;
}

View File

@ -47,6 +47,7 @@ import type {
ApiPaymentFormNativeParams,
ApiPaymentSavedInfo,
ApiPeerColors,
ApiPeerPhotos,
ApiPeerStories,
ApiPhoneCall,
ApiPhoto,
@ -80,6 +81,7 @@ import type {
ApiUpdateAuthorizationStateType,
ApiUpdateConnectionStateType,
ApiUser,
ApiUserCommonChats,
ApiUserFullInfo,
ApiUserStatus,
ApiVideo,
@ -933,7 +935,9 @@ export type GlobalState = {
// Obtained from GetFullUser / UserFullInfo
fullInfoById: Record<string, ApiUserFullInfo>;
previewMediaByBotId: Record<string, ApiBotPreviewMedia[]>;
commonChatsById: Record<string, ApiUserCommonChats>;
};
profilePhotosById: Record<string, ApiPeerPhotos>;
chats: {
// TODO Replace with `Partial<Record>` to properly handle missing keys
@ -2672,7 +2676,9 @@ export interface ActionPayloads {
deleteContact: { userId: string };
loadUser: { userId: string };
setUserSearchQuery: { query?: string } & WithTabId;
loadCommonChats: WithTabId | undefined;
loadCommonChats: {
userId: string;
};
reportSpam: { chatId: string };
loadFullUser: { userId: string; withPhotos?: boolean };
openAddContactDialog: { userId?: string } & WithTabId;