Settings: New design for self profile (#1373)

This commit is contained in:
Alexander Zinchuk 2021-08-20 23:46:57 +03:00
parent f039f69331
commit ad36326c83
14 changed files with 151 additions and 89 deletions

View File

@ -12,7 +12,7 @@ import {
import { import {
getChatDescription, getChatLink, getHasAdminRight, isChatChannel, isChatPrivate, isUserRightBanned, selectIsChatMuted, getChatDescription, getChatLink, getHasAdminRight, isChatChannel, isChatPrivate, isUserRightBanned, selectIsChatMuted,
} from '../../modules/helpers'; } from '../../modules/helpers';
import renderText from '../common/helpers/renderText'; import renderText from './helpers/renderText';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { copyTextToClipboard } from '../../util/clipboard'; import { copyTextToClipboard } from '../../util/clipboard';
import { formatPhoneNumberWithCode } from '../../util/phoneNumber'; import { formatPhoneNumberWithCode } from '../../util/phoneNumber';
@ -118,6 +118,7 @@ const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
<span className="subtitle">{lang('SetUrlPlaceholder')}</span> <span className="subtitle">{lang('SetUrlPlaceholder')}</span>
</ListItem> </ListItem>
)} )}
{!forceShowSelf && (
<ListItem icon="unmute" ripple onClick={handleNotificationChange}> <ListItem icon="unmute" ripple onClick={handleNotificationChange}>
<span>{lang('Notifications')}</span> <span>{lang('Notifications')}</span>
<Switcher <Switcher
@ -127,6 +128,7 @@ const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
inactive inactive
/> />
</ListItem> </ListItem>
)}
</div> </div>
); );
}; };

View File

@ -159,4 +159,25 @@
transform: scaleX(-1); transform: scaleX(-1);
} }
} }
&.self {
margin: 0 -.5rem .75rem;
overflow: hidden;
&.ghost {
margin: 0;
}
.prev-avatar-media {
z-index: 0;
}
.info {
padding-bottom: .75rem;
}
.status {
line-height: 1rem;
}
}
} }

View File

