[Perf] More performance fixes

This commit is contained in:
Alexander Zinchuk 2023-04-23 18:33:16 +04:00
parent 83557863b8
commit cfc71da0c1
337 changed files with 2022 additions and 1979 deletions

View File

@ -3,6 +3,7 @@ import type {
ApiEmojiStatus, ApiEmojiStatus,
ApiPremiumGiftOption, ApiPremiumGiftOption,
ApiUser, ApiUser,
ApiUserFullInfo,
ApiUserStatus, ApiUserStatus,
ApiUserType, ApiUserType,
} from '../../types'; } from '../../types';
@ -10,7 +11,7 @@ import { buildApiPeerId } from './peers';
import { buildApiBotInfo } from './bots'; import { buildApiBotInfo } from './bots';
import { buildApiPhoto, buildApiUsernames } from './common'; import { buildApiPhoto, buildApiUsernames } from './common';
export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUser { export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo {
const { const {
fullUser: { fullUser: {
about, commonChatsCount, pinnedMsgId, botInfo, blocked, about, commonChatsCount, pinnedMsgId, botInfo, blocked,
@ -20,22 +21,19 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse
users, users,
} = mtpUserFull; } = mtpUserFull;
const user = buildApiUser(users[0])!; const userId = buildApiPeerId(users[0].id, 'user');
return { return {
...user, bio: about,
fullInfo: { commonChatsCount,
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }), pinnedMessageId: pinnedMsgId,
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }), isBlocked: Boolean(blocked),
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }), noVoiceMessages: voiceMessagesForbidden,
bio: about, ...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
commonChatsCount, ...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
pinnedMessageId: pinnedMsgId, ...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }),
isBlocked: Boolean(blocked), ...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }),
noVoiceMessages: voiceMessagesForbidden, ...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }),
...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }),
...(botInfo && { botInfo: buildApiBotInfo(botInfo, user.id) }),
},
}; };
} }

View File

@ -4,6 +4,7 @@ import type {
ApiUpdateAuthorizationStateType, ApiUpdateAuthorizationStateType,
OnApiUpdate, OnApiUpdate,
ApiUser, ApiUser,
ApiUserFullInfo,
} from '../../types'; } from '../../types';
import { DEBUG } from '../../../config'; import { DEBUG } from '../../../config';
@ -117,10 +118,11 @@ export function onAuthReady() {
onUpdate(buildAuthStateUpdate('authorizationStateReady')); onUpdate(buildAuthStateUpdate('authorizationStateReady'));
} }
export function onCurrentUserUpdate(currentUser: ApiUser) { export function onCurrentUserUpdate(currentUser: ApiUser, currentUserFullInfo: ApiUserFullInfo) {
onUpdate({ onUpdate({
'@type': 'updateCurrentUser', '@type': 'updateCurrentUser',
currentUser, currentUser,
currentUserFullInfo,
}); });
} }

View File

@ -24,7 +24,7 @@ import {
import { updater } from '../updater'; import { updater } from '../updater';
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
import downloadMediaWithClient, { parseMediaUrl } from './media'; import downloadMediaWithClient, { parseMediaUrl } from './media';
import { buildApiUserFromFull } from '../apiBuilders/users'; import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users';
import localDb, { clearLocalDb } from '../localDb'; import localDb, { clearLocalDb } from '../localDb';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
import { addMessageToLocalDb, log } from '../helpers'; import { addMessageToLocalDb, log } from '../helpers';
@ -336,10 +336,11 @@ export async function fetchCurrentUser() {
localDb.photos[user.photo.id.toString()] = user.photo; localDb.photos[user.photo.id.toString()] = user.photo;
} }
localDb.users[buildApiPeerId(user.id, 'user')] = user; localDb.users[buildApiPeerId(user.id, 'user')] = user;
const currentUser = buildApiUserFromFull(userFull); const currentUserFullInfo = buildApiUserFullInfo(userFull);
const currentUser = buildApiUser(user)!;
setMessageBuilderCurrentUserId(currentUser.id); setMessageBuilderCurrentUserId(currentUser.id);
onCurrentUserUpdate(currentUser); onCurrentUserUpdate(currentUser, currentUserFullInfo);
currentUserId = currentUser.id; currentUserId = currentUser.id;
setIsPremium({ isPremium: Boolean(currentUser.isPremium) }); setIsPremium({ isPremium: Boolean(currentUser.isPremium) });

View File

