diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 403216ad4..014d9f70a 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -24,7 +24,7 @@ import type { import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { serializeBytes } from '../helpers'; -import { buildApiUsernames } from './common'; +import { buildApiUsernames, buildAvatarPhotoId } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiEmojiStatus, @@ -50,7 +50,7 @@ function buildApiChatFieldsFromPeerEntity( const accessHash = ('accessHash' in peerEntity) ? String(peerEntity.accessHash) : undefined; const hasVideoAvatar = 'photo' in peerEntity && peerEntity.photo && 'hasVideo' in peerEntity.photo && peerEntity.photo.hasVideo; - const avatarHash = ('photo' in peerEntity) && peerEntity.photo ? buildAvatarHash(peerEntity.photo) : undefined; + const avatarPhotoId = ('photo' in peerEntity) && peerEntity.photo ? buildAvatarPhotoId(peerEntity.photo) : undefined; const isSignaturesShown = Boolean('signatures' in peerEntity && peerEntity.signatures); const hasPrivateLink = Boolean('hasLink' in peerEntity && peerEntity.hasLink); const isScam = Boolean('scam' in peerEntity && peerEntity.scam); @@ -74,7 +74,7 @@ function buildApiChatFieldsFromPeerEntity( usernames, accessHash, hasVideoAvatar, - avatarHash, + avatarPhotoId, ...('verified' in peerEntity && { isVerified: peerEntity.verified }), ...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }), ...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }), @@ -308,14 +308,6 @@ function getUserName(user: GramJs.User) { : (user.lastName || ''); } -export function buildAvatarHash(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) { - if ('photoId' in photo) { - return String(photo.photoId); - } - - return undefined; -} - export function buildChatMember( member: GramJs.TypeChatParticipant | GramJs.TypeChannelParticipant, ): ApiChatMember | undefined { diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index f44e6148e..2a86d8838 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -277,3 +277,11 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess length, }; } + +export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) { + if ('photoId' in photo) { + return photo.photoId.toString(); + } + + return undefined; +} diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index 7bd992a35..9dc6a5643 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -14,8 +14,7 @@ import type { StatisticsStoryInteractionCounter, } from '../../types'; -import { buildAvatarHash } from './chats'; -import { buildApiUsernames } from './common'; +import { buildApiUsernames, buildAvatarPhotoId } from './common'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics { @@ -233,6 +232,8 @@ function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewP function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs.TypeChat[]): ApiMessagePublicForward { const peerId = getApiChatIdFromMtpPeer(message.peerId!); const channel = chats.find((c) => buildApiPeerId(c.id, 'channel') === peerId); + const channelProfilePhoto = channel && 'photo' in channel && channel.photo instanceof GramJs.Photo + ? channel.photo : undefined; return { messageId: message.id, @@ -243,9 +244,8 @@ function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs type: 'chatTypeChannel', title: (channel as GramJs.Channel).title, usernames: buildApiUsernames(channel as GramJs.Channel), - avatarHash: channel && 'photo' in channel - ? buildAvatarHash((channel as GramJs.Channel).photo) - : undefined, + avatarPhotoId: channelProfilePhoto && buildAvatarPhotoId(channelProfilePhoto), + hasVideoAvatar: Boolean(channelProfilePhoto?.videoSizes), }, }; } diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index a47282495..90df95c50 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -11,7 +11,7 @@ import type { import { buildApiBotInfo } from './bots'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; -import { buildApiPhoto, buildApiUsernames } from './common'; +import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; @@ -61,12 +61,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { const { id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId, } = mtpUser; - const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto - ? Boolean(mtpUser.photo.hasVideo) - : undefined; - const avatarHash = mtpUser.photo instanceof GramJs.UserProfilePhoto - ? String(mtpUser.photo.photoId) - : undefined; + const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; + const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo); const userType = buildApiUserType(mtpUser); const usernames = buildApiUsernames(mtpUser); const emojiStatus = mtpUser.emojiStatus ? buildApiEmojiStatus(mtpUser.emojiStatus) : undefined; @@ -90,7 +86,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { phoneNumber: mtpUser.phone || '', noStatus: !mtpUser.status, ...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }), - ...(avatarHash && { avatarHash }), + avatarPhotoId, emojiStatus, hasVideoAvatar, areStoriesHidden: Boolean(mtpUser.storiesHidden), diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index f48929458..6283d195b 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -6,7 +6,7 @@ import type { ApiUser, OnApiUpdate, } from '../../types'; -import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config'; +import { COMMON_CHATS_LIMIT } from '../../../config'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPeerId } from '../apiBuilders/peers'; @@ -85,10 +85,7 @@ export async function fetchFullUser({ onUpdate({ '@type': 'updateUser', id, - user: { - ...user, - avatarHash: user?.avatarHash || undefined, - }, + user, fullInfo, }); @@ -255,14 +252,24 @@ export async function deleteContact({ }); } -export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { +export async function fetchProfilePhotos({ + peer, + offset = 0, + limit = 0, +}: { + peer: ApiPeer; + offset?: number; + limit?: number; +}) { + const chat = 'title' in peer ? peer as ApiChat : undefined; + const user = !chat ? peer as ApiUser : undefined; if (user) { const { id, accessHash } = user; const result = await invokeRequest(new GramJs.photos.GetUserPhotos({ userId: buildInputEntity(id, accessHash) as GramJs.InputUser, - limit: PROFILE_PHOTOS_LIMIT, - offset: 0, + limit, + offset, maxId: BigInt('0'), })); @@ -272,11 +279,17 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { updateLocalDb(result); + const count = result instanceof GramJs.photos.PhotosSlice ? result.count : result.photos.length; + const proposedNextOffsetId = offset + result.photos.length; + const nextOffsetId = proposedNextOffsetId < count ? proposedNextOffsetId : undefined; + return { + count, photos: result.photos .filter((photo): photo is GramJs.Photo => photo instanceof GramJs.Photo) .map((photo) => buildApiPhoto(photo)), users: result.users.map(buildApiUser).filter(Boolean), + nextOffsetId, }; } @@ -285,18 +298,22 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { const result = await searchMessagesLocal({ chat: chat!, type: 'profilePhoto', - limit: PROFILE_PHOTOS_LIMIT, + limit, }); if (!result) { return undefined; } - const { messages, users } = result; + const { + messages, users, totalCount, nextOffsetId, + } = result; return { + count: totalCount, photos: messages.map((message) => message.content.action!.photo).filter(Boolean), users, + nextOffsetId, }; } diff --git a/src/api/gramjs/updates/updater.ts b/src/api/gramjs/updates/updater.ts index 659d41ab3..2cf19f5c8 100644 --- a/src/api/gramjs/updates/updater.ts +++ b/src/api/gramjs/updates/updater.ts @@ -20,12 +20,13 @@ import { buildApiChatFolder, buildApiChatFromPreview, buildApiChatSettings, - buildAvatarHash, buildChatMember, buildChatMembers, buildChatTypingStatus, } from '../apiBuilders/chats'; -import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuilders/common'; +import { + buildApiPhoto, buildApiUsernames, buildPrivacyRules, +} from '../apiBuilders/common'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildApiMessageExtendedMediaPreview, @@ -244,8 +245,10 @@ export function updater(update: Update) { }, }); } else if (action instanceof GramJs.MessageActionChatEditPhoto) { + const apiPhoto = action.photo instanceof GramJs.Photo && buildApiPhoto(action.photo); + if (!apiPhoto) return; + const photo = buildChatPhotoForLocalDb(action.photo); - const avatarHash = buildAvatarHash(photo); const localDbChatId = resolveMessageApiChatId(update.message)!; if (localDb.chats[localDbChatId]) { @@ -253,16 +256,11 @@ export function updater(update: Update) { } addPhotoToLocalDb(action.photo); - if (avatarHash) { - onUpdate({ - '@type': 'updateChat', - id: message.chatId, - chat: { - avatarHash, - }, - ...(action.photo instanceof GramJs.Photo && { newProfilePhoto: buildApiPhoto(action.photo) }), - }); - } + onUpdate({ + '@type': 'updateNewProfilePhoto', + peerId: message.chatId, + photo: apiPhoto, + }); } else if (action instanceof GramJs.MessageActionChatDeletePhoto) { const localDbChatId = resolveMessageApiChatId(update.message)!; if (localDb.chats[localDbChatId]) { @@ -270,9 +268,8 @@ export function updater(update: Update) { } onUpdate({ - '@type': 'updateChat', - id: message.chatId, - chat: { avatarHash: undefined }, + '@type': 'updateDeleteProfilePhoto', + peerId: message.chatId, }); } else if (action instanceof GramJs.MessageActionChatDeleteUser) { // eslint-disable-next-line no-underscore-dangle diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index ede1fe528..771d80670 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 } from './misc'; +import type { ApiChatInviteImporter, ApiPeerPhotos } from './misc'; import type { ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, } from './users'; @@ -35,12 +35,12 @@ export interface ApiChat { accessHash?: string; isMin?: boolean; hasVideoAvatar?: boolean; - avatarHash?: string; + avatarPhotoId?: string; usernames?: ApiUsername[]; membersCount?: number; creationDate?: number; isSupport?: true; - photos?: ApiPhoto[]; + profilePhotos?: ApiPeerPhotos; draftDate?: number; isProtected?: boolean; fakeType?: ApiFakeType; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 94450df2b..d53e0c3b8 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -277,3 +277,12 @@ export interface ApiCollectionInfo { purchaseDate: number; url: string; } + +export interface ApiPeerPhotos { + fallbackPhoto?: ApiPhoto; + personalPhoto?: ApiPhoto; + photos: ApiPhoto[]; + count: number; + nextOffset?: number; + isLoading?: boolean; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 3fd06a97c..b94385424 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -100,7 +100,6 @@ export type ApiUpdateChat = { '@type': 'updateChat'; id: string; chat: Partial; - newProfilePhoto?: ApiPhoto; noTopChatsRequest?: boolean; }; @@ -742,6 +741,18 @@ export type ApiUpdateStarsBalance = { balance: number; }; +export type ApiUpdateDeleteProfilePhoto = { + '@type': 'updateDeleteProfilePhoto'; + peerId: string; + photoId?: string; +}; + +export type ApiUpdateNewProfilePhoto = { + '@type': 'updateNewProfilePhoto'; + peerId: string; + photo: ApiPhoto; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -773,7 +784,8 @@ export type ApiUpdate = ( ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | - ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages + ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages | + ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 4d15a73a2..1d37d9cd9 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -3,6 +3,7 @@ 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; @@ -21,8 +22,8 @@ export interface ApiUser { phoneNumber: string; accessHash?: string; hasVideoAvatar?: boolean; - avatarHash?: string; - photos?: ApiPhoto[]; + avatarPhotoId?: string; + profilePhotos?: ApiPeerPhotos; botPlaceholder?: string; canBeInvitedToGroup?: boolean; commonChats?: { diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 3dfece0d0..5b7cf5e74 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -17,7 +17,7 @@ import { getChatTitle, getPeerStoryHtmlId, getUserFullName, - getVideoAvatarMediaHash, + getVideoProfilePhotoMediaHash, getWebDocumentHash, isAnonymousForwardsChat, isChatWithRepliesBot, @@ -121,7 +121,7 @@ const Avatar: FC = ({ } else if (photo) { imageHash = `photo${photo.id}?size=m`; if (photo.isVideo && withVideo) { - videoHash = getVideoAvatarMediaHash(photo); + videoHash = getVideoProfilePhotoMediaHash(photo); } } else if (webPhoto) { imageHash = getWebDocumentHash(webPhoto); diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 00d07904d..c5271ff11 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -109,7 +109,7 @@ const GroupChatInfo: FC = ({ const { loadFullChat, openMediaViewer, - loadProfilePhotos, + loadMoreProfilePhotos, } = getActions(); const lang = useOldLang(); @@ -121,9 +121,9 @@ const GroupChatInfo: FC = ({ useEffect(() => { if (chatId && !isMin) { if (withFullInfo) loadFullChat({ chatId }); - if (withMediaViewer) loadProfilePhotos({ profileId: chatId }); + if (withMediaViewer) loadMoreProfilePhotos({ peerId: chatId, isPreload: true }); } - }, [chatId, isMin, withFullInfo, loadFullChat, loadProfilePhotos, isSuperGroup, withMediaViewer]); + }, [chatId, isMin, withFullInfo, isSuperGroup, withMediaViewer]); const handleAvatarViewerOpen = useLastCallback( (e: React.MouseEvent, hasMedia: boolean) => { diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 8ea67239d..336e2f33a 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -102,7 +102,7 @@ const PrivateChatInfo: FC = ({ const { loadFullUser, openMediaViewer, - loadProfilePhotos, + loadMoreProfilePhotos, } = getActions(); const lang = useOldLang(); @@ -112,7 +112,7 @@ const PrivateChatInfo: FC = ({ useEffect(() => { if (userId) { if (withFullInfo && isSynced) loadFullUser({ userId }); - if (withMediaViewer) loadProfilePhotos({ profileId: userId }); + if (withMediaViewer) loadMoreProfilePhotos({ peerId: userId, isPreload: true }); } }, [userId, withFullInfo, withMediaViewer, isSynced]); diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 2bccdf00f..76e26f305 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -3,22 +3,19 @@ import React, { memo, useEffect, useState } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { - ApiChat, ApiPhoto, ApiSticker, ApiTopic, ApiUser, ApiUserStatus, + ApiChat, ApiSticker, ApiTopic, ApiUser, ApiUserStatus, } from '../../api/types'; -import type { GlobalState } from '../../global/types'; import { MediaViewerOrigin } from '../../types'; import { - getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserId, isUserOnline, + getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserOnline, } from '../../global/helpers'; import { selectChat, - selectChatFullInfo, selectCurrentMessageList, selectTabState, selectThreadMessagesCount, selectUser, - selectUserFullInfo, selectUserStatus, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -30,7 +27,6 @@ import renderText from './helpers/renderText'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import usePrevious from '../../hooks/usePrevious'; -import { useStateRef } from '../../hooks/useStateRef'; import usePhotosPreload from './hooks/usePhotosPreload'; import Transition from '../ui/Transition'; @@ -43,7 +39,7 @@ import './ProfileInfo.scss'; import styles from './ProfileInfo.module.scss'; type OwnProps = { - userId: string; + peerId: string; forceShowSelf?: boolean; canPlayVideo: boolean; }; @@ -57,16 +53,13 @@ type StateProps = avatarOwnerId?: string; topic?: ApiTopic; messagesCount?: number; - userPersonalPhoto?: ApiPhoto; - userProfilePhoto?: ApiPhoto; - userFallbackPhoto?: ApiPhoto; - chatProfilePhoto?: ApiPhoto; emojiStatusSticker?: ApiSticker; - } - & Pick; + }; const EMOJI_STATUS_SIZE = 24; const EMOJI_TOPIC_SIZE = 120; +const LOAD_MORE_THRESHOLD = 3; +const MAX_PHOTO_DASH_COUNT = 30; const ProfileInfo: FC = ({ forceShowSelf, @@ -74,33 +67,28 @@ const ProfileInfo: FC = ({ user, userStatus, chat, - isSynced, mediaIndex, avatarOwnerId, topic, messagesCount, - userPersonalPhoto, - userProfilePhoto, - userFallbackPhoto, - chatProfilePhoto, emojiStatusSticker, + peerId, }) => { const { - loadFullUser, openMediaViewer, openPremiumModal, openStickerSet, openPrivacySettingsNoticeModal, + loadMoreProfilePhotos, } = getActions(); const lang = useOldLang(); - const { id: userId } = user || {}; - const { id: chatId } = chat || {}; - const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY; + const userProfilePhotos = user?.profilePhotos; + const chatProfilePhotos = chat?.profilePhotos; + const photos = userProfilePhotos?.photos || chatProfilePhotos?.photos || MEMO_EMPTY_ARRAY; const prevMediaIndex = usePrevious(mediaIndex); const prevAvatarOwnerId = usePrevious(avatarOwnerId); - const mediaIndexRef = useStateRef(mediaIndex); const [hasSlideAnimation, setHasSlideAnimation] = useState(true); // slideOptimized doesn't work well when animation is dynamically disabled const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none'; @@ -109,6 +97,12 @@ const ProfileInfo: FC = ({ const isFirst = photos.length <= 1 || currentPhotoIndex === 0; const isLast = photos.length <= 1 || currentPhotoIndex === photos.length - 1; + useEffect(() => { + if (photos.length - currentPhotoIndex <= LOAD_MORE_THRESHOLD) { + loadMoreProfilePhotos({ peerId }); + } + }, [currentPhotoIndex, peerId, photos.length]); + // Set the current avatar photo to the last selected photo in Media Viewer after it is closed useEffect(() => { if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) { @@ -117,12 +111,6 @@ const ProfileInfo: FC = ({ } }, [mediaIndex, prevMediaIndex, prevAvatarOwnerId]); - // Reset the current avatar photo to the one selected in Media Viewer if photos have changed - useEffect(() => { - setHasSlideAnimation(false); - setCurrentPhotoIndex(mediaIndexRef.current || 0); - }, [mediaIndexRef, photos]); - // Deleting the last profile photo may result in an error useEffect(() => { if (currentPhotoIndex > photos.length) { @@ -131,32 +119,26 @@ const ProfileInfo: FC = ({ } }, [currentPhotoIndex, photos.length]); - useEffect(() => { - if (isSynced && userId && !forceShowSelf) { - loadFullUser({ userId }); - } - }, [userId, loadFullUser, isSynced, forceShowSelf]); - usePhotosPreload(photos, currentPhotoIndex); const handleProfilePhotoClick = useLastCallback(() => { openMediaViewer({ isAvatarView: true, - chatId: userId || chatId, + chatId: peerId, mediaIndex: currentPhotoIndex, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, }); }); const handleStatusClick = useLastCallback(() => { - if (!userId) { + if (!peerId) { openStickerSet({ stickerSetInfo: emojiStatusSticker!.stickerSetInfo, }); return; } - openPremiumModal({ fromUserId: userId }); + openPremiumModal({ fromUserId: peerId }); }); const selectPreviousMedia = useLastCallback(() => { @@ -231,14 +213,18 @@ const ProfileInfo: FC = ({ } function renderPhotoTabs() { - if (!photos || photos.length <= 1) { + const totalPhotosLength = Math.max(photos.length, userProfilePhotos?.count || 0, chatProfilePhotos?.count || 0); + if (!photos || totalPhotosLength <= 1) { return undefined; } + const enumerator = Array.from({ length: Math.min(totalPhotosLength, MAX_PHOTO_DASH_COUNT) }); + const activeDashIndex = currentPhotoIndex >= MAX_PHOTO_DASH_COUNT ? MAX_PHOTO_DASH_COUNT - 1 : currentPhotoIndex; + return (
- {photos.map((_, i) => ( - + {enumerator.map((_, i) => ( + ))}
); @@ -248,14 +234,13 @@ const ProfileInfo: FC = ({ const photo = photos.length > 0 ? photos[currentPhotoIndex] : undefined; - const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto; return ( @@ -263,8 +248,6 @@ const ProfileInfo: FC = ({ } function renderStatus() { - const peerId = (chatId || userId)!; - const isAnonymousForwards = isAnonymousForwardsChat(peerId); if (isAnonymousForwards) return undefined; @@ -310,18 +293,18 @@ const ProfileInfo: FC = ({ >
{renderPhotoTabs()} - {!forceShowSelf && userPersonalPhoto && ( + {!forceShowSelf && userProfilePhotos?.personalPhoto && (
- {lang(userPersonalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')} + {lang(userProfilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
)} - {forceShowSelf && userFallbackPhoto && ( + {forceShowSelf && userProfilePhotos?.fallbackPhoto && (
= ({
{!isLast && ( )} - {lang(userFallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')} + {lang(userProfilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
)} @@ -381,37 +364,28 @@ const ProfileInfo: FC = ({ }; export default memo(withGlobal( - (global, { userId }): StateProps => { - const { isSynced } = global; - const user = selectUser(global, userId); - const isPrivate = isUserId(userId); - const userStatus = selectUserStatus(global, userId); - const chat = selectChat(global, userId); + (global, { peerId }): StateProps => { + const user = selectUser(global, peerId); + const userStatus = selectUserStatus(global, peerId); + const chat = selectChat(global, peerId); const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const isForum = chat?.isForum; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; - const userFullInfo = isPrivate ? selectUserFullInfo(global, userId) : undefined; - const chatFullInfo = !isPrivate ? selectChatFullInfo(global, userId) : undefined; const emojiStatus = (user || chat)?.emojiStatus; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; return { - isSynced, user, userStatus, chat, - userPersonalPhoto: userFullInfo?.personalPhoto, - userProfilePhoto: userFullInfo?.profilePhoto, - userFallbackPhoto: userFullInfo?.fallbackPhoto, - chatProfilePhoto: chatFullInfo?.profilePhoto, mediaIndex, avatarOwnerId, emojiStatusSticker, ...(topic && { topic, - messagesCount: selectThreadMessagesCount(global, userId, currentTopicId!), + messagesCount: selectThreadMessagesCount(global, peerId, currentTopicId!), }), }; }, diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index f325b6246..cad040647 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -8,8 +8,10 @@ import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; import { getChatAvatarHash, getChatTitle, + getPhotoMediaHash, + getProfilePhotoMediaHash, getUserFullName, - getVideoAvatarMediaHash, + getVideoProfilePhotoMediaHash, isAnonymousForwardsChat, isChatWithRepliesBot, isDeletedUser, @@ -62,28 +64,30 @@ const ProfilePhoto: FC = ({ const isDeleted = user && isDeletedUser(user); const isRepliesChat = chat && isChatWithRepliesBot(chat.id); const isAnonymousForwards = chat && isAnonymousForwardsChat(chat.id); - const peer = user || chat; + const peer = (user || chat)!; const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat && !isAnonymousForwards; const { isVideo } = photo || {}; - const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal'); - const avatarBlobUrl = useMedia(avatarHash); + const avatarHash = (!photo || photo.id === peer.avatarPhotoId) && getChatAvatarHash(peer, 'normal'); - const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`; + const previewHash = canHaveMedia && photo && !avatarHash && getPhotoMediaHash(photo, 'pictogram'); + const previewBlobUrl = useMedia(previewHash || avatarHash); + + const photoHash = canHaveMedia && photo && !isVideo && getProfilePhotoMediaHash(photo); const photoBlobUrl = useMedia(photoHash); - const videoHash = canHaveMedia && photo && isVideo && getVideoAvatarMediaHash(photo); + const videoHash = canHaveMedia && photo && isVideo && getVideoProfilePhotoMediaHash(photo); const videoBlobUrl = useMedia(videoHash); const fullMediaData = videoBlobUrl || photoBlobUrl; const [isVideoReady, markVideoReady] = useFlag(); const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady)); const transitionClassNames = useMediaTransition(isFullMediaReady); - const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && photo?.thumbnail?.dataUri; + const isBlurredThumb = canHaveMedia && !isFullMediaReady && !previewBlobUrl && photo?.thumbnail?.dataUri; const blurredThumbCanvasRef = useCanvasBlur( photo?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED, ); - const hasMedia = photo || avatarBlobUrl || isBlurredThumb; + const hasMedia = photo || previewBlobUrl || isBlurredThumb; useEffect(() => { if (videoRef.current && !canPlayVideo) { @@ -121,7 +125,7 @@ const ProfilePhoto: FC = ({ {isBlurredThumb ? ( ) : ( - + )} {photo && ( isVideo ? ( diff --git a/src/components/common/hooks/usePhotosPreload.ts b/src/components/common/hooks/usePhotosPreload.ts index 3ef7de379..589fb0664 100644 --- a/src/components/common/hooks/usePhotosPreload.ts +++ b/src/components/common/hooks/usePhotosPreload.ts @@ -3,6 +3,7 @@ import { useEffect } from '../../../lib/teact/teact'; import type { ApiPhoto } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types'; +import { getProfilePhotoMediaHash } from '../../../global/helpers'; import * as mediaLoader from '../../../util/mediaLoader'; const PHOTOS_TO_PRELOAD = 4; @@ -13,9 +14,10 @@ export default function usePhotosPreload( ) { useEffect(() => { photos.slice(currentIndex, currentIndex + PHOTOS_TO_PRELOAD).forEach((photo) => { - const mediaData = mediaLoader.getFromMemory(`photo${photo.id}?size=c`); + const mediaHash = getProfilePhotoMediaHash(photo); + const mediaData = mediaLoader.getFromMemory(mediaHash); if (!mediaData) { - mediaLoader.fetch(`photo${photo.id}?size=c`, ApiMediaFormat.BlobUrl); + mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl); } }); }, [currentIndex, photos]); diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 7fddade19..e83f0289a 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -49,7 +49,7 @@ const SettingsMain: FC = ({ onReset, }) => { const { - loadProfilePhotos, + loadMoreProfilePhotos, openPremiumModal, openSupportChat, openUrl, @@ -63,9 +63,9 @@ const SettingsMain: FC = ({ useEffect(() => { if (currentUserId) { - loadProfilePhotos({ profileId: currentUserId }); + loadMoreProfilePhotos({ peerId: currentUserId, isPreload: true }); } - }, [currentUserId, loadProfilePhotos]); + }, [currentUserId]); useHistoryBack({ isActive, @@ -82,7 +82,7 @@ const SettingsMain: FC = ({
{currentUserId && ( diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 1503a2dca..e5321ea01 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -80,6 +80,7 @@ type StateProps = { }; const ANIMATION_DURATION = 250; +const AVATAR_LOAD_TRIGGER = 4; const MediaViewer = ({ chatId, @@ -109,6 +110,7 @@ const MediaViewer = ({ focusMessage, toggleChatInfo, searchChatMediaMessages, + loadMoreProfilePhotos, } = getActions(); const isOpen = Boolean(avatarOwner || message || standaloneMedia); @@ -255,9 +257,17 @@ const MediaViewer = ({ }, [isGif, isVideo]); const loadMoreItemsIfNeeded = useLastCallback((item?: MediaViewerItem) => { - if (!item || !withDynamicLoading || isLoadingMoreMedia) return; - if (item.type !== 'message') return; - searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: item.message.id }); + if (!item || isLoadingMoreMedia) return; + + if (item.type === 'avatar') { + const isNearEnd = item.mediaIndex >= item.avatarOwner.profilePhotos!.photos.length - AVATAR_LOAD_TRIGGER; + if (!isNearEnd) return; + loadMoreProfilePhotos({ peerId: item.avatarOwner.id }); + } + + if (item.type === 'message' && withDynamicLoading) { + searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: item.message.id }); + } }); const getNextItem = useLastCallback((from: MediaViewerItem, direction: number): MediaViewerItem | undefined => { @@ -276,7 +286,7 @@ const MediaViewer = ({ if (from.type === 'avatar') { const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from; const nextIndex = fromMediaIndex + direction; - if (nextIndex >= 0 && fromAvatarOwner.photos && nextIndex < fromAvatarOwner.photos.length) { + if (nextIndex >= 0 && fromAvatarOwner.profilePhotos && nextIndex < fromAvatarOwner.profilePhotos.photos.length) { return { type: 'avatar', avatarOwner: fromAvatarOwner, mediaIndex: nextIndex }; } @@ -332,7 +342,8 @@ const MediaViewer = ({ }); const handleBeforeDelete = useLastCallback(() => { - const mediaCount = avatarOwner?.photos?.length || standaloneMedia?.length || messageMediaIds?.length || 0; + const mediaCount = avatarOwner?.profilePhotos?.photos.length + || standaloneMedia?.length || messageMediaIds?.length || 0; if (mediaCount <= 1 || !currentItem) { handleClose(); return; @@ -344,7 +355,7 @@ const MediaViewer = ({ return; } - if (currentItem.type === 'avatar' || currentItem.type === 'standalone') { + if ((currentItem.type === 'avatar' && isUserId(currentItem.avatarOwner.id)) || currentItem.type === 'standalone') { // Keep current item, it'll update when indexes shift return; } @@ -451,9 +462,12 @@ export default memo(withGlobal( canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat); } + const profilePhotos = peer?.profilePhotos; + return { - avatar: peer?.photos?.[mediaIndex!], + avatar: profilePhotos?.photos[mediaIndex!], avatarOwner: peer, + isLoadingMoreMedia: profilePhotos?.isLoading, isChatWithSelf, canUpdateMedia, withAnimation, @@ -462,6 +476,7 @@ export default memo(withGlobal( isHidden, standaloneMedia, mediaIndex, + isSynced, }; } diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index 9d2d38024..83d37457e 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -129,7 +129,7 @@ const MediaViewerActions: FC = ({ const handleUpdate = useLastCallback(() => { if (item?.type !== 'avatar') return; const { avatarOwner, mediaIndex } = item; - const avatarPhoto = avatarOwner.photos?.[mediaIndex]!; + const avatarPhoto = avatarOwner.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.photos![item.mediaIndex!]} + photo={item.avatarOwner.profilePhotos!.photos[item.mediaIndex!]} /> ) : undefined; } @@ -212,7 +212,8 @@ const MediaViewerActions: FC = ({ if (item?.type === 'message') { openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', - message: item.message, onConfirm: onBeforeDelete, + message: item.message, + onConfirm: onBeforeDelete, }); } else { openDeleteModal(); @@ -389,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?.photos?.[item!.mediaIndex!]; + const avatarPhoto = avatarOwner?.profilePhotos?.photos[item!.mediaIndex!]; const currentMessageList = selectCurrentMessageList(global); const { threadId } = selectCurrentMessageList(global) || {}; @@ -398,10 +399,10 @@ export default memo(withGlobal( const isChatProtected = message && selectIsChatProtected(global, message?.chatId); const { canDelete: canDeleteMessage } = (threadId && message && selectAllowedMessageActions(global, message, threadId)) || {}; - const isCurrentAvatar = avatarPhoto && (avatarPhoto.id === avatarOwner?.avatarHash); - const canDeleteAvatar = canUpdateMedia && !!avatarPhoto; + const isCurrentAvatar = avatarPhoto && (avatarPhoto.id === avatarOwner?.avatarPhotoId); + const canDeleteAvatar = canUpdateMedia && Boolean(avatarPhoto); const canDelete = canDeleteMessage || canDeleteAvatar; - const canUpdate = canUpdateMedia && !!avatarPhoto && !isCurrentAvatar; + const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar; const messageListType = currentMessageList?.type; return { diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index d6a926637..fa906b572 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -10,7 +10,6 @@ import { } from '../../global/helpers'; import { selectSender, - selectUserFullInfo, } from '../../global/selectors'; import { formatMediaDateTime } from '../../util/dates/dateFormat'; import renderText from '../common/helpers/renderText'; @@ -29,7 +28,6 @@ type OwnProps = { type StateProps = { owner?: ApiPeer; - isFallbackAvatar?: boolean; }; const BULLET = '\u2022'; @@ -38,7 +36,6 @@ const ANIMATION_DURATION = 350; const SenderInfo: FC = ({ owner, item, - isFallbackAvatar, }) => { const { closeMediaViewer, @@ -71,11 +68,15 @@ const SenderInfo: FC = ({ if (!item || item.type === 'standalone') return undefined; const avatarOwner = item.type === 'avatar' ? item.avatarOwner : undefined; - const avatar = avatarOwner?.photos?.[item.mediaIndex!]; + const avatar = avatarOwner?.profilePhotos?.photos[item.mediaIndex!]; + const isFallbackAvatar = avatar?.id === avatarOwner?.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 countText = count && lang('Of', [item.mediaIndex! + 1, count]); const parts: string[] = []; if (avatar) { @@ -89,10 +90,12 @@ const SenderInfo: FC = ({ )); } + if (countText) parts.push(countText); + parts.push(formattedDate); return parts.join(` ${BULLET} `); - }, [item, isFallbackAvatar, lang]); + }, [item, lang]); if (!owner) { return undefined; @@ -122,15 +125,8 @@ export default withGlobal( const owner = item?.type === 'avatar' ? item.avatarOwner : messageSender; - const fallbackAvatar = item?.type === 'avatar' - ? selectUserFullInfo(global, item.avatarOwner.id)?.fallbackPhoto : undefined; - - const isFallbackAvatar = fallbackAvatar && item?.type === 'avatar' - && item.avatarOwner.photos?.[item.mediaIndex].id === fallbackAvatar.id; - return { owner, - isFallbackAvatar, }; }, )(SenderInfo); diff --git a/src/components/mediaViewer/helpers/getViewableMedia.ts b/src/components/mediaViewer/helpers/getViewableMedia.ts index 01f684595..84059264a 100644 --- a/src/components/mediaViewer/helpers/getViewableMedia.ts +++ b/src/components/mediaViewer/helpers/getViewableMedia.ts @@ -68,7 +68,7 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi } if (params.type === 'avatar') { - const avatar = params.avatarOwner.photos?.[params.mediaIndex]; + const avatar = params.avatarOwner.profilePhotos?.photos[params.mediaIndex]; if (avatar) { return { media: avatar, diff --git a/src/components/mediaViewer/hooks/useMediaProps.ts b/src/components/mediaViewer/hooks/useMediaProps.ts index 2d8761ac2..32e269b42 100644 --- a/src/components/mediaViewer/hooks/useMediaProps.ts +++ b/src/components/mediaViewer/hooks/useMediaProps.ts @@ -10,8 +10,9 @@ import { getMediaHash, getMediaThumbUri, getPhotoFullDimensions, - getVideoAvatarMediaHash, + getProfilePhotoMediaHash, getVideoDimensions, + getVideoProfilePhotoMediaHash, isDocumentPhoto, isDocumentVideo, } from '../../../global/helpers'; @@ -36,6 +37,7 @@ export const useMediaProps = ({ origin, delay, }: UseMediaProps) => { + const isPhotoAvatar = isAvatar && media?.mediaType === 'photo' && !media.isVideo; const isVideoAvatar = isAvatar && media?.mediaType === 'photo' && media.isVideo; const isDocument = media?.mediaType === 'document'; const isVideo = (media?.mediaType === 'video' && !media.isRound) || (isDocument && isDocumentVideo(media)); @@ -47,12 +49,16 @@ export const useMediaProps = ({ const getMediaOrAvatarHash = useMemo(() => (isFull?: boolean) => { if (!media) return undefined; + if ((isPhotoAvatar || isVideoAvatar) && !isFull) { + return getProfilePhotoMediaHash(media); + } + if (isVideoAvatar && isFull) { - return getVideoAvatarMediaHash(media); + return getVideoProfilePhotoMediaHash(media); } return getMediaHash(media, isFull ? 'full' : 'preview'); - }, [isVideoAvatar, media]); + }, [isVideoAvatar, isPhotoAvatar, media]); const pictogramBlobUrl = useMedia( media diff --git a/src/components/middle/ActionMessageSuggestedAvatar.tsx b/src/components/middle/ActionMessageSuggestedAvatar.tsx index 18e204925..ed067019a 100644 --- a/src/components/middle/ActionMessageSuggestedAvatar.tsx +++ b/src/components/middle/ActionMessageSuggestedAvatar.tsx @@ -7,7 +7,7 @@ import type { TextPart } from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; import { MediaViewerOrigin, SettingsScreens } from '../../types'; -import { getPhotoMediaHash, getVideoAvatarMediaHash } from '../../global/helpers'; +import { getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../global/helpers'; import { fetchBlob } from '../../util/files'; import useFlag from '../../hooks/useFlag'; @@ -39,7 +39,7 @@ const ActionMessageSuggestedAvatar: FC = ({ const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false); const photo = message.content.action!.photo!; const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full')); - const suggestedVideoUrl = useMedia(getVideoAvatarMediaHash(photo)); + const suggestedVideoUrl = useMedia(getVideoProfilePhotoMediaHash(photo)); const isVideo = message.content.action!.photo?.isVideo; const showAvatarNotification = useLastCallback(() => { diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 83bf2ea84..a98a4ba3e 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -204,7 +204,6 @@ const Profile: FC = ({ openMediaViewer, openAudioPlayer, focusMessage, - loadProfilePhotos, setNewChatMembersDialogState, loadPeerProfileStories, loadStoriesArchive, @@ -342,10 +341,6 @@ const Profile: FC = ({ setSharedMediaSearchType({ mediaType: tabType as SharedMediaType }); }, [setSharedMediaSearchType, tabType, threadId]); - useEffect(() => { - loadProfilePhotos({ profileId }); - }, [profileId]); - const handleSelectMedia = useLastCallback((messageId: number) => { openMediaViewer({ chatId: profileId, @@ -681,7 +676,7 @@ const Profile: FC = ({ function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) { return (
- +
); diff --git a/src/config.ts b/src/config.ts index b4d4763f4..f1ca9535e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -83,7 +83,6 @@ export const MEMBERS_SLICE = 30; export const MEMBERS_LOAD_SLICE = 200; export const PINNED_MESSAGES_LIMIT = 50; export const BLOCKED_LIST_LIMIT = 100; -export const PROFILE_PHOTOS_LIMIT = 40; export const PROFILE_SENSITIVE_AREA = 500; export const TOPIC_LIST_SENSITIVE_AREA = 600; export const COMMON_CHATS_LIMIT = 100; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 29ce9545a..3e0a26387 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -63,6 +63,7 @@ import { addUsers, addUserStatuses, deleteChatMessages, + deletePeerPhoto, deleteTopic, leaveChat, removeChatFromChatLists, @@ -574,7 +575,7 @@ addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => const loadChat = async () => { await loadFullChat(global, actions, chat, tabId); if (withPhotos) { - actions.loadProfilePhotos({ profileId: chatId }); + actions.loadMoreProfilePhotos({ peerId: chatId, shouldInvalidateCache: true }); } }; @@ -1727,17 +1728,12 @@ addActionHandler('updateChatPhoto', async (global, actions, payload): Promise photosToDelete.some((toDelete) => toDelete.id !== p.id)); global = getGlobal(); - global = updateChat(global, chatId, { photos: newPhotos }); - + global = deletePeerPhoto(global, chatId, photo.id); setGlobal(global); - // Delete references to the old photos - const result = await callApi('deleteProfilePhotos', photosToDelete); - if (!result) return; actions.loadFullChat({ chatId, tabId, withPhotos: true }); }); diff --git a/src/global/actions/api/management.ts b/src/global/actions/api/management.ts index 291c2ed45..5d59d53bc 100644 --- a/src/global/actions/api/management.ts +++ b/src/global/actions/api/management.ts @@ -455,7 +455,7 @@ addActionHandler('uploadContactProfilePhoto', async (global, actions, payload): return; } - actions.loadProfilePhotos({ profileId: userId }); + actions.loadMoreProfilePhotos({ peerId: userId, shouldInvalidateCache: true }); global = getGlobal(); global = updateManagementProgress(global, ManagementProgress.Complete, tabId); diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 0148e0254..0b957d135 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -19,12 +19,13 @@ import { callApi } from '../../../api/gramjs'; import { buildApiInputPrivacyRules } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { - addBlockedUser, addNotifyExceptions, addUsers, removeBlockedUser, replaceSettings, updateChat, updateChats, + addBlockedUser, addNotifyExceptions, addUsers, deletePeerPhoto, + removeBlockedUser, replaceSettings, updateChat, updateChats, updateNotifySettings, updateUser, updateUserFullInfo, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { - selectChat, selectTabState, selectUser, selectUserFullInfo, + selectChat, selectTabState, selectUser, } from '../../selectors'; addActionHandler('updateProfile', async (global, actions, payload): Promise => { @@ -110,7 +111,7 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise const currentUser = selectUser(global, currentUserId); if (!currentUser) return; - global = updateUser(global, currentUserId, { avatarHash: undefined }); + global = updateUser(global, currentUserId, { avatarPhotoId: undefined }); global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined }); setGlobal(global); @@ -129,32 +130,14 @@ addActionHandler('deleteProfilePhoto', async (global, actions, payload): Promise const { photo } = payload; const { currentUserId } = global; if (!currentUserId) return; - const currentUser = selectUser(global, currentUserId); - if (!currentUser) return; - const fullInfo = selectUserFullInfo(global, currentUserId); - - if (currentUser.avatarHash === photo.id || fullInfo?.profilePhoto?.id === photo.id) { - global = updateUser(global, currentUserId, { avatarHash: undefined }); - global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined }); - } - - if (fullInfo?.fallbackPhoto?.id === photo.id) { - global = updateUserFullInfo(global, currentUserId, { fallbackPhoto: undefined }); - } - - if (fullInfo?.personalPhoto?.id === photo.id) { - global = updateUserFullInfo(global, currentUserId, { personalPhoto: undefined }); - } - - const { photos = [] } = currentUser; - - const newPhotos = photos.filter((p) => p.id !== photo.id); - global = updateUser(global, currentUserId, { photos: newPhotos }); + const isDeleted = await callApi('deleteProfilePhotos', [photo]); + if (!isDeleted) return; + global = getGlobal(); + global = deletePeerPhoto(global, currentUserId, photo.id); setGlobal(global); - await callApi('deleteProfilePhotos', [photo]); actions.loadFullUser({ userId: currentUserId, withPhotos: true }); }); diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 947faed53..b9d2864d5 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -20,9 +20,10 @@ import { addUserStatuses, closeNewContactDialog, replaceUserStatuses, - updateChat, updateChats, updateManagementProgress, + updatePeerPhotos, + updatePeerPhotosIsLoading, updateUser, updateUserFullInfo, updateUsers, @@ -30,14 +31,21 @@ import { updateUserSearchFetchingStatus, } from '../../reducers'; import { - selectChat, selectChatFullInfo, selectCurrentMessageList, selectPeer, selectTabState, selectUser, selectUserFullInfo, + selectChat, + selectChatFullInfo, + selectCurrentMessageList, + selectPeer, + selectTabState, + selectUser, + selectUserFullInfo, } from '../../selectors'; +const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10; const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min const runThrottledForSearch = throttle((cb) => cb(), 500, false); addActionHandler('loadFullUser', async (global, actions, payload): Promise => { - const { userId, withPhotos } = payload!; + const { userId, withPhotos } = payload; const user = selectUser(global, userId); if (!user) { return; @@ -50,11 +58,11 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise global = getGlobal(); const fullInfo = selectUserFullInfo(global, userId); const { user: newUser, fullInfo: newFullInfo } = result; - const hasChangedAvatarHash = user.avatarHash !== newUser.avatarHash; + const hasChangedAvatar = user.avatarPhotoId !== newUser.avatarPhotoId; const hasChangedProfilePhoto = fullInfo?.profilePhoto?.id !== newFullInfo?.profilePhoto?.id; const hasChangedFallbackPhoto = fullInfo?.fallbackPhoto?.id !== newFullInfo?.fallbackPhoto?.id; const hasChangedPersonalPhoto = fullInfo?.personalPhoto?.id !== newFullInfo?.personalPhoto?.id; - const hasChangedPhoto = hasChangedAvatarHash + const hasChangedPhoto = hasChangedAvatar || hasChangedProfilePhoto || hasChangedFallbackPhoto || hasChangedPersonalPhoto; @@ -65,13 +73,13 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise global = updateChats(global, buildCollectionByKey(result.chats, 'id')); setGlobal(global); - if (withPhotos || (user.photos?.length && hasChangedPhoto)) { - actions.loadProfilePhotos({ profileId: userId }); + if (withPhotos || (user.profilePhotos?.count && hasChangedPhoto)) { + actions.loadMoreProfilePhotos({ peerId: userId, shouldInvalidateCache: true }); } }); addActionHandler('loadUser', async (global, actions, payload): Promise => { - const { userId } = payload!; + const { userId } = payload; const user = selectUser(global, userId); if (!user) { return; @@ -251,30 +259,36 @@ addActionHandler('deleteContact', async (global, actions, payload): Promise => { - const { profileId } = payload!; - const isPrivate = isUserId(profileId); +addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Promise => { + const { peerId, shouldInvalidateCache, isPreload } = payload; + const isPrivate = isUserId(peerId); - let user = isPrivate ? selectUser(global, profileId) : undefined; - const chat = !isPrivate ? selectChat(global, profileId) : undefined; - if (!user && !chat) { + const user = isPrivate ? selectUser(global, peerId) : undefined; + const chat = !isPrivate ? selectChat(global, peerId) : undefined; + const peer = user || chat; + if (!peer?.avatarPhotoId) { return; } - let userFullInfo = selectUserFullInfo(global, profileId); - let chatFullInfo = selectChatFullInfo(global, profileId); - if (user && !userFullInfo?.profilePhoto) { + if (peer.profilePhotos && !shouldInvalidateCache && (isPreload || !peer.profilePhotos.nextOffset)) return; + + global = updatePeerPhotosIsLoading(global, peerId, true); + setGlobal(global); + + global = getGlobal(); + + let userFullInfo = selectUserFullInfo(global, peerId); + let chatFullInfo = selectChatFullInfo(global, peerId); + if (user && !userFullInfo) { const { id, accessHash } = user; const result = await callApi('fetchFullUser', { id, accessHash }); if (!result?.user) { return; } - - user = result.user; userFullInfo = result.fullInfo; } - if (chat && !chatFullInfo?.profilePhoto) { + if (chat && !chatFullInfo) { const result = await callApi('fetchFullChat', chat); if (!result?.fullInfo) { return; @@ -283,38 +297,42 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload): Promise< chatFullInfo = result.fullInfo; } - const result = await callApi('fetchProfilePhotos', user, chat); + const peerFullInfo = userFullInfo || chatFullInfo; + if (!peerFullInfo) return; + + const offset = peer.profilePhotos?.nextOffset; + const limit = !offset || isPreload || shouldInvalidateCache ? PROFILE_PHOTOS_FIRST_LOAD_LIMIT : undefined; + + const result = await callApi('fetchProfilePhotos', { + peer, + offset, + limit, + }); if (!result || !result.photos) { return; } global = getGlobal(); - const userOrChat = user || chat; - const { photos, users } = result; - - const fallbackPhoto = userFullInfo?.fallbackPhoto; - const personalPhoto = userFullInfo?.personalPhoto; - const chatCurrentPhoto = chatFullInfo?.profilePhoto; - if (fallbackPhoto) photos.push(fallbackPhoto); - if (personalPhoto) photos.unshift(personalPhoto); - if (chatCurrentPhoto && photos[0]?.id !== chatCurrentPhoto.id) photos.unshift(chatCurrentPhoto); - - photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1)); + const { + photos, users, count, nextOffsetId, + } = result; global = addUsers(global, buildCollectionByKey(users, 'id')); - if (isPrivate) { - global = updateUser(global, profileId, { photos }); - } else { - global = updateChat(global, profileId, { photos }); - } + global = updatePeerPhotos(global, peerId, { + newPhotos: photos, + count, + nextOffset: nextOffsetId, + fullInfo: peerFullInfo, + shouldInvalidateCache, + }); setGlobal(global); }); addActionHandler('setUserSearchQuery', (global, actions, payload): ActionReturnType => { - const { query, tabId = getCurrentTabId() } = payload!; + const { query, tabId = getCurrentTabId() } = payload; if (!query) return; @@ -371,7 +389,7 @@ addActionHandler('importContact', async (global, actions, payload): Promise { - const { chatId } = payload!; + const { chatId } = payload; const peer = selectPeer(global, chatId); if (!peer) { return; @@ -381,13 +399,13 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => { }); addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => { - const { emojiStatus, expires } = payload!; + const { emojiStatus, expires } = payload; void callApi('updateEmojiStatus', emojiStatus, expires); }); addActionHandler('saveCloseFriends', async (global, actions, payload): Promise => { - const { userIds } = payload!; + const { userIds } = payload; const result = await callApi('saveCloseFriends', userIds); if (!result) { diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index 47ec4c12d..a79ff19c8 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -14,6 +14,7 @@ import { import { addUnreadMentions, deleteChatMessages, + deletePeerPhoto, leaveChat, removeUnreadMentions, replaceThreadParam, @@ -35,6 +36,7 @@ import { selectCommonBoxChatId, selectCurrentMessageList, selectIsChatListed, + selectPeer, selectTabState, selectThreadParam, selectTopicFromMessage, @@ -57,7 +59,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { const localChat = selectChat(global, update.id); - global = updateChat(global, update.id, update.chat, update.newProfilePhoto); + global = updateChat(global, update.id, update.chat); if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) { global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false); @@ -534,6 +536,43 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { isForumAsMessages: isEnabled, }); setGlobal(global); + break; + } + + case 'updateNewProfilePhoto': { + const { peerId, photo } = update; + + global = updateChat(global, peerId, { + avatarPhotoId: photo.id, + }); + setGlobal(global); + + actions.loadMoreProfilePhotos({ peerId, shouldInvalidateCache: true }); + + break; + } + + case 'updateDeleteProfilePhoto': { + const { peerId, photoId } = update; + + const peer = selectPeer(global, peerId); + if (!peer) { + return undefined; + } + + if (!photoId || peer.avatarPhotoId === photoId) { + global = updateChat(global, peerId, { + avatarPhotoId: undefined, + profilePhotos: undefined, + }); + } else { + global = deletePeerPhoto(global, peerId, photoId); + } + setGlobal(global); + + actions.loadMoreProfilePhotos({ peerId, shouldInvalidateCache: true }); + + break; } } diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index fac26ac0c..850e7127b 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -30,6 +30,7 @@ import { clearMessageTranslation, deleteChatMessages, deleteChatScheduledMessages, + deletePeerPhoto, deleteQuickReply, deleteQuickReplyMessages, deleteTopic, @@ -1069,6 +1070,10 @@ export function deleteMessages( return; } + if (message.content.action?.photo) { + global = deletePeerPhoto(global, chatId, message.content.action.photo.id, true); + } + global = updateThreadUnread(global, actions, message, true); const threadId = selectThreadIdFromMessage(global, message); @@ -1144,6 +1149,10 @@ export function deleteMessages( } } + if (message?.content.action?.photo) { + global = deletePeerPhoto(global, commonBoxChatId, message.content.action.photo.id, true); + } + setTimeout(() => { global = getGlobal(); global = deleteChatMessages(global, commonBoxChatId, [id]); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 226faccfe..e505b9673 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -104,22 +104,22 @@ export function getChatLink(chat: ApiChat) { export function getChatAvatarHash( owner: ApiPeer, size: 'normal' | 'big' = 'normal', - avatarHash = owner.avatarHash, + avatarPhotoId = owner.avatarPhotoId, ) { - if (!avatarHash) { + if (!avatarPhotoId) { return undefined; } switch (size) { case 'big': - return `profile${owner.id}?${avatarHash}`; + return `profile${owner.id}?${avatarPhotoId}`; default: - return `avatar${owner.id}?${avatarHash}`; + return `avatar${owner.id}?${avatarPhotoId}`; } } export function isChatAdmin(chat: ApiChat) { - return Boolean(chat.adminRights); + return Boolean(chat.adminRights || chat.isCreator); } export function getHasAdminRight(chat: ApiChat, key: keyof ApiChatAdminRights) { diff --git a/src/global/helpers/messageMedia.ts b/src/global/helpers/messageMedia.ts index cacfcaa4e..c129b4a4c 100644 --- a/src/global/helpers/messageMedia.ts +++ b/src/global/helpers/messageMedia.ts @@ -284,14 +284,18 @@ export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: Target, case 'preview': return `${base}?size=${isAction ? 'b' : 'x'}`; case 'download': - return !isVideo ? base : getVideoAvatarMediaHash(photo); + return !isVideo ? base : getVideoProfilePhotoMediaHash(photo); case 'full': default: return base; } } -export function getVideoAvatarMediaHash(photo: ApiPhoto) { +export function getProfilePhotoMediaHash(photo: ApiPhoto) { + return `photo${photo.id}?size=c`; +} + +export function getVideoProfilePhotoMediaHash(photo: ApiPhoto) { if (!photo.isVideo) return undefined; return `photo${photo.id}?size=u`; } diff --git a/src/global/reducers/chats.ts b/src/global/reducers/chats.ts index a0b1e6e65..87845e6c4 100644 --- a/src/global/reducers/chats.ts +++ b/src/global/reducers/chats.ts @@ -1,5 +1,5 @@ import type { - ApiChat, ApiChatFullInfo, ApiChatMember, ApiPhoto, ApiTopic, + ApiChat, ApiChatFullInfo, ApiChatMember, ApiTopic, } from '../../api/types'; import type { ChatListType, GlobalState } from '../types'; @@ -138,12 +138,11 @@ export function removeUnreadMentions( } export function updateChat( - global: T, chatId: string, chatUpdate: Partial, photo?: ApiPhoto, - noOmitUnreadReactionCount = false, + global: T, chatId: string, chatUpdate: Partial, noOmitUnreadReactionCount = false, ): T { const { byId } = global.chats; - const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo, noOmitUnreadReactionCount); + const updatedChat = getUpdatedChat(global, chatId, chatUpdate, noOmitUnreadReactionCount); if (!updatedChat) { return global; } @@ -257,7 +256,7 @@ export function addChats(global: T, newById: Record( - global: T, chatId: string, chatUpdate: Partial, photo?: ApiPhoto, + global: T, chatId: string, chatUpdate: Partial, noOmitUnreadReactionCount = false, ) { const { byId } = global.chats; @@ -279,7 +278,6 @@ function getUpdatedChat( const updatedChat: ApiChat = { ...chat, ...omit(chatUpdate, omitProps), - ...(photo && { photos: [photo, ...(chat.photos || [])] }), } as ApiChat; if (!updatedChat.id || !updatedChat.type) { diff --git a/src/global/reducers/general.ts b/src/global/reducers/general.ts index fde900ab5..bbd361744 100644 --- a/src/global/reducers/general.ts +++ b/src/global/reducers/general.ts @@ -1,9 +1,11 @@ import type { - ApiChat, ApiChatFullInfo, ApiUser, ApiUserFullInfo, + ApiChat, ApiChatFullInfo, ApiPhoto, ApiUser, ApiUserFullInfo, } from '../../api/types'; import type { GlobalState } from '../types'; -import { isUserId } from '../helpers'; +import { uniqueByField } from '../../util/iteratees'; +import { isChatChannel, isUserId } from '../helpers'; +import { selectChatFullInfo, selectPeer, selectUserFullInfo } from '../selectors'; import { updateChat, updateChatFullInfo } from './chats'; import { updateUser, updateUserFullInfo } from './users'; @@ -30,3 +32,137 @@ export function updatePeerFullInfo( return updateChatFullInfo(global, peerId, peerFullInfoUpdate); } + +export function updatePeerPhotosIsLoading( + global: T, + peerId: string, + isLoading: boolean, +) { + const peer = selectPeer(global, peerId); + if (!peer || !peer.profilePhotos) { + return global; + } + + return updatePeer(global, peerId, { + profilePhotos: { + ...peer.profilePhotos, + isLoading, + }, + }); +} + +export function updatePeerPhotos( + global: T, + peerId: string, + update: { + newPhotos: ApiPhoto[]; + count: number; + nextOffset?: number; + fullInfo: ApiChatFullInfo | ApiUserFullInfo; + shouldInvalidateCache?: boolean; + }, +) { + const peer = selectPeer(global, peerId); + if (!peer) { + return global; + } + + const { + newPhotos, count, nextOffset, fullInfo, shouldInvalidateCache, + } = update; + const currentPhotos = peer.profilePhotos; + const profilePhoto = fullInfo.profilePhoto; + const fallbackPhoto = 'fallbackPhoto' in fullInfo ? fullInfo.fallbackPhoto : undefined; + const personalPhoto = 'personalPhoto' in fullInfo ? fullInfo.personalPhoto : undefined; + + if (!currentPhotos || shouldInvalidateCache) { + // In some channels, last service message with photo change is deleted, so we need to patch it in + if (profilePhoto && profilePhoto.id !== newPhotos[0]?.id) { + newPhotos.unshift(profilePhoto); + } + + if (personalPhoto && personalPhoto.id !== newPhotos[0]?.id) { + newPhotos.unshift(personalPhoto); + } + + if (fallbackPhoto) { + newPhotos.push(fallbackPhoto); + } + + return updatePeer(global, peerId, { + profilePhotos: { + fallbackPhoto, + personalPhoto, + photos: newPhotos, + count, + nextOffset, + isLoading: false, + }, + }); + } + + const hasFallbackPhoto = currentPhotos.photos[currentPhotos.photos.length - 1].id === fallbackPhoto?.id; + 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, + }, + }); +} + +export function deletePeerPhoto( + global: T, + peerId: string, + photoId: string, + isFromActionMessage?: boolean, +) { + const peer = selectPeer(global, peerId); + if (!peer || !peer.profilePhotos) { + return global; + } + const isChannel = 'title' in peer && isChatChannel(peer); + + const userFullInfo = selectUserFullInfo(global, peerId); + const chatFullInfo = selectChatFullInfo(global, peerId); + + const isAvatar = peer.avatarPhotoId === photoId && (!isChannel || isFromActionMessage); + const nextAvatarPhoto = isAvatar ? peer.profilePhotos.photos[1] : undefined; + + if (userFullInfo) { + const newFallbackPhoto = userFullInfo.fallbackPhoto?.id === photoId ? undefined : userFullInfo.fallbackPhoto; + const newPersonalPhoto = userFullInfo.personalPhoto?.id === photoId ? undefined : userFullInfo.personalPhoto; + const newProfilePhoto = userFullInfo.profilePhoto?.id === photoId ? nextAvatarPhoto : userFullInfo.profilePhoto; + global = updateUserFullInfo(global, peerId, { + fallbackPhoto: newFallbackPhoto, + personalPhoto: newPersonalPhoto, + profilePhoto: newProfilePhoto, + }); + } + + if (chatFullInfo) { + const newProfilePhoto = chatFullInfo.profilePhoto?.id === photoId ? nextAvatarPhoto : chatFullInfo.profilePhoto; + global = updateChatFullInfo(global, peerId, { + profilePhoto: newProfilePhoto, + }); + } + + 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, { + avatarPhotoId, + profilePhotos: avatarPhotoId ? { + ...peer.profilePhotos, + photos, + count: peer.profilePhotos.count - 1, + } : undefined, + }); +} diff --git a/src/global/reducers/reactions.ts b/src/global/reducers/reactions.ts index b2c11245e..297c1751b 100644 --- a/src/global/reducers/reactions.ts +++ b/src/global/reducers/reactions.ts @@ -77,5 +77,5 @@ export function addMessageReaction( export function updateUnreadReactions( global: T, chatId: string, update: Pick, ): T { - return updateChat(global, chatId, update, undefined, true); + return updateChat(global, chatId, update, true); } diff --git a/src/global/types.ts b/src/global/types.ts index cb072d161..7fa3182e1 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2608,8 +2608,10 @@ export interface ActionPayloads { isMuted?: boolean; shouldSharePhoneNumber?: boolean; } & WithTabId; - loadProfilePhotos: { - profileId: string; + loadMoreProfilePhotos: { + peerId: string; + isPreload?: boolean; + shouldInvalidateCache?: boolean; }; deleteProfilePhoto: { photo: ApiPhoto;