Support member tags (#6749)

This commit is contained in:
zubiden 2026-03-31 11:29:01 +02:00 committed by Alexander Zinchuk
parent cdebd3b479
commit cbce577dab
60 changed files with 2416 additions and 1077 deletions

View File

@ -345,6 +345,7 @@ export function buildChatMember(
return {
userId,
rank: 'rank' in member ? member.rank : undefined,
inviterId: 'inviterId' in member && member.inviterId
? buildApiPeerId(member.inviterId, 'user')
: undefined,
@ -355,7 +356,6 @@ export function buildChatMember(
: undefined,
bannedRights: 'bannedRights' in member ? omitVirtualClassFields(member.bannedRights) : undefined,
adminRights: 'adminRights' in member ? omitVirtualClassFields(member.adminRights) : undefined,
customTitle: 'rank' in member ? member.rank : undefined,
isViaRequest: 'viaRequest' in member ? member.viaRequest : undefined,
...((member instanceof GramJs.ChannelParticipantAdmin || member instanceof GramJs.ChatParticipantAdmin) && {
isAdmin: true,

View File

@ -297,6 +297,7 @@ export function buildApiMessageWithChatId(
paidMessageStars: toJSNumber(mtpMessage.paidMessageStars),
restrictionReasons,
summaryLanguageCode: mtpMessage.summaryFromLanguage,
fromRank: mtpMessage.fromRank,
};
}

View File

@ -1411,8 +1411,8 @@ export function updateChatMemberBannedRights({
}
export function updateChatAdmin({
chat, user, adminRights, customTitle = DEFAULT_PRIMITIVES.STRING,
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle?: string }) {
chat, user, adminRights, rank,
}: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; rank?: string }) {
const channel = buildInputChannel(chat.id, chat.accessHash);
const userId = buildInputUser(user.id, user.accessHash);
@ -1420,7 +1420,7 @@ export function updateChatAdmin({
channel,
userId,
adminRights: buildChatAdminRights(adminRights),
rank: customTitle,
rank,
}), {
shouldReturnTrue: true,
});

View File

@ -1112,6 +1112,24 @@ export async function deleteParticipantHistory({
}
}
export function editChatParticipantRank({
chat, peer, rank,
}: {
chat: ApiChat;
peer: ApiPeer;
rank: string;
}) {
const participant = buildInputPeer(peer.id, peer.accessHash);
return invokeRequest(new GramJs.messages.EditChatParticipantRank({
peer: buildInputPeer(chat.id, chat.accessHash),
participant,
rank,
}), {
shouldReturnTrue: true,
});
}
export function deleteScheduledMessages({
chat, messageIds,
}: {

View File

@ -654,6 +654,13 @@ export function updater(update: Update) {
id: buildApiPeerId(update.chatId, 'chat'),
deletedMemberId: buildApiPeerId(update.userId, 'user'),
});
} else if (update instanceof GramJs.UpdateChatParticipantRank) {
sendApiUpdate({
'@type': 'updateChatParticipantRank',
id: buildApiPeerId(update.chatId, 'chat'),
userId: buildApiPeerId(update.userId, 'user'),
rank: update.rank,
});
} else if (
update instanceof GramJs.UpdatePinnedMessages
|| update instanceof GramJs.UpdatePinnedChannelMessages

View File

@ -160,13 +160,13 @@ export interface ApiChatFullInfo {
export interface ApiChatMember {
userId: string;
rank?: string;
inviterId?: string;
joinedDate?: number;
kickedByUserId?: string;
promotedByUserId?: string;
bannedRights?: ApiChatBannedRights;
adminRights?: ApiChatAdminRights;
customTitle?: string;
isAdmin?: true;
isOwner?: true;
isViaRequest?: true;
@ -188,6 +188,7 @@ export interface ApiChatAdminRights {
editStories?: true;
deleteStories?: true;
manageDirectMessages?: true;
manageRanks?: true;
}
export interface ApiChatBannedRights {
@ -211,6 +212,7 @@ export interface ApiChatBannedRights {
sendVoices?: true;
sendDocs?: true;
sendPlain?: true;
editRank?: true;
untilDate?: number;
}

View File

@ -698,6 +698,7 @@ export interface ApiMessage {
paidMessageStars?: number;
restrictionReasons?: ApiRestrictionReason[];
summaryLanguageCode?: string;
fromRank?: string;
isTypingDraft?: boolean; // Local field
}

View File

@ -179,6 +179,13 @@ export type ApiUpdateChatMembers = {
deletedMemberId?: string;
};
export type ApiUpdateChatParticipantRank = {
'@type': 'updateChatParticipantRank';
id: string;
userId: string;
rank: string;
};
export type ApiUpdatePinnedChatIds = {
'@type': 'updatePinnedChatIds';
ids?: string[];
@ -924,7 +931,8 @@ export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
ApiUpdateChat | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
ApiUpdateChatMembers | ApiUpdateChatParticipantRank | ApiUpdateChatJoin | ApiUpdateChatLeave
| ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption |
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M14.98 14.686a6.31 6.31 0 1 0 .003-12.62h-.002a6.31 6.31 0 0 0 0 12.62m-.224 11.128a4.4 4.4 0 0 1-.555-.708l-.153-.228-.1-.28a3.7 3.7 0 0 1-.206-1.16v-4.972c0-.202.023-.4.06-.594-6.067.298-9.243 2.86-10.606 4.377-.613.683-.837 1.595-.837 2.513a4.123 4.123 0 0 0 4.123 4.123h11.344z"/><path d="M29.64 24.56c0 .16-.053.321-.106.535-.054.214-.16.32-.321.48l-4.971 4.972c-.16.16-.268.214-.481.32-.214.108-.321.054-.481.054s-.321-.053-.535-.107c-.214-.053-.32-.16-.481-.32L16.17 24.4a3 3 0 0 1-.32-.428 2 2 0 0 1-.107-.534v-4.971c0-.374.16-.695.427-.962s.588-.428.963-.428h4.97c.161 0 .375.053.535.107.16.053.32.16.428.32l6.147 6.094q.24.24.32.481c.054.16.107.321.107.481m-9.728-4.383c0-.267-.107-.534-.32-.748-.214-.16-.428-.267-.749-.267s-.534.107-.748.32-.321.428-.321.749c0 .267.107.534.32.748s.428.32.75.32.534-.106.747-.32c.214-.267.321-.481.321-.802"/></svg>

After

Width:  |  Height:  |  Size: 950 B

View File

@ -130,6 +130,7 @@
"UserRestrictionsNoChangeInfo" = "can't change Info";
"UserRestrictionsInviteUsers" = "Add Users";
"UserRestrictionsPinMessages" = "Pin Messages";
"UserRestrictionsEditRank" = "Edit Own Tags";
"ChatPermissionNotAvailable" = "This permission is not available in public groups.";
"StatsMessageInteractionsTitle" = "INTERACTIONS";
"StatsGroupGrowthTitle" = "GROWTH";
@ -541,6 +542,7 @@
"PrivacyExceptions" = "Exceptions";
"AlwaysAllow" = "Always Allow";
"EditAdminAddUsers" = "Add Users";
"EditAdminEditRank" = "Edit Member Tags";
"NeverAllow" = "Never Allow";
"AlwaysAllowPlaceholder" = "Always allow...";
"NeverAllowPlaceholder" = "Never allow...";
@ -966,7 +968,7 @@
"StartVoipChatPermission" = "Manage Video Chats";
"EditAdminSendAnonymously" = "Remain Anonymous";
"ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin.";
"EditAdminRank" = "Custom title";
"EditAdminRank" = "Member tag";
"EditAdminRemoveAdmin" = "Dismiss Admin";
"EditAdminTransferChannelOwnership" = "Transfer Channel Ownership";
"EditAdminTransferGroupOwnership" = "Transfer Group Ownership";
@ -2732,4 +2734,19 @@
"GiftPreviewToggleRegularModels" = "View Primary Models >";
"AriaGiftPreviewPlay" = "Play random previews";
"AriaGiftPreviewStop" = "Pause random previews";
"RankModalEdit" = "Edit Tag";
"RankModalEditMy" = "Edit My Tag";
"MemberContextEditRank" = "Edit Tag";
"RankMemberTag" = "Member Tag";
"RankAdminTag" = "Admin Tag";
"RankOwnerTag" = "Owner Tag";
"RankModalMemberTagTitle" = "Member Tag";
"RankModalAdminTagTitle" = "Admin Tag";
"RankModalOwnerTagTitle" = "Owner Tag";
"RankModalMemberText" = "This gray tag {tag} is **{author}**'s member tag in **{group}**.";
"RankModalAdminText" = "This green tag {tag} is **{author}**'s admin tag in **{group}**.";
"RankModalOwnerText" = "This purple tag {tag} is **{author}**'s owner tag in **{group}**.";
"RankEditSave" = "Edit Tag";
"RankEditTextOwn" = "Share your role, title or how you're known in this group. Your tag is visible to all members.";
"RankEditText" = "Add a short tag next to {user}'s name.";
"MenuAddCaption" = "Add Caption";

View File

@ -105,6 +105,8 @@ export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/
export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal';
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
export { default as EditRankModal } from '../components/modals/rank/EditRankModal';
export { default as RankModal } from '../components/modals/rank/RankModal';
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
export { default as LeaveGroupModal } from '../components/modals/leaveGroup/LeaveGroupModal';

View File

@ -254,7 +254,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
)}
>
{isLoading && (
<Skeleton className={buildClassName(styles.video, styles.loader)} />
<Skeleton className={buildClassName(styles.video, styles.loader)} animation="wave" />
)}
{stream && (
<video

View File

@ -1,5 +1,5 @@
.root {
padding: 0.25em 0.5em;
padding: 0.25rem 0.5rem;
border-radius: 1em;
font-size: 0.75rem;
@ -12,6 +12,11 @@
transition: 150ms filter ease-in;
}
.plain {
color: rgba(var(--color-text-meta-rgb), 0.75);
background-color: transparent;
}
.clickable {
cursor: var(--custom-cursor, pointer);
@ -19,3 +24,7 @@
filter: brightness(1.1);
}
}
.inline {
display: inline;
}

View File

@ -1,12 +1,14 @@
import type React from '../../lib/teact/teact';
import { type TeactNode } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import styles from './BadgeButton.module.scss';
type OwnProps = {
children: React.ReactNode;
children: TeactNode;
className?: string;
isPlain?: boolean;
inline?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
};
@ -14,12 +16,20 @@ type OwnProps = {
const BadgeButton = ({
children,
className,
isPlain,
inline,
onClick,
onMouseDown,
}: OwnProps) => {
return (
<div
className={buildClassName(styles.root, onClick && styles.clickable, className)}
className={buildClassName(
styles.root,
isPlain && styles.plain,
onClick && styles.clickable,
inline && styles.inline,
className,
)}
onClick={onClick}
onMouseDown={onMouseDown}
>

View File

@ -0,0 +1,132 @@
.root {
--preview-background-overscan: 50%;
isolation: isolate;
position: relative;
overflow: hidden;
border-radius: 1.5rem;
background-color: var(--theme-background-color);
}
.background {
&::before,
&::after {
inset: calc(var(--preview-background-overscan) * -1);
}
}
.content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 100%;
padding: 0.75rem;
}
.message,
.bubble {
--preview-message-background: var(--color-background);
--preview-message-sender-color: var(--accent-color);
--preview-message-shadow: 0 1px 2px var(--color-default-shadow);
min-width: 0;
max-width: 100%;
padding: 0.375rem 0.5rem 0.4375rem 0.625rem;
border-radius: 1.125rem;
background: var(--preview-message-background);
box-shadow: var(--preview-message-shadow);
}
.message {
display: flex;
flex-direction: column;
}
.messageWithAvatar {
display: flex;
gap: 0.5rem;
align-items: flex-end;
width: 100%;
min-width: 0;
}
.avatar {
flex: 0 0 auto;
}
.bubble {
flex: 1 1 auto;
}
.header {
user-select: none;
display: flex;
align-items: center;
min-width: 0;
height: 1.25rem;
margin-bottom: 0.375rem;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
line-height: 1;
}
.sender {
overflow: hidden;
flex: 0 1 auto;
font-weight: var(--font-weight-medium);
color: var(--preview-message-sender-color);
text-overflow: ellipsis;
white-space: nowrap;
}
.spacer {
flex: 1 1 auto;
min-width: 0.5rem;
}
.badge {
display: flex;
flex: 0 0 auto;
align-items: center;
margin-inline-start: 0.5rem;
}
.body {
display: flex;
flex-direction: column;
gap: 0.375rem;
min-width: 0;
font-size: var(--message-text-size, 1rem);
}
.footer {
user-select: none;
display: flex;
justify-content: flex-end;
margin-top: 0.375rem;
}
.time {
display: inline-flex;
flex: 0 0 auto;
align-items: flex-end;
font-size: 0.75rem;
line-height: 1;
color: rgba(var(--color-text-meta-rgb), 0.75);
white-space: nowrap;
}

View File

@ -0,0 +1,241 @@
import type { FC, TeactNode } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ThemeKey } from '../../types';
import { selectTheme, selectThemeValues } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import useCustomBackground from '../../hooks/useCustomBackground';
import backgroundStyles from '../../styles/_patternBackground.module.scss';
import styles from './PreviewBlock.module.scss';
type OwnProps = {
children: TeactNode;
className?: string;
style?: string;
contentClassName?: string;
backgroundClassName?: string;
backgroundStyle?: string;
backgroundColor?: string;
patternColor?: string;
customBackground?: string;
isBackgroundBlurred?: boolean;
};
type StateProps = {
theme: ThemeKey;
themeBackgroundColor?: string;
themePatternColor?: string;
themeCustomBackground?: string;
themeIsBackgroundBlurred?: boolean;
};
type MessageProps = {
children?: TeactNode;
className?: string;
style?: string;
bubbleClassName?: string;
bubbleStyle?: string;
headerClassName?: string;
bodyClassName?: string;
footerClassName?: string;
avatar?: TeactNode;
sender?: TeactNode;
badge?: TeactNode;
footer?: TeactNode;
time?: TeactNode;
senderColor?: string;
backgroundColor?: string;
};
type MessageTimeProps = {
children?: TeactNode;
className?: string;
style?: string;
};
type PreviewBlockMessageComponent = FC<MessageProps> & {
Time: FC<MessageTimeProps>;
};
type PreviewBlockComponent = FC<OwnProps> & {
Message: PreviewBlockMessageComponent;
};
const PreviewBlockBase = ({
children,
className,
style,
contentClassName,
backgroundClassName,
backgroundStyle,
backgroundColor,
patternColor,
customBackground,
isBackgroundBlurred,
theme,
themeBackgroundColor,
themePatternColor,
themeCustomBackground,
themeIsBackgroundBlurred,
}: OwnProps & StateProps) => {
const resolvedBackgroundColor = backgroundColor ?? themeBackgroundColor;
const resolvedPatternColor = patternColor ?? themePatternColor;
const resolvedCustomBackground = customBackground ?? themeCustomBackground;
const resolvedIsBackgroundBlurred = isBackgroundBlurred ?? themeIsBackgroundBlurred;
const customBackgroundValue = useCustomBackground(theme, resolvedCustomBackground);
const backgroundClassNames = buildClassName(
styles.background,
backgroundStyles.background,
resolvedCustomBackground && backgroundStyles.customBgImage,
resolvedBackgroundColor && backgroundStyles.customBgColor,
resolvedCustomBackground && resolvedIsBackgroundBlurred && backgroundStyles.blurred,
backgroundClassName,
);
return (
<div
className={buildClassName(styles.root, className)}
style={buildStyle(
resolvedPatternColor && `--pattern-color: ${resolvedPatternColor}`,
resolvedBackgroundColor && `--theme-background-color: ${resolvedBackgroundColor}`,
style,
)}
>
<div
className={backgroundClassNames}
style={buildStyle(
customBackgroundValue && `--custom-background: ${customBackgroundValue}`,
backgroundStyle,
)}
/>
<div className={buildClassName(styles.content, contentClassName)}>
{children}
</div>
</div>
);
};
const PreviewBlockMessage: FC<MessageProps> = ({
children,
className,
style,
bubbleClassName,
bubbleStyle,
headerClassName,
bodyClassName,
footerClassName,
avatar,
sender,
badge,
footer,
time,
senderColor,
backgroundColor,
}) => {
const hasAvatar = avatar !== undefined;
const hasSender = sender !== undefined;
const hasBadge = badge !== undefined;
const hasChildren = children !== undefined;
const hasFooterContent = footer !== undefined;
const hasTime = time !== undefined;
const hasHeader = hasSender || hasBadge;
const hasFooter = hasFooterContent || hasTime;
const bubbleStyles = buildStyle(
senderColor && `--preview-message-sender-color: ${senderColor}`,
backgroundColor && `--preview-message-background: ${backgroundColor}`,
bubbleStyle,
);
const content = (
<>
{hasHeader ? (
<div className={buildClassName(styles.header, headerClassName)}>
{hasSender ? <span className={styles.sender}>{sender}</span> : undefined}
<span className={styles.spacer} />
{hasBadge ? <span className={styles.badge}>{badge}</span> : undefined}
</div>
) : undefined}
{hasChildren ? (
<div className={buildClassName(styles.body, bodyClassName)}>
{children}
</div>
) : undefined}
{hasFooter ? (
<div className={buildClassName(styles.footer, footerClassName)}>
{hasFooterContent ? footer : <PreviewBlockMessageTime>{time}</PreviewBlockMessageTime>}
</div>
) : undefined}
</>
);
if (hasAvatar) {
return (
<div
className={buildClassName(styles.messageWithAvatar, className)}
style={style}
>
<div className={styles.avatar}>{avatar}</div>
<div
className={buildClassName(styles.bubble, bubbleClassName)}
style={bubbleStyles}
>
{content}
</div>
</div>
);
}
return (
<div
className={buildClassName(styles.message, className, bubbleClassName)}
style={buildStyle(
senderColor && `--preview-message-sender-color: ${senderColor}`,
backgroundColor && `--preview-message-background: ${backgroundColor}`,
bubbleStyle,
style,
)}
>
{content}
</div>
);
};
const PreviewBlockMessageTime: FC<MessageTimeProps> = ({
children,
className,
style,
}) => (
<span className={buildClassName(styles.time, className)} style={style}>
{children}
</span>
);
const PreviewBlockMessageMemo = memo(PreviewBlockMessage) as PreviewBlockMessageComponent;
PreviewBlockMessageMemo.Time = memo(PreviewBlockMessageTime);
const PreviewBlock = memo(withGlobal<OwnProps>((global) => {
const theme = selectTheme(global);
const {
isBlurred: themeIsBackgroundBlurred,
background: themeCustomBackground,
backgroundColor: themeBackgroundColor,
patternColor: themePatternColor,
} = selectThemeValues(global, theme) || {};
return {
theme,
themeBackgroundColor,
themePatternColor,
themeCustomBackground,
themeIsBackgroundBlurred,
};
})(PreviewBlockBase)) as PreviewBlockComponent;
PreviewBlock.Message = PreviewBlockMessageMemo;
export default PreviewBlock;

View File

@ -19,6 +19,7 @@ import {
} from '../../global/selectors';
import { selectThreadMessagesCount } from '../../global/selectors/threads';
import buildClassName from '../../util/buildClassName';
import { hasRank } from './helpers/chatMember';
import { REM } from './helpers/mediaDimensions';
import renderText from './helpers/renderText';
@ -33,6 +34,7 @@ import Avatar from './Avatar';
import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle';
import Icon from './icons/Icon';
import RankBadge from './RankBadge';
import TopicIcon from './TopicIcon';
import TypingStatus from './TypingStatus';
@ -58,7 +60,8 @@ type BaseOwnProps = {
emojiStatusSize?: number;
noStatusOrTyping?: boolean;
noRtl?: boolean;
adminMember?: ApiChatMember;
chatMemberOriginId?: string;
chatMember?: ApiChatMember;
isSavedDialog?: boolean;
noAvatar?: boolean;
className?: string;
@ -118,7 +121,8 @@ const PrivateChatInfo = ({
isSavedMessages,
isSavedDialog,
areMessagesLoaded,
adminMember,
chatMember,
chatMemberOriginId,
ripple,
className,
storyViewerOrigin,
@ -237,10 +241,6 @@ const PrivateChatInfo = ({
);
}
const customTitle = adminMember
? adminMember.customTitle || oldLang(adminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin')
: undefined;
function renderNameTitle() {
if (isTopic) {
return (
@ -248,18 +248,27 @@ const PrivateChatInfo = ({
);
}
if (customTitle) {
if (chatMember && hasRank(chatMember)) {
return (
<div className="info-name-title">
<FullNameTitle
peer={user!}
peer={customPeer || user!}
noFake={noFake}
noVerified={noVerified}
withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
iconElement={iconElement}
onEmojiStatusClick={onEmojiStatusClick}
/>
{customTitle && <span className="custom-title">{customTitle}</span>}
<RankBadge
chatId={chatMemberOriginId!}
userId={chatMember.userId}
isAdmin={chatMember.isAdmin}
isOwner={chatMember.isOwner}
rank={chatMember.rank}
/>
</div>
);
}

View File

@ -0,0 +1,58 @@
import { memo } from '@teact';
import { getActions } from '../../global';
import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import { getPeerColorClass } from '../../hooks/usePeerColor';
import BadgeButton from './BadgeButton';
type OwnProps = {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
className?: string;
rank?: string;
isClickable?: boolean;
};
const OWNER_PEER_COLOR = 2;
const ADMIN_PEER_COLOR = 3;
const RankBadge = ({
chatId, className, userId, isAdmin, isOwner, rank, isClickable,
}: OwnProps) => {
const { openRankModal } = getActions();
const lang = useLang();
const hasCustomColor = isOwner || isAdmin;
const rankText = rank || (isOwner && lang('ChannelCreator')) || (isAdmin && lang('ChannelAdmin'));
const handleClick = useLastCallback(() => {
if (!chatId) return;
openRankModal({ chatId, userId, isAdmin, isOwner, rank });
});
if (!rankText) {
return undefined;
}
return (
<BadgeButton
className={buildClassName(
hasCustomColor && getPeerColorClass(isOwner ? OWNER_PEER_COLOR : ADMIN_PEER_COLOR),
className,
)}
isPlain={!hasCustomColor}
inline
onClick={isClickable ? handleClick : undefined}
>
{rankText}
</BadgeButton>
);
};
export default memo(RankBadge);

View File

@ -0,0 +1,5 @@
import type { ApiChatMember } from '../../../api/types';
export function hasRank(member?: ApiChatMember): boolean {
return Boolean(member && (member.rank || member.isOwner || member.isAdmin));
}

View File

@ -201,7 +201,7 @@ const ChatExtra = ({
return <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
}
return <Skeleton className={styles.businessLocation} />;
return <Skeleton className={styles.businessLocation} animation="wave" />;
}, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);

View File

@ -249,6 +249,17 @@ const PermissionCheckboxList = ({
onChange={handlePermissionChange}
/>
</div>
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
<Checkbox
name="editRank"
checked={!permissions.editRank}
label={lang('UserRestrictionsEditRank')}
blocking
permissionGroup={permissionGroup}
onChange={handlePermissionChange}
disabled={getControlIsDisabled && getControlIsDisabled('editRank')}
/>
</div>
<div
className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}

View File

@ -208,6 +208,7 @@ const MessageListAccountInfo: FC<OwnProps & StateProps> = ({
width={width}
height={height}
forceAspectRatio
animation="pulse"
/>
)}
{isVerifyCodes && (

View File

@ -57,7 +57,7 @@ const Game: FC<OwnProps> = ({
onClick={handleGameClick}
>
{!photoBlobUrl && !videoBlobUrl && (
<Skeleton className="skeleton preview-content" />
<Skeleton className="skeleton preview-content" animation="pulse" />
)}
{photoBlobUrl && (
<img

View File

@ -116,6 +116,7 @@ const Invoice: FC<OwnProps> = ({
width={width}
height={photo.dimensions?.height}
forceAspectRatio
animation="pulse"
/>
)}
</div>

View File

@ -24,7 +24,7 @@ function LastEditTimeMenuItem({
return (
<MenuItem icon="clock-edit" className={styles.item}>
{shouldRenderSkeleton ? <Skeleton className={styles.skeleton} /> : Boolean(editDate)
{shouldRenderSkeleton ? <Skeleton className={styles.skeleton} animation="wave" /> : Boolean(editDate)
&& lang('Chat.PrivateMessageEditTimestamp.Date', formatDateAtTime(lang, editDate * 1000))}
</MenuItem>
);

View File

@ -184,7 +184,7 @@ const Location: FC<OwnProps> = ({
}
function renderMap() {
if (!mapBlobUrl) return <Skeleton width={width} height={height} />;
if (!mapBlobUrl) return <Skeleton width={width} height={height} animation="pulse" />;
return (
<img
className="full-media map"

View File

@ -174,6 +174,7 @@ import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon';
import MessageText from '../../common/MessageText';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import RankBadge from '../../common/RankBadge';
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import Sparkles from '../../common/Sparkles';
import TopicChip from '../../common/TopicChip';
@ -302,7 +303,7 @@ type StateProps = {
isTranscribing?: boolean;
transcribedText?: string;
isPremium: boolean;
senderAdminMember?: ApiChatMember;
senderChatMember?: ApiChatMember;
messageTopic?: ApiTopic;
hasTopicChip?: boolean;
chatTranslations?: ChatTranslatedMessages;
@ -429,7 +430,7 @@ const Message = ({
repliesThreadInfo,
hasUnreadReaction,
memoFirstUnreadIdRef,
senderAdminMember,
senderChatMember,
messageTopic,
hasTopicChip,
chatTranslations,
@ -542,7 +543,7 @@ const Message = ({
const {
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
isTypingDraft,
isTypingDraft, fromRank,
} = message;
const hasSummary = Boolean(message.summaryLanguageCode);
@ -1677,7 +1678,6 @@ const Message = ({
const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium;
const shouldRenderForwardAvatar = asForwarded && senderPeer;
const hasBotSenderUsername = botSender?.hasUsername;
return (
<div className="message-title" dir="ltr">
{(senderTitle || asForwarded) ? (
@ -1733,16 +1733,20 @@ const Message = ({
</span>
)}
<div className="title-spacer" />
{!shouldSkipRenderAdminTitle && !hasBotSenderUsername ? (forwardInfo?.isLinkedChannelPost ? (
{(!shouldSkipRenderAdminTitle && !signature) ? (forwardInfo?.isLinkedChannelPost ? (
<span className="admin-title" dir="auto">{oldLang('DiscussChannel')}</span>
) : message.postAuthorTitle && isGroup && !asForwarded ? (
<span className="admin-title" dir="auto">{message.postAuthorTitle}</span>
) : senderAdminMember && !asForwarded && !viaBotId ? (
<span className="admin-title" dir="auto">
{senderAdminMember.customTitle || oldLang(
senderAdminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin',
)}
</span>
) : (senderChatMember || fromRank) && !asForwarded ? (
<RankBadge
chatId={chatId}
userId={(senderChatMember?.userId || sender?.id)!}
isAdmin={senderChatMember?.isAdmin}
isOwner={senderChatMember?.isOwner}
rank={senderChatMember?.rank || fromRank}
className="admin-title-badge"
isClickable
/>
) : undefined) : undefined}
{canShowSenderBoosts && (
<span className="sender-boosts" aria-hidden>
@ -2020,6 +2024,7 @@ export default memo(withGlobal<OwnProps>(
const isChannel = chat && isChatChannel(chat);
const isGroup = chat && isChatGroup(chat);
const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined;
const { adminMembersById, members, boostsApplied } = chatFullInfo || {};
const webPageStoryData = webPage?.story;
const webPageStory = webPageStoryData
? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id)
@ -2031,8 +2036,8 @@ export default memo(withGlobal<OwnProps>(
const sender = selectSender(global, message);
const originSender = selectForwardedSender(global, message);
const botSender = viaBotId ? selectUser(global, viaBotId) : undefined;
const senderAdminMember = sender?.id && isGroup
? chatFullInfo?.adminMembersById?.[sender?.id]
const senderChatMember = sender?.id
? (adminMembersById?.[sender?.id] || members?.find((member) => member.userId === sender?.id))
: undefined;
const isThreadTop = message.id === threadId;
@ -2119,7 +2124,7 @@ export default memo(withGlobal<OwnProps>(
const isPremium = selectIsCurrentUserPremium(global);
const senderBoosts = sender && selectIsChatWithSelf(global, sender.id)
? (chatFullInfo?.boostsApplied ?? message.senderBoosts) : message.senderBoosts;
? (boostsApplied ?? message.senderBoosts) : message.senderBoosts;
const chatLevel = chat?.boostLevel || 0;
const transcribeMinLevel = global.appConfig.groupTranscribeLevelMin;
@ -2202,7 +2207,7 @@ export default memo(withGlobal<OwnProps>(
isTranscribing: transcriptionId !== undefined && global.transcriptions[transcriptionId]?.isPending,
transcribedText: transcriptionId !== undefined ? global.transcriptions[transcriptionId]?.text : undefined,
isPremium,
senderAdminMember,
senderChatMember,
messageTopic,
hasTopicChip,
chatTranslations,

View File

@ -462,8 +462,8 @@ const MessageContextMenu: FC<OwnProps> = ({
<MenuSeparator size="thick" />
{!customEmojiSets && (
<>
<Skeleton inline className="menu-loading-row" />
<Skeleton inline className="menu-loading-row" />
<Skeleton inline className="menu-loading-row" animation="wave" />
<Skeleton inline className="menu-loading-row" animation="wave" />
</>
)}
{customEmojiSets && customEmojiSets.length === 1 && (

View File

@ -36,7 +36,7 @@ function ReadTimeMenuItem({
return (
<MenuItem icon="message-read" className={styles.item}>
<Transition name="fade" activeKey={shouldRenderSkeleton ? 1 : 2} className={styles.transition}>
{shouldRenderSkeleton ? <Skeleton className={styles.skeleton} /> : (
{shouldRenderSkeleton ? <Skeleton className={styles.skeleton} animation="wave" /> : (
<>
{Boolean(readDate) && lang('PmReadAt', formatDateAtTime(lang, readDate * 1000))}
{!readDate && shouldRenderShowWhen && (

View File

@ -154,7 +154,7 @@ const SimilarChannels = ({
return (
<div className={buildClassName(styles.root)}>
{shouldRenderSkeleton && <Skeleton className={styles.skeleton} />}
{shouldRenderSkeleton && <Skeleton className={styles.skeleton} animation="wave" />}
{shouldRenderChannels && (
<div
className={buildClassName(

View File

@ -303,7 +303,6 @@
user-select: none;
unicode-bidi: plaintext;
overflow: hidden;
display: flex;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
@ -399,11 +398,12 @@
flex-grow: 1;
}
.admin-title {
.admin-title-badge, .admin-title {
user-select: none;
margin-left: 1rem;
}
.admin-title {
font-size: 0.75rem;
font-weight: var(--font-weight-normal);
color: rgba(var(--color-text-meta-rgb), 0.75);
@ -414,6 +414,10 @@
}
}
.admin-title-badge {
margin-right: -0.1875rem;
}
.sender-boosts {
user-select: none;
@ -540,23 +544,6 @@
}
}
&.voice,
&.audio,
&.document {
.text-content {
margin-top: 0.25rem;
}
}
&.voice,
&.audio {
.message-title,
.Embedded {
margin-top: -0.1875rem;
margin-bottom: 0.1875rem;
}
}
&.audio {
min-width: 20rem;

View File

@ -58,6 +58,8 @@ import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
import QuickChatPickerModal from './quickChatPicker/QuickChatPickerModal.async';
import QuickPreviewModal from './quickPreview/QuickPreviewModal.async';
import EditRankModal from './rank/EditRankModal.async';
import RankModal from './rank/RankModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import ReportModal from './reportModal/ReportModal.async';
import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async';
@ -144,7 +146,9 @@ type ModalKey = keyof Pick<TabState,
'leaveGroupModal' |
'isTwoFaCheckModalOpen' |
'isQuickChatPickerOpen' |
'isCocoonModalOpen'
'isCocoonModalOpen' |
'editRankModal' |
'rankModal'
>;
type StateProps = {
@ -228,6 +232,8 @@ const MODALS: ModalRegistry = {
isTwoFaCheckModalOpen: TwoFaCheckModal,
isQuickChatPickerOpen: QuickChatPickerModal,
isCocoonModalOpen: CocoonModal,
editRankModal: EditRankModal,
rankModal: RankModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './EditRankModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const EditRankModalAsync = (props: OwnProps) => {
const { modal } = props;
const EditRankModal = useModuleLoader(Bundles.Extra, 'EditRankModal', !modal);
return EditRankModal ? <EditRankModal {...props} /> : undefined;
};
export default EditRankModalAsync;

View File

@ -0,0 +1,31 @@
.content {
display: flex;
flex-direction: column;
}
.previewMessage {
width: 100%;
}
.previewBlock {
margin-bottom: 1.5rem;
}
.previewRankBadge {
max-width: 10rem;
font-size: 0.75rem;
}
.previewLine {
display: flex;
min-width: 0;
}
.input {
margin-bottom: 0.25rem;
}
.description {
font-size: 0.875rem;
color: var(--color-text-secondary);
}

View File

@ -0,0 +1,177 @@
import { memo, useEffect, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { getUserFirstOrLastName } from '../../../global/helpers';
import { getPeerTitle } from '../../../global/helpers/peers';
import { selectCanEditRank, selectChat, selectUser } from '../../../global/selectors';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import PreviewBlock from '../../common/PreviewBlock';
import RankBadge from '../../common/RankBadge';
import Button from '../../ui/Button';
import InputText from '../../ui/InputText';
import Modal from '../../ui/Modal';
import Skeleton from '../../ui/placeholder/Skeleton';
import styles from './EditRankModal.module.scss';
export type OwnProps = {
modal: TabState['editRankModal'];
};
type StateProps = {
user?: ApiUser;
chat?: ApiChat;
canEditRank?: boolean;
isOwn?: boolean;
};
const MAX_RANK_LENGTH = 16;
const PREVIEW_TIME = '9:37';
const EditRankModal = ({
modal, user, chat, canEditRank, isOwn,
}: OwnProps & StateProps) => {
const { closeEditRankModal, editChatParticipantRank } = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const [rank, setRank] = useState('');
useEffect(() => {
if (!isOpen) return;
setRank((renderingModal?.rank || '').slice(0, MAX_RANK_LENGTH));
}, [renderingModal?.chatId, renderingModal?.rank, renderingModal?.userId, isOpen]);
const handleClose = useLastCallback(() => {
closeEditRankModal();
});
const handleInputChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setRank(e.target.value);
});
const {
userId, chatId, isAdmin, isOwner,
} = renderingModal || {};
const initialRank = (renderingModal?.rank || '').slice(0, MAX_RANK_LENGTH);
const shouldShowBadge = Boolean(rank || isAdmin || isOwner);
const isSubmitDisabled = !canEditRank || rank === initialRank;
const handleSubmit = useLastCallback(() => {
if (isSubmitDisabled) return;
if (!chatId || !userId) return;
editChatParticipantRank({
chatId,
userId,
rank,
});
handleClose();
});
const handleKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') return;
e.preventDefault();
handleSubmit();
});
if (!chatId || !userId) return undefined;
return (
<Modal
isOpen={isOpen}
title={lang(isOwn ? 'RankModalEditMy' : 'RankModalEdit')}
contentClassName={styles.content}
isSlim
hasCloseButton
onClose={handleClose}
>
<PreviewBlock className={styles.previewBlock} contentClassName={styles.previewBlockContent}>
<PreviewBlock.Message
className={styles.previewMessage}
avatar={user ? <Avatar peer={user} size="small" /> : undefined}
sender={user && getPeerTitle(lang, user)}
badge={shouldShowBadge ? (
<RankBadge
chatId={chatId}
userId={userId}
className={styles.previewRankBadge}
isAdmin={isAdmin}
isOwner={isOwner}
rank={rank || undefined}
/>
) : undefined}
time={PREVIEW_TIME}
>
{renderPreviewContent()}
</PreviewBlock.Message>
</PreviewBlock>
<InputText
id="edit-rank"
label={lang('EditAdminRank')}
className={styles.input}
value={rank}
maxLength={MAX_RANK_LENGTH}
disabled={!chat || !user || !canEditRank}
teactExperimentControlled
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<p className={styles.description}>
{isOwn ? lang('RankEditTextOwn') : lang('RankEditText', {
user: user && getUserFirstOrLastName(user),
})}
</p>
<Button
disabled={isSubmitDisabled}
onClick={handleSubmit}
>
{lang('RankEditSave')}
</Button>
</Modal>
);
};
function renderPreviewContent() {
return (
<>
<span className={styles.previewLine}>
<Skeleton variant="rounded-rect" height={10} inline />
</span>
<span className={styles.previewLine}>
<Skeleton variant="rounded-rect" width={128} height={10} inline />
</span>
</>
);
}
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
return {
user: modal?.userId ? selectUser(global, modal.userId) : undefined,
chat: modal?.chatId ? selectChat(global, modal.chatId) : undefined,
canEditRank: modal && selectCanEditRank(global, {
chatId: modal.chatId,
userId: modal.userId,
isAdmin: modal.isAdmin,
isOwner: modal.isOwner,
}),
isOwn: modal && modal.userId === global.currentUserId,
};
},
)(EditRankModal));

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './RankModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const RankModalAsync = (props: OwnProps) => {
const { modal } = props;
const RankModal = useModuleLoader(Bundles.Extra, 'RankModal', !modal);
return RankModal ? <RankModal {...props} /> : undefined;
};
export default RankModalAsync;

View File

@ -0,0 +1,102 @@
@use "../../../styles/mixins";
.root {
:global(.modal-dialog) {
overflow: hidden;
width: 26.25rem;
}
}
.content {
display: flex;
flex-direction: column;
padding: 0 1rem 1rem !important;
}
.body {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
padding-top: 2rem;
text-align: center;
}
.previewGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.previewBlockContent {
justify-content: flex-end;
}
.previewMessage {
--preview-crop-left: 1.5rem;
--preview-crop-fade-width: 1.5rem;
@include mixins.gradient-border-left(
calc(var(--preview-crop-left) + var(--preview-crop-fade-width)),
0.75rem
);
width: calc(100% + var(--preview-crop-left));
max-width: none;
margin-top: auto;
margin-left: calc(var(--preview-crop-left) * -1);
}
.previewLine,
.previewMetaLine {
display: flex;
min-width: 0;
}
.previewMetaLine {
gap: 0.375rem;
align-items: flex-end;
justify-content: space-between;
}
.memberTag {
padding: 0.25rem 0.625rem;
border-radius: 999px;
font-size: 0.75rem;
line-height: 1;
color: rgba(var(--color-text-meta-rgb), 0.8);
background: rgb(var(--color-text-meta-rgb) / 12%);
}
.previewRankBadge {
font-size: 0.75rem;
}
.footer {
display: flex;
align-self: stretch;
margin-top: 1rem;
}
.understoodIcon {
font-size: 1.1875rem;
}
.topIcon {
display: grid;
flex-shrink: 0;
place-items: center;
width: 6rem;
height: 6rem;
border-radius: 50%;
font-size: 4rem;
color: var(--color-white);
background: var(--color-primary);
}

View File

@ -0,0 +1,190 @@
import { memo, useMemo } from '@teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatMember, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { getPeerTitle } from '../../../global/helpers/peers';
import {
selectCanEditOwnRank, selectCanEditRank, selectChat, selectChatFullInfo, selectUser,
} from '../../../global/selectors';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import PreviewBlock from '../../common/PreviewBlock';
import RankBadge from '../../common/RankBadge';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import Skeleton from '../../ui/placeholder/Skeleton';
import styles from './RankModal.module.scss';
export type OwnProps = {
modal: TabState['rankModal'];
};
type StateProps = {
user?: ApiUser;
chat?: ApiChat;
canEditRank?: boolean;
canEditOwnRank?: boolean;
currentUserMember?: ApiChatMember;
};
const PLACEHOLDER_ID = 'placeholder';
const RankModal = ({
modal, user, chat, canEditRank, canEditOwnRank, currentUserMember,
}: OwnProps & StateProps) => {
const { closeRankModal, openEditRankModal } = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const isOwn = renderingModal?.userId === currentUserMember?.userId;
const handleClose = useLastCallback(() => {
closeRankModal();
});
const handleActionClick = useLastCallback(() => {
if (!renderingModal) return;
if (canEditRank) {
openEditRankModal(renderingModal);
} else if (canEditOwnRank && currentUserMember) {
openEditRankModal({
chatId: renderingModal.chatId,
userId: currentUserMember.userId,
isAdmin: currentUserMember.isAdmin,
isOwner: currentUserMember.isOwner,
rank: currentUserMember.rank,
});
}
closeRankModal();
});
const mainButtonText = useMemo(() => {
if (canEditRank) return isOwn ? lang('RankModalEditMy') : lang('RankModalEdit');
if (canEditOwnRank) return lang('RankModalEditMy');
return lang('ButtonUnderstood');
}, [canEditRank, canEditOwnRank, isOwn, lang]);
if (!renderingModal) return undefined;
const { userId, chatId, isAdmin, isOwner, rank } = renderingModal;
return (
<Modal
isOpen={isOpen}
className={styles.root}
contentClassName={styles.content}
hasAbsoluteCloseButton
onClose={handleClose}
>
<div className={styles.body}>
<div className={styles.topIcon}>
<Icon name="user-tag" />
</div>
<div>
<h3 className={styles.previewTitle}>
{lang(
isOwner ? 'RankModalOwnerTagTitle' : isAdmin ? 'RankModalAdminTagTitle' : 'RankModalMemberTagTitle',
)}
</h3>
<div className={styles.previewText}>
{lang(
isOwner ? 'RankModalOwnerText' : isAdmin ? 'RankModalAdminText' : 'RankModalMemberText',
{
tag: <RankBadge chatId={chatId} userId={userId} isAdmin={isAdmin} isOwner={isOwner} rank={rank} />,
author: user && getPeerTitle(lang, user),
group: chat && getPeerTitle(lang, chat),
},
{ withNodes: true, withMarkdown: true },
)}
</div>
</div>
<div className={styles.previewGrid}>
<PreviewBlock contentClassName={styles.previewBlockContent}>
<PreviewBlock.Message
className={styles.previewMessage}
badge={(
<RankBadge
chatId={PLACEHOLDER_ID}
userId={PLACEHOLDER_ID}
className={styles.previewRankBadge}
rank={lang('RankMemberTag')}
/>
)}
>
{renderPreviewContent()}
</PreviewBlock.Message>
</PreviewBlock>
<PreviewBlock contentClassName={styles.previewBlockContent}>
<PreviewBlock.Message
className={styles.previewMessage}
badge={(
<RankBadge
className={styles.previewRankBadge}
chatId={PLACEHOLDER_ID}
userId={PLACEHOLDER_ID}
isAdmin
rank={lang('RankAdminTag')}
/>
)}
>
{renderPreviewContent()}
</PreviewBlock.Message>
</PreviewBlock>
</div>
<div className={styles.footer}>
<Button
iconName={!canEditRank ? 'understood' : undefined}
iconClassName={styles.understoodIcon}
onClick={handleActionClick}
>
{mainButtonText}
</Button>
</div>
</div>
</Modal>
);
};
function renderPreviewContent() {
return (
<>
<span className={styles.previewLine}>
<Skeleton variant="rounded-rect" width={168} height={10} inline />
</span>
<span className={styles.previewMetaLine}>
<Skeleton variant="rounded-rect" width={104} height={10} inline />
<PreviewBlock.Message.Time>9:37</PreviewBlock.Message.Time>
</span>
</>
);
}
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const chatFullInfo = modal?.chatId ? selectChatFullInfo(global, modal.chatId) : undefined;
const currentUserMember = chatFullInfo?.adminMembersById?.[global.currentUserId!]
|| chatFullInfo?.members?.find((member) => member.userId === global.currentUserId);
return {
user: modal?.userId ? selectUser(global, modal.userId) : undefined,
chat: modal?.chatId ? selectChat(global, modal.chatId) : undefined,
canEditRank: modal && selectCanEditRank(global, {
chatId: modal.chatId,
userId: modal.userId,
isAdmin: modal.isAdmin,
isOwner: modal.isOwner,
}),
canEditOwnRank: modal && selectCanEditOwnRank(global, modal.chatId),
currentUserMember,
};
},
)(RankModal));

View File

@ -188,6 +188,7 @@ const Checkout: FC<OwnProps> = ({
width={photo.dimensions?.width}
height={photo.dimensions?.height}
className={styles.checkoutPicture}
animation="pulse"
forceAspectRatio
/>
)}

View File

@ -41,6 +41,7 @@ import {
import { getSavedGiftKey } from '../../global/helpers/stars';
import {
selectActiveDownloads,
selectCanEditRank,
selectCanUpdateMainTab,
selectChat,
selectChatFullInfo,
@ -74,6 +75,7 @@ import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
import { isUserId } from '../../util/entities/ids';
import { buildCollectionByKey } from '../../util/iteratees.ts';
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
import renderText from '../common/helpers/renderText';
@ -301,6 +303,7 @@ const Profile = ({
resetSelectedStoryAlbum,
changeProfileTab,
setMainProfileTab,
openEditRankModal,
} = getActions();
const containerRef = useRef<HTMLDivElement>();
@ -481,6 +484,10 @@ const Profile = ({
const { startViewTransition } = useViewTransition();
const { createVtnStyle } = useVtn();
const membersById = useMemo(() => {
return members && buildCollectionByKey(members, 'userId');
}, [members]);
const giftIds = useMemo(() => renderingGifts?.map((gift) => getSavedGiftKey(gift)), [renderingGifts]);
const activeTabIndex = useMemo(() => {
@ -724,13 +731,48 @@ const Profile = ({
activeTabIndex, activeCollectionId, selectedStoryAlbumId], renderingDelay);
function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined {
return memberId === currentUserId || !canDeleteMembers ? undefined : [{
title: oldLang('lng_context_remove_from_group'),
icon: 'stop',
handler: () => {
setDeletingUserId(memberId);
},
}];
const global = getGlobal();
const member = adminMembersById?.[memberId] || membersById?.[memberId];
const canEditRank = member && selectCanEditRank(global, {
chatId,
userId: memberId,
isAdmin: member?.isAdmin,
isOwner: member?.isOwner,
});
const actions: MenuItemContextAction[] = [];
if (memberId !== currentUserId && canDeleteMembers) {
actions.push({
title: oldLang('lng_context_remove_from_group'),
icon: 'stop',
handler: () => {
setDeletingUserId(memberId);
},
});
}
if (canEditRank) {
actions.push({
title: lang('MemberContextEditRank'),
icon: 'tag',
handler: () => {
openEditRankModal({
chatId,
userId: memberId,
isAdmin: member?.isAdmin,
isOwner: member?.isOwner,
rank: member?.rank,
});
},
});
}
if (actions.length === 0) {
return undefined;
}
return actions;
}
function renderNothingFoundGiftsWithFilter() {
@ -978,8 +1020,15 @@ const Profile = ({
onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)}
withPortalForMenu
>
<PrivateChatInfo userId={id} adminMember={adminMembersById?.[id]} forceShowSelf withStory />
<PrivateChatInfo
userId={id}
chatMemberOriginId={chatId}
chatMember={adminMembersById?.[id] || membersById?.[id]}
forceShowSelf
withStory
/>
</ListItem>
))
) : resultType === 'commonChats' ? (

View File

@ -1,5 +1,5 @@
import {
memo, useCallback, useEffect, useMemo, useState,
memo, useEffect, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
@ -9,7 +9,7 @@ import type {
import { ManagementScreens } from '../../../types';
import { getUserFullName, isChatBasicGroup, isChatChannel, isUserBot } from '../../../global/helpers';
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
import { selectCanEditRank, selectChat, selectChatFullInfo } from '../../../global/selectors';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -37,11 +37,12 @@ type OwnProps = {
type StateProps = {
chat: ApiChat;
usersById: Record<string, ApiUser>;
adminMembersById?: Record<string, ApiChatMember>;
selectedAdminMember?: ApiChatMember;
hasFullInfo: boolean;
currentUserId?: string;
isFormFullyDisabled: boolean;
defaultRights?: ApiChatAdminRights;
canEditRank?: boolean;
};
const CUSTOM_TITLE_MAX_LENGTH = 16;
@ -54,9 +55,10 @@ const ManageGroupAdminRights = ({
chat,
usersById,
currentUserId,
adminMembersById,
selectedAdminMember,
hasFullInfo,
isFormFullyDisabled,
canEditRank,
onClose,
onScreenSelect,
}: OwnProps & StateProps) => {
@ -71,7 +73,7 @@ const ManageGroupAdminRights = ({
const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag();
const [isTransferDialogOpen, openTransferDialog, closeTransferDialog] = useFlag();
const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag();
const [customTitle, setCustomTitle] = useState('');
const [rank, setRank] = useState('');
const lang = useLang();
const isChannel = isChatChannel(chat);
@ -83,9 +85,7 @@ const ManageGroupAdminRights = ({
onBack: onClose,
});
const selectedChatMember = useMemo(() => {
const selectedAdminMember = selectedUserId ? adminMembersById?.[selectedUserId] : undefined;
const selectedChatMember: ApiChatMember | undefined = useMemo(() => {
// If `selectedAdminMember` variable is filled with a value, then we have already saved the administrator,
// so now we need to return to the list of administrators
if (isNewAdmin && (selectedAdminMember || !selectedUserId)) {
@ -98,14 +98,14 @@ const ManageGroupAdminRights = ({
return user ? {
userId: user.id,
adminRights: defaultRights,
customTitle: lang('ChannelAdmin'),
isOwner: false,
rank: lang('ChannelAdmin'),
isOwner: undefined,
promotedByUserId: undefined,
} : undefined;
}
return selectedAdminMember;
}, [adminMembersById, defaultRights, isNewAdmin, lang, selectedUserId]);
}, [selectedAdminMember, defaultRights, isNewAdmin, lang, selectedUserId]);
useEffect(() => {
if (hasFullInfo && selectedUserId && !selectedChatMember) {
@ -115,12 +115,12 @@ const ManageGroupAdminRights = ({
useEffect(() => {
setPermissions(selectedChatMember?.adminRights || {});
setCustomTitle((selectedChatMember?.customTitle || '').substr(0, CUSTOM_TITLE_MAX_LENGTH));
setRank((selectedChatMember?.rank || '').slice(0, CUSTOM_TITLE_MAX_LENGTH));
setIsTouched(Boolean(isNewAdmin));
setIsLoading(false);
}, [defaultRights, isNewAdmin, selectedChatMember]);
const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const handlePermissionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
function getUpdatedPermissionValue(value: true | undefined) {
@ -132,23 +132,24 @@ const ManageGroupAdminRights = ({
[name]: getUpdatedPermissionValue(p[name as keyof ApiChatAdminRights]),
}));
setIsTouched(true);
}, []);
});
const handleSavePermissions = useCallback(() => {
const handleSavePermissions = useLastCallback(() => {
if (!selectedUserId) {
return;
}
const hasRankChanged = rank !== selectedAdminMember?.rank;
setIsLoading(true);
updateChatAdmin({
chatId: chat.id,
userId: selectedUserId,
adminRights: permissions,
customTitle,
rank: hasRankChanged ? rank : undefined,
});
}, [selectedUserId, updateChatAdmin, chat.id, permissions, customTitle]);
});
const handleDismissAdmin = useCallback(() => {
const handleDismissAdmin = useLastCallback(() => {
if (!selectedUserId) {
return;
}
@ -159,9 +160,9 @@ const ManageGroupAdminRights = ({
adminRights: {},
});
closeDismissConfirmationDialog();
}, [chat.id, closeDismissConfirmationDialog, selectedUserId, updateChatAdmin]);
});
const getControlIsDisabled = useCallback((key: keyof ApiChatAdminRights) => {
const getControlIsDisabled = useLastCallback((key: keyof ApiChatAdminRights) => {
if (isChatBasicGroup(chat)) {
return false;
}
@ -175,7 +176,7 @@ const ManageGroupAdminRights = ({
}
return !chat.adminRights[key];
}, [chat, isFormFullyDisabled]);
});
const memberStatus = useMemo(() => {
if (isNewAdmin || !selectedChatMember) {
@ -197,11 +198,11 @@ const ManageGroupAdminRights = ({
return lang('ChannelAdmin');
}, [isNewAdmin, selectedChatMember, usersById, lang]);
const handleCustomTitleChange = useCallback((e) => {
const handleRankChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setCustomTitle(value);
setRank(value);
setIsTouched(true);
}, []);
});
const handleStartTransfer = useLastCallback(() => {
if (!selectedUserId) return;
@ -373,6 +374,16 @@ const ManageGroupAdminRights = ({
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem">
<Checkbox
name="editRank"
checked={Boolean(permissions.manageRanks)}
label={lang('EditAdminEditRank')}
blocking
disabled={getControlIsDisabled('manageRanks')}
onChange={handlePermissionChange}
/>
</div>
{!isChannel && (
<div className="ListItem">
<Checkbox
@ -441,9 +452,9 @@ const ManageGroupAdminRights = ({
id="admin-title"
label={lang('EditAdminRank')}
className="input-admin-title"
onChange={handleCustomTitleChange}
value={customTitle}
disabled={isFormFullyDisabled}
onChange={handleRankChange}
value={rank}
disabled={isFormFullyDisabled || !canEditRank}
maxLength={CUSTOM_TITLE_MAX_LENGTH}
/>
)}
@ -503,12 +514,21 @@ const ManageGroupAdminRights = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, isPromotedByCurrentUser }): Complete<StateProps> => {
(global, { chatId, isPromotedByCurrentUser, selectedUserId }): Complete<StateProps> => {
const chat = selectChat(global, chatId)!;
const fullInfo = selectChatFullInfo(global, chatId);
const { byId: usersById } = global.users;
const { currentUserId } = global;
const isFormFullyDisabled = !(chat.isCreator || isPromotedByCurrentUser);
const adminMembersById = fullInfo?.adminMembersById;
const selectedAdminMember = selectedUserId ? adminMembersById?.[selectedUserId] : undefined;
const canEditRank = selectedAdminMember && selectCanEditRank(global, {
chatId,
userId: selectedAdminMember.userId,
isAdmin: selectedAdminMember.isAdmin,
isOwner: selectedAdminMember.isOwner,
});
return {
chat,
@ -517,7 +537,8 @@ export default memo(withGlobal<OwnProps>(
isFormFullyDisabled,
defaultRights: chat.adminRights,
hasFullInfo: Boolean(fullInfo),
adminMembersById: fullInfo?.adminMembersById,
selectedAdminMember,
canEditRank,
};
},
(global, { chatId }) => {

View File

@ -808,7 +808,7 @@ function Story({
/>
)}
{shouldRenderSkeleton && (
<Skeleton className={buildClassName(skeletonTransitionClassNames, styles.fullSize)} />
<Skeleton className={buildClassName(skeletonTransitionClassNames, styles.fullSize)} animation="pulse" />
)}
{!isVideo && fullMediaData && (
<img

View File

@ -45,7 +45,7 @@ const InputText = ({
disabled,
readOnly,
placeholder,
autoComplete,
autoComplete = 'off',
inputMode,
maxLength,
tabIndex,

View File

@ -245,6 +245,7 @@
.info-name-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.info-row,

View File

@ -8,7 +8,7 @@ import './Skeleton.scss';
type OwnProps = {
variant?: 'rectangular' | 'rounded-rect' | 'round';
animation?: 'wave' | 'pulse';
animation?: 'wave' | 'pulse' | 'none';
width?: number;
height?: number;
forceAspectRatio?: boolean;
@ -18,7 +18,7 @@ type OwnProps = {
const Skeleton: FC<OwnProps> = ({
variant = 'rectangular',
animation = 'wave',
animation = 'none',
width,
height,
forceAspectRatio,

View File

@ -85,6 +85,7 @@ import {
updateChatFullInfo,
updateChatLastMessageId,
updateChatListSecondaryInfo,
updateChatParticipantRank,
updateChats,
updateChatsLastMessageId,
updateListedTopicIds,
@ -2081,7 +2082,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
if (selectIsCurrentUserFrozen(global)) return;
const {
chatId, userId, adminRights, customTitle,
chatId, userId, adminRights, rank,
tabId = getCurrentTabId(),
} = payload;
@ -2095,7 +2096,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
if (!chat) return;
await callApi('updateChatAdmin', {
chat, user, adminRights, customTitle,
chat, user, adminRights, rank,
});
const chatAfterUpdate = await callApi('fetchFullChat', chat);
@ -2116,7 +2117,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
[userId]: {
...adminMembersById[userId],
adminRights,
customTitle,
rank,
},
};
}
@ -2129,6 +2130,31 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
}
});
addActionHandler('editChatParticipantRank', async (global, actions, payload): Promise<void> => {
if (selectIsCurrentUserFrozen(global)) {
return;
}
const {
chatId, userId, rank,
} = payload;
const chat = selectChat(global, chatId);
const peer = selectPeer(global, userId);
if (!chat || !peer) {
return;
}
const isUpdated = await callApi('editChatParticipantRank', { chat, peer, rank });
if (!isUpdated) {
return;
}
global = getGlobal();
global = updateChatParticipantRank(global, chat.id, userId, rank);
setGlobal(global);
});
addActionHandler('updateChat', async (global, actions, payload): Promise<void> => {
const {
chatId, title, about, photo, tabId = getCurrentTabId(),

View File

@ -23,6 +23,7 @@ import {
updateChat,
updateChatFullInfo,
updateChatListType,
updateChatParticipantRank,
updatePeerStoriesHidden,
updateTopic,
} from '../../reducers';
@ -444,6 +445,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
return undefined;
}
case 'updateChatParticipantRank': {
return updateChatParticipantRank(global, update.id, update.userId, update.rank);
}
case 'draftMessage': {
const {
chatId, threadId, draft,

View File

@ -77,3 +77,23 @@ addActionHandler('openProfileRatingModal', (global, actions, payload): ActionRet
});
addTabStateResetterAction('closeProfileRatingModal', 'profileRatingModal');
addActionHandler('openRankModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), ...rest } = payload;
return updateTabState(global, {
rankModal: rest,
}, tabId);
});
addTabStateResetterAction('closeRankModal', 'rankModal');
addActionHandler('openEditRankModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), ...rest } = payload;
return updateTabState(global, {
editRankModal: rest,
}, tabId);
});
addTabStateResetterAction('closeEditRankModal', 'editRankModal');

View File

@ -442,6 +442,55 @@ export function addChatMembers<T extends GlobalState>(global: T, chat: ApiChat,
});
}
export function updateChatParticipantRank<T extends GlobalState>(
global: T, chatId: string, userId: string, rank: string,
): T {
const fullInfo = selectChatFullInfo(global, chatId);
if (!fullInfo) {
return global;
}
const { adminMembersById, members } = fullInfo;
let fullInfoUpdate: Partial<ApiChatFullInfo> | undefined;
const currentAdminMember = adminMembersById?.[userId];
if (currentAdminMember && currentAdminMember.rank !== rank) {
fullInfoUpdate = {
...fullInfoUpdate,
adminMembersById: {
...adminMembersById,
[userId]: {
...currentAdminMember,
rank,
},
},
};
}
let shouldUpdateMembers = false;
const updatedMembers = members?.map((member) => {
if (member.userId !== userId || member.rank === rank) {
return member;
}
shouldUpdateMembers = true;
return {
...member,
rank,
};
});
if (shouldUpdateMembers) {
fullInfoUpdate = {
...fullInfoUpdate,
members: updatedMembers,
};
}
return fullInfoUpdate ? updateChatFullInfo(global, chatId, fullInfoUpdate) : global;
}
export function replaceSimilarChannels<T extends GlobalState>(
global: T,
chatId: string,

View File

@ -1,8 +1,8 @@
import type {
ApiChat, ApiChatFullInfo, ApiChatType,
} from '../../api/types';
import type { ChatListType } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import {
type ApiChat, type ApiChatFullInfo, type ApiChatType,
} from '../../api/types';
import {
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID,
@ -12,12 +12,14 @@ import { isUserId } from '../../util/entities/ids';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import {
getHasAdminRight,
isChatAdmin,
isChatChannel,
isChatPublic,
isChatSuperGroup,
isHistoryClearMessage,
isUserBot,
isUserOnline,
isUserRightBanned,
} from '../helpers';
import { selectActiveRestrictionReasons } from './messages';
import { selectTabState } from './tabs';
@ -378,3 +380,35 @@ export function selectAreFoldersPresent<T extends GlobalState>(global: T) {
const ids = global.chatFolders.orderedIds;
return Boolean(ids && ids.length > 1);
}
export function selectCanEditRank<T extends GlobalState>(global: T, {
chatId, userId, isAdmin, isOwner,
}: {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
}) {
const chat = selectChat(global, chatId);
if (!chat || chat.isNotJoined || chat.isRestricted) return false;
const isCurrentUserAdmin = isChatAdmin(chat);
const hasAdminRight = getHasAdminRight(chat, 'manageRanks');
if (!isCurrentUserAdmin) return userId === global.currentUserId && !isUserRightBanned(chat, 'editRank'); // Users can edit only their own rank
if (!hasAdminRight) return false;
if (userId === global.currentUserId) return true; // Admin can edit own rank with permission
if (!chat.isCreator && (isOwner || isAdmin)) return false; // Admin can't edit rank of owner or another admin
return true;
}
export function selectCanEditOwnRank<T extends GlobalState>(global: T, chatId: string) {
const chat = selectChat(global, chatId);
if (!chat || chat.isNotJoined || chat.isRestricted) return false;
const isCurrentUserAdmin = isChatAdmin(chat);
if (!isCurrentUserAdmin) return !isUserRightBanned(chat, 'editRank'); // Users can edit only their own rank
return getHasAdminRight(chat, 'manageRanks');
}

View File

@ -182,8 +182,13 @@ export interface ActionPayloads {
chatId: string;
userId: string;
adminRights: ApiChatAdminRights;
customTitle?: string;
rank?: string;
} & WithTabId;
editChatParticipantRank: {
chatId: string;
userId: string;
rank: string;
};
checkChatInvite: {
hash: string;
@ -1938,6 +1943,22 @@ export interface ActionPayloads {
level: number;
} & WithTabId;
closeProfileRatingModal: WithTabId | undefined;
openRankModal: {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
rank?: string;
} & WithTabId;
closeRankModal: WithTabId | undefined;
openEditRankModal: {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
rank?: string;
} & WithTabId;
closeEditRankModal: WithTabId | undefined;
loadMoreProfilePhotos: {
peerId: string;
isPreload?: boolean;

View File

@ -1035,5 +1035,19 @@ export type TabState = {
isCocoonModalOpen?: boolean;
rankModal?: {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
rank?: string;
};
editRankModal?: {
chatId: string;
userId: string;
isAdmin?: boolean;
isOwner?: boolean;
rank?: string;
};
shouldOpenMessageMediaEditor?: boolean;
};

View File

@ -1794,6 +1794,7 @@ messages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.Af
messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities;
messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates;
messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User;
messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;

View File

@ -248,6 +248,7 @@
"messages.summarizeText",
"messages.editChatCreator",
"messages.getFutureChatCreatorAfterLeave",
"messages.editChatParticipantRank",
"updates.getState",
"updates.getDifference",
"updates.getChannelDifference",

File diff suppressed because it is too large Load Diff

View File

@ -16,326 +16,327 @@
}
$icons-map: (
"zoom-out": "\f101",
"zoom-in": "\f102",
"word-wrap": "\f103",
"webapp": "\f104",
"web": "\f105",
"warning": "\f106",
"volume-3": "\f107",
"volume-2": "\f108",
"volume-1": "\f109",
"voice-chat": "\f10a",
"view-once": "\f10b",
"video": "\f10c",
"video-stop": "\f10d",
"video-outlined": "\f10e",
"user": "\f10f",
"user-stars": "\f110",
"user-online": "\f111",
"user-filled": "\f112",
"up": "\f113",
"unread": "\f114",
"unpin": "\f115",
"unmute": "\f116",
"unlock": "\f117",
"unlock-badge": "\f118",
"unlist": "\f119",
"unlist-outline": "\f11a",
"unique-profile": "\f11b",
"undo": "\f11c",
"understood": "\f11d",
"underlined": "\f11e",
"unarchive": "\f11f",
"truck": "\f120",
"transcribe": "\f121",
"trade": "\f122",
"topic-new": "\f123",
"tools": "\f124",
"toncoin": "\f125",
"timer": "\f126",
"tag": "\f127",
"tag-name": "\f128",
"tag-filter": "\f129",
"tag-crossed": "\f12a",
"tag-add": "\f12b",
"strikethrough": "\f12c",
"story-reply": "\f12d",
"story-priority": "\f12e",
"story-expired": "\f12f",
"story-caption": "\f130",
"stop": "\f131",
"stop-raising-hand": "\f132",
"stickers": "\f133",
"stealth-past": "\f134",
"stealth-future": "\f135",
"stats": "\f136",
"stars-refund": "\f137",
"stars-lock": "\f138",
"star": "\f139",
"sport": "\f13a",
"spoiler": "\f13b",
"spoiler-disable": "\f13c",
"speaker": "\f13d",
"speaker-story": "\f13e",
"speaker-outline": "\f13f",
"speaker-muted-story": "\f140",
"sort": "\f141",
"sort-by-price": "\f142",
"sort-by-number": "\f143",
"sort-by-date": "\f144",
"smile": "\f145",
"smallscreen": "\f146",
"skip-previous": "\f147",
"skip-next": "\f148",
"sidebar": "\f149",
"show-message": "\f14a",
"share-screen": "\f14b",
"share-screen-stop": "\f14c",
"share-screen-outlined": "\f14d",
"share-filled": "\f14e",
"settings": "\f14f",
"settings-filled": "\f150",
"send": "\f151",
"send-outline": "\f152",
"sell": "\f153",
"sell-outline": "\f154",
"select": "\f155",
"search": "\f156",
"sd-photo": "\f157",
"scheduled": "\f158",
"schedule": "\f159",
"saved-messages": "\f15a",
"save-story": "\f15b",
"rotate": "\f15c",
"revote": "\f15d",
"revenue-split": "\f15e",
"reply": "\f15f",
"reply-filled": "\f160",
"replies": "\f161",
"replace": "\f162",
"reorder-tabs": "\f163",
"reopen-topic": "\f164",
"remove": "\f165",
"remove-quote": "\f166",
"reload": "\f167",
"refund": "\f168",
"redo": "\f169",
"recent": "\f16a",
"readchats": "\f16b",
"radial-badge": "\f16c",
"quote": "\f16d",
"quote-text": "\f16e",
"proof-of-ownership": "\f16f",
"privacy-policy": "\f170",
"previous": "\f171",
"poll": "\f172",
"play": "\f173",
"play-story": "\f174",
"pip": "\f175",
"pinned-message": "\f176",
"pinned-chat": "\f177",
"pin": "\f178",
"pin-list": "\f179",
"pin-badge": "\f17a",
"photo": "\f17b",
"phone": "\f17c",
"phone-discard": "\f17d",
"phone-discard-outline": "\f17e",
"permissions": "\f17f",
"pause": "\f180",
"password-off": "\f181",
"open-in-new-tab": "\f182",
"one-filled": "\f183",
"note": "\f184",
"non-contacts": "\f185",
"noise-suppression": "\f186",
"nochannel": "\f187",
"next": "\f188",
"next-link": "\f189",
"new-chat-filled": "\f18a",
"my-notes": "\f18b",
"muted": "\f18c",
"mute": "\f18d",
"move-caption-up": "\f18e",
"move-caption-down": "\f18f",
"more": "\f190",
"more-circle": "\f191",
"monospace": "\f192",
"microphone": "\f193",
"microphone-alt": "\f194",
"message": "\f195",
"message-succeeded": "\f196",
"message-read": "\f197",
"active-sessions": "\f101",
"add-caption": "\f102",
"add-filled": "\f103",
"add-one-badge": "\f104",
"add-user-filled": "\f105",
"add-user": "\f106",
"add": "\f107",
"admin": "\f108",
"allow-speak": "\f109",
"animals": "\f10a",
"animations": "\f10b",
"archive-filled": "\f10c",
"archive-from-main": "\f10d",
"archive-to-main": "\f10e",
"archive": "\f10f",
"arrow-down-circle": "\f110",
"arrow-down": "\f111",
"arrow-left": "\f112",
"arrow-right": "\f113",
"ask-support": "\f114",
"attach": "\f115",
"auction-drop": "\f116",
"auction-filled": "\f117",
"auction-next-round": "\f118",
"auction": "\f119",
"author-hidden": "\f11a",
"avatar-archived-chats": "\f11b",
"avatar-deleted-account": "\f11c",
"avatar-saved-messages": "\f11d",
"bold": "\f11e",
"boost-craft-chance": "\f11f",
"boost-outline": "\f120",
"boost": "\f121",
"boostcircle": "\f122",
"boosts": "\f123",
"bot-command": "\f124",
"bot-commands-filled": "\f125",
"bots": "\f126",
"brush": "\f127",
"bug": "\f128",
"calendar-filter": "\f129",
"calendar": "\f12a",
"camera-add": "\f12b",
"camera": "\f12c",
"car": "\f12d",
"card": "\f12e",
"cash-circle": "\f12f",
"channel-filled": "\f130",
"channel": "\f131",
"channelviews": "\f132",
"chat-badge": "\f133",
"chats-badge": "\f134",
"check": "\f135",
"clock-edit": "\f136",
"clock": "\f137",
"close-circle": "\f138",
"close-topic": "\f139",
"close": "\f13a",
"closed-gift": "\f13b",
"cloud-download": "\f13c",
"collapse-modal": "\f13d",
"collapse": "\f13e",
"colorize": "\f13f",
"combine-craft": "\f140",
"comments-sticker": "\f141",
"comments": "\f142",
"copy-media": "\f143",
"copy": "\f144",
"craft": "\f145",
"crop": "\f146",
"crown-take-off-outline": "\f147",
"crown-take-off": "\f148",
"crown-wear-outline": "\f149",
"crown-wear": "\f14a",
"darkmode": "\f14b",
"data": "\f14c",
"delete-filled": "\f14d",
"delete-left": "\f14e",
"delete-user": "\f14f",
"delete": "\f150",
"diamond": "\f151",
"document": "\f152",
"double-badge": "\f153",
"down": "\f154",
"download": "\f155",
"dropdown-arrows": "\f156",
"eats": "\f157",
"edit": "\f158",
"email": "\f159",
"enter": "\f15a",
"expand-modal": "\f15b",
"expand": "\f15c",
"eye-crossed-outline": "\f15d",
"eye-crossed": "\f15e",
"eye-outline": "\f15f",
"eye": "\f160",
"favorite-filled": "\f161",
"favorite": "\f162",
"file-badge": "\f163",
"flag": "\f164",
"flip": "\f165",
"folder-badge": "\f166",
"folder-tabs-bot": "\f167",
"folder-tabs-channel": "\f168",
"folder-tabs-chat": "\f169",
"folder-tabs-chats": "\f16a",
"folder-tabs-folder": "\f16b",
"folder-tabs-group": "\f16c",
"folder-tabs-star": "\f16d",
"folder-tabs-user": "\f16e",
"folder": "\f16f",
"fontsize": "\f170",
"forums": "\f171",
"forward": "\f172",
"fragment": "\f173",
"frozen-time": "\f174",
"fullscreen": "\f175",
"gifs": "\f176",
"gift-transfer-inline": "\f177",
"gift": "\f178",
"group-filled": "\f179",
"group": "\f17a",
"grouped-disable": "\f17b",
"grouped": "\f17c",
"hand-stop": "\f17d",
"hashtag": "\f17e",
"hd-photo": "\f17f",
"heart-outline": "\f180",
"heart": "\f181",
"help": "\f182",
"info-filled": "\f183",
"info": "\f184",
"install": "\f185",
"italic": "\f186",
"key": "\f187",
"keyboard": "\f188",
"lamp": "\f189",
"language": "\f18a",
"large-pause": "\f18b",
"large-play": "\f18c",
"link-badge": "\f18d",
"link-broken": "\f18e",
"link": "\f18f",
"location": "\f190",
"lock-badge": "\f191",
"lock": "\f192",
"logout": "\f193",
"loop": "\f194",
"mention": "\f195",
"menu": "\f196",
"message-failed": "\f197",
"message-pending": "\f198",
"message-failed": "\f199",
"menu": "\f19a",
"mention": "\f19b",
"loop": "\f19c",
"logout": "\f19d",
"lock": "\f19e",
"lock-badge": "\f19f",
"location": "\f1a0",
"link": "\f1a1",
"link-broken": "\f1a2",
"link-badge": "\f1a3",
"large-play": "\f1a4",
"large-pause": "\f1a5",
"language": "\f1a6",
"lamp": "\f1a7",
"keyboard": "\f1a8",
"key": "\f1a9",
"italic": "\f1aa",
"install": "\f1ab",
"info": "\f1ac",
"info-filled": "\f1ad",
"help": "\f1ae",
"heart": "\f1af",
"heart-outline": "\f1b0",
"hd-photo": "\f1b1",
"hashtag": "\f1b2",
"hand-stop": "\f1b3",
"grouped": "\f1b4",
"grouped-disable": "\f1b5",
"group": "\f1b6",
"group-filled": "\f1b7",
"gift": "\f1b8",
"gift-transfer-inline": "\f1b9",
"gifs": "\f1ba",
"fullscreen": "\f1bb",
"frozen-time": "\f1bc",
"fragment": "\f1bd",
"forward": "\f1be",
"forums": "\f1bf",
"fontsize": "\f1c0",
"folder": "\f1c1",
"folder-badge": "\f1c2",
"flip": "\f1c3",
"flag": "\f1c4",
"file-badge": "\f1c5",
"favorite": "\f1c6",
"favorite-filled": "\f1c7",
"eye": "\f1c8",
"eye-outline": "\f1c9",
"eye-crossed": "\f1ca",
"eye-crossed-outline": "\f1cb",
"expand": "\f1cc",
"expand-modal": "\f1cd",
"enter": "\f1ce",
"email": "\f1cf",
"edit": "\f1d0",
"eats": "\f1d1",
"dropdown-arrows": "\f1d2",
"download": "\f1d3",
"down": "\f1d4",
"double-badge": "\f1d5",
"document": "\f1d6",
"diamond": "\f1d7",
"delete": "\f1d8",
"delete-user": "\f1d9",
"delete-left": "\f1da",
"delete-filled": "\f1db",
"data": "\f1dc",
"darkmode": "\f1dd",
"crown-wear": "\f1de",
"crown-wear-outline": "\f1df",
"crown-take-off": "\f1e0",
"crown-take-off-outline": "\f1e1",
"crop": "\f1e2",
"craft": "\f1e3",
"copy": "\f1e4",
"copy-media": "\f1e5",
"comments": "\f1e6",
"comments-sticker": "\f1e7",
"combine-craft": "\f1e8",
"colorize": "\f1e9",
"collapse": "\f1ea",
"collapse-modal": "\f1eb",
"cloud-download": "\f1ec",
"closed-gift": "\f1ed",
"close": "\f1ee",
"close-topic": "\f1ef",
"close-circle": "\f1f0",
"clock": "\f1f1",
"clock-edit": "\f1f2",
"check": "\f1f3",
"chats-badge": "\f1f4",
"chat-badge": "\f1f5",
"channelviews": "\f1f6",
"channel": "\f1f7",
"channel-filled": "\f1f8",
"cash-circle": "\f1f9",
"card": "\f1fa",
"car": "\f1fb",
"camera": "\f1fc",
"camera-add": "\f1fd",
"calendar": "\f1fe",
"calendar-filter": "\f1ff",
"bug": "\f200",
"brush": "\f201",
"bots": "\f202",
"bot-commands-filled": "\f203",
"bot-command": "\f204",
"boosts": "\f205",
"boostcircle": "\f206",
"boost": "\f207",
"boost-outline": "\f208",
"boost-craft-chance": "\f209",
"bold": "\f20a",
"avatar-saved-messages": "\f20b",
"avatar-deleted-account": "\f20c",
"avatar-archived-chats": "\f20d",
"author-hidden": "\f20e",
"auction": "\f20f",
"auction-next-round": "\f210",
"auction-filled": "\f211",
"auction-drop": "\f212",
"attach": "\f213",
"ask-support": "\f214",
"arrow-right": "\f215",
"arrow-left": "\f216",
"arrow-down": "\f217",
"arrow-down-circle": "\f218",
"archive": "\f219",
"archive-to-main": "\f21a",
"archive-from-main": "\f21b",
"archive-filled": "\f21c",
"animations": "\f21d",
"animals": "\f21e",
"allow-speak": "\f21f",
"admin": "\f220",
"add": "\f221",
"add-user": "\f222",
"add-user-filled": "\f223",
"add-one-badge": "\f224",
"add-filled": "\f225",
"add-caption": "\f226",
"active-sessions": "\f227",
"rating-icons-negative": "\f228",
"rating-icons-level90": "\f229",
"rating-icons-level9": "\f22a",
"rating-icons-level80": "\f22b",
"rating-icons-level8": "\f22c",
"rating-icons-level70": "\f22d",
"rating-icons-level7": "\f22e",
"rating-icons-level60": "\f22f",
"rating-icons-level6": "\f230",
"rating-icons-level50": "\f231",
"rating-icons-level5": "\f232",
"rating-icons-level40": "\f233",
"rating-icons-level4": "\f234",
"rating-icons-level30": "\f235",
"rating-icons-level3": "\f236",
"rating-icons-level20": "\f237",
"rating-icons-level2": "\f238",
"rating-icons-level10": "\f239",
"rating-icons-level1": "\f23a",
"folder-tabs-user": "\f23b",
"folder-tabs-star": "\f23c",
"folder-tabs-group": "\f23d",
"folder-tabs-folder": "\f23e",
"folder-tabs-chats": "\f23f",
"folder-tabs-chat": "\f240",
"folder-tabs-channel": "\f241",
"folder-tabs-bot": "\f242",
"message-read": "\f199",
"message-succeeded": "\f19a",
"message": "\f19b",
"microphone-alt": "\f19c",
"microphone": "\f19d",
"monospace": "\f19e",
"more-circle": "\f19f",
"more": "\f1a0",
"move-caption-down": "\f1a1",
"move-caption-up": "\f1a2",
"mute": "\f1a3",
"muted": "\f1a4",
"my-notes": "\f1a5",
"new-chat-filled": "\f1a6",
"next-link": "\f1a7",
"next": "\f1a8",
"nochannel": "\f1a9",
"noise-suppression": "\f1aa",
"non-contacts": "\f1ab",
"note": "\f1ac",
"one-filled": "\f1ad",
"open-in-new-tab": "\f1ae",
"password-off": "\f1af",
"pause": "\f1b0",
"permissions": "\f1b1",
"phone-discard-outline": "\f1b2",
"phone-discard": "\f1b3",
"phone": "\f1b4",
"photo": "\f1b5",
"pin-badge": "\f1b6",
"pin-list": "\f1b7",
"pin": "\f1b8",
"pinned-chat": "\f1b9",
"pinned-message": "\f1ba",
"pip": "\f1bb",
"play-story": "\f1bc",
"play": "\f1bd",
"poll": "\f1be",
"previous": "\f1bf",
"privacy-policy": "\f1c0",
"proof-of-ownership": "\f1c1",
"quote-text": "\f1c2",
"quote": "\f1c3",
"radial-badge": "\f1c4",
"rating-icons-level1": "\f1c5",
"rating-icons-level10": "\f1c6",
"rating-icons-level2": "\f1c7",
"rating-icons-level20": "\f1c8",
"rating-icons-level3": "\f1c9",
"rating-icons-level30": "\f1ca",
"rating-icons-level4": "\f1cb",
"rating-icons-level40": "\f1cc",
"rating-icons-level5": "\f1cd",
"rating-icons-level50": "\f1ce",
"rating-icons-level6": "\f1cf",
"rating-icons-level60": "\f1d0",
"rating-icons-level7": "\f1d1",
"rating-icons-level70": "\f1d2",
"rating-icons-level8": "\f1d3",
"rating-icons-level80": "\f1d4",
"rating-icons-level9": "\f1d5",
"rating-icons-level90": "\f1d6",
"rating-icons-negative": "\f1d7",
"readchats": "\f1d8",
"recent": "\f1d9",
"redo": "\f1da",
"refund": "\f1db",
"reload": "\f1dc",
"remove-quote": "\f1dd",
"remove": "\f1de",
"reopen-topic": "\f1df",
"reorder-tabs": "\f1e0",
"replace": "\f1e1",
"replies": "\f1e2",
"reply-filled": "\f1e3",
"reply": "\f1e4",
"revenue-split": "\f1e5",
"revote": "\f1e6",
"rotate": "\f1e7",
"save-story": "\f1e8",
"saved-messages": "\f1e9",
"schedule": "\f1ea",
"scheduled": "\f1eb",
"sd-photo": "\f1ec",
"search": "\f1ed",
"select": "\f1ee",
"sell-outline": "\f1ef",
"sell": "\f1f0",
"send-outline": "\f1f1",
"send": "\f1f2",
"settings-filled": "\f1f3",
"settings": "\f1f4",
"share-filled": "\f1f5",
"share-screen-outlined": "\f1f6",
"share-screen-stop": "\f1f7",
"share-screen": "\f1f8",
"show-message": "\f1f9",
"sidebar": "\f1fa",
"skip-next": "\f1fb",
"skip-previous": "\f1fc",
"smallscreen": "\f1fd",
"smile": "\f1fe",
"sort-by-date": "\f1ff",
"sort-by-number": "\f200",
"sort-by-price": "\f201",
"sort": "\f202",
"speaker-muted-story": "\f203",
"speaker-outline": "\f204",
"speaker-story": "\f205",
"speaker": "\f206",
"spoiler-disable": "\f207",
"spoiler": "\f208",
"sport": "\f209",
"star": "\f20a",
"stars-lock": "\f20b",
"stars-refund": "\f20c",
"stats": "\f20d",
"stealth-future": "\f20e",
"stealth-past": "\f20f",
"stickers": "\f210",
"stop-raising-hand": "\f211",
"stop": "\f212",
"story-caption": "\f213",
"story-expired": "\f214",
"story-priority": "\f215",
"story-reply": "\f216",
"strikethrough": "\f217",
"tag-add": "\f218",
"tag-crossed": "\f219",
"tag-filter": "\f21a",
"tag-name": "\f21b",
"tag": "\f21c",
"timer": "\f21d",
"toncoin": "\f21e",
"tools": "\f21f",
"topic-new": "\f220",
"trade": "\f221",
"transcribe": "\f222",
"truck": "\f223",
"unarchive": "\f224",
"underlined": "\f225",
"understood": "\f226",
"undo": "\f227",
"unique-profile": "\f228",
"unlist-outline": "\f229",
"unlist": "\f22a",
"unlock-badge": "\f22b",
"unlock": "\f22c",
"unmute": "\f22d",
"unpin": "\f22e",
"unread": "\f22f",
"up": "\f230",
"user-filled": "\f231",
"user-online": "\f232",
"user-stars": "\f233",
"user-tag": "\f234",
"user": "\f235",
"video-outlined": "\f236",
"video-stop": "\f237",
"video": "\f238",
"view-once": "\f239",
"voice-chat": "\f23a",
"volume-1": "\f23b",
"volume-2": "\f23c",
"volume-3": "\f23d",
"warning": "\f23e",
"web": "\f23f",
"webapp": "\f240",
"word-wrap": "\f241",
"zoom-in": "\f242",
"zoom-out": "\f243",
);

Binary file not shown.

Binary file not shown.

View File

@ -1,323 +1,324 @@
export type FontIconName =
| 'zoom-out'
| 'zoom-in'
| 'word-wrap'
| 'webapp'
| 'web'
| 'warning'
| 'volume-3'
| 'volume-2'
| 'volume-1'
| 'voice-chat'
| 'view-once'
| 'video'
| 'video-stop'
| 'video-outlined'
| 'user'
| 'user-stars'
| 'user-online'
| 'user-filled'
| 'up'
| 'unread'
| 'unpin'
| 'unmute'
| 'unlock'
| 'unlock-badge'
| 'unlist'
| 'unlist-outline'
| 'unique-profile'
| 'undo'
| 'understood'
| 'underlined'
| 'unarchive'
| 'truck'
| 'transcribe'
| 'trade'
| 'topic-new'
| 'tools'
| 'toncoin'
| 'timer'
| 'tag'
| 'tag-name'
| 'tag-filter'
| 'tag-crossed'
| 'tag-add'
| 'strikethrough'
| 'story-reply'
| 'story-priority'
| 'story-expired'
| 'story-caption'
| 'stop'
| 'stop-raising-hand'
| 'stickers'
| 'stealth-past'
| 'stealth-future'
| 'stats'
| 'stars-refund'
| 'stars-lock'
| 'star'
| 'sport'
| 'spoiler'
| 'spoiler-disable'
| 'speaker'
| 'speaker-story'
| 'speaker-outline'
| 'speaker-muted-story'
| 'sort'
| 'sort-by-price'
| 'sort-by-number'
| 'sort-by-date'
| 'smile'
| 'smallscreen'
| 'skip-previous'
| 'skip-next'
| 'sidebar'
| 'show-message'
| 'share-screen'
| 'share-screen-stop'
| 'share-screen-outlined'
| 'share-filled'
| 'settings'
| 'settings-filled'
| 'send'
| 'send-outline'
| 'sell'
| 'sell-outline'
| 'select'
| 'search'
| 'sd-photo'
| 'scheduled'
| 'schedule'
| 'saved-messages'
| 'save-story'
| 'rotate'
| 'revote'
| 'revenue-split'
| 'reply'
| 'reply-filled'
| 'replies'
| 'replace'
| 'reorder-tabs'
| 'reopen-topic'
| 'remove'
| 'remove-quote'
| 'reload'
| 'refund'
| 'redo'
| 'recent'
| 'readchats'
| 'radial-badge'
| 'quote'
| 'quote-text'
| 'proof-of-ownership'
| 'privacy-policy'
| 'previous'
| 'poll'
| 'play'
| 'play-story'
| 'pip'
| 'pinned-message'
| 'pinned-chat'
| 'pin'
| 'pin-list'
| 'pin-badge'
| 'photo'
| 'phone'
| 'phone-discard'
| 'phone-discard-outline'
| 'permissions'
| 'pause'
| 'password-off'
| 'open-in-new-tab'
| 'one-filled'
| 'note'
| 'non-contacts'
| 'noise-suppression'
| 'nochannel'
| 'next'
| 'next-link'
| 'new-chat-filled'
| 'my-notes'
| 'muted'
| 'mute'
| 'move-caption-up'
| 'move-caption-down'
| 'more'
| 'more-circle'
| 'monospace'
| 'microphone'
| 'microphone-alt'
| 'message'
| 'message-succeeded'
| 'message-read'
| 'message-pending'
| 'message-failed'
| 'menu'
| 'mention'
| 'loop'
| 'logout'
| 'lock'
| 'lock-badge'
| 'location'
| 'link'
| 'link-broken'
| 'link-badge'
| 'large-play'
| 'large-pause'
| 'language'
| 'lamp'
| 'keyboard'
| 'key'
| 'italic'
| 'install'
| 'info'
| 'info-filled'
| 'help'
| 'heart'
| 'heart-outline'
| 'hd-photo'
| 'hashtag'
| 'hand-stop'
| 'grouped'
| 'grouped-disable'
| 'group'
| 'group-filled'
| 'gift'
| 'gift-transfer-inline'
| 'gifs'
| 'fullscreen'
| 'frozen-time'
| 'fragment'
| 'forward'
| 'forums'
| 'fontsize'
| 'folder'
| 'folder-badge'
| 'flip'
| 'flag'
| 'file-badge'
| 'favorite'
| 'favorite-filled'
| 'eye'
| 'eye-outline'
| 'eye-crossed'
| 'eye-crossed-outline'
| 'expand'
| 'expand-modal'
| 'enter'
| 'email'
| 'edit'
| 'eats'
| 'dropdown-arrows'
| 'download'
| 'down'
| 'double-badge'
| 'document'
| 'diamond'
| 'delete'
| 'delete-user'
| 'delete-left'
| 'delete-filled'
| 'data'
| 'darkmode'
| 'crown-wear'
| 'crown-wear-outline'
| 'crown-take-off'
| 'crown-take-off-outline'
| 'crop'
| 'craft'
| 'copy'
| 'copy-media'
| 'comments'
| 'comments-sticker'
| 'combine-craft'
| 'colorize'
| 'collapse'
| 'collapse-modal'
| 'cloud-download'
| 'closed-gift'
| 'close'
| 'close-topic'
| 'close-circle'
| 'clock'
| 'clock-edit'
| 'check'
| 'chats-badge'
| 'chat-badge'
| 'channelviews'
| 'channel'
| 'channel-filled'
| 'cash-circle'
| 'card'
| 'car'
| 'camera'
| 'camera-add'
| 'calendar'
| 'calendar-filter'
| 'bug'
| 'brush'
| 'bots'
| 'bot-commands-filled'
| 'bot-command'
| 'boosts'
| 'boostcircle'
| 'boost'
| 'boost-outline'
| 'boost-craft-chance'
| 'bold'
| 'avatar-saved-messages'
| 'avatar-deleted-account'
| 'avatar-archived-chats'
| 'author-hidden'
| 'auction'
| 'auction-next-round'
| 'auction-filled'
| 'auction-drop'
| 'attach'
| 'ask-support'
| 'arrow-right'
| 'arrow-left'
| 'arrow-down'
| 'arrow-down-circle'
| 'archive'
| 'archive-to-main'
| 'archive-from-main'
| 'archive-filled'
| 'animations'
| 'animals'
| 'allow-speak'
| 'admin'
| 'add'
| 'add-user'
| 'add-user-filled'
| 'add-one-badge'
| 'add-filled'
| 'add-caption'
| 'active-sessions'
| 'rating-icons-negative'
| 'rating-icons-level90'
| 'rating-icons-level9'
| 'rating-icons-level80'
| 'rating-icons-level8'
| 'rating-icons-level70'
| 'rating-icons-level7'
| 'rating-icons-level60'
| 'rating-icons-level6'
| 'rating-icons-level50'
| 'rating-icons-level5'
| 'rating-icons-level40'
| 'rating-icons-level4'
| 'rating-icons-level30'
| 'rating-icons-level3'
| 'rating-icons-level20'
| 'rating-icons-level2'
| 'rating-icons-level10'
| 'rating-icons-level1'
| 'folder-tabs-user'
| 'folder-tabs-star'
| 'folder-tabs-group'
| 'folder-tabs-folder'
| 'folder-tabs-chats'
| 'folder-tabs-chat'
| 'add-caption'
| 'add-filled'
| 'add-one-badge'
| 'add-user-filled'
| 'add-user'
| 'add'
| 'admin'
| 'allow-speak'
| 'animals'
| 'animations'
| 'archive-filled'
| 'archive-from-main'
| 'archive-to-main'
| 'archive'
| 'arrow-down-circle'
| 'arrow-down'
| 'arrow-left'
| 'arrow-right'
| 'ask-support'
| 'attach'
| 'auction-drop'
| 'auction-filled'
| 'auction-next-round'
| 'auction'
| 'author-hidden'
| 'avatar-archived-chats'
| 'avatar-deleted-account'
| 'avatar-saved-messages'
| 'bold'
| 'boost-craft-chance'
| 'boost-outline'
| 'boost'
| 'boostcircle'
| 'boosts'
| 'bot-command'
| 'bot-commands-filled'
| 'bots'
| 'brush'
| 'bug'
| 'calendar-filter'
| 'calendar'
| 'camera-add'
| 'camera'
| 'car'
| 'card'
| 'cash-circle'
| 'channel-filled'
| 'channel'
| 'channelviews'
| 'chat-badge'
| 'chats-badge'
| 'check'
| 'clock-edit'
| 'clock'
| 'close-circle'
| 'close-topic'
| 'close'
| 'closed-gift'
| 'cloud-download'
| 'collapse-modal'
| 'collapse'
| 'colorize'
| 'combine-craft'
| 'comments-sticker'
| 'comments'
| 'copy-media'
| 'copy'
| 'craft'
| 'crop'
| 'crown-take-off-outline'
| 'crown-take-off'
| 'crown-wear-outline'
| 'crown-wear'
| 'darkmode'
| 'data'
| 'delete-filled'
| 'delete-left'
| 'delete-user'
| 'delete'
| 'diamond'
| 'document'
| 'double-badge'
| 'down'
| 'download'
| 'dropdown-arrows'
| 'eats'
| 'edit'
| 'email'
| 'enter'
| 'expand-modal'
| 'expand'
| 'eye-crossed-outline'
| 'eye-crossed'
| 'eye-outline'
| 'eye'
| 'favorite-filled'
| 'favorite'
| 'file-badge'
| 'flag'
| 'flip'
| 'folder-badge'
| 'folder-tabs-bot'
| 'folder-tabs-channel'
| 'folder-tabs-bot';
| 'folder-tabs-chat'
| 'folder-tabs-chats'
| 'folder-tabs-folder'
| 'folder-tabs-group'
| 'folder-tabs-star'
| 'folder-tabs-user'
| 'folder'
| 'fontsize'
| 'forums'
| 'forward'
| 'fragment'
| 'frozen-time'
| 'fullscreen'
| 'gifs'
| 'gift-transfer-inline'
| 'gift'
| 'group-filled'
| 'group'
| 'grouped-disable'
| 'grouped'
| 'hand-stop'
| 'hashtag'
| 'hd-photo'
| 'heart-outline'
| 'heart'
| 'help'
| 'info-filled'
| 'info'
| 'install'
| 'italic'
| 'key'
| 'keyboard'
| 'lamp'
| 'language'
| 'large-pause'
| 'large-play'
| 'link-badge'
| 'link-broken'
| 'link'
| 'location'
| 'lock-badge'
| 'lock'
| 'logout'
| 'loop'
| 'mention'
| 'menu'
| 'message-failed'
| 'message-pending'
| 'message-read'
| 'message-succeeded'
| 'message'
| 'microphone-alt'
| 'microphone'
| 'monospace'
| 'more-circle'
| 'more'
| 'move-caption-down'
| 'move-caption-up'
| 'mute'
| 'muted'
| 'my-notes'
| 'new-chat-filled'
| 'next-link'
| 'next'
| 'nochannel'
| 'noise-suppression'
| 'non-contacts'
| 'note'
| 'one-filled'
| 'open-in-new-tab'
| 'password-off'
| 'pause'
| 'permissions'
| 'phone-discard-outline'
| 'phone-discard'
| 'phone'
| 'photo'
| 'pin-badge'
| 'pin-list'
| 'pin'
| 'pinned-chat'
| 'pinned-message'
| 'pip'
| 'play-story'
| 'play'
| 'poll'
| 'previous'
| 'privacy-policy'
| 'proof-of-ownership'
| 'quote-text'
| 'quote'
| 'radial-badge'
| 'rating-icons-level1'
| 'rating-icons-level10'
| 'rating-icons-level2'
| 'rating-icons-level20'
| 'rating-icons-level3'
| 'rating-icons-level30'
| 'rating-icons-level4'
| 'rating-icons-level40'
| 'rating-icons-level5'
| 'rating-icons-level50'
| 'rating-icons-level6'
| 'rating-icons-level60'
| 'rating-icons-level7'
| 'rating-icons-level70'
| 'rating-icons-level8'
| 'rating-icons-level80'
| 'rating-icons-level9'
| 'rating-icons-level90'
| 'rating-icons-negative'
| 'readchats'
| 'recent'
| 'redo'
| 'refund'
| 'reload'
| 'remove-quote'
| 'remove'
| 'reopen-topic'
| 'reorder-tabs'
| 'replace'
| 'replies'
| 'reply-filled'
| 'reply'
| 'revenue-split'
| 'revote'
| 'rotate'
| 'save-story'
| 'saved-messages'
| 'schedule'
| 'scheduled'
| 'sd-photo'
| 'search'
| 'select'
| 'sell-outline'
| 'sell'
| 'send-outline'
| 'send'
| 'settings-filled'
| 'settings'
| 'share-filled'
| 'share-screen-outlined'
| 'share-screen-stop'
| 'share-screen'
| 'show-message'
| 'sidebar'
| 'skip-next'
| 'skip-previous'
| 'smallscreen'
| 'smile'
| 'sort-by-date'
| 'sort-by-number'
| 'sort-by-price'
| 'sort'
| 'speaker-muted-story'
| 'speaker-outline'
| 'speaker-story'
| 'speaker'
| 'spoiler-disable'
| 'spoiler'
| 'sport'
| 'star'
| 'stars-lock'
| 'stars-refund'
| 'stats'
| 'stealth-future'
| 'stealth-past'
| 'stickers'
| 'stop-raising-hand'
| 'stop'
| 'story-caption'
| 'story-expired'
| 'story-priority'
| 'story-reply'
| 'strikethrough'
| 'tag-add'
| 'tag-crossed'
| 'tag-filter'
| 'tag-name'
| 'tag'
| 'timer'
| 'toncoin'
| 'tools'
| 'topic-new'
| 'trade'
| 'transcribe'
| 'truck'
| 'unarchive'
| 'underlined'
| 'understood'
| 'undo'
| 'unique-profile'
| 'unlist-outline'
| 'unlist'
| 'unlock-badge'
| 'unlock'
| 'unmute'
| 'unpin'
| 'unread'
| 'up'
| 'user-filled'
| 'user-online'
| 'user-stars'
| 'user-tag'
| 'user'
| 'video-outlined'
| 'video-stop'
| 'video'
| 'view-once'
| 'voice-chat'
| 'volume-1'
| 'volume-2'
| 'volume-3'
| 'warning'
| 'web'
| 'webapp'
| 'word-wrap'
| 'zoom-in'
| 'zoom-out';

View File

@ -112,6 +112,7 @@ export interface LangPair {
'UserRestrictionsNoChangeInfo': undefined;
'UserRestrictionsInviteUsers': undefined;
'UserRestrictionsPinMessages': undefined;
'UserRestrictionsEditRank': undefined;
'ChatPermissionNotAvailable': undefined;
'StatsMessageInteractionsTitle': undefined;
'StatsGroupGrowthTitle': undefined;
@ -490,6 +491,7 @@ export interface LangPair {
'PrivacyExceptions': undefined;
'AlwaysAllow': undefined;
'EditAdminAddUsers': undefined;
'EditAdminEditRank': undefined;
'NeverAllow': undefined;
'AlwaysAllowPlaceholder': undefined;
'NeverAllowPlaceholder': undefined;
@ -2004,6 +2006,17 @@ export interface LangPair {
'GiftPreviewToggleRegularModels': undefined;
'AriaGiftPreviewPlay': undefined;
'AriaGiftPreviewStop': undefined;
'RankModalEdit': undefined;
'RankModalEditMy': undefined;
'MemberContextEditRank': undefined;
'RankMemberTag': undefined;
'RankAdminTag': undefined;
'RankOwnerTag': undefined;
'RankModalMemberTagTitle': undefined;
'RankModalAdminTagTitle': undefined;
'RankModalOwnerTagTitle': undefined;
'RankEditSave': undefined;
'RankEditTextOwn': undefined;
'MenuAddCaption': undefined;
}
@ -3519,6 +3532,24 @@ export interface LangPairWithVariables<V = LangVariable> {
'BotAuthSuccessText': {
'url': V;
};
'RankModalMemberText': {
'tag': V;
'author': V;
'group': V;
};
'RankModalAdminText': {
'tag': V;
'author': V;
'group': V;
};
'RankModalOwnerText': {
'tag': V;
'author': V;
'group': V;
};
'RankEditText': {
'user': V;
};
}
export interface LangPairPlural {