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) {
|
||||
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 {
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
fullUser: {
|
||||
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
||||
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
||||
fallbackPhoto,
|
||||
fallbackPhoto, personalPhoto,
|
||||
},
|
||||
users,
|
||||
} = mtpUserFull;
|
||||
@ -27,6 +27,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
fullInfo: {
|
||||
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
||||
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
||||
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }),
|
||||
bio: about,
|
||||
commonChatsCount,
|
||||
pinnedMessageId: pinnedMsgId,
|
||||
|
||||
@ -62,6 +62,7 @@ export {
|
||||
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
||||
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig,
|
||||
fetchGlobalPrivacySettings, updateGlobalPrivacySettings, toggleUsername, reorderUsernames, fetchConfig,
|
||||
uploadContactProfilePhoto,
|
||||
} from './settings';
|
||||
|
||||
export {
|
||||
|
||||
@ -160,7 +160,9 @@ async function download(
|
||||
let mimeType;
|
||||
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);
|
||||
if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) {
|
||||
fullSize = entity.media.document.size.toJSNumber();
|
||||
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
ApiError,
|
||||
ApiLangString,
|
||||
ApiLanguage,
|
||||
ApiNotifyException, ApiPhoto,
|
||||
ApiNotifyException, ApiPhoto, ApiUser,
|
||||
} from '../../types';
|
||||
import type { ApiPrivacyKey, InputPrivacyRules, LangCode } from '../../../types';
|
||||
import type { LANG_PACKS } from '../../../config';
|
||||
@ -101,10 +101,10 @@ export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean)
|
||||
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 result = await invokeRequest(new GramJs.photos.UploadProfilePhoto({
|
||||
file: inputFile,
|
||||
...(isVideo ? { video: inputFile, videoStartTs: videoTs } : { file: inputFile }),
|
||||
...(isFallback ? { fallback: true } : undefined),
|
||||
}));
|
||||
|
||||
@ -121,6 +121,40 @@ export async function uploadProfilePhoto(file: File, isFallback?: boolean) {
|
||||
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[]) {
|
||||
const photoIds = photos.map(buildInputPhoto).filter(Boolean);
|
||||
const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), true);
|
||||
|
||||
@ -46,11 +46,16 @@ export async function fetchFullUser({
|
||||
}
|
||||
|
||||
updateLocalDb(fullInfo);
|
||||
addUserToLocalDb(fullInfo.users[0], true);
|
||||
|
||||
if (fullInfo.fullUser.profilePhoto instanceof GramJs.Photo) {
|
||||
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) {
|
||||
localDb.photos[fullInfo.fullUser.fallbackPhoto.id.toString()] = fullInfo.fullUser.fallbackPhoto;
|
||||
}
|
||||
|
||||
@ -181,7 +181,10 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -263,7 +263,7 @@ export interface ApiAction {
|
||||
text: string;
|
||||
targetUserIds?: string[];
|
||||
targetChatId?: string;
|
||||
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'other';
|
||||
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' | 'other';
|
||||
photo?: ApiPhoto;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
|
||||
@ -42,6 +42,7 @@ export interface ApiUserFullInfo {
|
||||
botInfo?: ApiBotInfo;
|
||||
profilePhoto?: ApiPhoto;
|
||||
fallbackPhoto?: ApiPhoto;
|
||||
personalPhoto?: ApiPhoto;
|
||||
noVoiceMessages?: boolean;
|
||||
premiumGifts?: ApiPremiumGiftOption[];
|
||||
}
|
||||
|
||||
@ -54,8 +54,11 @@ type OwnProps = {
|
||||
isSavedMessages?: boolean;
|
||||
withVideo?: boolean;
|
||||
noLoop?: boolean;
|
||||
loopIndefinitely?: boolean;
|
||||
animationLevel?: AnimationLevel;
|
||||
noPersonalPhoto?: boolean;
|
||||
lastSyncTime?: number;
|
||||
showVideoOverwrite?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => void;
|
||||
};
|
||||
@ -71,8 +74,11 @@ const Avatar: FC<OwnProps> = ({
|
||||
isSavedMessages,
|
||||
withVideo,
|
||||
noLoop,
|
||||
loopIndefinitely,
|
||||
lastSyncTime,
|
||||
showVideoOverwrite,
|
||||
animationLevel,
|
||||
noPersonalPhoto,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
}) => {
|
||||
@ -87,24 +93,30 @@ const Avatar: FC<OwnProps> = ({
|
||||
let imageHash: 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 = (
|
||||
!VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX
|
||||
&& isIntersecting && withVideo && user?.isPremium && user?.hasVideoAvatar
|
||||
isIntersecting && withVideo && (shouldShowPhotoVideo || shouldShowUserVideo)
|
||||
);
|
||||
const profilePhoto = user?.fullInfo?.profilePhoto;
|
||||
const shouldLoadVideo = shouldShowVideo && profilePhoto?.isVideo;
|
||||
const profilePhoto = user?.fullInfo?.personalPhoto || user?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto;
|
||||
const hasProfileVideo = profilePhoto?.isVideo;
|
||||
const shouldLoadVideo = shouldShowVideo && (hasProfileVideo || shouldShowPhotoVideo);
|
||||
|
||||
const shouldFetchBig = size === 'jumbo';
|
||||
if (!isSavedMessages && !isDeleted) {
|
||||
if (user) {
|
||||
if (user && !noPersonalPhoto) {
|
||||
imageHash = getChatAvatarHash(user, shouldFetchBig ? 'big' : undefined);
|
||||
} else if (chat) {
|
||||
imageHash = getChatAvatarHash(chat, shouldFetchBig ? 'big' : undefined);
|
||||
} else if (photo) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
@ -120,11 +132,13 @@ const Avatar: FC<OwnProps> = ({
|
||||
const video = e.currentTarget;
|
||||
if (!videoBlobUrl) return;
|
||||
|
||||
if (loopIndefinitely) return;
|
||||
|
||||
videoLoopCountRef.current += 1;
|
||||
if (videoLoopCountRef.current >= LOOP_COUNT || noLoop) {
|
||||
video.style.display = 'none';
|
||||
}
|
||||
}, [noLoop, videoBlobUrl]);
|
||||
}, [loopIndefinitely, noLoop, videoBlobUrl]);
|
||||
|
||||
const userId = user?.id;
|
||||
useEffect(() => {
|
||||
@ -157,8 +171,9 @@ const Avatar: FC<OwnProps> = ({
|
||||
<OptimizedVideo
|
||||
canPlay
|
||||
src={videoBlobUrl}
|
||||
className={buildClassName(cn.media, 'avatar-media')}
|
||||
className={buildClassName(cn.media, 'avatar-media', 'poster')}
|
||||
muted
|
||||
loop={loopIndefinitely}
|
||||
autoPlay
|
||||
disablePictureInPicture
|
||||
playsInline
|
||||
|
||||
@ -258,6 +258,17 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<div className={styles.photoWrapper}>
|
||||
{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 && (
|
||||
<div className={buildClassName(
|
||||
styles.fallbackPhoto,
|
||||
|
||||
@ -56,7 +56,10 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
const isDeleted = user && isDeletedUser(user);
|
||||
const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
|
||||
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 { isVideo } = currentPhoto || {};
|
||||
|
||||
|
||||
@ -373,7 +373,7 @@ const LeftColumn: FC<StateProps> = ({
|
||||
}, [clearTwoFaError, loadPasswordInfo, settingsScreen]);
|
||||
|
||||
useOnChange(() => {
|
||||
if (nextSettingsScreen) {
|
||||
if (nextSettingsScreen !== undefined) {
|
||||
setContent(LeftColumnContent.Settings);
|
||||
setSettingsScreen(nextSettingsScreen);
|
||||
requestNextSettingsScreen(undefined);
|
||||
|
||||
@ -221,6 +221,10 @@
|
||||
border-radius: 0;
|
||||
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 {
|
||||
border-radius: var(--border-radius-messages);
|
||||
}
|
||||
|
||||
@ -3,26 +3,22 @@ import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { ApiChat, ApiMessage, ApiUser } from '../../api/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import {
|
||||
getChatMediaMessageIds, isChatAdmin, isUserId,
|
||||
} from '../../global/helpers';
|
||||
import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectChatMessages,
|
||||
selectChatScheduledMessages,
|
||||
selectCurrentMediaSearch,
|
||||
selectIsChatWithSelf,
|
||||
selectListedIds,
|
||||
selectOutlyingIds,
|
||||
selectScheduledMessage,
|
||||
selectChatScheduledMessages,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||
@ -113,6 +109,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
webPagePhoto,
|
||||
webPageVideo,
|
||||
isVideo,
|
||||
actionPhoto,
|
||||
isPhoto,
|
||||
bestImageData,
|
||||
bestData,
|
||||
@ -129,7 +126,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const isVisible = !isHidden && isOpen;
|
||||
|
||||
/* Navigation */
|
||||
const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined;
|
||||
const singleMediaId = webPagePhoto || webPageVideo || actionPhoto ? mediaId : undefined;
|
||||
|
||||
const mediaIds = useMemo(() => {
|
||||
if (singleMediaId) {
|
||||
@ -460,7 +457,9 @@ export default memo(withGlobal(
|
||||
}
|
||||
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);
|
||||
} else if (origin === MediaViewerOrigin.SharedMedia) {
|
||||
const currentSearch = selectCurrentMediaSearch(global);
|
||||
|
||||
@ -18,6 +18,7 @@ import stopEvent from '../../util/stopEvent';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
import MediaViewerFooter from './MediaViewerFooter';
|
||||
@ -78,11 +79,14 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
isMoving,
|
||||
} = props;
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isGhostAnimation = animationLevel === 2;
|
||||
|
||||
const {
|
||||
isVideo,
|
||||
isPhoto,
|
||||
actionPhoto,
|
||||
bestImageData,
|
||||
bestData,
|
||||
dimensions,
|
||||
@ -101,7 +105,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
setControlsVisible?.(isVisible);
|
||||
}, [setControlsVisible]);
|
||||
|
||||
if (avatarOwner) {
|
||||
if (avatarOwner || actionPhoto) {
|
||||
if (!isVideoAvatar) {
|
||||
return (
|
||||
<div key={chatId} className="MediaViewerContent">
|
||||
@ -142,7 +146,9 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
}
|
||||
|
||||
if (!message) return undefined;
|
||||
const textParts = renderMessageText(message);
|
||||
const textParts = message.content.action?.type === 'suggestProfilePhoto'
|
||||
? lang('Conversation.SuggestedPhotoTitle')
|
||||
: renderMessageText(message);
|
||||
const hasFooter = Boolean(textParts);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessage, ApiDimensions } from '../../../api/types';
|
||||
import type { ApiDimensions, ApiMessage } from '../../../api/types';
|
||||
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
@ -326,6 +326,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
mediaSelector = '.avatar-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SuggestedAvatar:
|
||||
containerSelector = `.Transition__slide--active > .MessageList #${getMessageHtmlId(message!.id)}`;
|
||||
mediaSelector = '.Avatar img';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.Inline:
|
||||
default:
|
||||
@ -359,7 +364,11 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.MiddleHeaderAvatar:
|
||||
case MediaViewerOrigin.SuggestedAvatar:
|
||||
ghost.classList.add('circle');
|
||||
if (origin === MediaViewerOrigin.SuggestedAvatar) {
|
||||
ghost.classList.add('transition-circle');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
getMessageDocument,
|
||||
getPhotoFullDimensions,
|
||||
getVideoDimensions,
|
||||
getMessageFileSize,
|
||||
getMessageFileSize, getMessageActionPhoto,
|
||||
} from '../../../global/helpers';
|
||||
import { useMemo } from '../../../lib/teact/teact';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
@ -44,6 +44,7 @@ export const useMediaProps = ({
|
||||
delay,
|
||||
}: UseMediaProps) => {
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
const actionPhoto = message ? getMessageActionPhoto(message) : undefined;
|
||||
const video = message ? getMessageVideo(message) : undefined;
|
||||
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
||||
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
||||
@ -51,9 +52,9 @@ export const useMediaProps = ({
|
||||
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
||||
const videoSize = message ? getMessageFileSize(message) : undefined;
|
||||
const avatarMedia = avatarOwner?.photos?.[mediaId];
|
||||
const isVideoAvatar = Boolean(avatarMedia?.isVideo);
|
||||
const isVideoAvatar = Boolean(avatarMedia?.isVideo || actionPhoto?.isVideo);
|
||||
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 isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
||||
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
||||
@ -73,8 +74,11 @@ export const useMediaProps = ({
|
||||
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
||||
}
|
||||
}
|
||||
if (actionPhoto && isVideoAvatar && isFull) {
|
||||
return `videoAvatar${actionPhoto.id}?size=u`;
|
||||
}
|
||||
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
|
||||
}, [avatarOwner, message, avatarMedia, mediaId]);
|
||||
}, [avatarOwner, actionPhoto, isVideoAvatar, message, avatarMedia, mediaId]);
|
||||
|
||||
const pictogramBlobUrl = useMedia(
|
||||
message
|
||||
@ -128,8 +132,8 @@ export const useMediaProps = ({
|
||||
if (message) {
|
||||
if (isDocumentPhoto || isDocumentVideo) {
|
||||
dimensions = getMessageDocument(message)!.mediaSize!;
|
||||
} else if (photo || webPagePhoto) {
|
||||
dimensions = getPhotoFullDimensions((photo || webPagePhoto)!)!;
|
||||
} else if (photo || webPagePhoto || actionPhoto) {
|
||||
dimensions = getPhotoFullDimensions((photo || webPagePhoto || actionPhoto)!)!;
|
||||
} else if (video || webPageVideo) {
|
||||
dimensions = getVideoDimensions((video || webPageVideo)!)!;
|
||||
}
|
||||
@ -142,6 +146,7 @@ export const useMediaProps = ({
|
||||
photo,
|
||||
video,
|
||||
webPagePhoto,
|
||||
actionPhoto,
|
||||
webPageVideo,
|
||||
isVideo,
|
||||
isPhoto,
|
||||
|
||||
@ -31,6 +31,7 @@ import useShowTransition from '../../hooks/useShowTransition';
|
||||
|
||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
||||
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
@ -96,6 +97,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
||||
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
|
||||
|
||||
useEffect(() => {
|
||||
if (noAppearanceAnimation) {
|
||||
@ -192,7 +194,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
const className = buildClassName(
|
||||
'ActionMessage message-list-item',
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
isGift && 'premium-gift',
|
||||
(isGift || isSuggestedAvatar) && 'centered-action',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isLastInList && 'last-in-list',
|
||||
transitionClassNames,
|
||||
@ -207,8 +209,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span>{content}</span>
|
||||
{!isSuggestedAvatar && <span>{content}</span>}
|
||||
{isGift && renderGift()}
|
||||
{isSuggestedAvatar && (
|
||||
<ActionMessageSuggestedAvatar
|
||||
message={message}
|
||||
content={content}
|
||||
/>
|
||||
)}
|
||||
{contextMenuPosition && (
|
||||
<ContextMenuContainer
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -296,6 +296,24 @@
|
||||
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 {
|
||||
display: inline-block;
|
||||
border-radius: var(--border-radius-default);
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useState,
|
||||
memo, useCallback, useEffect, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import { ManagementProgress } from '../../../types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
import {
|
||||
selectChat, selectNotifyExceptions, selectNotifySettings, selectUser,
|
||||
} from '../../../global/selectors';
|
||||
import { selectIsChatMuted } from '../../../global/helpers';
|
||||
import { isUserBot, selectIsChatMuted } from '../../../global/helpers';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -23,6 +24,8 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import SelectAvatar from '../../ui/SelectAvatar';
|
||||
import Avatar from '../../common/Avatar';
|
||||
|
||||
import './Management.scss';
|
||||
|
||||
@ -52,9 +55,11 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
updateContact,
|
||||
deleteContact,
|
||||
closeManagement,
|
||||
uploadContactProfilePhoto,
|
||||
} = getActions();
|
||||
|
||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||
const [isResetPersonalPhotoDialogOpen, openResetPersonalPhotoDialog, closeResetPersonalPhotoDialog] = useFlag();
|
||||
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const lang = useLang();
|
||||
@ -130,11 +135,39 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
closeManagement();
|
||||
}, [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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const canSetPersonalPhoto = !isUserBot(user) && user.id !== SERVICE_NOTIFICATIONS_USER_ID;
|
||||
const isLoading = progress === ManagementProgress.InProgress;
|
||||
const personalPhoto = user.fullInfo?.personalPhoto;
|
||||
const notPersonalPhoto = user.fullInfo?.profilePhoto || user.fullInfo?.fallbackPhoto;
|
||||
|
||||
return (
|
||||
<div className="Management">
|
||||
@ -170,6 +203,34 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
</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">
|
||||
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
|
||||
{lang('DeleteContact')}
|
||||
@ -196,6 +257,18 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
confirmHandler={handleDeleteContact}
|
||||
confirmIsDestructive
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isResetPersonalPhotoDialogOpen}
|
||||
onClose={closeResetPersonalPhotoDialog}
|
||||
text={lang('UserInfo.ResetToOriginalAlertText', user.firstName)}
|
||||
confirmLabel={lang('Reset')}
|
||||
confirmHandler={handleResetPersonalAvatar}
|
||||
confirmIsDestructive
|
||||
/>
|
||||
<SelectAvatar
|
||||
onChange={handleSelectAvatar}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.personal-photo {
|
||||
margin-right: 2rem;
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--color-background);
|
||||
|
||||
@ -15,7 +15,7 @@ type OwnProps = {
|
||||
onCloseAnimationEnd?: () => void;
|
||||
title?: string;
|
||||
header?: TeactNode;
|
||||
textParts?: TextPart[];
|
||||
textParts?: TextPart;
|
||||
text?: string;
|
||||
confirmLabel?: string;
|
||||
confirmHandler: () => void;
|
||||
|
||||
@ -37,7 +37,7 @@ async function ensureCroppie() {
|
||||
|
||||
let cropper: Croppie;
|
||||
|
||||
async function initCropper(imgFile: File) {
|
||||
async function initCropper(imgFile: Blob) {
|
||||
try {
|
||||
const cropContainer = document.getElementById('avatar-crop');
|
||||
if (!cropContainer) {
|
||||
@ -70,7 +70,7 @@ async function initCropper(imgFile: File) {
|
||||
}
|
||||
|
||||
type OwnProps = {
|
||||
file?: File;
|
||||
file?: Blob;
|
||||
onChange: (file: File) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@ -92,9 +92,11 @@ addActionHandler('setAuthPassword', (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;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
@ -8,8 +8,9 @@ import {
|
||||
} from '../../reducers';
|
||||
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
||||
import { migrateChat } from './chats';
|
||||
import { isChatBasicGroup } from '../../helpers';
|
||||
import { getUserFirstOrLastName, isChatBasicGroup } from '../../helpers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import * as langProvider from '../../../util/langProvider';
|
||||
|
||||
addActionHandler('checkPublicLink', async (global, actions, payload) => {
|
||||
const { chatId } = selectCurrentMessageList(global) || {};
|
||||
@ -399,3 +400,52 @@ addActionHandler('hideChatReportPanel', async (global, actions, payload) => {
|
||||
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 });
|
||||
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 });
|
||||
}
|
||||
});
|
||||
@ -266,7 +271,9 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload) => {
|
||||
const { photos, users } = result;
|
||||
photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1));
|
||||
const fallbackPhoto = user?.fullInfo?.fallbackPhoto;
|
||||
const personalPhoto = user?.fullInfo?.personalPhoto;
|
||||
if (fallbackPhoto) photos.push(fallbackPhoto);
|
||||
if (personalPhoto) photos.unshift(personalPhoto);
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||
|
||||
|
||||
@ -132,10 +132,13 @@ export function getChatAvatarHash(
|
||||
const { fullInfo } = owner;
|
||||
|
||||
if (type === 'video') {
|
||||
const userFullInfo = isUserId(owner.id) ? fullInfo as ApiUserFullInfo : undefined;
|
||||
if (userFullInfo?.personalPhoto?.isVideo) {
|
||||
return getVideoAvatarMediaHash(userFullInfo.personalPhoto);
|
||||
}
|
||||
if (fullInfo?.profilePhoto?.isVideo) {
|
||||
return getVideoAvatarMediaHash(fullInfo.profilePhoto);
|
||||
}
|
||||
const userFullInfo = isUserId(owner.id) ? fullInfo as ApiUserFullInfo : undefined;
|
||||
if (userFullInfo?.fallbackPhoto?.isVideo) {
|
||||
return getVideoAvatarMediaHash(userFullInfo.fallbackPhoto);
|
||||
}
|
||||
|
||||
@ -52,6 +52,10 @@ export function getMessagePhoto(message: ApiMessage) {
|
||||
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) {
|
||||
return message.content.video;
|
||||
}
|
||||
@ -180,13 +184,14 @@ export function getMessageMediaHash(
|
||||
target: Target,
|
||||
) {
|
||||
const {
|
||||
photo, video, sticker, audio, voice, document,
|
||||
video, sticker, audio, voice, document,
|
||||
} = 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 content = messagePhoto || messageVideo || sticker || audio || voice || document;
|
||||
const content = actionPhoto || messagePhoto || messageVideo || sticker || audio || voice || document;
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
@ -210,18 +215,18 @@ export function getMessageMediaHash(
|
||||
}
|
||||
}
|
||||
|
||||
if (messagePhoto) {
|
||||
if (messagePhoto || actionPhoto) {
|
||||
switch (target) {
|
||||
case 'micro':
|
||||
case 'pictogram':
|
||||
return `${base}?size=m`;
|
||||
return `${base}?size=${actionPhoto ? 'a' : 'm'}`;
|
||||
case 'inline':
|
||||
return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined;
|
||||
return !hasMessageLocalBlobUrl(message) ? `${base}?size=${actionPhoto ? 'b' : 'x'}` : undefined;
|
||||
case 'preview':
|
||||
return `${base}?size=x`;
|
||||
return `${base}?size=${actionPhoto ? 'b' : 'x'}`;
|
||||
case 'full':
|
||||
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;
|
||||
};
|
||||
closeEditTopicPanel: never;
|
||||
|
||||
uploadContactProfilePhoto: {
|
||||
userId: string;
|
||||
file?: File;
|
||||
isSuggest?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type NonTypedActionNames = (
|
||||
|
||||
@ -529,9 +529,12 @@ class TelegramClient {
|
||||
let media;
|
||||
if (messageOrMedia instanceof constructors.Message) {
|
||||
media = messageOrMedia.media;
|
||||
} else if (messageOrMedia instanceof constructors.MessageService) {
|
||||
media = messageOrMedia.action.photo;
|
||||
} else {
|
||||
media = messageOrMedia;
|
||||
}
|
||||
|
||||
if (typeof media === 'string') {
|
||||
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.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
|
||||
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.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;
|
||||
|
||||
@ -271,5 +271,6 @@
|
||||
"channels.editForumTopic",
|
||||
"channels.updatePinnedForumTopic",
|
||||
"channels.deleteTopicHistory",
|
||||
"channels.toggleParticipantsHidden"
|
||||
"channels.toggleParticipantsHidden",
|
||||
"photos.uploadContactProfilePhoto"
|
||||
]
|
||||
|
||||
@ -268,6 +268,7 @@ export enum MediaViewerOrigin {
|
||||
Album,
|
||||
ScheduledAlbum,
|
||||
SearchResult,
|
||||
SuggestedAvatar,
|
||||
}
|
||||
|
||||
export enum AudioOrigin {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user