Settings / Privacy: Public Profile Pictures (#2309)
This commit is contained in:
parent
3dab7609e3
commit
c1f3685217
@ -15,6 +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,
|
||||||
},
|
},
|
||||||
users,
|
users,
|
||||||
} = mtpUserFull;
|
} = mtpUserFull;
|
||||||
@ -25,6 +26,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
|
|||||||
...user,
|
...user,
|
||||||
fullInfo: {
|
fullInfo: {
|
||||||
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
||||||
|
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
||||||
bio: about,
|
bio: about,
|
||||||
commonChatsCount,
|
commonChatsCount,
|
||||||
pinnedMessageId: pinnedMsgId,
|
pinnedMessageId: pinnedMsgId,
|
||||||
|
|||||||
@ -82,10 +82,11 @@ export function updateUsername(username: string) {
|
|||||||
return invokeRequest(new GramJs.account.UpdateUsername({ username }), true);
|
return invokeRequest(new GramJs.account.UpdateUsername({ username }), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProfilePhoto(photo?: ApiPhoto) {
|
export async function updateProfilePhoto(photo?: ApiPhoto, isFallback?: boolean) {
|
||||||
const photoId = photo ? buildInputPhoto(photo) : new GramJs.InputPhotoEmpty();
|
const photoId = photo ? buildInputPhoto(photo) : new GramJs.InputPhotoEmpty();
|
||||||
const result = await invokeRequest(new GramJs.photos.UpdateProfilePhoto({
|
const result = await invokeRequest(new GramJs.photos.UpdateProfilePhoto({
|
||||||
id: photoId,
|
id: photoId,
|
||||||
|
...(isFallback ? { fallback: true } : undefined),
|
||||||
}));
|
}));
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
|
|
||||||
@ -100,10 +101,11 @@ export async function updateProfilePhoto(photo?: ApiPhoto) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadProfilePhoto(file: File) {
|
export async function uploadProfilePhoto(file: File, isFallback?: boolean) {
|
||||||
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,
|
file: inputFile,
|
||||||
|
...(isFallback ? { fallback: true } : undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
|
|||||||
@ -45,10 +45,16 @@ export async function fetchFullUser({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLocalDb(fullInfo);
|
||||||
|
|
||||||
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.fallbackPhoto instanceof GramJs.Photo) {
|
||||||
|
localDb.photos[fullInfo.fullUser.fallbackPhoto.id.toString()] = fullInfo.fullUser.fallbackPhoto;
|
||||||
|
}
|
||||||
|
|
||||||
const botInfo = fullInfo.fullUser.botInfo;
|
const botInfo = fullInfo.fullUser.botInfo;
|
||||||
if (botInfo?.descriptionPhoto instanceof GramJs.Photo) {
|
if (botInfo?.descriptionPhoto instanceof GramJs.Photo) {
|
||||||
localDb.photos[botInfo.descriptionPhoto.id.toString()] = botInfo.descriptionPhoto;
|
localDb.photos[botInfo.descriptionPhoto.id.toString()] = botInfo.descriptionPhoto;
|
||||||
@ -58,11 +64,14 @@ export async function fetchFullUser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userWithFullInfo = buildApiUserFromFull(fullInfo);
|
const userWithFullInfo = buildApiUserFromFull(fullInfo);
|
||||||
|
const user = buildApiUser(fullInfo.users[0]);
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateUser',
|
'@type': 'updateUser',
|
||||||
id,
|
id,
|
||||||
user: {
|
user: {
|
||||||
|
...user,
|
||||||
|
avatarHash: user?.avatarHash || undefined,
|
||||||
fullInfo: userWithFullInfo.fullInfo,
|
fullInfo: userWithFullInfo.fullInfo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface ApiUserFullInfo {
|
|||||||
pinnedMessageId?: number;
|
pinnedMessageId?: number;
|
||||||
botInfo?: ApiBotInfo;
|
botInfo?: ApiBotInfo;
|
||||||
profilePhoto?: ApiPhoto;
|
profilePhoto?: ApiPhoto;
|
||||||
|
fallbackPhoto?: ApiPhoto;
|
||||||
noVoiceMessages?: boolean;
|
noVoiceMessages?: boolean;
|
||||||
premiumGifts?: ApiPremiumGiftOption[];
|
premiumGifts?: ApiPremiumGiftOption[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.size-mini {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.size-small {
|
&.size-small {
|
||||||
width: 2.125rem;
|
width: 2.125rem;
|
||||||
height: 2.125rem;
|
height: 2.125rem;
|
||||||
|
|||||||
@ -45,7 +45,7 @@ cn.icon = cn('icon');
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: 'micro' | 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
size?: 'micro' | 'tiny' | 'mini' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
user?: ApiUser;
|
user?: ApiUser;
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
|
|||||||
@ -15,6 +15,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fallbackPhoto {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
transition: 0.25s ease-in-out opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallbackPhotoContents {
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-white);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
align-items: center;
|
||||||
|
height: 1.5rem;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
transition: 0.25s ease-in-out opacity;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallbackPhotoVisible {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.fallbackPhotoContents {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallbackPhotoAvatar {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.photoWrapper {
|
.photoWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -33,7 +78,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.125rem;
|
height: 0.125rem;
|
||||||
padding: 0 0.375rem;
|
padding: 0 0.375rem;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import FullNameTitle from './FullNameTitle';
|
|||||||
import ProfilePhoto from './ProfilePhoto';
|
import ProfilePhoto from './ProfilePhoto';
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
import TopicIcon from './TopicIcon';
|
import TopicIcon from './TopicIcon';
|
||||||
|
import Avatar from './Avatar';
|
||||||
|
|
||||||
import './ProfileInfo.scss';
|
import './ProfileInfo.scss';
|
||||||
import styles from './ProfileInfo.module.scss';
|
import styles from './ProfileInfo.module.scss';
|
||||||
@ -145,6 +146,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
setCurrentPhotoIndex(currentPhotoIndex + 1);
|
setCurrentPhotoIndex(currentPhotoIndex + 1);
|
||||||
}, [currentPhotoIndex, isLast]);
|
}, [currentPhotoIndex, isLast]);
|
||||||
|
|
||||||
|
function handleSelectFallbackPhoto() {
|
||||||
|
if (!isFirst) return;
|
||||||
|
setHasSlideAnimation(true);
|
||||||
|
setCurrentPhotoIndex(photos.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Swipe gestures
|
// Swipe gestures
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector<HTMLDivElement>(`.${styles.photoWrapper}`);
|
const element = document.querySelector<HTMLDivElement>(`.${styles.photoWrapper}`);
|
||||||
@ -251,6 +258,24 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<div className={styles.photoWrapper}>
|
<div className={styles.photoWrapper}>
|
||||||
{renderPhotoTabs()}
|
{renderPhotoTabs()}
|
||||||
|
{forceShowSelf && user?.fullInfo?.fallbackPhoto && (
|
||||||
|
<div className={buildClassName(
|
||||||
|
styles.fallbackPhoto,
|
||||||
|
(isFirst || isLast) && styles.fallbackPhotoVisible,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
|
||||||
|
{!isLast && (
|
||||||
|
<Avatar
|
||||||
|
photo={user.fullInfo.fallbackPhoto}
|
||||||
|
className={styles.fallbackPhotoAvatar}
|
||||||
|
size="mini"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{lang(user.fullInfo.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Transition activeKey={currentPhotoIndex} name={slideAnimation}>
|
<Transition activeKey={currentPhotoIndex} name={slideAnimation}>
|
||||||
{renderPhoto}
|
{renderPhoto}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@ -56,7 +56,7 @@ 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;
|
const currentPhoto = photo || userOrChat?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto;
|
||||||
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
|
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
|
||||||
const { isVideo } = currentPhoto || {};
|
const { isVideo } = currentPhoto || {};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
.fallback-photo {
|
||||||
|
margin-right: 2rem;
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import SelectAvatar from '../../ui/SelectAvatar';
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
|
||||||
|
import styles from './SettingsPrivacyPublicPhoto.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
currentUser: ApiUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsPrivacyPublicProfilePhoto: FC<OwnProps> = ({
|
||||||
|
currentUser,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
loadFullUser, uploadProfilePhoto, deleteProfilePhoto, showNotification,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const fallbackPhoto = currentUser.fullInfo?.fallbackPhoto;
|
||||||
|
const [isDeleteFallbackPhotoModalOpen, openDeleteFallbackPhotoModal, closeDeleteFallbackPhotoModal] = useFlag(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser.fullInfo) {
|
||||||
|
loadFullUser({ userId: currentUser.id });
|
||||||
|
}
|
||||||
|
}, [currentUser.fullInfo, currentUser.id, loadFullUser]);
|
||||||
|
|
||||||
|
const handleSelectFile = useCallback((file: File) => {
|
||||||
|
uploadProfilePhoto({
|
||||||
|
file,
|
||||||
|
isFallback: true,
|
||||||
|
});
|
||||||
|
showNotification({
|
||||||
|
message: lang('Privacy.ProfilePhoto.PublicPhotoSuccess'),
|
||||||
|
});
|
||||||
|
}, [lang, showNotification, uploadProfilePhoto]);
|
||||||
|
|
||||||
|
const handleConfirmDelete = useCallback(() => {
|
||||||
|
closeDeleteFallbackPhotoModal();
|
||||||
|
deleteProfilePhoto({ photo: fallbackPhoto! });
|
||||||
|
}, [closeDeleteFallbackPhotoModal, deleteProfilePhoto, fallbackPhoto]);
|
||||||
|
|
||||||
|
const handleOpenFileSelector = useCallback(() => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-item">
|
||||||
|
<ListItem
|
||||||
|
icon="camera-add"
|
||||||
|
onClick={handleOpenFileSelector}
|
||||||
|
>
|
||||||
|
<SelectAvatar
|
||||||
|
onChange={handleSelectFile}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
{lang(fallbackPhoto ? 'Privacy.ProfilePhoto.UpdatePublicPhoto' : 'Privacy.ProfilePhoto.SetPublicPhoto')}
|
||||||
|
</ListItem>
|
||||||
|
{fallbackPhoto && (
|
||||||
|
<ListItem
|
||||||
|
leftElement={<Avatar photo={fallbackPhoto} size="mini" className={styles.fallbackPhoto} />}
|
||||||
|
onClick={openDeleteFallbackPhotoModal}
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
{lang(fallbackPhoto.isVideo
|
||||||
|
? 'Privacy.ProfilePhoto.RemovePublicVideo'
|
||||||
|
: 'Privacy.ProfilePhoto.RemovePublicPhoto')}
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isDeleteFallbackPhotoModalOpen}
|
||||||
|
onClose={closeDeleteFallbackPhotoModal}
|
||||||
|
text={lang('Privacy.ResetPhoto.Confirm')}
|
||||||
|
confirmLabel={lang('Delete')}
|
||||||
|
confirmHandler={handleConfirmDelete}
|
||||||
|
confirmIsDestructive
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{lang('Privacy.ProfilePhoto.PublicPhotoInfo')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SettingsPrivacyPublicProfilePhoto);
|
||||||
@ -6,12 +6,14 @@ import type { ApiChat, ApiUser } from '../../../api/types';
|
|||||||
import type { ApiPrivacySettings } from '../../../types';
|
import type { ApiPrivacySettings } from '../../../types';
|
||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
|
|
||||||
|
import { getPrivacyKey } from './helpers/privacy';
|
||||||
|
import { selectUser } from '../../../global/selectors';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import RadioGroup from '../../ui/RadioGroup';
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
import { getPrivacyKey } from './helpers/privacy';
|
import SettingsPrivacyPublicProfilePhoto from './SettingsPrivacyPublicProfilePhoto';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
screen: SettingsScreens;
|
screen: SettingsScreens;
|
||||||
@ -24,6 +26,7 @@ type StateProps =
|
|||||||
Partial<ApiPrivacySettings> & {
|
Partial<ApiPrivacySettings> & {
|
||||||
chatsById?: Record<string, ApiChat>;
|
chatsById?: Record<string, ApiChat>;
|
||||||
usersById?: Record<string, ApiUser>;
|
usersById?: Record<string, ApiUser>;
|
||||||
|
currentUser: ApiUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
||||||
@ -37,6 +40,7 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
|||||||
blockUserIds,
|
blockUserIds,
|
||||||
blockChatIds,
|
blockChatIds,
|
||||||
chatsById,
|
chatsById,
|
||||||
|
currentUser,
|
||||||
}) => {
|
}) => {
|
||||||
const { setPrivacyVisibility } = getActions();
|
const { setPrivacyVisibility } = getActions();
|
||||||
|
|
||||||
@ -44,7 +48,6 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const visibilityOptions = useMemo(() => {
|
const visibilityOptions = useMemo(() => {
|
||||||
switch (screen) {
|
switch (screen) {
|
||||||
case SettingsScreens.PrivacyProfilePhoto:
|
|
||||||
case SettingsScreens.PrivacyGroupChats:
|
case SettingsScreens.PrivacyGroupChats:
|
||||||
return [
|
return [
|
||||||
{ value: 'everybody', label: lang('P2PEverybody') },
|
{ value: 'everybody', label: lang('P2PEverybody') },
|
||||||
@ -226,6 +229,10 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{screen === SettingsScreens.PrivacyProfilePhoto && exceptionLists.shouldShowAllowed && (
|
||||||
|
<SettingsPrivacyPublicProfilePhoto currentUser={currentUser} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -239,6 +246,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
settings: { privacy },
|
settings: { privacy },
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
|
const currentUser = selectUser(global, global.currentUserId!)!;
|
||||||
|
|
||||||
switch (screen) {
|
switch (screen) {
|
||||||
case SettingsScreens.PrivacyPhoneNumber:
|
case SettingsScreens.PrivacyPhoneNumber:
|
||||||
privacySettings = privacy.phoneNumber;
|
privacySettings = privacy.phoneNumber;
|
||||||
@ -274,12 +283,15 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!privacySettings) {
|
if (!privacySettings) {
|
||||||
return {};
|
return {
|
||||||
|
currentUser,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...privacySettings,
|
...privacySettings,
|
||||||
chatsById,
|
chatsById,
|
||||||
|
currentUser,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsPrivacyVisibility));
|
)(SettingsPrivacyVisibility));
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { MediaViewerOrigin } from '../../types';
|
|||||||
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
import {
|
import {
|
||||||
getChatMediaMessageIds, isChatAdmin,
|
getChatMediaMessageIds, isChatAdmin, isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -288,6 +288,8 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
key={mediaId}
|
key={mediaId}
|
||||||
chatId={avatarOwner.id}
|
chatId={avatarOwner.id}
|
||||||
isAvatar
|
isAvatar
|
||||||
|
isFallbackAvatar={isUserId(avatarOwner.id)
|
||||||
|
&& (avatarOwner as ApiUser).photos?.[mediaId!].id === (avatarOwner as ApiUser).fullInfo?.fallbackPhoto?.id}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SenderInfo
|
<SenderInfo
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type OwnProps = {
|
|||||||
chatId?: string;
|
chatId?: string;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
isAvatar?: boolean;
|
isAvatar?: boolean;
|
||||||
|
isFallbackAvatar?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -39,6 +40,7 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
sender,
|
sender,
|
||||||
|
isFallbackAvatar,
|
||||||
isAvatar,
|
isAvatar,
|
||||||
message,
|
message,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
@ -85,7 +87,7 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="date" dir="auto">
|
<div className="date" dir="auto">
|
||||||
{isAvatar
|
{isAvatar
|
||||||
? lang('lng_mediaview_profile_photo')
|
? lang(isFallbackAvatar ? 'lng_mediaview_profile_public_photo' : 'lng_mediaview_profile_photo')
|
||||||
: formatMediaDateTime(lang, message!.date * 1000, true)}
|
: formatMediaDateTime(lang, message!.date * 1000, true)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const AvatarEditable: FC<OwnProps> = ({
|
|||||||
function handleSelectFile(event: ChangeEvent<HTMLInputElement>) {
|
function handleSelectFile(event: ChangeEvent<HTMLInputElement>) {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
|
|
||||||
if (!target || !target.files || !target.files[0]) {
|
if (!target?.files?.[0]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/components/ui/SelectAvatar.module.scss
Normal file
3
src/components/ui/SelectAvatar.module.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
55
src/components/ui/SelectAvatar.tsx
Normal file
55
src/components/ui/SelectAvatar.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { ChangeEvent, RefObject } from 'react';
|
||||||
|
import React, { memo, useCallback, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import CropModal from './CropModal';
|
||||||
|
|
||||||
|
import styles from './SelectAvatar.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
inputRef: RefObject<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectAvatar: FC<OwnProps> = ({
|
||||||
|
onChange,
|
||||||
|
inputRef,
|
||||||
|
}) => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | undefined>();
|
||||||
|
|
||||||
|
function handleSelectFile(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (!target?.files?.[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFile(target.files[0]);
|
||||||
|
target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarCrop = useCallback((croppedImg: File) => {
|
||||||
|
setSelectedFile(undefined);
|
||||||
|
onChange(croppedImg);
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setSelectedFile(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={handleSelectFile}
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
ref={inputRef}
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
<CropModal file={selectedFile} onClose={handleModalClose} onChange={handleAvatarCrop} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SelectAvatar);
|
||||||
@ -92,14 +92,16 @@ addActionHandler('setAuthPassword', (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('uploadProfilePhoto', async (global, actions, payload) => {
|
addActionHandler('uploadProfilePhoto', async (global, actions, payload) => {
|
||||||
const { file } = payload!;
|
const { file, isFallback } = payload!;
|
||||||
|
|
||||||
const result = await callApi('uploadProfilePhoto', file);
|
const result = await callApi('uploadProfilePhoto', file, isFallback);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
actions.loadFullUser({ userId: global.currentUserId! });
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('signUp', (global, actions, payload) => {
|
addActionHandler('signUp', (global, actions, payload) => {
|
||||||
|
|||||||
@ -96,7 +96,7 @@ addActionHandler('updateProfile', async (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('updateProfilePhoto', async (global, actions, payload) => {
|
addActionHandler('updateProfilePhoto', async (global, actions, payload) => {
|
||||||
const { photo } = payload;
|
const { photo, isFallback } = payload;
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
if (!currentUserId) return;
|
if (!currentUserId) return;
|
||||||
const currentUser = selectChat(global, currentUserId);
|
const currentUser = selectChat(global, currentUserId);
|
||||||
@ -108,7 +108,7 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload) => {
|
|||||||
profilePhoto: undefined,
|
profilePhoto: undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const result = await callApi('updateProfilePhoto', photo);
|
const result = await callApi('updateProfilePhoto', photo, isFallback);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
const { photo: newPhoto, users } = result;
|
const { photo: newPhoto, users } = result;
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import * as langProvider from '../../../util/langProvider';
|
|||||||
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
||||||
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
||||||
|
|
||||||
addActionHandler('loadFullUser', (global, actions, payload) => {
|
addActionHandler('loadFullUser', async (global, actions, payload) => {
|
||||||
const { userId } = payload!;
|
const { userId } = payload!;
|
||||||
const user = selectUser(global, userId);
|
const user = selectUser(global, userId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -37,7 +37,12 @@ addActionHandler('loadFullUser', (global, actions, payload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id, accessHash } = user;
|
const { id, accessHash } = user;
|
||||||
callApi('fetchFullUser', { id, accessHash });
|
const newUser = await callApi('fetchFullUser', { id, accessHash });
|
||||||
|
if (!newUser) return;
|
||||||
|
|
||||||
|
if (user.avatarHash !== newUser.avatarHash && user.photos?.length) {
|
||||||
|
actions.loadProfilePhotos({ profileId: userId });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadUser', async (global, actions, payload) => {
|
addActionHandler('loadUser', async (global, actions, payload) => {
|
||||||
@ -238,12 +243,18 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload) => {
|
|||||||
const { profileId } = payload!;
|
const { profileId } = payload!;
|
||||||
const isPrivate = isUserId(profileId);
|
const isPrivate = isUserId(profileId);
|
||||||
|
|
||||||
const user = isPrivate ? selectUser(global, profileId) : undefined;
|
let user = isPrivate ? selectUser(global, profileId) : undefined;
|
||||||
const chat = !isPrivate ? selectChat(global, profileId) : undefined;
|
const chat = !isPrivate ? selectChat(global, profileId) : undefined;
|
||||||
if (!user && !chat) {
|
if (!user && !chat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user && !user?.fullInfo) {
|
||||||
|
const { id, accessHash } = user;
|
||||||
|
user = await callApi('fetchFullUser', { id, accessHash });
|
||||||
|
if (!user) return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await callApi('fetchProfilePhotos', user, chat);
|
const result = await callApi('fetchProfilePhotos', user, chat);
|
||||||
if (!result || !result.photos) {
|
if (!result || !result.photos) {
|
||||||
return;
|
return;
|
||||||
@ -254,6 +265,8 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload) => {
|
|||||||
const userOrChat = user || chat;
|
const userOrChat = user || chat;
|
||||||
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;
|
||||||
|
if (fallbackPhoto) photos.push(fallbackPhoto);
|
||||||
|
|
||||||
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
ApiChatBannedRights,
|
ApiChatBannedRights,
|
||||||
ApiChatAdminRights,
|
ApiChatAdminRights,
|
||||||
ApiChatFolder, ApiTopic,
|
ApiChatFolder, ApiTopic, ApiUserFullInfo,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import {
|
import {
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -124,8 +124,9 @@ export function getChatAvatarHash(
|
|||||||
owner: ApiChat | ApiUser,
|
owner: ApiChat | ApiUser,
|
||||||
size: 'normal' | 'big' = 'normal',
|
size: 'normal' | 'big' = 'normal',
|
||||||
type: 'photo' | 'video' = 'photo',
|
type: 'photo' | 'video' = 'photo',
|
||||||
|
avatarHash = owner.avatarHash,
|
||||||
) {
|
) {
|
||||||
if (!owner.avatarHash) {
|
if (!avatarHash) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const { fullInfo } = owner;
|
const { fullInfo } = owner;
|
||||||
@ -134,14 +135,18 @@ export function getChatAvatarHash(
|
|||||||
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) {
|
||||||
|
return getVideoAvatarMediaHash(userFullInfo.fallbackPhoto);
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'big':
|
case 'big':
|
||||||
return `profile${owner.id}?${owner.avatarHash}`;
|
return `profile${owner.id}?${avatarHash}`;
|
||||||
default:
|
default:
|
||||||
return `avatar${owner.id}?${owner.avatarHash}`;
|
return `avatar${owner.id}?${avatarHash}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -984,6 +984,7 @@ export interface ActionPayloads {
|
|||||||
};
|
};
|
||||||
updateProfilePhoto: {
|
updateProfilePhoto: {
|
||||||
photo: ApiPhoto;
|
photo: ApiPhoto;
|
||||||
|
isFallback?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forwards
|
// Forwards
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user