Avatar: Update sizes (#5337)

This commit is contained in:
zubiden 2024-12-29 11:58:54 +01:00 committed by Alexander Zinchuk
parent 5e4924189a
commit 7b2956cb3f
18 changed files with 148 additions and 303 deletions

View File

@ -2,12 +2,14 @@
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
--color-user: var(--color-primary);
--radius: 50%;
--_size: 0px;
--_font-size: max(calc(var(--_size) / 2 - 2px), 0.75rem);
flex: none;
align-items: center;
justify-content: center;
width: 3.375rem;
height: 3.375rem;
width: var(--_size);
height: var(--_size);
border-radius: var(--radius);
color: white;
font-weight: bold;
@ -16,6 +18,17 @@
user-select: none;
position: relative;
font-size: var(--_font-size);
.emoji {
width: var(--_font-size);
height: var(--_font-size);
}
&__icon {
font-size: calc(var(--_size) / 2);
}
> .inner {
overflow: hidden;
border-radius: var(--radius);
@ -40,123 +53,6 @@
object-fit: cover;
}
.emoji {
width: 1rem;
height: 1rem;
}
&__icon {
font-size: 1.25em;
}
&.size-micro {
width: 1rem;
height: 1rem;
font-size: 0.5rem;
.emoji {
width: 0.5625rem;
height: 0.5625rem;
}
}
&.size-tiny {
width: 2rem;
height: 2rem;
font-size: 0.875rem;
.emoji {
width: 0.875rem;
height: 0.875rem;
}
}
&.size-mini {
width: 1.5rem;
height: 1.5rem;
font-size: 0.75rem;
.emoji {
width: 0.75rem;
height: 0.75rem;
}
}
&.size-small {
width: 2.125rem;
height: 2.125rem;
font-size: 0.875rem;
.emoji {
width: 0.875rem;
height: 0.875rem;
}
}
&.size-small-mobile {
width: 2.5rem;
height: 2.5rem;
font-size: 0.875rem;
.emoji {
width: 0.875rem;
height: 0.875rem;
}
}
&.size-medium {
width: 2.75rem;
height: 2.75rem;
font-size: 1.1875rem;
.emoji {
width: 1rem;
height: 1rem;
}
}
&.size-large {
font-size: 1.3125rem;
.emoji {
width: 1.3125rem;
height: 1.3125rem;
}
}
&.size-giant {
width: 5rem;
height: 5rem;
font-size: 2.5rem;
.emoji {
width: 2.5rem;
height: 2.5rem;
}
}
&.size-huge {
width: 6rem;
height: 6rem;
font-size: 3rem;
.emoji {
width: 3rem;
height: 3rem;
}
}
&.size-jumbo {
width: 7.5rem;
height: 7.5rem;
font-size: 3.5rem;
.emoji {
width: 3.5rem;
height: 3.5rem;
}
}
&.interactive {
cursor: var(--custom-cursor, pointer);
}
@ -173,15 +69,15 @@
}
&.with-story-solid {
width: 3rem;
height: 3rem;
width: calc(var(--_size) - 0.25rem);
height: calc(var(--_size) - 0.25rem);
margin: 0.1875rem;
&::before {
content: "";
position: absolute;
width: 3.5rem;
height: 3.5rem;
width: calc(var(--_size) + 0.25rem);
height: calc(var(--_size) + 0.25rem);
left: -0.25rem;
top: -0.25rem;
border-radius: 50%;
@ -191,8 +87,8 @@
&::after {
content: "";
position: absolute;
width: 3.25rem;
height: 3.25rem;
width: var(--_size);
height: var(--_size);
left: -0.125rem;
top: -0.125rem;
border-radius: 50%;
@ -200,51 +96,6 @@
background: var(--color-background);
}
&.size-tiny {
width: 1.75rem;
height: 1.75rem;
&::before {
width: 2.25rem;
height: 2.25rem;
}
&::after {
width: 2rem;
height: 2rem;
}
}
&.size-medium {
width: 2.5rem;
height: 2.5rem;
&::before {
width: 3rem;
height: 3rem;
}
&::after {
width: 2.75rem;
height: 2.75rem;
}
}
&.size-small-mobile {
width: 2.25rem;
height: 2.25rem;
&::before {
width: 2.75rem;
height: 2.75rem;
}
&::after {
width: 2.5rem;
height: 2.5rem;
}
}
&.online::after {
bottom: -0.125rem;
right: -0.125rem;

View File

@ -28,6 +28,7 @@ import {
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import { getFirstLetters } from '../../util/textFormat';
import { REM } from './helpers/mediaDimensions';
import { getPeerColorClass } from './helpers/peerColor';
import renderText from './helpers/renderText';
@ -45,8 +46,19 @@ import './Avatar.scss';
const LOOP_COUNT = 3;
export const AVATAR_SIZES = {
micro: REM,
mini: 1.5 * REM,
tiny: 2 * REM,
small: 2.125 * REM,
medium: 2.75 * REM,
large: 3.375 * REM,
giant: 5.625 * REM,
jumbo: 7.5 * REM,
};
export type AvatarSize =
'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'giant' | 'huge' | 'jumbo';
'micro' | 'mini' | 'tiny' | 'small' | 'medium' | 'large' | 'giant' | 'jumbo' | number;
const cn = createClassNameBuilder('Avatar');
cn.media = cn('media');
@ -114,12 +126,14 @@ const Avatar: FC<OwnProps> = ({
let imageHash: string | undefined;
let videoHash: string | undefined;
const pxSize = typeof size === 'number' ? size : AVATAR_SIZES[size];
const shouldLoadVideo = withVideo && photo?.isVideo;
const shouldFetchBig = size === 'jumbo';
const isBig = pxSize >= AVATAR_SIZES.jumbo;
if (!isSavedMessages && !isDeleted) {
if ((user && !noPersonalPhoto) || chat) {
imageHash = getChatAvatarHash(peer as ApiPeer, shouldFetchBig ? 'big' : undefined);
imageHash = getChatAvatarHash(peer as ApiPeer, isBig ? 'big' : undefined);
} else if (photo) {
imageHash = `photo${photo.id}?size=m`;
if (photo.isVideo && withVideo) {
@ -233,7 +247,7 @@ const Avatar: FC<OwnProps> = ({
const customColor = isCustomPeer && peer.customPeerAvatarColor;
const fullClassName = buildClassName(
`Avatar size-${size}`,
'Avatar',
className,
getPeerColorClass(peer),
!peer && text && 'hidden-user',
@ -279,15 +293,15 @@ const Avatar: FC<OwnProps> = ({
data-peer-id={realPeer?.id}
data-test-sender-id={IS_TEST ? realPeer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined}
style={buildStyle(customColor && `--color-user: ${customColor}`)}
style={buildStyle(`--_size: ${pxSize}px;`, customColor && `--color-user: ${customColor}`)}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
<div className="inner">
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
{typeof content === 'string' ? renderText(content, [isBig ? 'hq_emoji' : 'emoji']) : content}
</div>
{withStory && realPeer?.hasStories && (
<AvatarStoryCircle peerId={realPeer.id} size={size} withExtraGap={withStoryGap} />
<AvatarStoryCircle peerId={realPeer.id} size={pxSize} withExtraGap={withStoryGap} />
)}
</div>
);

View File

@ -1,47 +1,11 @@
.root {
--_size: 0px;
--half-size: calc(var(--_size) / 2);
--spacing: calc(var(--_size) * 0.4);
--spacing-gap: calc(var(--_size) * 0.04);
display: flex;
position: relative;
--spacing: calc(var(--size) * 0.4);
--spacing-gap: calc(var(--size) * 0.04);
--size: 0px;
--half-size: calc(var(--size) / 2);
}
.size-micro {
--size: 1rem;
}
.size-mini {
--size: 1.5rem;
}
.size-tiny {
--size: 2rem;
}
.size-small {
--size: 2.125rem;
}
.size-small-mobile {
--size: 2.5rem;
}
.size-medium {
--size: 2.75rem;
}
.size-large {
--size: 3.375rem;
}
.size-huge {
--size: 6.5rem;
}
.size-jumbo {
--size: 7.5rem;
}
.avatar {

View File

@ -8,7 +8,7 @@ import buildClassName from '../../util/buildClassName';
import useOldLang from '../../hooks/useOldLang';
import Avatar from './Avatar';
import Avatar, { AVATAR_SIZES } from './Avatar';
import styles from './AvatarList.module.scss';
@ -30,6 +30,9 @@ const AvatarList: FC<OwnProps> = ({
badgeText,
}) => {
const lang = useOldLang();
const pxSize = typeof size === 'number' ? size : AVATAR_SIZES[size];
const renderingBadgeText = useMemo(() => {
if (badgeText) return badgeText;
if (!peers?.length || peers.length <= limit) return undefined;
@ -38,7 +41,8 @@ const AvatarList: FC<OwnProps> = ({
return (
<div
className={buildClassName(className, styles.root, styles[`size-${size}`])}
className={buildClassName(className, styles.root)}
style={`--_size: ${pxSize}px;`}
dir={lang.isRtl ? 'rtl' : 'ltr'}
>
{peers?.slice(0, limit).map((peer) => <Avatar size={size} peer={peer} className={styles.avatar} />)}

View File

@ -5,7 +5,6 @@ import { withGlobal } from '../../global';
import type { ApiTypeStory } from '../../api/types';
import type { ThemeKey } from '../../types';
import type { AvatarSize } from './Avatar';
import { selectPeerStories, selectTheme } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
@ -17,7 +16,7 @@ interface OwnProps {
// eslint-disable-next-line react/no-unused-prop-types
peerId: string;
className?: string;
size: AvatarSize;
size: number;
withExtraGap?: boolean;
}
@ -28,19 +27,6 @@ interface StateProps {
appTheme: ThemeKey;
}
const SIZES: Record<AvatarSize, number> = {
micro: 1.125 * REM,
tiny: 2.125 * REM,
mini: 1.625 * REM,
small: 2.25 * REM,
'small-mobile': 2.625 * REM,
medium: 2.875 * REM,
large: 3.5 * REM,
giant: 5.125 * REM,
huge: 6.125 * REM,
jumbo: 7.625 * REM,
};
const BLUE = ['#34C578', '#3CA3F3'];
const GREEN = ['#C9EB38', '#09C167'];
const PURPLE = ['#A667FF', '#55A5FF'];
@ -50,6 +36,7 @@ const STROKE_WIDTH = 0.125 * REM;
const STROKE_WIDTH_READ = 0.0625 * REM;
const GAP_PERCENT = 2;
const SEGMENTS_MAX = 45; // More than this breaks rendering in Safari and Chrome
const LARGE_AVATAR_SIZE = 3.5 * REM;
const GAP_PERCENT_EXTRA = 10;
const EXTRA_GAP_ANGLE = Math.PI / 4;
@ -58,7 +45,7 @@ const EXTRA_GAP_START = EXTRA_GAP_ANGLE - EXTRA_GAP_SIZE / 2;
const EXTRA_GAP_END = EXTRA_GAP_ANGLE + EXTRA_GAP_SIZE / 2;
function AvatarStoryCircle({
size = 'large',
size,
className,
peerStories,
storyIds,
@ -71,6 +58,8 @@ function AvatarStoryCircle({
const dpr = useDevicePixelRatio();
const adaptedSize = size + STROKE_WIDTH;
const values = useMemo(() => {
return (storyIds || []).reduce((acc, id) => {
acc.total += 1;
@ -104,7 +93,7 @@ function AvatarStoryCircle({
drawGradientCircle({
canvas: ref.current,
size: SIZES[size] * dpr,
size: adaptedSize * dpr,
segmentsCount: values.total,
color: isCloseFriend ? 'green' : 'blue',
readSegmentsCount: values.read,
@ -112,19 +101,17 @@ function AvatarStoryCircle({
readSegmentColor: appTheme === 'dark' ? DARK_GRAY : GRAY,
dpr,
});
}, [appTheme, isCloseFriend, size, values.read, values.total, withExtraGap, dpr]);
}, [appTheme, isCloseFriend, adaptedSize, values.read, values.total, withExtraGap, dpr]);
if (!values.total) {
return undefined;
}
const maxSize = SIZES[size];
return (
<canvas
ref={ref}
className={buildClassName('story-circle', size, className)}
style={`max-width: ${maxSize}px; max-height: ${maxSize}px;`}
className={buildClassName('story-circle', className)}
style={`max-width: ${adaptedSize}px; max-height: ${adaptedSize}px;`}
/>
);
}
@ -166,7 +153,7 @@ export function drawGradientCircle({
segmentsCount = SEGMENTS_MAX;
}
const strokeModifier = Math.max(Math.max(size - SIZES.large * dpr, 0) / dpr / REM / 1.5, 1) * dpr;
const strokeModifier = Math.max(Math.max(size - LARGE_AVATAR_SIZE * dpr, 0) / dpr / REM / 1.5, 1) * dpr;
const ctx = canvas.getContext('2d');
if (!ctx) {

View File

@ -3,6 +3,7 @@ import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global';
import type { GlobalState } from '../../../global/types';
import type { CustomPeer } from '../../../types';
import { ARCHIVED_FOLDER_ID } from '../../../config';
import { getChatTitle } from '../../../global/helpers';
@ -14,6 +15,8 @@ import renderText from '../../common/helpers/renderText';
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import Icon from '../../common/icons/Icon';
import Badge from '../../ui/Badge';
import ListItem, { type MenuItemContextAction } from '../../ui/ListItem';
@ -26,6 +29,12 @@ type OwnProps = {
};
const PREVIEW_SLICE = 5;
const ARCHIVE_CUSTOM_PEER: CustomPeer = {
isCustomPeer: true,
title: 'Archived Chats',
avatarIcon: 'archive-filled',
customPeerAvatarColor: '#9EAAB5',
};
const Archive: FC<OwnProps> = ({
archiveSettings,
@ -103,7 +112,7 @@ const Archive: FC<OwnProps> = ({
<div className="info-row">
<div className={buildClassName('title', styles.title)}>
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>
<i className={buildClassName(styles.icon, 'icon', 'icon-archive-filled')} />
<Icon name="archive-filled" className={styles.icon} />
{lang('ArchivedChats')}
</h3>
</div>
@ -120,9 +129,7 @@ const Archive: FC<OwnProps> = ({
return (
<>
<div className={buildClassName('status', styles.avatarWrapper)}>
<div className={buildClassName('Avatar', styles.avatar)}>
<i className="icon icon-archive-filled" />
</div>
<Avatar peer={ARCHIVE_CUSTOM_PEER} />
</div>
<div className={buildClassName(styles.info, 'info')}>
<div className="info-row">

View File

@ -11,7 +11,7 @@ import useOldLang from '../../../../hooks/useOldLang';
import useScrolledState from '../../../../hooks/useScrolledState';
import useDevicePixelRatio from '../../../../hooks/window/useDevicePixelRatio';
import Avatar from '../../../common/Avatar';
import Avatar, { AVATAR_SIZES } from '../../../common/Avatar';
import { drawGradientCircle } from '../../../common/AvatarStoryCircle';
import PremiumFeatureItem from '../PremiumFeatureItem';
@ -53,7 +53,7 @@ const STORY_FEATURE_ICONS = {
const STORY_FEATURE_ORDER = Object.keys(STORY_FEATURE_TITLES) as (keyof typeof STORY_FEATURE_TITLES)[];
const CIRCLE_SIZE = 5.25 * REM;
const CIRCLE_SIZE = AVATAR_SIZES.giant + 0.25 * REM;
const CIRCLE_SEGMENTS = 8;
const CIRCLE_READ_SEGMENTS = 0;

View File

@ -979,7 +979,7 @@ const Message: FC<OwnProps & StateProps> = ({
return (
<Avatar
size={isMobile ? 'small-mobile' : 'small'}
size="small"
peer={avatarPeer}
text={hiddenName}
onClick={avatarPeer ? handleAvatarClick : undefined}

View File

@ -51,6 +51,8 @@ type StateProps = {
user?: ApiUser;
};
const AVATAR_SIZE = 100;
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
modal,
starGiftsById,
@ -234,7 +236,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<div className={styles.avatars}>
<Avatar
size="huge"
size={AVATAR_SIZE}
peer={user}
/>
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />

View File

@ -1,9 +1,14 @@
.root :global(.modal-content) {
padding: 0;
@use "../../../../styles/mixins";
.modalDialog :global(.modal-dialog) {
overflow: hidden;
}
.root :global(.modal-dialog) {
overflow: hidden;
.content {
display: flex;
flex-direction: column;
padding: 0 !important;
overflow: hidden !important;
}
.main {
@ -48,6 +53,8 @@
flex-direction: column;
height: 100%;
padding: 0.5rem;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.header {
@ -85,6 +92,7 @@
.avatar {
margin: 2rem;
margin-bottom: 0.75rem;
}
.center {

View File

@ -41,6 +41,8 @@ type StateProps = {
user?: ApiUser;
};
const AVATAR_SIZE = 100;
const StarsGiftModal: FC<OwnProps & StateProps> = ({
modal,
user,
@ -136,20 +138,24 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
const parts = text.split('{link}');
return [
parts[0],
<SafeLink url={oldLang('StarsTOSLink')} text={oldLang('lng_credits_summary_options_about_link')} />,
<SafeLink
url={oldLang('StarsTOSLink')}
text={oldLang('lng_credits_summary_options_about_link')}
/>,
parts[1],
];
}, [oldLang]);
return (
<Modal
className={buildClassName(styles.modalDialog, styles.root)}
className={buildClassName(styles.modalDialog)}
contentClassName={styles.content}
dialogRef={dialogRef}
isSlim
onClose={handleClose}
isOpen={isOpen}
>
<div className={styles.main} onScroll={handleScroll}>
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<Button
round
size="smaller"
@ -170,7 +176,7 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
{user ? (
<>
<Avatar
size="huge"
size={AVATAR_SIZE}
peer={user}
className={styles.avatar}
/>

View File

@ -8,15 +8,11 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
gap: 0.25rem;
margin-block: 1rem;
position: relative;
}
.starsHeader {
gap: normal;
}
.title, .amount {
margin-bottom: 0;
}
@ -24,7 +20,9 @@
.title {
text-align: center;
text-wrap: balance;
font-size: 1.75rem;
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
margin-top: 1rem;
line-height: 1.25;
}
@ -44,8 +42,8 @@
.starsBackground {
position: absolute;
height: 8rem;
top: 0;
height: 7rem;
top: -0.75rem;
left: 50%;
transform: translateX(-50%);
z-index: -1;
@ -60,10 +58,10 @@
bottom: 0;
right: 0;
font-size: 2rem;
font-size: 1.75rem;
z-index: 1;
@include mixins.filter-outline(2px, var(--color-background));
@include mixins.filter-outline(1px, var(--color-background));
}
.amountStar {

View File

@ -125,9 +125,9 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
const isBotSubscription = isApiPeerUser(peer);
const header = (
<div className={buildClassName(styles.header, styles.starsHeader)}>
<div className={styles.header}>
<div className={styles.avatarWrapper}>
<Avatar peer={!photo ? peer : undefined} webPhoto={photo} size="jumbo" />
<Avatar peer={!photo ? peer : undefined} webPhoto={photo} size="giant" />
<StarIcon className={styles.subscriptionStar} type="gold" size="adaptive" />
</div>
<img

View File

@ -16,15 +16,11 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
gap: 0.25rem;
margin-block: 1rem;
position: relative;
}
.starsHeader {
gap: normal;
}
.amount {
display: flex;
gap: 0.25rem;
@ -42,7 +38,8 @@
text-wrap: balance;
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
margin: 0.5rem 0 0.25rem;
margin-top: 1rem;
line-height: 1.25;
}
.tid {
@ -69,8 +66,8 @@
.starsBackground {
position: absolute;
height: 8rem;
top: 0;
height: 7rem;
top: -0.75rem;
left: 50%;
transform: translateX(-50%);
z-index: -1;

View File

@ -104,7 +104,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const avatarPeer = !photo ? (peer || customPeer) : undefined;
const header = (
<div className={buildClassName(styles.header, styles.starsHeader)}>
<div className={styles.header}>
{media && (
<PaidMediaThumb
className={buildClassName(styles.mediaPreview, 'transaction-media-preview')}
@ -124,12 +124,14 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
{shouldDisplayAvatar && (
<Avatar peer={avatarPeer} webPhoto={photo} size="giant" />
)}
<img
className={buildClassName(styles.starsBackground)}
src={StarsBackground}
alt=""
draggable={false}
/>
{!topSticker && (
<img
className={buildClassName(styles.starsBackground)}
src={StarsBackground}
alt=""
draggable={false}
/>
)}
{title && <h1 className={styles.title}>{title}</h1>}
<p className={styles.description}>{description}</p>
<p className={styles.amount}>

View File

@ -80,7 +80,7 @@ function renderSummary(lang: OldLangFn, chat: ApiChat, blobUrl?: string) {
) : (
<Avatar
peer={chat}
size="small-mobile"
size="small"
className={styles.image}
withStorySolid
forceUnreadStorySolid

View File

@ -217,8 +217,8 @@
.modal-absolute-close-button {
position: absolute;
top: 0.5rem;
left: 0.5rem;
top: 0.375rem;
left: 0.375rem;
z-index: 1;
}
}

View File

@ -133,21 +133,26 @@ const Modal: FC<OwnProps> = ({
}
if (!title && !withCloseButton) return undefined;
const closeButton = (
<Button
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
round
color="translucent"
size="smaller"
ariaLabel={lang('Close')}
onClick={onClose}
>
<Icon name="close" />
</Button>
);
if (hasAbsoluteCloseButton) {
return closeButton;
}
return (
<div className={buildClassName('modal-header', headerClassName)}>
{withCloseButton && (
<Button
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
round
color="translucent"
size="smaller"
ariaLabel={lang('Close')}
onClick={onClose}
>
<Icon name="close" />
</Button>
)}
{withCloseButton && closeButton}
<div className="modal-title">{title}</div>
</div>
);