[Refactoring] Simplify avatars (#3365)

This commit is contained in:
Alexander Zinchuk 2023-07-05 13:14:20 +02:00
parent bb31151402
commit 05ff8ce87a
38 changed files with 149 additions and 193 deletions

View File

@ -29,14 +29,12 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
user?: ApiUser; peer?: ApiUser | ApiChat;
chat?: ApiChat;
}; };
const GroupCallParticipant: FC<OwnProps & StateProps> = ({ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
participant, participant,
user, peer,
chat,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -124,13 +122,13 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
isMutedByMe, isRaiseHand, isSelf, hasCustomVolume, isMuted, isSpeaking, participant.about, participant.volume, lang, isMutedByMe, isRaiseHand, isSelf, hasCustomVolume, isMuted, isSpeaking, participant.about, participant.volume, lang,
]); ]);
if (!user && !chat) { if (!peer) {
return undefined; return undefined;
} }
return ( return (
<ListItem <ListItem
leftElement={<Avatar user={user} chat={chat} className={styles.avatar} />} leftElement={<Avatar peer={peer} className={styles.avatar} />}
rightElement={<OutlinedMicrophoneIcon participant={participant} className={styles.icon} />} rightElement={<OutlinedMicrophoneIcon participant={participant} className={styles.icon} />}
className={styles.root} className={styles.root}
onClick={handleContextMenu} onClick={handleContextMenu}
@ -140,7 +138,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
ripple ripple
ref={ref} ref={ref}
> >
<FullNameTitle peer={user || chat!} withEmojiStatus className={styles.title} /> <FullNameTitle peer={peer} withEmojiStatus className={styles.title} />
<span className={buildClassName(styles.subtitle, 'subtitle', aboutColor)}> <span className={buildClassName(styles.subtitle, 'subtitle', aboutColor)}>
{hasPresentationStream && <i className="icon icon-share-screen" aria-hidden />} {hasPresentationStream && <i className="icon icon-share-screen" aria-hidden />}
{hasVideoStream && <i className="icon icon-video" aria-hidden />} {hasVideoStream && <i className="icon icon-video" aria-hidden />}
@ -166,8 +164,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { participant }): StateProps => { (global, { participant }): StateProps => {
return { return {
user: participant.isUser ? selectUser(global, participant.id) : undefined, peer: selectUser(global, participant.id) || selectChat(global, participant.id),
chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
}; };
}, },
)(GroupCallParticipant)); )(GroupCallParticipant));

View File

