Avatar: Display more than 40 profile picture (#4780)
This commit is contained in:
parent
8ced8b3b05
commit
43244b6e61
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -100,7 +100,6 @@ export type ApiUpdateChat = {
|
||||
'@type': 'updateChat';
|
||||
id: string;
|
||||
chat: Partial<ApiChat>;
|
||||
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;
|
||||
|
||||
@ -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?: {
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
getChatTitle,
|
||||
getPeerStoryHtmlId,
|
||||
getUserFullName,
|
||||
getVideoAvatarMediaHash,
|
||||
getVideoProfilePhotoMediaHash,
|
||||
getWebDocumentHash,
|
||||
isAnonymousForwardsChat,
|
||||
isChatWithRepliesBot,
|
||||
@ -121,7 +121,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
} 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);
|
||||
|
||||
@ -109,7 +109,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const {
|
||||
loadFullChat,
|
||||
openMediaViewer,
|
||||
loadProfilePhotos,
|
||||
loadMoreProfilePhotos,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -121,9 +121,9 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
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<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
|
||||
|
||||
@ -102,7 +102,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const {
|
||||
loadFullUser,
|
||||
openMediaViewer,
|
||||
loadProfilePhotos,
|
||||
loadMoreProfilePhotos,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -112,7 +112,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
if (withFullInfo && isSynced) loadFullUser({ userId });
|
||||
if (withMediaViewer) loadProfilePhotos({ profileId: userId });
|
||||
if (withMediaViewer) loadMoreProfilePhotos({ peerId: userId, isPreload: true });
|
||||
}
|
||||
}, [userId, withFullInfo, withMediaViewer, isSynced]);
|
||||
|
||||
|
||||
@ -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<GlobalState, 'isSynced'>;
|
||||
};
|
||||
|
||||
const EMOJI_STATUS_SIZE = 24;
|
||||
const EMOJI_TOPIC_SIZE = 120;
|
||||
const LOAD_MORE_THRESHOLD = 3;
|
||||
const MAX_PHOTO_DASH_COUNT = 30;
|
||||
|
||||
const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
forceShowSelf,
|
||||
@ -74,33 +67,28 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={styles.photoDashes}>
|
||||
{photos.map((_, i) => (
|
||||
<span className={buildClassName(styles.photoDash, i === currentPhotoIndex && styles.photoDash_current)} />
|
||||
{enumerator.map((_, i) => (
|
||||
<span className={buildClassName(styles.photoDash, i === activeDashIndex && styles.photoDash_current)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -248,14 +234,13 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
const photo = photos.length > 0
|
||||
? photos[currentPhotoIndex]
|
||||
: undefined;
|
||||
const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto;
|
||||
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={currentPhotoIndex}
|
||||
user={user}
|
||||
chat={chat}
|
||||
photo={profilePhoto}
|
||||
photo={photo}
|
||||
canPlayVideo={Boolean(isActive && canPlayVideo)}
|
||||
onClick={handleProfilePhotoClick}
|
||||
/>
|
||||
@ -263,8 +248,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
const peerId = (chatId || userId)!;
|
||||
|
||||
const isAnonymousForwards = isAnonymousForwardsChat(peerId);
|
||||
if (isAnonymousForwards) return undefined;
|
||||
|
||||
@ -310,18 +293,18 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<div className={styles.photoWrapper}>
|
||||
{renderPhotoTabs()}
|
||||
{!forceShowSelf && userPersonalPhoto && (
|
||||
{!forceShowSelf && userProfilePhotos?.personalPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
isFirst && styles.fallbackPhotoVisible,
|
||||
)}
|
||||
>
|
||||
<div className={styles.fallbackPhotoContents}>
|
||||
{lang(userPersonalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
|
||||
{lang(userProfilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{forceShowSelf && userFallbackPhoto && (
|
||||
{forceShowSelf && userProfilePhotos?.fallbackPhoto && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
(isFirst || isLast) && styles.fallbackPhotoVisible,
|
||||
@ -330,12 +313,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
|
||||
{!isLast && (
|
||||
<Avatar
|
||||
photo={userFallbackPhoto}
|
||||
photo={userProfilePhotos.fallbackPhoto}
|
||||
className={styles.fallbackPhotoAvatar}
|
||||
size="mini"
|
||||
/>
|
||||
)}
|
||||
{lang(userFallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
|
||||
{lang(userProfilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -381,37 +364,28 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(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!),
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
{isBlurredThumb ? (
|
||||
<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 && (
|
||||
isVideo ? (
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -49,7 +49,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
onReset,
|
||||
}) => {
|
||||
const {
|
||||
loadProfilePhotos,
|
||||
loadMoreProfilePhotos,
|
||||
openPremiumModal,
|
||||
openSupportChat,
|
||||
openUrl,
|
||||
@ -63,9 +63,9 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserId) {
|
||||
loadProfilePhotos({ profileId: currentUserId });
|
||||
loadMoreProfilePhotos({ peerId: currentUserId, isPreload: true });
|
||||
}
|
||||
}, [currentUserId, loadProfilePhotos]);
|
||||
}, [currentUserId]);
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -82,7 +82,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-main-menu">
|
||||
{currentUserId && (
|
||||
<ProfileInfo
|
||||
userId={currentUserId}
|
||||
peerId={currentUserId}
|
||||
canPlayVideo={Boolean(isActive)}
|
||||
forceShowSelf
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -129,7 +129,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
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<OwnProps>(
|
||||
|
||||
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<OwnProps>(
|
||||
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 {
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
owner,
|
||||
item,
|
||||
isFallbackAvatar,
|
||||
}) => {
|
||||
const {
|
||||
closeMediaViewer,
|
||||
@ -71,11 +68,15 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
||||
if (!item || item.type === 'standalone') return undefined;
|
||||
|
||||
const avatarOwner = item.type === 'avatar' ? item.avatarOwner : undefined;
|
||||
const avatar = avatarOwner?.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<OwnProps & StateProps> = ({
|
||||
));
|
||||
}
|
||||
|
||||
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<OwnProps>(
|
||||
|
||||
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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
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(() => {
|
||||
|
||||
@ -204,7 +204,6 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
openMediaViewer,
|
||||
openAudioPlayer,
|
||||
focusMessage,
|
||||
loadProfilePhotos,
|
||||
setNewChatMembersDialogState,
|
||||
loadPeerProfileStories,
|
||||
loadStoriesArchive,
|
||||
@ -342,10 +341,6 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) {
|
||||
return (
|
||||
<div className="profile-info">
|
||||
<ProfileInfo userId={profileId} canPlayVideo={isReady} />
|
||||
<ProfileInfo peerId={profileId} canPlayVideo={isReady} />
|
||||
<ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<vo
|
||||
const { photo, chatId, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
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', {
|
||||
chatId,
|
||||
accessHash: chat.accessHash,
|
||||
photo,
|
||||
});
|
||||
// Explicitly delete the old photo reference
|
||||
await callApi('deleteProfilePhotos', [photo]);
|
||||
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 chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
const photosToDelete = [photo];
|
||||
if (chat.avatarHash === photo.id) {
|
||||
// Select next photo to set as avatar
|
||||
const nextPhoto = chat.photos?.[1];
|
||||
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', {
|
||||
|
||||
let isDeleted;
|
||||
if (photo.id === chat.avatarPhotoId) {
|
||||
isDeleted = await callApi('editChatPhoto', {
|
||||
chatId,
|
||||
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 = 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 });
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<void> => {
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
|
||||
@ -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<void> => {
|
||||
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<void>
|
||||
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<void>
|
||||
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<void> => {
|
||||
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<void
|
||||
await callApi('deleteContact', { id, accessHash });
|
||||
});
|
||||
|
||||
addActionHandler('loadProfilePhotos', async (global, actions, payload): Promise<void> => {
|
||||
const { profileId } = payload!;
|
||||
const isPrivate = isUserId(profileId);
|
||||
addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Promise<void> => {
|
||||
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<void
|
||||
});
|
||||
|
||||
addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
|
||||
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<void> => {
|
||||
const { userIds } = payload!;
|
||||
const { userIds } = payload;
|
||||
|
||||
const result = await callApi('saveCloseFriends', userIds);
|
||||
if (!result) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
clearMessageTranslation,
|
||||
deleteChatMessages,
|
||||
deleteChatScheduledMessages,
|
||||
deletePeerPhoto,
|
||||
deleteQuickReply,
|
||||
deleteQuickReplyMessages,
|
||||
deleteTopic,
|
||||
@ -1069,6 +1070,10 @@ export function deleteMessages<T extends GlobalState>(
|
||||
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<T extends GlobalState>(
|
||||
}
|
||||
}
|
||||
|
||||
if (message?.content.action?.photo) {
|
||||
global = deletePeerPhoto(global, commonBoxChatId, message.content.action.photo.id, true);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
global = getGlobal();
|
||||
global = deleteChatMessages(global, commonBoxChatId, [id]);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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`;
|
||||
}
|
||||
|
||||
@ -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<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function updateChat<T extends GlobalState>(
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
noOmitUnreadReactionCount = false,
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>, 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<T extends GlobalState>(global: T, newById: Record<strin
|
||||
|
||||
// @optimization Don't spread/unspread global for each element, do it in a batch
|
||||
function getUpdatedChat<T extends GlobalState>(
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>,
|
||||
noOmitUnreadReactionCount = false,
|
||||
) {
|
||||
const { byId } = global.chats;
|
||||
@ -279,7 +278,6 @@ function getUpdatedChat<T extends GlobalState>(
|
||||
const updatedChat: ApiChat = {
|
||||
...chat,
|
||||
...omit(chatUpdate, omitProps),
|
||||
...(photo && { photos: [photo, ...(chat.photos || [])] }),
|
||||
} as ApiChat;
|
||||
|
||||
if (!updatedChat.id || !updatedChat.type) {
|
||||
|
||||
@ -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<T extends GlobalState>(
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -77,5 +77,5 @@ export function addMessageReaction<T extends GlobalState>(
|
||||
export function updateUnreadReactions<T extends GlobalState>(
|
||||
global: T, chatId: string, update: Pick<ApiChat, 'unreadReactionsCount' | 'unreadReactions'>,
|
||||
): T {
|
||||
return updateChat(global, chatId, update, undefined, true);
|
||||
return updateChat(global, chatId, update, true);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user