Avatar: Display more than 40 profile picture (#4780)

This commit is contained in:
zubiden 2024-08-06 20:06:18 +02:00 committed by Alexander Zinchuk
parent 8ced8b3b05
commit 43244b6e61
37 changed files with 492 additions and 296 deletions

View File

@ -24,7 +24,7 @@ import type {
import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees'; import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { serializeBytes } from '../helpers'; import { serializeBytes } from '../helpers';
import { buildApiUsernames } from './common'; import { buildApiUsernames, buildAvatarPhotoId } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { import {
buildApiEmojiStatus, buildApiEmojiStatus,
@ -50,7 +50,7 @@ function buildApiChatFieldsFromPeerEntity(
const accessHash = ('accessHash' in peerEntity) ? String(peerEntity.accessHash) : undefined; const accessHash = ('accessHash' in peerEntity) ? String(peerEntity.accessHash) : undefined;
const hasVideoAvatar = 'photo' in peerEntity && peerEntity.photo && 'hasVideo' in peerEntity.photo const hasVideoAvatar = 'photo' in peerEntity && peerEntity.photo && 'hasVideo' in peerEntity.photo
&& peerEntity.photo.hasVideo; && 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 isSignaturesShown = Boolean('signatures' in peerEntity && peerEntity.signatures);
const hasPrivateLink = Boolean('hasLink' in peerEntity && peerEntity.hasLink); const hasPrivateLink = Boolean('hasLink' in peerEntity && peerEntity.hasLink);
const isScam = Boolean('scam' in peerEntity && peerEntity.scam); const isScam = Boolean('scam' in peerEntity && peerEntity.scam);
@ -74,7 +74,7 @@ function buildApiChatFieldsFromPeerEntity(
usernames, usernames,
accessHash, accessHash,
hasVideoAvatar, hasVideoAvatar,
avatarHash, avatarPhotoId,
...('verified' in peerEntity && { isVerified: peerEntity.verified }), ...('verified' in peerEntity && { isVerified: peerEntity.verified }),
...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }), ...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }),
...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }), ...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }),
@ -308,14 +308,6 @@ function getUserName(user: GramJs.User) {
: (user.lastName || ''); : (user.lastName || '');
} }
export function buildAvatarHash(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) {
if ('photoId' in photo) {
return String(photo.photoId);
}
return undefined;
}
export function buildChatMember( export function buildChatMember(
member: GramJs.TypeChatParticipant | GramJs.TypeChannelParticipant, member: GramJs.TypeChatParticipant | GramJs.TypeChannelParticipant,
): ApiChatMember | undefined { ): ApiChatMember | undefined {

View File

@ -277,3 +277,11 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess
length, length,
}; };
} }
export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) {
if ('photoId' in photo) {
return photo.photoId.toString();
}
return undefined;
}

View File

@ -14,8 +14,7 @@ import type {
StatisticsStoryInteractionCounter, StatisticsStoryInteractionCounter,
} from '../../types'; } from '../../types';
import { buildAvatarHash } from './chats'; import { buildApiUsernames, buildAvatarPhotoId } from './common';
import { buildApiUsernames } from './common';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics { 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 { function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs.TypeChat[]): ApiMessagePublicForward {
const peerId = getApiChatIdFromMtpPeer(message.peerId!); const peerId = getApiChatIdFromMtpPeer(message.peerId!);
const channel = chats.find((c) => buildApiPeerId(c.id, 'channel') === 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 { return {
messageId: message.id, messageId: message.id,
@ -243,9 +244,8 @@ function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs
type: 'chatTypeChannel', type: 'chatTypeChannel',
title: (channel as GramJs.Channel).title, title: (channel as GramJs.Channel).title,
usernames: buildApiUsernames(channel as GramJs.Channel), usernames: buildApiUsernames(channel as GramJs.Channel),
avatarHash: channel && 'photo' in channel avatarPhotoId: channelProfilePhoto && buildAvatarPhotoId(channelProfilePhoto),
? buildAvatarHash((channel as GramJs.Channel).photo) hasVideoAvatar: Boolean(channelProfilePhoto?.videoSizes),
: undefined,
}, },
}; };
} }

View File

@ -11,7 +11,7 @@ import type {
import { buildApiBotInfo } from './bots'; import { buildApiBotInfo } from './bots';
import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business';
import { buildApiPhoto, buildApiUsernames } from './common'; import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers';
@ -61,12 +61,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
const { const {
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId, id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
} = mtpUser; } = mtpUser;
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
? Boolean(mtpUser.photo.hasVideo) const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
: undefined;
const avatarHash = mtpUser.photo instanceof GramJs.UserProfilePhoto
? String(mtpUser.photo.photoId)
: undefined;
const userType = buildApiUserType(mtpUser); const userType = buildApiUserType(mtpUser);
const usernames = buildApiUsernames(mtpUser); const usernames = buildApiUsernames(mtpUser);
const emojiStatus = mtpUser.emojiStatus ? buildApiEmojiStatus(mtpUser.emojiStatus) : undefined; const emojiStatus = mtpUser.emojiStatus ? buildApiEmojiStatus(mtpUser.emojiStatus) : undefined;
@ -90,7 +86,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
phoneNumber: mtpUser.phone || '', phoneNumber: mtpUser.phone || '',
noStatus: !mtpUser.status, noStatus: !mtpUser.status,
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }), ...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
...(avatarHash && { avatarHash }), avatarPhotoId,
emojiStatus, emojiStatus,
hasVideoAvatar, hasVideoAvatar,
areStoriesHidden: Boolean(mtpUser.storiesHidden), areStoriesHidden: Boolean(mtpUser.storiesHidden),

View File

