From 25b4aae4cdfbd8a405ec7d00ccf6dc6b821bbc22 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:43:20 +0200 Subject: [PATCH] [Refactoring] User: Extract nested data to a separate path (#4953) --- src/api/gramjs/methods/users.ts | 15 ++- src/api/types/chats.ts | 3 +- src/api/types/users.ts | 13 +-- src/components/common/ProfileInfo.tsx | 23 ++-- src/components/mediaViewer/MediaViewer.tsx | 29 +++-- .../mediaViewer/MediaViewerActions.tsx | 8 +- src/components/mediaViewer/SenderInfo.tsx | 9 +- .../mediaViewer/helpers/getViewableMedia.ts | 13 ++- src/components/right/Profile.tsx | 36 +++--- src/config.ts | 1 - src/global/actions/api/users.ts | 37 +++--- src/global/actions/apiUpdaters/chats.ts | 3 +- src/global/cache.ts | 5 +- src/global/initialState.ts | 2 + src/global/reducers/index.ts | 2 +- src/global/reducers/{general.ts => peers.ts} | 106 +++++++++++------- src/global/reducers/stories.ts | 2 +- src/global/reducers/users.ts | 17 ++- src/global/selectors/chats.ts | 6 +- src/global/selectors/index.ts | 1 + src/global/selectors/messages.ts | 2 +- src/global/selectors/peers.ts | 13 +++ src/global/selectors/stories.ts | 2 +- src/global/selectors/users.ts | 8 +- src/global/types.ts | 8 +- 25 files changed, 223 insertions(+), 141 deletions(-) rename src/global/reducers/{general.ts => peers.ts} (72%) create mode 100644 src/global/selectors/peers.ts diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index b9e43ac3f..3c50969db 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -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() { diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 34715350e..7739caa56 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -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; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index e305ec30d..69e703d7d 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -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; diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 48a158da4..396cb6a32 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -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 = ({ topic, messagesCount, emojiStatusSticker, + profilePhotos, peerId, }) => { const { @@ -84,9 +87,7 @@ const ProfileInfo: FC = ({ 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 = ({ } 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 = ({ >
{renderPhotoTabs()} - {!forceShowSelf && userProfilePhotos?.personalPhoto && ( + {!forceShowSelf && profilePhotos?.personalPhoto && (
- {lang(userProfilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')} + {lang(profilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
)} - {forceShowSelf && userProfilePhotos?.fallbackPhoto && ( + {forceShowSelf && profilePhotos?.fallbackPhoto && (
= ({
{!isLast && ( )} - {lang(userProfilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')} + {lang(profilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
)} @@ -368,6 +369,7 @@ export default memo(withGlobal( 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( mediaIndex, avatarOwnerId, emojiStatusSticker, + profilePhotos, ...(topic && { topic, messagesCount: selectThreadMessagesCount(global, peerId, currentTopicId!), diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 500c9d3d5..4de299876 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -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; 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, diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index 9f62a9fe2..f28a1f3b5 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -128,8 +128,8 @@ const MediaViewerActions: FC = ({ 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 = ({ 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( 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) || {}; diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index fa906b572..d73509e52 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -68,14 +68,15 @@ const SenderInfo: FC = ({ 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[] = []; diff --git a/src/components/mediaViewer/helpers/getViewableMedia.ts b/src/components/mediaViewer/helpers/getViewableMedia.ts index 0c7ef02d2..66c304ba5 100644 --- a/src/components/mediaViewer/helpers/getViewableMedia.ts +++ b/src/components/mediaViewer/helpers/getViewableMedia.ts @@ -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, diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index eaa930078..c02d28a24 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -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; foundIds?: number[]; mediaSearchType?: SharedMediaType; @@ -166,10 +164,8 @@ const Profile: FC = ({ chatId, threadId, profileState, - onProfileStateChange, theme, isChannel, - resolvedUserId, currentUserId, messagesById, foundIds, @@ -205,6 +201,7 @@ const Profile: FC = ({ isSavedDialog, forceScrollProfileTab, isSynced, + onProfileStateChange, }) => { const { setSharedMediaSearchType, @@ -230,7 +227,7 @@ const Profile: FC = ({ const lang = useOldLang(); const [deletingUserId, setDeletingUserId] = useState(); - 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 = ({ 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 = ({ const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( loadMoreMembers, - loadCommonChats, + handleLoadCommonChats, searchSharedMediaMessages, handleLoadPeerStories, handleLoadStoriesArchive, @@ -746,9 +746,12 @@ export default memo(withGlobal( (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( 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( return { theme: selectTheme(global), isChannel, - resolvedUserId, messagesById, foundIds, mediaSearchType, @@ -835,7 +831,7 @@ export default memo(withGlobal( isSynced: global.isSynced, limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'), ...(hasMembersTab && members && { members, adminMembersById }), - ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), + ...(hasCommonChatsTab && user && { commonChatIds: commonChats?.ids }), }; }, )(Profile)); diff --git a/src/config.ts b/src/config.ts index 1b7b4f3b4..44771826a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 39f45f8ad..dde03133c 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -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 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 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 => { - 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', { diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index 768861a14..dd1faa4ee 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -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); } diff --git a/src/global/cache.ts b/src/global/cache.ts index 30414556e..4c652af78 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -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(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: {}, }; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index f4641f6b7..ced1595f1 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -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: {}, }; diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index 78f3f3b2f..c48c7d4ff 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -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'; diff --git a/src/global/reducers/general.ts b/src/global/reducers/peers.ts similarity index 72% rename from src/global/reducers/general.ts rename to src/global/reducers/peers.ts index bbd361744..d6d6409f3 100644 --- a/src/global/reducers/general.ts +++ b/src/global/reducers/peers.ts @@ -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( 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( + 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( global: T, peerId: string, @@ -62,15 +86,12 @@ export function updatePeerPhotos( 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( 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( 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( 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( 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( 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; } diff --git a/src/global/reducers/stories.ts b/src/global/reducers/stories.ts index d1d66f298..a653a1fb0 100644 --- a/src/global/reducers/stories.ts +++ b/src/global/reducers/stories.ts @@ -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(global: T, newStoriesByPeerId: Record): T { diff --git a/src/global/reducers/users.ts b/src/global/reducers/users.ts index f7e50142a..a7b853c22 100644 --- a/src/global/reducers/users.ts +++ b/src/global/reducers/users.ts @@ -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( }; } +export function updateUserCommonChats( + 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(global: T, newById: Record): T { const { statusesById } = global.users; diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index dd24defeb..4846e6f4b 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -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(global: T, peerId: string): ApiPeer | undefined { - return selectUser(global, peerId) || selectChat(global, peerId); -} - export function selectChat(global: T, chatId: string): ApiChat | undefined { return global.chats.byId[chatId]; } diff --git a/src/global/selectors/index.ts b/src/global/selectors/index.ts index cffe277d3..469d88b7d 100644 --- a/src/global/selectors/index.ts +++ b/src/global/selectors/index.ts @@ -11,3 +11,4 @@ export * from './settings'; export * from './statistics'; export * from './stories'; export * from './tabs'; +export * from './peers'; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 1eb514dab..5485efb51 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -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'; diff --git a/src/global/selectors/peers.ts b/src/global/selectors/peers.ts new file mode 100644 index 000000000..33fea6a4b --- /dev/null +++ b/src/global/selectors/peers.ts @@ -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(global: T, peerId: string): ApiPeer | undefined { + return selectUser(global, peerId) || selectChat(global, peerId); +} + +export function selectPeerPhotos(global: T, peerId: string) { + return global.profilePhotosById[peerId]; +} diff --git a/src/global/selectors/stories.ts b/src/global/selectors/stories.ts index e1a9be0d0..860d684a2 100644 --- a/src/global/selectors/stories.ts +++ b/src/global/selectors/stories.ts @@ -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( diff --git a/src/global/selectors/users.ts b/src/global/selectors/users.ts index 557f299bc..cd556e41e 100644 --- a/src/global/selectors/users.ts +++ b/src/global/selectors/users.ts @@ -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(global: T, userId: str return global.users.fullInfoById[userId]; } +export function selectUserCommonChats( + global: T, userId: string, +): ApiUserCommonChats | undefined { + return global.users.commonChatsById[userId]; +} + export function selectIsUserBlocked(global: T, userId: string) { return selectUserFullInfo(global, userId)?.isBlocked; } diff --git a/src/global/types.ts b/src/global/types.ts index bdf130087..6c934e525 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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; previewMediaByBotId: Record; + commonChatsById: Record; }; + profilePhotosById: Record; chats: { // TODO Replace with `Partial` 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;