Profile / Photo: Suggest & Replace Profile Photos (#2333)
This commit is contained in:
parent
a6b7ebfcaa
commit
14819b271e
@ -1048,6 +1048,15 @@ function buildAction(
|
|||||||
}
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionAttachMenuBotAllowed) {
|
} else if (action instanceof GramJs.MessageActionAttachMenuBotAllowed) {
|
||||||
text = 'ActionAttachMenuBotAllowed';
|
text = 'ActionAttachMenuBotAllowed';
|
||||||
|
} else if (action instanceof GramJs.MessageActionSuggestProfilePhoto) {
|
||||||
|
const isVideo = action.photo instanceof GramJs.Photo && action.photo.videoSizes?.length;
|
||||||
|
text = senderId === currentUserId
|
||||||
|
? (isVideo ? 'ActionSuggestVideoFromYouDescription' : 'ActionSuggestPhotoFromYouDescription')
|
||||||
|
: (isVideo ? 'ActionSuggestVideoToYouDescription' : 'ActionSuggestPhotoToYouDescription');
|
||||||
|
type = 'suggestProfilePhoto';
|
||||||
|
translationValues.push('%target_user%');
|
||||||
|
|
||||||
|
if (targetPeerId) targetUserIds.push(targetPeerId);
|
||||||
} else {
|
} else {
|
||||||
text = 'ChatList.UnsupportedMessage';
|
text = 'ChatList.UnsupportedMessage';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
|
|||||||
fullUser: {
|
fullUser: {
|
||||||
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
||||||
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
||||||
fallbackPhoto,
|
fallbackPhoto, personalPhoto,
|
||||||
},
|
},
|
||||||
users,
|
users,
|
||||||
} = mtpUserFull;
|
} = mtpUserFull;
|
||||||
@ -27,6 +27,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
|
|||||||
fullInfo: {
|
fullInfo: {
|
||||||
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
||||||
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
||||||
|
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }),
|
||||||
bio: about,
|
bio: about,
|
||||||
commonChatsCount,
|
commonChatsCount,
|
||||||
pinnedMessageId: pinnedMsgId,
|
pinnedMessageId: pinnedMsgId,
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export {
|
|||||||
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
||||||
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig,
|
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig,
|
||||||
fetchGlobalPrivacySettings, updateGlobalPrivacySettings, toggleUsername, reorderUsernames, fetchConfig,
|
fetchGlobalPrivacySettings, updateGlobalPrivacySettings, toggleUsername, reorderUsernames, fetchConfig,
|
||||||
|
uploadContactProfilePhoto,
|
||||||
} from './settings';
|
} from './settings';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -160,7 +160,9 @@ async function download(
|
|||||||
let mimeType;
|
let mimeType;
|
||||||
let fullSize;
|
let fullSize;
|
||||||
|
|
||||||
if (entity instanceof GramJs.Message) {
|
if (entity instanceof GramJs.MessageService && entity.action instanceof GramJs.MessageActionSuggestProfilePhoto) {
|
||||||
|
mimeType = 'image/jpeg';
|
||||||
|
} else if (entity instanceof GramJs.Message) {
|
||||||
mimeType = getMessageMediaMimeType(entity, sizeType);
|
mimeType = getMessageMediaMimeType(entity, sizeType);
|
||||||
if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) {
|
if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) {
|
||||||
fullSize = entity.media.document.size.toJSNumber();
|
fullSize = entity.media.document.size.toJSNumber();
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type {
|
|||||||
ApiError,
|
ApiError,
|
||||||
ApiLangString,
|
ApiLangString,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
ApiNotifyException, ApiPhoto,
|
ApiNotifyException, ApiPhoto, ApiUser,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types';
|
import type { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types';
|
||||||
import type { LANG_PACKS } from '../../../config';
|
import type { LANG_PACKS } from '../../../config';
|
||||||
@ -101,10 +101,10 @@ export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean)
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadProfilePhoto(file: File, isFallback?: boolean) {
|
export async function uploadProfilePhoto(file: File, isFallback?: boolean, isVideo = false, videoTs = 0) {
|
||||||
const inputFile = await uploadFile(file);
|
const inputFile = await uploadFile(file);
|
||||||
const result = await invokeRequest(new GramJs.photos.UploadProfilePhoto({
|
const result = await invokeRequest(new GramJs.photos.UploadProfilePhoto({
|
||||||
file: inputFile,
|
...(isVideo ? { video: inputFile, videoStartTs: videoTs } : { file: inputFile }),
|
||||||
...(isFallback ? { fallback: true } : undefined),
|
...(isFallback ? { fallback: true } : undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -121,6 +121,40 @@ export async function uploadProfilePhoto(file: File, isFallback?: boolean) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadContactProfilePhoto({
|
||||||
|
file, isSuggest, user,
|
||||||
|
}: {
|
||||||
|
file?: File;
|
||||||
|
isSuggest?: boolean;
|
||||||
|
user: ApiUser;
|
||||||
|
}) {
|
||||||
|
const inputFile = file ? await uploadFile(file) : undefined;
|
||||||
|
const result = await invokeRequest(new GramJs.photos.UploadContactProfilePhoto({
|
||||||
|
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||||
|
file: inputFile,
|
||||||
|
...(isSuggest ? { suggest: true } : { save: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
addEntitiesWithPhotosToLocalDb(result.users);
|
||||||
|
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
|
||||||
|
if (result.photo instanceof GramJs.Photo) {
|
||||||
|
addPhotoToLocalDb(result.photo);
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
photo: buildApiPhoto(result.photo),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
photo: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteProfilePhotos(photos: ApiPhoto[]) {
|
export async function deleteProfilePhotos(photos: ApiPhoto[]) {
|
||||||
const photoIds = photos.map(buildInputPhoto).filter(Boolean);
|
const photoIds = photos.map(buildInputPhoto).filter(Boolean);
|
||||||
const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), true);
|
const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), true);
|
||||||
|
|||||||
@ -46,11 +46,16 @@ export async function fetchFullUser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLocalDb(fullInfo);
|
updateLocalDb(fullInfo);
|
||||||
|
addUserToLocalDb(fullInfo.users[0], true);
|
||||||
|
|
||||||
if (fullInfo.fullUser.profilePhoto instanceof GramJs.Photo) {
|
if (fullInfo.fullUser.profilePhoto instanceof GramJs.Photo) {
|
||||||
localDb.photos[fullInfo.fullUser.profilePhoto.id.toString()] = fullInfo.fullUser.profilePhoto;
|
localDb.photos[fullInfo.fullUser.profilePhoto.id.toString()] = fullInfo.fullUser.profilePhoto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fullInfo.fullUser.personalPhoto instanceof GramJs.Photo) {
|
||||||
|
localDb.photos[fullInfo.fullUser.personalPhoto.id.toString()] = fullInfo.fullUser.personalPhoto;
|
||||||
|
}
|
||||||
|
|
||||||
if (fullInfo.fullUser.fallbackPhoto instanceof GramJs.Photo) {
|
if (fullInfo.fullUser.fallbackPhoto instanceof GramJs.Photo) {
|
||||||
localDb.photos[fullInfo.fullUser.fallbackPhoto.id.toString()] = fullInfo.fullUser.fallbackPhoto;
|
localDb.photos[fullInfo.fullUser.fallbackPhoto.id.toString()] = fullInfo.fullUser.fallbackPhoto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,7 +181,10 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) {
|
if ((update.message instanceof GramJs.Message && isMessageWithMedia(update.message))
|
||||||
|
|| (update.message instanceof GramJs.MessageService
|
||||||
|
&& update.message.action instanceof GramJs.MessageActionSuggestProfilePhoto)
|
||||||
|
) {
|
||||||
addMessageToLocalDb(update.message);
|
addMessageToLocalDb(update.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -263,7 +263,7 @@ export interface ApiAction {
|
|||||||
text: string;
|
text: string;
|
||||||
targetUserIds?: string[];
|
targetUserIds?: string[];
|
||||||
targetChatId?: string;
|
targetChatId?: string;
|
||||||
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'other';
|
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' | 'other';
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export interface ApiUserFullInfo {
|
|||||||
botInfo?: ApiBotInfo;
|
botInfo?: ApiBotInfo;
|
||||||
profilePhoto?: ApiPhoto;
|
profilePhoto?: ApiPhoto;
|
||||||
fallbackPhoto?: ApiPhoto;
|
fallbackPhoto?: ApiPhoto;
|
||||||
|
personalPhoto?: ApiPhoto;
|
||||||
noVoiceMessages?: boolean;
|
noVoiceMessages?: boolean;
|
||||||
premiumGifts?: ApiPremiumGiftOption[];
|
premiumGifts?: ApiPremiumGiftOption[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,8 +54,11 @@ type OwnProps = {
|
|||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
withVideo?: boolean;
|
withVideo?: boolean;
|
||||||
noLoop?: boolean;
|
noLoop?: boolean;
|
||||||
|
loopIndefinitely?: boolean;
|
||||||
animationLevel?: AnimationLevel;
|
animationLevel?: AnimationLevel;
|
||||||
|
noPersonalPhoto?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
showVideoOverwrite?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => void;
|
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => void;
|
||||||
};
|
};
|
||||||
@ -71,8 +74,11 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
withVideo,
|
withVideo,
|
||||||
noLoop,
|
noLoop,
|
||||||
|
loopIndefinitely,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
showVideoOverwrite,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
noPersonalPhoto,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
@ -87,24 +93,30 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
let imageHash: string | undefined;
|
let imageHash: string | undefined;
|
||||||
let videoHash: string | undefined;
|
let videoHash: string | undefined;
|
||||||
|
|
||||||
|
const shouldShowUserVideo = !VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX
|
||||||
|
&& user?.isPremium && user?.hasVideoAvatar;
|
||||||
|
const shouldShowPhotoVideo = showVideoOverwrite && photo?.isVideo;
|
||||||
const shouldShowVideo = (
|
const shouldShowVideo = (
|
||||||
!VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX
|
isIntersecting && withVideo && (shouldShowPhotoVideo || shouldShowUserVideo)
|
||||||
&& isIntersecting && withVideo && user?.isPremium && user?.hasVideoAvatar
|
|
||||||
);
|
);
|
||||||
const profilePhoto = user?.fullInfo?.profilePhoto;
|
const profilePhoto = user?.fullInfo?.personalPhoto || user?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto;
|
||||||
const shouldLoadVideo = shouldShowVideo && profilePhoto?.isVideo;
|
const hasProfileVideo = profilePhoto?.isVideo;
|
||||||
|
const shouldLoadVideo = shouldShowVideo && (hasProfileVideo || shouldShowPhotoVideo);
|
||||||
|
|
||||||
const shouldFetchBig = size === 'jumbo';
|
const shouldFetchBig = size === 'jumbo';
|
||||||
if (!isSavedMessages && !isDeleted) {
|
if (!isSavedMessages && !isDeleted) {
|
||||||
if (user) {
|
if (user && !noPersonalPhoto) {
|
||||||
imageHash = getChatAvatarHash(user, shouldFetchBig ? 'big' : undefined);
|
imageHash = getChatAvatarHash(user, shouldFetchBig ? 'big' : undefined);
|
||||||
} else if (chat) {
|
} else if (chat) {
|
||||||
imageHash = getChatAvatarHash(chat, shouldFetchBig ? 'big' : undefined);
|
imageHash = getChatAvatarHash(chat, shouldFetchBig ? 'big' : undefined);
|
||||||
} else if (photo) {
|
} else if (photo) {
|
||||||
imageHash = `photo${photo.id}?size=m`;
|
imageHash = `photo${photo.id}?size=m`;
|
||||||
|
if (photo.isVideo && withVideo) {
|
||||||
|
videoHash = `videoAvatar${photo.id}?size=u`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldLoadVideo) {
|
if (hasProfileVideo) {
|
||||||
videoHash = getChatAvatarHash(user!, undefined, 'video');
|
videoHash = getChatAvatarHash(user!, undefined, 'video');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,11 +132,13 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
const video = e.currentTarget;
|
const video = e.currentTarget;
|
||||||
if (!videoBlobUrl) return;
|
if (!videoBlobUrl) return;
|
||||||
|
|
||||||
|
if (loopIndefinitely) return;
|
||||||
|
|
||||||
videoLoopCountRef.current += 1;
|
videoLoopCountRef.current += 1;
|
||||||
if (videoLoopCountRef.current >= LOOP_COUNT || noLoop) {
|
if (videoLoopCountRef.current >= LOOP_COUNT || noLoop) {
|
||||||
video.style.display = 'none';
|
video.style.display = 'none';
|
||||||
}
|
}
|
||||||
}, [noLoop, videoBlobUrl]);
|
}, [loopIndefinitely, noLoop, videoBlobUrl]);
|
||||||
|
|
||||||
const userId = user?.id;
|
const userId = user?.id;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -157,8 +171,9 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
<OptimizedVideo
|
<OptimizedVideo
|
||||||
canPlay
|
canPlay
|
||||||
src={videoBlobUrl}
|
src={videoBlobUrl}
|
||||||
className={buildClassName(cn.media, 'avatar-media')}
|
className={buildClassName(cn.media, 'avatar-media', 'poster')}
|
||||||
muted
|
muted
|
||||||
|
loop={loopIndefinitely}
|
||||||
autoPlay
|
autoPlay
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
playsInline
|
playsInline
|
||||||
|
|||||||
@ -258,6 +258,17 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<div className={styles.photoWrapper}>
|
<div className={styles.photoWrapper}>
|
||||||
{renderPhotoTabs()}
|
{renderPhotoTabs()}
|
||||||
|
{!forceShowSelf && user?.fullInfo?.personalPhoto && (
|
||||||
|
<div className={buildClassName(
|
||||||
|
styles.fallbackPhoto,
|
||||||
|
isFirst && styles.fallbackPhotoVisible,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.fallbackPhotoContents}>
|
||||||
|
{lang(user.fullInfo.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{forceShowSelf && user?.fullInfo?.fallbackPhoto && (
|
{forceShowSelf && user?.fullInfo?.fallbackPhoto && (
|
||||||
<div className={buildClassName(
|
<div className={buildClassName(
|
||||||
styles.fallbackPhoto,
|
styles.fallbackPhoto,
|
||||||
|
|||||||
@ -56,7 +56,10 @@ 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 userOrChat = user || chat;
|
const userOrChat = user || chat;
|
||||||
const currentPhoto = photo || userOrChat?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto;
|
const currentPhoto = photo
|
||||||
|
|| user?.fullInfo?.personalPhoto
|
||||||
|
|| userOrChat?.fullInfo?.profilePhoto
|
||||||
|
|| user?.fullInfo?.fallbackPhoto;
|
||||||
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
|
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
|
||||||
const { isVideo } = currentPhoto || {};
|
const { isVideo } = currentPhoto || {};
|
||||||
|
|
||||||
|
|||||||
@ -373,7 +373,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
||||||
|
|
||||||
useOnChange(() => {
|
useOnChange(() => {
|
||||||
if (nextSettingsScreen) {
|
if (nextSettingsScreen !== undefined) {
|
||||||
setContent(LeftColumnContent.Settings);
|
setContent(LeftColumnContent.Settings);
|
||||||
setSettingsScreen(nextSettingsScreen);
|
setSettingsScreen(nextSettingsScreen);
|
||||||
requestNextSettingsScreen(undefined);
|
requestNextSettingsScreen(undefined);
|
||||||
|
|||||||
@ -221,6 +221,10 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
|
||||||
|
&.transition-circle {
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease, border-radius 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
&.rounded-corners {
|
&.rounded-corners {
|
||||||
border-radius: var(--border-radius-messages);
|
border-radius: var(--border-radius-messages);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,26 +3,22 @@ import React, {
|
|||||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type {
|
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types';
|
||||||
ApiChat, ApiMessage, ApiUser,
|
|
||||||
} from '../../api/types';
|
|
||||||
import type { AnimationLevel } from '../../types';
|
import type { AnimationLevel } from '../../types';
|
||||||
import { MediaViewerOrigin } from '../../types';
|
import { MediaViewerOrigin } from '../../types';
|
||||||
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
import {
|
import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers';
|
||||||
getChatMediaMessageIds, isChatAdmin, isUserId,
|
|
||||||
} from '../../global/helpers';
|
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
|
selectChatScheduledMessages,
|
||||||
selectCurrentMediaSearch,
|
selectCurrentMediaSearch,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
selectListedIds,
|
selectListedIds,
|
||||||
selectOutlyingIds,
|
selectOutlyingIds,
|
||||||
selectScheduledMessage,
|
selectScheduledMessage,
|
||||||
selectChatScheduledMessages,
|
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||||
@ -113,6 +109,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
webPagePhoto,
|
webPagePhoto,
|
||||||
webPageVideo,
|
webPageVideo,
|
||||||
isVideo,
|
isVideo,
|
||||||
|
actionPhoto,
|
||||||
isPhoto,
|
isPhoto,
|
||||||
bestImageData,
|
bestImageData,
|
||||||
bestData,
|
bestData,
|
||||||
@ -129,7 +126,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
const isVisible = !isHidden && isOpen;
|
const isVisible = !isHidden && isOpen;
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined;
|
const singleMediaId = webPagePhoto || webPageVideo || actionPhoto ? mediaId : undefined;
|
||||||
|
|
||||||
const mediaIds = useMemo(() => {
|
const mediaIds = useMemo(() => {
|
||||||
if (singleMediaId) {
|
if (singleMediaId) {
|
||||||
@ -460,7 +457,9 @@ export default memo(withGlobal(
|
|||||||
}
|
}
|
||||||
let collectionIds: number[] | undefined;
|
let collectionIds: number[] | undefined;
|
||||||
|
|
||||||
if (origin === MediaViewerOrigin.Inline || origin === MediaViewerOrigin.Album) {
|
if (origin === MediaViewerOrigin.Inline
|
||||||
|
|| origin === MediaViewerOrigin.Album
|
||||||
|
|| origin === MediaViewerOrigin.SuggestedAvatar) {
|
||||||
collectionIds = selectOutlyingIds(global, chatId, threadId) || selectListedIds(global, chatId, threadId);
|
collectionIds = selectOutlyingIds(global, chatId, threadId) || selectListedIds(global, chatId, threadId);
|
||||||
} else if (origin === MediaViewerOrigin.SharedMedia) {
|
} else if (origin === MediaViewerOrigin.SharedMedia) {
|
||||||
const currentSearch = selectCurrentMediaSearch(global);
|
const currentSearch = selectCurrentMediaSearch(global);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import stopEvent from '../../util/stopEvent';
|
|||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { useMediaProps } from './hooks/useMediaProps';
|
import { useMediaProps } from './hooks/useMediaProps';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
import MediaViewerFooter from './MediaViewerFooter';
|
import MediaViewerFooter from './MediaViewerFooter';
|
||||||
@ -78,11 +79,14 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
isMoving,
|
isMoving,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const isGhostAnimation = animationLevel === 2;
|
const isGhostAnimation = animationLevel === 2;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isVideo,
|
isVideo,
|
||||||
isPhoto,
|
isPhoto,
|
||||||
|
actionPhoto,
|
||||||
bestImageData,
|
bestImageData,
|
||||||
bestData,
|
bestData,
|
||||||
dimensions,
|
dimensions,
|
||||||
@ -101,7 +105,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
setControlsVisible?.(isVisible);
|
setControlsVisible?.(isVisible);
|
||||||
}, [setControlsVisible]);
|
}, [setControlsVisible]);
|
||||||
|
|
||||||
if (avatarOwner) {
|
if (avatarOwner || actionPhoto) {
|
||||||
if (!isVideoAvatar) {
|
if (!isVideoAvatar) {
|
||||||
return (
|
return (
|
||||||
<div key={chatId} className="MediaViewerContent">
|
<div key={chatId} className="MediaViewerContent">
|
||||||
@ -142,7 +146,9 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!message) return undefined;
|
if (!message) return undefined;
|
||||||
const textParts = renderMessageText(message);
|
const textParts = message.content.action?.type === 'suggestProfilePhoto'
|
||||||
|
? lang('Conversation.SuggestedPhotoTitle')
|
||||||
|
: renderMessageText(message);
|
||||||
const hasFooter = Boolean(textParts);
|
const hasFooter = Boolean(textParts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ApiMessage, ApiDimensions } from '../../../api/types';
|
import type { ApiDimensions, ApiMessage } from '../../../api/types';
|
||||||
|
|
||||||
import { MediaViewerOrigin } from '../../../types';
|
import { MediaViewerOrigin } from '../../../types';
|
||||||
|
|
||||||
@ -326,6 +326,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
|||||||
mediaSelector = '.avatar-media';
|
mediaSelector = '.avatar-media';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MediaViewerOrigin.SuggestedAvatar:
|
||||||
|
containerSelector = `.Transition__slide--active > .MessageList #${getMessageHtmlId(message!.id)}`;
|
||||||
|
mediaSelector = '.Avatar img';
|
||||||
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.ScheduledInline:
|
case MediaViewerOrigin.ScheduledInline:
|
||||||
case MediaViewerOrigin.Inline:
|
case MediaViewerOrigin.Inline:
|
||||||
default:
|
default:
|
||||||
@ -359,7 +364,11 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.MiddleHeaderAvatar:
|
case MediaViewerOrigin.MiddleHeaderAvatar:
|
||||||
|
case MediaViewerOrigin.SuggestedAvatar:
|
||||||
ghost.classList.add('circle');
|
ghost.classList.add('circle');
|
||||||
|
if (origin === MediaViewerOrigin.SuggestedAvatar) {
|
||||||
|
ghost.classList.add('transition-circle');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
getMessageDocument,
|
getMessageDocument,
|
||||||
getPhotoFullDimensions,
|
getPhotoFullDimensions,
|
||||||
getVideoDimensions,
|
getVideoDimensions,
|
||||||
getMessageFileSize,
|
getMessageFileSize, getMessageActionPhoto,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { useMemo } from '../../../lib/teact/teact';
|
import { useMemo } from '../../../lib/teact/teact';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
@ -44,6 +44,7 @@ export const useMediaProps = ({
|
|||||||
delay,
|
delay,
|
||||||
}: UseMediaProps) => {
|
}: UseMediaProps) => {
|
||||||
const photo = message ? getMessagePhoto(message) : undefined;
|
const photo = message ? getMessagePhoto(message) : undefined;
|
||||||
|
const actionPhoto = message ? getMessageActionPhoto(message) : undefined;
|
||||||
const video = message ? getMessageVideo(message) : undefined;
|
const video = message ? getMessageVideo(message) : undefined;
|
||||||
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
||||||
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
||||||
@ -51,9 +52,9 @@ export const useMediaProps = ({
|
|||||||
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
||||||
const videoSize = message ? getMessageFileSize(message) : undefined;
|
const videoSize = message ? getMessageFileSize(message) : undefined;
|
||||||
const avatarMedia = avatarOwner?.photos?.[mediaId];
|
const avatarMedia = avatarOwner?.photos?.[mediaId];
|
||||||
const isVideoAvatar = Boolean(avatarMedia?.isVideo);
|
const isVideoAvatar = Boolean(avatarMedia?.isVideo || actionPhoto?.isVideo);
|
||||||
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
|
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
|
||||||
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto));
|
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto || actionPhoto));
|
||||||
const { isGif } = video || webPageVideo || {};
|
const { isGif } = video || webPageVideo || {};
|
||||||
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
||||||
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
||||||
@ -73,8 +74,11 @@ export const useMediaProps = ({
|
|||||||
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (actionPhoto && isVideoAvatar && isFull) {
|
||||||
|
return `videoAvatar${actionPhoto.id}?size=u`;
|
||||||
|
}
|
||||||
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
|
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
|
||||||
}, [avatarOwner, message, avatarMedia, mediaId]);
|
}, [avatarOwner, actionPhoto, isVideoAvatar, message, avatarMedia, mediaId]);
|
||||||
|
|
||||||
const pictogramBlobUrl = useMedia(
|
const pictogramBlobUrl = useMedia(
|
||||||
message
|
message
|
||||||
@ -128,8 +132,8 @@ export const useMediaProps = ({
|
|||||||
if (message) {
|
if (message) {
|
||||||
if (isDocumentPhoto || isDocumentVideo) {
|
if (isDocumentPhoto || isDocumentVideo) {
|
||||||
dimensions = getMessageDocument(message)!.mediaSize!;
|
dimensions = getMessageDocument(message)!.mediaSize!;
|
||||||
} else if (photo || webPagePhoto) {
|
} else if (photo || webPagePhoto || actionPhoto) {
|
||||||
dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!;
|
dimensions = getPhotoFullDimensions((photo || webPagePhoto || actionPhoto)!)!;
|
||||||
} else if (video || webPageVideo) {
|
} else if (video || webPageVideo) {
|
||||||
dimensions = getVideoDimensions((video || webPageVideo)!)!;
|
dimensions = getVideoDimensions((video || webPageVideo)!)!;
|
||||||
}
|
}
|
||||||
@ -142,6 +146,7 @@ export const useMediaProps = ({
|
|||||||
photo,
|
photo,
|
||||||
video,
|
video,
|
||||||
webPagePhoto,
|
webPagePhoto,
|
||||||
|
actionPhoto,
|
||||||
webPageVideo,
|
webPageVideo,
|
||||||
isVideo,
|
isVideo,
|
||||||
isPhoto,
|
isPhoto,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import useShowTransition from '../../hooks/useShowTransition';
|
|||||||
|
|
||||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||||
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
||||||
|
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
@ -96,6 +97,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||||
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
||||||
|
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (noAppearanceAnimation) {
|
if (noAppearanceAnimation) {
|
||||||
@ -192,7 +194,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'ActionMessage message-list-item',
|
'ActionMessage message-list-item',
|
||||||
isFocused && !noFocusHighlight && 'focused',
|
isFocused && !noFocusHighlight && 'focused',
|
||||||
isGift && 'premium-gift',
|
(isGift || isSuggestedAvatar) && 'centered-action',
|
||||||
isContextMenuShown && 'has-menu-open',
|
isContextMenuShown && 'has-menu-open',
|
||||||
isLastInList && 'last-in-list',
|
isLastInList && 'last-in-list',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
@ -207,8 +209,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<span>{content}</span>
|
{!isSuggestedAvatar && <span>{content}</span>}
|
||||||
{isGift && renderGift()}
|
{isGift && renderGift()}
|
||||||
|
{isSuggestedAvatar && (
|
||||||
|
<ActionMessageSuggestedAvatar
|
||||||
|
message={message}
|
||||||
|
content={content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{contextMenuPosition && (
|
{contextMenuPosition && (
|
||||||
<ContextMenuContainer
|
<ContextMenuContainer
|
||||||
isOpen={isContextMenuOpen}
|
isOpen={isContextMenuOpen}
|
||||||
|
|||||||
120
src/components/middle/ActionMessageSuggestedAvatar.tsx
Normal file
120
src/components/middle/ActionMessageSuggestedAvatar.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { memo, useCallback, useState } from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ApiMessage } from '../../api/types';
|
||||||
|
import type { TextPart } from '../../types';
|
||||||
|
import { MediaViewerOrigin, SettingsScreens } from '../../types';
|
||||||
|
import { ApiMediaFormat, MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
|
import { getMessageMediaHash } from '../../global/helpers';
|
||||||
|
import * as mediaLoader from '../../util/mediaLoader';
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
|
||||||
|
import Avatar from '../common/Avatar';
|
||||||
|
import CropModal from '../ui/CropModal';
|
||||||
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
message: ApiMessage;
|
||||||
|
content?: TextPart;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||||
|
message,
|
||||||
|
content,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
openMediaViewer, uploadProfilePhoto, showNotification, requestNextSettingsScreen,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const { isOutgoing } = message;
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [cropModalBlob, setCropModalBlob] = useState<Blob | undefined>();
|
||||||
|
const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false);
|
||||||
|
const suggestedPhotoUrl = useMedia(getMessageMediaHash(message, 'full'));
|
||||||
|
const isVideo = message.content.action!.photo?.isVideo;
|
||||||
|
|
||||||
|
const showAvatarNotification = useCallback(() => {
|
||||||
|
showNotification({
|
||||||
|
title: lang('ApplyAvatarHintTitle'),
|
||||||
|
message: lang('ApplyAvatarHint'),
|
||||||
|
action: () => requestNextSettingsScreen(SettingsScreens.Main),
|
||||||
|
actionText: lang('Open'),
|
||||||
|
});
|
||||||
|
}, [lang, requestNextSettingsScreen, showNotification]);
|
||||||
|
|
||||||
|
const handleSetSuggestedAvatar = useCallback((file: File) => {
|
||||||
|
setCropModalBlob(undefined);
|
||||||
|
uploadProfilePhoto({ file });
|
||||||
|
showAvatarNotification();
|
||||||
|
}, [showAvatarNotification, uploadProfilePhoto]);
|
||||||
|
|
||||||
|
const handleCloseCropModal = useCallback(() => {
|
||||||
|
setCropModalBlob(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSetVideo = useCallback(async () => {
|
||||||
|
closeVideoModal();
|
||||||
|
showAvatarNotification();
|
||||||
|
|
||||||
|
// TODO Once we support uploading video avatars, add crop/trim modal here
|
||||||
|
const photo = message.content.action!.photo!;
|
||||||
|
const blobUrl = await mediaLoader.fetch(`videoAvatar${photo.id}?size=u`, ApiMediaFormat.BlobUrl);
|
||||||
|
const blob = await fetch(blobUrl).then((r) => r.blob());
|
||||||
|
uploadProfilePhoto({
|
||||||
|
file: new File([blob], 'avatar.mp4'),
|
||||||
|
isVideo: true,
|
||||||
|
videoTs: photo.videoSizes?.find((l) => l.videoStartTs !== undefined)?.videoStartTs,
|
||||||
|
});
|
||||||
|
}, [closeVideoModal, message.content.action, showAvatarNotification, uploadProfilePhoto]);
|
||||||
|
|
||||||
|
const handleViewSuggestedAvatar = async () => {
|
||||||
|
if (!isOutgoing && suggestedPhotoUrl) {
|
||||||
|
if (isVideo) {
|
||||||
|
openVideoModal();
|
||||||
|
} else {
|
||||||
|
setCropModalBlob(await fetch(suggestedPhotoUrl).then((r) => r.blob()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openMediaViewer({
|
||||||
|
chatId: message.chatId,
|
||||||
|
mediaId: message.id,
|
||||||
|
threadId: MAIN_THREAD_ID,
|
||||||
|
origin: MediaViewerOrigin.SuggestedAvatar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="action-message-suggested-avatar" tabIndex={0} role="button" onClick={handleViewSuggestedAvatar}>
|
||||||
|
<Avatar
|
||||||
|
photo={message.content.action!.photo}
|
||||||
|
showVideoOverwrite
|
||||||
|
loopIndefinitely
|
||||||
|
withVideo={isVideo}
|
||||||
|
size="jumbo"
|
||||||
|
/>
|
||||||
|
<span>{content}</span>
|
||||||
|
|
||||||
|
<span className="action-message-button">{lang(isVideo ? 'ViewVideoAction' : 'ViewPhotoAction')}</span>
|
||||||
|
<CropModal
|
||||||
|
file={cropModalBlob}
|
||||||
|
onClose={handleCloseCropModal}
|
||||||
|
onChange={handleSetSuggestedAvatar}
|
||||||
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isVideoModalOpen}
|
||||||
|
title={lang('SuggestedVideo')}
|
||||||
|
confirmHandler={handleSetVideo}
|
||||||
|
onClose={closeVideoModal}
|
||||||
|
textParts={content}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ActionMessageSuggestedAvatar);
|
||||||
@ -279,7 +279,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ActionMessage.premium-gift {
|
.ActionMessage.centered-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -296,6 +296,24 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-message-suggested-avatar {
|
||||||
|
max-width: 16rem;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1rem !important;
|
||||||
|
padding-bottom: 0.75rem !important;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
width: 6.5rem;
|
||||||
|
height: 6.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.action-message-button {
|
.action-message-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: var(--border-radius-default);
|
border-radius: var(--border-radius-default);
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useState,
|
memo, useCallback, useEffect, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiUser } from '../../../api/types';
|
import type { ApiUser } from '../../../api/types';
|
||||||
import { ManagementProgress } from '../../../types';
|
import { ManagementProgress } from '../../../types';
|
||||||
|
|
||||||
|
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||||
import {
|
import {
|
||||||
selectChat, selectNotifyExceptions, selectNotifySettings, selectUser,
|
selectChat, selectNotifyExceptions, selectNotifySettings, selectUser,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import { selectIsChatMuted } from '../../../global/helpers';
|
import { isUserBot, selectIsChatMuted } from '../../../global/helpers';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
@ -23,6 +24,8 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
|
|||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
import SelectAvatar from '../../ui/SelectAvatar';
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
|
||||||
import './Management.scss';
|
import './Management.scss';
|
||||||
|
|
||||||
@ -52,9 +55,11 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
|||||||
updateContact,
|
updateContact,
|
||||||
deleteContact,
|
deleteContact,
|
||||||
closeManagement,
|
closeManagement,
|
||||||
|
uploadContactProfilePhoto,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||||
|
const [isResetPersonalPhotoDialogOpen, openResetPersonalPhotoDialog, closeResetPersonalPhotoDialog] = useFlag();
|
||||||
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
|
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>();
|
const [error, setError] = useState<string | undefined>();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -130,11 +135,39 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
|||||||
closeManagement();
|
closeManagement();
|
||||||
}, [closeDeleteDialog, closeManagement, deleteContact, userId]);
|
}, [closeDeleteDialog, closeManagement, deleteContact, userId]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const isSuggestRef = useRef(false);
|
||||||
|
|
||||||
|
const handleSuggestPhoto = useCallback(() => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
isSuggestRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSetPersonalPhoto = useCallback(() => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
isSuggestRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResetPersonalAvatar = useCallback(() => {
|
||||||
|
closeResetPersonalPhotoDialog();
|
||||||
|
setIsProfileFieldsTouched(true);
|
||||||
|
uploadContactProfilePhoto({ userId });
|
||||||
|
}, [closeResetPersonalPhotoDialog, uploadContactProfilePhoto, userId]);
|
||||||
|
|
||||||
|
const handleSelectAvatar = useCallback((file: File) => {
|
||||||
|
setIsProfileFieldsTouched(true);
|
||||||
|
uploadContactProfilePhoto({ userId, file, isSuggest: isSuggestRef.current });
|
||||||
|
}, [uploadContactProfilePhoto, userId]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canSetPersonalPhoto = !isUserBot(user) && user.id !== SERVICE_NOTIFICATIONS_USER_ID;
|
||||||
const isLoading = progress === ManagementProgress.InProgress;
|
const isLoading = progress === ManagementProgress.InProgress;
|
||||||
|
const personalPhoto = user.fullInfo?.personalPhoto;
|
||||||
|
const notPersonalPhoto = user.fullInfo?.profilePhoto || user.fullInfo?.fallbackPhoto;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Management">
|
<div className="Management">
|
||||||
@ -170,6 +203,34 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{canSetPersonalPhoto && (
|
||||||
|
<div className="section">
|
||||||
|
<ListItem icon="camera-add" ripple onClick={handleSuggestPhoto}>
|
||||||
|
{lang('UserInfo.SuggestPhoto', user.firstName)}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem icon="camera-add" ripple onClick={handleSetPersonalPhoto}>
|
||||||
|
{lang('UserInfo.SetCustomPhoto', user.firstName)}
|
||||||
|
</ListItem>
|
||||||
|
{personalPhoto && (
|
||||||
|
<ListItem
|
||||||
|
leftElement={(
|
||||||
|
<Avatar
|
||||||
|
photo={notPersonalPhoto}
|
||||||
|
noPersonalPhoto
|
||||||
|
user={user}
|
||||||
|
size="mini"
|
||||||
|
className="personal-photo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ripple
|
||||||
|
onClick={openResetPersonalPhotoDialog}
|
||||||
|
>
|
||||||
|
{lang('UserInfo.ResetCustomPhoto')}
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<p className="text-muted" dir="auto">{lang('UserInfo.CustomPhotoInfo', user.firstName)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
|
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
|
||||||
{lang('DeleteContact')}
|
{lang('DeleteContact')}
|
||||||
@ -196,6 +257,18 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
|||||||
confirmHandler={handleDeleteContact}
|
confirmHandler={handleDeleteContact}
|
||||||
confirmIsDestructive
|
confirmIsDestructive
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isResetPersonalPhotoDialogOpen}
|
||||||
|
onClose={closeResetPersonalPhotoDialog}
|
||||||
|
text={lang('UserInfo.ResetToOriginalAlertText', user.firstName)}
|
||||||
|
confirmLabel={lang('Reset')}
|
||||||
|
confirmHandler={handleResetPersonalAvatar}
|
||||||
|
confirmIsDestructive
|
||||||
|
/>
|
||||||
|
<SelectAvatar
|
||||||
|
onChange={handleSelectAvatar}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,11 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.personal-photo {
|
||||||
|
margin-right: 2rem;
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
|
|||||||
@ -15,7 +15,7 @@ type OwnProps = {
|
|||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
header?: TeactNode;
|
header?: TeactNode;
|
||||||
textParts?: TextPart[];
|
textParts?: TextPart;
|
||||||
text?: string;
|
text?: string;
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
confirmHandler: () => void;
|
confirmHandler: () => void;
|
||||||
|
|||||||
@ -37,7 +37,7 @@ async function ensureCroppie() {
|
|||||||
|
|
||||||
let cropper: Croppie;
|
let cropper: Croppie;
|
||||||
|
|
||||||
async function initCropper(imgFile: File) {
|
async function initCropper(imgFile: Blob) {
|
||||||
try {
|
try {
|
||||||
const cropContainer = document.getElementById('avatar-crop');
|
const cropContainer = document.getElementById('avatar-crop');
|
||||||
if (!cropContainer) {
|
if (!cropContainer) {
|
||||||
@ -70,7 +70,7 @@ async function initCropper(imgFile: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
file?: File;
|
file?: Blob;
|
||||||
onChange: (file: File) => void;
|
onChange: (file: File) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,9 +92,11 @@ addActionHandler('setAuthPassword', (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('uploadProfilePhoto', async (global, actions, payload) => {
|
addActionHandler('uploadProfilePhoto', async (global, actions, payload) => {
|
||||||
const { file, isFallback } = payload!;
|
const {
|
||||||
|
file, isFallback, isVideo, videoTs,
|
||||||
|
} = payload!;
|
||||||
|
|
||||||
const result = await callApi('uploadProfilePhoto', file, isFallback);
|
const result = await callApi('uploadProfilePhoto', file, isFallback, isVideo, videoTs);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import {
|
|||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
||||||
import { migrateChat } from './chats';
|
import { migrateChat } from './chats';
|
||||||
import { isChatBasicGroup } from '../../helpers';
|
import { getUserFirstOrLastName, isChatBasicGroup } from '../../helpers';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
|
import * as langProvider from '../../../util/langProvider';
|
||||||
|
|
||||||
addActionHandler('checkPublicLink', async (global, actions, payload) => {
|
addActionHandler('checkPublicLink', async (global, actions, payload) => {
|
||||||
const { chatId } = selectCurrentMessageList(global) || {};
|
const { chatId } = selectCurrentMessageList(global) || {};
|
||||||
@ -399,3 +400,52 @@ addActionHandler('hideChatReportPanel', async (global, actions, payload) => {
|
|||||||
settings: undefined,
|
settings: undefined,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('uploadContactProfilePhoto', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { userId, file, isSuggest } = payload;
|
||||||
|
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
global = updateManagementProgress(global, ManagementProgress.InProgress);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
const result = await callApi('uploadContactProfilePhoto', {
|
||||||
|
user,
|
||||||
|
file,
|
||||||
|
isSuggest,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateManagementProgress(global, ManagementProgress.Error);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
const { id, accessHash } = user;
|
||||||
|
const newUser = await callApi('fetchFullUser', { id, accessHash });
|
||||||
|
if (!newUser) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateManagementProgress(global, ManagementProgress.Error);
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.loadProfilePhotos({ profileId: userId });
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateManagementProgress(global, ManagementProgress.Complete);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (file && !isSuggest) {
|
||||||
|
actions.showNotification({
|
||||||
|
message: langProvider.getTranslation('UserInfo.SetCustomPhoto.SuccessPhotoText', getUserFirstOrLastName(user)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -40,7 +40,12 @@ addActionHandler('loadFullUser', async (global, actions, payload) => {
|
|||||||
const newUser = await callApi('fetchFullUser', { id, accessHash });
|
const newUser = await callApi('fetchFullUser', { id, accessHash });
|
||||||
if (!newUser) return;
|
if (!newUser) return;
|
||||||
|
|
||||||
if (user.avatarHash !== newUser.avatarHash && user.photos?.length) {
|
const hasChangedAvatarHash = user.avatarHash !== newUser.avatarHash;
|
||||||
|
const hasChangedProfilePhoto = user.fullInfo?.profilePhoto?.id !== newUser.fullInfo?.profilePhoto?.id;
|
||||||
|
const hasChangedFallbackPhoto = user.fullInfo?.fallbackPhoto?.id !== newUser.fullInfo?.fallbackPhoto?.id;
|
||||||
|
const hasChangedPersonalPhoto = user.fullInfo?.personalPhoto?.id !== newUser.fullInfo?.personalPhoto?.id;
|
||||||
|
if ((hasChangedAvatarHash || hasChangedProfilePhoto || hasChangedFallbackPhoto || hasChangedPersonalPhoto)
|
||||||
|
&& user.photos?.length) {
|
||||||
actions.loadProfilePhotos({ profileId: userId });
|
actions.loadProfilePhotos({ profileId: userId });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -266,7 +271,9 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload) => {
|
|||||||
const { photos, users } = result;
|
const { photos, users } = result;
|
||||||
photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1));
|
photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1));
|
||||||
const fallbackPhoto = user?.fullInfo?.fallbackPhoto;
|
const fallbackPhoto = user?.fullInfo?.fallbackPhoto;
|
||||||
|
const personalPhoto = user?.fullInfo?.personalPhoto;
|
||||||
if (fallbackPhoto) photos.push(fallbackPhoto);
|
if (fallbackPhoto) photos.push(fallbackPhoto);
|
||||||
|
if (personalPhoto) photos.unshift(personalPhoto);
|
||||||
|
|
||||||
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
|
||||||
|
|||||||
@ -132,10 +132,13 @@ export function getChatAvatarHash(
|
|||||||
const { fullInfo } = owner;
|
const { fullInfo } = owner;
|
||||||
|
|
||||||
if (type === 'video') {
|
if (type === 'video') {
|
||||||
|
const userFullInfo = isUserId(owner.id) ? fullInfo as ApiUserFullInfo : undefined;
|
||||||
|
if (userFullInfo?.personalPhoto?.isVideo) {
|
||||||
|
return getVideoAvatarMediaHash(userFullInfo.personalPhoto);
|
||||||
|
}
|
||||||
if (fullInfo?.profilePhoto?.isVideo) {
|
if (fullInfo?.profilePhoto?.isVideo) {
|
||||||
return getVideoAvatarMediaHash(fullInfo.profilePhoto);
|
return getVideoAvatarMediaHash(fullInfo.profilePhoto);
|
||||||
}
|
}
|
||||||
const userFullInfo = isUserId(owner.id) ? fullInfo as ApiUserFullInfo : undefined;
|
|
||||||
if (userFullInfo?.fallbackPhoto?.isVideo) {
|
if (userFullInfo?.fallbackPhoto?.isVideo) {
|
||||||
return getVideoAvatarMediaHash(userFullInfo.fallbackPhoto);
|
return getVideoAvatarMediaHash(userFullInfo.fallbackPhoto);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,10 @@ export function getMessagePhoto(message: ApiMessage) {
|
|||||||
return message.content.photo;
|
return message.content.photo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMessageActionPhoto(message: ApiMessage) {
|
||||||
|
return message.content.action?.type === 'suggestProfilePhoto' ? message.content.action.photo : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getMessageVideo(message: ApiMessage) {
|
export function getMessageVideo(message: ApiMessage) {
|
||||||
return message.content.video;
|
return message.content.video;
|
||||||
}
|
}
|
||||||
@ -180,13 +184,14 @@ export function getMessageMediaHash(
|
|||||||
target: Target,
|
target: Target,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
photo, video, sticker, audio, voice, document,
|
video, sticker, audio, voice, document,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
const messagePhoto = photo || getMessageWebPagePhoto(message) || getMessageDocumentPhoto(message);
|
const messagePhoto = getMessagePhoto(message) || getMessageWebPagePhoto(message) || getMessageDocumentPhoto(message);
|
||||||
|
const actionPhoto = getMessageActionPhoto(message);
|
||||||
const messageVideo = video || getMessageWebPageVideo(message) || getMessageDocumentVideo(message);
|
const messageVideo = video || getMessageWebPageVideo(message) || getMessageDocumentVideo(message);
|
||||||
|
|
||||||
const content = messagePhoto || messageVideo || sticker || audio || voice || document;
|
const content = actionPhoto || messagePhoto || messageVideo || sticker || audio || voice || document;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -210,18 +215,18 @@ export function getMessageMediaHash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messagePhoto) {
|
if (messagePhoto || actionPhoto) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case 'micro':
|
case 'micro':
|
||||||
case 'pictogram':
|
case 'pictogram':
|
||||||
return `${base}?size=m`;
|
return `${base}?size=${actionPhoto ? 'a' : 'm'}`;
|
||||||
case 'inline':
|
case 'inline':
|
||||||
return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined;
|
return !hasMessageLocalBlobUrl(message) ? `${base}?size=${actionPhoto ? 'b' : 'x'}` : undefined;
|
||||||
case 'preview':
|
case 'preview':
|
||||||
return `${base}?size=x`;
|
return `${base}?size=${actionPhoto ? 'b' : 'x'}`;
|
||||||
case 'full':
|
case 'full':
|
||||||
case 'download':
|
case 'download':
|
||||||
return document ? base : `${base}?size=z`;
|
return document ? base : `${base}?size=${actionPhoto ? 'c' : 'z'}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1409,6 +1409,12 @@ export interface ActionPayloads {
|
|||||||
topicId: number;
|
topicId: number;
|
||||||
};
|
};
|
||||||
closeEditTopicPanel: never;
|
closeEditTopicPanel: never;
|
||||||
|
|
||||||
|
uploadContactProfilePhoto: {
|
||||||
|
userId: string;
|
||||||
|
file?: File;
|
||||||
|
isSuggest?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NonTypedActionNames = (
|
export type NonTypedActionNames = (
|
||||||
|
|||||||
@ -529,9 +529,12 @@ class TelegramClient {
|
|||||||
let media;
|
let media;
|
||||||
if (messageOrMedia instanceof constructors.Message) {
|
if (messageOrMedia instanceof constructors.Message) {
|
||||||
media = messageOrMedia.media;
|
media = messageOrMedia.media;
|
||||||
|
} else if (messageOrMedia instanceof constructors.MessageService) {
|
||||||
|
media = messageOrMedia.action.photo;
|
||||||
} else {
|
} else {
|
||||||
media = messageOrMedia;
|
media = messageOrMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof media === 'string') {
|
if (typeof media === 'string') {
|
||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1264,6 +1264,7 @@ photos.updateProfilePhoto#1c3d5956 flags:# fallback:flags.0?true id:InputPhoto =
|
|||||||
photos.uploadProfilePhoto#89f30f69 flags:# fallback:flags.3?true file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
|
photos.uploadProfilePhoto#89f30f69 flags:# fallback:flags.3?true file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
|
||||||
photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
|
photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
|
||||||
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
|
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
|
||||||
|
photos.uploadContactProfilePhoto#b91a83bf flags:# suggest:flags.3?true save:flags.4?true user_id:InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
|
||||||
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
||||||
upload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File;
|
upload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File;
|
||||||
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
||||||
|
|||||||
@ -271,5 +271,6 @@
|
|||||||
"channels.editForumTopic",
|
"channels.editForumTopic",
|
||||||
"channels.updatePinnedForumTopic",
|
"channels.updatePinnedForumTopic",
|
||||||
"channels.deleteTopicHistory",
|
"channels.deleteTopicHistory",
|
||||||
"channels.toggleParticipantsHidden"
|
"channels.toggleParticipantsHidden",
|
||||||
|
"photos.uploadContactProfilePhoto"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -268,6 +268,7 @@ export enum MediaViewerOrigin {
|
|||||||
Album,
|
Album,
|
||||||
ScheduledAlbum,
|
ScheduledAlbum,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
|
SuggestedAvatar,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AudioOrigin {
|
export enum AudioOrigin {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user