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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
padding: 0.5rem 0.75rem;
border-radius: 0.75rem;
transition: 0.15s ease-out background-color;
cursor: pointer;
cursor: var(--custom-cursor, pointer);
&:hover {
background: #2f363e;
@ -72,7 +72,7 @@
}
.streams {
cursor: pointer;
cursor: var(--custom-cursor, pointer);
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 { withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types';
import type { ApiChat, ApiPhoto, ApiUser } from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import { selectChat, selectUser } from '../../../global/selectors';
import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
@ -23,6 +23,7 @@ type OwnProps = {
type StateProps = {
user?: ApiUser;
userProfilePhoto?: ApiPhoto;
chat?: ApiChat;
};
@ -30,6 +31,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
openParticipantMenu,
participant,
user,
userProfilePhoto,
chat,
}) => {
// eslint-disable-next-line no-null/no-null
@ -79,7 +81,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
onClick={handleOnClick}
ref={anchorRef}
>
<Avatar user={user} chat={chat} size="medium" />
<Avatar user={user} chat={chat} userProfilePhoto={userProfilePhoto} size="medium" />
<div className="info">
<span className="name">{name}</span>
<span className={buildClassName('about', aboutColor)}>{aboutText}</span>
@ -96,6 +98,7 @@ export default memo(withGlobal<OwnProps>(
return {
user: participant.isUser ? selectUser(global, participant.id) : undefined,
chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
userProfilePhoto: participant.isUser ? selectUserPhotoFromFullInfo(global, participant.id) : undefined,
};
},
)(GroupCallParticipant));

View File

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

View File

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

View File

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

View File

@ -4,11 +4,11 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback } from '../../../lib/teact/teact';
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 buildClassName from '../../../util/buildClassName';
import { selectChat, selectUser } from '../../../global/selectors';
import { selectChat, selectUser, selectUserPhotoFromFullInfo } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar';
@ -25,6 +25,7 @@ type OwnProps = {
type StateProps = {
user?: ApiUser;
chat?: ApiChat;
userProfilePhoto?: ApiPhoto;
currentUserId?: string;
isActive?: boolean;
};
@ -34,6 +35,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
onClick,
user,
chat,
userProfilePhoto,
isActive,
isFullscreen,
}) => {
@ -56,11 +58,11 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
>
{isFullscreen && (
<button className="back-button">
<i className="icon-arrow-left" />
<i className="icon icon-arrow-left" />
{lang('Back')}
</button>
)}
<Avatar user={user} chat={chat} className="thumbnail-avatar" />
<Avatar user={user} chat={chat} userProfilePhoto={userProfilePhoto} className="thumbnail-avatar" />
{!GROUP_CALL_THUMB_VIDEO_DISABLED && (
<div className="thumbnail-wrapper">
<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]} />
<div className="info">
<i className="icon-microphone-alt" />
<i className="icon icon-microphone-alt" />
<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>
);
@ -82,6 +84,7 @@ export default memo(withGlobal<OwnProps>(
currentUserId: global.currentUserId,
user: participant.isUser ? selectUser(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,
};
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,7 @@ type OwnProps = {
chat?: ApiChat;
user?: ApiUser;
photo?: ApiPhoto;
userProfilePhoto?: ApiPhoto;
userStatus?: ApiUserStatus;
text?: string;
isSavedMessages?: boolean;
@ -69,6 +70,7 @@ const Avatar: FC<OwnProps> = ({
chat,
user,
photo,
userProfilePhoto,
userStatus,
text,
isSavedMessages,
@ -96,8 +98,7 @@ const Avatar: FC<OwnProps> = ({
withVideo && !VIDEO_AVATARS_DISABLED && animationLevel === ANIMATION_LEVEL_MAX
&& user?.isPremium && user?.hasVideoAvatar
);
const profilePhoto = user?.fullInfo?.personalPhoto || user?.fullInfo?.profilePhoto || user?.fullInfo?.fallbackPhoto;
const hasProfileVideo = profilePhoto?.isVideo;
const hasProfileVideo = userProfilePhoto?.isVideo;
const isIntersectingForVideo = useIsIntersecting(
ref, canShowVideo && hasProfileVideo ? observeIntersection : undefined,
);
@ -117,7 +118,7 @@ const Avatar: FC<OwnProps> = ({
}
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;
useEffect(() => {
if (userId && canShowVideo && !profilePhoto) {
if (userId && canShowVideo && !userProfilePhoto) {
loadFullUser({ userId });
}
}, [loadFullUser, profilePhoto, userId, canShowVideo]);
}, [loadFullUser, userProfilePhoto, userId, canShowVideo]);
const lang = useLang();
@ -160,11 +161,35 @@ const Avatar: FC<OwnProps> = ({
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
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) {
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) {
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) {
content = (
<>

View File

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

View File

@ -249,7 +249,7 @@ const CalendarModal: FC<OwnProps> = ({
color="translucent"
onClick={onClose}
>
<i className="icon-close" />
<i className="icon icon-close" />
</Button>
<h4>
@ -265,7 +265,7 @@ const CalendarModal: FC<OwnProps> = ({
disabled={shouldDisablePrevMonth}
onClick={!shouldDisablePrevMonth ? handlePrevMonth : undefined}
>
<i className="icon-previous" />
<i className="icon icon-previous" />
</Button>
<Button
@ -275,7 +275,7 @@ const CalendarModal: FC<OwnProps> = ({
disabled={shouldDisableNextMonth}
onClick={!shouldDisableNextMonth ? handleNextMonth : undefined}
>
<i className="icon-next" />
<i className="icon icon-next" />
</Button>
</div>
</div>
@ -297,6 +297,7 @@ const CalendarModal: FC<OwnProps> = ({
onClick={() => handleDateSelect(gridDate)}
className={buildClassName(
'day-button',
'div-button',
isDisabledDay(
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 {
selectChat, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings, selectUser,
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectNotifyExceptions,
selectNotifySettings,
selectUser,
selectUserFullInfo,
} from '../../global/selectors';
import {
getChatDescription,
getChatLink,
getTopicLink,
getHasAdminRight,
@ -47,6 +52,8 @@ type StateProps =
isMuted?: boolean;
phoneCodeList: ApiCountryCode[];
topicId?: number;
description?: string;
chatInviteLink?: string;
}
& Pick<GlobalState, 'lastSyncTime'>;
@ -61,6 +68,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
isMuted,
phoneCodeList,
topicId,
description,
chatInviteLink,
}) => {
const {
loadFullUser,
@ -71,7 +80,6 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
const {
id: userId,
fullInfo,
usernames,
phoneNumber,
isSelf,
@ -108,8 +116,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
return isTopicInfo
? getTopicLink(chat.id, activeChatUsernames?.[0].username, topicId)
: getChatLink(chat);
}, [chat, isTopicInfo, activeChatUsernames, topicId]);
: getChatLink(chat) || chatInviteLink;
}, [chat, isTopicInfo, activeChatUsernames, topicId, chatInviteLink]);
const handleNotificationChange = useCallback(() => {
setAreNotificationsEnabled((current) => {
@ -141,7 +149,6 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
}
const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber);
const description = (fullInfo?.bio) || getChatDescription(chat);
function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) {
const [mainUsername, ...otherUsernames] = usernameList;
@ -259,6 +266,11 @@ export default memo(withGlobal<OwnProps>(
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
const { threadId } = selectCurrentMessageList(global) || {};
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 && (
(!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers'))
@ -273,6 +285,8 @@ export default memo(withGlobal<OwnProps>(
canInviteUsers,
isMuted,
topicId,
chatInviteLink,
description,
};
},
)(ChatExtra));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,10 @@ const TopicDefaultIcon: FC<OwnProps> = ({
if (topicId === GENERAL_TOPIC_ID) {
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 (

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;
width: 3rem;
height: 3rem;
cursor: default !important;
cursor: var(--custom-cursor, default) !important;
padding-bottom: unset !important;
border-radius: var(--border-radius-messages-small) !important;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
@keyframes bar-animation-transform-1 {
0% { transform: scaleY(0.33); }
12.5% { transform: scaleY(1.66); }
@ -24,7 +23,7 @@
}
.ChatCallStatus {
.root {
position: absolute;
right: 6px;
bottom: 0;
@ -35,47 +34,51 @@
border: 2px solid var(--color-background);
overflow: hidden;
z-index: 1;
.indicator {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
& > div {
width: 2px;
height: 6px;
background: var(--color-background);
border-radius: 1px;
margin: 1px;
will-change: transform;
transform: translateZ(0);
}
& > div:nth-child(odd) {
transform: scaleY(0.8);
}
& > div:nth-child(even) {
transform: scaleY(1.33);
}
}
&.selected {
background-color: var(--color-white);
border-color: var(--color-chat-active);
.indicator div{
background-color: var(--color-chat-active);
}
}
&.active .indicator {
div:nth-child(odd) {
animation: bar-animation-transform-2 3.2s normal infinite;
}
div:nth-child(even) {
animation: bar-animation-transform-1 3.2s normal infinite;
}
}
}
.indicator {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.indicatorInner {
width: 2px;
height: 6px;
background: var(--color-background);
border-radius: 1px;
margin: 1px;
will-change: transform;
transform: translateZ(0);
}
.indicatorInner:nth-child(odd) {
transform: scaleY(0.8);
}
.indicatorInner:nth-child(even) {
transform: scaleY(1.33);
}
.selected {
background-color: var(--color-white);
border-color: var(--color-chat-active);
.indicatorInner {
background-color: var(--color-chat-active);
}
}
.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 buildClassName from '../../../util/buildClassName';
import './ChatCallStatus.scss';
import styles from './ChatCallStatus.module.scss';
type OwnProps = {
isSelected?: boolean;
@ -17,15 +17,15 @@ const ChatCallStatus: FC<OwnProps> = ({
}) => {
return (
<div className={buildClassName(
'ChatCallStatus',
isActive && 'active',
isSelected && !isMobile && 'selected',
styles.root,
isActive && styles.active,
isSelected && !isMobile && styles.selected,
)}
>
<div className="indicator">
<div />
<div />
<div />
<div className={styles.indicator}>
<div className={styles.indicatorInner} />
<div className={styles.indicatorInner} />
<div className={styles.indicatorInner} />
</div>
</div>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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