@ -27,6 +27,8 @@ type StateProps = {
isActive: boolean; isActive: boolean;
}; };
const PREVIEW_AVATARS_COUNT = 3;
const GroupCallTopPane: FC<OwnProps & StateProps> = ({ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
chatId, chatId,
isActive, isActive,
@ -58,19 +60,10 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
return Object.values(participants).filter((_, i) => i < 3).map(({ id, isUser }) => { return Object.values(participants)
if (isUser) { .slice(0, PREVIEW_AVATARS_COUNT)
if (!usersById[id]) { .map(({ id }) => usersById[id] || chatsById[id])
return undefined; .filter(Boolean);
}
return { user: usersById[id] };
} else {
if (!chatsById[id]) {
return undefined;
}
return { chat: chatsById[id] };
}
}).filter(Boolean);
}, [participants]); }, [participants]);
useEffect(() => { useEffect(() => {
@ -107,17 +100,12 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
<span className="participants">{lang('Participants', groupCall.participantsCount || 0, 'i')}</span> <span className="participants">{lang('Participants', groupCall.participantsCount || 0, 'i')}</span>
</div> </div>
<div className="avatars"> <div className="avatars">
{fetchedParticipants.map((p) => { {fetchedParticipants.map((peer) => (
if (!p) return undefined; <Avatar
key={peer.id}
return ( peer={peer}
<Avatar />
key={p.user ? p.user.id : p.chat.id} ))}
chat={p.chat}
user={p.user}
/>
);
})}
</div> </div>
<Button round className="join"> <Button round className="join">
{lang('VoipChatJoin')} {lang('VoipChatJoin')}

View File

@ -234,7 +234,7 @@ const PhoneCall: FC<StateProps> = ({
dialogRef={containerRef} dialogRef={containerRef}
> >
<Avatar <Avatar
user={user} peer={user}
size="jumbo" size="jumbo"
className={hasVideo || hasPresentation ? styles.blurred : ''} className={hasVideo || hasPresentation ? styles.blurred : ''}
/> />

View File

@ -7,16 +7,16 @@
width: 3.375rem; width: 3.375rem;
height: 3.375rem; height: 3.375rem;
border-radius: var(--radius); border-radius: var(--radius);
background: linear-gradient(var(--color-white) -125%, var(--color-user)); background-image: linear-gradient(var(--color-white) -125%, var(--color-user));
color: white; color: white;
font-weight: bold; font-weight: bold;
display: flex; display: flex;
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;
position: relative; position: relative;
overflow: hidden;
&__media { &__media {
border-radius: var(--radius);
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -124,29 +124,6 @@
} }
} }
&.online::after {
content: "";
display: block;
position: absolute;
bottom: 0.0625rem;
right: 0.0625rem;
width: 0.875rem;
height: 0.875rem;
border-radius: 50%;
border: 2px solid var(--color-background);
background-color: #0ac630;
flex-shrink: 0;
opacity: 0.5;
transform: scale(0);
transition: opacity 200ms, transform 200ms;
}
&.online-open::after {
opacity: 1;
transform: scale(1);
}
&.interactive { &.interactive {
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
} }

View File

@ -1,11 +1,11 @@
import type { MouseEvent as ReactMouseEvent } from 'react'; import type { MouseEvent as ReactMouseEvent } from 'react';
import React, { import React, {
memo, useMemo, useRef, memo, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC, TeactNode } from '../../lib/teact/teact';
import type { import type {
ApiChat, ApiPhoto, ApiUser, ApiUserStatus, ApiChat, ApiPhoto, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
@ -19,7 +19,6 @@ import {
isUserId, isUserId,
isChatWithRepliesBot, isChatWithRepliesBot,
isDeletedUser, isDeletedUser,
isUserOnline,
} from '../../global/helpers'; } from '../../global/helpers';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName'; import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
@ -44,10 +43,8 @@ cn.icon = cn('icon');
type OwnProps = { type OwnProps = {
className?: string; className?: string;
size?: 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'jumbo'; size?: 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'jumbo';
chat?: ApiChat; peer?: ApiChat | ApiUser;
user?: ApiUser;
photo?: ApiPhoto; photo?: ApiPhoto;
userStatus?: ApiUserStatus;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
withVideo?: boolean; withVideo?: boolean;
@ -60,10 +57,8 @@ type OwnProps = {
const Avatar: FC<OwnProps> = ({ const Avatar: FC<OwnProps> = ({
className, className,
size = 'large', size = 'large',
chat, peer,
user,
photo, photo,
userStatus,
text, text,
isSavedMessages, isSavedMessages,
withVideo, withVideo,
@ -74,8 +69,10 @@ const Avatar: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const videoLoopCountRef = useRef(0); const videoLoopCountRef = useRef(0);
const user = peer && isUserId(peer.id) ? peer as ApiUser : undefined;
const chat = peer && !isUserId(peer.id) ? peer as ApiChat : undefined;
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isReplies = user && isChatWithRepliesBot(user.id); const isReplies = peer && isChatWithRepliesBot(peer.id);
const isForum = chat?.isForum; const isForum = chat?.isForum;
let imageHash: string | undefined; let imageHash: string | undefined;
let videoHash: string | undefined; let videoHash: string | undefined;
@ -84,10 +81,8 @@ const Avatar: FC<OwnProps> = ({
const shouldFetchBig = size === 'jumbo'; const shouldFetchBig = size === 'jumbo';
if (!isSavedMessages && !isDeleted) { if (!isSavedMessages && !isDeleted) {
if (user && !noPersonalPhoto) { if ((user && !noPersonalPhoto) || chat) {
imageHash = getChatAvatarHash(user, shouldFetchBig ? 'big' : undefined); imageHash = getChatAvatarHash(peer!, shouldFetchBig ? 'big' : undefined);
} else if (chat) {
imageHash = getChatAvatarHash(chat, shouldFetchBig ? 'big' : undefined);
} else if (photo) { } else if (photo) {
imageHash = `photo${photo.id}?size=m`; imageHash = `photo${photo.id}?size=m`;
if (photo.isVideo && withVideo) { if (photo.isVideo && withVideo) {
@ -104,12 +99,6 @@ const Avatar: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(hasBlobUrl); const transitionClassNames = useMediaTransition(hasBlobUrl);
const isOnline = !isSavedMessages && user && userStatus && isUserOnline(user, userStatus);
const onlineTransitionClassNames = useMediaTransition(isOnline);
const onlineClassNamesPrefixed = useMemo(() => {
return onlineTransitionClassNames.split(' ').map((c) => (c === 'shown' ? 'online' : `online-${c}`)).join(' ');
}, [onlineTransitionClassNames]);
const handleVideoEnded = useLastCallback((e) => { const handleVideoEnded = useLastCallback((e) => {
const video = e.currentTarget; const video = e.currentTarget;
if (!videoBlobUrl) return; if (!videoBlobUrl) return;
@ -200,12 +189,11 @@ const Avatar: FC<OwnProps> = ({
const fullClassName = buildClassName( const fullClassName = buildClassName(
`Avatar size-${size}`, `Avatar size-${size}`,
className, className,
`color-bg-${getUserColorKey(user || chat)}`, `color-bg-${getUserColorKey(peer)}`,
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',
isForum && 'forum', isForum && 'forum',
onlineClassNamesPrefixed,
onClick && 'interactive', onClick && 'interactive',
(!isSavedMessages && !imgBlobUrl) && 'no-photo', (!isSavedMessages && !imgBlobUrl) && 'no-photo',
); );
@ -218,13 +206,11 @@ const Avatar: FC<OwnProps> = ({
} }
}); });
const senderId = (user || chat) && (user || chat)!.id;
return ( return (
<div <div
ref={ref} ref={ref}
className={fullClassName} className={fullClassName}
data-test-sender-id={IS_TEST ? senderId : undefined} data-test-sender-id={IS_TEST ? peer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined} aria-label={typeof content === 'string' ? author : undefined}
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}

View File

@ -123,7 +123,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}> <div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
<Avatar <Avatar
size="tiny" size="tiny"
chat={chat} peer={chat}
isSavedMessages={isChatWithSelf} isSavedMessages={isChatWithSelf}
/> />
<h3 className="modal-title">{lang(renderTitle())}</h3> <h3 className="modal-title">{lang(renderTitle())}</h3>

View File

@ -182,7 +182,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
key={chat.id} key={chat.id}
size={avatarSize} size={avatarSize}
chat={chat} peer={chat}
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/> />
)} )}

View File

@ -56,11 +56,10 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
); );
titleText = title; titleText = title;
} else if (chat || user) { } else if (user || chat) {
iconElement = ( iconElement = (
<Avatar <Avatar
chat={chat} peer={user || chat}
user={user}
size="small" size="small"
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
/> />

View File

@ -175,7 +175,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
key={user.id} key={user.id}
size={avatarSize} size={avatarSize}
user={user} peer={user}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/> />

View File

@ -222,6 +222,27 @@
transition: opacity var(--layer-transition); transition: opacity var(--layer-transition);
} }
} }
.avatar-online {
position: absolute;
bottom: 0.0625rem;
right: 0.0625rem;
width: 0.875rem;
height: 0.875rem;
border-radius: 50%;
border: 2px solid var(--color-background);
background-color: #0ac630;
flex-shrink: 0;
opacity: 0.5;
transform: scale(0);
transition: opacity 200ms, transform 200ms;
&.avatar-online-shown {
opacity: 1;
transform: scale(1);
}
}
} }
.info { .info {

View File

@ -21,6 +21,7 @@ import {
getMessageAction, getMessageAction,
getPrivateChatUserId, getPrivateChatUserId,
isUserId, isUserId,
isUserOnline,
selectIsChatMuted, selectIsChatMuted,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { import {
@ -49,6 +50,7 @@ import useFlag from '../../../hooks/useFlag';
import useChatListEntry from './hooks/useChatListEntry'; import useChatListEntry from './hooks/useChatListEntry';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useShowTransition from '../../../hooks/useShowTransition';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -225,10 +227,15 @@ const Chat: FC<OwnProps & StateProps> = ({
} }
}, [chat, chatId, isForum, isIntersecting]); }, [chat, chatId, isForum, isIntersecting]);
const isOnline = user && userStatus && isUserOnline(user, userStatus);
const { hasShownClass: isAvatarOnlineShown } = useShowTransition(isOnline);
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
const peer = user || chat;
const className = buildClassName( const className = buildClassName(
'Chat chat-item-clickable', 'Chat chat-item-clickable',
isUserId(chatId) ? 'private' : 'group', isUserId(chatId) ? 'private' : 'group',
@ -251,12 +258,11 @@ const Chat: FC<OwnProps & StateProps> = ({
> >
<div className="status"> <div className="status">
<Avatar <Avatar
chat={chat} peer={peer}
user={user}
userStatus={userStatus}
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
/> />
<div className="avatar-badge-wrapper"> <div className="avatar-badge-wrapper">
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={getIsForumPanelClosed} /> <ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={getIsForumPanelClosed} />
</div> </div>
{chat.isCallActive && chat.isCallNotEmpty && ( {chat.isCallActive && chat.isCallNotEmpty && (
@ -266,7 +272,7 @@ const Chat: FC<OwnProps & StateProps> = ({
<div className="info"> <div className="info">
<div className="info-row"> <div className="info-row">
<FullNameTitle <FullNameTitle
peer={user || chat} peer={peer}
withEmojiStatus withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf} isSavedMessages={chatId === user?.id && user?.isSelf}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}

View File

@ -72,6 +72,8 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
const peer = privateChatUser || chat;
return ( return (
<ListItem <ListItem
className="ChatMessage chat-item-clickable" className="ChatMessage chat-item-clickable"
@ -80,14 +82,13 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
buttonRef={buttonRef} buttonRef={buttonRef}
> >
<Avatar <Avatar
chat={chat} peer={peer}
user={privateChatUser}
isSavedMessages={privateChatUser?.isSelf} isSavedMessages={privateChatUser?.isSelf}
/> />
<div className="info"> <div className="info">
<div className="info-row"> <div className="info-row">
<FullNameTitle <FullNameTitle
peer={privateChatUser || chat} peer={peer}
withEmojiStatus withEmojiStatus
isSavedMessages={chatId === privateChatUser?.id && privateChatUser?.isSelf} isSavedMessages={chatId === privateChatUser?.id && privateChatUser?.isSelf}
/> />

View File

@ -83,7 +83,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
onClick={() => handleClick(userId)} onClick={() => handleClick(userId)}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
<Avatar user={usersById[userId]} /> <Avatar peer={usersById[userId]} />
<div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div> <div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div>
</div> </div>
))} ))}

View File

@ -75,7 +75,7 @@ const SettingsActiveWebsite: FC<OwnProps & StateProps> = ({
> >
<Avatar <Avatar
className={styles.avatar} className={styles.avatar}
user={renderingBot} peer={renderingBot}
size="large" size="large"
/> />
{renderingBot && <FullNameTitle className={styles.title} peer={renderingBot} />} {renderingBot && <FullNameTitle className={styles.title} peer={renderingBot} />}

View File

@ -110,7 +110,7 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => handleOpenSessionModal(session.hash)} onClick={() => handleOpenSessionModal(session.hash)}
> >
<Avatar className={styles.avatar} user={bot} size="tiny" /> <Avatar className={styles.avatar} peer={bot} size="tiny" />
<div className="multiline-menu-item full-size" dir="auto"> <div className="multiline-menu-item full-size" dir="auto">
<span className="date">{formatPastTimeShort(lang, session.dateActive * 1000)}</span> <span className="date">{formatPastTimeShort(lang, session.dateActive * 1000)}</span>
{bot && <FullNameTitle className={styles.title} peer={bot} />} {bot && <FullNameTitle className={styles.title} peer={bot} />}

View File

@ -68,9 +68,9 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
function renderContact(contactId: string, i: number, viewportOffset: number) { function renderContact(contactId: string, i: number, viewportOffset: number) {
const isPrivate = isUserId(contactId); const isPrivate = isUserId(contactId);
const user = isPrivate ? usersByIds[contactId] : undefined; const user = usersByIds[contactId];
const chat = !isPrivate ? chatsByIds[contactId] : undefined; const chat = chatsByIds[contactId];
const userOrChat = user || chat; const peer = user || chat;
const className = buildClassName( const className = buildClassName(
'Chat chat-item-clickable blocked-list-item small-icon', 'Chat chat-item-clickable blocked-list-item small-icon',
@ -96,11 +96,10 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
> >
<Avatar <Avatar
size="medium" size="medium"
user={user} peer={peer}
chat={chat}
/> />
<div className="contact-info" dir="auto"> <div className="contact-info" dir="auto">
{userOrChat && <FullNameTitle peer={userOrChat} />} {peer && <FullNameTitle peer={peer} />}
{user?.phoneNumber && ( {user?.phoneNumber && (
<div className="contact-phone" dir="auto">{formatPhoneNumberWithCode(phoneCodeList, user.phoneNumber)}</div> <div className="contact-phone" dir="auto">{formatPhoneNumberWithCode(phoneCodeList, user.phoneNumber)}</div>
)} )}

View File

@ -122,7 +122,7 @@ const NewContactModal: FC<OwnProps & StateProps> = ({
<div className="NewContactModal__profile" dir={lang.isRtl ? 'rtl' : undefined}> <div className="NewContactModal__profile" dir={lang.isRtl ? 'rtl' : undefined}>
<Avatar <Avatar
size="jumbo" size="jumbo"
user={renderingUser} peer={renderingUser}
text={`${firstName} ${lastName}`} text={`${firstName} ${lastName}`}
/> />
<div className="NewContactModal__profile-info"> <div className="NewContactModal__profile-info">

View File

@ -125,7 +125,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
<i className="icon icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<Avatar <Avatar
user={renderedUser} peer={renderedUser}
size="jumbo" size="jumbo"
className={styles.avatar} className={styles.avatar}
/> />

View File

@ -77,11 +77,7 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
return ( return (
<div className="SenderInfo" onClick={handleFocusMessage}> <div className="SenderInfo" onClick={handleFocusMessage}>
{isUserId(sender.id) ? ( <Avatar key={sender.id} size="medium" peer={sender} />
<Avatar key={sender.id} size="medium" user={sender as ApiUser} />
) : (
<Avatar key={sender.id} size="medium" chat={sender as ApiChat} />
)}
<div className="meta"> <div className="meta">
<div className="title" dir="auto"> <div className="title" dir="auto">
{senderTitle && renderText(senderTitle)} {senderTitle && renderText(senderTitle)}

View File

@ -1,20 +1,17 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useMemo, useEffect, useState, useRef, memo, useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiAvailableReaction, ApiMessage, ApiReaction } from '../../api/types'; import type { ApiAvailableReaction, ApiMessage, ApiReaction } from '../../api/types';
import { LoadMoreDirection } from '../../types'; import { LoadMoreDirection } from '../../types';
import { import { selectChatMessage, selectTabState } from '../../global/selectors';
selectChatMessage,
selectTabState,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatIntegerCompact } from '../../util/textFormat'; import { formatIntegerCompact } from '../../util/textFormat';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import { isSameReaction, getReactionUniqueKey } from '../../global/helpers'; import { getReactionUniqueKey, isSameReaction } from '../../global/helpers';
import { formatDateAtTime } from '../../util/dateFormat'; import { formatDateAtTime } from '../../util/dateFormat';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -63,6 +60,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const chatsById = getGlobal().chats.byId;
const lang = useLang(); const lang = useLang();
const [isClosing, startClosing, stopClosing] = useFlag(false); const [isClosing, startClosing, stopClosing] = useFlag(false);
@ -187,25 +185,26 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
onLoadMore={getMore} onLoadMore={getMore}
> >
{viewportIds?.flatMap( {viewportIds?.flatMap(
(userId) => { (peerId) => {
const user = usersById[userId]; const peer = usersById[peerId] || chatsById[peerId];
const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === userId);
const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === peerId);
const items: React.ReactNode[] = []; const items: React.ReactNode[] = [];
const seenByUser = seenByDates?.[userId]; const seenByUser = seenByDates?.[peerId];
userReactions?.forEach((r) => { userReactions?.forEach((r) => {
if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return; if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return;
items.push( items.push(
<ListItem <ListItem
key={`${userId}-${getReactionUniqueKey(r.reaction)}`} key={`${peerId}-${getReactionUniqueKey(r.reaction)}`}
className="chat-item-clickable reactors-list-item" className="chat-item-clickable reactors-list-item"
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => handleClick(userId)} onClick={() => handleClick(peerId)}
> >
<Avatar user={user} size="medium" /> <Avatar peer={peer} size="medium" />
<div className="info"> <div className="info">
<FullNameTitle peer={user} withEmojiStatus /> <FullNameTitle peer={peer} withEmojiStatus />
<span className="status" dir="auto"> <span className="status" dir="auto">
<i className="icon icon-heart-outline status-icon" /> <i className="icon icon-heart-outline status-icon" />
{formatDateAtTime(lang, r.addedDate * 1000)} {formatDateAtTime(lang, r.addedDate * 1000)}
@ -225,13 +224,13 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
if (!chosenTab && !userReactions?.length) { if (!chosenTab && !userReactions?.length) {
items.push( items.push(
<ListItem <ListItem
key={`${userId}-seen-by`} key={`${peerId}-seen-by`}
className="chat-item-clickable scroll-item small-icon" className="chat-item-clickable scroll-item small-icon"
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => handleClick(userId)} onClick={() => handleClick(peerId)}
> >
<PrivateChatInfo <PrivateChatInfo
userId={userId} userId={peerId}
noStatusOrTyping noStatusOrTyping
avatarSize="medium" avatarSize="medium"
status={seenByUser ? formatDateAtTime(lang, seenByUser * 1000) : undefined} status={seenByUser ? formatDateAtTime(lang, seenByUser * 1000) : undefined}

View File

@ -36,7 +36,7 @@ const BotCommand: FC<OwnProps> = ({
focus={focus} focus={focus}
> >
{withAvatar && ( {withAvatar && (
<Avatar size="small" user={bot} /> <Avatar size="small" peer={bot} />
)} )}
<div className="content-inner"> <div className="content-inner">
<span className="title">/{botCommand.command}</span> <span className="title">/{botCommand.command}</span>

View File

@ -1383,8 +1383,7 @@ const Composer: FC<OwnProps & StateProps> = ({
className={buildClassName('send-as-button', shouldAnimateSendAsButtonRef.current && 'appear-animation')} className={buildClassName('send-as-button', shouldAnimateSendAsButtonRef.current && 'appear-animation')}
> >
<Avatar <Avatar
user={sendAsUser} peer={sendAsUser || sendAsChat}
chat={sendAsChat}
size="tiny" size="tiny"
/> />
</Button> </Button>

View File

@ -7,7 +7,6 @@ import type { ApiSendAsPeerId } from '../../../api/types';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import { isUserId } from '../../../global/helpers';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
@ -95,9 +94,9 @@ const SendAsMenu: FC<OwnProps> = ({
> >
<div className="send-as-title" dir="auto">{lang('SendMessageAsTitle')}</div> <div className="send-as-title" dir="auto">{lang('SendMessageAsTitle')}</div>
{usersById && chatsById && sendAsPeerIds?.map(({ id, isPremium }, index) => { {usersById && chatsById && sendAsPeerIds?.map(({ id, isPremium }, index) => {
const user = isUserId(id) ? usersById[id] : undefined; const user = usersById[id];
const chat = !user ? chatsById[id] : undefined; const chat = chatsById[id];
const userOrChat = user || chat; const peer = user || chat;
const handleClick = () => { const handleClick = () => {
if (!isPremium || isCurrentUserPremium) { if (!isPremium || isCurrentUserPremium) {
@ -128,12 +127,11 @@ const SendAsMenu: FC<OwnProps> = ({
> >
<Avatar <Avatar
size="small" size="small"
chat={chat} peer={peer}
user={user}
className={avatarClassName} className={avatarClassName}
/> />
<div className="info"> <div className="info">
{userOrChat && <FullNameTitle peer={userOrChat} noFake />} {peer && <FullNameTitle peer={peer} noFake />}
<span className="subtitle">{user <span className="subtitle">{user
? lang('VoipGroupPersonalAccount') ? lang('VoipGroupPersonalAccount')
: lang('Subscribers', chat?.membersCount, 'i')} : lang('Subscribers', chat?.membersCount, 'i')}

View File

@ -282,7 +282,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
) : stickerSet.id === FAVORITE_SYMBOL_SET_ID ? ( ) : stickerSet.id === FAVORITE_SYMBOL_SET_ID ? (
<i className="icon icon-favorite" /> <i className="icon icon-favorite" />
) : stickerSet.id === CHAT_STICKER_SET_ID ? ( ) : stickerSet.id === CHAT_STICKER_SET_ID ? (
<Avatar chat={chat} size="small" /> <Avatar peer={chat} size="small" />
) : ( ) : (
<StickerSetCover <StickerSetCover
stickerSet={stickerSet as ApiStickerSet} stickerSet={stickerSet as ApiStickerSet}

View File

@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import type { import type {
ApiChat, ApiThreadInfo, ApiUser, ApiThreadInfo,
} from '../../../api/types'; } from '../../../api/types';
import { isUserId } from '../../../global/helpers'; import { isUserId } from '../../../global/helpers';
@ -57,14 +57,13 @@ const CommentButton: FC<OwnProps> = ({
function renderRecentRepliers() { function renderRecentRepliers() {
return ( return (
recentRepliers && recentRepliers.length > 0 && ( Boolean(recentRepliers?.length) && (
<div className="recent-repliers" dir={lang.isRtl ? 'rtl' : 'ltr'}> <div className="recent-repliers" dir={lang.isRtl ? 'rtl' : 'ltr'}>
{recentRepliers.map((user) => ( {recentRepliers!.map((peer) => (
<Avatar <Avatar
key={user.id} key={peer.id}
size="small" size="small"
user={isUserId(user.id) ? user as ApiUser : undefined} peer={peer}
chat={!isUserId(user.id) ? user as ApiChat : undefined}
/> />
))} ))}
</div> </div>

View File

@ -49,7 +49,7 @@ const Contact: FC<OwnProps & StateProps> = ({
> >
<Avatar <Avatar
size="large" size="large"
user={user} peer={user}
text={firstName || lastName} text={firstName || lastName}
/> />
<div className="contact-info"> <div className="contact-info">

View File

@ -12,7 +12,6 @@ import {
getMessageLocation, getMessageLocation,
buildStaticMapHash, buildStaticMapHash,
isGeoLiveExpired, isGeoLiveExpired,
isUserId,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat'; import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat';
import { import {
@ -87,10 +86,6 @@ const Location: FC<OwnProps> = ({
const prevMediaBlobUrl = usePrevious(mediaBlobUrl); const prevMediaBlobUrl = usePrevious(mediaBlobUrl);
const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl; const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl;
const isPeerUser = peer && isUserId(peer.id);
const avatarUser = (peer && isPeerUser) ? peer as ApiUser : undefined;
const avatarChat = (peer && !isPeerUser) ? peer as ApiChat : undefined;
const accuracyRadiusPx = useMemo(() => { const accuracyRadiusPx = useMemo(() => {
if (type !== 'geoLive' || !point.accuracyRadius) { if (type !== 'geoLive' || !point.accuracyRadius) {
return 0; return 0;
@ -213,7 +208,7 @@ const Location: FC<OwnProps> = ({
if (type === 'geoLive') { if (type === 'geoLive') {
return ( return (
<div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN}> <div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN}>
<Avatar chat={avatarChat} user={avatarUser} className="location-avatar" /> <Avatar peer={peer} className="location-avatar" />
{location.heading !== undefined && ( {location.heading !== undefined && (
<div className="direction" style={`--direction: ${location.heading}deg`} /> <div className="direction" style={`--direction: ${location.heading}deg`} />
)} )}

View File

@ -774,18 +774,14 @@ const Message: FC<OwnProps & StateProps> = ({
} }
function renderAvatar() { function renderAvatar() {
const isAvatarPeerUser = avatarPeer && isUserId(avatarPeer.id);
const avatarUser = (avatarPeer && isAvatarPeerUser) ? avatarPeer as ApiUser : undefined;
const avatarChat = (avatarPeer && !isAvatarPeerUser) ? avatarPeer as ApiChat : undefined;
const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined; const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined;
return ( return (
<Avatar <Avatar
size={isMobile ? 'small-mobile' : 'small'} size={isMobile ? 'small-mobile' : 'small'}
user={avatarUser} peer={avatarPeer}
chat={avatarChat}
text={hiddenName} text={hiddenName}
onClick={(avatarUser || avatarChat) ? handleAvatarClick : undefined} onClick={avatarPeer ? handleAvatarClick : undefined}
/> />
); );
} }

View File

@ -415,7 +415,7 @@ const MessageContextMenu: FC<OwnProps> = ({
{seenByRecentUsers?.map((user) => ( {seenByRecentUsers?.map((user) => (
<Avatar <Avatar
size="micro" size="micro"
user={user} peer={user}
/> />
))} ))}
</div> </div>

View File

@ -226,7 +226,7 @@ const Poll: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
key={user.id} key={user.id}
size="micro" size="micro"
user={user} peer={user}
/> />
))} ))}
</div> </div>

View File

@ -84,7 +84,7 @@ const ReactionButton: FC<{
{recentReactors.map((user) => ( {recentReactors.map((user) => (
<Avatar <Avatar
key={user.id} key={user.id}
user={user} peer={user}
size="micro" size="micro"
/> />
))} ))}

View File

@ -13,7 +13,6 @@ import {
selectChat, selectChat,
selectCurrentTextSearch, selectCurrentTextSearch,
} from '../../global/selectors'; } from '../../global/selectors';
import { isChatChannel } from '../../global/helpers';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager'; import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { renderMessageSummary } from '../common/helpers/renderMessageText'; import { renderMessageSummary } from '../common/helpers/renderMessageText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -37,7 +36,6 @@ export type OwnProps = {
}; };
type StateProps = { type StateProps = {
chat?: ApiChat;
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
query?: string; query?: string;
totalCount?: number; totalCount?: number;
@ -48,7 +46,6 @@ const RightSearch: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
isActive, isActive,
chat,
messagesById, messagesById,
query, query,
totalCount, totalCount,
@ -96,26 +93,29 @@ const RightSearch: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
const senderUser = message.senderId ? selectUser(getGlobal(), message.senderId) : undefined; const global = getGlobal();
let senderChat; let senderPeer = message.senderId
if (chat && isChatChannel(chat)) { ? selectUser(global, message.senderId) || selectChat(global, message.senderId)
senderChat = chat; : undefined;
} else if (message.forwardInfo) {
if (!senderPeer && message.forwardInfo) {
const { isChannelPost, fromChatId } = message.forwardInfo; const { isChannelPost, fromChatId } = message.forwardInfo;
senderChat = isChannelPost && fromChatId ? selectChat(getGlobal(), fromChatId) : undefined; const originalSender = isChannelPost && fromChatId ? selectChat(global, fromChatId) : undefined;
} else { if (originalSender) senderPeer = originalSender;
senderChat = message.senderId ? selectChat(getGlobal(), message.senderId) : undefined; }
if (!senderPeer) {
return undefined;
} }
return { return {
message, message,
senderUser, senderPeer: senderPeer!,
senderChat,
onClick: () => focusMessage({ chatId, threadId, messageId: id }), onClick: () => focusMessage({ chatId, threadId, messageId: id }),
}; };
}).filter(Boolean); }).filter(Boolean);
}, [query, viewportIds, messagesById, chat, focusMessage, chatId, threadId]); }, [query, viewportIds, messagesById, focusMessage, chatId, threadId]);
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => { const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
const foundResult = viewportResults?.[index === -1 ? 0 : index]; const foundResult = viewportResults?.[index === -1 ? 0 : index];
@ -125,11 +125,10 @@ const RightSearch: FC<OwnProps & StateProps> = ({
}, '.ListItem-button', true); }, '.ListItem-button', true);
const renderSearchResult = ({ const renderSearchResult = ({
message, senderUser, senderChat, onClick, message, senderPeer, onClick,
}: { }: {
message: ApiMessage; message: ApiMessage;
senderUser?: ApiUser; senderPeer: ApiUser | ApiChat;
senderChat?: ApiChat;
onClick: NoneToVoidFunction; onClick: NoneToVoidFunction;
}) => { }) => {
const text = renderMessageSummary(lang, message, undefined, query); const text = renderMessageSummary(lang, message, undefined, query);
@ -142,12 +141,11 @@ const RightSearch: FC<OwnProps & StateProps> = ({
onClick={onClick} onClick={onClick}
> >
<Avatar <Avatar
chat={senderChat} peer={senderPeer}
user={senderUser}
/> />
<div className="info"> <div className="info">
<div className="search-result-message-top"> <div className="search-result-message-top">
<FullNameTitle peer={(senderUser || senderChat)!} withEmojiStatus /> <FullNameTitle peer={senderPeer} withEmojiStatus />
<LastMessageMeta message={message} /> <LastMessageMeta message={message} />
</div> </div>
<div className="subtitle" dir="auto"> <div className="subtitle" dir="auto">
@ -189,9 +187,8 @@ const RightSearch: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const messagesById = selectChatMessages(global, chatId);
const messagesById = chat && selectChatMessages(global, chat.id); if (!messagesById) {
if (!chat || !messagesById) {
return {}; return {};
} }
@ -199,7 +196,6 @@ export default memo(withGlobal<OwnProps>(
const { totalCount, foundIds } = results || {}; const { totalCount, foundIds } = results || {};
return { return {
chat,
messagesById, messagesById,
query, query,
totalCount, totalCount,

View File

@ -67,7 +67,7 @@ const JoinRequest: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
key={userId} key={userId}
size="medium" size="medium"
user={user} peer={user}
/> />
<div className={buildClassName('user-info')}> <div className={buildClassName('user-info')}>
<div className={buildClassName('user-name')}>{fullName}</div> <div className={buildClassName('user-name')}>{fullName}</div>

View File

@ -118,7 +118,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
<div className="modal-header"> <div className="modal-header">
<Avatar <Avatar
size="tiny" size="tiny"
chat={linkedChat} peer={linkedChat}
/> />
<div className="modal-title"> <div className="modal-title">
{lang(isChannel ? 'DiscussionUnlinkGroup' : 'DiscussionUnlinkChannel')} {lang(isChannel ? 'DiscussionUnlinkGroup' : 'DiscussionUnlinkChannel')}
@ -136,7 +136,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
<div className="modal-header"> <div className="modal-header">
<Avatar <Avatar
size="tiny" size="tiny"
chat={linkedGroup} peer={linkedGroup}
/> />
<div className="modal-title"> <div className="modal-title">
{lang('Channel.DiscussionGroup.LinkGroup')} {lang('Channel.DiscussionGroup.LinkGroup')}

View File

@ -230,7 +230,7 @@ const ManageUser: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
photo={notPersonalPhoto} photo={notPersonalPhoto}
noPersonalPhoto noPersonalPhoto
user={user} peer={user}
size="mini" size="mini"
className="personal-photo" className="personal-photo"
/> />

View File

@ -27,7 +27,7 @@ const StatisticsPublicForward: FC<OwnProps> = ({ data }) => {
return ( return (
<div className="StatisticsPublicForward" onClick={handleClick}> <div className="StatisticsPublicForward" onClick={handleClick}>
<Avatar size="medium" chat={data.chat} /> <Avatar size="medium" peer={data.chat} />
<div className="StatisticsPublicForward__info"> <div className="StatisticsPublicForward__info">
<div className="StatisticsPublicForward__title"> <div className="StatisticsPublicForward__title">

View File

@ -171,6 +171,10 @@ export function isUserOnline(user: ApiUser, userStatus?: ApiUserStatus) {
return false; return false;
} }
if (user.isSelf) {
return false;
}
return userStatus.type === 'userStatusOnline' && type !== 'userTypeBot'; return userStatus.type === 'userStatusOnline' && type !== 'userTypeBot';
} }

View File

@ -178,7 +178,7 @@ class TelegramClient {
return topic.id < offsetTopicId; return topic.id < offsetTopicId;
} }
return true; return true;
}).filter((_, i) => i < limit), }).slice(0, limit),
users: [], users: [],
chats: [], chats: [],
messages: [], messages: [],