@ -6,7 +6,7 @@ import type {
ApiUser, OnApiUpdate, ApiUser, OnApiUpdate,
} from '../../types'; } from '../../types';
import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config'; import { COMMON_CHATS_LIMIT } from '../../../config';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPhoto } from '../apiBuilders/common';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
@ -85,10 +85,7 @@ export async function fetchFullUser({
onUpdate({ onUpdate({
'@type': 'updateUser', '@type': 'updateUser',
id, id,
user: { user,
...user,
avatarHash: user?.avatarHash || undefined,
},
fullInfo, 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) { if (user) {
const { id, accessHash } = user; const { id, accessHash } = user;
const result = await invokeRequest(new GramJs.photos.GetUserPhotos({ const result = await invokeRequest(new GramJs.photos.GetUserPhotos({
userId: buildInputEntity(id, accessHash) as GramJs.InputUser, userId: buildInputEntity(id, accessHash) as GramJs.InputUser,
limit: PROFILE_PHOTOS_LIMIT, limit,
offset: 0, offset,
maxId: BigInt('0'), maxId: BigInt('0'),
})); }));
@ -272,11 +279,17 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
updateLocalDb(result); 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 { return {
count,
photos: result.photos photos: result.photos
.filter((photo): photo is GramJs.Photo => photo instanceof GramJs.Photo) .filter((photo): photo is GramJs.Photo => photo instanceof GramJs.Photo)
.map((photo) => buildApiPhoto(photo)), .map((photo) => buildApiPhoto(photo)),
users: result.users.map(buildApiUser).filter(Boolean), users: result.users.map(buildApiUser).filter(Boolean),
nextOffsetId,
}; };
} }
@ -285,18 +298,22 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
const result = await searchMessagesLocal({ const result = await searchMessagesLocal({
chat: chat!, chat: chat!,
type: 'profilePhoto', type: 'profilePhoto',
limit: PROFILE_PHOTOS_LIMIT, limit,
}); });
if (!result) { if (!result) {
return undefined; return undefined;
} }
const { messages, users } = result; const {
messages, users, totalCount, nextOffsetId,
} = result;
return { return {
count: totalCount,
photos: messages.map((message) => message.content.action!.photo).filter(Boolean), photos: messages.map((message) => message.content.action!.photo).filter(Boolean),
users, users,
nextOffsetId,
}; };
} }

View File

@ -20,12 +20,13 @@ import {
buildApiChatFolder, buildApiChatFolder,
buildApiChatFromPreview, buildApiChatFromPreview,
buildApiChatSettings, buildApiChatSettings,
buildAvatarHash,
buildChatMember, buildChatMember,
buildChatMembers, buildChatMembers,
buildChatTypingStatus, buildChatTypingStatus,
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuilders/common'; import {
buildApiPhoto, buildApiUsernames, buildPrivacyRules,
} from '../apiBuilders/common';
import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers';
import { import {
buildApiMessageExtendedMediaPreview, buildApiMessageExtendedMediaPreview,
@ -244,8 +245,10 @@ export function updater(update: Update) {
}, },
}); });
} else if (action instanceof GramJs.MessageActionChatEditPhoto) { } 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 photo = buildChatPhotoForLocalDb(action.photo);
const avatarHash = buildAvatarHash(photo);
const localDbChatId = resolveMessageApiChatId(update.message)!; const localDbChatId = resolveMessageApiChatId(update.message)!;
if (localDb.chats[localDbChatId]) { if (localDb.chats[localDbChatId]) {
@ -253,16 +256,11 @@ export function updater(update: Update) {
} }
addPhotoToLocalDb(action.photo); addPhotoToLocalDb(action.photo);
if (avatarHash) { onUpdate({
onUpdate({ '@type': 'updateNewProfilePhoto',
'@type': 'updateChat', peerId: message.chatId,
id: message.chatId, photo: apiPhoto,
chat: { });
avatarHash,
},
...(action.photo instanceof GramJs.Photo && { newProfilePhoto: buildApiPhoto(action.photo) }),
});
}
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) { } else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
const localDbChatId = resolveMessageApiChatId(update.message)!; const localDbChatId = resolveMessageApiChatId(update.message)!;
if (localDb.chats[localDbChatId]) { if (localDb.chats[localDbChatId]) {
@ -270,9 +268,8 @@ export function updater(update: Update) {
} }
onUpdate({ onUpdate({
'@type': 'updateChat', '@type': 'updateDeleteProfilePhoto',
id: message.chatId, peerId: message.chatId,
chat: { avatarHash: undefined },
}); });
} else if (action instanceof GramJs.MessageActionChatDeleteUser) { } else if (action instanceof GramJs.MessageActionChatDeleteUser) {
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle

View File

@ -3,7 +3,7 @@ import type { ApiBotCommand } from './bots';
import type { import type {
ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet,
} from './messages'; } from './messages';
import type { ApiChatInviteImporter } from './misc'; import type { ApiChatInviteImporter, ApiPeerPhotos } from './misc';
import type { import type {
ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername,
} from './users'; } from './users';
@ -35,12 +35,12 @@ export interface ApiChat {
accessHash?: string; accessHash?: string;
isMin?: boolean; isMin?: boolean;
hasVideoAvatar?: boolean; hasVideoAvatar?: boolean;
avatarHash?: string; avatarPhotoId?: string;
usernames?: ApiUsername[]; usernames?: ApiUsername[];
membersCount?: number; membersCount?: number;
creationDate?: number; creationDate?: number;
isSupport?: true; isSupport?: true;
photos?: ApiPhoto[]; profilePhotos?: ApiPeerPhotos;
draftDate?: number; draftDate?: number;
isProtected?: boolean; isProtected?: boolean;
fakeType?: ApiFakeType; fakeType?: ApiFakeType;

View File

@ -277,3 +277,12 @@ export interface ApiCollectionInfo {
purchaseDate: number; purchaseDate: number;
url: string; url: string;
} }
export interface ApiPeerPhotos {
fallbackPhoto?: ApiPhoto;
personalPhoto?: ApiPhoto;
photos: ApiPhoto[];
count: number;
nextOffset?: number;
isLoading?: boolean;
}

View File

@ -100,7 +100,6 @@ export type ApiUpdateChat = {
'@type': 'updateChat'; '@type': 'updateChat';
id: string; id: string;
chat: Partial<ApiChat>; chat: Partial<ApiChat>;
newProfilePhoto?: ApiPhoto;
noTopChatsRequest?: boolean; noTopChatsRequest?: boolean;
}; };
@ -742,6 +741,18 @@ export type ApiUpdateStarsBalance = {
balance: number; balance: number;
}; };
export type ApiUpdateDeleteProfilePhoto = {
'@type': 'updateDeleteProfilePhoto';
peerId: string;
photoId?: string;
};
export type ApiUpdateNewProfilePhoto = {
'@type': 'updateNewProfilePhoto';
peerId: string;
photo: ApiPhoto;
};
export type ApiUpdate = ( export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
@ -773,7 +784,8 @@ export type ApiUpdate = (
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance |
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages |
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -3,6 +3,7 @@ import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business'; import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiPeerColor } from './chats'; import type { ApiPeerColor } from './chats';
import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiPeerPhotos } from './misc';
export interface ApiUser { export interface ApiUser {
id: string; id: string;
@ -21,8 +22,8 @@ export interface ApiUser {
phoneNumber: string; phoneNumber: string;
accessHash?: string; accessHash?: string;
hasVideoAvatar?: boolean; hasVideoAvatar?: boolean;
avatarHash?: string; avatarPhotoId?: string;
photos?: ApiPhoto[]; profilePhotos?: ApiPeerPhotos;
botPlaceholder?: string; botPlaceholder?: string;
canBeInvitedToGroup?: boolean; canBeInvitedToGroup?: boolean;
commonChats?: { commonChats?: {

View File

@ -17,7 +17,7 @@ import {
getChatTitle, getChatTitle,
getPeerStoryHtmlId, getPeerStoryHtmlId,
getUserFullName, getUserFullName,
getVideoAvatarMediaHash, getVideoProfilePhotoMediaHash,
getWebDocumentHash, getWebDocumentHash,
isAnonymousForwardsChat, isAnonymousForwardsChat,
isChatWithRepliesBot, isChatWithRepliesBot,
@ -121,7 +121,7 @@ const Avatar: FC<OwnProps> = ({
} else if (photo) { } else if (photo) {
imageHash = `photo${photo.id}?size=m`; imageHash = `photo${photo.id}?size=m`;
if (photo.isVideo && withVideo) { if (photo.isVideo && withVideo) {
videoHash = getVideoAvatarMediaHash(photo); videoHash = getVideoProfilePhotoMediaHash(photo);
} }
} else if (webPhoto) { } else if (webPhoto) {
imageHash = getWebDocumentHash(webPhoto); imageHash = getWebDocumentHash(webPhoto);

View File

@ -109,7 +109,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
const { const {
loadFullChat, loadFullChat,
openMediaViewer, openMediaViewer,
loadProfilePhotos, loadMoreProfilePhotos,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -121,9 +121,9 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (chatId && !isMin) { if (chatId && !isMin) {
if (withFullInfo) loadFullChat({ chatId }); 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( const handleAvatarViewerOpen = useLastCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => { (e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {

View File

@ -102,7 +102,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
const { const {
loadFullUser, loadFullUser,
openMediaViewer, openMediaViewer,
loadProfilePhotos, loadMoreProfilePhotos,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -112,7 +112,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (userId) { if (userId) {
if (withFullInfo && isSynced) loadFullUser({ userId }); if (withFullInfo && isSynced) loadFullUser({ userId });
if (withMediaViewer) loadProfilePhotos({ profileId: userId }); if (withMediaViewer) loadMoreProfilePhotos({ peerId: userId, isPreload: true });
} }
}, [userId, withFullInfo, withMediaViewer, isSynced]); }, [userId, withFullInfo, withMediaViewer, isSynced]);

View File

@ -3,22 +3,19 @@ import React, { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiPhoto, ApiSticker, ApiTopic, ApiUser, ApiUserStatus, ApiChat, ApiSticker, ApiTopic, ApiUser, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { import {
getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserId, isUserOnline, getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserOnline,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectChat, selectChat,
selectChatFullInfo,
selectCurrentMessageList, selectCurrentMessageList,
selectTabState, selectTabState,
selectThreadMessagesCount, selectThreadMessagesCount,
selectUser, selectUser,
selectUserFullInfo,
selectUserStatus, selectUserStatus,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -30,7 +27,6 @@ import renderText from './helpers/renderText';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import { useStateRef } from '../../hooks/useStateRef';
import usePhotosPreload from './hooks/usePhotosPreload'; import usePhotosPreload from './hooks/usePhotosPreload';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
@ -43,7 +39,7 @@ import './ProfileInfo.scss';
import styles from './ProfileInfo.module.scss'; import styles from './ProfileInfo.module.scss';
type OwnProps = { type OwnProps = {
userId: string; peerId: string;
forceShowSelf?: boolean; forceShowSelf?: boolean;
canPlayVideo: boolean; canPlayVideo: boolean;
}; };
@ -57,16 +53,13 @@ type StateProps =
avatarOwnerId?: string; avatarOwnerId?: string;
topic?: ApiTopic; topic?: ApiTopic;
messagesCount?: number; messagesCount?: number;
userPersonalPhoto?: ApiPhoto;
userProfilePhoto?: ApiPhoto;
userFallbackPhoto?: ApiPhoto;
chatProfilePhoto?: ApiPhoto;
emojiStatusSticker?: ApiSticker; emojiStatusSticker?: ApiSticker;
} };
& Pick<GlobalState, 'isSynced'>;
const EMOJI_STATUS_SIZE = 24; const EMOJI_STATUS_SIZE = 24;
const EMOJI_TOPIC_SIZE = 120; const EMOJI_TOPIC_SIZE = 120;
const LOAD_MORE_THRESHOLD = 3;
const MAX_PHOTO_DASH_COUNT = 30;
const ProfileInfo: FC<OwnProps & StateProps> = ({ const ProfileInfo: FC<OwnProps & StateProps> = ({
forceShowSelf, forceShowSelf,
@ -74,33 +67,28 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
user, user,
userStatus, userStatus,
chat, chat,
isSynced,
mediaIndex, mediaIndex,
avatarOwnerId, avatarOwnerId,
topic, topic,
messagesCount, messagesCount,
userPersonalPhoto,
userProfilePhoto,
userFallbackPhoto,
chatProfilePhoto,
emojiStatusSticker, emojiStatusSticker,
peerId,
}) => { }) => {
const { const {
loadFullUser,
openMediaViewer, openMediaViewer,
openPremiumModal, openPremiumModal,
openStickerSet, openStickerSet,
openPrivacySettingsNoticeModal, openPrivacySettingsNoticeModal,
loadMoreProfilePhotos,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
const { id: userId } = user || {}; const userProfilePhotos = user?.profilePhotos;
const { id: chatId } = chat || {}; const chatProfilePhotos = chat?.profilePhotos;
const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY; const photos = userProfilePhotos?.photos || chatProfilePhotos?.photos || MEMO_EMPTY_ARRAY;
const prevMediaIndex = usePrevious(mediaIndex); const prevMediaIndex = usePrevious(mediaIndex);
const prevAvatarOwnerId = usePrevious(avatarOwnerId); const prevAvatarOwnerId = usePrevious(avatarOwnerId);
const mediaIndexRef = useStateRef(mediaIndex);
const [hasSlideAnimation, setHasSlideAnimation] = useState(true); const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
// slideOptimized doesn't work well when animation is dynamically disabled // slideOptimized doesn't work well when animation is dynamically disabled
const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none'; const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none';
@ -109,6 +97,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const isFirst = photos.length <= 1 || currentPhotoIndex === 0; const isFirst = photos.length <= 1 || currentPhotoIndex === 0;
const isLast = photos.length <= 1 || currentPhotoIndex === photos.length - 1; 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 // Set the current avatar photo to the last selected photo in Media Viewer after it is closed
useEffect(() => { useEffect(() => {
if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) { if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) {
@ -117,12 +111,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
}, [mediaIndex, prevMediaIndex, prevAvatarOwnerId]); }, [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 // Deleting the last profile photo may result in an error
useEffect(() => { useEffect(() => {
if (currentPhotoIndex > photos.length) { if (currentPhotoIndex > photos.length) {
@ -131,32 +119,26 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
}, [currentPhotoIndex, photos.length]); }, [currentPhotoIndex, photos.length]);
useEffect(() => {
if (isSynced && userId && !forceShowSelf) {
loadFullUser({ userId });
}
}, [userId, loadFullUser, isSynced, forceShowSelf]);
usePhotosPreload(photos, currentPhotoIndex); usePhotosPreload(photos, currentPhotoIndex);
const handleProfilePhotoClick = useLastCallback(() => { const handleProfilePhotoClick = useLastCallback(() => {
openMediaViewer({ openMediaViewer({
isAvatarView: true, isAvatarView: true,
chatId: userId || chatId, chatId: peerId,
mediaIndex: currentPhotoIndex, mediaIndex: currentPhotoIndex,
origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
}); });
}); });
const handleStatusClick = useLastCallback(() => { const handleStatusClick = useLastCallback(() => {
if (!userId) { if (!peerId) {
openStickerSet({ openStickerSet({
stickerSetInfo: emojiStatusSticker!.stickerSetInfo, stickerSetInfo: emojiStatusSticker!.stickerSetInfo,
}); });
return; return;
} }
openPremiumModal({ fromUserId: userId }); openPremiumModal({ fromUserId: peerId });
}); });
const selectPreviousMedia = useLastCallback(() => { const selectPreviousMedia = useLastCallback(() => {
@ -231,14 +213,18 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
function renderPhotoTabs() { 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; 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 ( return (
<div className={styles.photoDashes}> <div className={styles.photoDashes}>
{photos.map((_, i) => ( {enumerator.map((_, i) => (
<span className={buildClassName(styles.photoDash, i === currentPhotoIndex && styles.photoDash_current)} /> <span className={buildClassName(styles.photoDash, i === activeDashIndex && styles.photoDash_current)} />
))} ))}
</div> </div>
); );
@ -248,14 +234,13 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const photo = photos.length > 0 const photo = photos.length > 0
? photos[currentPhotoIndex] ? photos[currentPhotoIndex]
: undefined; : undefined;
const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto;
return ( return (
<ProfilePhoto <ProfilePhoto
key={currentPhotoIndex} key={currentPhotoIndex}
user={user} user={user}
chat={chat} chat={chat}
photo={profilePhoto} photo={photo}
canPlayVideo={Boolean(isActive && canPlayVideo)} canPlayVideo={Boolean(isActive && canPlayVideo)}
onClick={handleProfilePhotoClick} onClick={handleProfilePhotoClick}
/> />
@ -263,8 +248,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
function renderStatus() { function renderStatus() {
const peerId = (chatId || userId)!;
const isAnonymousForwards = isAnonymousForwardsChat(peerId); const isAnonymousForwards = isAnonymousForwardsChat(peerId);
if (isAnonymousForwards) return undefined; if (isAnonymousForwards) return undefined;
@ -310,18 +293,18 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
> >
<div className={styles.photoWrapper}> <div className={styles.photoWrapper}>
{renderPhotoTabs()} {renderPhotoTabs()}
{!forceShowSelf && userPersonalPhoto && ( {!forceShowSelf && userProfilePhotos?.personalPhoto && (
<div className={buildClassName( <div className={buildClassName(
styles.fallbackPhoto, styles.fallbackPhoto,
isFirst && styles.fallbackPhotoVisible, isFirst && styles.fallbackPhotoVisible,
)} )}
> >
<div className={styles.fallbackPhotoContents}> <div className={styles.fallbackPhotoContents}>
{lang(userPersonalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')} {lang(userProfilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
</div> </div>
</div> </div>
)} )}
{forceShowSelf && userFallbackPhoto && ( {forceShowSelf && userProfilePhotos?.fallbackPhoto && (
<div className={buildClassName( <div className={buildClassName(
styles.fallbackPhoto, styles.fallbackPhoto,
(isFirst || isLast) && styles.fallbackPhotoVisible, (isFirst || isLast) && styles.fallbackPhotoVisible,
@ -330,12 +313,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}> <div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
{!isLast && ( {!isLast && (
<Avatar <Avatar
photo={userFallbackPhoto} photo={userProfilePhotos.fallbackPhoto}
className={styles.fallbackPhotoAvatar} className={styles.fallbackPhotoAvatar}
size="mini" size="mini"
/> />
)} )}
{lang(userFallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')} {lang(userProfilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
</div> </div>
</div> </div>
)} )}
@ -381,37 +364,28 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => { (global, { peerId }): StateProps => {
const { isSynced } = global; const user = selectUser(global, peerId);
const user = selectUser(global, userId); const userStatus = selectUserStatus(global, peerId);
const isPrivate = isUserId(userId); const chat = selectChat(global, peerId);
const userStatus = selectUserStatus(global, userId);
const chat = selectChat(global, userId);
const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum; const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; 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 emojiStatus = (user || chat)?.emojiStatus;
const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined;
return { return {
isSynced,
user, user,
userStatus, userStatus,
chat, chat,
userPersonalPhoto: userFullInfo?.personalPhoto,
userProfilePhoto: userFullInfo?.profilePhoto,
userFallbackPhoto: userFullInfo?.fallbackPhoto,
chatProfilePhoto: chatFullInfo?.profilePhoto,
mediaIndex, mediaIndex,
avatarOwnerId, avatarOwnerId,
emojiStatusSticker, emojiStatusSticker,
...(topic && { ...(topic && {
topic, topic,
messagesCount: selectThreadMessagesCount(global, userId, currentTopicId!), messagesCount: selectThreadMessagesCount(global, peerId, currentTopicId!),
}), }),
}; };
}, },

View File

@ -8,8 +8,10 @@ import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
import { import {
getChatAvatarHash, getChatAvatarHash,
getChatTitle, getChatTitle,
getPhotoMediaHash,
getProfilePhotoMediaHash,
getUserFullName, getUserFullName,
getVideoAvatarMediaHash, getVideoProfilePhotoMediaHash,
isAnonymousForwardsChat, isAnonymousForwardsChat,
isChatWithRepliesBot, isChatWithRepliesBot,
isDeletedUser, isDeletedUser,
@ -62,28 +64,30 @@ const ProfilePhoto: FC<OwnProps> = ({
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id); const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
const isAnonymousForwards = chat && isAnonymousForwardsChat(chat.id); const isAnonymousForwards = chat && isAnonymousForwardsChat(chat.id);
const peer = user || chat; const peer = (user || chat)!;
const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat && !isAnonymousForwards; const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat && !isAnonymousForwards;
const { isVideo } = photo || {}; const { isVideo } = photo || {};
const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal'); const avatarHash = (!photo || photo.id === peer.avatarPhotoId) && getChatAvatarHash(peer, 'normal');
const avatarBlobUrl = useMedia(avatarHash);
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 photoBlobUrl = useMedia(photoHash);
const videoHash = canHaveMedia && photo && isVideo && getVideoAvatarMediaHash(photo); const videoHash = canHaveMedia && photo && isVideo && getVideoProfilePhotoMediaHash(photo);
const videoBlobUrl = useMedia(videoHash); const videoBlobUrl = useMedia(videoHash);
const fullMediaData = videoBlobUrl || photoBlobUrl; const fullMediaData = videoBlobUrl || photoBlobUrl;
const [isVideoReady, markVideoReady] = useFlag(); const [isVideoReady, markVideoReady] = useFlag();
const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady)); const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady));
const transitionClassNames = useMediaTransition(isFullMediaReady); const transitionClassNames = useMediaTransition(isFullMediaReady);
const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && photo?.thumbnail?.dataUri; const isBlurredThumb = canHaveMedia && !isFullMediaReady && !previewBlobUrl && photo?.thumbnail?.dataUri;
const blurredThumbCanvasRef = useCanvasBlur( const blurredThumbCanvasRef = useCanvasBlur(
photo?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED, photo?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED,
); );
const hasMedia = photo || avatarBlobUrl || isBlurredThumb; const hasMedia = photo || previewBlobUrl || isBlurredThumb;
useEffect(() => { useEffect(() => {
if (videoRef.current && !canPlayVideo) { if (videoRef.current && !canPlayVideo) {
@ -121,7 +125,7 @@ const ProfilePhoto: FC<OwnProps> = ({
{isBlurredThumb ? ( {isBlurredThumb ? (
<canvas ref={blurredThumbCanvasRef} className="thumb canvas-blur-setup" /> <canvas ref={blurredThumbCanvasRef} className="thumb canvas-blur-setup" />
) : ( ) : (
<img src={avatarBlobUrl} draggable={false} className="thumb" alt="" /> <img src={previewBlobUrl} draggable={false} className="thumb" alt="" />
)} )}
{photo && ( {photo && (
isVideo ? ( isVideo ? (

View File

@ -3,6 +3,7 @@ import { useEffect } from '../../../lib/teact/teact';
import type { ApiPhoto } from '../../../api/types'; import type { ApiPhoto } from '../../../api/types';
import { ApiMediaFormat } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types';
import { getProfilePhotoMediaHash } from '../../../global/helpers';
import * as mediaLoader from '../../../util/mediaLoader'; import * as mediaLoader from '../../../util/mediaLoader';
const PHOTOS_TO_PRELOAD = 4; const PHOTOS_TO_PRELOAD = 4;
@ -13,9 +14,10 @@ export default function usePhotosPreload(
) { ) {
useEffect(() => { useEffect(() => {
photos.slice(currentIndex, currentIndex + PHOTOS_TO_PRELOAD).forEach((photo) => { 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) { if (!mediaData) {
mediaLoader.fetch(`photo${photo.id}?size=c`, ApiMediaFormat.BlobUrl); mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl);
} }
}); });
}, [currentIndex, photos]); }, [currentIndex, photos]);

View File

@ -49,7 +49,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
onReset, onReset,
}) => { }) => {
const { const {
loadProfilePhotos, loadMoreProfilePhotos,
openPremiumModal, openPremiumModal,
openSupportChat, openSupportChat,
openUrl, openUrl,
@ -63,9 +63,9 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (currentUserId) { if (currentUserId) {
loadProfilePhotos({ profileId: currentUserId }); loadMoreProfilePhotos({ peerId: currentUserId, isPreload: true });
} }
}, [currentUserId, loadProfilePhotos]); }, [currentUserId]);
useHistoryBack({ useHistoryBack({
isActive, isActive,
@ -82,7 +82,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
<div className="settings-main-menu"> <div className="settings-main-menu">
{currentUserId && ( {currentUserId && (
<ProfileInfo <ProfileInfo
userId={currentUserId} peerId={currentUserId}
canPlayVideo={Boolean(isActive)} canPlayVideo={Boolean(isActive)}
forceShowSelf forceShowSelf
/> />

View File

@ -80,6 +80,7 @@ type StateProps = {
}; };
const ANIMATION_DURATION = 250; const ANIMATION_DURATION = 250;
const AVATAR_LOAD_TRIGGER = 4;
const MediaViewer = ({ const MediaViewer = ({
chatId, chatId,
@ -109,6 +110,7 @@ const MediaViewer = ({
focusMessage, focusMessage,
toggleChatInfo, toggleChatInfo,
searchChatMediaMessages, searchChatMediaMessages,
loadMoreProfilePhotos,
} = getActions(); } = getActions();
const isOpen = Boolean(avatarOwner || message || standaloneMedia); const isOpen = Boolean(avatarOwner || message || standaloneMedia);
@ -255,9 +257,17 @@ const MediaViewer = ({
}, [isGif, isVideo]); }, [isGif, isVideo]);
const loadMoreItemsIfNeeded = useLastCallback((item?: MediaViewerItem) => { const loadMoreItemsIfNeeded = useLastCallback((item?: MediaViewerItem) => {
if (!item || !withDynamicLoading || isLoadingMoreMedia) return; if (!item || isLoadingMoreMedia) return;
if (item.type !== 'message') return;
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: item.message.id }); 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 => { const getNextItem = useLastCallback((from: MediaViewerItem, direction: number): MediaViewerItem | undefined => {
@ -276,7 +286,7 @@ const MediaViewer = ({
if (from.type === 'avatar') { if (from.type === 'avatar') {
const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from; const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from;
const nextIndex = fromMediaIndex + direction; 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 }; return { type: 'avatar', avatarOwner: fromAvatarOwner, mediaIndex: nextIndex };
} }
@ -332,7 +342,8 @@ const MediaViewer = ({
}); });
const handleBeforeDelete = useLastCallback(() => { 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) { if (mediaCount <= 1 || !currentItem) {
handleClose(); handleClose();
return; return;
@ -344,7 +355,7 @@ const MediaViewer = ({
return; 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 // Keep current item, it'll update when indexes shift
return; return;
} }
@ -451,9 +462,12 @@ export default memo(withGlobal(
canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat); canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat);
} }
const profilePhotos = peer?.profilePhotos;
return { return {
avatar: peer?.photos?.[mediaIndex!], avatar: profilePhotos?.photos[mediaIndex!],
avatarOwner: peer, avatarOwner: peer,
isLoadingMoreMedia: profilePhotos?.isLoading,
isChatWithSelf, isChatWithSelf,
canUpdateMedia, canUpdateMedia,
withAnimation, withAnimation,
@ -462,6 +476,7 @@ export default memo(withGlobal(
isHidden, isHidden,
standaloneMedia, standaloneMedia,
mediaIndex, mediaIndex,
isSynced,
}; };
} }

View File

@ -129,7 +129,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
const handleUpdate = useLastCallback(() => { const handleUpdate = useLastCallback(() => {
if (item?.type !== 'avatar') return; if (item?.type !== 'avatar') return;
const { avatarOwner, mediaIndex } = item; const { avatarOwner, mediaIndex } = item;
const avatarPhoto = avatarOwner.photos?.[mediaIndex]!; const avatarPhoto = avatarOwner.profilePhotos?.photos[mediaIndex]!;
if (isUserId(avatarOwner.id)) { if (isUserId(avatarOwner.id)) {
updateProfilePhoto({ photo: avatarPhoto }); updateProfilePhoto({ photo: avatarPhoto });
} else { } else {
@ -170,7 +170,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal} onClose={closeDeleteModal}
onConfirm={onBeforeDelete} onConfirm={onBeforeDelete}
profileId={item.avatarOwner.id} profileId={item.avatarOwner.id}
photo={item.avatarOwner.photos![item.mediaIndex!]} photo={item.avatarOwner.profilePhotos!.photos[item.mediaIndex!]}
/> />
) : undefined; ) : undefined;
} }
@ -212,7 +212,8 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
if (item?.type === 'message') { if (item?.type === 'message') {
openDeleteMessageModal({ openDeleteMessageModal({
isSchedule: messageListType === 'scheduled', isSchedule: messageListType === 'scheduled',
message: item.message, onConfirm: onBeforeDelete, message: item.message,
onConfirm: onBeforeDelete,
}); });
} else { } else {
openDeleteModal(); openDeleteModal();
@ -389,7 +390,7 @@ export default memo(withGlobal<OwnProps>(
const message = item?.type === 'message' ? item.message : undefined; const message = item?.type === 'message' ? item.message : undefined;
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : 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 currentMessageList = selectCurrentMessageList(global);
const { threadId } = selectCurrentMessageList(global) || {}; const { threadId } = selectCurrentMessageList(global) || {};
@ -398,10 +399,10 @@ export default memo(withGlobal<OwnProps>(
const isChatProtected = message && selectIsChatProtected(global, message?.chatId); const isChatProtected = message && selectIsChatProtected(global, message?.chatId);
const { canDelete: canDeleteMessage } = (threadId const { canDelete: canDeleteMessage } = (threadId
&& message && selectAllowedMessageActions(global, message, threadId)) || {}; && message && selectAllowedMessageActions(global, message, threadId)) || {};
const isCurrentAvatar = avatarPhoto && (avatarPhoto.id === avatarOwner?.avatarHash); const isCurrentAvatar = avatarPhoto && (avatarPhoto.id === avatarOwner?.avatarPhotoId);
const canDeleteAvatar = canUpdateMedia && !!avatarPhoto; const canDeleteAvatar = canUpdateMedia && Boolean(avatarPhoto);
const canDelete = canDeleteMessage || canDeleteAvatar; const canDelete = canDeleteMessage || canDeleteAvatar;
const canUpdate = canUpdateMedia && !!avatarPhoto && !isCurrentAvatar; const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar;
const messageListType = currentMessageList?.type; const messageListType = currentMessageList?.type;
return { return {

View File

@ -10,7 +10,6 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectSender, selectSender,
selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import { formatMediaDateTime } from '../../util/dates/dateFormat'; import { formatMediaDateTime } from '../../util/dates/dateFormat';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
@ -29,7 +28,6 @@ type OwnProps = {
type StateProps = { type StateProps = {
owner?: ApiPeer; owner?: ApiPeer;
isFallbackAvatar?: boolean;
}; };
const BULLET = '\u2022'; const BULLET = '\u2022';
@ -38,7 +36,6 @@ const ANIMATION_DURATION = 350;
const SenderInfo: FC<OwnProps & StateProps> = ({ const SenderInfo: FC<OwnProps & StateProps> = ({
owner, owner,
item, item,
isFallbackAvatar,
}) => { }) => {
const { const {
closeMediaViewer, closeMediaViewer,
@ -71,11 +68,15 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
if (!item || item.type === 'standalone') return undefined; if (!item || item.type === 'standalone') return undefined;
const avatarOwner = item.type === 'avatar' ? item.avatarOwner : 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; const date = item.type === 'message' ? item.message.date : avatar?.date;
if (!date) return undefined; if (!date) return undefined;
const formattedDate = formatMediaDateTime(lang, date * 1000, true); 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[] = []; const parts: string[] = [];
if (avatar) { if (avatar) {
@ -89,10 +90,12 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
)); ));
} }
if (countText) parts.push(countText);
parts.push(formattedDate); parts.push(formattedDate);
return parts.join(` ${BULLET} `); return parts.join(` ${BULLET} `);
}, [item, isFallbackAvatar, lang]); }, [item, lang]);
if (!owner) { if (!owner) {
return undefined; return undefined;
@ -122,15 +125,8 @@ export default withGlobal<OwnProps>(
const owner = item?.type === 'avatar' ? item.avatarOwner : messageSender; 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 { return {
owner, owner,
isFallbackAvatar,
}; };
}, },
)(SenderInfo); )(SenderInfo);

View File

@ -68,7 +68,7 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi
} }
if (params.type === 'avatar') { if (params.type === 'avatar') {
const avatar = params.avatarOwner.photos?.[params.mediaIndex]; const avatar = params.avatarOwner.profilePhotos?.photos[params.mediaIndex];
if (avatar) { if (avatar) {
return { return {
media: avatar, media: avatar,

View File

@ -10,8 +10,9 @@ import {
getMediaHash, getMediaHash,
getMediaThumbUri, getMediaThumbUri,
getPhotoFullDimensions, getPhotoFullDimensions,
getVideoAvatarMediaHash, getProfilePhotoMediaHash,
getVideoDimensions, getVideoDimensions,
getVideoProfilePhotoMediaHash,
isDocumentPhoto, isDocumentPhoto,
isDocumentVideo, isDocumentVideo,
} from '../../../global/helpers'; } from '../../../global/helpers';
@ -36,6 +37,7 @@ export const useMediaProps = ({
origin, origin,
delay, delay,
}: UseMediaProps) => { }: UseMediaProps) => {
const isPhotoAvatar = isAvatar && media?.mediaType === 'photo' && !media.isVideo;
const isVideoAvatar = isAvatar && media?.mediaType === 'photo' && media.isVideo; const isVideoAvatar = isAvatar && media?.mediaType === 'photo' && media.isVideo;
const isDocument = media?.mediaType === 'document'; const isDocument = media?.mediaType === 'document';
const isVideo = (media?.mediaType === 'video' && !media.isRound) || (isDocument && isDocumentVideo(media)); const isVideo = (media?.mediaType === 'video' && !media.isRound) || (isDocument && isDocumentVideo(media));
@ -47,12 +49,16 @@ export const useMediaProps = ({
const getMediaOrAvatarHash = useMemo(() => (isFull?: boolean) => { const getMediaOrAvatarHash = useMemo(() => (isFull?: boolean) => {
if (!media) return undefined; if (!media) return undefined;
if ((isPhotoAvatar || isVideoAvatar) && !isFull) {
return getProfilePhotoMediaHash(media);
}
if (isVideoAvatar && isFull) { if (isVideoAvatar && isFull) {
return getVideoAvatarMediaHash(media); return getVideoProfilePhotoMediaHash(media);
} }
return getMediaHash(media, isFull ? 'full' : 'preview'); return getMediaHash(media, isFull ? 'full' : 'preview');
}, [isVideoAvatar, media]); }, [isVideoAvatar, isPhotoAvatar, media]);
const pictogramBlobUrl = useMedia( const pictogramBlobUrl = useMedia(
media media

View File

@ -7,7 +7,7 @@ import type { TextPart } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { MediaViewerOrigin, SettingsScreens } from '../../types'; import { MediaViewerOrigin, SettingsScreens } from '../../types';
import { getPhotoMediaHash, getVideoAvatarMediaHash } from '../../global/helpers'; import { getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../global/helpers';
import { fetchBlob } from '../../util/files'; import { fetchBlob } from '../../util/files';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
@ -39,7 +39,7 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false); const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false);
const photo = message.content.action!.photo!; const photo = message.content.action!.photo!;
const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full')); const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full'));
const suggestedVideoUrl = useMedia(getVideoAvatarMediaHash(photo)); const suggestedVideoUrl = useMedia(getVideoProfilePhotoMediaHash(photo));
const isVideo = message.content.action!.photo?.isVideo; const isVideo = message.content.action!.photo?.isVideo;
const showAvatarNotification = useLastCallback(() => { const showAvatarNotification = useLastCallback(() => {

View File

@ -204,7 +204,6 @@ const Profile: FC<OwnProps & StateProps> = ({
openMediaViewer, openMediaViewer,
openAudioPlayer, openAudioPlayer,
focusMessage, focusMessage,
loadProfilePhotos,
setNewChatMembersDialogState, setNewChatMembersDialogState,
loadPeerProfileStories, loadPeerProfileStories,
loadStoriesArchive, loadStoriesArchive,
@ -342,10 +341,6 @@ const Profile: FC<OwnProps & StateProps> = ({
setSharedMediaSearchType({ mediaType: tabType as SharedMediaType }); setSharedMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setSharedMediaSearchType, tabType, threadId]); }, [setSharedMediaSearchType, tabType, threadId]);
useEffect(() => {
loadProfilePhotos({ profileId });
}, [profileId]);
const handleSelectMedia = useLastCallback((messageId: number) => { const handleSelectMedia = useLastCallback((messageId: number) => {
openMediaViewer({ openMediaViewer({
chatId: profileId, chatId: profileId,
@ -681,7 +676,7 @@ const Profile: FC<OwnProps & StateProps> = ({
function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) { function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) {
return ( return (
<div className="profile-info"> <div className="profile-info">
<ProfileInfo userId={profileId} canPlayVideo={isReady} /> <ProfileInfo peerId={profileId} canPlayVideo={isReady} />
<ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} /> <ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} />
</div> </div>
); );

View File

@ -83,7 +83,6 @@ export const MEMBERS_SLICE = 30;
export const MEMBERS_LOAD_SLICE = 200; export const MEMBERS_LOAD_SLICE = 200;
export const PINNED_MESSAGES_LIMIT = 50; export const PINNED_MESSAGES_LIMIT = 50;
export const BLOCKED_LIST_LIMIT = 100; export const BLOCKED_LIST_LIMIT = 100;
export const PROFILE_PHOTOS_LIMIT = 40;
export const PROFILE_SENSITIVE_AREA = 500; export const PROFILE_SENSITIVE_AREA = 500;
export const TOPIC_LIST_SENSITIVE_AREA = 600; export const TOPIC_LIST_SENSITIVE_AREA = 600;
export const COMMON_CHATS_LIMIT = 100; export const COMMON_CHATS_LIMIT = 100;

View File

@ -63,6 +63,7 @@ import {
addUsers, addUsers,
addUserStatuses, addUserStatuses,
deleteChatMessages, deleteChatMessages,
deletePeerPhoto,
deleteTopic, deleteTopic,
leaveChat, leaveChat,
removeChatFromChatLists, removeChatFromChatLists,
@ -574,7 +575,7 @@ addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType =>
const loadChat = async () => { const loadChat = async () => {
await loadFullChat(global, actions, chat, tabId); await loadFullChat(global, actions, chat, tabId);
if (withPhotos) { if (withPhotos) {
actions.loadProfilePhotos({ profileId: chatId }); actions.loadMoreProfilePhotos({ peerId: chatId, shouldInvalidateCache: true });
} }
}; };
@ -1727,17 +1728,12 @@ addActionHandler('updateChatPhoto', async (global, actions, payload): Promise<vo
const { photo, chatId, tabId = getCurrentTabId() } = payload; const { photo, chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
global = updateChat(global, chatId, { avatarHash: undefined });
global = updateChatFullInfo(global, chatId, { profilePhoto: undefined });
setGlobal(global);
// This method creates a new entry in photos array
await callApi('editChatPhoto', { await callApi('editChatPhoto', {
chatId, chatId,
accessHash: chat.accessHash, accessHash: chat.accessHash,
photo, photo,
}); });
// Explicitly delete the old photo reference
await callApi('deleteProfilePhotos', [photo]);
actions.loadFullChat({ chatId, tabId, withPhotos: true }); actions.loadFullChat({ chatId, tabId, withPhotos: true });
}); });
@ -1745,35 +1741,22 @@ addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<vo
const { photo, chatId, tabId = getCurrentTabId() } = payload; const { photo, chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const photosToDelete = [photo];
if (chat.avatarHash === photo.id) { let isDeleted;
// Select next photo to set as avatar if (photo.id === chat.avatarPhotoId) {
const nextPhoto = chat.photos?.[1]; isDeleted = await callApi('editChatPhoto', {
if (nextPhoto) {
photosToDelete.push(nextPhoto);
}
global = updateChat(global, chatId, { avatarHash: undefined });
global = updateChatFullInfo(global, chatId, { profilePhoto: undefined });
setGlobal(global);
// Set next photo as avatar
await callApi('editChatPhoto', {
chatId, chatId,
accessHash: chat.accessHash, accessHash: chat.accessHash,
photo: nextPhoto,
}); });
} else {
isDeleted = await callApi('deleteProfilePhotos', [photo]);
} }
if (!isDeleted) return;
const { photos = [] } = chat;
const newPhotos = photos.filter((p) => photosToDelete.some((toDelete) => toDelete.id !== p.id));
global = getGlobal(); global = getGlobal();
global = updateChat(global, chatId, { photos: newPhotos }); global = deletePeerPhoto(global, chatId, photo.id);
setGlobal(global); setGlobal(global);
// Delete references to the old photos
const result = await callApi('deleteProfilePhotos', photosToDelete);
if (!result) return;
actions.loadFullChat({ chatId, tabId, withPhotos: true }); actions.loadFullChat({ chatId, tabId, withPhotos: true });
}); });

View File

@ -455,7 +455,7 @@ addActionHandler('uploadContactProfilePhoto', async (global, actions, payload):
return; return;
} }
actions.loadProfilePhotos({ profileId: userId }); actions.loadMoreProfilePhotos({ peerId: userId, shouldInvalidateCache: true });
global = getGlobal(); global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.Complete, tabId); global = updateManagementProgress(global, ManagementProgress.Complete, tabId);

View File

@ -19,12 +19,13 @@ import { callApi } from '../../../api/gramjs';
import { buildApiInputPrivacyRules } from '../../helpers'; import { buildApiInputPrivacyRules } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
addBlockedUser, addNotifyExceptions, addUsers, removeBlockedUser, replaceSettings, updateChat, updateChats, addBlockedUser, addNotifyExceptions, addUsers, deletePeerPhoto,
removeBlockedUser, replaceSettings, updateChat, updateChats,
updateNotifySettings, updateUser, updateUserFullInfo, updateNotifySettings, updateUser, updateUserFullInfo,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectChat, selectTabState, selectUser, selectUserFullInfo, selectChat, selectTabState, selectUser,
} from '../../selectors'; } from '../../selectors';
addActionHandler('updateProfile', async (global, actions, payload): Promise<void> => { addActionHandler('updateProfile', async (global, actions, payload): Promise<void> => {
@ -110,7 +111,7 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise
const currentUser = selectUser(global, currentUserId); const currentUser = selectUser(global, currentUserId);
if (!currentUser) return; if (!currentUser) return;
global = updateUser(global, currentUserId, { avatarHash: undefined }); global = updateUser(global, currentUserId, { avatarPhotoId: undefined });
global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined }); global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined });
setGlobal(global); setGlobal(global);
@ -129,32 +130,14 @@ addActionHandler('deleteProfilePhoto', async (global, actions, payload): Promise
const { photo } = payload; const { photo } = payload;
const { currentUserId } = global; const { currentUserId } = global;
if (!currentUserId) return; if (!currentUserId) return;
const currentUser = selectUser(global, currentUserId);
if (!currentUser) return;
const fullInfo = selectUserFullInfo(global, currentUserId); const isDeleted = await callApi('deleteProfilePhotos', [photo]);
if (!isDeleted) return;
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 });
global = getGlobal();
global = deletePeerPhoto(global, currentUserId, photo.id);
setGlobal(global); setGlobal(global);
await callApi('deleteProfilePhotos', [photo]);
actions.loadFullUser({ userId: currentUserId, withPhotos: true }); actions.loadFullUser({ userId: currentUserId, withPhotos: true });
}); });

View File

@ -20,9 +20,10 @@ import {
addUserStatuses, addUserStatuses,
closeNewContactDialog, closeNewContactDialog,
replaceUserStatuses, replaceUserStatuses,
updateChat,
updateChats, updateChats,
updateManagementProgress, updateManagementProgress,
updatePeerPhotos,
updatePeerPhotosIsLoading,
updateUser, updateUser,
updateUserFullInfo, updateUserFullInfo,
updateUsers, updateUsers,
@ -30,14 +31,21 @@ import {
updateUserSearchFetchingStatus, updateUserSearchFetchingStatus,
} from '../../reducers'; } from '../../reducers';
import { import {
selectChat, selectChatFullInfo, selectCurrentMessageList, selectPeer, selectTabState, selectUser, selectUserFullInfo, selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectPeer,
selectTabState,
selectUser,
selectUserFullInfo,
} from '../../selectors'; } from '../../selectors';
const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10;
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runThrottledForSearch = throttle((cb) => cb(), 500, false); const runThrottledForSearch = throttle((cb) => cb(), 500, false);
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => { addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
const { userId, withPhotos } = payload!; const { userId, withPhotos } = payload;
const user = selectUser(global, userId); const user = selectUser(global, userId);
if (!user) { if (!user) {
return; return;
@ -50,11 +58,11 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise<void>
global = getGlobal(); global = getGlobal();
const fullInfo = selectUserFullInfo(global, userId); const fullInfo = selectUserFullInfo(global, userId);
const { user: newUser, fullInfo: newFullInfo } = result; 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 hasChangedProfilePhoto = fullInfo?.profilePhoto?.id !== newFullInfo?.profilePhoto?.id;
const hasChangedFallbackPhoto = fullInfo?.fallbackPhoto?.id !== newFullInfo?.fallbackPhoto?.id; const hasChangedFallbackPhoto = fullInfo?.fallbackPhoto?.id !== newFullInfo?.fallbackPhoto?.id;
const hasChangedPersonalPhoto = fullInfo?.personalPhoto?.id !== newFullInfo?.personalPhoto?.id; const hasChangedPersonalPhoto = fullInfo?.personalPhoto?.id !== newFullInfo?.personalPhoto?.id;
const hasChangedPhoto = hasChangedAvatarHash const hasChangedPhoto = hasChangedAvatar
|| hasChangedProfilePhoto || hasChangedProfilePhoto
|| hasChangedFallbackPhoto || hasChangedFallbackPhoto
|| hasChangedPersonalPhoto; || hasChangedPersonalPhoto;
@ -65,13 +73,13 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise<void>
global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
setGlobal(global); setGlobal(global);
if (withPhotos || (user.photos?.length && hasChangedPhoto)) { if (withPhotos || (user.profilePhotos?.count && hasChangedPhoto)) {
actions.loadProfilePhotos({ profileId: userId }); actions.loadMoreProfilePhotos({ peerId: userId, shouldInvalidateCache: true });
} }
}); });
addActionHandler('loadUser', async (global, actions, payload): Promise<void> => { addActionHandler('loadUser', async (global, actions, payload): Promise<void> => {
const { userId } = payload!; const { userId } = payload;
const user = selectUser(global, userId); const user = selectUser(global, userId);
if (!user) { if (!user) {
return; return;
@ -251,30 +259,36 @@ addActionHandler('deleteContact', async (global, actions, payload): Promise<void
await callApi('deleteContact', { id, accessHash }); await callApi('deleteContact', { id, accessHash });
}); });
addActionHandler('loadProfilePhotos', async (global, actions, payload): Promise<void> => { addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Promise<void> => {
const { profileId } = payload!; const { peerId, shouldInvalidateCache, isPreload } = payload;
const isPrivate = isUserId(profileId); const isPrivate = isUserId(peerId);
let user = isPrivate ? selectUser(global, profileId) : undefined; const user = isPrivate ? selectUser(global, peerId) : undefined;
const chat = !isPrivate ? selectChat(global, profileId) : undefined; const chat = !isPrivate ? selectChat(global, peerId) : undefined;
if (!user && !chat) { const peer = user || chat;
if (!peer?.avatarPhotoId) {
return; return;
} }
let userFullInfo = selectUserFullInfo(global, profileId); if (peer.profilePhotos && !shouldInvalidateCache && (isPreload || !peer.profilePhotos.nextOffset)) return;
let chatFullInfo = selectChatFullInfo(global, profileId);
if (user && !userFullInfo?.profilePhoto) { 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 { id, accessHash } = user;
const result = await callApi('fetchFullUser', { id, accessHash }); const result = await callApi('fetchFullUser', { id, accessHash });
if (!result?.user) { if (!result?.user) {
return; return;
} }
user = result.user;
userFullInfo = result.fullInfo; userFullInfo = result.fullInfo;
} }
if (chat && !chatFullInfo?.profilePhoto) { if (chat && !chatFullInfo) {
const result = await callApi('fetchFullChat', chat); const result = await callApi('fetchFullChat', chat);
if (!result?.fullInfo) { if (!result?.fullInfo) {
return; return;
@ -283,38 +297,42 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload): Promise<
chatFullInfo = result.fullInfo; 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) { if (!result || !result.photos) {
return; return;
} }
global = getGlobal(); global = getGlobal();
const userOrChat = user || chat; const {
const { photos, users } = result; photos, users, count, nextOffsetId,
} = 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));
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
if (isPrivate) { global = updatePeerPhotos(global, peerId, {
global = updateUser(global, profileId, { photos }); newPhotos: photos,
} else { count,
global = updateChat(global, profileId, { photos }); nextOffset: nextOffsetId,
} fullInfo: peerFullInfo,
shouldInvalidateCache,
});
setGlobal(global); setGlobal(global);
}); });
addActionHandler('setUserSearchQuery', (global, actions, payload): ActionReturnType => { addActionHandler('setUserSearchQuery', (global, actions, payload): ActionReturnType => {
const { query, tabId = getCurrentTabId() } = payload!; const { query, tabId = getCurrentTabId() } = payload;
if (!query) return; if (!query) return;
@ -371,7 +389,7 @@ addActionHandler('importContact', async (global, actions, payload): Promise<void
}); });
addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => { addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
const { chatId } = payload!; const { chatId } = payload;
const peer = selectPeer(global, chatId); const peer = selectPeer(global, chatId);
if (!peer) { if (!peer) {
return; return;
@ -381,13 +399,13 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
}); });
addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => { addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => {
const { emojiStatus, expires } = payload!; const { emojiStatus, expires } = payload;
void callApi('updateEmojiStatus', emojiStatus, expires); void callApi('updateEmojiStatus', emojiStatus, expires);
}); });
addActionHandler('saveCloseFriends', async (global, actions, payload): Promise<void> => { addActionHandler('saveCloseFriends', async (global, actions, payload): Promise<void> => {
const { userIds } = payload!; const { userIds } = payload;
const result = await callApi('saveCloseFriends', userIds); const result = await callApi('saveCloseFriends', userIds);
if (!result) { if (!result) {

View File

@ -14,6 +14,7 @@ import {
import { import {
addUnreadMentions, addUnreadMentions,
deleteChatMessages, deleteChatMessages,
deletePeerPhoto,
leaveChat, leaveChat,
removeUnreadMentions, removeUnreadMentions,
replaceThreadParam, replaceThreadParam,
@ -35,6 +36,7 @@ import {
selectCommonBoxChatId, selectCommonBoxChatId,
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatListed, selectIsChatListed,
selectPeer,
selectTabState, selectTabState,
selectThreadParam, selectThreadParam,
selectTopicFromMessage, selectTopicFromMessage,
@ -57,7 +59,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const localChat = selectChat(global, update.id); 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) { if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) {
global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false); global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false);
@ -534,6 +536,43 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isForumAsMessages: isEnabled, isForumAsMessages: isEnabled,
}); });
setGlobal(global); 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;
} }
} }

View File

@ -30,6 +30,7 @@ import {
clearMessageTranslation, clearMessageTranslation,
deleteChatMessages, deleteChatMessages,
deleteChatScheduledMessages, deleteChatScheduledMessages,
deletePeerPhoto,
deleteQuickReply, deleteQuickReply,
deleteQuickReplyMessages, deleteQuickReplyMessages,
deleteTopic, deleteTopic,
@ -1069,6 +1070,10 @@ export function deleteMessages<T extends GlobalState>(
return; return;
} }
if (message.content.action?.photo) {
global = deletePeerPhoto(global, chatId, message.content.action.photo.id, true);
}
global = updateThreadUnread(global, actions, message, true); global = updateThreadUnread(global, actions, message, true);
const threadId = selectThreadIdFromMessage(global, message); const threadId = selectThreadIdFromMessage(global, message);
@ -1144,6 +1149,10 @@ export function deleteMessages<T extends GlobalState>(
} }
} }
if (message?.content.action?.photo) {
global = deletePeerPhoto(global, commonBoxChatId, message.content.action.photo.id, true);
}
setTimeout(() => { setTimeout(() => {
global = getGlobal(); global = getGlobal();
global = deleteChatMessages(global, commonBoxChatId, [id]); global = deleteChatMessages(global, commonBoxChatId, [id]);

View File

@ -104,22 +104,22 @@ export function getChatLink(chat: ApiChat) {
export function getChatAvatarHash( export function getChatAvatarHash(
owner: ApiPeer, owner: ApiPeer,
size: 'normal' | 'big' = 'normal', size: 'normal' | 'big' = 'normal',
avatarHash = owner.avatarHash, avatarPhotoId = owner.avatarPhotoId,
) { ) {
if (!avatarHash) { if (!avatarPhotoId) {
return undefined; return undefined;
} }
switch (size) { switch (size) {
case 'big': case 'big':
return `profile${owner.id}?${avatarHash}`; return `profile${owner.id}?${avatarPhotoId}`;
default: default:
return `avatar${owner.id}?${avatarHash}`; return `avatar${owner.id}?${avatarPhotoId}`;
} }
} }
export function isChatAdmin(chat: ApiChat) { export function isChatAdmin(chat: ApiChat) {
return Boolean(chat.adminRights); return Boolean(chat.adminRights || chat.isCreator);
} }
export function getHasAdminRight(chat: ApiChat, key: keyof ApiChatAdminRights) { export function getHasAdminRight(chat: ApiChat, key: keyof ApiChatAdminRights) {

View File

@ -284,14 +284,18 @@ export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: Target,
case 'preview': case 'preview':
return `${base}?size=${isAction ? 'b' : 'x'}`; return `${base}?size=${isAction ? 'b' : 'x'}`;
case 'download': case 'download':
return !isVideo ? base : getVideoAvatarMediaHash(photo); return !isVideo ? base : getVideoProfilePhotoMediaHash(photo);
case 'full': case 'full':
default: default:
return base; 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; if (!photo.isVideo) return undefined;
return `photo${photo.id}?size=u`; return `photo${photo.id}?size=u`;
} }

View File

@ -1,5 +1,5 @@
import type { import type {
ApiChat, ApiChatFullInfo, ApiChatMember, ApiPhoto, ApiTopic, ApiChat, ApiChatFullInfo, ApiChatMember, ApiTopic,
} from '../../api/types'; } from '../../api/types';
import type { ChatListType, GlobalState } from '../types'; import type { ChatListType, GlobalState } from '../types';
@ -138,12 +138,11 @@ export function removeUnreadMentions<T extends GlobalState>(
} }
export function updateChat<T extends GlobalState>( export function updateChat<T extends GlobalState>(
global: T, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto, global: T, chatId: string, chatUpdate: Partial<ApiChat>, noOmitUnreadReactionCount = false,
noOmitUnreadReactionCount = false,
): T { ): T {
const { byId } = global.chats; const { byId } = global.chats;
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo, noOmitUnreadReactionCount); const updatedChat = getUpdatedChat(global, chatId, chatUpdate, noOmitUnreadReactionCount);
if (!updatedChat) { if (!updatedChat) {
return global; return global;
} }
@ -257,7 +256,7 @@ export function addChats<T extends GlobalState>(global: T, newById: Record<strin
// @optimization Don't spread/unspread global for each element, do it in a batch // @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedChat<T extends GlobalState>( function getUpdatedChat<T extends GlobalState>(
global: T, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto, global: T, chatId: string, chatUpdate: Partial<ApiChat>,
noOmitUnreadReactionCount = false, noOmitUnreadReactionCount = false,
) { ) {
const { byId } = global.chats; const { byId } = global.chats;
@ -279,7 +278,6 @@ function getUpdatedChat<T extends GlobalState>(
const updatedChat: ApiChat = { const updatedChat: ApiChat = {
...chat, ...chat,
...omit(chatUpdate, omitProps), ...omit(chatUpdate, omitProps),
...(photo && { photos: [photo, ...(chat.photos || [])] }),
} as ApiChat; } as ApiChat;
if (!updatedChat.id || !updatedChat.type) { if (!updatedChat.id || !updatedChat.type) {

View File

@ -1,9 +1,11 @@
import type { import type {
ApiChat, ApiChatFullInfo, ApiUser, ApiUserFullInfo, ApiChat, ApiChatFullInfo, ApiPhoto, ApiUser, ApiUserFullInfo,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState } from '../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 { updateChat, updateChatFullInfo } from './chats';
import { updateUser, updateUserFullInfo } from './users'; import { updateUser, updateUserFullInfo } from './users';
@ -30,3 +32,137 @@ export function updatePeerFullInfo<T extends GlobalState>(
return updateChatFullInfo(global, peerId, peerFullInfoUpdate); return updateChatFullInfo(global, peerId, peerFullInfoUpdate);
} }
export function updatePeerPhotosIsLoading<T extends GlobalState>(
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<T extends GlobalState>(
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<T extends GlobalState>(
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,
});
}

View File

@ -77,5 +77,5 @@ export function addMessageReaction<T extends GlobalState>(
export function updateUnreadReactions<T extends GlobalState>( export function updateUnreadReactions<T extends GlobalState>(
global: T, chatId: string, update: Pick<ApiChat, 'unreadReactionsCount' | 'unreadReactions'>, global: T, chatId: string, update: Pick<ApiChat, 'unreadReactionsCount' | 'unreadReactions'>,
): T { ): T {
return updateChat(global, chatId, update, undefined, true); return updateChat(global, chatId, update, true);
} }

View File

@ -2608,8 +2608,10 @@ export interface ActionPayloads {
isMuted?: boolean; isMuted?: boolean;
shouldSharePhoneNumber?: boolean; shouldSharePhoneNumber?: boolean;
} & WithTabId; } & WithTabId;
loadProfilePhotos: { loadMoreProfilePhotos: {
profileId: string; peerId: string;
isPreload?: boolean;
shouldInvalidateCache?: boolean;
}; };
deleteProfilePhoto: { deleteProfilePhoto: {
photo: ApiPhoto; photo: ApiPhoto;