@ -14,11 +14,11 @@ import {
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
export async function fetchChannelStatistics({ export async function fetchChannelStatistics({
chat, chat, dcId,
}: { chat: ApiChat }) { }: { chat: ApiChat; dcId?: number }) {
const result = await invokeRequest(new GramJs.stats.GetBroadcastStats({ const result = await invokeRequest(new GramJs.stats.GetBroadcastStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId); }), undefined, undefined, undefined, dcId);
if (!result) { if (!result) {
return undefined; return undefined;
@ -31,11 +31,11 @@ export async function fetchChannelStatistics({
} }
export async function fetchGroupStatistics({ export async function fetchGroupStatistics({
chat, chat, dcId,
}: { chat: ApiChat }) { }: { chat: ApiChat; dcId?: number }) {
const result = await invokeRequest(new GramJs.stats.GetMegagroupStats({ const result = await invokeRequest(new GramJs.stats.GetMegagroupStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId); }), undefined, undefined, undefined, dcId);
if (!result) { if (!result) {
return undefined; return undefined;
@ -52,14 +52,16 @@ export async function fetchGroupStatistics({
export async function fetchMessageStatistics({ export async function fetchMessageStatistics({
chat, chat,
messageId, messageId,
dcId,
}: { }: {
chat: ApiChat; chat: ApiChat;
messageId: number; messageId: number;
dcId?: number;
}): Promise<ApiMessageStatistics | undefined> { }): Promise<ApiMessageStatistics | undefined> {
const result = await invokeRequest(new GramJs.stats.GetMessageStats({ const result = await invokeRequest(new GramJs.stats.GetMessageStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
msgId: messageId, msgId: messageId,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId); }), undefined, undefined, undefined, dcId);
if (!result) { if (!result) {
return undefined; return undefined;

View File

@ -15,7 +15,7 @@ import {
getEntityTypeById, getEntityTypeById,
buildInputEmojiStatus, buildInputEmojiStatus,
} from '../gramjsBuilders'; } from '../gramjsBuilders';
import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildApiUser, buildApiUserFullInfo, buildApiUsersAndStatuses } from '../apiBuilders/users';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPhoto } from '../apiBuilders/common';
import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers'; import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers';
@ -40,28 +40,28 @@ export async function fetchFullUser({
return undefined; return undefined;
} }
const fullInfo = await invokeRequest(new GramJs.users.GetFullUser({ id: input })); const result = await invokeRequest(new GramJs.users.GetFullUser({ id: input }));
if (!fullInfo) { if (!result) {
return undefined; return undefined;
} }
updateLocalDb(fullInfo); updateLocalDb(result);
addUserToLocalDb(fullInfo.users[0], true); addUserToLocalDb(result.users[0], true);
if (fullInfo.fullUser.profilePhoto instanceof GramJs.Photo) { if (result.fullUser.profilePhoto instanceof GramJs.Photo) {
localDb.photos[fullInfo.fullUser.profilePhoto.id.toString()] = fullInfo.fullUser.profilePhoto; localDb.photos[result.fullUser.profilePhoto.id.toString()] = result.fullUser.profilePhoto;
} }
if (fullInfo.fullUser.personalPhoto instanceof GramJs.Photo) { if (result.fullUser.personalPhoto instanceof GramJs.Photo) {
localDb.photos[fullInfo.fullUser.personalPhoto.id.toString()] = fullInfo.fullUser.personalPhoto; localDb.photos[result.fullUser.personalPhoto.id.toString()] = result.fullUser.personalPhoto;
} }
if (fullInfo.fullUser.fallbackPhoto instanceof GramJs.Photo) { if (result.fullUser.fallbackPhoto instanceof GramJs.Photo) {
localDb.photos[fullInfo.fullUser.fallbackPhoto.id.toString()] = fullInfo.fullUser.fallbackPhoto; localDb.photos[result.fullUser.fallbackPhoto.id.toString()] = result.fullUser.fallbackPhoto;
} }
const botInfo = fullInfo.fullUser.botInfo; const botInfo = result.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;
} }
@ -69,8 +69,8 @@ export async function fetchFullUser({
localDb.documents[botInfo.descriptionDocument.id.toString()] = botInfo.descriptionDocument; localDb.documents[botInfo.descriptionDocument.id.toString()] = botInfo.descriptionDocument;
} }
const userWithFullInfo = buildApiUserFromFull(fullInfo); const fullInfo = buildApiUserFullInfo(result);
const user = buildApiUser(fullInfo.users[0]); const user = buildApiUser(result.users[0])!;
onUpdate({ onUpdate({
'@type': 'updateUser', '@type': 'updateUser',
@ -78,11 +78,11 @@ export async function fetchFullUser({
user: { user: {
...user, ...user,
avatarHash: user?.avatarHash || undefined, avatarHash: user?.avatarHash || undefined,
fullInfo: userWithFullInfo.fullInfo,
}, },
fullInfo,
}); });
return userWithFullInfo; return { user, fullInfo };
} }
export async function fetchCommonChats(id: string, accessHash?: string, maxId?: string) { export async function fetchCommonChats(id: string, accessHash?: string, maxId?: string) {

View File

@ -67,8 +67,6 @@ export interface ApiChat {
// Obtained from GetChatSettings // Obtained from GetChatSettings
settings?: ApiChatSettings; settings?: ApiChatSettings;
// Obtained from GetFullChat / GetFullChannel
fullInfo?: ApiChatFullInfo;
joinRequests?: ApiChatInviteImporter[]; joinRequests?: ApiChatInviteImporter[];
isJoinToSend?: boolean; isJoinToSend?: boolean;

View File

@ -87,6 +87,7 @@ export type ApiUpdateConnectionState = {
export type ApiUpdateCurrentUser = { export type ApiUpdateCurrentUser = {
'@type': 'updateCurrentUser'; '@type': 'updateCurrentUser';
currentUser: ApiUser; currentUser: ApiUser;
currentUserFullInfo: ApiUserFullInfo;
}; };
export type ApiUpdateChat = { export type ApiUpdateChat = {
@ -338,6 +339,7 @@ export type ApiUpdateUser = {
'@type': 'updateUser'; '@type': 'updateUser';
id: string; id: string;
user: Partial<ApiUser>; user: Partial<ApiUser>;
fullInfo?: ApiUserFullInfo;
}; };
export type ApiUpdateRequestUserUpdate = { export type ApiUpdateRequestUserUpdate = {

View File

@ -29,9 +29,6 @@ export interface ApiUser {
fakeType?: ApiFakeType; fakeType?: ApiFakeType;
isAttachBot?: boolean; isAttachBot?: boolean;
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatus;
// Obtained from GetFullUser / UserFullInfo
fullInfo?: ApiUserFullInfo;
} }
export interface ApiUserFullInfo { export interface ApiUserFullInfo {

View File

@ -103,7 +103,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover, &:hover,
&:focus { &:focus {

View File

@ -98,13 +98,13 @@ const AuthCode: FC<StateProps> = ({
<h1> <h1>
{authPhoneNumber} {authPhoneNumber}
<div <div
className="auth-number-edit" className="auth-number-edit div-button"
onClick={handleReturnToAuthPhoneNumber} onClick={handleReturnToAuthPhoneNumber}
role="button" role="button"
tabIndex={0} tabIndex={0}
title={lang('WrongNumber')} title={lang('WrongNumber')}
> >
<i className="icon-edit" /> <i className="icon icon-edit" />
</div> </div>
</h1> </h1>
<p className="note"> <p className="note">

View File

@ -1,6 +1,6 @@
.CountryCodeInput { .CountryCodeInput {
.input-group { .input-group {
cursor: pointer; cursor: var(--custom-cursor, pointer);
z-index: var(--z-country-code-input-group); z-index: var(--z-country-code-input-group);

View File

@ -84,7 +84,7 @@
align-items: center; align-items: center;
border-radius: 0.75rem; border-radius: 0.75rem;
transition: 0.15s ease-out background-color; transition: 0.15s ease-out background-color;
cursor: pointer; cursor: var(--custom-cursor, pointer);
color: var(--color-text-secondary); color: var(--color-text-secondary);
&:hover { &:hover {
@ -97,7 +97,7 @@
overflow: hidden; overflow: hidden;
} }
.icon { .icon-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -127,7 +127,7 @@
height: 8.75rem; height: 8.75rem;
button { button {
cursor: pointer; cursor: var(--custom-cursor, pointer);
} }
&::before { &::before {

View File

@ -143,7 +143,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
onClick={onTrigger} onClick={onTrigger}
ariaLabel={lang('AccDescrMoreOptions')} ariaLabel={lang('AccDescrMoreOptions')}
> >
<i className="icon-more" /> <i className="icon icon-more" />
</Button> </Button>
); );
}, [lang]); }, [lang]);
@ -276,7 +276,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
onClick={handleToggleFullscreen} onClick={handleToggleFullscreen}
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')} ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
> >
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} /> <i className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')} />
</Button> </Button>
)} )}
{isLandscapeLayout && ( {isLandscapeLayout && (
@ -286,7 +286,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
color="translucent" color="translucent"
onClick={handleToggleSidebar} onClick={handleToggleSidebar}
> >
<i className="icon-sidebar" /> <i className="icon icon-sidebar" />
</Button> </Button>
)} )}
{((IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand) || isAdmin) && ( {((IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand) || isAdmin) && (
@ -319,7 +319,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
color="translucent" color="translucent"
onClick={handleClose} onClick={handleClose}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
</div> </div>
@ -359,7 +359,11 @@ const GroupCall: FC<OwnProps & StateProps> = ({
)} )}
onClick={handleClickVideoOrSpeaker} onClick={handleClickVideoOrSpeaker}
> >
<i className={shouldRaiseHand ? 'icon-speaker' : (hasVideo ? 'icon-video-stop' : 'icon-video')} /> <i className={buildClassName(
'icon',
shouldRaiseHand ? 'icon-speaker' : (hasVideo ? 'icon-video-stop' : 'icon-video'),
)}
/>
</button> </button>
</div> </div>
@ -372,7 +376,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
<div className="button-wrapper"> <div className="button-wrapper">
<button className="small-button leave" onClick={handleLeaveGroupCall}> <button className="small-button leave" onClick={handleLeaveGroupCall}>
<i className="icon-phone-discard" /> <i className="icon icon-phone-discard" />
</button> </button>
<div className="button-text"> <div className="button-text">

View File

@ -7,7 +7,7 @@
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.75rem; border-radius: 0.75rem;
transition: 0.15s ease-out background-color; transition: 0.15s ease-out background-color;
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover { &:hover {
background: #2f363e; background: #2f363e;
@ -72,7 +72,7 @@
} }
.streams { .streams {
cursor: pointer; cursor: var(--custom-cursor, pointer);
display: flex; display: flex;
} }
} }

View File

@ -4,10 +4,10 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact'; import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types'; import type { ApiChat, ApiPhoto, ApiUser } from '../../../api/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectChat, selectUser } from '../../../global/selectors'; import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
@ -23,6 +23,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
userProfilePhoto?: ApiPhoto;
chat?: ApiChat; chat?: ApiChat;
}; };
@ -30,6 +31,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
openParticipantMenu, openParticipantMenu,
participant, participant,
user, user,
userProfilePhoto,
chat, chat,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -79,7 +81,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
onClick={handleOnClick} onClick={handleOnClick}
ref={anchorRef} ref={anchorRef}
> >
<Avatar user={user} chat={chat} size="medium" /> <Avatar user={user} chat={chat} userProfilePhoto={userProfilePhoto} size="medium" />
<div className="info"> <div className="info">
<span className="name">{name}</span> <span className="name">{name}</span>
<span className={buildClassName('about', aboutColor)}>{aboutText}</span> <span className={buildClassName('about', aboutColor)}>{aboutText}</span>
@ -96,6 +98,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
user: participant.isUser ? selectUser(global, participant.id) : undefined, user: participant.isUser ? selectUser(global, participant.id) : undefined,
chat: !participant.isUser ? selectChat(global, participant.id) : undefined, chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
userProfilePhoto: participant.isUser ? selectUserPhotoFromFullInfo(global, participant.id) : undefined,
}; };
}, },
)(GroupCallParticipant)); )(GroupCallParticipant));

View File

@ -53,8 +53,8 @@ const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
return ( return (
<div className="participants"> <div className="participants">
<div className="invite-btn" onClick={handleCreateGroupCallInviteLink}> <div className="invite-btn" onClick={handleCreateGroupCallInviteLink}>
<div className="icon"> <div className="icon-wrapper">
<i className="icon-add-user" /> <i className="icon icon-add-user" />
</div> </div>
<div className="text">{lang('VoipGroupInviteMember')}</div> <div className="text">{lang('VoipGroupInviteMember')}</div>
</div> </div>

View File

@ -66,7 +66,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: var(--custom-cursor, pointer);
@mixin thumb-styles() { @mixin thumb-styles() {
border: none; border: none;

View File

@ -6,7 +6,7 @@
width: calc(50% - 0.25rem); width: calc(50% - 0.25rem);
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: 0.25s ease-out width; transition: 0.25s ease-out width;
cursor: pointer; cursor: var(--custom-cursor, pointer);
.thumbnail-avatar { .thumbnail-avatar {
position: absolute; position: absolute;
@ -57,7 +57,7 @@
gap: 0.25rem; gap: 0.25rem;
transition: 0.25s ease-out opacity, 0.25s ease-out background-color; transition: 0.25s ease-out opacity, 0.25s ease-out background-color;
opacity: 0; opacity: 0;
cursor: pointer; cursor: var(--custom-cursor, pointer);
outline: none !important; outline: none !important;
&:hover { &:hover {

View File

@ -4,11 +4,11 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback } from '../../../lib/teact/teact'; import React, { memo, useCallback } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types'; import type { ApiChat, ApiPhoto, ApiUser } from '../../../api/types';
import { GROUP_CALL_THUMB_VIDEO_DISABLED } from '../../../config'; import { GROUP_CALL_THUMB_VIDEO_DISABLED } from '../../../config';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectChat, selectUser } from '../../../global/selectors'; import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -25,6 +25,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
chat?: ApiChat; chat?: ApiChat;
userProfilePhoto?: ApiPhoto;
currentUserId?: string; currentUserId?: string;
isActive?: boolean; isActive?: boolean;
}; };
@ -34,6 +35,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
onClick, onClick,
user, user,
chat, chat,
userProfilePhoto,
isActive, isActive,
isFullscreen, isFullscreen,
}) => { }) => {
@ -56,11 +58,11 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
> >
{isFullscreen && ( {isFullscreen && (
<button className="back-button"> <button className="back-button">
<i className="icon-arrow-left" /> <i className="icon icon-arrow-left" />
{lang('Back')} {lang('Back')}
</button> </button>
)} )}
<Avatar user={user} chat={chat} className="thumbnail-avatar" /> <Avatar user={user} chat={chat} userProfilePhoto={userProfilePhoto} className="thumbnail-avatar" />
{!GROUP_CALL_THUMB_VIDEO_DISABLED && ( {!GROUP_CALL_THUMB_VIDEO_DISABLED && (
<div className="thumbnail-wrapper"> <div className="thumbnail-wrapper">
<video className="thumbnail" muted autoPlay playsInline srcObject={streams?.[type]} /> <video className="thumbnail" muted autoPlay playsInline srcObject={streams?.[type]} />
@ -68,9 +70,9 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
)} )}
<video className="video" muted autoPlay playsInline srcObject={streams?.[type]} /> <video className="video" muted autoPlay playsInline srcObject={streams?.[type]} />
<div className="info"> <div className="info">
<i className="icon-microphone-alt" /> <i className="icon icon-microphone-alt" />
<span className="name">{user?.firstName || chat?.title}</span> <span className="name">{user?.firstName || chat?.title}</span>
{type === 'presentation' && <i className="last-icon icon-active-sessions" />} {type === 'presentation' && <i className="icon last-icon icon-active-sessions" />}
</div> </div>
</div> </div>
); );
@ -82,6 +84,7 @@ export default memo(withGlobal<OwnProps>(
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
user: participant.isUser ? selectUser(global, participant.id) : undefined, user: participant.isUser ? selectUser(global, participant.id) : undefined,
chat: !participant.isUser ? selectChat(global, participant.id) : undefined, chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
userProfilePhoto: participant.isUser ? selectUserPhotoFromFullInfo(global, participant.id) : undefined,
isActive: (participant.amplitude || 0) > THRESHOLD, isActive: (participant.amplitude || 0) > THRESHOLD,
}; };
}, },

View File

@ -13,7 +13,7 @@
padding: 0.375rem 0.5rem 0.375rem 0.75rem; padding: 0.375rem 0.5rem 0.375rem 0.75rem;
background: var(--color-background); background: var(--color-background);
z-index: -1; z-index: -1;
cursor: pointer; cursor: var(--custom-cursor, pointer);
&::before { &::before {
content: ""; content: "";

View File

@ -2,9 +2,9 @@ import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
memo, useCallback, useEffect, useMemo, memo, useCallback, useEffect, useMemo,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat, ApiGroupCall, ApiUser } from '../../../api/types'; import type { ApiGroupCall } from '../../../api/types';
import type { AnimationLevel } from '../../../types'; import type { AnimationLevel } from '../../../types';
import { selectChatGroupCall } from '../../../global/selectors/calls'; import { selectChatGroupCall } from '../../../global/selectors/calls';
@ -14,6 +14,7 @@ import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import UserAvatar from '../../common/UserAvatar';
import './GroupCallTopPane.scss'; import './GroupCallTopPane.scss';
@ -26,8 +27,6 @@ type OwnProps = {
type StateProps = { type StateProps = {
groupCall?: ApiGroupCall; groupCall?: ApiGroupCall;
isActive: boolean; isActive: boolean;
usersById: Record<string, ApiUser>;
chatsById: Record<string, ApiChat>;
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
}; };
@ -37,8 +36,6 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
className, className,
groupCall, groupCall,
hasPinnedOffset, hasPinnedOffset,
usersById,
chatsById,
animationLevel, animationLevel,
}) => { }) => {
const { const {
@ -57,22 +54,28 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
const participants = groupCall?.participants; const participants = groupCall?.participants;
const fetchedParticipants = useMemo(() => { const fetchedParticipants = useMemo(() => {
if (participants) { if (!participants) {
return Object.values(participants).filter((_, i) => i < 3).map(({ id, isUser }) => { return [];
if (isUser) { }
if (!usersById[id]) {
return undefined; // No need for expensive global updates on users and chats, so we avoid them
} const usersById = getGlobal().users.byId;
return { user: usersById[id] }; const chatsById = getGlobal().chats.byId;
} else {
if (!chatsById[id]) { return Object.values(participants).filter((_, i) => i < 3).map(({ id, isUser }) => {
return undefined; if (isUser) {
} if (!usersById[id]) {
return { chat: chatsById[id] }; return undefined;
} }
}).filter(Boolean); return { user: usersById[id] };
} else return []; } else {
}, [chatsById, participants, usersById]); if (!chatsById[id]) {
return undefined;
}
return { chat: chatsById[id] };
}
}).filter(Boolean);
}, [participants]);
useEffect(() => { useEffect(() => {
if (!groupCall?.id) return undefined; if (!groupCall?.id) return undefined;
@ -111,7 +114,7 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
{fetchedParticipants.map((p) => { {fetchedParticipants.map((p) => {
if (!p) return undefined; if (!p) return undefined;
if (p.user) { if (p.user) {
return <Avatar key={p.user.id} user={p.user} animationLevel={animationLevel} />; return <UserAvatar key={p.user.id} user={p.user} />;
} else { } else {
return <Avatar key={p.chat.id} chat={p.chat} animationLevel={animationLevel} />; return <Avatar key={p.chat.id} chat={p.chat} animationLevel={animationLevel} />;
} }
@ -125,18 +128,18 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }) => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId)!; const chat = selectChat(global, chatId)!;
const groupCall = selectChatGroupCall(global, chatId); const groupCall = selectChatGroupCall(global, chatId);
const activeGroupCallId = selectTabState(global).isMasterTab ? global.groupCalls.activeGroupCallId : undefined; const activeGroupCallId = selectTabState(global).isMasterTab ? global.groupCalls.activeGroupCallId : undefined;
return { return {
groupCall, groupCall,
usersById: global.users.byId, isActive: activeGroupCallId !== groupCall?.id && Boolean(
chatsById: global.chats.byId, groupCall
activeGroupCallId: global.groupCalls.activeGroupCallId, ? groupCall.participantsCount > 0 && groupCall.isLoaded
isActive: ((!groupCall ? (chat && chat.isCallNotEmpty && chat.isCallActive) : chat && chat.isCallNotEmpty && chat.isCallActive,
: (groupCall.participantsCount > 0 && groupCall.isLoaded))) ),
&& (activeGroupCallId !== groupCall?.id),
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,
}; };
}, },

View File

@ -82,7 +82,7 @@
.emojis { .emojis {
user-select: none; user-select: none;
pointer-events: auto; pointer-events: auto;
cursor: pointer; cursor: var(--custom-cursor, pointer);
margin-top: 1rem; margin-top: 1rem;
height: 3rem; height: 3rem;
transition: 0.25s ease-in-out transform; transition: 0.25s ease-in-out transform;

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import '../../../global/actions/calls'; import '../../../global/actions/calls';
import type { ApiPhoneCall, ApiUser } from '../../../api/types'; import type { ApiPhoneCall, ApiPhoto, ApiUser } from '../../../api/types';
import type { AnimationLevel } from '../../../types'; import type { AnimationLevel } from '../../../types';
import { import {
@ -14,7 +14,7 @@ import {
IS_REQUEST_FULLSCREEN_SUPPORTED, IS_REQUEST_FULLSCREEN_SUPPORTED,
} from '../../../util/windowEnvironment'; } from '../../../util/windowEnvironment';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import { selectTabState } from '../../../global/selectors'; import { selectTabState, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectPhoneCallUser } from '../../../global/selectors/calls'; import { selectPhoneCallUser } from '../../../global/selectors/calls';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -39,6 +39,7 @@ import styles from './PhoneCall.module.scss';
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
phoneCall?: ApiPhoneCall; phoneCall?: ApiPhoneCall;
userProfilePhoto?: ApiPhoto;
isOutgoing: boolean; isOutgoing: boolean;
isCallPanelVisible?: boolean; isCallPanelVisible?: boolean;
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
@ -46,6 +47,7 @@ type StateProps = {
const PhoneCall: FC<StateProps> = ({ const PhoneCall: FC<StateProps> = ({
user, user,
userProfilePhoto,
isOutgoing, isOutgoing,
phoneCall, phoneCall,
isCallPanelVisible, isCallPanelVisible,
@ -238,6 +240,7 @@ const PhoneCall: FC<StateProps> = ({
> >
<Avatar <Avatar
user={user} user={user}
userProfilePhoto={userProfilePhoto}
size="jumbo" size="jumbo"
className={hasVideo || hasPresentation ? styles.blurred : ''} className={hasVideo || hasPresentation ? styles.blurred : ''}
withVideo withVideo
@ -279,7 +282,7 @@ const PhoneCall: FC<StateProps> = ({
onClick={handleToggleFullscreen} onClick={handleToggleFullscreen}
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')} ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
> >
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} /> <i className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')} />
</Button> </Button>
)} )}
@ -290,7 +293,7 @@ const PhoneCall: FC<StateProps> = ({
onClick={handleClose} onClick={handleClose}
className={styles.closeButton} className={styles.closeButton}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
</div> </div>
<div <div
@ -372,10 +375,13 @@ export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const { phoneCall, currentUserId } = global; const { phoneCall, currentUserId } = global;
const { isCallPanelVisible, isMasterTab } = selectTabState(global); const { isCallPanelVisible, isMasterTab } = selectTabState(global);
const user = selectPhoneCallUser(global);
const userProfilePhoto = user ? selectUserPhotoFromFullInfo(global, user.id) : undefined;
return { return {
isCallPanelVisible: Boolean(isCallPanelVisible), isCallPanelVisible: Boolean(isCallPanelVisible),
user: selectPhoneCallUser(global), user,
userProfilePhoto,
isOutgoing: phoneCall?.adminId === currentUserId, isOutgoing: phoneCall?.adminId === currentUserId,
phoneCall: isMasterTab ? phoneCall : undefined, phoneCall: isMasterTab ? phoneCall : undefined,
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,

View File

@ -36,7 +36,7 @@ const PhoneCallButton: FC<OwnProps> = ({
onClick={onClick} onClick={onClick}
disabled={isDisabled} disabled={isDisabled}
> >
{customIcon || <i className={buildClassName(iconClassName, `icon-${icon}`)} />} {customIcon || <i className={buildClassName(iconClassName, 'icon', `icon-${icon}`)} />}
</Button> </Button>
<div className={styles.buttonText}>{label}</div> <div className={styles.buttonText}>{label}</div>
</div> </div>

View File

@ -6,7 +6,7 @@
} }
.star { .star {
cursor: pointer; cursor: var(--custom-cursor, pointer);
color: var(--color-text-secondary); color: var(--color-text-secondary);
&:not(:first-child) { &:not(:first-child) {

View File

@ -56,6 +56,7 @@ const RatePhoneCallModal: FC<OwnProps> = ({
return ( return (
<i <i
className={buildClassName( className={buildClassName(
'icon',
isFilled ? 'icon-favorite-filled' : 'icon-favorite', isFilled ? 'icon-favorite-filled' : 'icon-favorite',
isFilled && styles.isFilled, isFilled && styles.isFilled,
styles.star, styles.star,

View File

@ -48,7 +48,7 @@
height: 3rem; height: 3rem;
margin-inline-end: 0.75rem; margin-inline-end: 0.75rem;
i { .icon {
font-size: 1.625rem; font-size: 1.625rem;
&.icon-pause { &.icon-pause {
@ -57,7 +57,7 @@
} }
} }
i { .icon {
position: absolute; position: absolute;
&.icon-play { &.icon-play {
@ -94,7 +94,7 @@
border: 0.125rem solid var(--background-color); border: 0.125rem solid var(--background-color);
z-index: 1; z-index: 1;
i { .icon {
font-size: 0.8125rem; font-size: 0.8125rem;
} }
} }
@ -208,7 +208,7 @@
} }
.waveform { .waveform {
cursor: pointer; cursor: var(--custom-cursor, pointer);
margin-left: 1px; margin-left: 1px;
touch-action: none; touch-action: none;
display: flex; display: flex;
@ -273,7 +273,7 @@
height: 1.25rem; height: 1.25rem;
position: relative; position: relative;
top: 3px; top: 3px;
cursor: pointer; cursor: var(--custom-cursor, pointer);
touch-action: none; touch-action: none;
&::before { &::before {
@ -295,7 +295,7 @@
width: 100%; width: 100%;
top: 6px; top: 6px;
i { &-inner {
position: absolute; position: absolute;
width: 100%; width: 100%;
background-color: var(--color-interactive-active); background-color: var(--color-interactive-active);
@ -321,7 +321,7 @@
top: 7px; top: 7px;
left: 0; left: 0;
i { &-inner {
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@ -340,7 +340,7 @@ const Audio: FC<OwnProps> = ({
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : 'ltr'}> <div className={fullClassName} dir={lang.isRtl ? 'rtl' : 'ltr'}>
{isSelectable && ( {isSelectable && (
<div className="message-select-control"> <div className="message-select-control">
{isSelected && <i className="icon-select" />} {isSelected && <i className="icon icon-select" />}
</div> </div>
)} )}
<Button <Button
@ -354,8 +354,8 @@ const Audio: FC<OwnProps> = ({
isRtl={lang.isRtl} isRtl={lang.isRtl}
backgroundImage={coverBlobUrl} backgroundImage={coverBlobUrl}
> >
<i className="icon-play" /> <i className="icon icon-play" />
<i className="icon-pause" /> <i className="icon icon-pause" />
</Button> </Button>
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<div className={buildClassName('media-loading', spinnerClassNames, shouldRenderCross && 'interactive')}> <div className={buildClassName('media-loading', spinnerClassNames, shouldRenderCross && 'interactive')}>
@ -376,7 +376,7 @@ const Audio: FC<OwnProps> = ({
ariaLabel={isDownloading ? 'Cancel download' : 'Download'} ariaLabel={isDownloading ? 'Cancel download' : 'Download'}
onClick={handleDownloadClick} onClick={handleDownloadClick}
> >
<i className={isDownloading ? 'icon-close' : 'icon-arrow-down'} /> <i className={buildClassName('icon', isDownloading ? 'icon-close' : 'icon-arrow-down')} />
</Button> </Button>
)} )}
{origin === AudioOrigin.Search && renderWithTitle()} {origin === AudioOrigin.Search && renderWithTitle()}
@ -519,6 +519,7 @@ function renderVoice(
> >
<i className={buildClassName( <i className={buildClassName(
'transcribe-icon', 'transcribe-icon',
'icon',
(isTranscribed || isTranscriptionError) ? 'icon-down' : 'icon-transcribe', (isTranscribed || isTranscriptionError) ? 'icon-down' : 'icon-transcribe',
(isTranscribed || isTranscriptionError) && !isTranscriptionHidden && 'transcribe-shown', (isTranscribed || isTranscriptionError) && !isTranscriptionHidden && 'transcribe-shown',
)} )}
@ -606,11 +607,13 @@ function renderSeekline(
))} ))}
<span className="seekline-play-progress"> <span className="seekline-play-progress">
<i <i
className="seekline-play-progress-inner"
style={`transform: translateX(${playProgress * 100}%)`} style={`transform: translateX(${playProgress * 100}%)`}
/> />
</span> </span>
<span className="seekline-thumb"> <span className="seekline-thumb">
<i <i
className="seekline-thumb-inner"
style={`transform: translateX(${playProgress * 100}%)`} style={`transform: translateX(${playProgress * 100}%)`}
/> />
</span> </span>

View File

@ -99,7 +99,7 @@
&.size-large { &.size-large {
font-size: 1.3125rem; font-size: 1.3125rem;
i { .icon {
font-size: 1.625rem; font-size: 1.625rem;
} }
@ -148,7 +148,7 @@
} }
&.interactive { &.interactive {
cursor: pointer; cursor: var(--custom-cursor, pointer);
} }
.poster { .poster {

View File

@ -49,6 +49,7 @@ type OwnProps = {
chat?: ApiChat; chat?: ApiChat;
user?: ApiUser; user?: ApiUser;
photo?: ApiPhoto; photo?: ApiPhoto;
userProfilePhoto?: ApiPhoto;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
@ -69,6 +70,7 @@ const Avatar: FC<OwnProps> = ({
chat, chat,
user, user,
photo, photo,
userProfilePhoto,
userStatus, userStatus,
text, text,
isSavedMessages, isSavedMessages,
@ -96,8 +98,7 @@ const Avatar: FC<OwnProps> = ({
withVideo && !VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX withVideo && !VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX
&& user?.isPremium && user?.hasVideoAvatar && user?.isPremium && user?.hasVideoAvatar
); );
const profilePhoto = user?.fullInfo?.personalPhoto || user?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto; const hasProfileVideo = userProfilePhoto?.isVideo;
const hasProfileVideo = profilePhoto?.isVideo;
const isIntersectingForVideo = useIsIntersecting( const isIntersectingForVideo = useIsIntersecting(
ref, canShowVideo && hasProfileVideo ? observeIntersection : undefined, ref, canShowVideo && hasProfileVideo ? observeIntersection : undefined,
); );
@ -117,7 +118,7 @@ const Avatar: FC<OwnProps> = ({
} }
if (hasProfileVideo) { if (hasProfileVideo) {
videoHash = getChatAvatarHash(user!, undefined, 'video'); videoHash = getChatAvatarHash(user!, undefined, 'video', undefined, userProfilePhoto);
} }
} }
@ -149,10 +150,10 @@ const Avatar: FC<OwnProps> = ({
const userId = user?.id; const userId = user?.id;
useEffect(() => { useEffect(() => {
if (userId && canShowVideo && !profilePhoto) { if (userId && canShowVideo && !userProfilePhoto) {
loadFullUser({ userId }); loadFullUser({ userId });
} }
}, [loadFullUser, profilePhoto, userId, canShowVideo]); }, [loadFullUser, userProfilePhoto, userId, canShowVideo]);
const lang = useLang(); const lang = useLang();
@ -160,11 +161,35 @@ const Avatar: FC<OwnProps> = ({
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text); const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
if (isSavedMessages) { if (isSavedMessages) {
content = <i className={buildClassName(cn.icon, 'icon-avatar-saved-messages')} role="img" aria-label={author} />; content = (
<i
className={buildClassName(cn.icon,
'icon',
'icon-avatar-saved-messages')}
role="img"
aria-label={author}
/>
);
} else if (isDeleted) { } else if (isDeleted) {
content = <i className={buildClassName(cn.icon, 'icon-avatar-deleted-account')} role="img" aria-label={author} />; content = (
<i
className={buildClassName(cn.icon,
'icon',
'icon-avatar-deleted-account')}
role="img"
aria-label={author}
/>
);
} else if (isReplies) { } else if (isReplies) {
content = <i className={buildClassName(cn.icon, 'icon-reply-filled')} role="img" aria-label={author} />; content = (
<i
className={buildClassName(cn.icon,
'icon',
'icon-reply-filled')}
role="img"
aria-label={author}
/>
);
} else if (hasBlobUrl) { } else if (hasBlobUrl) {
content = ( content = (
<> <>

View File

@ -80,7 +80,7 @@
} }
&.clickable { &.clickable {
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover { &:hover {
background-color: var(--color-interactive-element-hover); background-color: var(--color-interactive-element-hover);

View File

@ -249,7 +249,7 @@ const CalendarModal: FC<OwnProps> = ({
color="translucent" color="translucent"
onClick={onClose} onClick={onClose}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<h4> <h4>
@ -265,7 +265,7 @@ const CalendarModal: FC<OwnProps> = ({
disabled={shouldDisablePrevMonth} disabled={shouldDisablePrevMonth}
onClick={!shouldDisablePrevMonth ? handlePrevMonth : undefined} onClick={!shouldDisablePrevMonth ? handlePrevMonth : undefined}
> >
<i className="icon-previous" /> <i className="icon icon-previous" />
</Button> </Button>
<Button <Button
@ -275,7 +275,7 @@ const CalendarModal: FC<OwnProps> = ({
disabled={shouldDisableNextMonth} disabled={shouldDisableNextMonth}
onClick={!shouldDisableNextMonth ? handleNextMonth : undefined} onClick={!shouldDisableNextMonth ? handleNextMonth : undefined}
> >
<i className="icon-next" /> <i className="icon icon-next" />
</Button> </Button>
</div> </div>
</div> </div>
@ -297,6 +297,7 @@ const CalendarModal: FC<OwnProps> = ({
onClick={() => handleDateSelect(gridDate)} onClick={() => handleDateSelect(gridDate)}
className={buildClassName( className={buildClassName(
'day-button', 'day-button',
'div-button',
isDisabledDay( isDisabledDay(
currentYear, currentMonth, gridDate, minDate, maxDate, currentYear, currentMonth, gridDate, minDate, maxDate,
) )

View File

@ -12,10 +12,15 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { TME_LINK_PREFIX } from '../../config'; import { TME_LINK_PREFIX } from '../../config';
import { import {
selectChat, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings, selectUser, selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectNotifyExceptions,
selectNotifySettings,
selectUser,
selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import { import {
getChatDescription,
getChatLink, getChatLink,
getTopicLink, getTopicLink,
getHasAdminRight, getHasAdminRight,
@ -47,6 +52,8 @@ type StateProps =
isMuted?: boolean; isMuted?: boolean;
phoneCodeList: ApiCountryCode[]; phoneCodeList: ApiCountryCode[];
topicId?: number; topicId?: number;
description?: string;
chatInviteLink?: string;
} }
& Pick<GlobalState, 'lastSyncTime'>; & Pick<GlobalState, 'lastSyncTime'>;
@ -61,6 +68,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
isMuted, isMuted,
phoneCodeList, phoneCodeList,
topicId, topicId,
description,
chatInviteLink,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -71,7 +80,6 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
const { const {
id: userId, id: userId,
fullInfo,
usernames, usernames,
phoneNumber, phoneNumber,
isSelf, isSelf,
@ -108,8 +116,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
return isTopicInfo return isTopicInfo
? getTopicLink(chat.id, activeChatUsernames?.[0].username, topicId) ? getTopicLink(chat.id, activeChatUsernames?.[0].username, topicId)
: getChatLink(chat); : getChatLink(chat) || chatInviteLink;
}, [chat, isTopicInfo, activeChatUsernames, topicId]); }, [chat, isTopicInfo, activeChatUsernames, topicId, chatInviteLink]);
const handleNotificationChange = useCallback(() => { const handleNotificationChange = useCallback(() => {
setAreNotificationsEnabled((current) => { setAreNotificationsEnabled((current) => {
@ -141,7 +149,6 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
} }
const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber); const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber);
const description = (fullInfo?.bio) || getChatDescription(chat);
function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) { function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) {
const [mainUsername, ...otherUsernames] = usernameList; const [mainUsername, ...otherUsernames] = usernameList;
@ -259,6 +266,11 @@ export default memo(withGlobal<OwnProps>(
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
const { threadId } = selectCurrentMessageList(global) || {}; const { threadId } = selectCurrentMessageList(global) || {};
const topicId = isForum ? threadId : undefined; const topicId = isForum ? threadId : undefined;
const chatInviteLink = chat ? selectChatFullInfo(global, chat.id)?.inviteLink : undefined;
let description = user ? selectUserFullInfo(global, user.id)?.bio : undefined;
if (!description && chat) {
description = selectChatFullInfo(global, chat.id)?.about;
}
const canInviteUsers = chat && !user && ( const canInviteUsers = chat && !user && (
(!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers')) (!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers'))
@ -273,6 +285,8 @@ export default memo(withGlobal<OwnProps>(
canInviteUsers, canInviteUsers,
isMuted, isMuted,
topicId, topicId,
chatInviteLink,
description,
}; };
}, },
)(ChatExtra)); )(ChatExtra));

View File

@ -158,7 +158,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
<> <>
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}> <div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
<Button round color="translucent" size="smaller" ariaLabel={lang('Back')} onClick={handleHeaderBackClick}> <Button round color="translucent" size="smaller" ariaLabel={lang('Back')} onClick={handleHeaderBackClick}>
<i className="icon-arrow-left" /> <i className="icon icon-arrow-left" />
</Button> </Button>
<InputText <InputText
ref={topicSearchRef} ref={topicSearchRef}
@ -211,7 +211,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
ariaLabel={lang('Close')} ariaLabel={lang('Close')}
onClick={onClose} onClick={onClose}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<InputText <InputText
ref={searchRef} ref={searchRef}
@ -266,7 +266,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
onClose={onClose} onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd} onCloseAnimationEnd={onCloseAnimationEnd}
> >
<Transition activeKey={activeKey} name="slide-fade"> <Transition activeKey={activeKey} name="slideFade">
{() => { {() => {
return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList(); return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList();
}} }}

View File

@ -320,7 +320,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
onClick={() => selectStickerSet(index)} onClick={() => selectStickerSet(index)}
> >
{(stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID) ? ( {(stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID) ? (
<i className="icon-recent" /> <i className="icon icon-recent" />
) : ( ) : (
<StickerSetCover <StickerSetCover
stickerSet={stickerSet as ApiStickerSet} stickerSet={stickerSet as ApiStickerSet}

View File

@ -8,7 +8,7 @@
border-radius: var(--border-radius-messages-small); border-radius: var(--border-radius-messages-small);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: var(--custom-cursor, pointer);
direction: ltr; direction: ltr;
@for $i from 1 through 8 { @for $i from 1 through 8 {

View File

@ -115,7 +115,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
</p> </p>
<div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div> <div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div>
</div> </div>
{hasContextMenu && <i className="embedded-more icon-more" />} {hasContextMenu && <i className="embedded-more icon icon-more" />}
</div> </div>
); );
}; };

View File

@ -97,7 +97,7 @@
} }
&.interactive .file-icon-container { &.interactive .file-icon-container {
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover { &:hover {
.file-icon::after { .file-icon::after {

View File

@ -95,7 +95,7 @@ const File: FC<OwnProps> = ({
<div ref={elementRef} className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}> <div ref={elementRef} className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
{isSelectable && ( {isSelectable && (
<div className="message-select-control"> <div className="message-select-control">
{isSelected && <i className="icon-select" />} {isSelected && <i className="icon icon-select" />}
</div> </div>
)} )}
<div className="file-icon-container" onClick={isUploading ? undefined : onClick}> <div className="file-icon-container" onClick={isUploading ? undefined : onClick}>
@ -135,6 +135,7 @@ const File: FC<OwnProps> = ({
<i <i
className={buildClassName( className={buildClassName(
'action-icon', 'action-icon',
'icon',
actionIcon || 'icon-download', actionIcon || 'icon-download',
shouldSpinnerRender && 'hidden', shouldSpinnerRender && 'hidden',
)} )}

View File

@ -25,7 +25,7 @@
} }
&.interactive { &.interactive {
cursor: pointer; cursor: var(--custom-cursor, pointer);
} }
.thumbnail { .thumbnail {

View File

@ -154,7 +154,7 @@ const GifButton: FC<OwnProps> = ({
pill pill
onClick={handleUnsaveClick} onClick={handleUnsaveClick}
> >
<i className="icon-close gif-unsave-button-icon" /> <i className="icon icon-close gif-unsave-button-icon" />
</Button> </Button>
)} )}
{withThumb && ( {withThumb && (

View File

@ -165,7 +165,7 @@ const ManageUsernames: FC<OwnProps> = ({
> >
<ListItem <ListItem
key={usernameData.username} key={usernameData.username}
className={buildClassName('mb-2 no-icon', styles.item)} className={buildClassName('drag-item mb-2 no-icon', styles.item)}
narrow narrow
secondaryIcon="more" secondaryIcon="more"
icon={usernameData.isActive ? 'link' : 'link-broken'} icon={usernameData.isActive ? 'link' : 'link-broken'}

View File

@ -3,7 +3,7 @@
padding-bottom: 100%; padding-bottom: 100%;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
cursor: pointer; cursor: var(--custom-cursor, pointer);
.video-duration { .video-duration {
position: absolute; position: absolute;

View File

@ -6,7 +6,7 @@
line-height: 1; line-height: 1;
font-size: 1.1875rem; font-size: 1.1875rem;
i { .icon {
background: var(--background-color); background: var(--background-color);
} }

View File

@ -21,9 +21,9 @@ const MessageOutgoingStatus: FC<OwnProps> = ({ status }) => {
<Transition name="reveal" activeKey={Keys[status]}> <Transition name="reveal" activeKey={Keys[status]}>
{status === 'failed' ? ( {status === 'failed' ? (
<div className="MessageOutgoingStatus--failed"> <div className="MessageOutgoingStatus--failed">
<i className="icon-message-failed" /> <i className="icon icon-message-failed" />
</div> </div>
) : <i className={`icon-message-${status}`} />} ) : <i className={`icon icon-message-${status}`} />}
</Transition> </Transition>
</div> </div>
); );

View File

@ -141,13 +141,13 @@ const PasswordForm: FC<OwnProps> = ({
/> />
<label>{error || hint || placeholder}</label> <label>{error || hint || placeholder}</label>
<div <div
className="toggle-password" className="div-button toggle-password"
onClick={togglePasswordVisibility} onClick={togglePasswordVisibility}
role="button" role="button"
tabIndex={0} tabIndex={0}
title="Toggle password visibility" title="Toggle password visibility"
> >
<i className={isPasswordVisible ? 'icon-eye' : 'icon-eye-closed'} /> <i className={buildClassName('icon', isPasswordVisible ? 'icon-eye' : 'icon-eye-closed')} />
</div> </div>
</div> </div>
{description && <p className="description">{description}</p>} {description && <p className="description">{description}</p>}

View File

@ -8,7 +8,7 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
padding-right: 1rem; padding-right: 1rem;
border-radius: 1rem; border-radius: 1rem;
cursor: pointer; cursor: var(--custom-cursor, pointer);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
flex-shrink: 1; flex-shrink: 1;
@ -68,7 +68,7 @@
flex-shrink: 0; flex-shrink: 0;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
.Avatar__icon, i { .icon {
font-size: 1rem; font-size: 1rem;
} }
} }
@ -82,7 +82,7 @@
background-color: var(--color-primary); background-color: var(--color-primary);
color: white; color: white;
i { .icon {
font-size: 1.25rem; font-size: 1.25rem;
position: relative; position: relative;
top: -1px; top: -1px;

View File

@ -2,9 +2,9 @@ import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global'; import { withGlobal } from '../../global';
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC, TeactNode } from '../../lib/teact/teact';
import type { ApiChat, ApiUser } from '../../api/types'; import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
import { selectChat, selectUser } from '../../global/selectors'; import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../global/selectors';
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers'; import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -28,6 +28,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
user?: ApiUser; user?: ApiUser;
userProfilePhoto?: ApiPhoto;
currentUserId?: string; currentUserId?: string;
}; };
@ -39,6 +40,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
clickArg, clickArg,
chat, chat,
user, user,
userProfilePhoto,
className, className,
currentUserId, currentUserId,
onClick, onClick,
@ -51,7 +53,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
if (icon && title) { if (icon && title) {
iconElement = ( iconElement = (
<div className="item-icon"> <div className="item-icon">
<i className={`icon-${icon}`} /> <i className={buildClassName('icon', `icon-${icon}`)} />
</div> </div>
); );
@ -61,6 +63,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
chat={chat} chat={chat}
user={user} user={user}
userProfilePhoto={userProfilePhoto}
size="small" size="small"
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
/> />
@ -96,7 +99,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
)} )}
{canClose && ( {canClose && (
<div className="item-remove"> <div className="item-remove">
<i className="icon-close" /> <i className="icon icon-close" />
</div> </div>
)} )}
</div> </div>
@ -111,10 +114,12 @@ export default memo(withGlobal<OwnProps>(
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
const userProfilePhoto = user ? selectUserPhotoFromFullInfo(global, user.id) : undefined;
return { return {
chat, chat,
user, user,
userProfilePhoto,
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
}; };
}, },

View File

@ -17,7 +17,7 @@
} }
&.clickable { &.clickable {
cursor: pointer; cursor: var(--custom-cursor, pointer);
pointer-events: auto; pointer-events: auto;
} }
} }

View File

@ -5,13 +5,15 @@ import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import type { import type {
ApiUser, ApiTypingStatus, ApiUserStatus, ApiChatMember, ApiUser, ApiTypingStatus, ApiUserStatus, ApiChatMember, ApiPhoto,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { AnimationLevel } from '../../types'; import type { AnimationLevel } from '../../types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors'; import {
selectChatMessages, selectUser, selectUserPhotoFromFullInfo, selectUserStatus,
} from '../../global/selectors';
import { getMainUsername, getUserStatus, isUserOnline } from '../../global/helpers'; import { getMainUsername, getUserStatus, isUserOnline } from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -45,6 +47,7 @@ type StateProps =
{ {
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
userProfilePhoto?: ApiPhoto;
isSavedMessages?: boolean; isSavedMessages?: boolean;
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
areMessagesLoaded: boolean; areMessagesLoaded: boolean;
@ -66,6 +69,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
noRtl, noRtl,
user, user,
userStatus, userStatus,
userProfilePhoto,
isSavedMessages, isSavedMessages,
areMessagesLoaded, areMessagesLoaded,
animationLevel, animationLevel,
@ -171,6 +175,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
key={user.id} key={user.id}
size={avatarSize} size={avatarSize}
user={user} user={user}
userProfilePhoto={userProfilePhoto}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
withVideo={withVideoAvatar} withVideo={withVideoAvatar}
@ -191,11 +196,13 @@ export default memo(withGlobal<OwnProps>(
const userStatus = selectUserStatus(global, userId); const userStatus = selectUserStatus(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); const areMessagesLoaded = Boolean(selectChatMessages(global, userId));
const userProfilePhoto = user ? selectUserPhotoFromFullInfo(global, user.id) : undefined;
return { return {
lastSyncTime, lastSyncTime,
user, user,
userStatus, userStatus,
userProfilePhoto,
isSavedMessages, isSavedMessages,
areMessagesLoaded, areMessagesLoaded,
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,

View File

@ -35,7 +35,7 @@
font-size: 0.75rem; font-size: 0.75rem;
color: var(--color-white); color: var(--color-white);
opacity: 0.5; opacity: 0.5;
cursor: pointer; cursor: var(--custom-cursor, pointer);
user-select: none; user-select: none;
align-items: center; align-items: center;
height: 1.5rem; height: 1.5rem;
@ -112,7 +112,7 @@
opacity: 0.25; opacity: 0.25;
transition: opacity 0.15s; transition: opacity 0.15s;
outline: none !important; outline: none !important;
cursor: pointer; cursor: var(--custom-cursor, pointer);
z-index: 1; z-index: 1;
&:global(:hover), &:global(:hover),

View File

@ -46,6 +46,6 @@
color: var(--color-white); color: var(--color-white);
pointer-events: auto; pointer-events: auto;
cursor: pointer; cursor: var(--custom-cursor, pointer);
} }
} }

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiUser, ApiChat, ApiUserStatus, ApiTopic, ApiUser, ApiChat, ApiUserStatus, ApiTopic, ApiPhoto,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { AnimationLevel } from '../../types'; import type { AnimationLevel } from '../../types';
@ -14,10 +14,18 @@ import { MediaViewerOrigin } from '../../types';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { import {
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectTabState, selectTabState,
selectChat, selectCurrentMessageList, selectThreadMessagesCount, selectUser, selectUserStatus, selectThreadMessagesCount,
selectUser,
selectUserFullInfo,
selectUserStatus,
} from '../../global/selectors'; } from '../../global/selectors';
import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers'; import {
getUserStatus, isChatChannel, isUserId, isUserOnline,
} from '../../global/helpers';
import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -52,6 +60,10 @@ type StateProps =
avatarOwnerId?: string; avatarOwnerId?: string;
topic?: ApiTopic; topic?: ApiTopic;
messagesCount?: number; messagesCount?: number;
userPersonalPhoto?: ApiPhoto;
userProfilePhoto?: ApiPhoto;
userFallbackPhoto?: ApiPhoto;
chatProfilePhoto?: ApiPhoto;
} }
& Pick<GlobalState, 'connectionState'>; & Pick<GlobalState, 'connectionState'>;
@ -71,6 +83,10 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
avatarOwnerId, avatarOwnerId,
topic, topic,
messagesCount, messagesCount,
userPersonalPhoto,
userProfilePhoto,
userFallbackPhoto,
chatProfilePhoto,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -87,7 +103,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const prevAvatarOwnerId = usePrevious(avatarOwnerId); const prevAvatarOwnerId = usePrevious(avatarOwnerId);
const [hasSlideAnimation, setHasSlideAnimation] = useState(true); const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
const slideAnimation = hasSlideAnimation const slideAnimation = hasSlideAnimation
? animationLevel >= 1 ? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized') : 'none' ? animationLevel >= 1 ? (lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized') : 'none'
: 'none'; : 'none';
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
@ -216,12 +232,14 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const photo = !isSavedMessages && photos.length > 0 const photo = !isSavedMessages && photos.length > 0
? photos[currentPhotoIndex] ? photos[currentPhotoIndex]
: undefined; : undefined;
const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto;
return ( return (
<ProfilePhoto <ProfilePhoto
key={currentPhotoIndex} key={currentPhotoIndex}
user={user} user={user}
chat={chat} chat={chat}
photo={photo} photo={profilePhoto}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
canPlayVideo={Boolean(isActive && canPlayVideo)} canPlayVideo={Boolean(isActive && canPlayVideo)}
onClick={handleProfilePhotoClick} onClick={handleProfilePhotoClick}
@ -259,18 +277,18 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
> >
<div className={styles.photoWrapper}> <div className={styles.photoWrapper}>
{renderPhotoTabs()} {renderPhotoTabs()}
{!forceShowSelf && user?.fullInfo?.personalPhoto && ( {!forceShowSelf && userPersonalPhoto && (
<div className={buildClassName( <div className={buildClassName(
styles.fallbackPhoto, styles.fallbackPhoto,
isFirst && styles.fallbackPhotoVisible, isFirst && styles.fallbackPhotoVisible,
)} )}
> >
<div className={styles.fallbackPhotoContents}> <div className={styles.fallbackPhotoContents}>
{lang(user.fullInfo.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')} {lang(userPersonalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
</div> </div>
</div> </div>
)} )}
{forceShowSelf && user?.fullInfo?.fallbackPhoto && ( {forceShowSelf && userFallbackPhoto && (
<div className={buildClassName( <div className={buildClassName(
styles.fallbackPhoto, styles.fallbackPhoto,
(isFirst || isLast) && styles.fallbackPhotoVisible, (isFirst || isLast) && styles.fallbackPhotoVisible,
@ -279,12 +297,12 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
<div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}> <div className={styles.fallbackPhotoContents} onClick={handleSelectFallbackPhoto}>
{!isLast && ( {!isLast && (
<Avatar <Avatar
photo={user.fullInfo.fallbackPhoto} photo={userFallbackPhoto}
className={styles.fallbackPhotoAvatar} className={styles.fallbackPhotoAvatar}
size="mini" size="mini"
/> />
)} )}
{lang(user.fullInfo.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')} {lang(userFallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
</div> </div>
</div> </div>
)} )}
@ -333,6 +351,7 @@ export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { connectionState } = global; const { connectionState } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const isPrivate = isUserId(userId);
const userStatus = selectUserStatus(global, userId); const userStatus = selectUserStatus(global, userId);
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
@ -341,12 +360,18 @@ export default memo(withGlobal<OwnProps>(
const isForum = chat?.isForum; const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;
const userFullInfo = isPrivate ? selectUserFullInfo(global, userId) : undefined;
const chatFullInfo = !isPrivate ? selectChatFullInfo(global, userId) : undefined;
return { return {
connectionState, connectionState,
user, user,
userStatus, userStatus,
chat, chat,
userPersonalPhoto: userFullInfo?.personalPhoto,
userProfilePhoto: userFullInfo?.profilePhoto,
userFallbackPhoto: userFullInfo?.fallbackPhoto,
chatProfilePhoto: chatFullInfo?.profilePhoto,
isSavedMessages, isSavedMessages,
animationLevel, animationLevel,
mediaId, mediaId,

View File

@ -1,7 +1,7 @@
.ProfilePhoto { .ProfilePhoto {
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: pointer; cursor: var(--custom-cursor, pointer);
position: relative; position: relative;
.avatar-media { .avatar-media {
@ -24,7 +24,7 @@
justify-content: center; justify-content: center;
color: var(--color-white); color: var(--color-white);
background: linear-gradient(var(--color-white) -125%, var(--color-user)); background: linear-gradient(var(--color-white) -125%, var(--color-user));
cursor: default; cursor: var(--custom-cursor, default);
} }
&.no-photo { &.no-photo {

View File

@ -56,31 +56,27 @@ 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
|| user?.fullInfo?.personalPhoto
|| userOrChat?.fullInfo?.profilePhoto
|| user?.fullInfo?.fallbackPhoto;
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat; const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
const { isVideo } = currentPhoto || {}; const { isVideo } = photo || {};
const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal', 'photo'); const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal', 'photo');
const avatarBlobUrl = useMedia(avatarHash, undefined, undefined, lastSyncTime); const avatarBlobUrl = useMedia(avatarHash, undefined, undefined, lastSyncTime);
const photoHash = canHaveMedia && currentPhoto && !isVideo && `photo${currentPhoto.id}?size=c`; const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`;
const photoBlobUrl = useMedia(photoHash, undefined, undefined, lastSyncTime); const photoBlobUrl = useMedia(photoHash, undefined, undefined, lastSyncTime);
const videoHash = canHaveMedia && currentPhoto && isVideo && getVideoAvatarMediaHash(currentPhoto); const videoHash = canHaveMedia && photo && isVideo && getVideoAvatarMediaHash(photo);
const videoBlobUrl = useMedia(videoHash, undefined, undefined, lastSyncTime); const videoBlobUrl = useMedia(videoHash, undefined, undefined, lastSyncTime);
const fullMediaData = videoBlobUrl || photoBlobUrl; const fullMediaData = videoBlobUrl || photoBlobUrl;
const [isVideoReady, markVideoReady] = useFlag(); const [isVideoReady, markVideoReady] = useFlag();
const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady)); const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady));
const transitionClassNames = useMediaTransition(isFullMediaReady); const transitionClassNames = useMediaTransition(isFullMediaReady);
const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && currentPhoto?.thumbnail?.dataUri; const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && photo?.thumbnail?.dataUri;
const blurredThumbCanvasRef = useCanvasBlur( const blurredThumbCanvasRef = useCanvasBlur(
currentPhoto?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED, photo?.thumbnail?.dataUri, !isBlurredThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED,
); );
const hasMedia = currentPhoto || avatarBlobUrl || isBlurredThumb; const hasMedia = photo || avatarBlobUrl || isBlurredThumb;
useEffect(() => { useEffect(() => {
if (videoRef.current && !canPlayVideo) { if (videoRef.current && !canPlayVideo) {
@ -91,11 +87,11 @@ const ProfilePhoto: FC<OwnProps> = ({
let content: TeactNode | undefined; let content: TeactNode | undefined;
if (isSavedMessages) { if (isSavedMessages) {
content = <i className="icon-avatar-saved-messages" />; content = <i className="icon icon-avatar-saved-messages" />;
} else if (isDeleted) { } else if (isDeleted) {
content = <i className="icon-avatar-deleted-account" />; content = <i className="icon icon-avatar-deleted-account" />;
} else if (isRepliesChat) { } else if (isRepliesChat) {
content = <i className="icon-reply-filled" />; content = <i className="icon icon-reply-filled" />;
} else if (hasMedia) { } else if (hasMedia) {
content = ( content = (
<> <>
@ -104,7 +100,7 @@ const ProfilePhoto: FC<OwnProps> = ({
) : ( ) : (
<img src={avatarBlobUrl} className="thumb" alt="" /> <img src={avatarBlobUrl} className="thumb" alt="" />
)} )}
{currentPhoto && ( {photo && (
isVideo ? ( isVideo ? (
<OptimizedVideo <OptimizedVideo
canPlay={canPlayVideo} canPlay={canPlayVideo}

View File

@ -1,7 +1,7 @@
.root { .root {
--custom-emoji-size: 2.5rem; --custom-emoji-size: 2.5rem;
cursor: pointer; cursor: var(--custom-cursor, pointer);
display: inline-block; display: inline-block;
width: var(--custom-emoji-size); width: var(--custom-emoji-size);
height: var(--custom-emoji-size); height: var(--custom-emoji-size);

View File

@ -32,7 +32,7 @@
.dot { .dot {
flex: none; flex: none;
cursor: pointer; cursor: var(--custom-cursor, pointer);
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #DFE1E5; background: #DFE1E5;

View File

@ -45,7 +45,7 @@ const SliderDots: FC<OwnProps> = ({
round round
onClick={handleGoBack} onClick={handleGoBack}
> >
<i className="icon-previous" /> <i className="icon icon-previous" />
</Button> </Button>
)} )}
<div className={styles.root} style={`--start-from: ${startFrom}; --length: ${length}; --count: ${count};`}> <div className={styles.root} style={`--start-from: ${startFrom}; --length: ${length}; --count: ${count};`}>
@ -80,7 +80,7 @@ const SliderDots: FC<OwnProps> = ({
disabled={active === length - 1} disabled={active === length - 1}
onClick={handleGoForward} onClick={handleGoForward}
> >
<i className="icon-next" /> <i className="icon icon-next" />
</Button> </Button>
)} )}
</div> </div>

View File

@ -70,7 +70,7 @@
} }
&.interactive { &.interactive {
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover { &:hover {
background-color: var(--color-interactive-element-hover); background-color: var(--color-interactive-element-hover);
@ -124,7 +124,7 @@
height: 1.25rem; height: 1.25rem;
padding: 0.125rem; padding: 0.125rem;
i { .icon {
font-size: 1rem; font-size: 1rem;
} }
@ -134,7 +134,7 @@
.sticker-context-menu { .sticker-context-menu {
position: absolute; position: absolute;
cursor: default; cursor: var(--custom-cursor, default);
z-index: var(--z-header-menu); z-index: var(--z-header-menu);
.bubble { .bubble {

View File

@ -310,12 +310,12 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
<div <div
className="sticker-locked" className="sticker-locked"
> >
<i className="icon-lock-badge" /> <i className="icon icon-lock-badge" />
</div> </div>
)} )}
{!noShowPremium && isPremium && !isLocked && ( {!noShowPremium && isPremium && !isLocked && (
<div className="sticker-premium"> <div className="sticker-premium">
<i className="icon-premium" /> <i className="icon icon-premium" />
</div> </div>
)} )}
{shouldShowCloseButton && ( {shouldShowCloseButton && (
@ -325,7 +325,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
round round
onClick={handleRemoveClick} onClick={handleRemoveClick}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
)} )}
{Boolean(contextMenuItems.length) && ( {Boolean(contextMenuItems.length) && (

View File

@ -244,11 +244,11 @@ const StickerSet: FC<OwnProps> = ({
{!shouldHideHeader && ( {!shouldHideHeader && (
<div className="symbol-set-header"> <div className="symbol-set-header">
<p className="symbol-set-name"> <p className="symbol-set-name">
{isLocked && <i className="symbol-set-locked-icon icon-lock-badge" />} {isLocked && <i className="symbol-set-locked-icon icon icon-lock-badge" />}
{stickerSet.title} {stickerSet.title}
</p> </p>
{isRecent && ( {isRecent && (
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} /> <i className="symbol-set-remove icon icon-close" onClick={openConfirmModal} />
)} )}
{!isRecent && isEmoji && !isInstalled && !isPopular && ( {!isRecent && isEmoji && !isInstalled && !isPopular && (
<Button <Button
@ -291,7 +291,7 @@ const StickerSet: FC<OwnProps> = ({
onClick={handleDefaultStatusIconClick} onClick={handleDefaultStatusIconClick}
key="default-status-icon" key="default-status-icon"
> >
<i className="icon-premium" /> <i className="icon icon-premium" />
</Button> </Button>
)} )}
{shouldRender && stickerSet.reactions?.map((reaction) => { {shouldRender && stickerSet.reactions?.map((reaction) => {

View File

@ -161,7 +161,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
onClick={onTrigger} onClick={onTrigger}
ariaLabel="More actions" ariaLabel="More actions"
> >
<i className="icon-more" /> <i className="icon icon-more" />
</Button> </Button>
); );
}, [isMobile]); }, [isMobile]);
@ -170,7 +170,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
return ( return (
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}> <div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}> <Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}>
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<div className="modal-title"> <div className="modal-title">
{renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')} {renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}

View File

@ -10,7 +10,7 @@
background-color: var(--background-color); background-color: var(--background-color);
border-radius: var(--border-radius-messages); border-radius: var(--border-radius-messages);
color: var(--topic-button-accent-color); color: var(--topic-button-accent-color);
cursor: pointer; cursor: var(--custom-cursor, pointer);
&::before { &::before {
content: ""; content: "";

View File

@ -39,8 +39,8 @@ const TopicChip: FC<OwnProps> = ({
? <TopicIcon topic={topic} size={TOPIC_ICON_SIZE} /> ? <TopicIcon topic={topic} size={TOPIC_ICON_SIZE} />
: <img src={blankSrc} alt="" />} : <img src={blankSrc} alt="" />}
{topic?.title ? renderText(topic.title) : lang('Loading')} {topic?.title ? renderText(topic.title) : lang('Loading')}
{topic?.isClosed && <i className="icon-lock" />} {topic?.isClosed && <i className="icon icon-lock" />}
<i className="icon-next" /> <i className="icon icon-next" />
</div> </div>
); );
}; };

View File

@ -31,7 +31,10 @@ const TopicDefaultIcon: FC<OwnProps> = ({
if (topicId === GENERAL_TOPIC_ID) { if (topicId === GENERAL_TOPIC_ID) {
return ( return (
<i className={buildClassName(styles.root, className, 'icon-hashtag', 'general-forum-icon')} onClick={onClick} /> <i
className={buildClassName(styles.root, className, 'icon', 'icon-hashtag', 'general-forum-icon')}
onClick={onClick}
/>
); );
} }
return ( return (

View File

@ -0,0 +1,51 @@
import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiPhoto, ApiUser } from '../../api/types';
import type { AnimationLevel } from '../../types';
import { selectUserPhotoFromFullInfo } from '../../global/selectors';
import Avatar from './Avatar';
type OwnProps = {
user?: ApiUser;
withVideo?: boolean;
className?: string;
size?: 'micro' | 'tiny' | 'small' | 'medium' | 'large';
};
interface StateProps {
profilePhoto?: ApiPhoto;
animationLevel?: AnimationLevel;
}
const UserAvatar: FC<OwnProps & StateProps> = ({
user,
profilePhoto,
className,
animationLevel,
withVideo,
size,
}) => {
return (
<Avatar
user={user}
className={className}
userProfilePhoto={profilePhoto}
animationLevel={animationLevel}
withVideo={withVideo}
size={size}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { user }): StateProps => {
return {
profilePhoto: user ? selectUserPhotoFromFullInfo(global, user.id) : undefined,
animationLevel: global.settings.byKey.animationLevel,
};
},
)(UserAvatar));

View File

@ -33,7 +33,7 @@
top: 0; top: 0;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
cursor: default !important; cursor: var(--custom-cursor, default) !important;
padding-bottom: unset !important; padding-bottom: unset !important;
border-radius: var(--border-radius-messages-small) !important; border-radius: var(--border-radius-messages-small) !important;
} }

View File

@ -16,7 +16,7 @@
border-radius: 0.125rem; border-radius: 0.125rem;
margin: 0.125rem; margin: 0.125rem;
transition: background-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out;
cursor: pointer; cursor: var(--custom-cursor, pointer);
&:hover, &.wrapOn { &:hover, &.wrapOn {
background-color: var(--color-background-compact-menu-hover); background-color: var(--color-background-compact-menu-hover);

View File

@ -63,12 +63,12 @@ const CodeOverlay: FC<OwnProps> = ({
<div className={contentClass}> <div className={contentClass}>
{withWordWrapButton && ( {withWordWrapButton && (
<div className={wrapClass} onClick={handleWordWrapClick} title="Word Wrap"> <div className={wrapClass} onClick={handleWordWrapClick} title="Word Wrap">
<i className="icon-word-wrap" /> <i className="icon icon-word-wrap" />
</div> </div>
)} )}
{!noCopy && ( {!noCopy && (
<div className={styles.copy} onClick={handleCopy} title={lang('Copy')}> <div className={styles.copy} onClick={handleCopy} title={lang('Copy')}>
<i className="icon-copy" /> <i className="icon icon-copy" />
</div> </div>
)} )}
</div> </div>

View File

@ -1,6 +1,6 @@
.Spoiler { .Spoiler {
&--concealed { &--concealed {
cursor: pointer; cursor: var(--custom-cursor, pointer);
background-image: url('../../../assets/spoiler-dots-black.png'); background-image: url('../../../assets/spoiler-dots-black.png');
background-size: auto min(100%, 1.125rem); background-size: auto min(100%, 1.125rem);
border-radius: 0.5rem; border-radius: 0.5rem;

View File

@ -87,7 +87,7 @@ const ArchivedChats: FC<OwnProps> = ({
)} )}
onTransitionEnd={handleDropdownMenuTransitionEnd} onTransitionEnd={handleDropdownMenuTransitionEnd}
> >
<i className="icon-arrow-left" /> <i className="icon icon-arrow-left" />
</Button> </Button>
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>} {shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
{archiveSettings.isHidden && ( {archiveSettings.isHidden && (

View File

@ -28,7 +28,7 @@
border-radius: var(--border-radius-default); border-radius: var(--border-radius-default);
&.interactive { &.interactive {
cursor: pointer; cursor: var(--custom-cursor, pointer);
} }
> .Spinner { > .Spinner {
@ -57,7 +57,7 @@
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
min-width: 0; min-width: 0;
> .Transition__slide { > .Transition_slide {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;

View File

@ -27,7 +27,7 @@ const ConnectionStatusOverlay: FC<OwnProps> = ({
<div id="ConnectionStatusOverlay" dir={lang.isRtl ? 'rtl' : undefined} onClick={onClick}> <div id="ConnectionStatusOverlay" dir={lang.isRtl ? 'rtl' : undefined} onClick={onClick}>
<Spinner color="black" /> <Spinner color="black" />
<div className="state-text"> <div className="state-text">
<Transition activeKey={connectionStatus} name="slide-fade"> <Transition activeKey={connectionStatus} name="slideFade">
{connectionStatusText} {connectionStatusText}
</Transition> </Transition>
</div> </div>
@ -36,7 +36,7 @@ const ConnectionStatusOverlay: FC<OwnProps> = ({
size="tiny" size="tiny"
color="translucent-black" color="translucent-black"
> >
<span className="icon-close" /> <span className="icon icon-close" />
</Button> </Button>
</div> </div>
); );

View File

@ -67,8 +67,8 @@ const NewChatButton: FC<OwnProps> = ({
ariaLabel={lang(isMenuOpen ? 'Close' : 'NewMessageTitle')} ariaLabel={lang(isMenuOpen ? 'Close' : 'NewMessageTitle')}
tabIndex={-1} tabIndex={-1}
> >
<i className="icon-new-chat-filled" /> <i className="icon icon-new-chat-filled" />
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<Menu <Menu
isOpen={isMenuOpen} isOpen={isMenuOpen}

View File

@ -103,7 +103,7 @@ const Archive: FC<OwnProps> = ({
<div className="info-row"> <div className="info-row">
<div className={buildClassName('title', styles.title)}> <div className={buildClassName('title', styles.title)}>
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}> <h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>
<i className={buildClassName(styles.icon, 'icon-archive-filled')} /> <i className={buildClassName(styles.icon, 'icon', 'icon-archive-filled')} />
{lang('ArchivedChats')} {lang('ArchivedChats')}
</h3> </h3>
</div> </div>
@ -122,7 +122,7 @@ const Archive: FC<OwnProps> = ({
<> <>
<div className={buildClassName('status', styles.avatarWrapper)}> <div className={buildClassName('status', styles.avatarWrapper)}>
<div className={buildClassName('Avatar', styles.avatar)}> <div className={buildClassName('Avatar', styles.avatar)}>
<i className="icon-archive-filled" /> <i className="icon icon-archive-filled" />
</div> </div>
</div> </div>
<div className={buildClassName(styles.info, 'info')}> <div className={buildClassName(styles.info, 'info')}>

View File

@ -70,7 +70,7 @@
width: 1.5rem; width: 1.5rem;
padding: 0; padding: 0;
i { .icon {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
@ -83,7 +83,7 @@
width: 1.5rem; width: 1.5rem;
padding: 0.25rem; padding: 0.25rem;
i { .icon {
font-size: 1rem; font-size: 1rem;
vertical-align: super; vertical-align: super;
} }
@ -92,7 +92,7 @@
width: 1.375rem; width: 1.375rem;
padding: 0.25rem; padding: 0.25rem;
i { .icon {
font-size: 0.875rem; font-size: 0.875rem;
} }
} }

View File

@ -67,13 +67,13 @@ const Badge: FC<OwnProps> = ({
function renderContent() { function renderContent() {
const unreadReactionsElement = unreadReactionsCount && ( const unreadReactionsElement = unreadReactionsCount && (
<div className={buildClassName('Badge reaction', shouldBeMuted && 'muted')}> <div className={buildClassName('Badge reaction', shouldBeMuted && 'muted')}>
<i className="icon-heart" /> <i className="icon icon-heart" />
</div> </div>
); );
const unreadMentionsElement = unreadMentionsCount && ( const unreadMentionsElement = unreadMentionsCount && (
<div className="Badge mention"> <div className="Badge mention">
<i className="icon-mention" /> <i className="icon icon-mention" />
</div> </div>
); );
@ -89,7 +89,7 @@ const Badge: FC<OwnProps> = ({
const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && ( const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && (
<div className={className}> <div className={className}>
<i className="icon-pinned-chat" /> <i className="icon icon-pinned-chat" />
</div> </div>
); );

View File

@ -5,13 +5,14 @@ import type { FC } from '../../../lib/teact/teact';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { import type {
ApiChat, ApiChat,
ApiUser, ApiFormattedText,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiFormattedText, ApiPhoto,
ApiUserStatus,
ApiTopic, ApiTopic,
ApiTypingStatus, ApiTypingStatus,
ApiUser,
ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { AnimationLevel } from '../../../types'; import type { AnimationLevel } from '../../../types';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
@ -19,23 +20,25 @@ import type { ChatAnimationTypes } from './hooks';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
import { import {
isUserId,
getPrivateChatUserId,
getMessageAction, getMessageAction,
getPrivateChatUserId,
isUserId,
selectIsChatMuted, selectIsChatMuted,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { import {
selectChat, selectChat,
selectUser,
selectChatMessage, selectChatMessage,
selectOutgoingStatus,
selectDraft,
selectCurrentMessageList, selectCurrentMessageList,
selectNotifySettings, selectDraft,
selectNotifyExceptions, selectNotifyExceptions,
selectUserStatus, selectNotifySettings,
selectOutgoingStatus,
selectTabState,
selectThreadParam,
selectTopicFromMessage, selectTopicFromMessage,
selectThreadParam, selectTabState, selectUser,
selectUserPhotoFromFullInfo,
selectUserStatus,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { createLocationHash } from '../../../util/routing'; import { createLocationHash } from '../../../util/routing';
@ -75,6 +78,7 @@ type StateProps = {
isMuted?: boolean; isMuted?: boolean;
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
userProfilePhoto?: ApiPhoto;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetChatId?: string; actionTargetChatId?: string;
@ -102,6 +106,7 @@ const Chat: FC<OwnProps & StateProps> = ({
isMuted, isMuted,
user, user,
userStatus, userStatus,
userProfilePhoto,
actionTargetUserIds, actionTargetUserIds,
lastMessageSender, lastMessageSender,
lastMessageOutgoingStatus, lastMessageOutgoingStatus,
@ -235,6 +240,7 @@ const Chat: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
chat={chat} chat={chat}
user={user} user={user}
userProfilePhoto={userProfilePhoto}
userStatus={userStatus} userStatus={userStatus}
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
@ -255,7 +261,7 @@ const Chat: FC<OwnProps & StateProps> = ({
isSavedMessages={chatId === user?.id && user?.isSelf} isSavedMessages={chatId === user?.id && user?.isSelf}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
/> />
{isMuted && <i className="icon-muted" />} {isMuted && <i className="icon icon-muted" />}
<div className="separator" /> <div className="separator" />
{chat.lastMessage && ( {chat.lastMessage && (
<LastMessageMeta <LastMessageMeta
@ -324,6 +330,7 @@ export default memo(withGlobal<OwnProps>(
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined; const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
const userProfilePhoto = user ? selectUserPhotoFromFullInfo(global, user.id) : undefined;
const lastMessageTopic = chat.lastMessage && selectTopicFromMessage(global, chat.lastMessage); const lastMessageTopic = chat.lastMessage && selectTopicFromMessage(global, chat.lastMessage);
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus'); const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
@ -347,6 +354,7 @@ export default memo(withGlobal<OwnProps>(
}), }),
user, user,
userStatus, userStatus,
userProfilePhoto,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatus,
}; };

View File

@ -1,4 +1,3 @@
@keyframes bar-animation-transform-1 { @keyframes bar-animation-transform-1 {
0% { transform: scaleY(0.33); } 0% { transform: scaleY(0.33); }
12.5% { transform: scaleY(1.66); } 12.5% { transform: scaleY(1.66); }
@ -24,7 +23,7 @@
} }
.ChatCallStatus { .root {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 0; bottom: 0;
@ -35,47 +34,51 @@
border: 2px solid var(--color-background); border: 2px solid var(--color-background);
overflow: hidden; overflow: hidden;
z-index: 1; z-index: 1;
}
.indicator {
width: 100%;
height: 100%; .indicator {
display: flex; width: 100%;
align-items: center; height: 100%;
justify-content: center; display: flex;
border-radius: 50%; align-items: center;
& > div { justify-content: center;
width: 2px; border-radius: 50%;
height: 6px; }
background: var(--color-background);
border-radius: 1px; .indicatorInner {
margin: 1px; width: 2px;
will-change: transform; height: 6px;
transform: translateZ(0); background: var(--color-background);
} border-radius: 1px;
& > div:nth-child(odd) { margin: 1px;
transform: scaleY(0.8); will-change: transform;
} transform: translateZ(0);
& > div:nth-child(even) { }
transform: scaleY(1.33);
} .indicatorInner:nth-child(odd) {
} transform: scaleY(0.8);
}
&.selected {
background-color: var(--color-white); .indicatorInner:nth-child(even) {
border-color: var(--color-chat-active); transform: scaleY(1.33);
.indicator div{ }
background-color: var(--color-chat-active);
} .selected {
} background-color: var(--color-white);
border-color: var(--color-chat-active);
&.active .indicator {
div:nth-child(odd) { .indicatorInner {
animation: bar-animation-transform-2 3.2s normal infinite; background-color: var(--color-chat-active);
} }
}
div:nth-child(even) {
animation: bar-animation-transform-1 3.2s normal infinite; .active {
} .indicatorInner:nth-child(odd) {
} animation: bar-animation-transform-2 3.2s normal infinite;
}
.indicatorInner:nth-child(even) {
animation: bar-animation-transform-1 3.2s normal infinite;
}
} }

View File

@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact'; import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import './ChatCallStatus.scss'; import styles from './ChatCallStatus.module.scss';
type OwnProps = { type OwnProps = {
isSelected?: boolean; isSelected?: boolean;
@ -17,15 +17,15 @@ const ChatCallStatus: FC<OwnProps> = ({
}) => { }) => {
return ( return (
<div className={buildClassName( <div className={buildClassName(
'ChatCallStatus', styles.root,
isActive && 'active', isActive && styles.active,
isSelected && !isMobile && 'selected', isSelected && !isMobile && styles.selected,
)} )}
> >
<div className="indicator"> <div className={styles.indicator}>
<div /> <div className={styles.indicatorInner} />
<div /> <div className={styles.indicatorInner} />
<div /> <div className={styles.indicatorInner} />
</div> </div>
</div> </div>
); );

View File

@ -242,7 +242,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
) : undefined} ) : undefined}
<Transition <Transition
ref={transitionRef} ref={transitionRef}
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'} name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
activeKey={activeChatFolder} activeKey={activeChatFolder}
renderCount={shouldRenderFolders ? folderTabs.length : undefined} renderCount={shouldRenderFolders ? folderTabs.length : undefined}
> >

View File

@ -92,7 +92,7 @@ const ContactList: FC<OwnProps & StateProps> = ({
onClick={openNewContactDialog} onClick={openNewContactDialog}
ariaLabel={lang('CreateNewContact')} ariaLabel={lang('CreateNewContact')}
> >
<i className="icon-add-user-filled" /> <i className="icon icon-add-user-filled" />
</FloatingActionButton> </FloatingActionButton>
</InfiniteScroll> </InfiniteScroll>
); );

View File

@ -48,7 +48,7 @@
white-space: nowrap; white-space: nowrap;
} }
i { :global(.icon) {
margin-inline-end: 0.625rem; margin-inline-end: 0.625rem;
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

@ -58,7 +58,7 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
size="smaller" size="smaller"
isRtl={lang.isRtl} isRtl={lang.isRtl}
> >
<i className="icon-settings" /> <i className="icon icon-settings" />
<div className={styles.buttonText}> <div className={styles.buttonText}>
{lang('ChatList.EmptyChatListEditFilter')} {lang('ChatList.EmptyChatListEditFilter')}
</div> </div>

View File

@ -25,7 +25,7 @@
white-space: nowrap; white-space: nowrap;
} }
i { :global(.icon) {
margin-inline-end: 0.625rem; margin-inline-end: 0.625rem;
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

@ -72,7 +72,7 @@
margin-left: 0.4375rem; margin-left: 0.4375rem;
min-width: 0; min-width: 0;
width: 100%; width: 100%;
cursor: pointer; cursor: var(--custom-cursor, pointer);
:global(.info) { :global(.info) {
display: flex; display: flex;

View File

@ -224,7 +224,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
onClick={handleClose} onClick={handleClose}
ariaLabel={lang('Close')} ariaLabel={lang('Close')}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
{chat && ( {chat && (

View File

@ -167,7 +167,7 @@ const LeftMain: FC<OwnProps> = ({
isClosingSearch={isClosingSearch} isClosingSearch={isClosingSearch}
/> />
<Transition <Transition
name={shouldSkipTransition ? 'none' : 'zoom-fade'} name={shouldSkipTransition ? 'none' : 'zoomFade'}
renderCount={TRANSITION_RENDER_COUNT} renderCount={TRANSITION_RENDER_COUNT}
activeKey={content} activeKey={content}
shouldCleanup shouldCleanup

View File

@ -5,13 +5,12 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { AnimationLevel, ISettings } from '../../../types'; import type { AnimationLevel, ISettings } from '../../../types';
import { LeftColumnContent, SettingsScreens } from '../../../types';
import type { ApiChat } from '../../../api/types';
import type { TabState, GlobalState } from '../../../global/types'; import type { TabState, GlobalState } from '../../../global/types';
import { LeftColumnContent, SettingsScreens } from '../../../types';
import { import {
ANIMATION_LEVEL_MAX, APP_NAME, APP_VERSION, ARCHIVED_FOLDER_ID,
APP_NAME, APP_VERSION,
BETA_CHANGELOG_URL, BETA_CHANGELOG_URL,
DEBUG, DEBUG,
FEEDBACK_URL, FEEDBACK_URL,
@ -22,13 +21,11 @@ import {
import { IS_PWA } from '../../../util/windowEnvironment'; import { IS_PWA } from '../../../util/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatDateToString } from '../../../util/dateFormat'; import { formatDateToString } from '../../../util/dateFormat';
import switchTheme from '../../../util/switchTheme';
import { setPermanentWebVersion } from '../../../util/permanentWebVersion'; import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
import { clearWebsync } from '../../../util/websync'; import { clearWebsync } from '../../../util/websync';
import { import {
selectCurrentMessageList, selectIsCurrentUserPremium, selectTabState, selectTheme, selectCurrentMessageList, selectIsCurrentUserPremium, selectTabState, selectTheme,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { isChatArchived } from '../../../global/helpers';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useConnectionStatus from '../../../hooks/useConnectionStatus'; import useConnectionStatus from '../../../hooks/useConnectionStatus';
import { useHotkeys } from '../../../hooks/useHotkeys'; import { useHotkeys } from '../../../hooks/useHotkeys';
@ -48,6 +45,7 @@ import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
import StatusButton from './StatusButton'; import StatusButton from './StatusButton';
import './LeftMainHeader.scss'; import './LeftMainHeader.scss';
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
type OwnProps = { type OwnProps = {
shouldHideSearch?: boolean; shouldHideSearch?: boolean;
@ -71,7 +69,6 @@ type StateProps =
searchDate?: number; searchDate?: number;
theme: ISettings['theme']; theme: ISettings['theme'];
animationLevel: AnimationLevel; animationLevel: AnimationLevel;
chatsById?: Record<string, ApiChat>;
isMessageListOpen: boolean; isMessageListOpen: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized']; isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
@ -79,7 +76,8 @@ type StateProps =
hasPasscode?: boolean; hasPasscode?: boolean;
isAuthRememberMe?: boolean; isAuthRememberMe?: boolean;
} }
& Pick<GlobalState, 'connectionState' | 'isSyncing' | 'archiveSettings'> & Pick<TabState, 'canInstall'>; & Pick<GlobalState, 'connectionState' | 'isSyncing' | 'archiveSettings'>
& Pick<TabState, 'canInstall'>;
const ANIMATION_LEVEL_OPTIONS = [0, 1, 2]; const ANIMATION_LEVEL_OPTIONS = [0, 1, 2];
const WEBK_VERSION_URL = 'https://web.telegram.org/k/'; const WEBK_VERSION_URL = 'https://web.telegram.org/k/';
@ -103,7 +101,6 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
searchDate, searchDate,
theme, theme,
animationLevel, animationLevel,
chatsById,
connectionState, connectionState,
isSyncing, isSyncing,
isMessageListOpen, isMessageListOpen,
@ -136,19 +133,8 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
? formatDateToString(new Date(searchDate * 1000)) ? formatDateToString(new Date(searchDate * 1000))
: undefined; : undefined;
}, [searchDate]); }, [searchDate]);
const archivedUnreadChatsCount = useMemo(() => {
if (!hasMenu || !chatsById) {
return 0;
}
return Object.values(chatsById).reduce((total, chat) => { const archivedUnreadChatsCount = useFolderManagerForUnreadCounters()[ARCHIVED_FOLDER_ID]?.chatsCount || 0;
if (!isChatArchived(chat)) {
return total;
}
return chat.unreadCount ? total + 1 : total;
}, 0);
}, [hasMenu, chatsById]);
const { connectionStatus, connectionStatusText, connectionStatusPosition } = useConnectionStatus( const { connectionStatus, connectionStatusText, connectionStatusPosition } = useConnectionStatus(
lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, !areChatsLoaded, lang, connectionState, isSyncing, isMessageListOpen, isConnectionStatusMinimized, !areChatsLoaded,
@ -215,8 +201,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
setSettingOption({ theme: newTheme }); setSettingOption({ theme: newTheme });
setSettingOption({ shouldUseSystemTheme: false }); setSettingOption({ shouldUseSystemTheme: false });
switchTheme(newTheme, animationLevel === ANIMATION_LEVEL_MAX); }, [setSettingOption, theme]);
}, [animationLevel, setSettingOption, theme]);
const handleAnimationLevelChange = useCallback((e: React.SyntheticEvent<HTMLElement>) => { const handleAnimationLevelChange = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
e.stopPropagation(); e.stopPropagation();
@ -347,7 +332,8 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
)} )}
{withOtherVersions && ( {withOtherVersions && (
<MenuItem <MenuItem
icon="char-K" icon="K"
isCharIcon
href={WEBK_VERSION_URL} href={WEBK_VERSION_URL}
onClick={handleSwitchToWebK} onClick={handleSwitchToWebK}
> >
@ -437,7 +423,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
onClick={handleLockScreen} onClick={handleLockScreen}
className={buildClassName(!isCurrentUserPremium && 'extra-spacing')} className={buildClassName(!isCurrentUserPremium && 'extra-spacing')}
> >
<i className="icon-lock" /> <i className="icon icon-lock" />
</Button> </Button>
)} )}
<ShowTransition <ShowTransition
@ -465,14 +451,12 @@ export default memo(withGlobal<OwnProps>(
const { const {
currentUserId, connectionState, isSyncing, archiveSettings, currentUserId, connectionState, isSyncing, archiveSettings,
} = global; } = global;
const { byId: chatsById } = global.chats;
const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey; const { isConnectionStatusMinimized, animationLevel } = global.settings.byKey;
return { return {
searchQuery, searchQuery,
isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false, isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false,
currentUserId, currentUserId,
chatsById,
globalSearchChatId: chatId, globalSearchChatId: chatId,
searchDate: date, searchDate: date,
theme: selectTheme(global), theme: selectTheme(global),

View File

@ -156,10 +156,11 @@ const Topic: FC<OwnProps & StateProps> = ({
<TopicIcon topic={topic} className={styles.topicIcon} /> <TopicIcon topic={topic} className={styles.topicIcon} />
<h3 dir="auto" className="fullName">{renderText(topic.title)}</h3> <h3 dir="auto" className="fullName">{renderText(topic.title)}</h3>
</div> </div>
{topic.isMuted && <i className="icon-muted" />} {topic.isMuted && <i className="icon icon-muted" />}
<div className="separator" /> <div className="separator" />
{isClosed && ( {isClosed && (
<i className={buildClassName( <i className={buildClassName(
'icon',
'icon-lock-badge', 'icon-lock-badge',
styles.closedIcon, styles.closedIcon,
)} )}

View File

@ -226,7 +226,7 @@ function renderSummary(
buildClassName('media-preview--image', isRoundVideo && 'round', isSpoiler && 'media-preview-spoiler') buildClassName('media-preview--image', isRoundVideo && 'round', isSpoiler && 'media-preview-spoiler')
} }
/> />
{getMessageVideo(message) && <i className="icon-play" />} {getMessageVideo(message) && <i className="icon icon-play" />}
{messageSummary} {messageSummary}
</span> </span>
); );

View File

@ -102,7 +102,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
onClick={onReset} onClick={onReset}
ariaLabel="Return to Chat List" ariaLabel="Return to Chat List"
> >
<i className="icon-arrow-left" /> <i className="icon icon-arrow-left" />
</Button> </Button>
<h3>{lang('GroupAddMembers')}</h3> <h3>{lang('GroupAddMembers')}</h3>
</div> </div>
@ -123,7 +123,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
onClick={handleNextStep} onClick={handleNextStep}
ariaLabel={isChannel ? 'Continue To Channel Info' : 'Continue To Group Info'} ariaLabel={isChannel ? 'Continue To Channel Info' : 'Continue To Group Info'}
> >
<i className="icon-arrow-right" /> <i className="icon icon-arrow-right" />
</FloatingActionButton> </FloatingActionButton>
</div> </div>
</div> </div>

View File

@ -133,7 +133,7 @@ const NewChatStep2: FC<OwnProps & StateProps > = ({
onClick={() => onReset()} onClick={() => onReset()}
ariaLabel="Return to member selection" ariaLabel="Return to member selection"
> >
<i className="icon-arrow-left" /> <i className="icon icon-arrow-left" />
</Button> </Button>
<h3>{lang(isChannel ? 'NewChannel' : 'NewGroup')}</h3> <h3>{lang(isChannel ? 'NewChannel' : 'NewGroup')}</h3>
</div> </div>
@ -187,7 +187,7 @@ const NewChatStep2: FC<OwnProps & StateProps > = ({
{isLoading ? ( {isLoading ? (
<Spinner color="white" /> <Spinner color="white" />
) : ( ) : (
<i className="icon-arrow-right" /> <i className="icon icon-arrow-right" />
)} )}
</FloatingActionButton> </FloatingActionButton>
</div> </div>

View File

@ -3,7 +3,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiPhoto,
} from '../../../api/types'; } from '../../../api/types';
import type { AnimationLevel } from '../../../types'; import type { AnimationLevel } from '../../../types';
import type { LangFn } from '../../../hooks/useLang'; import type { LangFn } from '../../../hooks/useLang';
@ -17,7 +17,7 @@ import {
getMessageSticker, getMessageSticker,
getMessageIsSpoiler, getMessageIsSpoiler,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { selectChat, selectUser } from '../../../global/selectors'; import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatPastTimeShort } from '../../../util/dateFormat'; import { formatPastTimeShort } from '../../../util/dateFormat';
import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import { renderMessageSummary } from '../../common/helpers/renderMessageText';
@ -43,6 +43,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
privateChatUser?: ApiUser; privateChatUser?: ApiUser;
userProfilePhoto?: ApiPhoto;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
lastSyncTime?: number; lastSyncTime?: number;
animationLevel?: AnimationLevel; animationLevel?: AnimationLevel;
@ -54,6 +55,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
chatId, chatId,
chat, chat,
privateChatUser, privateChatUser,
userProfilePhoto,
animationLevel, animationLevel,
lastSyncTime, lastSyncTime,
}) => { }) => {
@ -86,6 +88,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
chat={chat} chat={chat}
user={privateChatUser} user={privateChatUser}
userProfilePhoto={userProfilePhoto}
isSavedMessages={privateChatUser?.isSelf} isSavedMessages={privateChatUser?.isSelf}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
withVideo withVideo
@ -133,7 +136,7 @@ function renderSummary(
buildClassName('media-preview--image', isRoundVideo && 'round', isSpoiler && 'media-preview-spoiler') buildClassName('media-preview--image', isRoundVideo && 'round', isSpoiler && 'media-preview-spoiler')
} }
/> />
{getMessageVideo(message) && <i className="icon-play" />} {getMessageVideo(message) && <i className="icon icon-play" />}
{renderMessageSummary(lang, message, true, searchQuery)} {renderMessageSummary(lang, message, true, searchQuery)}
</span> </span>
); );
@ -147,12 +150,17 @@ export default memo(withGlobal<OwnProps>(
} }
const privateChatUserId = getPrivateChatUserId(chat); const privateChatUserId = getPrivateChatUserId(chat);
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userProfilePhoto = privateChatUser ? selectUserPhotoFromFullInfo(global, privateChatUser.id) : undefined;
return { return {
chat, chat,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
animationLevel: global.settings.byKey.animationLevel, animationLevel: global.settings.byKey.animationLevel,
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }), ...(privateChatUserId && {
privateChatUser,
userProfilePhoto,
}),
}; };
}, },
)(ChatMessage)); )(ChatMessage));

View File

@ -11,7 +11,7 @@
flex: 1 1 auto; flex: 1 1 auto;
min-width: 8rem; min-width: 8rem;
margin-top: 0.375rem; margin-top: 0.375rem;
cursor: pointer; cursor: var(--custom-cursor, pointer);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: var(--color-text-secondary); color: var(--color-text-secondary);

View File

@ -24,7 +24,7 @@ const DateSuggest: FC<OwnProps> = ({
className="date-item" className="date-item"
key={text} key={text}
> >
<i className="icon-calendar" /> <i className="icon icon-calendar" />
<span>{text}</span> <span>{text}</span>
</div> </div>
); );

View File

@ -91,7 +91,7 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}> <div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} /> <TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
<Transition <Transition
name={lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'} name={lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
renderCount={TRANSITION_RENDER_COUNT} renderCount={TRANSITION_RENDER_COUNT}
activeKey={currentContent} activeKey={currentContent}
> >

View File

@ -33,7 +33,7 @@
padding: 0.625rem 0.25rem; padding: 0.625rem 0.25rem;
margin-left: 0.5rem; margin-left: 0.5rem;
border-radius: var(--border-radius-default); border-radius: var(--border-radius-default);
cursor: pointer; cursor: var(--custom-cursor, pointer);
position: relative; position: relative;
overflow: hidden; overflow: hidden;

View File

@ -5,7 +5,6 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiUser } from '../../../api/types'; import type { ApiUser } from '../../../api/types';
import type { AnimationLevel } from '../../../types';
import { getUserFirstOrLastName } from '../../../global/helpers'; import { getUserFirstOrLastName } from '../../../global/helpers';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -13,9 +12,9 @@ import { throttle } from '../../../util/schedulers';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import LeftSearchResultChat from './LeftSearchResultChat'; import LeftSearchResultChat from './LeftSearchResultChat';
import UserAvatar from '../../common/UserAvatar';
import './RecentContacts.scss'; import './RecentContacts.scss';
@ -27,7 +26,6 @@ type StateProps = {
topUserIds?: string[]; topUserIds?: string[];
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
recentlyFoundChatIds?: string[]; recentlyFoundChatIds?: string[];
animationLevel: AnimationLevel;
}; };
const SEARCH_CLOSE_TIMEOUT_MS = 250; const SEARCH_CLOSE_TIMEOUT_MS = 250;
@ -39,7 +37,6 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
topUserIds, topUserIds,
usersById, usersById,
recentlyFoundChatIds, recentlyFoundChatIds,
animationLevel,
onReset, onReset,
}) => { }) => {
const { const {
@ -80,8 +77,13 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
<div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}> <div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}>
<div ref={topUsersRef} className="top-peers no-selection"> <div ref={topUsersRef} className="top-peers no-selection">
{topUserIds.map((userId) => ( {topUserIds.map((userId) => (
<div className="top-peer-item" onClick={() => handleClick(userId)} dir={lang.isRtl ? 'rtl' : undefined}> <div
<Avatar user={usersById[userId]} animationLevel={animationLevel} withVideo /> key={userId}
className="top-peer-item"
onClick={() => handleClick(userId)}
dir={lang.isRtl ? 'rtl' : undefined}
>
<UserAvatar user={usersById[userId]} withVideo />
<div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div> <div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div>
</div> </div>
))} ))}
@ -101,7 +103,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
onClick={handleClearRecentlyFoundChats} onClick={handleClearRecentlyFoundChats}
isRtl={lang.isRtl} isRtl={lang.isRtl}
> >
<i className="icon-close" /> <i className="icon icon-close" />
</Button> </Button>
</h3> </h3>
{recentlyFoundChatIds.map((id) => ( {recentlyFoundChatIds.map((id) => (
@ -121,13 +123,11 @@ export default memo(withGlobal<OwnProps>(
const { userIds: topUserIds } = global.topPeers; const { userIds: topUserIds } = global.topPeers;
const usersById = global.users.byId; const usersById = global.users.byId;
const { recentlyFoundChatIds } = global; const { recentlyFoundChatIds } = global;
const { animationLevel } = global.settings.byKey;
return { return {
topUserIds, topUserIds,
usersById, usersById,
recentlyFoundChatIds, recentlyFoundChatIds,
animationLevel,
}; };
}, },
)(RecentContacts)); )(RecentContacts));

View File

@ -3,7 +3,7 @@
#Settings { #Settings {
height: 100%; height: 100%;
> .Transition__slide { > .Transition_slide {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@ -98,7 +98,7 @@
.settings-content-icon { .settings-content-icon {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
&.opacity-transition:not(.shown) { &.opacity-transition.not-shown {
display: block; display: block;
visibility: hidden; visibility: hidden;
} }
@ -242,7 +242,7 @@
.ListItem-button { .ListItem-button {
color: var(--color-error); color: var(--color-error);
i { .icon {
color: inherit; color: inherit;
} }
} }

Some files were not shown because too many files have changed in this diff Show More