@ -12,13 +12,14 @@ import { selectChat, selectUser } from '../../modules/selectors';
import { import {
getUserFullName, getUserStatus, isChatChannel, isUserOnline, getUserFullName, getUserStatus, isChatChannel, isUserOnline,
} from '../../modules/helpers'; } from '../../modules/helpers';
import renderText from '../common/helpers/renderText'; import renderText from './helpers/renderText';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import buildClassName from '../../util/buildClassName';
import usePhotosPreload from './hooks/usePhotosPreload'; import usePhotosPreload from './hooks/usePhotosPreload';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import VerifiedIcon from '../common/VerifiedIcon'; import VerifiedIcon from './VerifiedIcon';
import ProfilePhoto from './ProfilePhoto'; import ProfilePhoto from './ProfilePhoto';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
@ -35,15 +36,16 @@ type StateProps = {
isSavedMessages?: boolean; isSavedMessages?: boolean;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
serverTimeOffset: number; serverTimeOffset: number;
} & Pick<GlobalState, 'lastSyncTime'>; } & Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>; type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
forceShowSelf,
user, user,
chat, chat,
isSavedMessages, isSavedMessages,
lastSyncTime, connectionState,
animationLevel, animationLevel,
loadFullUser, loadFullUser,
openMediaViewer, openMediaViewer,
@ -69,10 +71,10 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang(); const lang = useLang();
useEffect(() => { useEffect(() => {
if (lastSyncTime && userId) { if (connectionState === 'connectionStateReady' && userId && !forceShowSelf) {
loadFullUser({ userId }); loadFullUser({ userId });
} }
}, [userId, loadFullUser, lastSyncTime]); }, [userId, loadFullUser, connectionState, forceShowSelf]);
usePhotosPreload(user || chat, photos, currentPhotoIndex); usePhotosPreload(user || chat, photos, currentPhotoIndex);
@ -80,9 +82,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
openMediaViewer({ openMediaViewer({
avatarOwnerId: userId || chatId, avatarOwnerId: userId || chatId,
profilePhotoIndex: currentPhotoIndex, profilePhotoIndex: currentPhotoIndex,
origin: MediaViewerOrigin.ProfileAvatar, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
}); });
}, [openMediaViewer, userId, chatId, currentPhotoIndex]); }, [openMediaViewer, userId, chatId, currentPhotoIndex, forceShowSelf]);
const selectPreviousMedia = useCallback(() => { const selectPreviousMedia = useCallback(() => {
if (isFirst) { if (isFirst) {
@ -174,7 +176,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
const isVerifiedIconShown = (user && user.isVerified) || (chat && chat.isVerified); const isVerifiedIconShown = (user && user.isVerified) || (chat && chat.isVerified);
return ( return (
<div className="ProfileInfo" dir={lang.isRtl ? 'rtl' : undefined}> <div className={buildClassName('ProfileInfo', forceShowSelf && 'self')} dir={lang.isRtl ? 'rtl' : undefined}>
<div className="photo-wrapper"> <div className="photo-wrapper">
{renderPhotoTabs()} {renderPhotoTabs()}
<Transition activeKey={currentPhotoIndex} name={slideAnimation} className="profile-slide-container"> <Transition activeKey={currentPhotoIndex} name={slideAnimation} className="profile-slide-container">
@ -218,15 +220,15 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { lastSyncTime, serverTimeOffset } = global; const { connectionState, serverTimeOffset } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { animationLevel } = global.settings.byKey; const { animationLevel } = global.settings.byKey;
return { return {
lastSyncTime, user, chat, isSavedMessages, animationLevel, serverTimeOffset, connectionState, user, chat, isSavedMessages, animationLevel, serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']),
)(PrivateChatInfo)); )(ProfileInfo));

View File

@ -7,7 +7,7 @@ import {
import { import {
getChatAvatarHash, isDeletedUser, getUserColorKey, getChatTitle, isChatPrivate, getUserFullName, getChatAvatarHash, isDeletedUser, getUserColorKey, getChatTitle, isChatPrivate, getUserFullName,
} from '../../modules/helpers'; } from '../../modules/helpers';
import renderText from '../common/helpers/renderText'; import renderText from './helpers/renderText';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
@ -59,11 +59,16 @@ const ProfilePhoto: FC<OwnProps> = ({
} }
const imageHash = getMediaHash(); const imageHash = getMediaHash();
const fullMediaData = useMedia(imageHash, false, ApiMediaFormat.BlobUrl, lastSyncTime); const fullMediaData = useMedia(
imageHash,
false,
imageHash?.startsWith('avatar') ? ApiMediaFormat.DataUri : ApiMediaFormat.BlobUrl,
lastSyncTime,
);
const avatarThumbnailData = useMedia( const avatarThumbnailData = useMedia(
!fullMediaData && isFirstPhoto ? getMediaHash('normal', true) : undefined, !fullMediaData && isFirstPhoto ? getMediaHash('normal', true) : undefined,
false, false,
ApiMediaFormat.BlobUrl, ApiMediaFormat.DataUri,
lastSyncTime, lastSyncTime,
); );
const thumbDataUri = useBlurSync(!fullMediaData && photo && photo.thumbnail && photo.thumbnail.dataUri); const thumbDataUri = useBlurSync(!fullMediaData && photo && photo.thumbnail && photo.thumbnail.dataUri);

View File

@ -19,6 +19,7 @@
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 500; font-weight: 500;
margin-left: 1.375rem; margin-left: 1.375rem;
margin-right: auto;
} }
.SearchInput { .SearchInput {
@ -33,4 +34,13 @@
@media (max-width: 600px) { @media (max-width: 600px) {
padding: 0.5rem; padding: 0.5rem;
} }
.Button.smaller {
width: 2.5rem;
height: 2.5rem;
+ .DropdownMenu {
margin-left: .25rem;
}
}
} }

View File

@ -6,6 +6,10 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.left-header {
padding-right: .8125rem;
}
} }
.settings-main-header { .settings-main-header {
@ -30,6 +34,7 @@
background: var(--color-background); background: var(--color-background);
height: calc(100% - var(--header-height)); height: calc(100% - var(--header-height));
overflow-y: auto; overflow-y: auto;
overflow-y: overlay;
&.infinite-scroll { &.infinite-scroll {
display: flex; display: flex;
@ -84,36 +89,17 @@
} }
.settings-main-menu { .settings-main-menu {
padding: 0 0.5rem 1rem; padding: 0 0.5rem .75rem;
}
.settings-current-user { > .ChatExtra {
margin-bottom: 1.125rem; padding: 0 .5rem .3125rem;
text-align: center; margin: 0 -.5rem .625rem;
box-shadow: inset 0 -.0625rem 0 0 var(--color-background-secondary-accent);
border-bottom: .625rem solid var(--color-background-secondary);
.Avatar { .ListItem.narrow {
margin: 0 auto 1.5rem; margin-bottom: .25rem;
} }
.name {
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
font-size: 1.5rem;
line-height: 2rem;
margin: 0;
.VerifiedIcon {
margin-left: 0.25rem;
margin-top: 0.1rem;
}
}
.phone {
font-size: 0.875rem;
color: #868e96;
margin: 0;
} }
} }

View File

@ -334,6 +334,7 @@ const Settings: FC<OwnProps> = ({
currentScreen={currentScreen} currentScreen={currentScreen}
onReset={handleReset} onReset={handleReset}
onSaveFilter={handleSaveFilter} onSaveFilter={handleSaveFilter}
onScreenSelect={onScreenSelect}
editedFolderId={foldersState.folderId} editedFolderId={foldersState.folderId}
/> />
{renderCurrentSectionContent(isScreenActive, currentKey)} {renderCurrentSectionContent(isScreenActive, currentKey)}

View File

@ -20,6 +20,7 @@ type OwnProps = {
editedFolderId?: number; editedFolderId?: number;
onReset: () => void; onReset: () => void;
onSaveFilter: () => void; onSaveFilter: () => void;
onScreenSelect: (screen: SettingsScreens) => void;
}; };
type DispatchProps = Pick<GlobalActions, 'signOut' | 'deleteChatFolder'>; type DispatchProps = Pick<GlobalActions, 'signOut' | 'deleteChatFolder'>;
@ -31,6 +32,7 @@ const SettingsHeader: FC<OwnProps & DispatchProps> = ({
onSaveFilter, onSaveFilter,
signOut, signOut,
deleteChatFolder, deleteChatFolder,
onScreenSelect,
}) => { }) => {
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false); const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] = useState(false); const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] = useState(false);
@ -205,6 +207,16 @@ const SettingsHeader: FC<OwnProps & DispatchProps> = ({
<div className="settings-main-header"> <div className="settings-main-header">
<h3>{lang('SETTINGS')}</h3> <h3>{lang('SETTINGS')}</h3>
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
size="smaller"
color="translucent"
onClick={() => onScreenSelect(SettingsScreens.EditProfile)}
ariaLabel={lang('lng_settings_information')}
>
<i className="icon-edit" />
</Button>
<DropdownMenu <DropdownMenu
className="settings-more-menu" className="settings-more-menu"
trigger={SettingsMenuButton} trigger={SettingsMenuButton}

View File

@ -1,18 +1,18 @@
import React, { FC, memo } from '../../../lib/teact/teact'; import React, { FC, memo, useEffect } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
import { ApiUser } from '../../../api/types'; import { ApiUser } from '../../../api/types';
import { selectUser } from '../../../modules/selectors'; import { selectUser } from '../../../modules/selectors';
import { getUserFullName } from '../../../modules/helpers'; import { pick } from '../../../util/iteratees';
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
import renderText from '../../common/helpers/renderText';
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 Avatar from '../../common/Avatar'; import ProfileInfo from '../../common/ProfileInfo';
import ChatExtra from '../../common/ChatExtra';
type OwnProps = { type OwnProps = {
isActive?: boolean; isActive?: boolean;
@ -22,16 +22,27 @@ type OwnProps = {
type StateProps = { type StateProps = {
currentUser?: ApiUser; currentUser?: ApiUser;
lastSyncTime?: number;
}; };
const SettingsMain: FC<OwnProps & StateProps> = ({ type DispatchProps = Pick<GlobalActions, 'loadProfilePhotos'>;
const SettingsMain: FC<OwnProps & StateProps & DispatchProps> = ({
isActive, isActive,
onScreenSelect, onScreenSelect,
onReset, onReset,
loadProfilePhotos,
currentUser, currentUser,
lastSyncTime,
}) => { }) => {
const lang = useLang(); const lang = useLang();
const fullName = getUserFullName(currentUser); const profileId = currentUser ? currentUser.id : undefined;
useEffect(() => {
if (profileId && lastSyncTime) {
loadProfilePhotos({ profileId });
}
}, [lastSyncTime, profileId, loadProfilePhotos]);
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main); useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main);
@ -39,24 +50,17 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
<div className="settings-content custom-scroll"> <div className="settings-content custom-scroll">
<div className="settings-main-menu"> <div className="settings-main-menu">
{currentUser && ( {currentUser && (
<div className="settings-current-user"> <ProfileInfo
<Avatar user={currentUser} size="jumbo" /> userId={currentUser.id}
<p className="name">{fullName && renderText(fullName)}</p> forceShowSelf
<p className="phone">{formatPhoneNumberWithCode(currentUser.phoneNumber)}</p> />
</div> )}
{currentUser && (
<ChatExtra
chatOrUserId={currentUser.id}
forceShowSelf
/>
)} )}
<ListItem
icon="edit"
onClick={() => onScreenSelect(SettingsScreens.EditProfile)}
>
{lang('lng_settings_information')}
</ListItem>
<ListItem
icon="folder"
onClick={() => onScreenSelect(SettingsScreens.Folders)}
>
{lang('Filters')}
</ListItem>
<ListItem <ListItem
icon="settings" icon="settings"
onClick={() => onScreenSelect(SettingsScreens.General)} onClick={() => onScreenSelect(SettingsScreens.General)}
@ -75,6 +79,12 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
> >
{lang('PrivacySettings')} {lang('PrivacySettings')}
</ListItem> </ListItem>
<ListItem
icon="folder"
onClick={() => onScreenSelect(SettingsScreens.Folders)}
>
{lang('Filters')}
</ListItem>
<ListItem <ListItem
icon="language" icon="language"
onClick={() => onScreenSelect(SettingsScreens.Language)} onClick={() => onScreenSelect(SettingsScreens.Language)}
@ -88,10 +98,12 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const { currentUserId } = global; const { currentUserId, lastSyncTime } = global;
return { return {
currentUser: currentUserId ? selectUser(global, currentUserId) : undefined, currentUser: currentUserId ? selectUser(global, currentUserId) : undefined,
lastSyncTime,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadProfilePhotos']),
)(SettingsMain)); )(SettingsMain));

View File

@ -143,7 +143,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
const existingGhost = document.getElementsByClassName('ghost')[0] as HTMLDivElement; const existingGhost = document.getElementsByClassName('ghost')[0] as HTMLDivElement;
const ghost = existingGhost || createGhost(bestImageData || toImage, origin === MediaViewerOrigin.ProfileAvatar); const ghost = existingGhost || createGhost(bestImageData || toImage, origin);
if (!existingGhost) { if (!existingGhost) {
applyStyles(ghost, { applyStyles(ghost, {
top: `${toTop}px`, top: `${toTop}px`,
@ -203,7 +203,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
}); });
} }
function createGhost(source: string | HTMLImageElement | HTMLVideoElement, shouldAppendProfileInfo = false) { function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origin?: MediaViewerOrigin) {
const ghost = document.createElement('div'); const ghost = document.createElement('div');
ghost.classList.add('ghost'); ghost.classList.add('ghost');
@ -219,9 +219,16 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement, shoul
ghost.appendChild(img); ghost.appendChild(img);
if (shouldAppendProfileInfo) { if (origin === MediaViewerOrigin.ProfileAvatar || origin === MediaViewerOrigin.SettingsAvatar) {
ghost.classList.add('ProfileInfo'); ghost.classList.add('ProfileInfo');
const profileInfo = document.querySelector('#RightColumn .ProfileInfo .info'); if (origin === MediaViewerOrigin.SettingsAvatar) {
ghost.classList.add('self');
}
const profileInfo = document.querySelector(
origin === MediaViewerOrigin.ProfileAvatar
? '#RightColumn .ProfileInfo .info'
: '#Settings .ProfileInfo .info',
);
if (profileInfo) { if (profileInfo) {
ghost.appendChild(profileInfo.cloneNode(true)); ghost.appendChild(profileInfo.cloneNode(true));
} }
@ -314,6 +321,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
mediaSelector = 'img.avatar-media'; mediaSelector = 'img.avatar-media';
break; break;
case MediaViewerOrigin.SettingsAvatar:
containerSelector = '#Settings .ProfileInfo .active .ProfilePhoto';
mediaSelector = 'img.avatar-media';
break;
case MediaViewerOrigin.ProfileAvatar: case MediaViewerOrigin.ProfileAvatar:
containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto'; containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto';
mediaSelector = 'img.avatar-media'; mediaSelector = 'img.avatar-media';
@ -345,6 +357,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
break; break;
case MediaViewerOrigin.SharedMedia: case MediaViewerOrigin.SharedMedia:
case MediaViewerOrigin.SettingsAvatar:
case MediaViewerOrigin.ProfileAvatar: case MediaViewerOrigin.ProfileAvatar:
case MediaViewerOrigin.SearchResult: case MediaViewerOrigin.SearchResult:
(ghost.firstChild as HTMLElement).style.objectFit = 'cover'; (ghost.firstChild as HTMLElement).style.objectFit = 'cover';

View File

@ -46,10 +46,10 @@ import TabList from '../ui/TabList';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import ListItem from '../ui/ListItem'; import ListItem from '../ui/ListItem';
import PrivateChatInfo from '../common/PrivateChatInfo'; import PrivateChatInfo from '../common/PrivateChatInfo';
import ProfileInfo from './ProfileInfo'; import ProfileInfo from '../common/ProfileInfo';
import Document from '../common/Document'; import Document from '../common/Document';
import Audio from '../common/Audio'; import Audio from '../common/Audio';
import ChatExtra from './ChatExtra'; import ChatExtra from '../common/ChatExtra';
import Media from '../common/Media'; import Media from '../common/Media';
import WebLink from '../common/WebLink'; import WebLink from '../common/WebLink';
import NothingFound from '../common/NothingFound'; import NothingFound from '../common/NothingFound';
@ -405,11 +405,8 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
function renderProfileInfo(chatId: number, resolvedUserId?: number) { function renderProfileInfo(chatId: number, resolvedUserId?: number) {
return ( return (
<div className="profile-info"> <div className="profile-info">
<ProfileInfo <ProfileInfo userId={resolvedUserId || chatId} />
userId={resolvedUserId || chatId} <ChatExtra chatOrUserId={resolvedUserId || chatId} />
forceShowSelf={resolvedUserId !== chatId}
/>
<ChatExtra chatOrUserId={resolvedUserId || chatId} forceShowSelf={resolvedUserId !== chatId} />
</div> </div>
); );
} }

View File

@ -230,6 +230,7 @@ export enum MediaViewerOrigin {
ScheduledInline, ScheduledInline,
SharedMedia, SharedMedia,
ProfileAvatar, ProfileAvatar,
SettingsAvatar,
MiddleHeaderAvatar, MiddleHeaderAvatar,
Album, Album,
ScheduledAlbum, ScheduledAlbum,